mirror of
https://github.com/Nexoniarz/YAPF.git
synced 2026-11-03 13:04:26 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06a4e7db9e |
||
|
|
11ebb12270 |
||
|
|
0ba1f0af17 |
||
|
|
fdbd0a6b8f |
||
|
|
a6c032d824 |
||
|
|
83588d8e2a |
||
|
|
7b1af7f275 |
||
|
|
fb7212c62b |
||
|
|
6afd0488aa |
||
|
|
a226b06368 |
||
|
|
5642b8c85d |
46 changed files with 7789 additions and 414 deletions
143
README.md
143
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.
|
and natural textures — the price of having no entropy coder.
|
||||||
- **Made for games and tools too**: mipmaps, GPU format hint, sRGB /
|
- **Made for games and tools too**: mipmaps, GPU format hint, sRGB /
|
||||||
premultiplied flags, and random access to 64×64 tiles.
|
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,
|
- **Tiny and portable**: two C99 files, no dependencies. Plus a CLI, Python,
|
||||||
JavaScript and plugins for the tools you already use.
|
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,68 @@ yapf compare photo.png ui.png # size and decode speed vs the original fo
|
||||||
```
|
```
|
||||||
|
|
||||||
Options when writing `.yapf`: `--mips`, `--linear` (pixels are not sRGB),
|
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);
|
||||||
|
```
|
||||||
|
|
||||||
|
Every extension opens version 2 files: GIMP (at full 16-bit / float
|
||||||
|
precision, faces and frames as layers), Blender (HDR as float images),
|
||||||
|
KDE and Linux thumbnails, VS Code (with face / layer / frame / slice
|
||||||
|
pickers), Figma and Photoshop. The Python binding (`yapf.load_texture`)
|
||||||
|
and the JavaScript port (`YAPF.decodeTexture`) read every texture; the
|
||||||
|
other bindings wrap the v1 functions, which also read 8-bit version 2
|
||||||
|
files (2D, cube face 0, array layer 0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -198,6 +266,47 @@ void yapf_free(yapf_image_t *img);
|
||||||
```
|
```
|
||||||
Releases memory returned by the loaders. Passing `NULL` is safe.
|
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
|
### Return codes
|
||||||
|
|
||||||
| Constant | Value | Meaning |
|
| Constant | Value | Meaning |
|
||||||
|
|
@ -206,6 +315,10 @@ Releases memory returned by the loaders. Passing `NULL` is safe.
|
||||||
| `YAPF_ERR_INVALID` | –1 | NULL or logically invalid argument |
|
| `YAPF_ERR_INVALID` | –1 | NULL or logically invalid argument |
|
||||||
| `YAPF_ERR_IO` | –2 | File could not be opened or written |
|
| `YAPF_ERR_IO` | –2 | File could not be opened or written |
|
||||||
| `YAPF_ERR_OOM` | –3 | Out of memory |
|
| `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
|
### Channel constants
|
||||||
|
|
||||||
|
|
@ -223,6 +336,7 @@ YAPF_CHANNELS_RGBA 4
|
||||||
| System | Command | Output |
|
| System | Command | Output |
|
||||||
| :----- | :------ | :----- |
|
| :----- | :------ | :----- |
|
||||||
| Linux, macOS, BSD | `make` · `make test` · `sudo make install` | `build/`: `libyapf.a`, `libyapf.so` / `.dylib`, `yapf` |
|
| Linux, macOS, BSD | `make` · `make test` · `sudo make install` | `build/`: `libyapf.a`, `libyapf.so` / `.dylib`, `yapf` |
|
||||||
|
| NixOS / Nix | `nix-build` (or `nix-env -if .`) — see [extensions/linux](extensions/linux#nixos) | `result/`: `bin/yapf`, `lib/`, `include/`, MIME type, thumbnailer |
|
||||||
| Linux, macOS (LLVM) | `./build.sh` | `dist/<platform>/` incl. macOS universal binaries |
|
| Linux, macOS (LLVM) | `./build.sh` | `dist/<platform>/` incl. macOS universal binaries |
|
||||||
| Windows (LLVM) | `build.bat` | `dist\windows-x64\`, `dist\windows-arm64\`: `yapf.dll`, `yapf.lib`, `libyapf.a`, `yapf.exe` |
|
| Windows (LLVM) | `build.bat` | `dist\windows-x64\`, `dist\windows-arm64\`: `yapf.dll`, `yapf.lib`, `libyapf.a`, `yapf.exe` |
|
||||||
| Windows (Visual Studio) | `cl /O2 /c yapf.c` and add `yapf.obj` to your project | |
|
| Windows (Visual Studio) | `cl /O2 /c yapf.c` and add `yapf.obj` to your project | |
|
||||||
|
|
@ -256,7 +370,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,
|
7. **Independent tiles** — any tile can be decoded on its own, in any order,
|
||||||
on any thread.
|
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,
|
HEADER (20 bytes) "YAPF", version 1, channels, GPU hint, mip count,
|
||||||
|
|
@ -273,7 +388,7 @@ TILE (64×64) header byte: stored raw, or constant-plane mask + colour tr
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
```sh
|
```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
|
node bindings/js/test.js # JavaScript: round-trips, sample file, corrupt input
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -286,6 +401,16 @@ What has been verified:
|
||||||
C++, and under AddressSanitizer, UndefinedBehaviorSanitizer and
|
C++, and under AddressSanitizer, UndefinedBehaviorSanitizer and
|
||||||
ThreadSanitizer;
|
ThreadSanitizer;
|
||||||
- thousands of corrupted and truncated files are rejected without crashing;
|
- 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#,
|
- every language binding runs its example; JavaScript, Python, C++, C#,
|
||||||
Rust, Go, Java and Lua re-encode the sample to byte-identical output;
|
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;
|
- the GIMP plugin in GIMP 3.0.8 and the Blender extension in Blender 5.1;
|
||||||
|
|
@ -293,14 +418,20 @@ What has been verified:
|
||||||
|
|
||||||
**Not yet verified:** running on a Mac (macOS builds are cross-compiled
|
**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
|
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
|
## 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/yapf_cli.c the `yapf` command-line tool
|
||||||
tools/build_release.sh builds every download for all platforms
|
tools/build_release.sh builds every download for all platforms
|
||||||
tests/test_yapf.c C test suite
|
tests/test_yapf.c C test suite
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# yapf.js — YAPF in JavaScript
|
# yapf.js — YAPF in JavaScript
|
||||||
|
|
||||||
Encoder and decoder for YAPF v1 in one dependency-free file. Works in
|
Encoder (version 1) and decoder (versions 1 and 2) in one dependency-free file. Works in
|
||||||
browsers, Node.js, web workers, Figma plugins and VS Code webviews, and
|
browsers, Node.js, web workers, Figma plugins and VS Code webviews, and
|
||||||
produces exactly the same bytes as the C library.
|
produces exactly the same bytes as the C library.
|
||||||
|
|
||||||
|
|
@ -22,3 +22,22 @@ image decodes in about 150 ms in Node / Chrome (the C library: 6 ms).
|
||||||
TypeScript types are in `yapf.d.ts`.
|
TypeScript types are in `yapf.d.ts`.
|
||||||
|
|
||||||
Example: `node example.mjs ../../other/YAPF.YAPF` · tests: `node test.js`.
|
Example: `node example.mjs ../../other/YAPF.YAPF` · tests: `node test.js`.
|
||||||
|
|
||||||
|
## Textures (YAPF 2)
|
||||||
|
|
||||||
|
```js
|
||||||
|
const tex = YAPF.decodeTexture(bytes); // any v1 / v2 file
|
||||||
|
// tex: { width, height, depth, layers, frames, faces, shape, mipLevels,
|
||||||
|
// payload, format, sample, channels, flags, channelMap, normal,
|
||||||
|
// primaries, transfer, frameUs, notes, surfaces: [...] }
|
||||||
|
const s = YAPF.surfaceIndex(tex, frame, layer, face, mip);
|
||||||
|
const view = YAPF.textureToRGBA(tex, s); // { width, height, rgba } for a canvas
|
||||||
|
```
|
||||||
|
|
||||||
|
`surfaces[s]` is a `Uint8Array` (8-bit pixels or GPU blocks, ready for
|
||||||
|
WebGL / WebGPU compressed textures), `Uint16Array` (16-bit, half) or
|
||||||
|
`Float32Array`. Checksums are verified (`{ verify: false }` skips them);
|
||||||
|
errors are `YAPF.YapfError` with a `code` (`corrupt`, `checksum`,
|
||||||
|
`truncated`, `not-yapf`, `version`, `unsupported`). `decode()` also reads
|
||||||
|
8-bit version 2 files. The decoder is checked against the C library on the
|
||||||
|
files in `test-data/`.
|
||||||
|
|
|
||||||
BIN
bindings/js/test-data/v2-animation-notes.yapf
Normal file
BIN
bindings/js/test-data/v2-animation-notes.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-bc3-packed.yapf
Normal file
BIN
bindings/js/test-data/v2-bc3-packed.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-bc5-packed.yapf
Normal file
BIN
bindings/js/test-data/v2-bc5-packed.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-bc6h-encoded.yapf
Normal file
BIN
bindings/js/test-data/v2-bc6h-encoded.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-bc6h-random-blocks.yapf
Normal file
BIN
bindings/js/test-data/v2-bc6h-random-blocks.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-bc7-encoded.yapf
Normal file
BIN
bindings/js/test-data/v2-bc7-encoded.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-bc7-random-blocks.yapf
Normal file
BIN
bindings/js/test-data/v2-bc7-random-blocks.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-cube-mips.yapf
Normal file
BIN
bindings/js/test-data/v2-cube-mips.yapf
Normal file
Binary file not shown.
137
bindings/js/test-data/v2-expected.json
Normal file
137
bindings/js/test-data/v2-expected.json
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
{
|
||||||
|
"v2-rgba8.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
480599072
|
||||||
|
],
|
||||||
|
"preview0": 480599072
|
||||||
|
},
|
||||||
|
"v2-float-gray-entropy.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
1656156314
|
||||||
|
],
|
||||||
|
"preview0": 2474033295
|
||||||
|
},
|
||||||
|
"v2-half-gray-alpha-entropy.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
2599040047
|
||||||
|
],
|
||||||
|
"preview0": 297007736
|
||||||
|
},
|
||||||
|
"v2-rgba8-entropy.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
554695116
|
||||||
|
],
|
||||||
|
"preview0": 554695116
|
||||||
|
},
|
||||||
|
"v2-u16-rgba-entropy.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
3416063949
|
||||||
|
],
|
||||||
|
"preview0": 1518500476
|
||||||
|
},
|
||||||
|
"v2-cube-mips.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
3098789294,
|
||||||
|
2090642189,
|
||||||
|
3178169037,
|
||||||
|
3641419477,
|
||||||
|
333399741,
|
||||||
|
2333110333,
|
||||||
|
1703812753,
|
||||||
|
3098789294,
|
||||||
|
2090642189,
|
||||||
|
3178169037,
|
||||||
|
3641419477,
|
||||||
|
333399741,
|
||||||
|
2333110333,
|
||||||
|
1703812753,
|
||||||
|
3098789294,
|
||||||
|
2090642189,
|
||||||
|
3178169037,
|
||||||
|
3641419477,
|
||||||
|
333399741,
|
||||||
|
2333110333,
|
||||||
|
1703812753,
|
||||||
|
3098789294,
|
||||||
|
2090642189,
|
||||||
|
3178169037,
|
||||||
|
3641419477,
|
||||||
|
333399741,
|
||||||
|
2333110333,
|
||||||
|
1703812753,
|
||||||
|
3098789294,
|
||||||
|
2090642189,
|
||||||
|
3178169037,
|
||||||
|
3641419477,
|
||||||
|
333399741,
|
||||||
|
2333110333,
|
||||||
|
1703812753,
|
||||||
|
3098789294,
|
||||||
|
2090642189,
|
||||||
|
3178169037,
|
||||||
|
3641419477,
|
||||||
|
333399741,
|
||||||
|
2333110333,
|
||||||
|
1703812753
|
||||||
|
],
|
||||||
|
"preview0": 3098789294
|
||||||
|
},
|
||||||
|
"v2-u16-volume.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
1504297975,
|
||||||
|
2857113671,
|
||||||
|
3272367306,
|
||||||
|
4248642449
|
||||||
|
],
|
||||||
|
"preview0": 3611091147
|
||||||
|
},
|
||||||
|
"v2-animation-notes.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
2496952478,
|
||||||
|
2084186354,
|
||||||
|
2971951804,
|
||||||
|
4204487618
|
||||||
|
],
|
||||||
|
"preview0": 2496952478
|
||||||
|
},
|
||||||
|
"v2-bc6h-random-blocks.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
2051803746,
|
||||||
|
562394057,
|
||||||
|
2093502260
|
||||||
|
],
|
||||||
|
"preview0": 2386146506
|
||||||
|
},
|
||||||
|
"v2-bc7-random-blocks.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
3714498223,
|
||||||
|
242396130,
|
||||||
|
2835116443
|
||||||
|
],
|
||||||
|
"preview0": 4042788133
|
||||||
|
},
|
||||||
|
"v2-bc3-packed.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
1798067479
|
||||||
|
],
|
||||||
|
"preview0": 3027945807
|
||||||
|
},
|
||||||
|
"v2-bc5-packed.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
1681957115
|
||||||
|
],
|
||||||
|
"preview0": 1079658926
|
||||||
|
},
|
||||||
|
"v2-bc7-encoded.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
3415378546
|
||||||
|
],
|
||||||
|
"preview0": 851871445
|
||||||
|
},
|
||||||
|
"v2-bc6h-encoded.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
3372163902
|
||||||
|
],
|
||||||
|
"preview0": 89762387
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
bindings/js/test-data/v2-float-gray-entropy.yapf
Normal file
BIN
bindings/js/test-data/v2-float-gray-entropy.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-half-gray-alpha-entropy.yapf
Normal file
BIN
bindings/js/test-data/v2-half-gray-alpha-entropy.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-rgba8-entropy.yapf
Normal file
BIN
bindings/js/test-data/v2-rgba8-entropy.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-rgba8.yapf
Normal file
BIN
bindings/js/test-data/v2-rgba8.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-u16-rgba-entropy.yapf
Normal file
BIN
bindings/js/test-data/v2-u16-rgba-entropy.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-u16-volume.yapf
Normal file
BIN
bindings/js/test-data/v2-u16-volume.yapf
Normal file
Binary file not shown.
|
|
@ -53,5 +53,39 @@ for (let i = 0; i < 2000; i++) {
|
||||||
}
|
}
|
||||||
check(threwOnTruncation, 'truncated files are rejected');
|
check(threwOnTruncation, 'truncated files are rejected');
|
||||||
|
|
||||||
|
/* Version 2 fixtures written by the C library: every surface and the
|
||||||
|
* preview must match what C decodes (FNV-1a hashes in v2-expected.json). */
|
||||||
|
const fnv = (u8) => { let h = 2166136261; for (const v of u8) h = Math.imul(h ^ v, 16777619) >>> 0; return h; };
|
||||||
|
const dataDir = path.join(__dirname, 'test-data');
|
||||||
|
const expected = JSON.parse(fs.readFileSync(path.join(dataDir, 'v2-expected.json'), 'utf8'));
|
||||||
|
for (const [name, want] of Object.entries(expected)) {
|
||||||
|
const bytes = fs.readFileSync(path.join(dataDir, name));
|
||||||
|
let tex = null;
|
||||||
|
try { tex = YAPF.decodeTexture(bytes); } catch (e) { check(false, `${name}: ${e.message}`); continue; }
|
||||||
|
const got = tex.surfaces.map((s) => fnv(new Uint8Array(s.buffer, s.byteOffset, s.byteLength)));
|
||||||
|
check(JSON.stringify(got) === JSON.stringify(want.surfaces), `${name}: surfaces match the C decoder`);
|
||||||
|
if (want.preview0 !== null) {
|
||||||
|
const view = YAPF.textureToRGBA(tex, 0, 0);
|
||||||
|
/* float previews may differ by 1 in rounding; compare only exact kinds */
|
||||||
|
if (tex.payload === 1 ? tex.format !== 0x47 && tex.format !== 0x48 : tex.sample < 2)
|
||||||
|
check(fnv(view.rgba) === want.preview0, `${name}: preview matches the C library`);
|
||||||
|
}
|
||||||
|
/* damage must be caught by the checksums */
|
||||||
|
const broken = Uint8Array.from(bytes);
|
||||||
|
broken[broken.length - 3] ^= 0x10;
|
||||||
|
let code = null;
|
||||||
|
try { YAPF.decodeTexture(broken); } catch (e) { code = e.code; }
|
||||||
|
check(code === 'checksum', `${name}: a damaged byte is reported as a checksum error`);
|
||||||
|
}
|
||||||
|
{ /* v1 decode() reads 8-bit v2 files and names what it cannot read */
|
||||||
|
const cube = YAPF.decode(fs.readFileSync(path.join(dataDir, 'v2-cube-mips.yapf')));
|
||||||
|
check(cube.mipLevels === 7 && cube.width === 64, 'decode() reads face 0 of a v2 cube');
|
||||||
|
let msg = '';
|
||||||
|
try { YAPF.decode(fs.readFileSync(path.join(dataDir, 'v2-bc7-encoded.yapf'))); } catch (e) { msg = e.message; }
|
||||||
|
check(/decodeTexture/.test(msg), 'decode() points GPU-block files to decodeTexture');
|
||||||
|
const yapfSum = (t) => YAPF.checksum(new TextEncoder().encode(t));
|
||||||
|
check(yapfSum('') === 0xd7550ed0 && yapfSum('YAPF') === 0xc8cc41d4, 'yapf-sum test vectors');
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`${tests} tests, ${fails} failures`);
|
console.log(`${tests} tests, ${fails} failures`);
|
||||||
process.exit(fails ? 1 : 0);
|
process.exit(fails ? 1 : 0);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* yapf.js — YAPF v1 encoder / decoder in plain JavaScript.
|
* yapf.js — YAPF encoder (v1) and decoder (v1 and v2) in plain JavaScript.
|
||||||
*
|
*
|
||||||
* Copyright 2026 Nexoniarz — Apache License 2.0.
|
* Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
*
|
*
|
||||||
|
|
@ -10,9 +10,14 @@
|
||||||
* const file = YAPF.encode(img); // image → Uint8Array
|
* const file = YAPF.encode(img); // image → Uint8Array
|
||||||
* const rgba = YAPF.toRGBA(img); // any channel count → RGBA pixels
|
* const rgba = YAPF.toRGBA(img); // any channel count → RGBA pixels
|
||||||
*
|
*
|
||||||
|
* const tex = YAPF.decodeTexture(bytes); // any v1 / v2 file
|
||||||
|
* const view = YAPF.textureToRGBA(tex, surface); // { width, height, rgba }
|
||||||
|
*
|
||||||
* An image is { width, height, channels (1–4), pixels: Uint8Array,
|
* An image is { width, height, channels (1–4), pixels: Uint8Array,
|
||||||
* gpuFormat, flags, mipLevels, mips: [Uint8Array, …] }.
|
* gpuFormat, flags, mipLevels, mips: [Uint8Array, …] }.
|
||||||
* The specification is the comment at the top of yapf.h.
|
* Version 2 textures (GPU blocks, 16-bit / float, cubes, arrays, volumes,
|
||||||
|
* animation) are decoded by decodeTexture; encoding writes version 1.
|
||||||
|
* The specification: yapf.h (v1) and docs/FORMAT.md (v2).
|
||||||
*/
|
*/
|
||||||
(function (root, factory) {
|
(function (root, factory) {
|
||||||
if (typeof module === 'object' && module.exports) module.exports = factory();
|
if (typeof module === 'object' && module.exports) module.exports = factory();
|
||||||
|
|
@ -42,7 +47,676 @@
|
||||||
const filterValid = (f) => f <= F_UPAVG || (f >= F_UP && f < F_UP + 8);
|
const filterValid = (f) => f <= F_UPAVG || (f >= F_UP && f < F_UP + 8);
|
||||||
const upK = (f) => (f >= F_UP ? f - F_UP + 1 : 1);
|
const upK = (f) => (f >= F_UP ? f - F_UP + 1 : 1);
|
||||||
|
|
||||||
function corrupt() { throw new Error('YAPF: corrupt or unsupported file'); }
|
function corrupt() { throw new YapfError('corrupt', 'corrupt file'); }
|
||||||
|
|
||||||
|
/* ── Version 2: textures (decoding) ───────────────────────────────── */
|
||||||
|
|
||||||
|
const ENTROPY = 0x20, NOXFORM = 0x40, BLOCK_TILE = 16;
|
||||||
|
const HUFF_BITS = 11, HUFF_SIZE = 1 << HUFF_BITS, SLOTS = 48, CTXS = 7;
|
||||||
|
const NEED = { WIDE: 1, BLOCKS: 2, ENTROPY: 4, LAYOUT: 8, FRAMES: 16, ALL: 31 };
|
||||||
|
const r16 = (b, p) => b[p] | (b[p + 1] << 8);
|
||||||
|
const r64 = (b, p) => r32(b, p) + r32(b, p + 4) * 4294967296;
|
||||||
|
|
||||||
|
class YapfError extends Error {
|
||||||
|
constructor(code, msg) { super('YAPF: ' + msg); this.code = code; }
|
||||||
|
}
|
||||||
|
const fail = (code, msg) => { throw new YapfError(code, msg); };
|
||||||
|
const bad = () => fail('corrupt', 'corrupt file');
|
||||||
|
|
||||||
|
/* yapf-sum: 64-bit lanes kept as [hi, lo] uint32 pairs. */
|
||||||
|
const KH = 0x9E3779B9, KL = 0x7F4A7C15;
|
||||||
|
function mul64(ah, al, bh, bl) { /* (a · b) mod 2^64 */
|
||||||
|
const a0 = al & 0xFFFF, a1 = al >>> 16, b0 = bl & 0xFFFF, b1 = bl >>> 16;
|
||||||
|
const p00 = a0 * b0, p01 = a0 * b1, p10 = a1 * b0, p11 = a1 * b1;
|
||||||
|
const mid = (p00 >>> 16) + (p01 & 0xFFFF) + (p10 & 0xFFFF);
|
||||||
|
const lo = ((mid & 0xFFFF) * 65536 + (p00 & 0xFFFF)) >>> 0;
|
||||||
|
const hi = (p11 + (p01 >>> 16) + (p10 >>> 16) + Math.floor(mid / 65536) +
|
||||||
|
Math.imul(ah, bl) + Math.imul(al, bh)) >>> 0;
|
||||||
|
return [hi, lo];
|
||||||
|
}
|
||||||
|
function checksum(b, p, n) {
|
||||||
|
const lanes = [];
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const [h, l] = mul64(0, i + 1, KH, KL);
|
||||||
|
lanes.push([(h ^ Math.floor(n / 4294967296)) >>> 0, (l ^ (n >>> 0)) >>> 0]);
|
||||||
|
}
|
||||||
|
const words = Math.ceil(n / 8);
|
||||||
|
for (let j = 0; j < words; j++) {
|
||||||
|
const q = p + j * 8;
|
||||||
|
let wl, wh;
|
||||||
|
if (q + 8 <= p + n) { wl = r32(b, q); wh = r32(b, q + 4); }
|
||||||
|
else {
|
||||||
|
const t = new Uint8Array(8); t.set(b.subarray(q, p + n));
|
||||||
|
wl = r32(t, 0); wh = r32(t, 4);
|
||||||
|
}
|
||||||
|
const a = lanes[j & 3];
|
||||||
|
const [h, l] = mul64((a[0] ^ wh) >>> 0, (a[1] ^ wl) >>> 0, KH, KL);
|
||||||
|
a[0] = h; a[1] = (l ^ h) >>> 0; /* a ^= a >> 32 */
|
||||||
|
}
|
||||||
|
const rot = (x, r) => r === 32 ? [x[1], x[0]]
|
||||||
|
: [((x[0] << r) | (x[1] >>> (32 - r))) >>> 0, ((x[1] << r) | (x[0] >>> (32 - r))) >>> 0];
|
||||||
|
const r1 = rot(lanes[1], 16), r2 = rot(lanes[2], 32), r3 = rot(lanes[3], 48 - 32);
|
||||||
|
const r3b = [r3[1], r3[0]]; /* rotl 48 = rotl 16 then swap */
|
||||||
|
let hh = (lanes[0][0] ^ r1[0] ^ r2[0] ^ r3b[0]) >>> 0, hl = (lanes[0][1] ^ r1[1] ^ r2[1] ^ r3b[1]) >>> 0;
|
||||||
|
[hh, hl] = mul64(hh, hl, KH, KL);
|
||||||
|
return (hl ^ hh) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Huffman code table → lut[off .. off + 2048) (length << 8 | symbol). */
|
||||||
|
function huffTable(b, p, end, lut, off) {
|
||||||
|
if (p >= end) bad();
|
||||||
|
const n = b[p++] + 1, nbytes = (n + 1) >> 1;
|
||||||
|
if (end - p < nbytes) bad();
|
||||||
|
const len = new Uint8Array(n), count = new Uint32Array(HUFF_BITS + 2);
|
||||||
|
let kraft = 0;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const l = (b[p + (i >> 1)] >> ((i & 1) * 4)) & 15;
|
||||||
|
if (l > HUFF_BITS) bad();
|
||||||
|
len[i] = l; count[l]++;
|
||||||
|
if (l) kraft += HUFF_SIZE >> l;
|
||||||
|
}
|
||||||
|
if ((n & 1) && (b[p + (n >> 1)] >> 4)) bad();
|
||||||
|
if (kraft === 0 || kraft > HUFF_SIZE) bad();
|
||||||
|
p += nbytes;
|
||||||
|
const next = new Uint32Array(HUFF_BITS + 2);
|
||||||
|
let code = 0;
|
||||||
|
count[0] = 0;
|
||||||
|
for (let l = 1; l <= HUFF_BITS; l++) { code = (code + count[l - 1]) << 1; next[l] = code; }
|
||||||
|
for (let s = 0; s < n; s++) {
|
||||||
|
const l = len[s];
|
||||||
|
if (!l) continue;
|
||||||
|
let c = next[l]++, r = 0;
|
||||||
|
for (let i = 0; i < l; i++) { r = (r << 1) | (c & 1); c >>= 1; }
|
||||||
|
for (let k = r; k < HUFF_SIZE; k += 1 << l) lut[off + k] = (l << 8) | s;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code book → per slot one Uint16Array of 7 × 2048 entries. */
|
||||||
|
function readBook(b, p, len) {
|
||||||
|
const end = p + len;
|
||||||
|
if (len < 2) bad();
|
||||||
|
const n = r16(b, p);
|
||||||
|
p += 2;
|
||||||
|
if (n === 0 || n > SLOTS * CTXS) bad();
|
||||||
|
const book = new Array(SLOTS).fill(null), seen = new Uint8Array(SLOTS * CTXS);
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
if (end - p < 3) bad();
|
||||||
|
const slot = b[p], ctx = b[p + 1];
|
||||||
|
if (slot >= SLOTS || ctx >= CTXS || seen[slot * CTXS + ctx]) bad();
|
||||||
|
seen[slot * CTXS + ctx] = 1;
|
||||||
|
if (!book[slot]) book[slot] = new Uint16Array(CTXS * HUFF_SIZE);
|
||||||
|
p = huffTable(b, p + 2, end, book[slot], ctx * HUFF_SIZE);
|
||||||
|
}
|
||||||
|
if (p !== end) bad();
|
||||||
|
return book;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Context of a residual from the folded residuals left + above. */
|
||||||
|
const ctxOf = (s) => (s > 0) + (s > 2) + (s > 6) + (s > 14) + (s > 30) + (s > 62);
|
||||||
|
|
||||||
|
/* Entropy plane payload: filters, u16 length, context-coded residuals. */
|
||||||
|
function huffPlane(b, p, end, z, tw, th, book, slot) {
|
||||||
|
const nb = (th + BAND - 1) >> 3;
|
||||||
|
if (!book || !book[slot] || end - p < nb + 2) bad();
|
||||||
|
const filt = b.subarray(p, p + nb);
|
||||||
|
for (let i = 0; i < nb; i++) if (!filterValid(filt[i])) bad();
|
||||||
|
p += nb;
|
||||||
|
const L = r16(b, p);
|
||||||
|
p += 2;
|
||||||
|
if (end - p < L) bad();
|
||||||
|
const lut = book[slot], se = p + L;
|
||||||
|
let buf = 0, bits = 0, pos = p, used = 0, i = 0;
|
||||||
|
for (let y = 0; y < th; y++)
|
||||||
|
for (let x = 0; x < tw; x++, i++) {
|
||||||
|
while (bits <= 24) { buf |= (pos < se ? b[pos] : 0) << bits; pos++; bits += 8; }
|
||||||
|
const s = (x ? z[i - 1] : 0) + (y ? z[i - tw] : 0);
|
||||||
|
const e = lut[(ctxOf(s) << HUFF_BITS) | (buf & (HUFF_SIZE - 1))];
|
||||||
|
if (!e) bad();
|
||||||
|
const n = e >> 8;
|
||||||
|
buf >>>= n; bits -= n; used += n;
|
||||||
|
z[i] = e & 255;
|
||||||
|
}
|
||||||
|
if (Math.ceil(used / 8) !== L) bad();
|
||||||
|
z.fill(0, tw * th, tw * th + GROUP);
|
||||||
|
return { p: se, filt };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wide samples: byte planes, transforms at the sample width. */
|
||||||
|
function decodeTileWide(b, p, end, out, imgW, tx, ty, tw, th, ch, sample, book) {
|
||||||
|
const sb = sample === 3 ? 4 : 2, bits = 8 * sb, isf = sample >= 2, np = ch * sb;
|
||||||
|
const half = bits === 32 ? 0x80000000 : 0x8000;
|
||||||
|
const mod = bits === 32 ? (x) => x >>> 0 : (x) => x & 0xFFFF;
|
||||||
|
const sar1 = (v) => mod((v >>> 1) | (v & half));
|
||||||
|
if (end <= p) bad();
|
||||||
|
const hdr = b[p++];
|
||||||
|
if (hdr === STORED) {
|
||||||
|
if (end - p !== tw * th * ch * sb) bad();
|
||||||
|
for (let y = 0; y < th; y++) {
|
||||||
|
const d = ((ty + y) * imgW + tx) * ch;
|
||||||
|
for (let i = 0; i < tw * ch; i++, p += sb)
|
||||||
|
out[d + i] = sb === 2 ? r16(b, p) : r32(b, p);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hdr & 0x8F) bad();
|
||||||
|
if ((hdr & NOXFORM) && (hdr & SUBGREEN)) bad();
|
||||||
|
if (ch < 3 && (hdr & (NOXFORM | SUBGREEN))) bad();
|
||||||
|
const xf = ch < 3 ? 2 : (hdr & NOXFORM) ? 2 : (hdr & SUBGREEN) ? 1 : 0;
|
||||||
|
const tsel = ch < 3 ? 0 : xf;
|
||||||
|
if (end - p < 2) bad();
|
||||||
|
const cmask = r16(b, p);
|
||||||
|
p += 2;
|
||||||
|
if (cmask >> np) bad();
|
||||||
|
let emask = 0;
|
||||||
|
if (hdr & ENTROPY) {
|
||||||
|
if (end - p < 2 || !book) bad();
|
||||||
|
emask = r16(b, p);
|
||||||
|
p += 2;
|
||||||
|
if ((emask >> np) || (emask & cmask)) bad();
|
||||||
|
}
|
||||||
|
const RL = TILE + 1, PL = TILE * RL;
|
||||||
|
const planes = new Uint8Array(np * PL), z = new Uint8Array(TILE_PX + GROUP);
|
||||||
|
for (let pl = 0; pl < np; pl++) {
|
||||||
|
if (!(cmask & (1 << pl))) continue;
|
||||||
|
if (p >= end) bad();
|
||||||
|
planes.fill(b[p++], pl * PL, (pl + 1) * PL);
|
||||||
|
}
|
||||||
|
for (let pl = 0; pl < np; pl++) {
|
||||||
|
if (cmask & (1 << pl)) continue;
|
||||||
|
z.fill(0);
|
||||||
|
const r = (emask & (1 << pl)) ? huffPlane(b, p, end, z, tw, th, book, tsel * 16 + pl)
|
||||||
|
: unpackPlane(b, p, end, z, tw, th);
|
||||||
|
p = r.p;
|
||||||
|
const base = pl * PL;
|
||||||
|
for (let y = 0; y < th; y++) {
|
||||||
|
const f = r.filt[y >> 3], k = upK(f);
|
||||||
|
unfilterRow(f, planes, base + y * RL,
|
||||||
|
y ? planes : ZERO_ROW, y ? base + (y - 1) * RL : 0,
|
||||||
|
y >= k ? planes : ZERO_ROW, y >= k ? base + (y - k) * RL : 0,
|
||||||
|
z, y * tw, tw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p !== end) bad();
|
||||||
|
const v = [0, 0, 0, 0];
|
||||||
|
for (let y = 0; y < th; y++) {
|
||||||
|
const d0 = ((ty + y) * imgW + tx) * ch, r0 = y * RL;
|
||||||
|
for (let x = 0; x < tw; x++) {
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
let a = 0;
|
||||||
|
for (let k = 0; k < sb; k++) a = a * 256 + planes[(c * sb + k) * PL + r0 + x];
|
||||||
|
v[c] = a;
|
||||||
|
}
|
||||||
|
if (xf === 0) {
|
||||||
|
const o = mod(v[1] - half), q = mod(v[2] - half), t = mod(v[0] - sar1(q));
|
||||||
|
const g = mod(q + t), bb = mod(t - sar1(o));
|
||||||
|
v[0] = mod(bb + o); v[1] = g; v[2] = bb;
|
||||||
|
} else if (xf === 1) {
|
||||||
|
const g = v[0];
|
||||||
|
v[0] = mod(v[1] + g - half); v[2] = mod(v[2] + g - half); v[1] = g;
|
||||||
|
}
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
let s = v[c];
|
||||||
|
if (isf) s = (s & half) ? mod(s ^ half) : mod(~s);
|
||||||
|
out[d0 + x * ch + c] = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GPU block tile: raw, or packed lane by lane with the book. */
|
||||||
|
function decodeBlockTile(b, p, end, packed, out, o0, rowb, tbw, tbh, S, book) {
|
||||||
|
const n = tbw * tbh;
|
||||||
|
if (!packed) {
|
||||||
|
if (end - p !== n * S) bad();
|
||||||
|
for (let r = 0; r < tbh; r++) out.set(b.subarray(p + r * tbw * S, p + (r + 1) * tbw * S), o0 + r * rowb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!book) bad();
|
||||||
|
let buf = 0, bits = 0, pos = p, used = 0;
|
||||||
|
for (let k = 0; k < S; k++) {
|
||||||
|
const lut = book[k];
|
||||||
|
if (!lut) bad();
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
while (bits <= 24) { buf |= (pos < end ? b[pos] : 0) << bits; pos++; bits += 8; }
|
||||||
|
const e = lut[buf & (HUFF_SIZE - 1)];
|
||||||
|
if (!e) bad();
|
||||||
|
const l = e >> 8;
|
||||||
|
buf >>>= l; bits -= l; used += l;
|
||||||
|
out[o0 + Math.floor(i / tbw) * rowb + (i % tbw) * S + k] = e & 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Math.ceil(used / 8) !== end - p) bad();
|
||||||
|
}
|
||||||
|
|
||||||
|
const BLOCK_FORMATS = {
|
||||||
|
0x40: [4, 4, 8], 0x41: [4, 4, 16], 0x42: [4, 4, 16], 0x43: [4, 4, 8], 0x44: [4, 4, 8],
|
||||||
|
0x45: [4, 4, 16], 0x46: [4, 4, 16], 0x47: [4, 4, 16], 0x48: [4, 4, 16], 0x49: [4, 4, 16],
|
||||||
|
0x50: [4, 4, 8], 0x51: [4, 4, 8], 0x52: [4, 4, 16], 0x53: [4, 4, 8], 0x54: [4, 4, 8],
|
||||||
|
0x55: [4, 4, 16], 0x56: [4, 4, 16]
|
||||||
|
};
|
||||||
|
[[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]].forEach(([w, h], i) => { BLOCK_FORMATS[0x60 + i] = [w, h, 16]; });
|
||||||
|
|
||||||
|
function mipSize(t, m) {
|
||||||
|
return [Math.max(1, t.width >>> m), Math.max(1, t.height >>> m),
|
||||||
|
t.shape === 2 ? Math.max(1, t.depth >>> m) : 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function surfaceBytes(t, m) {
|
||||||
|
const [w, h, d] = mipSize(t, m);
|
||||||
|
if (t.payload === 1) {
|
||||||
|
const [bw, bh, S] = BLOCK_FORMATS[t.format];
|
||||||
|
return Math.ceil(w / bw) * Math.ceil(h / bh) * d * S;
|
||||||
|
}
|
||||||
|
return w * h * d * t.channels * (t.sample === 0 ? 1 : t.sample === 3 ? 4 : 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Decodes one surface (v2) into a typed array. */
|
||||||
|
function decodeSurface(b, t, s, verify) {
|
||||||
|
const m = s % t.mipLevels, [w, h, d] = mipSize(t, m);
|
||||||
|
const off = t._off[s], size = t._size[s], blocks = t.payload === 1;
|
||||||
|
let tcx, tcy, nbx = 0, nby = 0, S = 0;
|
||||||
|
if (blocks) {
|
||||||
|
const f = BLOCK_FORMATS[t.format];
|
||||||
|
nbx = Math.ceil(w / f[0]); nby = Math.ceil(h / f[1]); S = f[2];
|
||||||
|
tcx = Math.ceil(nbx / BLOCK_TILE); tcy = Math.ceil(nby / BLOCK_TILE);
|
||||||
|
} else {
|
||||||
|
tcx = Math.ceil(w / TILE); tcy = Math.ceil(h / TILE);
|
||||||
|
}
|
||||||
|
const tiles = tcx * tcy * d;
|
||||||
|
if (size < 4) bad();
|
||||||
|
const bl = r32(b, off);
|
||||||
|
if (bl > size - 4 || Math.floor((size - 4 - bl) / 8) < tiles) bad();
|
||||||
|
const pre = 4 + bl;
|
||||||
|
if (verify && checksum(b, off, pre + tiles * 8) !== t._tcheck[s]) fail('checksum', 'damaged file: a checksum does not match');
|
||||||
|
let book = null;
|
||||||
|
if (bl) {
|
||||||
|
if (!(t.needs & NEED.ENTROPY)) bad();
|
||||||
|
book = readBook(b, off + 4, bl);
|
||||||
|
}
|
||||||
|
const table = off + pre, end = off + size;
|
||||||
|
let pos = table + tiles * 8;
|
||||||
|
const sizes = new Array(tiles), packed = new Array(tiles), starts = new Array(tiles);
|
||||||
|
for (let i = 0; i < tiles; i++) {
|
||||||
|
let sz = r32(b, table + i * 8);
|
||||||
|
packed[i] = sz >>> 31;
|
||||||
|
sz &= 0x7FFFFFFF;
|
||||||
|
if (packed[i] && (!blocks || !book)) bad();
|
||||||
|
if (sz > end - pos) bad();
|
||||||
|
starts[i] = pos; sizes[i] = sz; pos += sz;
|
||||||
|
}
|
||||||
|
if (pos !== end) bad();
|
||||||
|
|
||||||
|
const bytes = surfaceBytes(t, m);
|
||||||
|
let out, store;
|
||||||
|
if (blocks || t.sample === 0) out = store = new Uint8Array(bytes);
|
||||||
|
else if (t.sample === 3) { store = new Uint32Array(bytes / 4); out = new Float32Array(store.buffer); }
|
||||||
|
else out = store = new Uint16Array(bytes / 2);
|
||||||
|
for (let i = 0; i < tiles; i++) {
|
||||||
|
const p = starts[i], e = p + sizes[i];
|
||||||
|
if (verify && checksum(b, p, sizes[i]) !== r32(b, table + i * 8 + 4))
|
||||||
|
fail('checksum', 'damaged file: a checksum does not match');
|
||||||
|
const row = Math.floor(i / tcx), z = Math.floor(row / tcy), ty = row % tcy, tx = i % tcx;
|
||||||
|
if (blocks) {
|
||||||
|
const bx0 = tx * BLOCK_TILE, by0 = ty * BLOCK_TILE, rowb = nbx * S;
|
||||||
|
decodeBlockTile(b, p, e, packed[i], store, z * nby * rowb + by0 * rowb + bx0 * S, rowb,
|
||||||
|
Math.min(BLOCK_TILE, nbx - bx0), Math.min(BLOCK_TILE, nby - by0), S, book);
|
||||||
|
} else {
|
||||||
|
const x0 = tx * TILE, y0 = ty * TILE, tw = Math.min(TILE, w - x0), th = Math.min(TILE, h - y0);
|
||||||
|
const slice = w * h * t.channels;
|
||||||
|
const view = store.subarray(z * slice, (z + 1) * slice);
|
||||||
|
if (t.sample === 0) decodeTile(b, p, e, view, w, x0, y0, tw, th, t.channels, true, book);
|
||||||
|
else decodeTileWide(b, p, e, view, w, x0, y0, tw, th, t.channels, t.sample, book);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* decodeTexture(bytes, { verify = true }) — any YAPF file (v1 or v2).
|
||||||
|
* Returns { version, width, height, depth, layers, frames, faces, shape,
|
||||||
|
* mipLevels, payload, format, sample, channels, flags, channelMap,
|
||||||
|
* linearMask, normal, primaries, transfer, mipFilter, alphaRef,
|
||||||
|
* loopCount, frameUs, notes: [{ tag, data }], surfaces: [...] }, where
|
||||||
|
* surfaces[s] is a Uint8Array (8-bit pixels, GPU blocks), Uint16Array
|
||||||
|
* (16-bit, half bits) or Float32Array, numbered
|
||||||
|
* s = ((frame · layers + layer) · faces + face) · mipLevels + mip.
|
||||||
|
* Errors are YapfError with .code 'not-yapf', 'version', 'unsupported',
|
||||||
|
* 'corrupt', 'checksum' or 'truncated'.
|
||||||
|
*/
|
||||||
|
function decodeTexture(input, opts) {
|
||||||
|
const b = input instanceof Uint8Array ? input : new Uint8Array(input);
|
||||||
|
const verify = !(opts && opts.verify === false);
|
||||||
|
if (b.length < 5 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
|
||||||
|
fail('not-yapf', 'not a YAPF file');
|
||||||
|
if (b[4] === 1) {
|
||||||
|
const img = decodeV1(b);
|
||||||
|
return { version: 1, width: img.width, height: img.height, depth: 1, layers: 1, frames: 1,
|
||||||
|
faces: 1, shape: 0, mipLevels: img.mipLevels, payload: 0, format: img.gpuFormat,
|
||||||
|
sample: 0, channels: img.channels, flags: img.flags, channelMap: [0, 0, 0, 0],
|
||||||
|
linearMask: 0, normal: 0, primaries: 0, transfer: 0, mipFilter: 0, alphaRef: 0,
|
||||||
|
loopCount: 0, frameUs: [], notes: [], surfaces: img.mips };
|
||||||
|
}
|
||||||
|
if (b[4] !== 2) fail('version', 'format version ' + b[4] + ' is newer than this decoder');
|
||||||
|
if (b.length < 64) fail('truncated', 'the file is truncated');
|
||||||
|
if (verify && checksum(b, 0, 0x3C) !== r32(b, 0x3C)) fail('checksum', 'damaged file: a checksum does not match');
|
||||||
|
const t = {
|
||||||
|
version: 2, channels: b[5], format: b[6], mipLevels: b[7], flags: b[8], payload: b[9],
|
||||||
|
sample: b[10], shape: b[11], width: r32(b, 0x0C), height: r32(b, 0x10), depth: r32(b, 0x14),
|
||||||
|
layers: r16(b, 0x18), frames: r16(b, 0x1A), needs: r32(b, 0x1C),
|
||||||
|
channelMap: Array.from(b.subarray(0x20, 0x24)), linearMask: b[0x24], normal: b[0x25],
|
||||||
|
primaries: b[0x26], transfer: b[0x27], mipFilter: b[0x28], alphaRef: b[0x29],
|
||||||
|
loopCount: r16(b, 0x2A), frameUs: [], notes: []
|
||||||
|
};
|
||||||
|
const notesSize = r32(b, 0x2C), fileSize = r64(b, 0x30);
|
||||||
|
if (t.needs & ~NEED.ALL) fail('unsupported', 'the file needs a feature this decoder does not have');
|
||||||
|
t.faces = t.shape === 1 ? 6 : 1;
|
||||||
|
if (t.channels < 1 || t.channels > 4 || t.sample > 3 || t.payload > 1 || t.shape > 2 ||
|
||||||
|
t.width < 1 || t.width > MAX_DIM || t.height < 1 || t.height > MAX_DIM ||
|
||||||
|
!t.layers || !t.frames || t.mipLevels < 1 || t.mipLevels > MAX_MIPS) bad();
|
||||||
|
if (t.shape === 2 ? (t.depth < 1 || t.depth > MAX_DIM || t.layers !== 1) : t.depth > 1) bad();
|
||||||
|
if (t.shape !== 2) t.depth = 1;
|
||||||
|
if (t.shape === 1 && t.width !== t.height) bad();
|
||||||
|
if (t.payload === 1 && !BLOCK_FORMATS[t.format]) bad();
|
||||||
|
if ((t.payload === 0 && t.sample !== 0 && !(t.needs & NEED.WIDE)) ||
|
||||||
|
(t.payload === 1 && !(t.needs & NEED.BLOCKS)) ||
|
||||||
|
((t.shape !== 0 || t.layers > 1) && !(t.needs & NEED.LAYOUT)) ||
|
||||||
|
(t.frames > 1 && !(t.needs & NEED.FRAMES))) bad();
|
||||||
|
const S = t.frames * t.layers * t.faces * t.mipLevels;
|
||||||
|
if (S > (1 << 24)) bad();
|
||||||
|
const front = 64 + 16 * S + (t.frames > 1 ? 4 * t.frames : 0) + notesSize;
|
||||||
|
if (front > fileSize) bad();
|
||||||
|
if (b.length < fileSize) fail('truncated', 'the file is truncated');
|
||||||
|
if (verify && checksum(b, 64, front - 64) !== r32(b, 0x38)) fail('checksum', 'damaged file: a checksum does not match');
|
||||||
|
let q = 64 + 16 * S;
|
||||||
|
for (let f = 0; t.frames > 1 && f < t.frames; f++, q += 4) t.frameUs.push(r32(b, q));
|
||||||
|
const qe = q + notesSize;
|
||||||
|
while (q < qe) {
|
||||||
|
if (qe - q < 8) bad();
|
||||||
|
const sz = r32(b, q + 4);
|
||||||
|
if (qe - q - 8 < sz) bad();
|
||||||
|
t.notes.push({ tag: String.fromCharCode(b[q], b[q + 1], b[q + 2], b[q + 3]), data: b.slice(q + 8, q + 8 + sz) });
|
||||||
|
q += 8 + sz;
|
||||||
|
}
|
||||||
|
t._off = []; t._size = []; t._tcheck = [];
|
||||||
|
for (let s = 0; s < S; s++) {
|
||||||
|
const e = 64 + 16 * s, off = r64(b, e), size = r32(b, e + 8);
|
||||||
|
if (off < front || off > fileSize || size > fileSize - off) bad();
|
||||||
|
t._off.push(off); t._size.push(size); t._tcheck.push(r32(b, e + 12));
|
||||||
|
}
|
||||||
|
t.surfaces = [];
|
||||||
|
for (let s = 0; s < S; s++) t.surfaces.push(decodeSurface(b, t, s, verify));
|
||||||
|
delete t._off; delete t._size; delete t._tcheck;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── GPU block decoding (previews) ───────────────────────────────── */
|
||||||
|
|
||||||
|
const PART2 = [0xCCCC, 0x8888, 0xEEEE, 0xECC8, 0xC880, 0xFEEC, 0xFEC8, 0xEC80, 0xC800, 0xFFEC, 0xFE80, 0xE800,
|
||||||
|
0xFFE8, 0xFF00, 0xFFF0, 0xF000, 0xF710, 0x008E, 0x7100, 0x08CE, 0x008C, 0x7310, 0x3100, 0x8CCE, 0x088C, 0x3110,
|
||||||
|
0x6666, 0x366C, 0x17E8, 0x0FF0, 0x718E, 0x399C, 0xAAAA, 0xF0F0, 0x5A5A, 0x33CC, 0x3C3C, 0x55AA, 0x9696, 0xA55A,
|
||||||
|
0x73CE, 0x13C8, 0x324C, 0x3BDC, 0x6996, 0xC33C, 0x9966, 0x0660, 0x0272, 0x04E4, 0x4E40, 0x2720, 0xC936, 0x936C,
|
||||||
|
0x39C6, 0x639C, 0x9336, 0x9CC6, 0x817E, 0xE718, 0xCCF0, 0x0FCC, 0x7744, 0xEE22];
|
||||||
|
const PART3 = [0xAA685050, 0x6A5A5040, 0x5A5A4200, 0x5450A0A8, 0xA5A50000, 0xA0A05050, 0x5555A0A0, 0x5A5A5050,
|
||||||
|
0xAA550000, 0xAA555500, 0xAAAA5500, 0x90909090, 0x94949494, 0xA4A4A4A4, 0xA9A59450, 0x2A0A4250, 0xA5945040,
|
||||||
|
0x0A425054, 0xA5A5A500, 0x55A0A0A0, 0xA8A85454, 0x6A6A4040, 0xA4A45000, 0x1A1A0500, 0x0050A4A4, 0xAAA59090,
|
||||||
|
0x14696914, 0x69691400, 0xA08585A0, 0xAA821414, 0x50A4A450, 0x6A5A0200, 0xA9A58000, 0x5090A0A8, 0xA8A09050,
|
||||||
|
0x24242424, 0x00AA5500, 0x24924924, 0x24499224, 0x50A50A50, 0x500AA550, 0xAAAA4444, 0x66660000, 0xA5A0A5A0,
|
||||||
|
0x50A050A0, 0x69286928, 0x44AAAA44, 0x66666600, 0xAA444444, 0x54A854A8, 0x95809580, 0x96969600, 0xA85454A8,
|
||||||
|
0x80959580, 0xAA141414, 0x96960000, 0xAAAA1414, 0xA05050A0, 0xA0A5A5A0, 0x96000000, 0x40804080, 0xA9A8A9A8,
|
||||||
|
0xAAAAAA44, 0x2A4A5254];
|
||||||
|
const ANCHOR2 = [15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 2, 8, 2, 2, 8, 8, 15, 2, 8, 2, 2,
|
||||||
|
8, 8, 2, 2, 15, 15, 6, 8, 2, 8, 15, 15, 2, 8, 2, 2, 2, 15, 15, 6, 6, 2, 6, 8, 15, 15, 2, 2, 15, 15, 15, 15, 15, 2, 2, 15];
|
||||||
|
const ANCHOR3A = [3, 3, 15, 15, 8, 3, 15, 15, 8, 8, 6, 6, 6, 5, 3, 3, 3, 3, 8, 15, 3, 3, 6, 10, 5, 8, 8, 6, 8, 5, 15,
|
||||||
|
15, 8, 15, 3, 5, 6, 10, 8, 15, 15, 3, 15, 5, 15, 15, 15, 15, 3, 15, 5, 5, 5, 8, 5, 10, 5, 10, 8, 13, 15, 12, 3, 3];
|
||||||
|
const ANCHOR3B = [15, 8, 8, 3, 15, 15, 3, 8, 15, 15, 15, 15, 15, 15, 15, 8, 15, 8, 15, 3, 15, 8, 15, 8, 3, 15, 6, 10,
|
||||||
|
15, 15, 10, 8, 15, 3, 15, 10, 10, 8, 9, 10, 6, 15, 8, 15, 3, 6, 6, 8, 15, 3, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||||
|
15, 3, 15, 15, 8];
|
||||||
|
const WEIGHTS = { 2: [0, 21, 43, 64], 3: [0, 9, 18, 27, 37, 46, 55, 64],
|
||||||
|
4: [0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64] };
|
||||||
|
const interp = (a, b, w) => (a * (64 - w) + b * w + 32) >> 6;
|
||||||
|
|
||||||
|
function bits128(b, o) {
|
||||||
|
let pos = 0;
|
||||||
|
return (n) => {
|
||||||
|
let v = 0;
|
||||||
|
for (let i = 0; i < n; i++, pos++) v |= ((b[o + (pos >> 3)] >> (pos & 7)) & 1) << i;
|
||||||
|
return v >>> 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgb565(c) {
|
||||||
|
const r = (c >> 11) & 31, g = (c >> 5) & 63, bl = c & 31;
|
||||||
|
return [(r << 3) | (r >> 2), (g << 2) | (g >> 4), (bl << 3) | (bl >> 2)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function bc1Block(b, o, out, fourOnly) {
|
||||||
|
const c0 = r16(b, o), c1 = r16(b, o + 2), idx = r32(b, o + 4), four = c0 > c1 || fourOnly;
|
||||||
|
const p0 = rgb565(c0), p1 = rgb565(c1), pal = [p0.concat(255), p1.concat(255), [0, 0, 0, 255], [0, 0, 0, 255]];
|
||||||
|
for (let k = 0; k < 3; k++) {
|
||||||
|
if (four) { pal[2][k] = ((2 * p0[k] + p1[k] + 1) / 3) | 0; pal[3][k] = ((p0[k] + 2 * p1[k] + 1) / 3) | 0; }
|
||||||
|
else { pal[2][k] = (p0[k] + p1[k] + 1) >> 1; pal[3][k] = 0; }
|
||||||
|
}
|
||||||
|
if (!four) pal[3][3] = 0;
|
||||||
|
for (let i = 0; i < 16; i++) { const c = pal[(idx >>> (2 * i)) & 3]; for (let k = 0; k < 4; k++) out[i * 4 + k] = c[k]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bc4Block(b, o, out, ch, snorm) {
|
||||||
|
const pal = new Array(8);
|
||||||
|
let r0 = b[o], r1 = b[o + 1];
|
||||||
|
if (snorm) { r0 = Math.max(-127, (r0 << 24) >> 24); r1 = Math.max(-127, (r1 << 24) >> 24); }
|
||||||
|
pal[0] = r0; pal[1] = r1;
|
||||||
|
const div = (v, d) => (v >= 0 ? Math.floor((v + (d >> 1)) / d) : -Math.floor((-v + (d >> 1)) / d));
|
||||||
|
if (r0 > r1) for (let i = 2; i < 8; i++) pal[i] = div((8 - i) * r0 + (i - 1) * r1, 7);
|
||||||
|
else { for (let i = 2; i < 6; i++) pal[i] = div((6 - i) * r0 + (i - 1) * r1, 5); pal[6] = snorm ? -127 : 0; pal[7] = snorm ? 127 : 255; }
|
||||||
|
let lo = r32(b, o + 2), hi = r16(b, o + 6);
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
const bit = 3 * i, v = bit < 30 ? (lo >>> bit) & 7 : bit === 30 ? ((lo >>> 30) | ((hi & 1) << 2)) : (hi >> (bit - 32)) & 7;
|
||||||
|
out[i * 4 + ch] = pal[v] + (snorm ? 128 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BC7 = [[3, 4, 0, 0, 4, 0, 1, 0, 3, 0], [2, 6, 0, 0, 6, 0, 0, 1, 3, 0], [3, 6, 0, 0, 5, 0, 0, 0, 2, 0],
|
||||||
|
[2, 6, 0, 0, 7, 0, 1, 0, 2, 0], [1, 0, 2, 1, 5, 6, 0, 0, 2, 3], [1, 0, 2, 0, 7, 8, 0, 0, 2, 2],
|
||||||
|
[1, 0, 0, 0, 7, 7, 1, 0, 4, 0], [2, 6, 0, 0, 5, 5, 1, 0, 2, 0]];
|
||||||
|
const subsetOf = (ns, part, i) => ns === 2 ? (PART2[part] >> i) & 1 : ns === 3 ? (PART3[part] >>> (2 * i)) & 3 : 0;
|
||||||
|
const isAnchor = (ns, part, i) => i === 0 || (ns === 2 && i === ANCHOR2[part]) ||
|
||||||
|
(ns === 3 && (i === ANCHOR3A[part] || i === ANCHOR3B[part]));
|
||||||
|
|
||||||
|
function bc7Block(b, o, out) {
|
||||||
|
const get = bits128(b, o);
|
||||||
|
let mode = 0;
|
||||||
|
while (mode < 8 && !get(1)) mode++;
|
||||||
|
if (mode === 8) { out.fill(0, 0, 64); return; }
|
||||||
|
const [ns, pb, rb, isbB, cb, ab, epb, spb, ib, ib2] = BC7[mode];
|
||||||
|
const part = get(pb), rot = get(rb), isb = get(isbB), ne = ns * 2;
|
||||||
|
const ep = []; for (let e = 0; e < ne; e++) ep.push([0, 0, 0, 255]);
|
||||||
|
for (let c = 0; c < 3; c++) for (let e = 0; e < ne; e++) ep[e][c] = get(cb);
|
||||||
|
for (let e = 0; e < ne; e++) ep[e][3] = ab ? get(ab) : 255;
|
||||||
|
const pbit = epb | spb;
|
||||||
|
if (epb) for (let e = 0; e < ne; e++) { const p = get(1); for (let c = 0; c < 4; c++) if (c < 3 || ab) ep[e][c] = (ep[e][c] << 1) | p; }
|
||||||
|
else if (spb) for (let k = 0; k < ns; k++) { const p = get(1); for (let c = 0; c < 3; c++) { ep[2 * k][c] = (ep[2 * k][c] << 1) | p; ep[2 * k + 1][c] = (ep[2 * k + 1][c] << 1) | p; } }
|
||||||
|
for (let e = 0; e < ne; e++) for (let c = 0; c < 4; c++) {
|
||||||
|
if (c === 3 && !ab) continue;
|
||||||
|
const nb = (c < 3 ? cb : ab) + pbit, v = ep[e][c] << (8 - nb);
|
||||||
|
ep[e][c] = v | (v >> nb);
|
||||||
|
}
|
||||||
|
const idx = [], idx2 = [];
|
||||||
|
for (let i = 0; i < 16; i++) idx.push(get(ib - (isAnchor(ns, part, i) ? 1 : 0)));
|
||||||
|
for (let i = 0; i < 16; i++) idx2.push(ib2 ? get(ib2 - (i === 0 ? 1 : 0)) : 0);
|
||||||
|
const w1 = WEIGHTS[ib], w2 = WEIGHTS[ib2 || 2];
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
const k = subsetOf(ns, part, i), A = ep[2 * k], B = ep[2 * k + 1];
|
||||||
|
let cw, aw;
|
||||||
|
if (!ib2) cw = aw = w1[idx[i]];
|
||||||
|
else if (!isb) { cw = w1[idx[i]]; aw = w2[idx2[i]]; }
|
||||||
|
else { cw = w2[idx2[i]]; aw = w1[idx[i]]; }
|
||||||
|
const px = [interp(A[0], B[0], cw), interp(A[1], B[1], cw), interp(A[2], B[2], cw), interp(A[3], B[3], aw)];
|
||||||
|
if (rot) { const t = px[3]; px[3] = px[rot - 1]; px[rot - 1] = t; }
|
||||||
|
for (let c = 0; c < 4; c++) out[i * 4 + c] = px[c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BC6H header layouts: [field, first bit, count]; fields 0–3 red w x y z,
|
||||||
|
* 4–7 green, 8–11 blue, 12 partition. */
|
||||||
|
const RW = 0, RX = 1, RY = 2, RZ = 3, GW = 4, GX = 5, GY = 6, GZ = 7, BW = 8, BX = 9, BY = 10, BZ = 11, PD = 12;
|
||||||
|
const BC6 = [
|
||||||
|
[0x00, 10, 5, 5, 5, [[GY,4,1],[BY,4,1],[BZ,4,1],[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,5],[GZ,4,1],[GY,0,4],[GX,0,5],[BZ,0,1],[GZ,0,4],[BX,0,5],[BZ,1,1],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x01, 7, 6, 6, 6, [[GY,5,1],[GZ,4,1],[GZ,5,1],[RW,0,7],[BZ,0,1],[BZ,1,1],[BY,4,1],[GW,0,7],[BY,5,1],[BZ,2,1],[GY,4,1],[BW,0,7],[BZ,3,1],[BZ,5,1],[BZ,4,1],[RX,0,6],[GY,0,4],[GX,0,6],[GZ,0,4],[BX,0,6],[BY,0,4],[RY,0,6],[RZ,0,6],[PD,0,5]]],
|
||||||
|
[0x02, 11, 5, 4, 4, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,5],[RW,10,1],[GY,0,4],[GX,0,4],[GW,10,1],[BZ,0,1],[GZ,0,4],[BX,0,4],[BW,10,1],[BZ,1,1],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x06, 11, 4, 5, 4, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,4],[RW,10,1],[GZ,4,1],[GY,0,4],[GX,0,5],[GW,10,1],[GZ,0,4],[BX,0,4],[BW,10,1],[BZ,1,1],[BY,0,4],[RY,0,4],[BZ,0,1],[BZ,2,1],[RZ,0,4],[GY,4,1],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x0A, 11, 4, 4, 5, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,4],[RW,10,1],[BY,4,1],[GY,0,4],[GX,0,4],[GW,10,1],[BZ,0,1],[GZ,0,4],[BX,0,5],[BW,10,1],[BY,0,4],[RY,0,4],[BZ,1,1],[BZ,2,1],[RZ,0,4],[BZ,4,1],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x0E, 9, 5, 5, 5, [[RW,0,9],[BY,4,1],[GW,0,9],[GY,4,1],[BW,0,9],[BZ,4,1],[RX,0,5],[GZ,4,1],[GY,0,4],[GX,0,5],[BZ,0,1],[GZ,0,4],[BX,0,5],[BZ,1,1],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x12, 8, 6, 5, 5, [[RW,0,8],[GZ,4,1],[BY,4,1],[GW,0,8],[BZ,2,1],[GY,4,1],[BW,0,8],[BZ,3,1],[BZ,4,1],[RX,0,6],[GY,0,4],[GX,0,5],[BZ,0,1],[GZ,0,4],[BX,0,5],[BZ,1,1],[BY,0,4],[RY,0,6],[RZ,0,6],[PD,0,5]]],
|
||||||
|
[0x16, 8, 5, 6, 5, [[RW,0,8],[BZ,0,1],[BY,4,1],[GW,0,8],[GY,5,1],[GY,4,1],[BW,0,8],[GZ,5,1],[BZ,4,1],[RX,0,5],[GZ,4,1],[GY,0,4],[GX,0,6],[GZ,0,4],[BX,0,5],[BZ,1,1],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x1A, 8, 5, 5, 6, [[RW,0,8],[BZ,1,1],[BY,4,1],[GW,0,8],[BY,5,1],[GY,4,1],[BW,0,8],[BZ,5,1],[BZ,4,1],[RX,0,5],[GZ,4,1],[GY,0,4],[GX,0,5],[BZ,0,1],[GZ,0,4],[BX,0,6],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x1E, 6, 6, 6, 6, [[RW,0,6],[GZ,4,1],[BZ,0,1],[BZ,1,1],[BY,4,1],[GW,0,6],[GY,5,1],[BY,5,1],[BZ,2,1],[GY,4,1],[BW,0,6],[GZ,5,1],[BZ,3,1],[BZ,5,1],[BZ,4,1],[RX,0,6],[GY,0,4],[GX,0,6],[GZ,0,4],[BX,0,6],[BY,0,4],[RY,0,6],[RZ,0,6],[PD,0,5]]],
|
||||||
|
[0x03, 10, 10, 10, 10, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,10],[GX,0,10],[BX,0,10]]],
|
||||||
|
[0x07, 11, 9, 9, 9, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,9],[RW,10,1],[GX,0,9],[GW,10,1],[BX,0,9],[BW,10,1]]],
|
||||||
|
[0x0B, 12, 8, 8, 8, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,8],[RW,11,1],[RW,10,1],[GX,0,8],[GW,11,1],[GW,10,1],[BX,0,8],[BW,11,1],[BW,10,1]]],
|
||||||
|
[0x0F, 16, 4, 4, 4, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,4],[RW,15,1],[RW,14,1],[RW,13,1],[RW,12,1],[RW,11,1],[RW,10,1],[GX,0,4],[GW,15,1],[GW,14,1],[GW,13,1],[GW,12,1],[GW,11,1],[GW,10,1],[BX,0,4],[BW,15,1],[BW,14,1],[BW,13,1],[BW,12,1],[BW,11,1],[BW,10,1]]]
|
||||||
|
];
|
||||||
|
const sext = (v, n) => { const m = 1 << (n - 1); v &= (1 << n) - 1; return (v ^ m) - m; };
|
||||||
|
function bc6Unq(v, n, sgn) {
|
||||||
|
if (!sgn) {
|
||||||
|
if (n >= 15) return v;
|
||||||
|
if (v === 0) return 0;
|
||||||
|
if (v === (1 << n) - 1) return 0xFFFF;
|
||||||
|
return ((v << 16) + 0x8000) >> n;
|
||||||
|
}
|
||||||
|
if (n >= 16) return v;
|
||||||
|
const s = v < 0; if (s) v = -v;
|
||||||
|
const u = v === 0 ? 0 : v >= (1 << (n - 1)) - 1 ? 0x7FFF : ((v << 15) + 0x4000) >> (n - 1);
|
||||||
|
return s ? -u : u;
|
||||||
|
}
|
||||||
|
function bc6Finish(v, sgn) {
|
||||||
|
if (!sgn) return (v * 31) >> 6;
|
||||||
|
v = v < 0 ? -(((-v) * 31) >> 5) : (v * 31) >> 5;
|
||||||
|
return v < 0 ? 0x8000 | -v : v;
|
||||||
|
}
|
||||||
|
function bc6Block(b, o, out, sgn) {
|
||||||
|
const get = bits128(b, o);
|
||||||
|
let code = get(2);
|
||||||
|
if (code > 1) code |= get(3) << 2;
|
||||||
|
const m = BC6.findIndex((x) => x[0] === code);
|
||||||
|
if (m < 0) { out.fill(0, 0, 48); return; }
|
||||||
|
const [, W, dr, dg, db, seg] = BC6[m], f = new Array(13).fill(0);
|
||||||
|
for (const [fi, lo, n] of seg) f[fi] |= get(n) << lo;
|
||||||
|
const regions = m < 10 ? 2 : 1, ne = regions * 2, dbits = [dr, dg, db], ep = [[], [], [], []];
|
||||||
|
for (let c = 0; c < 3; c++) {
|
||||||
|
for (let e = 0; e < ne; e++) ep[e][c] = f[c * 4 + e];
|
||||||
|
if (sgn) ep[0][c] = sext(ep[0][c], W);
|
||||||
|
const delta = m !== 9 && m !== 10;
|
||||||
|
for (let e = 1; e < ne; e++) {
|
||||||
|
if (delta || sgn) ep[e][c] = sext(ep[e][c], dbits[c]);
|
||||||
|
if (delta) { ep[e][c] = (ep[e][c] + ep[0][c]) & ((1 << W) - 1); if (sgn) ep[e][c] = sext(ep[e][c], W); }
|
||||||
|
}
|
||||||
|
for (let e = 0; e < ne; e++) ep[e][c] = bc6Unq(ep[e][c], W, sgn);
|
||||||
|
}
|
||||||
|
const part = f[PD], ib = regions === 2 ? 3 : 4, wt = WEIGHTS[ib];
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
const k = regions === 2 ? (PART2[part] >> i) & 1 : 0;
|
||||||
|
const anchor = i === 0 || (regions === 2 && i === ANCHOR2[part]);
|
||||||
|
const ix = get(ib - (anchor ? 1 : 0));
|
||||||
|
for (let c = 0; c < 3; c++) out[i * 3 + c] = bc6Finish(interp(ep[2 * k][c], ep[2 * k + 1][c], wt[ix]), sgn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* decodeBlocks(format, blocks, w, h) → RGBA Uint8Array (LDR) or RGB
|
||||||
|
* half-bits Uint16Array (BC6H); null for ETC2 / EAC / ASTC. */
|
||||||
|
function decodeBlocks(format, blocks, w, h) {
|
||||||
|
if (format >= 0x50 || !BLOCK_FORMATS[format]) return null;
|
||||||
|
const S = BLOCK_FORMATS[format][2], nbx = Math.ceil(w / 4), nby = Math.ceil(h / 4);
|
||||||
|
const hdr = format === 0x47 || format === 0x48;
|
||||||
|
const out = hdr ? new Uint16Array(w * h * 3) : new Uint8Array(w * h * 4);
|
||||||
|
const px = new Int32Array(64), hx = new Int32Array(48);
|
||||||
|
let o = 0;
|
||||||
|
for (let by = 0; by < nby; by++)
|
||||||
|
for (let bx = 0; bx < nbx; bx++, o += S) {
|
||||||
|
switch (format) {
|
||||||
|
case 0x40: bc1Block(blocks, o, px, false); break;
|
||||||
|
case 0x41:
|
||||||
|
bc1Block(blocks, o + 8, px, true);
|
||||||
|
for (let i = 0; i < 16; i++) px[i * 4 + 3] = ((blocks[o + (i >> 1)] >> (4 * (i & 1))) & 15) * 17;
|
||||||
|
break;
|
||||||
|
case 0x42: bc1Block(blocks, o + 8, px, true); bc4Block(blocks, o, px, 3, false); break;
|
||||||
|
case 0x43: case 0x44:
|
||||||
|
px.fill(0); bc4Block(blocks, o, px, 0, format === 0x44);
|
||||||
|
for (let i = 0; i < 16; i++) px[i * 4 + 3] = 255;
|
||||||
|
break;
|
||||||
|
case 0x45: case 0x46:
|
||||||
|
px.fill(0); bc4Block(blocks, o, px, 0, format === 0x46); bc4Block(blocks, o + 8, px, 1, format === 0x46);
|
||||||
|
for (let i = 0; i < 16; i++) px[i * 4 + 3] = 255;
|
||||||
|
break;
|
||||||
|
case 0x49: bc7Block(blocks, o, px); break;
|
||||||
|
default: bc6Block(blocks, o, hx, format === 0x48);
|
||||||
|
}
|
||||||
|
for (let y = 0; y < 4 && by * 4 + y < h; y++)
|
||||||
|
for (let x = 0; x < 4 && bx * 4 + x < w; x++) {
|
||||||
|
const d = (by * 4 + y) * w + bx * 4 + x, i = y * 4 + x;
|
||||||
|
if (hdr) for (let c = 0; c < 3; c++) out[d * 3 + c] = hx[i * 3 + c];
|
||||||
|
else for (let c = 0; c < 4; c++) out[d * 4 + c] = px[i * 4 + c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function halfToFloat(h) {
|
||||||
|
const s = h & 0x8000 ? -1 : 1, e = (h >> 10) & 31, m = h & 0x3FF;
|
||||||
|
if (e === 0) return s * m * Math.pow(2, -24);
|
||||||
|
if (e === 31) return m ? NaN : s * Infinity;
|
||||||
|
return s * (1 + m / 1024) * Math.pow(2, e - 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
const linToSrgb = (v) => !(v > 0) ? 0 : v >= 1 ? 255
|
||||||
|
: Math.round(255 * (v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - 0.055));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* textureToRGBA(tex, surface = 0, slice = 0) → { width, height,
|
||||||
|
* rgba: Uint8ClampedArray } for display: gray expanded, 16-bit rounded,
|
||||||
|
* half / float clamped and sRGB encoded, GPU blocks decoded. Throws for
|
||||||
|
* ETC2 / EAC / ASTC.
|
||||||
|
*/
|
||||||
|
function textureToRGBA(tex, surface, slice) {
|
||||||
|
surface = surface || 0; slice = slice || 0;
|
||||||
|
const m = surface % tex.mipLevels, [w, h, d] = mipSize(tex, m), n = w * h;
|
||||||
|
const data = tex.surfaces[surface], rgba = new Uint8ClampedArray(n * 4);
|
||||||
|
let src, ch = tex.channels, kind = tex.payload === 1 ? 'u8' : ['u8', 'u16', 'f16', 'f32'][tex.sample];
|
||||||
|
if (tex.payload === 1) {
|
||||||
|
const per = data.length / d, blk = decodeBlocks(tex.format, data.subarray(per * slice, per * (slice + 1)), w, h);
|
||||||
|
if (!blk) fail('unsupported', 'ETC2 / EAC / ASTC blocks cannot be shown');
|
||||||
|
if (blk instanceof Uint16Array) { src = blk; ch = 3; kind = 'f16'; }
|
||||||
|
else {
|
||||||
|
rgba.set(blk);
|
||||||
|
if (tex.channels === 3) for (let i = 0; i < n; i++) rgba[i * 4 + 3] = 255;
|
||||||
|
return { width: w, height: h, rgba };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
src = data.subarray(n * ch * slice, n * ch * (slice + 1));
|
||||||
|
}
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const v = [0, 0, 0, 255];
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
const x = src[i * ch + c], alpha = (ch === 2 && c === 1) || (ch === 4 && c === 3);
|
||||||
|
let q;
|
||||||
|
if (kind === 'u8') q = x;
|
||||||
|
else if (kind === 'u16') q = Math.round(x / 257);
|
||||||
|
else {
|
||||||
|
const f = kind === 'f16' ? halfToFloat(x) : x;
|
||||||
|
q = alpha ? Math.max(0, Math.min(255, Math.round(f * 255))) : linToSrgb(f);
|
||||||
|
}
|
||||||
|
v[ch <= 2 ? (c === 0 ? 0 : 3) : c] = q;
|
||||||
|
}
|
||||||
|
if (ch <= 2) v[1] = v[2] = v[0];
|
||||||
|
rgba.set(v, i * 4);
|
||||||
|
}
|
||||||
|
return { width: w, height: h, rgba };
|
||||||
|
}
|
||||||
|
|
||||||
|
function surfaceIndex(tex, frame, layer, face, mip) {
|
||||||
|
return (((frame || 0) * tex.layers + (layer || 0)) * tex.faces + (face || 0)) * tex.mipLevels + (mip || 0);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Decoder ──────────────────────────────────────────────────────── */
|
/* ── Decoder ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
|
@ -102,7 +776,7 @@
|
||||||
R[r0 + tw] = R[r0 + tw - 1];
|
R[r0 + tw] = R[r0 + tw - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeTile(b, p, end, out, imgW, tx, ty, tw, th, ch) {
|
function decodeTile(b, p, end, out, imgW, tx, ty, tw, th, ch, v2, book) {
|
||||||
if (end <= p) corrupt();
|
if (end <= p) corrupt();
|
||||||
let hdr = b[p++];
|
let hdr = b[p++];
|
||||||
if (hdr === STORED) {
|
if (hdr === STORED) {
|
||||||
|
|
@ -112,10 +786,18 @@
|
||||||
out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch);
|
out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hdr & 0xE0) corrupt(); /* reserved bits set */
|
if (hdr & (v2 ? 0x80 : 0xE0)) corrupt(); /* reserved bits set */
|
||||||
const xf = hdr & SUBGREEN;
|
if ((hdr & 0x40) && (hdr & SUBGREEN)) corrupt();
|
||||||
|
/* xf: 0 YCoCg-R, 1 subtract-green, 2 none (v2) */
|
||||||
|
const xf = (hdr & 0x40) ? 2 : (hdr & SUBGREEN) ? 1 : 0, ent = hdr & 0x20;
|
||||||
hdr &= 0x0F;
|
hdr &= 0x0F;
|
||||||
if ((hdr >> ch) || (xf && ch < 3)) corrupt();
|
if ((hdr >> ch) || (xf && ch < 3)) corrupt();
|
||||||
|
let emask = 0;
|
||||||
|
if (ent) { /* v2: entropy planes */
|
||||||
|
if (p >= end || !book) corrupt();
|
||||||
|
emask = b[p++];
|
||||||
|
if ((emask >> ch) || (emask & hdr)) corrupt();
|
||||||
|
}
|
||||||
|
|
||||||
const rowLen = TILE + 1, planeLen = RING * rowLen;
|
const rowLen = TILE + 1, planeLen = RING * rowLen;
|
||||||
const rows = new Uint8Array(4 * planeLen);
|
const rows = new Uint8Array(4 * planeLen);
|
||||||
|
|
@ -128,7 +810,8 @@
|
||||||
for (let c = 0; c < ch; c++) {
|
for (let c = 0; c < ch; c++) {
|
||||||
if (hdr & (1 << c)) continue;
|
if (hdr & (1 << c)) continue;
|
||||||
z[c] = new Uint8Array(TILE_PX + GROUP);
|
z[c] = new Uint8Array(TILE_PX + GROUP);
|
||||||
const r = unpackPlane(b, p, end, z[c], tw, th);
|
const r = (emask & (1 << c)) ? huffPlane(b, p, end, z[c], tw, th, book, xf * 16 + c)
|
||||||
|
: unpackPlane(b, p, end, z[c], tw, th);
|
||||||
p = r.p; filt[c] = r.filt;
|
p = r.p; filt[c] = r.filt;
|
||||||
}
|
}
|
||||||
if (p !== end) corrupt();
|
if (p !== end) corrupt();
|
||||||
|
|
@ -147,7 +830,11 @@
|
||||||
const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri;
|
const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri;
|
||||||
for (let x = 0; x < tw; x++, d += ch) {
|
for (let x = 0; x < tw; x++, d += ch) {
|
||||||
if (ch >= 3) {
|
if (ch >= 3) {
|
||||||
if (xf) {
|
if (xf === 2) {
|
||||||
|
out[d] = rows[P0 + x];
|
||||||
|
out[d + 1] = rows[P1 + x];
|
||||||
|
out[d + 2] = rows[P2 + x];
|
||||||
|
} else if (xf) {
|
||||||
const g = rows[P0 + x];
|
const g = rows[P0 + x];
|
||||||
out[d] = rows[P1 + x] + g - 128;
|
out[d] = rows[P1 + x] + g - 128;
|
||||||
out[d + 1] = g;
|
out[d + 1] = g;
|
||||||
|
|
@ -171,11 +858,27 @@
|
||||||
|
|
||||||
const r32 = (b, p) => (b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24)) >>> 0;
|
const r32 = (b, p) => (b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24)) >>> 0;
|
||||||
|
|
||||||
|
/* decode(bytes) → 8-bit image. Reads version 1, and version 2 files
|
||||||
|
* that hold 8-bit pixels (2D; cube face 0 / layer 0 / frame 0). Other
|
||||||
|
* textures: see decodeTexture. */
|
||||||
function decode(input) {
|
function decode(input) {
|
||||||
const b = input instanceof Uint8Array ? input : new Uint8Array(input);
|
const b = input instanceof Uint8Array ? input : new Uint8Array(input);
|
||||||
|
if (b.length >= 5 && b[0] === 0x59 && b[1] === 0x41 && b[2] === 0x50 && b[3] === 0x46 && b[4] === 2) {
|
||||||
|
const t = decodeTexture(b);
|
||||||
|
if (t.payload !== 0 || t.sample !== 0 || t.shape === 2)
|
||||||
|
throw new YapfError('unsupported', 'this is a YAPF 2 texture (GPU blocks, 16-bit / float or a ' +
|
||||||
|
'volume); use YAPF.decodeTexture and YAPF.textureToRGBA');
|
||||||
|
const mips = t.surfaces.slice(0, t.mipLevels);
|
||||||
|
return { width: t.width, height: t.height, channels: t.channels, gpuFormat: t.format,
|
||||||
|
flags: t.flags, mipLevels: t.mipLevels, pixels: mips[0], mips };
|
||||||
|
}
|
||||||
|
return decodeV1(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeV1(b) {
|
||||||
if (b.length < 20 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
|
if (b.length < 20 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
|
||||||
throw new Error('YAPF: not a YAPF file');
|
throw new YapfError('not-yapf', 'not a YAPF file');
|
||||||
if (b[4] !== 1) throw new Error('YAPF: unsupported format version ' + b[4]);
|
if (b[4] !== 1) throw new YapfError('version', 'unsupported format version ' + b[4]);
|
||||||
const ch = b[5], mipLevels = b[7], W = r32(b, 12), H = r32(b, 16);
|
const ch = b[5], mipLevels = b[7], W = r32(b, 12), H = r32(b, 16);
|
||||||
if (ch < 1 || ch > 4 || mipLevels < 1 || mipLevels > MAX_MIPS ||
|
if (ch < 1 || ch > 4 || mipLevels < 1 || mipLevels > MAX_MIPS ||
|
||||||
W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt();
|
W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt();
|
||||||
|
|
@ -459,5 +1162,7 @@
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { decode, encode, toRGBA, GPU, FLAG, VERSION: 1 };
|
return { decode, encode, toRGBA, decodeTexture, textureToRGBA, decodeBlocks, surfaceIndex,
|
||||||
|
checksum: (b) => checksum(b, 0, b.length), YapfError, GPU, FLAG, NEED,
|
||||||
|
VERSION: 1 /* written by encode */, READS: [1, 2] };
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -23,3 +23,17 @@ Without the library it falls back to a pure-Python decoder that needs
|
||||||
tells you which one is in use.
|
tells you which one is in use.
|
||||||
|
|
||||||
Example: `YAPF_LIBRARY=../../build/libyapf.so python3 example.py ../../other/YAPF.YAPF`
|
Example: `YAPF_LIBRARY=../../build/libyapf.so python3 example.py ../../other/YAPF.YAPF`
|
||||||
|
|
||||||
|
## Textures (YAPF 2)
|
||||||
|
|
||||||
|
```python
|
||||||
|
tex = yapf.load_texture("albedo.yapf") # any v1 / v2 file, needs the C library
|
||||||
|
tex.payload, tex.format, tex.sample, tex.channels, tex.mip_levels
|
||||||
|
tex.shape, tex.layers, tex.frames, tex.channel_map, tex.normal, tex.notes
|
||||||
|
raw = tex.surface(frame=0, layer=0, face=0, mip=0) # bytes as stored (GPU blocks / samples)
|
||||||
|
rgba = tex.rgba_float() # numpy float32 (h, w, 4) for display
|
||||||
|
```
|
||||||
|
|
||||||
|
`yapf.load` / `yapf.decode` still return 8-bit images; on a texture they
|
||||||
|
cannot represent they raise `YapfError` saying so. Errors name the
|
||||||
|
reason (damaged, truncated, not YAPF, newer version).
|
||||||
|
|
|
||||||
BIN
bindings/python/gradient.yapf
Normal file
BIN
bindings/python/gradient.yapf
Normal file
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
yapf.py — Python bindings for YAPF v1 images.
|
yapf.py — Python bindings for YAPF images (v1) and textures (v2).
|
||||||
|
|
||||||
Copyright 2026 Nexoniarz — Apache License 2.0.
|
Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
|
||||||
|
|
@ -8,6 +8,11 @@ Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
img.width, img.height, img.channels # pixels: bytes, row-major
|
img.width, img.height, img.channels # pixels: bytes, row-major
|
||||||
yapf.save("out.yapf", img) # or yapf.encode(img) -> bytes
|
yapf.save("out.yapf", img) # or yapf.encode(img) -> bytes
|
||||||
|
|
||||||
|
tex = yapf.load_texture("albedo.yapf") # any v1 / v2 file (C library)
|
||||||
|
tex.payload, tex.format, tex.sample # GPU blocks, 16-bit, float, ...
|
||||||
|
raw = tex.surface(mip=0) # bytes as stored (blocks or samples)
|
||||||
|
rgba = tex.rgba_float() # numpy float32 (h, w, 4) for display
|
||||||
|
|
||||||
Uses the C library through ctypes when one can be found (fast, can
|
Uses the C library through ctypes when one can be found (fast, can
|
||||||
encode); otherwise falls back to a pure-Python decoder that needs numpy
|
encode); otherwise falls back to a pure-Python decoder that needs numpy
|
||||||
(decode only). The library is looked up in this order:
|
(decode only). The library is looked up in this order:
|
||||||
|
|
@ -26,12 +31,22 @@ import platform
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
__all__ = ["Image", "load", "save", "decode", "encode", "backend",
|
__all__ = ["Image", "Texture", "load", "save", "decode", "encode", "backend",
|
||||||
|
"load_texture", "decode_texture",
|
||||||
"GPU_RGBA8", "GPU_RGB8", "GPU_RG8", "GPU_R8", "GPU_SRGB8_A8", "GPU_SRGB8",
|
"GPU_RGBA8", "GPU_RGB8", "GPU_RG8", "GPU_R8", "GPU_SRGB8_A8", "GPU_SRGB8",
|
||||||
"FLAG_PREMULT_ALPHA", "FLAG_SRGB", "YapfError"]
|
"FLAG_PREMULT_ALPHA", "FLAG_SRGB", "YapfError"]
|
||||||
|
|
||||||
GPU_RGBA8, GPU_RGB8, GPU_RG8, GPU_R8, GPU_SRGB8_A8, GPU_SRGB8 = range(6)
|
GPU_RGBA8, GPU_RGB8, GPU_RG8, GPU_R8, GPU_SRGB8_A8, GPU_SRGB8 = range(6)
|
||||||
FLAG_PREMULT_ALPHA, FLAG_SRGB = 1, 2
|
GPU_BC1, GPU_BC2, GPU_BC3, GPU_BC4, GPU_BC4_SNORM, GPU_BC5, GPU_BC5_SNORM = range(0x40, 0x47)
|
||||||
|
GPU_BC6H, GPU_BC6H_SF, GPU_BC7 = 0x47, 0x48, 0x49
|
||||||
|
FLAG_PREMULT_ALPHA, FLAG_SRGB, FLAG_COVERAGE = 1, 2, 4
|
||||||
|
PAYLOAD_PIXELS, PAYLOAD_BLOCKS = 0, 1
|
||||||
|
SAMPLE_U8, SAMPLE_U16, SAMPLE_F16, SAMPLE_F32 = 0, 1, 2, 3
|
||||||
|
SHAPE_2D, SHAPE_CUBE, SHAPE_VOLUME = 0, 1, 2
|
||||||
|
|
||||||
|
_ERRORS = {-1: "invalid argument", -2: "cannot read the file", -3: "out of memory",
|
||||||
|
-4: "corrupt or not a YAPF file", -5: "needs a feature this library does not have",
|
||||||
|
-6: "damaged: a checksum does not match", -7: "the file is truncated"}
|
||||||
|
|
||||||
|
|
||||||
class YapfError(Exception):
|
class YapfError(Exception):
|
||||||
|
|
@ -70,6 +85,26 @@ class _CImage(ctypes.Structure):
|
||||||
("mips", ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)))]
|
("mips", ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)))]
|
||||||
|
|
||||||
|
|
||||||
|
class _CNote(ctypes.Structure):
|
||||||
|
_fields_ = [("tag", ctypes.c_char * 4), ("size", ctypes.c_uint32),
|
||||||
|
("data", ctypes.POINTER(ctypes.c_uint8))]
|
||||||
|
|
||||||
|
|
||||||
|
class _CTexture(ctypes.Structure):
|
||||||
|
_fields_ = [("width", ctypes.c_uint32), ("height", ctypes.c_uint32), ("depth", ctypes.c_uint32),
|
||||||
|
("layers", ctypes.c_uint16), ("frames", ctypes.c_uint16),
|
||||||
|
("shape", ctypes.c_uint8), ("mip_levels", ctypes.c_uint8),
|
||||||
|
("payload", ctypes.c_uint8), ("format", ctypes.c_uint8),
|
||||||
|
("sample", ctypes.c_uint8), ("channels", ctypes.c_uint8), ("flags", ctypes.c_uint8),
|
||||||
|
("channel_map", ctypes.c_uint8 * 4), ("linear_mask", ctypes.c_uint8),
|
||||||
|
("normal", ctypes.c_uint8), ("primaries", ctypes.c_uint8), ("transfer", ctypes.c_uint8),
|
||||||
|
("mip_filter", ctypes.c_uint8), ("alpha_ref", ctypes.c_uint8),
|
||||||
|
("loop_count", ctypes.c_uint16),
|
||||||
|
("frame_us", ctypes.POINTER(ctypes.c_uint32)),
|
||||||
|
("note_count", ctypes.c_uint32), ("notes", ctypes.POINTER(_CNote)),
|
||||||
|
("data", ctypes.POINTER(ctypes.c_void_p)), ("internal_", ctypes.c_void_p)]
|
||||||
|
|
||||||
|
|
||||||
def _platform_dir():
|
def _platform_dir():
|
||||||
machine = platform.machine().lower()
|
machine = platform.machine().lower()
|
||||||
arch = "arm64" if machine in ("arm64", "aarch64") else "x64"
|
arch = "arm64" if machine in ("arm64", "aarch64") else "x64"
|
||||||
|
|
@ -97,6 +132,20 @@ def _find_library():
|
||||||
lib.yapf_encode.argtypes = [ctypes.POINTER(_CImage), ctypes.POINTER(ctypes.c_void_p),
|
lib.yapf_encode.argtypes = [ctypes.POINTER(_CImage), ctypes.POINTER(ctypes.c_void_p),
|
||||||
ctypes.POINTER(ctypes.c_size_t)]
|
ctypes.POINTER(ctypes.c_size_t)]
|
||||||
lib.yapf_free_buffer.argtypes = [ctypes.c_void_p]
|
lib.yapf_free_buffer.argtypes = [ctypes.c_void_p]
|
||||||
|
try: # version 2 (texture) API
|
||||||
|
T = ctypes.POINTER(_CTexture)
|
||||||
|
lib.yapf_tex_load_memory.restype = T
|
||||||
|
lib.yapf_tex_load_memory.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int,
|
||||||
|
ctypes.c_int, ctypes.POINTER(ctypes.c_int)]
|
||||||
|
lib.yapf_tex_free.argtypes = [T]
|
||||||
|
lib.yapf_tex_surface_bytes.restype = ctypes.c_size_t
|
||||||
|
lib.yapf_tex_surface_bytes.argtypes = [T, ctypes.c_uint32]
|
||||||
|
lib.yapf_tex_to_rgba8.argtypes = [T, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_void_p]
|
||||||
|
lib.yapf_blocks_decode.argtypes = [ctypes.c_uint8, ctypes.c_void_p, ctypes.c_uint32,
|
||||||
|
ctypes.c_uint32, ctypes.c_void_p]
|
||||||
|
lib.has_textures = True
|
||||||
|
except AttributeError: # an older library: v1 only
|
||||||
|
lib.has_textures = False
|
||||||
return lib
|
return lib
|
||||||
except (OSError, AttributeError):
|
except (OSError, AttributeError):
|
||||||
continue
|
continue
|
||||||
|
|
@ -115,7 +164,7 @@ def _c_decode(data):
|
||||||
data = bytes(data)
|
data = bytes(data)
|
||||||
p = _lib.yapf_load_memory_mt(data, len(data), 0)
|
p = _lib.yapf_load_memory_mt(data, len(data), 0)
|
||||||
if not p:
|
if not p:
|
||||||
raise YapfError("not a valid YAPF v1 file")
|
raise YapfError(_why(data))
|
||||||
try:
|
try:
|
||||||
c = p.contents
|
c = p.contents
|
||||||
mips = []
|
mips = []
|
||||||
|
|
@ -157,6 +206,9 @@ def _py_decode(data):
|
||||||
b = np.frombuffer(bytes(data), dtype=np.uint8)
|
b = np.frombuffer(bytes(data), dtype=np.uint8)
|
||||||
if len(b) < 20 or bytes(b[:4]) != b"YAPF":
|
if len(b) < 20 or bytes(b[:4]) != b"YAPF":
|
||||||
raise YapfError("not a YAPF file")
|
raise YapfError("not a YAPF file")
|
||||||
|
if b[4] == 2:
|
||||||
|
raise YapfError("this is a YAPF 2 file; reading it needs the YAPF native library, "
|
||||||
|
"which was not found (the pure-Python decoder reads version 1 only)")
|
||||||
if b[4] != 1:
|
if b[4] != 1:
|
||||||
raise YapfError("unsupported YAPF version %d" % b[4])
|
raise YapfError("unsupported YAPF version %d" % b[4])
|
||||||
ch, gpu, mip_levels, flags = int(b[5]), int(b[6]), int(b[7]), int(b[8])
|
ch, gpu, mip_levels, flags = int(b[5]), int(b[6]), int(b[7]), int(b[8])
|
||||||
|
|
@ -275,6 +327,127 @@ def _py_tile(np, b, p, end, tw, th, ch, bad):
|
||||||
return np.stack(out, axis=-1).astype(np.uint8)
|
return np.stack(out, axis=-1).astype(np.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
# ── textures (version 2, C library) ──────────────────────────────────
|
||||||
|
|
||||||
|
def _why(data):
|
||||||
|
"""Why `decode` could not read data, in words."""
|
||||||
|
if len(data) < 5 or data[:4] != b"YAPF":
|
||||||
|
return "not a YAPF file"
|
||||||
|
if data[4] not in (1, 2):
|
||||||
|
return "YAPF format version %d is newer than this library" % data[4]
|
||||||
|
if _lib and getattr(_lib, "has_textures", False):
|
||||||
|
err = ctypes.c_int(0)
|
||||||
|
p = _lib.yapf_tex_load_memory(data, len(data), 0, 1, ctypes.byref(err))
|
||||||
|
if p:
|
||||||
|
_lib.yapf_tex_free(p)
|
||||||
|
return ("this is a YAPF 2 texture (GPU blocks, 16-bit / float or a volume) "
|
||||||
|
"that yapf.decode cannot return as 8-bit pixels; use yapf.load_texture")
|
||||||
|
return _ERRORS.get(err.value, "error %d" % err.value)
|
||||||
|
if data[4] == 2:
|
||||||
|
return "YAPF 2 files need the YAPF native library (not found)"
|
||||||
|
return "corrupt YAPF file"
|
||||||
|
|
||||||
|
|
||||||
|
class Texture:
|
||||||
|
"""A decoded YAPF texture (version 1 or 2). Description fields mirror
|
||||||
|
yapf_texture_t; `surfaces[s]` holds the bytes of surface s as stored
|
||||||
|
(GPU blocks, or samples in native byte order), numbered
|
||||||
|
s = ((frame * layers + layer) * faces + face) * mip_levels + mip."""
|
||||||
|
|
||||||
|
def __init__(self, c, lib):
|
||||||
|
for f, _ in _CTexture._fields_:
|
||||||
|
if f not in ("frame_us", "notes", "data", "internal_", "channel_map", "note_count"):
|
||||||
|
setattr(self, f, getattr(c, f))
|
||||||
|
self.channel_map = list(c.channel_map)
|
||||||
|
self.faces = 6 if c.shape == SHAPE_CUBE else 1
|
||||||
|
self.frame_us = [c.frame_us[i] for i in range(c.frames)] if c.frame_us else []
|
||||||
|
self.notes = [(c.notes[i].tag.decode("latin-1"), ctypes.string_at(c.notes[i].data, c.notes[i].size))
|
||||||
|
for i in range(c.note_count)]
|
||||||
|
count = c.frames * c.layers * self.faces * c.mip_levels
|
||||||
|
self.surfaces = [ctypes.string_at(c.data[s], lib.yapf_tex_surface_bytes(ctypes.byref(c), s % c.mip_levels))
|
||||||
|
for s in range(count)]
|
||||||
|
|
||||||
|
def surface_index(self, frame=0, layer=0, face=0, mip=0):
|
||||||
|
return ((frame * self.layers + layer) * self.faces + face) * self.mip_levels + mip
|
||||||
|
|
||||||
|
def surface(self, frame=0, layer=0, face=0, mip=0):
|
||||||
|
return self.surfaces[self.surface_index(frame, layer, face, mip)]
|
||||||
|
|
||||||
|
def mip_size(self, mip=0):
|
||||||
|
d = max(1, self.depth >> mip) if self.shape == SHAPE_VOLUME else 1
|
||||||
|
return max(1, self.width >> mip), max(1, self.height >> mip), d
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_hdr(self):
|
||||||
|
"""Float / half samples or BC6H blocks: values may go above 1."""
|
||||||
|
if self.payload == PAYLOAD_BLOCKS:
|
||||||
|
return self.format in (GPU_BC6H, GPU_BC6H_SF)
|
||||||
|
return self.sample in (SAMPLE_F16, SAMPLE_F32)
|
||||||
|
|
||||||
|
def rgba_float(self, frame=0, layer=0, face=0, mip=0, slice=0):
|
||||||
|
"""One image as numpy float32 (height, width, 4), rows top to bottom:
|
||||||
|
8 / 16-bit scaled to 0..1 (still in their stored colour space),
|
||||||
|
floats as stored, GPU blocks decoded. Needs numpy."""
|
||||||
|
import numpy as np
|
||||||
|
w, h, d = self.mip_size(mip)
|
||||||
|
raw = self.surface(frame, layer, face, mip)
|
||||||
|
raw = raw[len(raw) // d * slice:len(raw) // d * (slice + 1)]
|
||||||
|
out = np.ones((h, w, 4), dtype=np.float32)
|
||||||
|
if self.payload == PAYLOAD_BLOCKS:
|
||||||
|
hdr = self.format in (GPU_BC6H, GPU_BC6H_SF)
|
||||||
|
buf = ctypes.create_string_buffer(w * h * (6 if hdr else 4))
|
||||||
|
rc = _lib.yapf_blocks_decode(self.format, raw, w, h, buf)
|
||||||
|
if rc != 0:
|
||||||
|
raise YapfError("GPU block format 0x%02x cannot be decoded here" % self.format)
|
||||||
|
if hdr:
|
||||||
|
out[..., :3] = np.frombuffer(buf.raw, dtype=np.float16).reshape(h, w, 3)
|
||||||
|
else:
|
||||||
|
out[...] = np.frombuffer(buf.raw, dtype=np.uint8).reshape(h, w, 4) / 255.0
|
||||||
|
if self.channels == 3:
|
||||||
|
out[..., 3] = 1.0
|
||||||
|
return out
|
||||||
|
dt = {SAMPLE_U8: np.uint8, SAMPLE_U16: np.uint16, SAMPLE_F16: np.float16, SAMPLE_F32: np.float32}[self.sample]
|
||||||
|
ch = self.channels
|
||||||
|
src = np.frombuffer(raw, dtype=dt).reshape(h, w, ch).astype(np.float32)
|
||||||
|
if self.sample == SAMPLE_U8:
|
||||||
|
src /= 255.0
|
||||||
|
elif self.sample == SAMPLE_U16:
|
||||||
|
src /= 65535.0
|
||||||
|
out[..., :3] = src[..., :3] if ch >= 3 else src[..., :1]
|
||||||
|
if ch in (2, 4):
|
||||||
|
out[..., 3] = src[..., ch - 1]
|
||||||
|
return out
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
w, h, d = self.mip_size(0)
|
||||||
|
return "<yapf.Texture %dx%d%s, %d mip(s), %s>" % (
|
||||||
|
w, h, "x%d" % d if d > 1 else "", self.mip_levels,
|
||||||
|
"blocks 0x%02x" % self.format if self.payload == PAYLOAD_BLOCKS
|
||||||
|
else "%d ch %s" % (self.channels, ["u8", "u16", "f16", "f32"][self.sample]))
|
||||||
|
|
||||||
|
|
||||||
|
def decode_texture(data, verify=True):
|
||||||
|
"""Decode any YAPF file (v1 or v2) into a Texture. Needs the C library."""
|
||||||
|
if not (_lib and getattr(_lib, "has_textures", False)):
|
||||||
|
raise YapfError("YAPF textures need the YAPF native library (version 2), which was not found")
|
||||||
|
data = bytes(data)
|
||||||
|
err = ctypes.c_int(0)
|
||||||
|
p = _lib.yapf_tex_load_memory(data, len(data), 0 if verify else 1, 0, ctypes.byref(err))
|
||||||
|
if not p:
|
||||||
|
raise YapfError(_why(data) if len(data) < 5 or data[:4] != b"YAPF"
|
||||||
|
else _ERRORS.get(err.value, "error %d" % err.value))
|
||||||
|
try:
|
||||||
|
return Texture(p.contents, _lib)
|
||||||
|
finally:
|
||||||
|
_lib.yapf_tex_free(p)
|
||||||
|
|
||||||
|
|
||||||
|
def load_texture(path, verify=True):
|
||||||
|
"""Read and decode any .yapf file into a Texture."""
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
return decode_texture(f.read(), verify)
|
||||||
|
|
||||||
|
|
||||||
# ── public API ────────────────────────────────────────────────────────
|
# ── public API ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def decode(data):
|
def decode(data):
|
||||||
|
|
|
||||||
39
default.nix
Normal file
39
default.nix
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# YAPF for Nix / NixOS: the `yapf` tool, libyapf (static + shared) and
|
||||||
|
# yapf.h, plus the image/x-yapf MIME type and a freedesktop thumbnailer.
|
||||||
|
#
|
||||||
|
# nix-build build ./result
|
||||||
|
# nix-env -if . install for your user
|
||||||
|
# NixOS: environment.systemPackages = [ (pkgs.callPackage /path/to/YAPF { }) ];
|
||||||
|
{ pkgs ? import <nixpkgs> { }, lib ? pkgs.lib, stdenv ? pkgs.stdenv }:
|
||||||
|
|
||||||
|
stdenv.mkDerivation {
|
||||||
|
pname = "yapf";
|
||||||
|
version = "2.0";
|
||||||
|
|
||||||
|
src = lib.cleanSourceWith {
|
||||||
|
src = ./.;
|
||||||
|
filter = path: type:
|
||||||
|
let base = baseNameOf path; in
|
||||||
|
!(lib.elem base [ "build" "dist" "result" ".git" "node_modules" ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
makeFlags = [ "PREFIX=$(out)" ];
|
||||||
|
doCheck = true;
|
||||||
|
checkTarget = "test";
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
install -Dm644 extensions/linux/yapf-mime.xml $out/share/mime/packages/yapf.xml
|
||||||
|
install -Dm644 extensions/linux/yapf.thumbnailer $out/share/thumbnailers/yapf.thumbnailer
|
||||||
|
substituteInPlace $out/share/thumbnailers/yapf.thumbnailer \
|
||||||
|
--replace-fail "TryExec=yapf" "TryExec=$out/bin/yapf" \
|
||||||
|
--replace-fail "Exec=yapf " "Exec=$out/bin/yapf "
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "YAPF: fast lossless images and GPU-ready textures";
|
||||||
|
homepage = "https://github.com/Nexoniarz/YAPF";
|
||||||
|
license = lib.licenses.asl20;
|
||||||
|
platforms = lib.platforms.unix;
|
||||||
|
mainProgram = "yapf";
|
||||||
|
};
|
||||||
|
}
|
||||||
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,15 @@ 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
|
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.
|
image and checks that the result is byte-identical to the original file.
|
||||||
|
|
||||||
|
**YAPF 2 textures.** Most 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. Python (`yapf.load_texture`) and JavaScript (`YAPF.decodeTexture`)
|
||||||
|
read every version 2 texture; the JavaScript port writes version 1.
|
||||||
|
|
||||||
## Getting the native library
|
## Getting the native library
|
||||||
|
|
||||||
Bindings other than C, C++, Rust, Go, Zig and JavaScript load the shared
|
Bindings other than C, C++, Rust, Go, Zig and JavaScript load the shared
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,14 @@ Blender 5.1).
|
||||||
Blender cannot read `.yapf` on its own, so imported images are **packed into
|
Blender cannot read `.yapf` on its own, so imported images are **packed into
|
||||||
the .blend** and survive saving and reopening.
|
the .blend** and survive saving and reopening.
|
||||||
|
|
||||||
|
Every YAPF file opens, version 1 and 2: 8-bit images; GPU blocks (BC1–BC7,
|
||||||
|
BC6H) decoded; 16-bit, half and float textures as **float images** (HDR
|
||||||
|
values above 1 are kept, packed as EXR); for cubes, arrays and animations
|
||||||
|
the first image. Normal maps follow Blender's OpenGL convention: DirectX
|
||||||
|
normals get their green flipped, and X/Y-only normals (such as BC5) get Z
|
||||||
|
rebuilt. Version 2 files need the bundled native library; the pure-Python
|
||||||
|
fallback reads version 1 only. Tested in Blender 4.4.3 (nixpkgs).
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
1. Get `yapf_image-blender.zip` from the
|
1. Get `yapf_image-blender.zip` from the
|
||||||
|
|
|
||||||
|
|
@ -33,30 +33,61 @@ from . import yapf
|
||||||
|
|
||||||
# ── conversion helpers ────────────────────────────────────────────────
|
# ── conversion helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
def load_image(filepath):
|
def _set_colorspace(bimg, names):
|
||||||
"""Read a .yapf file into a packed bpy.types.Image."""
|
for n in names: # names differ between versions
|
||||||
img = yapf.load(filepath)
|
try:
|
||||||
|
bimg.colorspace_settings.name = n
|
||||||
|
return
|
||||||
|
except TypeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
def _read(filepath):
|
||||||
|
"""(rgba float32 h×w×4, has_alpha, flags, float_data, texture or None)."""
|
||||||
|
if yapf.backend() == "c" and getattr(yapf._lib, "has_textures", False):
|
||||||
|
tex = yapf.load_texture(filepath) # every v1 / v2 file
|
||||||
|
rgba = tex.rgba_float() # frame 0, layer 0, face +X, mip 0
|
||||||
|
if tex.normal & 3 == 2: # DirectX (-Y) → Blender's OpenGL (+Y)
|
||||||
|
rgba[..., 1] = 1.0 - rgba[..., 1]
|
||||||
|
if tex.normal & 4 or (tex.normal and tex.channels == 2): # XY: rebuild Z
|
||||||
|
x, y = rgba[..., 0] * 2.0 - 1.0, rgba[..., 1] * 2.0 - 1.0
|
||||||
|
rgba[..., 2] = (np.sqrt(np.clip(1.0 - x * x - y * y, 0.0, 1.0)) + 1.0) * 0.5
|
||||||
|
alpha = tex.channels in (2, 4) and not tex.normal
|
||||||
|
wide = tex.is_hdr or (tex.payload == yapf.PAYLOAD_PIXELS and tex.sample == yapf.SAMPLE_U16)
|
||||||
|
return rgba, alpha, tex.flags, wide, tex
|
||||||
|
img = yapf.load(filepath) # pure-Python decoder: v1 only
|
||||||
w, h, ch = img.width, img.height, img.channels
|
w, h, ch = img.width, img.height, img.channels
|
||||||
src = np.frombuffer(img.pixels, dtype=np.uint8).reshape(h, w, ch).astype(np.float32) / 255.0
|
src = np.frombuffer(img.pixels, dtype=np.uint8).reshape(h, w, ch).astype(np.float32) / 255.0
|
||||||
rgba = np.ones((h, w, 4), dtype=np.float32)
|
rgba = np.ones((h, w, 4), dtype=np.float32)
|
||||||
if ch >= 3:
|
rgba[..., :3] = src[..., :3] if ch >= 3 else src[..., :1]
|
||||||
rgba[..., :3] = src[..., :3]
|
|
||||||
else:
|
|
||||||
rgba[..., :3] = src[..., :1]
|
|
||||||
if ch in (2, 4):
|
if ch in (2, 4):
|
||||||
rgba[..., 3] = src[..., ch - 1]
|
rgba[..., 3] = src[..., ch - 1]
|
||||||
|
return rgba, ch in (2, 4), img.flags, False, None
|
||||||
|
|
||||||
|
|
||||||
|
def load_image(filepath):
|
||||||
|
"""Read a .yapf file into a packed bpy.types.Image. 16-bit, half,
|
||||||
|
float and BC6H textures become float images (packed as EXR so nothing
|
||||||
|
is lost); GPU blocks are decoded."""
|
||||||
|
rgba, alpha, flags, wide, tex = _read(filepath)
|
||||||
|
h, w = rgba.shape[:2]
|
||||||
|
|
||||||
name = os.path.basename(filepath)
|
name = os.path.basename(filepath)
|
||||||
bimg = bpy.data.images.new(name, w, h, alpha=ch in (2, 4))
|
bimg = bpy.data.images.new(name, w, h, alpha=alpha, float_buffer=wide)
|
||||||
bimg.colorspace_settings.name = "sRGB" if img.flags & yapf.FLAG_SRGB else "Non-Color"
|
if tex is not None and tex.is_hdr:
|
||||||
if img.flags & yapf.FLAG_PREMULT_ALPHA:
|
_set_colorspace(bimg, ("Linear Rec.709", "Linear"))
|
||||||
|
elif flags & yapf.FLAG_SRGB:
|
||||||
|
_set_colorspace(bimg, ("sRGB",))
|
||||||
|
else:
|
||||||
|
_set_colorspace(bimg, ("Non-Color",))
|
||||||
|
if flags & yapf.FLAG_PREMULT_ALPHA:
|
||||||
bimg.alpha_mode = "PREMUL"
|
bimg.alpha_mode = "PREMUL"
|
||||||
bimg.pixels.foreach_set(rgba[::-1].ravel()) # Blender rows go bottom-up
|
bimg.pixels.foreach_set(rgba[::-1].ravel()) # Blender rows go bottom-up
|
||||||
bimg.file_format = "PNG"
|
bimg.file_format = "OPEN_EXR" if wide else "PNG"
|
||||||
bimg.pack() # keep it inside the .blend
|
bimg.pack() # keep it inside the .blend
|
||||||
bimg.use_fake_user = True # …even before anything uses it
|
bimg.use_fake_user = True # …even before anything uses it
|
||||||
bimg["yapf_source"] = filepath
|
bimg["yapf_source"] = filepath
|
||||||
bimg["yapf_alpha"] = ch in (2, 4)
|
bimg["yapf_alpha"] = alpha
|
||||||
return bimg
|
return bimg
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,12 @@ Both directions are pixel-exact: the plugin carries its own small PNG
|
||||||
encoder/decoder instead of going through a canvas, which would alter
|
encoder/decoder instead of going through a canvas, which would alter
|
||||||
semi-transparent pixels.
|
semi-transparent pixels.
|
||||||
|
|
||||||
|
Import reads every YAPF file, version 1 and 2. 8-bit images keep their
|
||||||
|
exact pixels; GPU blocks (BC1–BC7, BC6H), 16-bit and float textures are
|
||||||
|
converted to 8-bit for display (Figma images are 8-bit); cubes, arrays,
|
||||||
|
volumes and animations bring their first image. Damaged files are
|
||||||
|
reported as such.
|
||||||
|
|
||||||
## Install (development plugin)
|
## Install (development plugin)
|
||||||
|
|
||||||
1. Use the **Figma desktop app**.
|
1. Use the **Figma desktop app**.
|
||||||
|
|
|
||||||
|
|
@ -64,14 +64,26 @@ $('tab-import').onclick = () => tab('import');
|
||||||
$('tab-export').onclick = () => tab('export');
|
$('tab-export').onclick = () => tab('export');
|
||||||
|
|
||||||
/* Import: .yapf → pixels → exact PNG → Figma image */
|
/* Import: .yapf → pixels → exact PNG → Figma image */
|
||||||
|
/* Any YAPF file → 8-bit pixels. 8-bit images keep their exact pixels and
|
||||||
|
* channels; GPU blocks, 16-bit and float textures are converted for display
|
||||||
|
* (RGBA). Cubes, arrays, volumes and animations give their first image. */
|
||||||
|
function yapfToPixels(tex) {
|
||||||
|
const w = tex.width, h = tex.height;
|
||||||
|
if (tex.payload === 0 && tex.sample === 0)
|
||||||
|
return { width: w, height: h, channels: tex.channels,
|
||||||
|
pixels: tex.surfaces[0].subarray(0, w * h * tex.channels) };
|
||||||
|
const v = YAPF.textureToRGBA(tex, 0, 0);
|
||||||
|
return { width: w, height: h, channels: 4, pixels: new Uint8Array(v.rgba.buffer),
|
||||||
|
note: tex.payload === 1 ? 'GPU blocks decoded' : 'converted to 8-bit' };
|
||||||
|
}
|
||||||
async function importFiles(files) {
|
async function importFiles(files) {
|
||||||
const images = [];
|
const images = [];
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
try {
|
try {
|
||||||
const img = YAPF.decode(new Uint8Array(await f.arrayBuffer()));
|
const img = yapfToPixels(YAPF.decodeTexture(new Uint8Array(await f.arrayBuffer())));
|
||||||
const png = await PNG.encode(img.pixels, img.width, img.height, img.channels);
|
const png = await PNG.encode(img.pixels, img.width, img.height, img.channels);
|
||||||
images.push({ name: f.name.replace(/\.yapf$/i, ''), width: img.width, height: img.height, png });
|
images.push({ name: f.name.replace(/\.yapf$/i, ''), width: img.width, height: img.height, png });
|
||||||
status('Decoded ' + f.name + ' (' + img.width + '×' + img.height + ')');
|
status('Decoded ' + f.name + ' (' + img.width + '×' + img.height + (img.note ? ', ' + img.note : '') + ')');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
status(f.name + ': ' + e.message);
|
status(f.name + ': ' + e.message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
|
|
||||||
<script>/*
|
<script>/*
|
||||||
* yapf.js — YAPF v1 encoder / decoder in plain JavaScript.
|
* yapf.js — YAPF encoder (v1) and decoder (v1 and v2) in plain JavaScript.
|
||||||
*
|
*
|
||||||
* Copyright 2026 Nexoniarz — Apache License 2.0.
|
* Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
*
|
*
|
||||||
|
|
@ -58,9 +58,14 @@
|
||||||
* const file = YAPF.encode(img); // image → Uint8Array
|
* const file = YAPF.encode(img); // image → Uint8Array
|
||||||
* const rgba = YAPF.toRGBA(img); // any channel count → RGBA pixels
|
* const rgba = YAPF.toRGBA(img); // any channel count → RGBA pixels
|
||||||
*
|
*
|
||||||
|
* const tex = YAPF.decodeTexture(bytes); // any v1 / v2 file
|
||||||
|
* const view = YAPF.textureToRGBA(tex, surface); // { width, height, rgba }
|
||||||
|
*
|
||||||
* An image is { width, height, channels (1–4), pixels: Uint8Array,
|
* An image is { width, height, channels (1–4), pixels: Uint8Array,
|
||||||
* gpuFormat, flags, mipLevels, mips: [Uint8Array, …] }.
|
* gpuFormat, flags, mipLevels, mips: [Uint8Array, …] }.
|
||||||
* The specification is the comment at the top of yapf.h.
|
* Version 2 textures (GPU blocks, 16-bit / float, cubes, arrays, volumes,
|
||||||
|
* animation) are decoded by decodeTexture; encoding writes version 1.
|
||||||
|
* The specification: yapf.h (v1) and docs/FORMAT.md (v2).
|
||||||
*/
|
*/
|
||||||
(function (root, factory) {
|
(function (root, factory) {
|
||||||
if (typeof module === 'object' && module.exports) module.exports = factory();
|
if (typeof module === 'object' && module.exports) module.exports = factory();
|
||||||
|
|
@ -90,7 +95,676 @@
|
||||||
const filterValid = (f) => f <= F_UPAVG || (f >= F_UP && f < F_UP + 8);
|
const filterValid = (f) => f <= F_UPAVG || (f >= F_UP && f < F_UP + 8);
|
||||||
const upK = (f) => (f >= F_UP ? f - F_UP + 1 : 1);
|
const upK = (f) => (f >= F_UP ? f - F_UP + 1 : 1);
|
||||||
|
|
||||||
function corrupt() { throw new Error('YAPF: corrupt or unsupported file'); }
|
function corrupt() { throw new YapfError('corrupt', 'corrupt file'); }
|
||||||
|
|
||||||
|
/* ── Version 2: textures (decoding) ───────────────────────────────── */
|
||||||
|
|
||||||
|
const ENTROPY = 0x20, NOXFORM = 0x40, BLOCK_TILE = 16;
|
||||||
|
const HUFF_BITS = 11, HUFF_SIZE = 1 << HUFF_BITS, SLOTS = 48, CTXS = 7;
|
||||||
|
const NEED = { WIDE: 1, BLOCKS: 2, ENTROPY: 4, LAYOUT: 8, FRAMES: 16, ALL: 31 };
|
||||||
|
const r16 = (b, p) => b[p] | (b[p + 1] << 8);
|
||||||
|
const r64 = (b, p) => r32(b, p) + r32(b, p + 4) * 4294967296;
|
||||||
|
|
||||||
|
class YapfError extends Error {
|
||||||
|
constructor(code, msg) { super('YAPF: ' + msg); this.code = code; }
|
||||||
|
}
|
||||||
|
const fail = (code, msg) => { throw new YapfError(code, msg); };
|
||||||
|
const bad = () => fail('corrupt', 'corrupt file');
|
||||||
|
|
||||||
|
/* yapf-sum: 64-bit lanes kept as [hi, lo] uint32 pairs. */
|
||||||
|
const KH = 0x9E3779B9, KL = 0x7F4A7C15;
|
||||||
|
function mul64(ah, al, bh, bl) { /* (a · b) mod 2^64 */
|
||||||
|
const a0 = al & 0xFFFF, a1 = al >>> 16, b0 = bl & 0xFFFF, b1 = bl >>> 16;
|
||||||
|
const p00 = a0 * b0, p01 = a0 * b1, p10 = a1 * b0, p11 = a1 * b1;
|
||||||
|
const mid = (p00 >>> 16) + (p01 & 0xFFFF) + (p10 & 0xFFFF);
|
||||||
|
const lo = ((mid & 0xFFFF) * 65536 + (p00 & 0xFFFF)) >>> 0;
|
||||||
|
const hi = (p11 + (p01 >>> 16) + (p10 >>> 16) + Math.floor(mid / 65536) +
|
||||||
|
Math.imul(ah, bl) + Math.imul(al, bh)) >>> 0;
|
||||||
|
return [hi, lo];
|
||||||
|
}
|
||||||
|
function checksum(b, p, n) {
|
||||||
|
const lanes = [];
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const [h, l] = mul64(0, i + 1, KH, KL);
|
||||||
|
lanes.push([(h ^ Math.floor(n / 4294967296)) >>> 0, (l ^ (n >>> 0)) >>> 0]);
|
||||||
|
}
|
||||||
|
const words = Math.ceil(n / 8);
|
||||||
|
for (let j = 0; j < words; j++) {
|
||||||
|
const q = p + j * 8;
|
||||||
|
let wl, wh;
|
||||||
|
if (q + 8 <= p + n) { wl = r32(b, q); wh = r32(b, q + 4); }
|
||||||
|
else {
|
||||||
|
const t = new Uint8Array(8); t.set(b.subarray(q, p + n));
|
||||||
|
wl = r32(t, 0); wh = r32(t, 4);
|
||||||
|
}
|
||||||
|
const a = lanes[j & 3];
|
||||||
|
const [h, l] = mul64((a[0] ^ wh) >>> 0, (a[1] ^ wl) >>> 0, KH, KL);
|
||||||
|
a[0] = h; a[1] = (l ^ h) >>> 0; /* a ^= a >> 32 */
|
||||||
|
}
|
||||||
|
const rot = (x, r) => r === 32 ? [x[1], x[0]]
|
||||||
|
: [((x[0] << r) | (x[1] >>> (32 - r))) >>> 0, ((x[1] << r) | (x[0] >>> (32 - r))) >>> 0];
|
||||||
|
const r1 = rot(lanes[1], 16), r2 = rot(lanes[2], 32), r3 = rot(lanes[3], 48 - 32);
|
||||||
|
const r3b = [r3[1], r3[0]]; /* rotl 48 = rotl 16 then swap */
|
||||||
|
let hh = (lanes[0][0] ^ r1[0] ^ r2[0] ^ r3b[0]) >>> 0, hl = (lanes[0][1] ^ r1[1] ^ r2[1] ^ r3b[1]) >>> 0;
|
||||||
|
[hh, hl] = mul64(hh, hl, KH, KL);
|
||||||
|
return (hl ^ hh) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Huffman code table → lut[off .. off + 2048) (length << 8 | symbol). */
|
||||||
|
function huffTable(b, p, end, lut, off) {
|
||||||
|
if (p >= end) bad();
|
||||||
|
const n = b[p++] + 1, nbytes = (n + 1) >> 1;
|
||||||
|
if (end - p < nbytes) bad();
|
||||||
|
const len = new Uint8Array(n), count = new Uint32Array(HUFF_BITS + 2);
|
||||||
|
let kraft = 0;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const l = (b[p + (i >> 1)] >> ((i & 1) * 4)) & 15;
|
||||||
|
if (l > HUFF_BITS) bad();
|
||||||
|
len[i] = l; count[l]++;
|
||||||
|
if (l) kraft += HUFF_SIZE >> l;
|
||||||
|
}
|
||||||
|
if ((n & 1) && (b[p + (n >> 1)] >> 4)) bad();
|
||||||
|
if (kraft === 0 || kraft > HUFF_SIZE) bad();
|
||||||
|
p += nbytes;
|
||||||
|
const next = new Uint32Array(HUFF_BITS + 2);
|
||||||
|
let code = 0;
|
||||||
|
count[0] = 0;
|
||||||
|
for (let l = 1; l <= HUFF_BITS; l++) { code = (code + count[l - 1]) << 1; next[l] = code; }
|
||||||
|
for (let s = 0; s < n; s++) {
|
||||||
|
const l = len[s];
|
||||||
|
if (!l) continue;
|
||||||
|
let c = next[l]++, r = 0;
|
||||||
|
for (let i = 0; i < l; i++) { r = (r << 1) | (c & 1); c >>= 1; }
|
||||||
|
for (let k = r; k < HUFF_SIZE; k += 1 << l) lut[off + k] = (l << 8) | s;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code book → per slot one Uint16Array of 7 × 2048 entries. */
|
||||||
|
function readBook(b, p, len) {
|
||||||
|
const end = p + len;
|
||||||
|
if (len < 2) bad();
|
||||||
|
const n = r16(b, p);
|
||||||
|
p += 2;
|
||||||
|
if (n === 0 || n > SLOTS * CTXS) bad();
|
||||||
|
const book = new Array(SLOTS).fill(null), seen = new Uint8Array(SLOTS * CTXS);
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
if (end - p < 3) bad();
|
||||||
|
const slot = b[p], ctx = b[p + 1];
|
||||||
|
if (slot >= SLOTS || ctx >= CTXS || seen[slot * CTXS + ctx]) bad();
|
||||||
|
seen[slot * CTXS + ctx] = 1;
|
||||||
|
if (!book[slot]) book[slot] = new Uint16Array(CTXS * HUFF_SIZE);
|
||||||
|
p = huffTable(b, p + 2, end, book[slot], ctx * HUFF_SIZE);
|
||||||
|
}
|
||||||
|
if (p !== end) bad();
|
||||||
|
return book;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Context of a residual from the folded residuals left + above. */
|
||||||
|
const ctxOf = (s) => (s > 0) + (s > 2) + (s > 6) + (s > 14) + (s > 30) + (s > 62);
|
||||||
|
|
||||||
|
/* Entropy plane payload: filters, u16 length, context-coded residuals. */
|
||||||
|
function huffPlane(b, p, end, z, tw, th, book, slot) {
|
||||||
|
const nb = (th + BAND - 1) >> 3;
|
||||||
|
if (!book || !book[slot] || end - p < nb + 2) bad();
|
||||||
|
const filt = b.subarray(p, p + nb);
|
||||||
|
for (let i = 0; i < nb; i++) if (!filterValid(filt[i])) bad();
|
||||||
|
p += nb;
|
||||||
|
const L = r16(b, p);
|
||||||
|
p += 2;
|
||||||
|
if (end - p < L) bad();
|
||||||
|
const lut = book[slot], se = p + L;
|
||||||
|
let buf = 0, bits = 0, pos = p, used = 0, i = 0;
|
||||||
|
for (let y = 0; y < th; y++)
|
||||||
|
for (let x = 0; x < tw; x++, i++) {
|
||||||
|
while (bits <= 24) { buf |= (pos < se ? b[pos] : 0) << bits; pos++; bits += 8; }
|
||||||
|
const s = (x ? z[i - 1] : 0) + (y ? z[i - tw] : 0);
|
||||||
|
const e = lut[(ctxOf(s) << HUFF_BITS) | (buf & (HUFF_SIZE - 1))];
|
||||||
|
if (!e) bad();
|
||||||
|
const n = e >> 8;
|
||||||
|
buf >>>= n; bits -= n; used += n;
|
||||||
|
z[i] = e & 255;
|
||||||
|
}
|
||||||
|
if (Math.ceil(used / 8) !== L) bad();
|
||||||
|
z.fill(0, tw * th, tw * th + GROUP);
|
||||||
|
return { p: se, filt };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wide samples: byte planes, transforms at the sample width. */
|
||||||
|
function decodeTileWide(b, p, end, out, imgW, tx, ty, tw, th, ch, sample, book) {
|
||||||
|
const sb = sample === 3 ? 4 : 2, bits = 8 * sb, isf = sample >= 2, np = ch * sb;
|
||||||
|
const half = bits === 32 ? 0x80000000 : 0x8000;
|
||||||
|
const mod = bits === 32 ? (x) => x >>> 0 : (x) => x & 0xFFFF;
|
||||||
|
const sar1 = (v) => mod((v >>> 1) | (v & half));
|
||||||
|
if (end <= p) bad();
|
||||||
|
const hdr = b[p++];
|
||||||
|
if (hdr === STORED) {
|
||||||
|
if (end - p !== tw * th * ch * sb) bad();
|
||||||
|
for (let y = 0; y < th; y++) {
|
||||||
|
const d = ((ty + y) * imgW + tx) * ch;
|
||||||
|
for (let i = 0; i < tw * ch; i++, p += sb)
|
||||||
|
out[d + i] = sb === 2 ? r16(b, p) : r32(b, p);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hdr & 0x8F) bad();
|
||||||
|
if ((hdr & NOXFORM) && (hdr & SUBGREEN)) bad();
|
||||||
|
if (ch < 3 && (hdr & (NOXFORM | SUBGREEN))) bad();
|
||||||
|
const xf = ch < 3 ? 2 : (hdr & NOXFORM) ? 2 : (hdr & SUBGREEN) ? 1 : 0;
|
||||||
|
const tsel = ch < 3 ? 0 : xf;
|
||||||
|
if (end - p < 2) bad();
|
||||||
|
const cmask = r16(b, p);
|
||||||
|
p += 2;
|
||||||
|
if (cmask >> np) bad();
|
||||||
|
let emask = 0;
|
||||||
|
if (hdr & ENTROPY) {
|
||||||
|
if (end - p < 2 || !book) bad();
|
||||||
|
emask = r16(b, p);
|
||||||
|
p += 2;
|
||||||
|
if ((emask >> np) || (emask & cmask)) bad();
|
||||||
|
}
|
||||||
|
const RL = TILE + 1, PL = TILE * RL;
|
||||||
|
const planes = new Uint8Array(np * PL), z = new Uint8Array(TILE_PX + GROUP);
|
||||||
|
for (let pl = 0; pl < np; pl++) {
|
||||||
|
if (!(cmask & (1 << pl))) continue;
|
||||||
|
if (p >= end) bad();
|
||||||
|
planes.fill(b[p++], pl * PL, (pl + 1) * PL);
|
||||||
|
}
|
||||||
|
for (let pl = 0; pl < np; pl++) {
|
||||||
|
if (cmask & (1 << pl)) continue;
|
||||||
|
z.fill(0);
|
||||||
|
const r = (emask & (1 << pl)) ? huffPlane(b, p, end, z, tw, th, book, tsel * 16 + pl)
|
||||||
|
: unpackPlane(b, p, end, z, tw, th);
|
||||||
|
p = r.p;
|
||||||
|
const base = pl * PL;
|
||||||
|
for (let y = 0; y < th; y++) {
|
||||||
|
const f = r.filt[y >> 3], k = upK(f);
|
||||||
|
unfilterRow(f, planes, base + y * RL,
|
||||||
|
y ? planes : ZERO_ROW, y ? base + (y - 1) * RL : 0,
|
||||||
|
y >= k ? planes : ZERO_ROW, y >= k ? base + (y - k) * RL : 0,
|
||||||
|
z, y * tw, tw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p !== end) bad();
|
||||||
|
const v = [0, 0, 0, 0];
|
||||||
|
for (let y = 0; y < th; y++) {
|
||||||
|
const d0 = ((ty + y) * imgW + tx) * ch, r0 = y * RL;
|
||||||
|
for (let x = 0; x < tw; x++) {
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
let a = 0;
|
||||||
|
for (let k = 0; k < sb; k++) a = a * 256 + planes[(c * sb + k) * PL + r0 + x];
|
||||||
|
v[c] = a;
|
||||||
|
}
|
||||||
|
if (xf === 0) {
|
||||||
|
const o = mod(v[1] - half), q = mod(v[2] - half), t = mod(v[0] - sar1(q));
|
||||||
|
const g = mod(q + t), bb = mod(t - sar1(o));
|
||||||
|
v[0] = mod(bb + o); v[1] = g; v[2] = bb;
|
||||||
|
} else if (xf === 1) {
|
||||||
|
const g = v[0];
|
||||||
|
v[0] = mod(v[1] + g - half); v[2] = mod(v[2] + g - half); v[1] = g;
|
||||||
|
}
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
let s = v[c];
|
||||||
|
if (isf) s = (s & half) ? mod(s ^ half) : mod(~s);
|
||||||
|
out[d0 + x * ch + c] = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GPU block tile: raw, or packed lane by lane with the book. */
|
||||||
|
function decodeBlockTile(b, p, end, packed, out, o0, rowb, tbw, tbh, S, book) {
|
||||||
|
const n = tbw * tbh;
|
||||||
|
if (!packed) {
|
||||||
|
if (end - p !== n * S) bad();
|
||||||
|
for (let r = 0; r < tbh; r++) out.set(b.subarray(p + r * tbw * S, p + (r + 1) * tbw * S), o0 + r * rowb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!book) bad();
|
||||||
|
let buf = 0, bits = 0, pos = p, used = 0;
|
||||||
|
for (let k = 0; k < S; k++) {
|
||||||
|
const lut = book[k];
|
||||||
|
if (!lut) bad();
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
while (bits <= 24) { buf |= (pos < end ? b[pos] : 0) << bits; pos++; bits += 8; }
|
||||||
|
const e = lut[buf & (HUFF_SIZE - 1)];
|
||||||
|
if (!e) bad();
|
||||||
|
const l = e >> 8;
|
||||||
|
buf >>>= l; bits -= l; used += l;
|
||||||
|
out[o0 + Math.floor(i / tbw) * rowb + (i % tbw) * S + k] = e & 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Math.ceil(used / 8) !== end - p) bad();
|
||||||
|
}
|
||||||
|
|
||||||
|
const BLOCK_FORMATS = {
|
||||||
|
0x40: [4, 4, 8], 0x41: [4, 4, 16], 0x42: [4, 4, 16], 0x43: [4, 4, 8], 0x44: [4, 4, 8],
|
||||||
|
0x45: [4, 4, 16], 0x46: [4, 4, 16], 0x47: [4, 4, 16], 0x48: [4, 4, 16], 0x49: [4, 4, 16],
|
||||||
|
0x50: [4, 4, 8], 0x51: [4, 4, 8], 0x52: [4, 4, 16], 0x53: [4, 4, 8], 0x54: [4, 4, 8],
|
||||||
|
0x55: [4, 4, 16], 0x56: [4, 4, 16]
|
||||||
|
};
|
||||||
|
[[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]].forEach(([w, h], i) => { BLOCK_FORMATS[0x60 + i] = [w, h, 16]; });
|
||||||
|
|
||||||
|
function mipSize(t, m) {
|
||||||
|
return [Math.max(1, t.width >>> m), Math.max(1, t.height >>> m),
|
||||||
|
t.shape === 2 ? Math.max(1, t.depth >>> m) : 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function surfaceBytes(t, m) {
|
||||||
|
const [w, h, d] = mipSize(t, m);
|
||||||
|
if (t.payload === 1) {
|
||||||
|
const [bw, bh, S] = BLOCK_FORMATS[t.format];
|
||||||
|
return Math.ceil(w / bw) * Math.ceil(h / bh) * d * S;
|
||||||
|
}
|
||||||
|
return w * h * d * t.channels * (t.sample === 0 ? 1 : t.sample === 3 ? 4 : 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Decodes one surface (v2) into a typed array. */
|
||||||
|
function decodeSurface(b, t, s, verify) {
|
||||||
|
const m = s % t.mipLevels, [w, h, d] = mipSize(t, m);
|
||||||
|
const off = t._off[s], size = t._size[s], blocks = t.payload === 1;
|
||||||
|
let tcx, tcy, nbx = 0, nby = 0, S = 0;
|
||||||
|
if (blocks) {
|
||||||
|
const f = BLOCK_FORMATS[t.format];
|
||||||
|
nbx = Math.ceil(w / f[0]); nby = Math.ceil(h / f[1]); S = f[2];
|
||||||
|
tcx = Math.ceil(nbx / BLOCK_TILE); tcy = Math.ceil(nby / BLOCK_TILE);
|
||||||
|
} else {
|
||||||
|
tcx = Math.ceil(w / TILE); tcy = Math.ceil(h / TILE);
|
||||||
|
}
|
||||||
|
const tiles = tcx * tcy * d;
|
||||||
|
if (size < 4) bad();
|
||||||
|
const bl = r32(b, off);
|
||||||
|
if (bl > size - 4 || Math.floor((size - 4 - bl) / 8) < tiles) bad();
|
||||||
|
const pre = 4 + bl;
|
||||||
|
if (verify && checksum(b, off, pre + tiles * 8) !== t._tcheck[s]) fail('checksum', 'damaged file: a checksum does not match');
|
||||||
|
let book = null;
|
||||||
|
if (bl) {
|
||||||
|
if (!(t.needs & NEED.ENTROPY)) bad();
|
||||||
|
book = readBook(b, off + 4, bl);
|
||||||
|
}
|
||||||
|
const table = off + pre, end = off + size;
|
||||||
|
let pos = table + tiles * 8;
|
||||||
|
const sizes = new Array(tiles), packed = new Array(tiles), starts = new Array(tiles);
|
||||||
|
for (let i = 0; i < tiles; i++) {
|
||||||
|
let sz = r32(b, table + i * 8);
|
||||||
|
packed[i] = sz >>> 31;
|
||||||
|
sz &= 0x7FFFFFFF;
|
||||||
|
if (packed[i] && (!blocks || !book)) bad();
|
||||||
|
if (sz > end - pos) bad();
|
||||||
|
starts[i] = pos; sizes[i] = sz; pos += sz;
|
||||||
|
}
|
||||||
|
if (pos !== end) bad();
|
||||||
|
|
||||||
|
const bytes = surfaceBytes(t, m);
|
||||||
|
let out, store;
|
||||||
|
if (blocks || t.sample === 0) out = store = new Uint8Array(bytes);
|
||||||
|
else if (t.sample === 3) { store = new Uint32Array(bytes / 4); out = new Float32Array(store.buffer); }
|
||||||
|
else out = store = new Uint16Array(bytes / 2);
|
||||||
|
for (let i = 0; i < tiles; i++) {
|
||||||
|
const p = starts[i], e = p + sizes[i];
|
||||||
|
if (verify && checksum(b, p, sizes[i]) !== r32(b, table + i * 8 + 4))
|
||||||
|
fail('checksum', 'damaged file: a checksum does not match');
|
||||||
|
const row = Math.floor(i / tcx), z = Math.floor(row / tcy), ty = row % tcy, tx = i % tcx;
|
||||||
|
if (blocks) {
|
||||||
|
const bx0 = tx * BLOCK_TILE, by0 = ty * BLOCK_TILE, rowb = nbx * S;
|
||||||
|
decodeBlockTile(b, p, e, packed[i], store, z * nby * rowb + by0 * rowb + bx0 * S, rowb,
|
||||||
|
Math.min(BLOCK_TILE, nbx - bx0), Math.min(BLOCK_TILE, nby - by0), S, book);
|
||||||
|
} else {
|
||||||
|
const x0 = tx * TILE, y0 = ty * TILE, tw = Math.min(TILE, w - x0), th = Math.min(TILE, h - y0);
|
||||||
|
const slice = w * h * t.channels;
|
||||||
|
const view = store.subarray(z * slice, (z + 1) * slice);
|
||||||
|
if (t.sample === 0) decodeTile(b, p, e, view, w, x0, y0, tw, th, t.channels, true, book);
|
||||||
|
else decodeTileWide(b, p, e, view, w, x0, y0, tw, th, t.channels, t.sample, book);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* decodeTexture(bytes, { verify = true }) — any YAPF file (v1 or v2).
|
||||||
|
* Returns { version, width, height, depth, layers, frames, faces, shape,
|
||||||
|
* mipLevels, payload, format, sample, channels, flags, channelMap,
|
||||||
|
* linearMask, normal, primaries, transfer, mipFilter, alphaRef,
|
||||||
|
* loopCount, frameUs, notes: [{ tag, data }], surfaces: [...] }, where
|
||||||
|
* surfaces[s] is a Uint8Array (8-bit pixels, GPU blocks), Uint16Array
|
||||||
|
* (16-bit, half bits) or Float32Array, numbered
|
||||||
|
* s = ((frame · layers + layer) · faces + face) · mipLevels + mip.
|
||||||
|
* Errors are YapfError with .code 'not-yapf', 'version', 'unsupported',
|
||||||
|
* 'corrupt', 'checksum' or 'truncated'.
|
||||||
|
*/
|
||||||
|
function decodeTexture(input, opts) {
|
||||||
|
const b = input instanceof Uint8Array ? input : new Uint8Array(input);
|
||||||
|
const verify = !(opts && opts.verify === false);
|
||||||
|
if (b.length < 5 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
|
||||||
|
fail('not-yapf', 'not a YAPF file');
|
||||||
|
if (b[4] === 1) {
|
||||||
|
const img = decodeV1(b);
|
||||||
|
return { version: 1, width: img.width, height: img.height, depth: 1, layers: 1, frames: 1,
|
||||||
|
faces: 1, shape: 0, mipLevels: img.mipLevels, payload: 0, format: img.gpuFormat,
|
||||||
|
sample: 0, channels: img.channels, flags: img.flags, channelMap: [0, 0, 0, 0],
|
||||||
|
linearMask: 0, normal: 0, primaries: 0, transfer: 0, mipFilter: 0, alphaRef: 0,
|
||||||
|
loopCount: 0, frameUs: [], notes: [], surfaces: img.mips };
|
||||||
|
}
|
||||||
|
if (b[4] !== 2) fail('version', 'format version ' + b[4] + ' is newer than this decoder');
|
||||||
|
if (b.length < 64) fail('truncated', 'the file is truncated');
|
||||||
|
if (verify && checksum(b, 0, 0x3C) !== r32(b, 0x3C)) fail('checksum', 'damaged file: a checksum does not match');
|
||||||
|
const t = {
|
||||||
|
version: 2, channels: b[5], format: b[6], mipLevels: b[7], flags: b[8], payload: b[9],
|
||||||
|
sample: b[10], shape: b[11], width: r32(b, 0x0C), height: r32(b, 0x10), depth: r32(b, 0x14),
|
||||||
|
layers: r16(b, 0x18), frames: r16(b, 0x1A), needs: r32(b, 0x1C),
|
||||||
|
channelMap: Array.from(b.subarray(0x20, 0x24)), linearMask: b[0x24], normal: b[0x25],
|
||||||
|
primaries: b[0x26], transfer: b[0x27], mipFilter: b[0x28], alphaRef: b[0x29],
|
||||||
|
loopCount: r16(b, 0x2A), frameUs: [], notes: []
|
||||||
|
};
|
||||||
|
const notesSize = r32(b, 0x2C), fileSize = r64(b, 0x30);
|
||||||
|
if (t.needs & ~NEED.ALL) fail('unsupported', 'the file needs a feature this decoder does not have');
|
||||||
|
t.faces = t.shape === 1 ? 6 : 1;
|
||||||
|
if (t.channels < 1 || t.channels > 4 || t.sample > 3 || t.payload > 1 || t.shape > 2 ||
|
||||||
|
t.width < 1 || t.width > MAX_DIM || t.height < 1 || t.height > MAX_DIM ||
|
||||||
|
!t.layers || !t.frames || t.mipLevels < 1 || t.mipLevels > MAX_MIPS) bad();
|
||||||
|
if (t.shape === 2 ? (t.depth < 1 || t.depth > MAX_DIM || t.layers !== 1) : t.depth > 1) bad();
|
||||||
|
if (t.shape !== 2) t.depth = 1;
|
||||||
|
if (t.shape === 1 && t.width !== t.height) bad();
|
||||||
|
if (t.payload === 1 && !BLOCK_FORMATS[t.format]) bad();
|
||||||
|
if ((t.payload === 0 && t.sample !== 0 && !(t.needs & NEED.WIDE)) ||
|
||||||
|
(t.payload === 1 && !(t.needs & NEED.BLOCKS)) ||
|
||||||
|
((t.shape !== 0 || t.layers > 1) && !(t.needs & NEED.LAYOUT)) ||
|
||||||
|
(t.frames > 1 && !(t.needs & NEED.FRAMES))) bad();
|
||||||
|
const S = t.frames * t.layers * t.faces * t.mipLevels;
|
||||||
|
if (S > (1 << 24)) bad();
|
||||||
|
const front = 64 + 16 * S + (t.frames > 1 ? 4 * t.frames : 0) + notesSize;
|
||||||
|
if (front > fileSize) bad();
|
||||||
|
if (b.length < fileSize) fail('truncated', 'the file is truncated');
|
||||||
|
if (verify && checksum(b, 64, front - 64) !== r32(b, 0x38)) fail('checksum', 'damaged file: a checksum does not match');
|
||||||
|
let q = 64 + 16 * S;
|
||||||
|
for (let f = 0; t.frames > 1 && f < t.frames; f++, q += 4) t.frameUs.push(r32(b, q));
|
||||||
|
const qe = q + notesSize;
|
||||||
|
while (q < qe) {
|
||||||
|
if (qe - q < 8) bad();
|
||||||
|
const sz = r32(b, q + 4);
|
||||||
|
if (qe - q - 8 < sz) bad();
|
||||||
|
t.notes.push({ tag: String.fromCharCode(b[q], b[q + 1], b[q + 2], b[q + 3]), data: b.slice(q + 8, q + 8 + sz) });
|
||||||
|
q += 8 + sz;
|
||||||
|
}
|
||||||
|
t._off = []; t._size = []; t._tcheck = [];
|
||||||
|
for (let s = 0; s < S; s++) {
|
||||||
|
const e = 64 + 16 * s, off = r64(b, e), size = r32(b, e + 8);
|
||||||
|
if (off < front || off > fileSize || size > fileSize - off) bad();
|
||||||
|
t._off.push(off); t._size.push(size); t._tcheck.push(r32(b, e + 12));
|
||||||
|
}
|
||||||
|
t.surfaces = [];
|
||||||
|
for (let s = 0; s < S; s++) t.surfaces.push(decodeSurface(b, t, s, verify));
|
||||||
|
delete t._off; delete t._size; delete t._tcheck;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── GPU block decoding (previews) ───────────────────────────────── */
|
||||||
|
|
||||||
|
const PART2 = [0xCCCC, 0x8888, 0xEEEE, 0xECC8, 0xC880, 0xFEEC, 0xFEC8, 0xEC80, 0xC800, 0xFFEC, 0xFE80, 0xE800,
|
||||||
|
0xFFE8, 0xFF00, 0xFFF0, 0xF000, 0xF710, 0x008E, 0x7100, 0x08CE, 0x008C, 0x7310, 0x3100, 0x8CCE, 0x088C, 0x3110,
|
||||||
|
0x6666, 0x366C, 0x17E8, 0x0FF0, 0x718E, 0x399C, 0xAAAA, 0xF0F0, 0x5A5A, 0x33CC, 0x3C3C, 0x55AA, 0x9696, 0xA55A,
|
||||||
|
0x73CE, 0x13C8, 0x324C, 0x3BDC, 0x6996, 0xC33C, 0x9966, 0x0660, 0x0272, 0x04E4, 0x4E40, 0x2720, 0xC936, 0x936C,
|
||||||
|
0x39C6, 0x639C, 0x9336, 0x9CC6, 0x817E, 0xE718, 0xCCF0, 0x0FCC, 0x7744, 0xEE22];
|
||||||
|
const PART3 = [0xAA685050, 0x6A5A5040, 0x5A5A4200, 0x5450A0A8, 0xA5A50000, 0xA0A05050, 0x5555A0A0, 0x5A5A5050,
|
||||||
|
0xAA550000, 0xAA555500, 0xAAAA5500, 0x90909090, 0x94949494, 0xA4A4A4A4, 0xA9A59450, 0x2A0A4250, 0xA5945040,
|
||||||
|
0x0A425054, 0xA5A5A500, 0x55A0A0A0, 0xA8A85454, 0x6A6A4040, 0xA4A45000, 0x1A1A0500, 0x0050A4A4, 0xAAA59090,
|
||||||
|
0x14696914, 0x69691400, 0xA08585A0, 0xAA821414, 0x50A4A450, 0x6A5A0200, 0xA9A58000, 0x5090A0A8, 0xA8A09050,
|
||||||
|
0x24242424, 0x00AA5500, 0x24924924, 0x24499224, 0x50A50A50, 0x500AA550, 0xAAAA4444, 0x66660000, 0xA5A0A5A0,
|
||||||
|
0x50A050A0, 0x69286928, 0x44AAAA44, 0x66666600, 0xAA444444, 0x54A854A8, 0x95809580, 0x96969600, 0xA85454A8,
|
||||||
|
0x80959580, 0xAA141414, 0x96960000, 0xAAAA1414, 0xA05050A0, 0xA0A5A5A0, 0x96000000, 0x40804080, 0xA9A8A9A8,
|
||||||
|
0xAAAAAA44, 0x2A4A5254];
|
||||||
|
const ANCHOR2 = [15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 2, 8, 2, 2, 8, 8, 15, 2, 8, 2, 2,
|
||||||
|
8, 8, 2, 2, 15, 15, 6, 8, 2, 8, 15, 15, 2, 8, 2, 2, 2, 15, 15, 6, 6, 2, 6, 8, 15, 15, 2, 2, 15, 15, 15, 15, 15, 2, 2, 15];
|
||||||
|
const ANCHOR3A = [3, 3, 15, 15, 8, 3, 15, 15, 8, 8, 6, 6, 6, 5, 3, 3, 3, 3, 8, 15, 3, 3, 6, 10, 5, 8, 8, 6, 8, 5, 15,
|
||||||
|
15, 8, 15, 3, 5, 6, 10, 8, 15, 15, 3, 15, 5, 15, 15, 15, 15, 3, 15, 5, 5, 5, 8, 5, 10, 5, 10, 8, 13, 15, 12, 3, 3];
|
||||||
|
const ANCHOR3B = [15, 8, 8, 3, 15, 15, 3, 8, 15, 15, 15, 15, 15, 15, 15, 8, 15, 8, 15, 3, 15, 8, 15, 8, 3, 15, 6, 10,
|
||||||
|
15, 15, 10, 8, 15, 3, 15, 10, 10, 8, 9, 10, 6, 15, 8, 15, 3, 6, 6, 8, 15, 3, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||||
|
15, 3, 15, 15, 8];
|
||||||
|
const WEIGHTS = { 2: [0, 21, 43, 64], 3: [0, 9, 18, 27, 37, 46, 55, 64],
|
||||||
|
4: [0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64] };
|
||||||
|
const interp = (a, b, w) => (a * (64 - w) + b * w + 32) >> 6;
|
||||||
|
|
||||||
|
function bits128(b, o) {
|
||||||
|
let pos = 0;
|
||||||
|
return (n) => {
|
||||||
|
let v = 0;
|
||||||
|
for (let i = 0; i < n; i++, pos++) v |= ((b[o + (pos >> 3)] >> (pos & 7)) & 1) << i;
|
||||||
|
return v >>> 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgb565(c) {
|
||||||
|
const r = (c >> 11) & 31, g = (c >> 5) & 63, bl = c & 31;
|
||||||
|
return [(r << 3) | (r >> 2), (g << 2) | (g >> 4), (bl << 3) | (bl >> 2)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function bc1Block(b, o, out, fourOnly) {
|
||||||
|
const c0 = r16(b, o), c1 = r16(b, o + 2), idx = r32(b, o + 4), four = c0 > c1 || fourOnly;
|
||||||
|
const p0 = rgb565(c0), p1 = rgb565(c1), pal = [p0.concat(255), p1.concat(255), [0, 0, 0, 255], [0, 0, 0, 255]];
|
||||||
|
for (let k = 0; k < 3; k++) {
|
||||||
|
if (four) { pal[2][k] = ((2 * p0[k] + p1[k] + 1) / 3) | 0; pal[3][k] = ((p0[k] + 2 * p1[k] + 1) / 3) | 0; }
|
||||||
|
else { pal[2][k] = (p0[k] + p1[k] + 1) >> 1; pal[3][k] = 0; }
|
||||||
|
}
|
||||||
|
if (!four) pal[3][3] = 0;
|
||||||
|
for (let i = 0; i < 16; i++) { const c = pal[(idx >>> (2 * i)) & 3]; for (let k = 0; k < 4; k++) out[i * 4 + k] = c[k]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bc4Block(b, o, out, ch, snorm) {
|
||||||
|
const pal = new Array(8);
|
||||||
|
let r0 = b[o], r1 = b[o + 1];
|
||||||
|
if (snorm) { r0 = Math.max(-127, (r0 << 24) >> 24); r1 = Math.max(-127, (r1 << 24) >> 24); }
|
||||||
|
pal[0] = r0; pal[1] = r1;
|
||||||
|
const div = (v, d) => (v >= 0 ? Math.floor((v + (d >> 1)) / d) : -Math.floor((-v + (d >> 1)) / d));
|
||||||
|
if (r0 > r1) for (let i = 2; i < 8; i++) pal[i] = div((8 - i) * r0 + (i - 1) * r1, 7);
|
||||||
|
else { for (let i = 2; i < 6; i++) pal[i] = div((6 - i) * r0 + (i - 1) * r1, 5); pal[6] = snorm ? -127 : 0; pal[7] = snorm ? 127 : 255; }
|
||||||
|
let lo = r32(b, o + 2), hi = r16(b, o + 6);
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
const bit = 3 * i, v = bit < 30 ? (lo >>> bit) & 7 : bit === 30 ? ((lo >>> 30) | ((hi & 1) << 2)) : (hi >> (bit - 32)) & 7;
|
||||||
|
out[i * 4 + ch] = pal[v] + (snorm ? 128 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BC7 = [[3, 4, 0, 0, 4, 0, 1, 0, 3, 0], [2, 6, 0, 0, 6, 0, 0, 1, 3, 0], [3, 6, 0, 0, 5, 0, 0, 0, 2, 0],
|
||||||
|
[2, 6, 0, 0, 7, 0, 1, 0, 2, 0], [1, 0, 2, 1, 5, 6, 0, 0, 2, 3], [1, 0, 2, 0, 7, 8, 0, 0, 2, 2],
|
||||||
|
[1, 0, 0, 0, 7, 7, 1, 0, 4, 0], [2, 6, 0, 0, 5, 5, 1, 0, 2, 0]];
|
||||||
|
const subsetOf = (ns, part, i) => ns === 2 ? (PART2[part] >> i) & 1 : ns === 3 ? (PART3[part] >>> (2 * i)) & 3 : 0;
|
||||||
|
const isAnchor = (ns, part, i) => i === 0 || (ns === 2 && i === ANCHOR2[part]) ||
|
||||||
|
(ns === 3 && (i === ANCHOR3A[part] || i === ANCHOR3B[part]));
|
||||||
|
|
||||||
|
function bc7Block(b, o, out) {
|
||||||
|
const get = bits128(b, o);
|
||||||
|
let mode = 0;
|
||||||
|
while (mode < 8 && !get(1)) mode++;
|
||||||
|
if (mode === 8) { out.fill(0, 0, 64); return; }
|
||||||
|
const [ns, pb, rb, isbB, cb, ab, epb, spb, ib, ib2] = BC7[mode];
|
||||||
|
const part = get(pb), rot = get(rb), isb = get(isbB), ne = ns * 2;
|
||||||
|
const ep = []; for (let e = 0; e < ne; e++) ep.push([0, 0, 0, 255]);
|
||||||
|
for (let c = 0; c < 3; c++) for (let e = 0; e < ne; e++) ep[e][c] = get(cb);
|
||||||
|
for (let e = 0; e < ne; e++) ep[e][3] = ab ? get(ab) : 255;
|
||||||
|
const pbit = epb | spb;
|
||||||
|
if (epb) for (let e = 0; e < ne; e++) { const p = get(1); for (let c = 0; c < 4; c++) if (c < 3 || ab) ep[e][c] = (ep[e][c] << 1) | p; }
|
||||||
|
else if (spb) for (let k = 0; k < ns; k++) { const p = get(1); for (let c = 0; c < 3; c++) { ep[2 * k][c] = (ep[2 * k][c] << 1) | p; ep[2 * k + 1][c] = (ep[2 * k + 1][c] << 1) | p; } }
|
||||||
|
for (let e = 0; e < ne; e++) for (let c = 0; c < 4; c++) {
|
||||||
|
if (c === 3 && !ab) continue;
|
||||||
|
const nb = (c < 3 ? cb : ab) + pbit, v = ep[e][c] << (8 - nb);
|
||||||
|
ep[e][c] = v | (v >> nb);
|
||||||
|
}
|
||||||
|
const idx = [], idx2 = [];
|
||||||
|
for (let i = 0; i < 16; i++) idx.push(get(ib - (isAnchor(ns, part, i) ? 1 : 0)));
|
||||||
|
for (let i = 0; i < 16; i++) idx2.push(ib2 ? get(ib2 - (i === 0 ? 1 : 0)) : 0);
|
||||||
|
const w1 = WEIGHTS[ib], w2 = WEIGHTS[ib2 || 2];
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
const k = subsetOf(ns, part, i), A = ep[2 * k], B = ep[2 * k + 1];
|
||||||
|
let cw, aw;
|
||||||
|
if (!ib2) cw = aw = w1[idx[i]];
|
||||||
|
else if (!isb) { cw = w1[idx[i]]; aw = w2[idx2[i]]; }
|
||||||
|
else { cw = w2[idx2[i]]; aw = w1[idx[i]]; }
|
||||||
|
const px = [interp(A[0], B[0], cw), interp(A[1], B[1], cw), interp(A[2], B[2], cw), interp(A[3], B[3], aw)];
|
||||||
|
if (rot) { const t = px[3]; px[3] = px[rot - 1]; px[rot - 1] = t; }
|
||||||
|
for (let c = 0; c < 4; c++) out[i * 4 + c] = px[c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BC6H header layouts: [field, first bit, count]; fields 0–3 red w x y z,
|
||||||
|
* 4–7 green, 8–11 blue, 12 partition. */
|
||||||
|
const RW = 0, RX = 1, RY = 2, RZ = 3, GW = 4, GX = 5, GY = 6, GZ = 7, BW = 8, BX = 9, BY = 10, BZ = 11, PD = 12;
|
||||||
|
const BC6 = [
|
||||||
|
[0x00, 10, 5, 5, 5, [[GY,4,1],[BY,4,1],[BZ,4,1],[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,5],[GZ,4,1],[GY,0,4],[GX,0,5],[BZ,0,1],[GZ,0,4],[BX,0,5],[BZ,1,1],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x01, 7, 6, 6, 6, [[GY,5,1],[GZ,4,1],[GZ,5,1],[RW,0,7],[BZ,0,1],[BZ,1,1],[BY,4,1],[GW,0,7],[BY,5,1],[BZ,2,1],[GY,4,1],[BW,0,7],[BZ,3,1],[BZ,5,1],[BZ,4,1],[RX,0,6],[GY,0,4],[GX,0,6],[GZ,0,4],[BX,0,6],[BY,0,4],[RY,0,6],[RZ,0,6],[PD,0,5]]],
|
||||||
|
[0x02, 11, 5, 4, 4, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,5],[RW,10,1],[GY,0,4],[GX,0,4],[GW,10,1],[BZ,0,1],[GZ,0,4],[BX,0,4],[BW,10,1],[BZ,1,1],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x06, 11, 4, 5, 4, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,4],[RW,10,1],[GZ,4,1],[GY,0,4],[GX,0,5],[GW,10,1],[GZ,0,4],[BX,0,4],[BW,10,1],[BZ,1,1],[BY,0,4],[RY,0,4],[BZ,0,1],[BZ,2,1],[RZ,0,4],[GY,4,1],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x0A, 11, 4, 4, 5, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,4],[RW,10,1],[BY,4,1],[GY,0,4],[GX,0,4],[GW,10,1],[BZ,0,1],[GZ,0,4],[BX,0,5],[BW,10,1],[BY,0,4],[RY,0,4],[BZ,1,1],[BZ,2,1],[RZ,0,4],[BZ,4,1],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x0E, 9, 5, 5, 5, [[RW,0,9],[BY,4,1],[GW,0,9],[GY,4,1],[BW,0,9],[BZ,4,1],[RX,0,5],[GZ,4,1],[GY,0,4],[GX,0,5],[BZ,0,1],[GZ,0,4],[BX,0,5],[BZ,1,1],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x12, 8, 6, 5, 5, [[RW,0,8],[GZ,4,1],[BY,4,1],[GW,0,8],[BZ,2,1],[GY,4,1],[BW,0,8],[BZ,3,1],[BZ,4,1],[RX,0,6],[GY,0,4],[GX,0,5],[BZ,0,1],[GZ,0,4],[BX,0,5],[BZ,1,1],[BY,0,4],[RY,0,6],[RZ,0,6],[PD,0,5]]],
|
||||||
|
[0x16, 8, 5, 6, 5, [[RW,0,8],[BZ,0,1],[BY,4,1],[GW,0,8],[GY,5,1],[GY,4,1],[BW,0,8],[GZ,5,1],[BZ,4,1],[RX,0,5],[GZ,4,1],[GY,0,4],[GX,0,6],[GZ,0,4],[BX,0,5],[BZ,1,1],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x1A, 8, 5, 5, 6, [[RW,0,8],[BZ,1,1],[BY,4,1],[GW,0,8],[BY,5,1],[GY,4,1],[BW,0,8],[BZ,5,1],[BZ,4,1],[RX,0,5],[GZ,4,1],[GY,0,4],[GX,0,5],[BZ,0,1],[GZ,0,4],[BX,0,6],[BY,0,4],[RY,0,5],[BZ,2,1],[RZ,0,5],[BZ,3,1],[PD,0,5]]],
|
||||||
|
[0x1E, 6, 6, 6, 6, [[RW,0,6],[GZ,4,1],[BZ,0,1],[BZ,1,1],[BY,4,1],[GW,0,6],[GY,5,1],[BY,5,1],[BZ,2,1],[GY,4,1],[BW,0,6],[GZ,5,1],[BZ,3,1],[BZ,5,1],[BZ,4,1],[RX,0,6],[GY,0,4],[GX,0,6],[GZ,0,4],[BX,0,6],[BY,0,4],[RY,0,6],[RZ,0,6],[PD,0,5]]],
|
||||||
|
[0x03, 10, 10, 10, 10, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,10],[GX,0,10],[BX,0,10]]],
|
||||||
|
[0x07, 11, 9, 9, 9, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,9],[RW,10,1],[GX,0,9],[GW,10,1],[BX,0,9],[BW,10,1]]],
|
||||||
|
[0x0B, 12, 8, 8, 8, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,8],[RW,11,1],[RW,10,1],[GX,0,8],[GW,11,1],[GW,10,1],[BX,0,8],[BW,11,1],[BW,10,1]]],
|
||||||
|
[0x0F, 16, 4, 4, 4, [[RW,0,10],[GW,0,10],[BW,0,10],[RX,0,4],[RW,15,1],[RW,14,1],[RW,13,1],[RW,12,1],[RW,11,1],[RW,10,1],[GX,0,4],[GW,15,1],[GW,14,1],[GW,13,1],[GW,12,1],[GW,11,1],[GW,10,1],[BX,0,4],[BW,15,1],[BW,14,1],[BW,13,1],[BW,12,1],[BW,11,1],[BW,10,1]]]
|
||||||
|
];
|
||||||
|
const sext = (v, n) => { const m = 1 << (n - 1); v &= (1 << n) - 1; return (v ^ m) - m; };
|
||||||
|
function bc6Unq(v, n, sgn) {
|
||||||
|
if (!sgn) {
|
||||||
|
if (n >= 15) return v;
|
||||||
|
if (v === 0) return 0;
|
||||||
|
if (v === (1 << n) - 1) return 0xFFFF;
|
||||||
|
return ((v << 16) + 0x8000) >> n;
|
||||||
|
}
|
||||||
|
if (n >= 16) return v;
|
||||||
|
const s = v < 0; if (s) v = -v;
|
||||||
|
const u = v === 0 ? 0 : v >= (1 << (n - 1)) - 1 ? 0x7FFF : ((v << 15) + 0x4000) >> (n - 1);
|
||||||
|
return s ? -u : u;
|
||||||
|
}
|
||||||
|
function bc6Finish(v, sgn) {
|
||||||
|
if (!sgn) return (v * 31) >> 6;
|
||||||
|
v = v < 0 ? -(((-v) * 31) >> 5) : (v * 31) >> 5;
|
||||||
|
return v < 0 ? 0x8000 | -v : v;
|
||||||
|
}
|
||||||
|
function bc6Block(b, o, out, sgn) {
|
||||||
|
const get = bits128(b, o);
|
||||||
|
let code = get(2);
|
||||||
|
if (code > 1) code |= get(3) << 2;
|
||||||
|
const m = BC6.findIndex((x) => x[0] === code);
|
||||||
|
if (m < 0) { out.fill(0, 0, 48); return; }
|
||||||
|
const [, W, dr, dg, db, seg] = BC6[m], f = new Array(13).fill(0);
|
||||||
|
for (const [fi, lo, n] of seg) f[fi] |= get(n) << lo;
|
||||||
|
const regions = m < 10 ? 2 : 1, ne = regions * 2, dbits = [dr, dg, db], ep = [[], [], [], []];
|
||||||
|
for (let c = 0; c < 3; c++) {
|
||||||
|
for (let e = 0; e < ne; e++) ep[e][c] = f[c * 4 + e];
|
||||||
|
if (sgn) ep[0][c] = sext(ep[0][c], W);
|
||||||
|
const delta = m !== 9 && m !== 10;
|
||||||
|
for (let e = 1; e < ne; e++) {
|
||||||
|
if (delta || sgn) ep[e][c] = sext(ep[e][c], dbits[c]);
|
||||||
|
if (delta) { ep[e][c] = (ep[e][c] + ep[0][c]) & ((1 << W) - 1); if (sgn) ep[e][c] = sext(ep[e][c], W); }
|
||||||
|
}
|
||||||
|
for (let e = 0; e < ne; e++) ep[e][c] = bc6Unq(ep[e][c], W, sgn);
|
||||||
|
}
|
||||||
|
const part = f[PD], ib = regions === 2 ? 3 : 4, wt = WEIGHTS[ib];
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
const k = regions === 2 ? (PART2[part] >> i) & 1 : 0;
|
||||||
|
const anchor = i === 0 || (regions === 2 && i === ANCHOR2[part]);
|
||||||
|
const ix = get(ib - (anchor ? 1 : 0));
|
||||||
|
for (let c = 0; c < 3; c++) out[i * 3 + c] = bc6Finish(interp(ep[2 * k][c], ep[2 * k + 1][c], wt[ix]), sgn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* decodeBlocks(format, blocks, w, h) → RGBA Uint8Array (LDR) or RGB
|
||||||
|
* half-bits Uint16Array (BC6H); null for ETC2 / EAC / ASTC. */
|
||||||
|
function decodeBlocks(format, blocks, w, h) {
|
||||||
|
if (format >= 0x50 || !BLOCK_FORMATS[format]) return null;
|
||||||
|
const S = BLOCK_FORMATS[format][2], nbx = Math.ceil(w / 4), nby = Math.ceil(h / 4);
|
||||||
|
const hdr = format === 0x47 || format === 0x48;
|
||||||
|
const out = hdr ? new Uint16Array(w * h * 3) : new Uint8Array(w * h * 4);
|
||||||
|
const px = new Int32Array(64), hx = new Int32Array(48);
|
||||||
|
let o = 0;
|
||||||
|
for (let by = 0; by < nby; by++)
|
||||||
|
for (let bx = 0; bx < nbx; bx++, o += S) {
|
||||||
|
switch (format) {
|
||||||
|
case 0x40: bc1Block(blocks, o, px, false); break;
|
||||||
|
case 0x41:
|
||||||
|
bc1Block(blocks, o + 8, px, true);
|
||||||
|
for (let i = 0; i < 16; i++) px[i * 4 + 3] = ((blocks[o + (i >> 1)] >> (4 * (i & 1))) & 15) * 17;
|
||||||
|
break;
|
||||||
|
case 0x42: bc1Block(blocks, o + 8, px, true); bc4Block(blocks, o, px, 3, false); break;
|
||||||
|
case 0x43: case 0x44:
|
||||||
|
px.fill(0); bc4Block(blocks, o, px, 0, format === 0x44);
|
||||||
|
for (let i = 0; i < 16; i++) px[i * 4 + 3] = 255;
|
||||||
|
break;
|
||||||
|
case 0x45: case 0x46:
|
||||||
|
px.fill(0); bc4Block(blocks, o, px, 0, format === 0x46); bc4Block(blocks, o + 8, px, 1, format === 0x46);
|
||||||
|
for (let i = 0; i < 16; i++) px[i * 4 + 3] = 255;
|
||||||
|
break;
|
||||||
|
case 0x49: bc7Block(blocks, o, px); break;
|
||||||
|
default: bc6Block(blocks, o, hx, format === 0x48);
|
||||||
|
}
|
||||||
|
for (let y = 0; y < 4 && by * 4 + y < h; y++)
|
||||||
|
for (let x = 0; x < 4 && bx * 4 + x < w; x++) {
|
||||||
|
const d = (by * 4 + y) * w + bx * 4 + x, i = y * 4 + x;
|
||||||
|
if (hdr) for (let c = 0; c < 3; c++) out[d * 3 + c] = hx[i * 3 + c];
|
||||||
|
else for (let c = 0; c < 4; c++) out[d * 4 + c] = px[i * 4 + c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function halfToFloat(h) {
|
||||||
|
const s = h & 0x8000 ? -1 : 1, e = (h >> 10) & 31, m = h & 0x3FF;
|
||||||
|
if (e === 0) return s * m * Math.pow(2, -24);
|
||||||
|
if (e === 31) return m ? NaN : s * Infinity;
|
||||||
|
return s * (1 + m / 1024) * Math.pow(2, e - 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
const linToSrgb = (v) => !(v > 0) ? 0 : v >= 1 ? 255
|
||||||
|
: Math.round(255 * (v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - 0.055));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* textureToRGBA(tex, surface = 0, slice = 0) → { width, height,
|
||||||
|
* rgba: Uint8ClampedArray } for display: gray expanded, 16-bit rounded,
|
||||||
|
* half / float clamped and sRGB encoded, GPU blocks decoded. Throws for
|
||||||
|
* ETC2 / EAC / ASTC.
|
||||||
|
*/
|
||||||
|
function textureToRGBA(tex, surface, slice) {
|
||||||
|
surface = surface || 0; slice = slice || 0;
|
||||||
|
const m = surface % tex.mipLevels, [w, h, d] = mipSize(tex, m), n = w * h;
|
||||||
|
const data = tex.surfaces[surface], rgba = new Uint8ClampedArray(n * 4);
|
||||||
|
let src, ch = tex.channels, kind = tex.payload === 1 ? 'u8' : ['u8', 'u16', 'f16', 'f32'][tex.sample];
|
||||||
|
if (tex.payload === 1) {
|
||||||
|
const per = data.length / d, blk = decodeBlocks(tex.format, data.subarray(per * slice, per * (slice + 1)), w, h);
|
||||||
|
if (!blk) fail('unsupported', 'ETC2 / EAC / ASTC blocks cannot be shown');
|
||||||
|
if (blk instanceof Uint16Array) { src = blk; ch = 3; kind = 'f16'; }
|
||||||
|
else {
|
||||||
|
rgba.set(blk);
|
||||||
|
if (tex.channels === 3) for (let i = 0; i < n; i++) rgba[i * 4 + 3] = 255;
|
||||||
|
return { width: w, height: h, rgba };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
src = data.subarray(n * ch * slice, n * ch * (slice + 1));
|
||||||
|
}
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const v = [0, 0, 0, 255];
|
||||||
|
for (let c = 0; c < ch; c++) {
|
||||||
|
const x = src[i * ch + c], alpha = (ch === 2 && c === 1) || (ch === 4 && c === 3);
|
||||||
|
let q;
|
||||||
|
if (kind === 'u8') q = x;
|
||||||
|
else if (kind === 'u16') q = Math.round(x / 257);
|
||||||
|
else {
|
||||||
|
const f = kind === 'f16' ? halfToFloat(x) : x;
|
||||||
|
q = alpha ? Math.max(0, Math.min(255, Math.round(f * 255))) : linToSrgb(f);
|
||||||
|
}
|
||||||
|
v[ch <= 2 ? (c === 0 ? 0 : 3) : c] = q;
|
||||||
|
}
|
||||||
|
if (ch <= 2) v[1] = v[2] = v[0];
|
||||||
|
rgba.set(v, i * 4);
|
||||||
|
}
|
||||||
|
return { width: w, height: h, rgba };
|
||||||
|
}
|
||||||
|
|
||||||
|
function surfaceIndex(tex, frame, layer, face, mip) {
|
||||||
|
return (((frame || 0) * tex.layers + (layer || 0)) * tex.faces + (face || 0)) * tex.mipLevels + (mip || 0);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Decoder ──────────────────────────────────────────────────────── */
|
/* ── Decoder ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
|
@ -150,7 +824,7 @@
|
||||||
R[r0 + tw] = R[r0 + tw - 1];
|
R[r0 + tw] = R[r0 + tw - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeTile(b, p, end, out, imgW, tx, ty, tw, th, ch) {
|
function decodeTile(b, p, end, out, imgW, tx, ty, tw, th, ch, v2, book) {
|
||||||
if (end <= p) corrupt();
|
if (end <= p) corrupt();
|
||||||
let hdr = b[p++];
|
let hdr = b[p++];
|
||||||
if (hdr === STORED) {
|
if (hdr === STORED) {
|
||||||
|
|
@ -160,10 +834,18 @@
|
||||||
out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch);
|
out.set(b.subarray(p + rowb * y, p + rowb * (y + 1)), ((ty + y) * imgW + tx) * ch);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hdr & 0xE0) corrupt(); /* reserved bits set */
|
if (hdr & (v2 ? 0x80 : 0xE0)) corrupt(); /* reserved bits set */
|
||||||
const xf = hdr & SUBGREEN;
|
if ((hdr & 0x40) && (hdr & SUBGREEN)) corrupt();
|
||||||
|
/* xf: 0 YCoCg-R, 1 subtract-green, 2 none (v2) */
|
||||||
|
const xf = (hdr & 0x40) ? 2 : (hdr & SUBGREEN) ? 1 : 0, ent = hdr & 0x20;
|
||||||
hdr &= 0x0F;
|
hdr &= 0x0F;
|
||||||
if ((hdr >> ch) || (xf && ch < 3)) corrupt();
|
if ((hdr >> ch) || (xf && ch < 3)) corrupt();
|
||||||
|
let emask = 0;
|
||||||
|
if (ent) { /* v2: entropy planes */
|
||||||
|
if (p >= end || !book) corrupt();
|
||||||
|
emask = b[p++];
|
||||||
|
if ((emask >> ch) || (emask & hdr)) corrupt();
|
||||||
|
}
|
||||||
|
|
||||||
const rowLen = TILE + 1, planeLen = RING * rowLen;
|
const rowLen = TILE + 1, planeLen = RING * rowLen;
|
||||||
const rows = new Uint8Array(4 * planeLen);
|
const rows = new Uint8Array(4 * planeLen);
|
||||||
|
|
@ -176,7 +858,8 @@
|
||||||
for (let c = 0; c < ch; c++) {
|
for (let c = 0; c < ch; c++) {
|
||||||
if (hdr & (1 << c)) continue;
|
if (hdr & (1 << c)) continue;
|
||||||
z[c] = new Uint8Array(TILE_PX + GROUP);
|
z[c] = new Uint8Array(TILE_PX + GROUP);
|
||||||
const r = unpackPlane(b, p, end, z[c], tw, th);
|
const r = (emask & (1 << c)) ? huffPlane(b, p, end, z[c], tw, th, book, xf * 16 + c)
|
||||||
|
: unpackPlane(b, p, end, z[c], tw, th);
|
||||||
p = r.p; filt[c] = r.filt;
|
p = r.p; filt[c] = r.filt;
|
||||||
}
|
}
|
||||||
if (p !== end) corrupt();
|
if (p !== end) corrupt();
|
||||||
|
|
@ -195,7 +878,11 @@
|
||||||
const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri;
|
const P0 = ri, P1 = planeLen + ri, P2 = 2 * planeLen + ri, P3 = 3 * planeLen + ri;
|
||||||
for (let x = 0; x < tw; x++, d += ch) {
|
for (let x = 0; x < tw; x++, d += ch) {
|
||||||
if (ch >= 3) {
|
if (ch >= 3) {
|
||||||
if (xf) {
|
if (xf === 2) {
|
||||||
|
out[d] = rows[P0 + x];
|
||||||
|
out[d + 1] = rows[P1 + x];
|
||||||
|
out[d + 2] = rows[P2 + x];
|
||||||
|
} else if (xf) {
|
||||||
const g = rows[P0 + x];
|
const g = rows[P0 + x];
|
||||||
out[d] = rows[P1 + x] + g - 128;
|
out[d] = rows[P1 + x] + g - 128;
|
||||||
out[d + 1] = g;
|
out[d + 1] = g;
|
||||||
|
|
@ -219,11 +906,27 @@
|
||||||
|
|
||||||
const r32 = (b, p) => (b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24)) >>> 0;
|
const r32 = (b, p) => (b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24)) >>> 0;
|
||||||
|
|
||||||
|
/* decode(bytes) → 8-bit image. Reads version 1, and version 2 files
|
||||||
|
* that hold 8-bit pixels (2D; cube face 0 / layer 0 / frame 0). Other
|
||||||
|
* textures: see decodeTexture. */
|
||||||
function decode(input) {
|
function decode(input) {
|
||||||
const b = input instanceof Uint8Array ? input : new Uint8Array(input);
|
const b = input instanceof Uint8Array ? input : new Uint8Array(input);
|
||||||
|
if (b.length >= 5 && b[0] === 0x59 && b[1] === 0x41 && b[2] === 0x50 && b[3] === 0x46 && b[4] === 2) {
|
||||||
|
const t = decodeTexture(b);
|
||||||
|
if (t.payload !== 0 || t.sample !== 0 || t.shape === 2)
|
||||||
|
throw new YapfError('unsupported', 'this is a YAPF 2 texture (GPU blocks, 16-bit / float or a ' +
|
||||||
|
'volume); use YAPF.decodeTexture and YAPF.textureToRGBA');
|
||||||
|
const mips = t.surfaces.slice(0, t.mipLevels);
|
||||||
|
return { width: t.width, height: t.height, channels: t.channels, gpuFormat: t.format,
|
||||||
|
flags: t.flags, mipLevels: t.mipLevels, pixels: mips[0], mips };
|
||||||
|
}
|
||||||
|
return decodeV1(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeV1(b) {
|
||||||
if (b.length < 20 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
|
if (b.length < 20 || b[0] !== 0x59 || b[1] !== 0x41 || b[2] !== 0x50 || b[3] !== 0x46)
|
||||||
throw new Error('YAPF: not a YAPF file');
|
throw new YapfError('not-yapf', 'not a YAPF file');
|
||||||
if (b[4] !== 1) throw new Error('YAPF: unsupported format version ' + b[4]);
|
if (b[4] !== 1) throw new YapfError('version', 'unsupported format version ' + b[4]);
|
||||||
const ch = b[5], mipLevels = b[7], W = r32(b, 12), H = r32(b, 16);
|
const ch = b[5], mipLevels = b[7], W = r32(b, 12), H = r32(b, 16);
|
||||||
if (ch < 1 || ch > 4 || mipLevels < 1 || mipLevels > MAX_MIPS ||
|
if (ch < 1 || ch > 4 || mipLevels < 1 || mipLevels > MAX_MIPS ||
|
||||||
W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt();
|
W < 1 || W > MAX_DIM || H < 1 || H > MAX_DIM) corrupt();
|
||||||
|
|
@ -507,7 +1210,9 @@
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { decode, encode, toRGBA, GPU, FLAG, VERSION: 1 };
|
return { decode, encode, toRGBA, decodeTexture, textureToRGBA, decodeBlocks, surfaceIndex,
|
||||||
|
checksum: (b) => checksum(b, 0, b.length), YapfError, GPU, FLAG, NEED,
|
||||||
|
VERSION: 1 /* written by encode */, READS: [1, 2] };
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
<script>/*
|
<script>/*
|
||||||
|
|
@ -653,14 +1358,26 @@ $('tab-import').onclick = () => tab('import');
|
||||||
$('tab-export').onclick = () => tab('export');
|
$('tab-export').onclick = () => tab('export');
|
||||||
|
|
||||||
/* Import: .yapf → pixels → exact PNG → Figma image */
|
/* Import: .yapf → pixels → exact PNG → Figma image */
|
||||||
|
/* Any YAPF file → 8-bit pixels. 8-bit images keep their exact pixels and
|
||||||
|
* channels; GPU blocks, 16-bit and float textures are converted for display
|
||||||
|
* (RGBA). Cubes, arrays, volumes and animations give their first image. */
|
||||||
|
function yapfToPixels(tex) {
|
||||||
|
const w = tex.width, h = tex.height;
|
||||||
|
if (tex.payload === 0 && tex.sample === 0)
|
||||||
|
return { width: w, height: h, channels: tex.channels,
|
||||||
|
pixels: tex.surfaces[0].subarray(0, w * h * tex.channels) };
|
||||||
|
const v = YAPF.textureToRGBA(tex, 0, 0);
|
||||||
|
return { width: w, height: h, channels: 4, pixels: new Uint8Array(v.rgba.buffer),
|
||||||
|
note: tex.payload === 1 ? 'GPU blocks decoded' : 'converted to 8-bit' };
|
||||||
|
}
|
||||||
async function importFiles(files) {
|
async function importFiles(files) {
|
||||||
const images = [];
|
const images = [];
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
try {
|
try {
|
||||||
const img = YAPF.decode(new Uint8Array(await f.arrayBuffer()));
|
const img = yapfToPixels(YAPF.decodeTexture(new Uint8Array(await f.arrayBuffer())));
|
||||||
const png = await PNG.encode(img.pixels, img.width, img.height, img.channels);
|
const png = await PNG.encode(img.pixels, img.width, img.height, img.channels);
|
||||||
images.push({ name: f.name.replace(/\.yapf$/i, ''), width: img.width, height: img.height, png });
|
images.push({ name: f.name.replace(/\.yapf$/i, ''), width: img.width, height: img.height, png });
|
||||||
status('Decoded ' + f.name + ' (' + img.width + '×' + img.height + ')');
|
status('Decoded ' + f.name + ' (' + img.width + '×' + img.height + (img.note ? ', ' + img.note : '') + ')');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
status(f.name + ': ' + e.message);
|
status(f.name + ': ' + e.message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@
|
||||||
|
|
||||||
Open and export `.yapf` images in **GIMP 3.0 or later**.
|
Open and export `.yapf` images in **GIMP 3.0 or later**.
|
||||||
|
|
||||||
- **File → Open** reads `.yapf` (Gray, Gray + Alpha, RGB, RGBA).
|
- **File → Open** reads every `.yapf` file, version 1 and 2: Gray, Gray +
|
||||||
|
Alpha, RGB, RGBA in 8-bit, 16-bit, half and float precision; GPU blocks
|
||||||
|
(BC1–BC7, BC6H HDR) decoded for viewing; cube faces, array layers, volume
|
||||||
|
slices and animation frames as separate layers. When a file cannot be
|
||||||
|
opened, the message says why (damaged, truncated, not YAPF, newer format).
|
||||||
- **File → Export As… → `name.yapf`** writes the visible image (layers are
|
- **File → Export As… → `name.yapf`** writes the visible image (layers are
|
||||||
flattened into a copy; your document is untouched), with an optional
|
flattened into a copy; your document is untouched), with an optional
|
||||||
**Store mipmaps** checkbox for game engines.
|
**Store mipmaps** checkbox for game engines.
|
||||||
|
|
@ -16,6 +20,7 @@ You need GIMP 3 with its development files (`gimptool`).
|
||||||
| Arch / Manjaro | `sudo pacman -S gimp` (includes them) |
|
| Arch / Manjaro | `sudo pacman -S gimp` (includes them) |
|
||||||
| Debian / Ubuntu | `sudo apt install libgimp-3.0-dev` (GIMP 3 packages) |
|
| Debian / Ubuntu | `sudo apt install libgimp-3.0-dev` (GIMP 3 packages) |
|
||||||
| Fedora | `sudo dnf install gimp-devel` |
|
| Fedora | `sudo dnf install gimp-devel` |
|
||||||
|
| NixOS | `nix-shell gimp-shell.nix` (in this folder), then build inside that shell |
|
||||||
| Windows | MSYS2: `pacman -S mingw-w64-ucrt-x86_64-gimp` |
|
| Windows | MSYS2: `pacman -S mingw-w64-ucrt-x86_64-gimp` |
|
||||||
| macOS | build GIMP 3 from source or use MacPorts/Homebrew |
|
| macOS | build GIMP 3 from source or use MacPorts/Homebrew |
|
||||||
|
|
||||||
|
|
@ -28,6 +33,11 @@ gimptool --install yapf-gimp.c
|
||||||
That compiles the plugin (it pulls in `../../yapf.c` itself) and installs it
|
That compiles the plugin (it pulls in `../../yapf.c` itself) and installs it
|
||||||
into your personal GIMP plug-ins folder. Restart GIMP.
|
into your personal GIMP plug-ins folder. Restart GIMP.
|
||||||
|
|
||||||
|
On NixOS, build with the same nixpkgs channel your GIMP comes from (the
|
||||||
|
plugin links against that GIMP's libraries), and build it again after a
|
||||||
|
GIMP update. Your GIMP must be GIMP 3: `environment.systemPackages = [
|
||||||
|
pkgs.gimp3 ];`, or `nix-shell -p gimp3 --run gimp` to try it.
|
||||||
|
|
||||||
### Manual build
|
### Manual build
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
@ -44,5 +54,9 @@ GIMP config folder is `~/.config/GIMP/3.0` on Linux,
|
||||||
- Pixels are saved with the sRGB flag, because GIMP's 8-bit buffers are
|
- Pixels are saved with the sRGB flag, because GIMP's 8-bit buffers are
|
||||||
gamma encoded. Files marked linear load as linear.
|
gamma encoded. Files marked linear load as linear.
|
||||||
- When a file has mipmaps, GIMP opens the full-size image.
|
- When a file has mipmaps, GIMP opens the full-size image.
|
||||||
|
- ETC2 / EAC / ASTC blocks cannot be shown yet. Export writes 8-bit
|
||||||
|
version 1 files, as before.
|
||||||
- Tested with GIMP 3.0.8 on Linux: opening matches the reference decoder
|
- Tested with GIMP 3.0.8 on Linux: opening matches the reference decoder
|
||||||
pixel for pixel, and export equals GIMP's own flattened PNG of the image.
|
pixel for pixel, and export equals GIMP's own flattened PNG of the image.
|
||||||
|
The version 2 loader was tested in GIMP 3.0.6 (nixpkgs 25.05, headless):
|
||||||
|
BC7, BC5, BC6H, half float, 16-bit, cube, animation and a damaged file.
|
||||||
|
|
|
||||||
7
extensions/gimp/gimp-shell.nix
Normal file
7
extensions/gimp/gimp-shell.nix
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Development shell for building the plugin on NixOS: nix-shell gimp-shell.nix
|
||||||
|
# `gimp3` is GIMP 3 on every channel (on unstable it is an alias of `gimp`).
|
||||||
|
with import <nixpkgs> {};
|
||||||
|
mkShell {
|
||||||
|
nativeBuildInputs = [ pkg-config ];
|
||||||
|
buildInputs = [ gimp3 gimp3.dev gtk3 glib gdk-pixbuf gegl babl cairo pango ];
|
||||||
|
}
|
||||||
|
|
@ -138,8 +138,44 @@ static GimpProcedure *yapf_plugin_create_procedure(GimpPlugIn *plug_in,
|
||||||
return procedure;
|
return procedure;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static GimpValueArray *yapf_error(GimpProcedure *procedure, const gchar *msg);
|
||||||
|
|
||||||
/* ── Load handler ────────────────────────────────────────────────────── */
|
/* ── Load handler ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Why a file could not be opened, in words. */
|
||||||
|
static gchar *yapf_load_reason(const gchar *path, int err) {
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
uint8_t h[8] = {0};
|
||||||
|
size_t n = f ? fread(h, 1, sizeof(h), f) : 0;
|
||||||
|
if (f) fclose(f);
|
||||||
|
if (!f || err == YAPF_ERR_IO)
|
||||||
|
return g_strdup("The file cannot be read.");
|
||||||
|
if (n < 5 || memcmp(h, "YAPF", 4) != 0)
|
||||||
|
return g_strdup("This is not a YAPF file (it does not start with \"YAPF\").");
|
||||||
|
if (h[4] != 1 && h[4] != 2)
|
||||||
|
return g_strdup_printf("This YAPF file is format version %u; this plug-in reads "
|
||||||
|
"versions 1 and 2. Update the plug-in.", h[4]);
|
||||||
|
switch (err) {
|
||||||
|
case YAPF_ERR_UNSUPPORTED:
|
||||||
|
return g_strdup("This YAPF file needs a feature this plug-in does not have yet. "
|
||||||
|
"Update the plug-in.");
|
||||||
|
case YAPF_ERR_CHECKSUM:
|
||||||
|
return g_strdup("This YAPF file is damaged: a checksum does not match.");
|
||||||
|
case YAPF_ERR_TRUNCATED:
|
||||||
|
return g_strdup("This YAPF file is incomplete (truncated).");
|
||||||
|
case YAPF_ERR_OOM:
|
||||||
|
return g_strdup("Out of memory.");
|
||||||
|
default:
|
||||||
|
return g_strdup_printf("This YAPF file (version %u) is corrupt, or was written by a "
|
||||||
|
"tool that does not follow the YAPF specification.", h[4]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const gchar *yapf_face_name(uint32_t f) {
|
||||||
|
static const gchar *n[6] = { "+X", "-X", "+Y", "-Y", "+Z", "-Z" };
|
||||||
|
return f < 6 ? n[f] : "?";
|
||||||
|
}
|
||||||
|
|
||||||
static GimpValueArray *yapf_load_run(
|
static GimpValueArray *yapf_load_run(
|
||||||
GimpProcedure *procedure,
|
GimpProcedure *procedure,
|
||||||
GimpRunMode run_mode,
|
GimpRunMode run_mode,
|
||||||
|
|
@ -151,60 +187,94 @@ static GimpValueArray *yapf_load_run(
|
||||||
{
|
{
|
||||||
(void)run_mode; (void)metadata; (void)flags; (void)config; (void)run_data;
|
(void)run_mode; (void)metadata; (void)flags; (void)config; (void)run_data;
|
||||||
|
|
||||||
gchar *path = g_file_get_path(file);
|
gchar *path = g_file_get_path(file);
|
||||||
yapf_image_t *img = yapf_load_mt(path, 0); /* all cores */
|
int err = 0;
|
||||||
|
yapf_texture_t *t = yapf_tex_load(path, 0, 0, &err); /* v1 and v2, all cores */
|
||||||
|
if (!t) {
|
||||||
|
gchar *why = yapf_load_reason(path, err);
|
||||||
|
g_free(path);
|
||||||
|
GError *e = g_error_new(GIMP_PLUG_IN_ERROR, 0, "Failed to load YAPF file: %s", why);
|
||||||
|
g_free(why);
|
||||||
|
return gimp_procedure_new_return_values(procedure, GIMP_PDB_EXECUTION_ERROR, e);
|
||||||
|
}
|
||||||
g_free(path);
|
g_free(path);
|
||||||
|
|
||||||
if (!img) {
|
/* What the pixels look like after decoding: GPU blocks decode to
|
||||||
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0,
|
* RGBA 8-bit, BC6H to RGB half float. */
|
||||||
"Failed to load YAPF file.");
|
gboolean blocks = t->payload == YAPF_PAYLOAD_BLOCKS;
|
||||||
return gimp_procedure_new_return_values(procedure,
|
gboolean bc6 = blocks && (t->format == YAPF_GPU_BC6H || t->format == YAPF_GPU_BC6H_SF);
|
||||||
GIMP_PDB_EXECUTION_ERROR, err);
|
if (blocks && t->format >= YAPF_GPU_ETC2_RGB8) {
|
||||||
|
yapf_tex_free(t);
|
||||||
|
return yapf_error(procedure, "Failed to load YAPF file: it holds ETC2 / EAC / ASTC "
|
||||||
|
"GPU blocks, which this plug-in cannot show yet.");
|
||||||
}
|
}
|
||||||
|
int ch = blocks ? (bc6 ? 3 : 4) : t->channels;
|
||||||
|
int sample = blocks ? (bc6 ? YAPF_SAMPLE_F16 : YAPF_SAMPLE_U8) : t->sample;
|
||||||
|
/* sRGB data is gamma encoded; floats are always linear */
|
||||||
|
gboolean gamma = (t->flags & YAPF_FLAG_SRGB) &&
|
||||||
|
(sample == YAPF_SAMPLE_U8 || sample == YAPF_SAMPLE_U16);
|
||||||
|
|
||||||
/* Map YAPF channel count to GIMP image / layer types.
|
static const gchar *chn[2][4] = { { "Y", "YA", "RGB", "RGBA" }, { "Y'", "Y'A", "R'G'B'", "R'G'B'A" } };
|
||||||
* Use gamma-corrected babl formats (R'G'B') when the file carries the
|
static const gchar *typ[4] = { "u8", "u16", "half", "float" };
|
||||||
* sRGB flag; fall back to linear (RGB) otherwise. */
|
gchar *fmt_name = g_strdup_printf("%s %s", chn[gamma ? 1 : 0][ch - 1], typ[sample]);
|
||||||
gboolean is_srgb = (img->flags & YAPF_FLAG_SRGB) != 0;
|
const Babl *fmt = babl_format(fmt_name);
|
||||||
GimpImageBaseType base_type = GIMP_RGB;
|
g_free(fmt_name);
|
||||||
GimpImageType layer_type = GIMP_RGBA_IMAGE;
|
|
||||||
const gchar *babl_fmt = is_srgb ? "R'G'B'A u8" : "RGBA u8";
|
|
||||||
|
|
||||||
switch (img->channels) {
|
GimpPrecision prec;
|
||||||
case YAPF_CHANNELS_GRAY:
|
switch (sample) {
|
||||||
base_type = GIMP_GRAY;
|
case YAPF_SAMPLE_U16: prec = gamma ? GIMP_PRECISION_U16_NON_LINEAR : GIMP_PRECISION_U16_LINEAR; break;
|
||||||
layer_type = GIMP_GRAY_IMAGE;
|
case YAPF_SAMPLE_F16: prec = GIMP_PRECISION_HALF_LINEAR; break;
|
||||||
babl_fmt = is_srgb ? "Y' u8" : "Y u8";
|
case YAPF_SAMPLE_F32: prec = GIMP_PRECISION_FLOAT_LINEAR; break;
|
||||||
break;
|
default: prec = gamma ? GIMP_PRECISION_U8_NON_LINEAR : GIMP_PRECISION_U8_LINEAR; break;
|
||||||
case YAPF_CHANNELS_GRAY_ALPHA:
|
|
||||||
base_type = GIMP_GRAY;
|
|
||||||
layer_type = GIMP_GRAYA_IMAGE;
|
|
||||||
babl_fmt = is_srgb ? "Y'A u8" : "YA u8";
|
|
||||||
break;
|
|
||||||
case YAPF_CHANNELS_RGB:
|
|
||||||
base_type = GIMP_RGB;
|
|
||||||
layer_type = GIMP_RGB_IMAGE;
|
|
||||||
babl_fmt = is_srgb ? "R'G'B' u8" : "RGB u8";
|
|
||||||
break;
|
|
||||||
default: /* RGBA */
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
GimpImageBaseType base = ch <= 2 ? GIMP_GRAY : GIMP_RGB;
|
||||||
|
GimpImageType ltype = ch == 1 ? GIMP_GRAY_IMAGE : ch == 2 ? GIMP_GRAYA_IMAGE
|
||||||
|
: ch == 3 ? GIMP_RGB_IMAGE : GIMP_RGBA_IMAGE;
|
||||||
|
|
||||||
GimpImage *image = gimp_image_new(img->width, img->height, base_type);
|
/* Mip 0 of every frame, layer, face and slice becomes one GIMP layer
|
||||||
GimpLayer *layer = gimp_layer_new(image, "Background",
|
* (the first at the bottom, as GIMP animations expect). */
|
||||||
img->width, img->height, layer_type,
|
uint32_t w, h, d, faces = yapf_tex_faces(t);
|
||||||
100.0,
|
yapf_tex_mip_size(t, 0, &w, &h, &d);
|
||||||
gimp_image_get_default_new_layer_mode(image));
|
GimpImage *image = gimp_image_new_with_precision(w, h, base, prec);
|
||||||
gimp_image_insert_layer(image, layer, NULL, 0);
|
size_t texel = (size_t)ch * yapf_sample_bytes((uint8_t)sample);
|
||||||
|
void *tmp = blocks ? g_malloc((size_t)w * h * texel) : NULL;
|
||||||
|
gint count = (gint)(t->frames * t->layers * faces * d);
|
||||||
|
|
||||||
GeglBuffer *buffer = gimp_drawable_get_buffer(GIMP_DRAWABLE(layer));
|
for (uint32_t fr = 0; fr < t->frames; fr++)
|
||||||
gegl_buffer_set(buffer,
|
for (uint32_t ly = 0; ly < t->layers; ly++)
|
||||||
GEGL_RECTANGLE(0, 0, (gint)img->width, (gint)img->height),
|
for (uint32_t fc = 0; fc < faces; fc++)
|
||||||
0, babl_format(babl_fmt),
|
for (uint32_t z = 0; z < d; z++) {
|
||||||
img->pixels, GEGL_AUTO_ROWSTRIDE);
|
uint32_t s = yapf_tex_surface(t, fr, ly, fc, 0);
|
||||||
g_object_unref(buffer);
|
size_t slice = yapf_tex_surface_bytes(t, 0) / d;
|
||||||
|
const uint8_t *src = (const uint8_t *)t->data[s] + slice * z;
|
||||||
|
if (blocks) {
|
||||||
|
yapf_blocks_decode(t->format, src, w, h, tmp);
|
||||||
|
src = (const uint8_t *)tmp;
|
||||||
|
}
|
||||||
|
GString *name = g_string_new(NULL);
|
||||||
|
if (t->frames > 1) {
|
||||||
|
g_string_append_printf(name, "Frame %u", fr + 1);
|
||||||
|
if (t->frame_us)
|
||||||
|
g_string_append_printf(name, " (%ums)", (t->frame_us[fr] + 500) / 1000);
|
||||||
|
}
|
||||||
|
if (t->layers > 1) g_string_append_printf(name, "%sLayer %u", name->len ? ", " : "", ly);
|
||||||
|
if (faces > 1) g_string_append_printf(name, "%sFace %s", name->len ? ", " : "", yapf_face_name(fc));
|
||||||
|
if (d > 1) g_string_append_printf(name, "%sSlice %u", name->len ? ", " : "", z);
|
||||||
|
if (!name->len) g_string_append(name, "Background");
|
||||||
|
|
||||||
yapf_free(img);
|
GimpLayer *layer = gimp_layer_new(image, name->str, (gint)w, (gint)h, ltype,
|
||||||
|
100.0, gimp_image_get_default_new_layer_mode(image));
|
||||||
|
g_string_free(name, TRUE);
|
||||||
|
gimp_image_insert_layer(image, layer, NULL, 0);
|
||||||
|
if (count > 1 && !(fr == 0 && ly == 0 && fc == 0 && z == 0))
|
||||||
|
gimp_item_set_visible(GIMP_ITEM(layer), FALSE);
|
||||||
|
GeglBuffer *buffer = gimp_drawable_get_buffer(GIMP_DRAWABLE(layer));
|
||||||
|
gegl_buffer_set(buffer, GEGL_RECTANGLE(0, 0, (gint)w, (gint)h), 0, fmt,
|
||||||
|
src, GEGL_AUTO_ROWSTRIDE);
|
||||||
|
g_object_unref(buffer);
|
||||||
|
}
|
||||||
|
g_free(tmp);
|
||||||
|
yapf_tex_free(t);
|
||||||
|
|
||||||
GimpValueArray *ret = gimp_procedure_new_return_values(procedure,
|
GimpValueArray *ret = gimp_procedure_new_return_values(procedure,
|
||||||
GIMP_PDB_SUCCESS, NULL);
|
GIMP_PDB_SUCCESS, NULL);
|
||||||
|
|
|
||||||
|
|
@ -48,3 +48,9 @@ Log out and back in, then enable **YAPF Images** in Dolphin's preview settings.
|
||||||
Built against KF6 / Qt 6 and tested by loading the plugin through KDE's
|
Built against KF6 / Qt 6 and tested by loading the plugin through KDE's
|
||||||
plugin factory and creating thumbnails (RGBA, RGB, mipmapped, wide images;
|
plugin factory and creating thumbnails (RGBA, RGB, mipmapped, wide images;
|
||||||
corrupt files are rejected). Not yet tried inside a running Dolphin.
|
corrupt files are rejected). Not yet tried inside a running Dolphin.
|
||||||
|
|
||||||
|
Reads YAPF version 1 and 2: GPU blocks (BC1–BC7, BC6H), 16-bit and float
|
||||||
|
textures, cubes (face +X), arrays, volumes and animations (the first
|
||||||
|
image). Only the mip level the thumbnail needs is decoded. Tested with
|
||||||
|
KF6 from nixpkgs 25.05 on BC7, BC5, BC6H, half float, 16-bit, cube and
|
||||||
|
animation files. ETC2 / ASTC files get no thumbnail.
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,11 @@
|
||||||
|
|
||||||
#include <KIO/ThumbnailCreator>
|
#include <KIO/ThumbnailCreator>
|
||||||
#include <KPluginFactory>
|
#include <KPluginFactory>
|
||||||
|
#include <QFile>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include "yapf.h"
|
#include "yapf.h"
|
||||||
}
|
}
|
||||||
|
|
@ -20,52 +23,50 @@ public:
|
||||||
|
|
||||||
KIO::ThumbnailResult create(const KIO::ThumbnailRequest &request) override
|
KIO::ThumbnailResult create(const KIO::ThumbnailRequest &request) override
|
||||||
{
|
{
|
||||||
const QByteArray path = request.url().toLocalFile().toLocal8Bit();
|
QFile file(request.url().toLocalFile());
|
||||||
yapf_image_t *img = yapf_load(path.constData());
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
if (!img) {
|
return KIO::ThumbnailResult::fail();
|
||||||
|
}
|
||||||
|
const QByteArray data = file.readAll();
|
||||||
|
|
||||||
|
// Version 1 and 2: read the tables, then decode only the mip we
|
||||||
|
// need (frame 0, layer 0, face 0, slice 0).
|
||||||
|
int err = 0;
|
||||||
|
yapf_texture_t *tex = yapf_tex_open(data.constData(), size_t(data.size()), 0, &err);
|
||||||
|
if (!tex) {
|
||||||
return KIO::ThumbnailResult::fail();
|
return KIO::ThumbnailResult::fail();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start from the smallest stored mip level that still covers the
|
// The smallest stored mip that still covers the requested size;
|
||||||
// requested size; Qt scales the rest.
|
// Qt scales the rest.
|
||||||
const QSize want = request.targetSize();
|
const QSize want = request.targetSize();
|
||||||
int m = 0;
|
uint32_t m = 0, w = 0, h = 0;
|
||||||
while (m + 1 < img->mip_levels &&
|
while (m + 1 < tex->mip_levels) {
|
||||||
int(img->width >> (m + 1)) >= want.width() &&
|
yapf_tex_mip_size(tex, m + 1, &w, &h, nullptr);
|
||||||
int(img->height >> (m + 1)) >= want.height()) {
|
if (int(w) < want.width() || int(h) < want.height()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
++m;
|
++m;
|
||||||
}
|
}
|
||||||
const int w = qMax(1, int(img->width >> m));
|
yapf_tex_mip_size(tex, m, &w, &h, nullptr);
|
||||||
const int h = qMax(1, int(img->height >> m));
|
const uint32_t s = yapf_tex_surface(tex, 0, 0, 0, m);
|
||||||
const uchar *px = img->mips[m];
|
|
||||||
|
|
||||||
QImage image;
|
QImage image(int(w), int(h), (tex->flags & YAPF_FLAG_PREMULT_ALPHA) ? QImage::Format_RGBA8888_Premultiplied
|
||||||
switch (img->channels) {
|
: QImage::Format_RGBA8888);
|
||||||
case 1:
|
bool ok = !image.isNull() &&
|
||||||
image = QImage(px, w, h, w, QImage::Format_Grayscale8).copy();
|
yapf_tex_decode_surface(tex, data.constData(), size_t(data.size()), s, 0) == YAPF_OK;
|
||||||
break;
|
if (ok) {
|
||||||
case 2: { // gray + alpha → RGBA
|
// RGBA 8-bit rows (blocks decoded, 16-bit / float converted)
|
||||||
image = QImage(w, h, QImage::Format_RGBA8888);
|
QByteArray rgba(int(size_t(w) * h * 4), Qt::Uninitialized);
|
||||||
for (int y = 0; y < h; ++y) {
|
ok = yapf_tex_to_rgba8(tex, s, 0, reinterpret_cast<uint8_t *>(rgba.data())) == YAPF_OK;
|
||||||
const uchar *s = px + size_t(y) * w * 2;
|
for (int y = 0; ok && y < int(h); ++y) {
|
||||||
uchar *d = image.scanLine(y);
|
memcpy(image.scanLine(y), rgba.constData() + size_t(y) * w * 4, size_t(w) * 4);
|
||||||
for (int x = 0; x < w; ++x, s += 2, d += 4) {
|
|
||||||
d[0] = d[1] = d[2] = s[0];
|
|
||||||
d[3] = s[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case 3:
|
yapf_tex_free(tex);
|
||||||
image = QImage(px, w, h, w * 3, QImage::Format_RGB888).copy();
|
if (!ok) {
|
||||||
break;
|
return KIO::ThumbnailResult::fail();
|
||||||
default:
|
|
||||||
image = QImage(px, w, h, w * 4,
|
|
||||||
(img->flags & YAPF_FLAG_PREMULT_ALPHA) ? QImage::Format_RGBA8888_Premultiplied
|
|
||||||
: QImage::Format_RGBA8888).copy();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
yapf_free(img);
|
|
||||||
|
|
||||||
if (image.width() > want.width() || image.height() > want.height()) {
|
if (image.width() > want.width() || image.height() > want.height()) {
|
||||||
image = image.scaled(want, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
image = image.scaled(want, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,22 @@ on your `PATH` — `sudo make install` puts it in `/usr/local/bin`). They work
|
||||||
in file managers that use freedesktop thumbnailers: GNOME Files (Nautilus),
|
in file managers that use freedesktop thumbnailers: GNOME Files (Nautilus),
|
||||||
Nemo, Caja, and Thunar (with tumbler). KDE Dolphin uses its own plugin
|
Nemo, Caja, and Thunar (with tumbler). KDE Dolphin uses its own plugin
|
||||||
system and is not covered yet.
|
system and is not covered yet.
|
||||||
|
|
||||||
|
Thumbnails cover every YAPF file the tool reads: version 1 and 2, GPU
|
||||||
|
blocks (BC1–BC7, BC6H), 16-bit and float textures, and the first image of
|
||||||
|
cubes, arrays and animations. KDE Dolphin: see [../kde](../kde).
|
||||||
|
|
||||||
|
### NixOS
|
||||||
|
|
||||||
|
`/usr/share` is not used on NixOS. Install the package from the
|
||||||
|
repository's `default.nix` instead — it carries the MIME type and a
|
||||||
|
thumbnailer that calls `yapf` by its full store path:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# configuration.nix
|
||||||
|
environment.systemPackages = [ (pkgs.callPackage /path/to/YAPF { }) ];
|
||||||
|
environment.pathsToLink = [ "/share/thumbnailers" ]; # if your desktop does not link it already
|
||||||
|
```
|
||||||
|
|
||||||
|
For your user only: `nix-env -if /path/to/YAPF`, then
|
||||||
|
`sh extensions/linux/install.sh` (the per-user install).
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,11 @@
|
||||||
if (!(files instanceof Array)) files = [files];
|
if (!(files instanceof Array)) files = [files];
|
||||||
|
|
||||||
for (var i = 0; i < files.length; i++) {
|
for (var i = 0; i < files.length; i++) {
|
||||||
var tmp = yapfTempFile(".png");
|
// 8 / 16-bit images come through PNG (16 bits are kept); half,
|
||||||
|
// float and BC6H HDR textures through Radiance .hdr (32-bit).
|
||||||
|
var tmp = yapfTempFile(yapfIsHdr(files[i]) ? ".hdr" : ".png");
|
||||||
if (!yapfRun([files[i].fsName, tmp.fsName], tmp)) {
|
if (!yapfRun([files[i].fsName, tmp.fsName], tmp)) {
|
||||||
alert("Could not open " + files[i].name + ".\n\n" + YAPF_TOOL_HELP);
|
alert("Could not open " + files[i].name + ".\n\n" + yapfFailText());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var doc = app.open(tmp);
|
var doc = app.open(tmp);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,16 @@ native plugin.
|
||||||
|
|
||||||
Instead of copying `yapf` next to the scripts you can put it on your `PATH`.
|
Instead of copying `yapf` next to the scripts you can put it on your `PATH`.
|
||||||
|
|
||||||
|
## What opens how
|
||||||
|
|
||||||
|
Every YAPF file the `yapf` tool reads (version 1 and 2): 8-bit images open
|
||||||
|
as 8-bit documents, 16-bit textures as 16-bit documents, half / float and
|
||||||
|
BC6H HDR textures as 32-bit documents (through Radiance `.hdr`), other GPU
|
||||||
|
blocks (BC1–BC7) decoded to 8-bit. Cubes, arrays and animations open their
|
||||||
|
first image. When a file cannot be opened, the alert shows the tool's
|
||||||
|
reason (for example a damaged file). Saving keeps 8 or 16 bits per
|
||||||
|
channel.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
The scripts are written for Photoshop CC's ExtendScript and are not yet
|
The scripts are written for Photoshop CC's ExtendScript and are not yet
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@
|
||||||
try {
|
try {
|
||||||
if (copy.mode !== DocumentMode.RGB && copy.mode !== DocumentMode.GRAYSCALE)
|
if (copy.mode !== DocumentMode.RGB && copy.mode !== DocumentMode.GRAYSCALE)
|
||||||
copy.changeMode(ChangeMode.RGB);
|
copy.changeMode(ChangeMode.RGB);
|
||||||
if (copy.bitsPerChannel !== BitsPerChannelType.EIGHT)
|
// 8 and 16 bits per channel are kept (16-bit becomes a 16-bit
|
||||||
copy.bitsPerChannel = BitsPerChannelType.EIGHT;
|
// YAPF texture); 32-bit documents are saved as 16-bit.
|
||||||
|
if (copy.bitsPerChannel === BitsPerChannelType.THIRTYTWO)
|
||||||
|
copy.bitsPerChannel = BitsPerChannelType.SIXTEEN;
|
||||||
var opts = new PNGSaveOptions();
|
var opts = new PNGSaveOptions();
|
||||||
opts.interlaced = false;
|
opts.interlaced = false;
|
||||||
copy.saveAs(tmp, opts, true, Extension.LOWERCASE);
|
copy.saveAs(tmp, opts, true, Extension.LOWERCASE);
|
||||||
|
|
@ -35,6 +37,6 @@
|
||||||
if (mips) args.push("--mips");
|
if (mips) args.push("--mips");
|
||||||
var ok = yapfRun(args, out);
|
var ok = yapfRun(args, out);
|
||||||
tmp.remove();
|
tmp.remove();
|
||||||
if (!ok) { alert("Could not write " + out.name + ".\n\n" + YAPF_TOOL_HELP); return; }
|
if (!ok) { alert("Could not write " + out.name + ".\n\n" + yapfFailText()); return; }
|
||||||
alert("Saved " + out.fsName + " (" + Math.round(out.length / 1024) + " KB)");
|
alert("Saved " + out.fsName + " (" + Math.round(out.length / 1024) + " KB)");
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,52 @@ function yapfTool() {
|
||||||
return names[0]; // rely on the PATH
|
return names[0]; // rely on the PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs the tool; returns true when `expectOut` was produced.
|
// Runs the tool with its output (stdout) going to `stdoutFile` when given.
|
||||||
function yapfRun(args, expectOut) {
|
// Returns the tool's error text ("" when it printed none).
|
||||||
|
function yapfExec(args, stdoutFile) {
|
||||||
|
var errFile = yapfTempFile(".txt");
|
||||||
var cmd = yapfQuote(yapfTool());
|
var cmd = yapfQuote(yapfTool());
|
||||||
for (var i = 0; i < args.length; i++) cmd += " " + (args[i].charAt(0) === "-" ? args[i] : yapfQuote(args[i]));
|
for (var i = 0; i < args.length; i++) cmd += " " + (args[i].charAt(0) === "-" ? args[i] : yapfQuote(args[i]));
|
||||||
|
if (stdoutFile) cmd += " > " + yapfQuote(stdoutFile.fsName);
|
||||||
|
cmd += " 2> " + yapfQuote(errFile.fsName);
|
||||||
if (YAPF_IS_WINDOWS) cmd = '"' + cmd + '"'; // cmd.exe strips one pair of quotes
|
if (YAPF_IS_WINDOWS) cmd = '"' + cmd + '"'; // cmd.exe strips one pair of quotes
|
||||||
if (expectOut.exists) expectOut.remove();
|
|
||||||
app.system(cmd);
|
app.system(cmd);
|
||||||
|
var text = yapfReadText(errFile);
|
||||||
|
errFile.remove();
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function yapfReadText(f) {
|
||||||
|
if (!f.exists || !f.open("r")) return "";
|
||||||
|
var t = f.read();
|
||||||
|
f.close();
|
||||||
|
return t.replace(/\s+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs the tool; returns true when `expectOut` was produced. The tool's
|
||||||
|
// own message (why it failed) is left in yapfLastError.
|
||||||
|
var yapfLastError = "";
|
||||||
|
function yapfRun(args, expectOut) {
|
||||||
|
if (expectOut.exists) expectOut.remove();
|
||||||
|
yapfLastError = yapfExec(args, null);
|
||||||
return expectOut.exists;
|
return expectOut.exists;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The tool's message, or the help text when the tool could not be run.
|
||||||
|
function yapfFailText() {
|
||||||
|
return /yapf: /.test(yapfLastError) ? yapfLastError : YAPF_TOOL_HELP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True when a .yapf file holds float / HDR data (half, float, BC6H), so it
|
||||||
|
// should open as a 32-bit document instead of a clamped 8-bit one.
|
||||||
|
function yapfIsHdr(file) {
|
||||||
|
var info = yapfTempFile(".txt");
|
||||||
|
yapfExec(["info", file.fsName], info);
|
||||||
|
var text = yapfReadText(info);
|
||||||
|
info.remove();
|
||||||
|
return /payload\s*:[^\n]*(half float|, float|BC6H)/.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
function yapfTempFile(ext) {
|
function yapfTempFile(ext) {
|
||||||
return new File(Folder.temp.fsName + "/yapf_" + new Date().getTime() + "_" +
|
return new File(Folder.temp.fsName + "/yapf_" + new Date().getTime() + "_" +
|
||||||
Math.floor(Math.random() * 1e6) + ext);
|
Math.floor(Math.random() * 1e6) + ext);
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,13 @@ Click any `.yapf` file in VS Code to see the image.
|
||||||
- Checkerboard behind transparent pixels.
|
- Checkerboard behind transparent pixels.
|
||||||
- **Click** to switch between *fit to window* and *100 %*; **Ctrl + scroll**
|
- **Click** to switch between *fit to window* and *100 %*; **Ctrl + scroll**
|
||||||
to zoom (pixels stay sharp when enlarged).
|
to zoom (pixels stay sharp when enlarged).
|
||||||
- Picker for every **mip level** stored in the file.
|
- Opens every YAPF file, version 1 and 2: GPU blocks (BC1–BC7, BC6H)
|
||||||
- Info bar: size, channels, sRGB / linear, GPU format hint, file size and
|
decoded, 16-bit and float textures shown in 8-bit (floats sRGB encoded).
|
||||||
compression ratio, decode time.
|
- Pickers for **mip level**, cube **face**, array **layer**, animation
|
||||||
|
**frame** and volume **slice**.
|
||||||
|
- Info bar: size, shape, storage (e.g. *GPU BC7*, *RGB half float*), sRGB /
|
||||||
|
linear, normal-map convention, format version, file size, decode time.
|
||||||
|
Damaged files say so (checksum mismatch).
|
||||||
- Reloads automatically when the file changes on disk.
|
- Reloads automatically when the file changes on disk.
|
||||||
|
|
||||||
Decoding runs in plain JavaScript inside the preview; no native code.
|
Decoding runs in plain JavaScript inside the preview; no native code.
|
||||||
|
|
@ -30,5 +34,6 @@ Should work in VS Code 1.75+ and VS Code–based editors such as VSCodium.
|
||||||
|
|
||||||
The `.vsix` packages cleanly, and the preview code was tested in a browser
|
The `.vsix` packages cleanly, and the preview code was tested in a browser
|
||||||
engine (headless Chromium) with real files: correct pixels, mip picker,
|
engine (headless Chromium) with real files: correct pixels, mip picker,
|
||||||
error message for corrupt files. **It has not yet been run inside VS Code
|
error message for corrupt files; the YAPF 2 preview was tested the same
|
||||||
itself.**
|
way (BC7, BC5, BC6H, half float, 16-bit, cube, animation, damaged and
|
||||||
|
non-YAPF files). **It has not yet been run inside VS Code itself.**
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,49 @@
|
||||||
// Webview side of the YAPF preview: decode, draw, zoom, mip picker, info.
|
// Webview side of the YAPF preview: decode, draw, zoom, surface pickers, info.
|
||||||
(function () {
|
(function () {
|
||||||
const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : null;
|
const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : null;
|
||||||
const canvas = document.getElementById('canvas');
|
const canvas = document.getElementById('canvas');
|
||||||
const info = document.getElementById('info');
|
const info = document.getElementById('info');
|
||||||
const controls = document.getElementById('controls');
|
const controls = document.getElementById('controls');
|
||||||
const errorBox = document.getElementById('error');
|
const errorBox = document.getElementById('error');
|
||||||
const GPU_NAMES = ['RGBA8', 'RGB8', 'RG8', 'R8', 'SRGB8_A8', 'SRGB8'];
|
const PIXEL_HINTS = ['RGBA8', 'RGB8', 'RG8', 'R8', 'SRGB8_A8', 'SRGB8', 'RGBA16', 'RGB16', 'RG16', 'R16',
|
||||||
|
'RGBA16F', 'RGB16F', 'RG16F', 'R16F', 'RGBA32F', 'RGB32F', 'RG32F', 'R32F'];
|
||||||
|
const BLOCKS = { 0x40: 'BC1', 0x41: 'BC2', 0x42: 'BC3', 0x43: 'BC4', 0x44: 'BC4 snorm', 0x45: 'BC5',
|
||||||
|
0x46: 'BC5 snorm', 0x47: 'BC6H', 0x48: 'BC6H signed', 0x49: 'BC7', 0x50: 'ETC2 RGB',
|
||||||
|
0x51: 'ETC2 RGB A1', 0x52: 'ETC2 RGBA', 0x53: 'EAC R11', 0x54: 'EAC R11 snorm',
|
||||||
|
0x55: 'EAC RG11', 0x56: 'EAC RG11 snorm' };
|
||||||
|
['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']
|
||||||
|
.forEach((n, i) => { BLOCKS[0x60 + i] = 'ASTC ' + n; });
|
||||||
const CH_NAMES = ['', 'Gray', 'Gray + Alpha', 'RGB', 'RGBA'];
|
const CH_NAMES = ['', 'Gray', 'Gray + Alpha', 'RGB', 'RGBA'];
|
||||||
|
const SAMPLES = ['8-bit', '16-bit', 'half float', 'float'];
|
||||||
|
const FACES = ['+X', '−X', '+Y', '−Y', '+Z', '−Z'];
|
||||||
|
|
||||||
let image = null, level = 0, fit = true, zoom = 1;
|
let tex = null, fit = true, zoom = 1;
|
||||||
|
const sel = { mip: 0, face: 0, layer: 0, frame: 0, slice: 0 };
|
||||||
|
|
||||||
function kb(n) { return n < 1024 * 1024 ? (n / 1024).toFixed(1) + ' KB' : (n / 1048576).toFixed(2) + ' MB'; }
|
function kb(n) { return n < 1024 * 1024 ? (n / 1024).toFixed(1) + ' KB' : (n / 1048576).toFixed(2) + ' MB'; }
|
||||||
|
|
||||||
|
function mipDims(m) {
|
||||||
|
return [Math.max(1, tex.width >>> m), Math.max(1, tex.height >>> m),
|
||||||
|
tex.shape === 2 ? Math.max(1, tex.depth >>> m) : 1];
|
||||||
|
}
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
const w = Math.max(1, image.width >>> level), h = Math.max(1, image.height >>> level);
|
const s = YAPF.surfaceIndex(tex, sel.frame, sel.layer, sel.face, sel.mip);
|
||||||
canvas.width = w; canvas.height = h;
|
sel.slice = Math.min(sel.slice, mipDims(sel.mip)[2] - 1);
|
||||||
canvas.getContext('2d').putImageData(new ImageData(YAPF.toRGBA(image, level), w, h), 0, 0);
|
try {
|
||||||
|
const view = YAPF.textureToRGBA(tex, s, sel.slice);
|
||||||
|
canvas.width = view.width; canvas.height = view.height;
|
||||||
|
canvas.getContext('2d').putImageData(new ImageData(view.rgba, view.width, view.height), 0, 0);
|
||||||
|
errorBox.hidden = true;
|
||||||
|
} catch (e) {
|
||||||
|
errorBox.hidden = false;
|
||||||
|
errorBox.textContent = e.message;
|
||||||
|
}
|
||||||
layout();
|
layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
function layout() {
|
function layout() {
|
||||||
if (!image) return;
|
if (!tex) return;
|
||||||
const w = canvas.width, h = canvas.height;
|
const w = canvas.width, h = canvas.height;
|
||||||
const stage = document.getElementById('stage');
|
const stage = document.getElementById('stage');
|
||||||
const scale = fit ? Math.min(1, (stage.clientWidth - 32) / w, (stage.clientHeight - 32) / h) : zoom;
|
const scale = fit ? Math.min(1, (stage.clientWidth - 32) / w, (stage.clientHeight - 32) / h) : zoom;
|
||||||
|
|
@ -31,49 +54,76 @@
|
||||||
stage.title = fit ? 'Click for 100%, Ctrl+scroll to zoom' : 'Click to fit';
|
stage.title = fit ? 'Click for 100%, Ctrl+scroll to zoom' : 'Click to fit';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function picker(key, count, label) {
|
||||||
|
if (count <= 1) return;
|
||||||
|
const el = document.createElement('select');
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = i;
|
||||||
|
o.textContent = label(i);
|
||||||
|
el.appendChild(o);
|
||||||
|
}
|
||||||
|
sel[key] = Math.min(sel[key], count - 1);
|
||||||
|
el.value = sel[key];
|
||||||
|
el.onchange = () => { sel[key] = +el.value; if (key === 'mip') buildControls(); draw(); };
|
||||||
|
controls.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildControls() {
|
||||||
|
controls.textContent = '';
|
||||||
|
picker('mip', tex.mipLevels, (m) => { const [w, h, d] = mipDims(m); return 'Mip ' + m + ' (' + w + '×' + h + (d > 1 ? '×' + d : '') + ')'; });
|
||||||
|
picker('face', tex.faces, (f) => 'Face ' + FACES[f]);
|
||||||
|
picker('layer', tex.layers, (l) => 'Layer ' + l);
|
||||||
|
picker('frame', tex.frames, (f) => 'Frame ' + (f + 1) + (tex.frameUs[f] ? ' (' + Math.round(tex.frameUs[f] / 1000) + ' ms)' : ''));
|
||||||
|
picker('slice', mipDims(sel.mip)[2], (z) => 'Slice ' + z);
|
||||||
|
}
|
||||||
|
|
||||||
function show(name, bytes) {
|
function show(name, bytes) {
|
||||||
errorBox.hidden = true;
|
errorBox.hidden = true;
|
||||||
const t = performance.now();
|
const t = performance.now();
|
||||||
try {
|
try {
|
||||||
image = YAPF.decode(bytes);
|
tex = YAPF.decodeTexture(bytes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
image = null;
|
tex = null;
|
||||||
errorBox.hidden = false;
|
errorBox.hidden = false;
|
||||||
errorBox.textContent = name + ': ' + e.message;
|
errorBox.textContent = name + ': ' + e.message.replace(/^YAPF: /, '');
|
||||||
info.textContent = '';
|
info.textContent = '';
|
||||||
|
controls.textContent = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ms = performance.now() - t;
|
const ms = performance.now() - t;
|
||||||
const raw = image.width * image.height * image.channels;
|
let raw = 0;
|
||||||
level = Math.min(level, image.mipLevels - 1);
|
for (let m = 0; m < tex.mipLevels; m++) {
|
||||||
info.textContent = [
|
const [w, h, d] = mipDims(m);
|
||||||
image.width + ' × ' + image.height,
|
raw += w * h * d * (tex.payload ? 4 : tex.channels * [1, 2, 2, 4][tex.sample]);
|
||||||
CH_NAMES[image.channels],
|
|
||||||
(image.flags & 2 ? 'sRGB' : 'linear') + (image.flags & 1 ? ', premultiplied' : ''),
|
|
||||||
'GPU ' + (GPU_NAMES[image.gpuFormat] || '?'),
|
|
||||||
kb(bytes.length) + ' (' + (100 * bytes.length / raw).toFixed(1) + '% of raw)',
|
|
||||||
'decoded in ' + ms.toFixed(1) + ' ms',
|
|
||||||
].join(' · ');
|
|
||||||
|
|
||||||
controls.textContent = '';
|
|
||||||
if (image.mipLevels > 1) {
|
|
||||||
const sel = document.createElement('select');
|
|
||||||
for (let m = 0; m < image.mipLevels; m++) {
|
|
||||||
const o = document.createElement('option');
|
|
||||||
o.value = m;
|
|
||||||
o.textContent = 'Mip ' + m + ' (' + Math.max(1, image.width >>> m) + '×' + Math.max(1, image.height >>> m) + ')';
|
|
||||||
sel.appendChild(o);
|
|
||||||
}
|
|
||||||
sel.value = level;
|
|
||||||
sel.onchange = () => { level = +sel.value; draw(); };
|
|
||||||
controls.appendChild(sel);
|
|
||||||
}
|
}
|
||||||
|
raw *= tex.frames * tex.layers * tex.faces;
|
||||||
|
const [w0, h0, d0] = mipDims(0);
|
||||||
|
const shape = tex.shape === 1 ? (tex.layers > 1 ? 'cube array of ' + tex.layers : 'cubemap')
|
||||||
|
: tex.shape === 2 ? 'volume' : tex.layers > 1 ? 'array of ' + tex.layers : '';
|
||||||
|
const storage = tex.payload === 1 ? 'GPU ' + (BLOCKS[tex.format] || '?')
|
||||||
|
: CH_NAMES[tex.channels] + ' ' + SAMPLES[tex.sample] + ', GPU ' + (PIXEL_HINTS[tex.format] || '?');
|
||||||
|
const parts = [
|
||||||
|
w0 + ' × ' + h0 + (d0 > 1 ? ' × ' + d0 : ''),
|
||||||
|
shape,
|
||||||
|
tex.frames > 1 ? tex.frames + ' frames' : '',
|
||||||
|
storage,
|
||||||
|
(tex.flags & 2 ? 'sRGB' : 'linear') + (tex.flags & 1 ? ', premultiplied' : ''),
|
||||||
|
tex.normal ? 'normal map (' + ((tex.normal & 3) === 2 ? 'DirectX' : (tex.normal & 3) === 1 ? 'OpenGL' : '?') + ')' : '',
|
||||||
|
tex.version === 2 ? 'YAPF 2' : 'YAPF 1',
|
||||||
|
kb(bytes.length) + (tex.payload ? '' : ' (' + (100 * bytes.length / raw).toFixed(1) + '% of raw)'),
|
||||||
|
'decoded in ' + ms.toFixed(1) + ' ms',
|
||||||
|
];
|
||||||
|
const author = tex.notes.find((n) => n.tag === 'AUTH');
|
||||||
|
if (author) parts.push('by ' + new TextDecoder().decode(author.data));
|
||||||
|
info.textContent = parts.filter(Boolean).join(' · ');
|
||||||
|
buildControls();
|
||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('stage').addEventListener('click', () => { fit = !fit; zoom = 1; layout(); });
|
document.getElementById('stage').addEventListener('click', () => { fit = !fit; zoom = 1; layout(); });
|
||||||
document.getElementById('stage').addEventListener('wheel', (e) => {
|
document.getElementById('stage').addEventListener('wheel', (e) => {
|
||||||
if (!e.ctrlKey || !image) return;
|
if (!e.ctrlKey || !tex) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
fit = false;
|
fit = false;
|
||||||
zoom = Math.min(64, Math.max(1 / 64, zoom * (e.deltaY < 0 ? 1.25 : 0.8)));
|
zoom = Math.min(64, Math.max(1 / 64, zoom * (e.deltaY < 0 ? 1.25 : 0.8)));
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
* Round-trips many sizes, channel counts and image kinds, checks mip
|
* Round-trips many sizes, channel counts and image kinds, checks mip
|
||||||
* chains, single- vs multi-threaded decode, the bundled sample file, and
|
* chains, single- vs multi-threaded decode, the bundled sample file, and
|
||||||
* that corrupted or truncated files are rejected without crashing.
|
* 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.
|
* Exit code 0 means every test passed.
|
||||||
*/
|
*/
|
||||||
#include "../yapf.h"
|
#include "../yapf.h"
|
||||||
|
|
@ -14,6 +16,7 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
/* FNV-1a of the decoded pixels of other/YAPF.YAPF (the YAPF logo). */
|
/* FNV-1a of the decoded pixels of other/YAPF.YAPF (the YAPF logo). */
|
||||||
#define SAMPLE_HASH 0x8891c2b4u
|
#define SAMPLE_HASH 0x8891c2b4u
|
||||||
|
|
@ -171,6 +174,523 @@ static uint32_t fnv(const uint8_t *p, size_t n) {
|
||||||
return h;
|
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_preview(void) {
|
||||||
|
uint8_t out[4 * 4 * 4];
|
||||||
|
/* gray + alpha u16 */
|
||||||
|
yapf_texture_t d = desc(4, 4, 2, YAPF_SAMPLE_U16);
|
||||||
|
yapf_texture_t *t = yapf_tex_create(&d);
|
||||||
|
uint16_t *p = (uint16_t *)t->data[0];
|
||||||
|
for (int i = 0; i < 16; i++) { p[2 * i] = 65535; p[2 * i + 1] = 32896; }
|
||||||
|
int ok = yapf_tex_to_rgba8(t, 0, 0, out) == YAPF_OK &&
|
||||||
|
out[0] == 255 && out[1] == 255 && out[2] == 255 && out[3] == 128;
|
||||||
|
yapf_tex_free(t);
|
||||||
|
/* linear half 0.2140 → sRGB 128, 2.0 clamps, alpha stays linear */
|
||||||
|
d = desc(4, 4, 4, YAPF_SAMPLE_F16);
|
||||||
|
t = yapf_tex_create(&d);
|
||||||
|
p = (uint16_t *)t->data[0];
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
p[4 * i] = yapf_float_to_half(0.2158605f); p[4 * i + 1] = yapf_float_to_half(2.0f);
|
||||||
|
p[4 * i + 2] = yapf_float_to_half(-1.0f); p[4 * i + 3] = yapf_float_to_half(0.5f);
|
||||||
|
}
|
||||||
|
ok = ok && yapf_tex_to_rgba8(t, 0, 0, out) == YAPF_OK &&
|
||||||
|
out[0] == 128 && out[1] == 255 && out[2] == 0 && out[3] == 128;
|
||||||
|
/* BC7 and BC6H go through the block decoders */
|
||||||
|
yapf_texture_t *c = NULL;
|
||||||
|
ok = ok && yapf_tex_compress(t, YAPF_GPU_BC6H, &c) == YAPF_OK &&
|
||||||
|
yapf_tex_to_rgba8(c, 0, 0, out) == YAPF_OK && out[3] == 255 && out[1] == 255;
|
||||||
|
yapf_tex_free(c);
|
||||||
|
yapf_tex_free(t);
|
||||||
|
CHECK(ok, "yapf_tex_to_rgba8 converts every payload for display");
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
int main(int argc, char **argv) {
|
||||||
static const int sizes[][2] = {
|
static const int sizes[][2] = {
|
||||||
{1, 1}, {1, 7}, {7, 1}, {63, 63}, {64, 64}, {65, 65}, {127, 1},
|
{1, 1}, {1, 7}, {7, 1}, {63, 63}, {64, 64}, {65, 65}, {127, 1},
|
||||||
|
|
@ -197,6 +717,17 @@ int main(int argc, char **argv) {
|
||||||
printf("note: sample %s not found, skipped\n", sample);
|
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();
|
||||||
|
v2_preview();
|
||||||
|
|
||||||
printf("%d tests, %d failures\n", tests, fails);
|
printf("%d tests, %d failures\n", tests, fails);
|
||||||
return fails != 0;
|
return fails != 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
926
tools/yapf_cli.c
926
tools/yapf_cli.c
File diff suppressed because it is too large
Load diff
369
yapf.h
369
yapf.h
|
|
@ -38,6 +38,13 @@
|
||||||
* loader can upload the decoded buffer straight to a texture without any
|
* loader can upload the decoded buffer straight to a texture without any
|
||||||
* additional transcoding step.
|
* 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)
|
* FILE FORMAT (version 1)
|
||||||
* =========================================================================
|
* =========================================================================
|
||||||
|
|
@ -140,6 +147,37 @@
|
||||||
* ─────────────────────────────────────────────────
|
* ─────────────────────────────────────────────────
|
||||||
* bit 0 PREMULT_ALPHA alpha is premultiplied into RGB
|
* bit 0 PREMULT_ALPHA alpha is premultiplied into RGB
|
||||||
* bit 1 SRGB pixels are in sRGB gamma (informational only)
|
* 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
|
#ifndef YAPF_H
|
||||||
|
|
@ -156,11 +194,58 @@
|
||||||
#define YAPF_GPU_R8 0x03
|
#define YAPF_GPU_R8 0x03
|
||||||
#define YAPF_GPU_SRGB8_A8 0x04
|
#define YAPF_GPU_SRGB8_A8 0x04
|
||||||
#define YAPF_GPU_SRGB8 0x05
|
#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 ───────────────────────────────────────────────────────────── */
|
/* ── Flags ───────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
#define YAPF_FLAG_PREMULT_ALPHA 0x01
|
#define YAPF_FLAG_PREMULT_ALPHA 0x01
|
||||||
#define YAPF_FLAG_SRGB 0x02
|
#define YAPF_FLAG_SRGB 0x02
|
||||||
|
#define YAPF_FLAG_COVERAGE 0x04 /* v2: mips preserve alpha-test coverage */
|
||||||
|
|
||||||
/* ── Channel constants ───────────────────────────────────────────────── */
|
/* ── Channel constants ───────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
|
@ -176,12 +261,16 @@
|
||||||
#define YAPF_ERR_IO -2
|
#define YAPF_ERR_IO -2
|
||||||
#define YAPF_ERR_OOM -3
|
#define YAPF_ERR_OOM -3
|
||||||
#define YAPF_ERR_CORRUPT -4
|
#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 ──────────────────────────────────────────────────────────── */
|
/* ── Limits ──────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
#define YAPF_MAX_DIM 65535u
|
#define YAPF_MAX_DIM 65535u
|
||||||
#define YAPF_MAX_MIPS 16u
|
#define YAPF_MAX_MIPS 16u
|
||||||
#define YAPF_TILE_SIZE 64u
|
#define YAPF_TILE_SIZE 64u
|
||||||
|
#define YAPF_BLOCK_TILE 16u /* v2 block tiles: 16 × 16 GPU blocks */
|
||||||
|
|
||||||
/* ── Image descriptor ────────────────────────────────────────────────── */
|
/* ── Image descriptor ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
|
@ -196,6 +285,150 @@ typedef struct {
|
||||||
uint8_t **mips; /* mips[0]=base alias, mips[1..mip_levels-1] owned */
|
uint8_t **mips; /* mips[0]=base alias, mips[1..mip_levels-1] owned */
|
||||||
} yapf_image_t;
|
} 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 ──────────────────────────────────────────────────────── */
|
/* ── Public API ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|
@ -249,6 +482,142 @@ void yapf_free_buffer(void *data);
|
||||||
*/
|
*/
|
||||||
void yapf_free(yapf_image_t *img);
|
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_tex_to_rgba8 — one slice of a decoded surface as 8-bit RGBA for
|
||||||
|
* display (thumbnails, previews): gray is expanded, 16-bit is rounded to
|
||||||
|
* 8 bits, float and half are clamped to 0..1 and sRGB encoded (they are
|
||||||
|
* linear), GPU blocks are decoded. out holds w × h × 4 bytes of that
|
||||||
|
* mip. Returns YAPF_ERR_UNSUPPORTED for ETC2 / EAC / ASTC blocks.
|
||||||
|
*/
|
||||||
|
int yapf_tex_to_rgba8(const yapf_texture_t *tex, uint32_t surface, uint32_t slice,
|
||||||
|
uint8_t *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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue