diff --git a/README.md b/README.md index 97d312d..cb7a21d 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,7 @@ See Color-pair dictionary. The six color deltas, the top-table reference, the ma ======= ## BC4 / BC5 -BC4 (one 8-bit channel) and BC5 (red + green channels) reuse the BC1 machinery: static Rice-Golomb models fitted from a histogram pre-pass, 64 independent strips, and a self-describing byte stream. The entry points are `packed_bc4_maxsize` / `packed_bc5_maxsize`, `bcp_compress_bc4` / `bcp_compress_bc5`, `bcp_get_stats_bc5`, and `bcp_decompress_bc4` / `bcp_decompress_bc5` (see `src/bc_packed.h`), with the same signature shape as their BC1 counterparts. +BC4 (one 8-bit channel) and BC5 (red + green channels) reuse the BC1 machinery: static Rice-Golomb models fitted from a histogram pre-pass, 64 independent strips, and a self-describing byte stream. The entry points are `packed_bc4_maxsize` / `packed_bc5_maxsize`, `bcp_compress_bc4` / `bcp_compress_bc5`, and `bcp_decompress_bc4` / `bcp_decompress_bc5` (see `src/bc_packed.h`), with the same signature shape as their BC1 counterparts. ### Blocks @@ -276,10 +276,10 @@ That is 3 endpoint-range families x 8 prediction values = 24 index models per ch ```text family = |endpoint0 - endpoint1| (unsigned range in [0; 255]) - < 8 → 0, < 16 → 1, else → 2 + < 5 → 0, < 40 → 1, else → 2 ``` -The 8-symbol models use a compact rank table (8 entries instead of 256): `3 + num_symbols ≤ 11` bytes each in the stream. The family thresholds (8 / 16) are the design values; refining them (for example into more range buckets) is the obvious tuning lever if the ratio needs more headroom. +The 8-symbol models use a compact rank table (8 entries instead of 256): `3 + num_symbols ≤ 11` bytes each in the stream. The family thresholds (5 / 40) are fitted values: the index-XOR distributions drift smoothly with the endpoint contrast, and a sweep of the 3-family splits over the image suite puts the minimum here — every one of the 10 suite images improves and the stream shrinks 0.24% versus the original 8 / 16. Changing the family count instead (2 or 4 families) measured worse. ### Compressed stream @@ -302,7 +302,6 @@ For BC5 the red channel's models and seed come first, then the green channel's, * CPU + GPU: `bcp_compress_bc4/bc5` and `bcp_decompress_bc4/bc5` are implemented and tested; the GPU decoders are the `bcp_decompress_bc4` / `bcp_decompress_bc5` compute kernels of [src/bc_decompress.metal](src/bc_decompress.metal): one thread per strip, bit-identical to the CPU. Measured on the M5 Pro (1024×1024, 1000 runs): **14540 MiB/s** for BC4, **7868 MiB/s** for BC5. * `width >= 16` and `height >= 256` are required (multiples of 4, as BC1); textures whose height in blocks is not a multiple of 64 silently drop the trailing block rows. * The output buffer must be at least `packed_bc4_maxsize` / `packed_bc5_maxsize` — 2× the standard block size (16 / 32 bytes per 4×4 block), the BC1 convention. -* `bcp_get_stats_bc5` reports the range-coded payload after a successful `bcp_compress_bc5`: `endpoint_bytes`, `pair_bytes` (the whole index payload), and `dict_bytes` (reserved for a future index dictionary, currently 0). ### Validation @@ -315,9 +314,46 @@ Everything is validated byte-exact over all 64 strips, on CPU and GPU: The 8-bit formats leave less headroom than BC1: the indices of these maps sit close to 3 near-uniform bits each, so most of the gain comes from the endpoint deltas. + +======= +## BC3 + +BC3 combines a BC1 color block and a BC4 alpha block in the standard D3D layout, 16 bytes per 4×4 block. The entry points are `packed_bc3_maxsize`, `bcp_compress_bc3`, and `bcp_decompress_bc3` (see `src/bc_packed.h`), with the same signature shape as their BC1 counterparts. + +### Scheme + +BC3 is the composition of the two pipelines above, applied to the two halves of the block grid: the BC1 pipeline on the color half, the single-channel BC4 pipeline on the alpha half. No payload logic, model, or context state is new: the strip decomposition, the zigzag chains, the BC1 predictors / top table / color-pair dictionary, and the BC4 endpoint / index models are all the existing ones, fitted on their own half as for the standalone formats. + +### Compressed stream + +Self-describing, as the other streams (no width, height, or format): + +```text +* u32 little-endian: the size in bytes of the BC1 substream that follows. +* The BC1 stream of the color half, exactly the [Compressed stream](#compressed-stream) format. +* The BC4 stream of the alpha half, exactly the [BC4 / BC5 → Compressed stream](#compressed-stream-1) format with 1 channel. +``` + +Each substream is self-contained (its own models, strip seed, 63 strip offsets, 64 strips) and ends 4-byte aligned, so the two can be decoded independently; the 64 strips of both halves share the same decomposition, so one `bcp_decompress_bc3` call decodes one strip of both. + +### Status + +* CPU only: `bcp_compress_bc3` / `bcp_decompress_bc3` are implemented and tested; BC3 is niche, so there is no Metal decoder (the BC1 and BC4 kernels of [src/bc_decompress.metal](src/bc_decompress.metal) cover the two halves, a `bcp_decompress_bc3` kernel is a possible follow-up). +* `width >= 16` and `height >= 256` are required (multiples of 4, as BC1); textures whose height in blocks is not a multiple of 64 silently drop the trailing block rows. +* The output buffer must be at least `packed_bc3_maxsize` — 2× the standard BC3 block size (32 bytes per 4×4 block), the BC1 convention, plus the 4-byte prefix. + +### Validation + +Everything is validated byte-exact over all 64 strips on CPU: + +* 4 synthetic 256×256 roundtrips (flat, checkerboard, gradient, SplitMix32 random): the color half and the alpha half each use the corresponding BC1 / BC4 pattern, so the random pattern forces the raw-mode fallback and the full model usage in both halves at once. +* 2 albedo + alpha maps (`images/bc3`), 1024×1024, converted per block with stb_dxt: the per-image ratio is printed to stdout (no CSV) and the suite average must stay above 1:1. Measured: **2.7149:1** average (2.9445:1 for leak-stain_albedo, 2.4853:1 for leaves). The ratio is dominated by the BC1 color half, which keeps the BC1 headroom; the 8-bit alpha half compresses like a standalone BC4. + ## Images The images used in the unit test come from this sources: https://opengameart.org/content/tiny-texture-pack-2 https://r0k.us/graphics/kodak/ + +The `images/bc3` set (albedo + alpha maps) is used by the BC3 suite; the leak-stain and leaves sources are the project author's own assets. diff --git a/failed_experiments.md b/failed_experiments.md index edc6e99..6247569 100644 --- a/failed_experiments.md +++ b/failed_experiments.md @@ -43,6 +43,7 @@ Rice `k` and rank table per endpoint fits better. Kept. | 17 | Per-row color predictor (1 bit/row, the open-thread lead of exp 9/11) | offline study; same color symbols as per-strip, only the flag word differs — a loss on 74/74 | n/a (+0.16…+0.70%, 0/74 win) | not adopted — per-strip (exp 11) is already optimal; lead closed | | 18 | Per-model raw-byte `q_escape` for static models, searched jointly with the Rice `k` against the histogram (replaces the fixed per-k escape table; one header byte per model) | round-trips; colors −1.19%, 74/74 improved, worst +0.01% | **1.4438:1** (+0.52%) | **kept — this is the new baseline** | | 20 | Color-pair sliding dictionary: a 1-bit flag + a `ceil(log2(min(pos,256)))`-bit reference to a matching block within the last 256 replaces the six color deltas when the two endpoint colors exactly repeat, plus a per-strip enable flag | round-trips (CPU + GPU); forced version was a net loss (25 improved / 44 regressed), the per-strip version: 44 improved / 5 regressed / 25 flat, the flat ones byte-identical to the baseline | 1.4587 → 1.4654 (+0.46%) avg; 1.4020 → 1.4073 (+0.38%) byte-weighted; best +2.95% (wall2), worst −0.14% (kodim15) | **kept — this is the new baseline** (the per-strip flag turns the forced-match net loss into a net win) | +| 21 | `bc4_index_family` endpoint-range thresholds: 30+ split variants on the 10-image bc4/bc5/bc3 suite (3-family 8/16 → 5/40; also 2- and 4-family splits) | round-trips (CPU + GPU); 10/10 images improved, best −1.04% (bc3 leak-stain alpha), worst −0.02%; 2- and 4-family counts all lose, the 3-family minimum is a wide plateau (t1 4..6 / t2 32..48 all ≥ −0.19%) | bc4 1.1244 → 1.1279, bc5 1.0756 → 1.0772, bc3 2.7149 → 2.7307 (suite averages); stream −0.235% overall | **kept — this is the new baseline for bc4/bc5/bc3** (the 74-image bc1 suite is untouched; the README's "obvious tuning lever" exercised) | --- diff --git a/images/bc3/leak-stain_albedo.png b/images/bc3/leak-stain_albedo.png new file mode 100644 index 0000000..755568a Binary files /dev/null and b/images/bc3/leak-stain_albedo.png differ diff --git a/images/bc3/leaves.png b/images/bc3/leaves.png new file mode 100644 index 0000000..7cacda5 Binary files /dev/null and b/images/bc3/leaves.png differ diff --git a/plan/bc4_bc5_support.md b/plan/bc4_bc5_support.md deleted file mode 100644 index 0f7584c..0000000 --- a/plan/bc4_bc5_support.md +++ /dev/null @@ -1,291 +0,0 @@ -# BC4 / BC5 support — implementation plan - -Status: **done 2026-10-11** (approved same day, all open questions answered; implemented, validated, documented in the README). - -Extends `bc_packed` with lossless asymmetric compression of BC4 (single channel, 8 bytes per -4×4 block) and BC5 (two channels, 16 bytes per 4×4 block) textures, mirroring the BC1 pipeline: -static Rice-Golomb models fitted from a histogram pre-pass, 64 independent strips for parallel -decompression, and a byte-stream format the decoder can parse standalone. - -Design source: the BC4 / BC5 section of `README.md` (the repo-root note was merged in and deleted, Q9). -Entropy expectations (indices ≈ 2.97 bits raw, endpoints save ≈ 1.8 bits/symbol → expected -≈ 1.05–1.15:1) came from the study tool `tools/bc5_analysis.c`, which was removed before the API -was finalized; the image suite in §6.2 plays the role of the end-to-end check. - ---- - -## 1. Block layout (I/O contract) - -Standard D3D layout, exactly as `stb_compress_bc4_block` / `stb_compress_bc5_block` write it: - -```c -typedef struct bc4_block -{ - uint8_t endpoint[2]; // [0] = max endpoint (stb mx), [1] = min endpoint (stb mn) - uint8_t indices[6]; // 16 x 3-bit indices, little-endian 48-bit field, raster order -} bc4_block; // 8 bytes - -// BC5 block = 16 bytes: the red bc4_block followed by the green bc4_block -``` - -- Index `i` (raster order, index 0 = top-left pixel) occupies bits `[3i, 3i+3)` of the 48-bit - little-endian field in `indices[0..5]`. -- BC5 block `bi` = red channel at bytes `[bi*16, bi*16+7]`, green at `[bi*16+8, bi*16+15]`. -- Verified against the committed study tool: its 3-bit repack of stb output is byte-identical to - the stb 6-byte field, so `bcp_compress_bc4/bc5` consume stb output directly (open question Q1). - -## 2. Compression scheme - -### 2.1 Endpoints (one channel) - -Per-endpoint wrapped 8-bit delta (`curr - pred` mod 256, `prev + delta` on decode — the -`delta_encode_wrap` / `delta_decode_wrap` of the design doc), with one static 256-symbol model -per endpoint ("1 context per endpoint" → 2 models per channel). - -Prediction per block of a strip (same zigzag chain as BC1: -`zigzag_x = (y & 1) ? x : width_blocks - 1 - x`, `previous` initialized to the strip seed): - -```text -strip first row (y == start_y): pred = previous (seed for the strip's first block) -row start (x == 0, later rows): pred = top (block of the row above, same column) -other blocks: pred = previous + top - top_previous (mod 256, per endpoint) -``` - -- `top` = block at `(y-1, same physical x)`, `top_previous` = block above the chain-previous. - All references are inside the strip (or the seed), so strips stay independent. -- Strip seed = integer average over the 64 strips of the first block in chain order of each strip - (`x0 = (start_y & 1) ? 0 : width_blocks - 1`), per channel, per endpoint — same convention as - the BC1 color seed. (Open question Q2: the study tool's "exact copy" predictor uses the 2D - formula at row starts with an `above → seed` fallback; we implement the doc's top-only.) - -### 2.2 Indices (one channel) - -16 3-bit indices in raster order: - -```text -index 0: 3 raw bits (le_write_bits / le_read_bits, no model) -index i: symbol = prediction XOR index[i], encoded with a static 8-symbol model - prediction = index[i-4] (directly above) for i in {4, 8, 12} - = index[i-1] (left) otherwise - context = family * 8 + prediction - family = |endpoint[0] - endpoint[1]| < 8 ? 0 - |endpoint[0] - endpoint[1]| < 16 ? 1 : 2 (unsigned range, Q3) -→ 3 families x 8 prediction values = 24 models per channel -``` - -The family is derived from the (already decoded) endpoints, so it costs no stream bits and is -identical on encoder and decoder. No top table, no dictionary for the indices (Q5). - -### 2.3 BC5 - -Two channels, each with its own 2 endpoint models + 24 index models + 2-byte seed -(52 models + 4 seed bytes in the header for BC5, 26 + 2 for BC4). One implementation -parameterized by `num_channels` (1 or 2): channel `c` of block `bi` is -`((const bc4_block*)image + bi * num_channels + c)`. Per-block payload order: channel 0 -(red) complete, then channel 1 (green); per channel: endpoint 0, endpoint 1, index 0, indices 1..15. - -## 3. Stream format - -```text -* [num_channels * 2] endpoint models — per channel, per endpoint: 256-symbol static model, - 3 + num_symbols bytes each, no padding (save_static_model / load_static_model, as BC1). -* [num_channels * 24] index models — per channel, family-major: 8-symbol static model each. -* Strip seed: 2 bytes per channel (endpoint 0, endpoint 1). -* Padded to a 2-byte boundary. -* 63 strip offsets (uint16_t), relative deltas in dword units (strip 0 implicit zero), as BC1. -* Padded to a 4-byte boundary. -* 64 strip bitstreams, each 4-byte aligned, each flushed independently (le_stream), as BC1. -``` - -No per-strip flags: the endpoint predictor is fixed by §2.1 (BC1's per-strip predictor flag and -color-pair dictionary do not carry over). No format magic: the bc4/bc5 decompressors are -separate entry points and know their own header (model count) and block size. - -## 4. Public API (src/bc_packed.h) - -```c -// Maximum size of the compressed stream for a given BC4 / BC5 texture, in bytes: -// 2x the standard block size (16 / 32 bytes per 4x4 block), the BC1 convention. -size_t packed_bc4_maxsize(uint32_t width, uint32_t height); -size_t packed_bc5_maxsize(uint32_t width, uint32_t height); - -// [bc4_image] / [bc5_image]: standard D3D BC4 / BC5 blocks, (width/4) * (height/4) of them, -// width and height multiples of 4, width >= 16, height >= 256 (64 strips x >= 4 block rows). -// [num_strips] receives the number of strips (64). Returns the stream size, or 0 on failure. -size_t bcp_compress_bc4(bcp_context* ctx, const void* bc4_image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips); -size_t bcp_compress_bc5(bcp_context* ctx, const void* bc5_image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips); - -// Extracts statistics after compression (valid after a successful bcp_compress_bc5). -// pair_bytes = the whole index payload (name kept for the study tool, Q5); dict_bytes is -// reserved for a future inter-block index dictionary, currently always 0. -typedef struct bcp_stats_bc5 -{ - uint32_t endpoint_bytes; // 4 endpoint delta symbols per block (2 endpoints x 2 channels) - uint32_t pair_bytes; // index payload: 3 raw bits + 15 contextual symbols per channel block - uint32_t dict_bytes; // reserved, always 0 for now -} bcp_stats_bc5; -void bcp_get_stats_bc5(bcp_context* ctx, bcp_stats_bc5* stats); - -// Decompresses one strip into [output] (the full standard BC4/BC5 block grid); call it -// num_strips times for a complete image. Returns false on error, as bcp_decompress_bc1. -bool bcp_decompress_bc4(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index); -bool bcp_decompress_bc5(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index); -``` - -- `bcp_context` gains (opaque, no ABI surface): `le_model bc4_endpoint_model[2][2]`, - `le_model bc4_index_model[2][24]`, `uint8_t bc4_strip_seed[2][2]`, and per-component bit - counters for the stats. ≈ 27 KB added to the malloc'd context — kept static (like the BC1 - models), no extra allocation path. -- 2× standard maxsize is provably sufficient: every model is fitted as min(Rice, raw), and the - full-raw fallback costs ≤ 2·8 + 3 + 15·3 = 53 bits per 8-byte channel block, so the payload - never exceeds the standard texture size (plus the small static header). -- GPU/Metal decoder: **out of scope** (Q6). No `bc_decompress.metal` change. - -## 5. Core implementation (src/bc_packed.c) - -Appended after the BC1 section, same file, same conventions (Allman, `//` comments, `le_*` -reuse, no new dependencies, no global state): - -```c -// constants -#define BC4_INDEX_FAMILIES (3) -#define BC4_INDEX_CONTEXTS (BC4_INDEX_FAMILIES * 8) // 24 -#define BCP_NUM_CHANNELS (2) // max channels; BC4 uses 1, BC5 uses 2 - -// helpers -static inline uint8_t bc4_endpoint_predict(bool first_row, bool row_start, uint8_t prev, uint8_t top, uint8_t top_prev); -static inline uint8_t bc4_index_family(uint8_t e0, uint8_t e1); -static inline void bc4_unpack_indices(const bc4_block* b, uint8_t idx[16]); // 48-bit LE field -> 16 x 3-bit -static inline void bc4_pack_indices(const uint8_t idx[16], bc4_block* b); - -// per-channel histogram: 2 x 256 endpoint + 24 x 8 index symbols (~2.8 KB) -typedef struct bc4_hists -{ - uint32_t endpoint[2][256]; - uint32_t index[BC4_INDEX_CONTEXTS][8]; -} bc4_hists; - -// model fit: pass 1 computes the per-channel seeds, pass 2 accumulates the histograms with the -// exact strip predictors of §2 (one pass suffices: there is no per-strip mode decision) -static void bc4_init_models(bcp_context* ctx, const void* input, uint32_t width_blocks, uint32_t strip_width, uint32_t num_strips, uint32_t num_channels); - -// shared implementation, num_channels 1 or 2 -static size_t bcp_compress_bc45(bcp_context* ctx, const void* image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips, uint32_t num_channels); -static bool bcp_decompress_bc45(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index, uint32_t num_channels); -``` - -- Encoder: per strip, walk the BC1 zigzag chain (`previous` seeded from `bc4_strip_seed`, plus - `top` / `top_previous` bookkeeping); per block, per channel: endpoint symbols, 3 raw bits, - 15 contextual index symbols — the same three call sites (hist / encode / decode) share the - `bc4_endpoint_predict` helper; each per-channel loop stays well under the 50-line - duplication threshold (same pattern as the BC1 color path). -- Stream header save/load reuses `save_static_model` / `load_static_model` / `stream_align` / - `read_stream_align`; strip offsets and per-strip `le_stream` handling mirror `bcp_compress_bc1` - verbatim (u16 dword deltas, 4-byte strip alignment, `assert(strip_size < 262144)`). -- Guards: `width < 16 || height < 256 || !pointers` → 0 / false, as BC1; `height_blocks % 64 != 0` - silently drops trailing block rows, as BC1. - -## 6. Tests - -### 6.1 Synthetic roundtrips (tests/synthetic_tests.c, no new CMake entry) - -A `bc45_roundtrip(image, width, height, num_channels)` helper mirroring the BC1 one (compress -into `packed_bc?_maxsize`, decompress all strips, byte-exact compare), exercised per pattern -with both 1 and 2 channels at 256×256: flat, checkerboard, gradient, SplitMix32 random. The -random pattern forces wide endpoint ranges (family 2), near-uniform deltas (raw-mode fallback), -and full 24-context usage — the edge cases of the format. - -### 6.2 Image suite (tests/image_tests.c, existing HAS_IMAGE_TESTS guard, stb already included) - -`test_bc4_images` + `test_bc5_images`, added to `suite_image` (no main_test.c change): - -- Hardcoded filenames (no folder scan, per design doc): - `images/bc4/{grey_roof_tiles_02, patterned_cobblestone, red_brick, rough_wood}_ao_1k.png` and - `images/bc5/{...}_nor_dx_1k.png` (all 1024×1024). -- Per image: stb_image load (RGBA) → per 4×4 block: BC4 = 16 red-channel bytes through - `stb_compress_bc4_block`; BC5 = 32 interleaved R,G bytes through `stb_compress_bc5_block` → - `bcp_compress_bc?` (assert success) → ratio = standard size / stream length, printed to stdout - per image + average (no CSV, per design doc) → assert **suite-average ratio ≥ 1.0** (Q8) → - decompress all 64 strips, byte-exact compare. - -### 6.3 ~~Study tool link check~~ - -Dropped: `tools/bc5_analysis.c` was removed (the API was finalized without it). The image -suite of §6.2 exercises the full compress → stats → decompress path instead. - -## 7. Validation checklist - -1. ✅ Build clean under `-Werror -Wall -Wextra -Wpedantic`. -2. ✅ `./build/unit_tests` — 24/24 pass; BC1 unchanged (74-image average 1.4654:1, all - synthetic/benchmark/gpu tests green). -3. ✅ 8 synthetic roundtrips + 8 image roundtrips, suite averages above 1:1 (bc4 1.1244:1, - bc5 1.0756:1; all per-image ratios also above 1:1). -4. ~~`bc5_analysis` tool builds~~ — dropped, the tool was removed (see §6.3). -5. ✅ README: BC4/BC5 sections (blocks, endpoints, indices, stream, status, validation); - design note folded in and deleted (Q9). -6. ✅ No test failures, no sub-1:1 image — nothing to document in `failed_experiments.md`. - -## 8. Decisions (answered 2026-10-11) - -Q1 yes (standard D3D, direct stb output). Q2 doc version: top-only at row starts. Q3 yes -(unsigned range). Q4 yes (per-channel models). Q5 keep the tool's `pair_bytes`/`dict_bytes` -names. Q6 CPU only, GPU is a future task. Q7 yes (2× standard). Q8 **average** ≥ 1:1 per suite -(not per-image). Q9 merge the design note into the README and delete it. Additionally, the -index models use a new compact `le_model8` (8-entry rank tables, 20 bytes retained) instead of -the full 516-byte `le_model`: context growth ≈ 3 KB, not 27 KB. - ---- - -Original questions (kept for the record): - -Q1. **I/O layout.** Confirm `bcp_compress_bc4/bc5` take standard D3D blocks (3-bit indices in the - 6-byte little-endian field, ep0 = max / ep1 = min) — i.e. direct stb output, and - decompression reproduces standard D3D bytes. (The committed study tool implies exactly this; - its repack is byte-identical to stb output.) - -Q2. **Row-start endpoint predictor.** The doc says "first block of a row uses only top"; the - study tool's "exact copy of bc5_predict_endpoints" uses the 2D formula `prev + top − above` - at row starts (with `above → seed` when the chain-previous sits in the strip's first row). - Plan: implement the doc (top-only — fewer special cases, no seed mixed into the 2D term). - OK? If so, optionally sync the tool's histogram to match the shipped predictor. - -Q3. **Family range signedness.** `abs(e1 − e0)` as an unsigned value in [0, 255] (the true - palette contrast)? Study shows ~80% of normal-map blocks land in family 2 (range 16–63), so - only narrow-contrast blocks are affected by the interpretation. - -Q4. **Per-channel models.** Each channel gets its own endpoint + index models (52 in the BC5 - header, ≤ ~2.1 KB)? Study shows red/green midpoints and ranges are uncorrelated, so - per-channel is the right call. Confirm. - -Q5. **`bcp_stats_bc5` fields.** The committed tool pins the names `endpoint_bytes` / - `pair_bytes` / `dict_bytes`. The doc's scheme has no pair model and no dictionary. Plan: - keep the names untouched (tool stays as-is): `pair_bytes` = index payload bytes, - `dict_bytes` = reserved, 0. Alternative: rename to `index_bytes`, drop `dict_bytes`, patch - the tool's one printf. Which? - -Q6. **GPU scope.** No Metal decoder for BC4/BC5 in this change (design doc's tests are - CPU-only)? GPU support would be a follow-up. - -Q7. **maxsize.** 2× standard (16 / 32 bytes per block), mirroring BC1 — provably sufficient - (§4). Confirm. - -Q8. **Threshold.** The study shows the 1:1 target is *thin* on these normal maps: index entropy - ≈ 2.97 bits raw, the doc's predictor recovers only ≈ 0.12 bits/index, endpoints save - ≈ 1.8 bits/symbol → expected ≈ 1.05–1.15:1 before Rice quantization. Do we assert - per-image ≥ 1.0 (plan) or average ≥ 1.0? If an image misses, tune the models (study levers: - 8 range buckets instead of 3 families, 12-bit-row delta) or document the miss in - `failed_experiments.md`? - -Q9. **Docs.** README gets the full BC4/BC5 documentation; the repo-root design note - (`# BC4 and BC5 support.md`) is merged into the README and deleted (or moved to `plan/`)? - -## 9. Execution order - -1. ✅ `bc_packed.h`: block docs + API (Q1–Q7 answers first) — compile check. -2. ✅ `bc_packed.c`: helpers + `bc4_init_models` + `bcp_compress_bc45` + `bcp_decompress_bc45` + - public wrappers. -3. ✅ Synthetic tests (§6.1) — roundtrip correctness before anything else. -4. ✅ Image tests (§6.2) — real ratios above 1:1, no tuning needed (Q8). -5. ✅ README + design-note cleanup (§7.5) (tool link check dropped, §6.3). -6. ✅ Full `./build/unit_tests` run + BC1 regression check. Commit pending user permission (no - push without permission). diff --git a/src/bc_decompress.metal b/src/bc_decompress.metal index dab1044..c0ec61b 100644 --- a/src/bc_decompress.metal +++ b/src/bc_decompress.metal @@ -552,9 +552,9 @@ uint8_t bc4_index_family(uint8_t e0, uint8_t e1) { uint32_t range = (e0 >= e1) ? (uint32_t)(e0 - e1) : (uint32_t)(e1 - e0); - if (range < 8) + if (range < 5) return 0; - if (range < 16) + if (range < 40) return 1; return 2; diff --git a/src/bc_packed.c b/src/bc_packed.c index 557cd0b..e97252b 100644 --- a/src/bc_packed.c +++ b/src/bc_packed.c @@ -539,16 +539,12 @@ struct bcp_context // matching blocks) pay no flag overhead. see the per-strip dictionary enable in init_static_models uint64_t strip_dict_enable; - // bit counters of the range-coded payload, per component - uint64_t colors_bits, reference_bits, mask_bits, difference_bits; - // bc4/bc5: per-channel static models — the 2 endpoint models (256 symbols) and the 24 // index models (8 symbols each, 3 endpoint-range families x 8 prediction values); each // retains the full 516-byte le_model in memory, the stream only stores num_symbols entries le_model bc4_endpoint_model[BCP_MAX_CHANNELS][2]; // [channel][endpoint] le_model bc4_index_model[BCP_MAX_CHANNELS][BC4_INDEX_CONTEXTS]; uint8_t bc4_strip_seed[BCP_MAX_CHANNELS][2]; // [channel][endpoint] - uint64_t bc4_endpoint_bits, bc4_index_bits; }; typedef struct bc1_block @@ -930,7 +926,7 @@ uint32_t nearest32(const uint32_t* table, uint32_t table_size, uint32_t bitfield // [sample_step] to keep the round cheap, and [start_offset] de-aliases consecutive rounds. // The bucket feedback drops the skip while the accumulated popcount error is high, so a round // adapts between the two: with [sample_step] 1 both branches advance by one block. -static void vq_refine_round(const void* input, uint32_t num_blocks, uint32_t* centroids, uint32_t num_centroids, +static void vq_refine_round(const void* input, uint32_t num_blocks, uint32_t block_stride, uint32_t* centroids, uint32_t num_centroids, vq_cluster* clusters, uint32_t sample_step, uint32_t start_offset) { for(uint32_t i=0; iindices); uint32_t score = (result >> 16); @@ -986,7 +982,7 @@ static void vq_refine_round(const void* input, uint32_t num_blocks, uint32_t* ce } //---------------------------------------------------------------------------------------------------------------------------- -void vq_top_table(const void* input, uint32_t num_blocks, uint32_t* output, uint32_t* num_entries) +void vq_top_table(const void* input, uint32_t num_blocks, uint32_t block_stride, uint32_t* output, uint32_t* num_entries) { // static array of odd steps to avoid aliasing and branches // first iteration is always 1 to ensure 100% initial coverage, and the two final full-assignment @@ -1003,7 +999,7 @@ void vq_top_table(const void* input, uint32_t num_blocks, uint32_t* output, uint // multiple iterations to stabilize the clusters for(uint32_t iteration=0; iteration<6; ++iteration) - vq_refine_round(input, num_blocks, centroids, *num_entries, clusters, steps[iteration], iteration % steps[iteration]); + vq_refine_round(input, num_blocks, block_stride, centroids, *num_entries, clusters, steps[iteration], iteration % steps[iteration]); uint32_t num_clusters = 0; @@ -1017,7 +1013,7 @@ void vq_top_table(const void* input, uint32_t num_blocks, uint32_t* output, uint } //---------------------------------------------------------------------------------------------------------------------------- -void build_top_table(hashmap_entry* hashmap, const void* input, uint32_t num_blocks, uint32_t* output, uint32_t* num_entries) +void build_top_table(hashmap_entry* hashmap, const void* input, uint32_t num_blocks, uint32_t block_stride, uint32_t* output, uint32_t* num_entries) { // clear the hashmap for(uint32_t i=0; iindices); uint32_t index = h & (HASHMAP_SIZE - 1); @@ -1090,7 +1086,7 @@ void build_top_table(hashmap_entry* hashmap, const void* input, uint32_t num_blo (*num_entries)++; } - vq_top_table(input, num_blocks, output, num_entries); + vq_top_table(input, num_blocks, block_stride, output, num_entries); // an empty top table corrupts the stream (the size is stored as size-1 in a byte, 0 reads back as 256 entries), // fall back to the single most frequent exact pattern, the hashmap still holds the exact pattern counts @@ -1134,7 +1130,7 @@ static inline void init_color_models(bcp_context* ctx, const color_hists* hist) } //----------------------------------------------------------------------------------------------------------------------------- -void init_static_models(bcp_context* ctx, const void* input, uint32_t width_blocks, uint32_t strip_width, uint32_t num_strips) +void init_static_models(bcp_context* ctx, const void* input, uint32_t width_blocks, uint32_t strip_width, uint32_t num_strips, uint32_t block_stride) { color_hists hist; uint32_t hist_reference[TOP_TABLE_SIZE]; @@ -1154,7 +1150,7 @@ void init_static_models(bcp_context* ctx, const void* input, uint32_t width_bloc uint32_t start_y = strip_index * strip_width; uint32_t zigzag_x = (start_y & 1) ? 0 : width_blocks - 1; - const bc1_block* first = (const bc1_block*) input + (start_y * width_blocks) + zigzag_x; + const bc1_block* first = (const bc1_block*) input + ((start_y * width_blocks) + zigzag_x) * block_stride; for(uint32_t j=0; j<2; ++j) { @@ -1188,11 +1184,11 @@ void init_static_models(bcp_context* ctx, const void* input, uint32_t width_bloc for(uint32_t x = 0; x < width_blocks; ++x) { uint32_t zigzag_x = (y&1) ? x : width_blocks - x - 1; - const bc1_block* current = (const bc1_block*) input + (y * width_blocks) + zigzag_x; + const bc1_block* current = (const bc1_block*) input + ((y * width_blocks) + zigzag_x) * block_stride; const bc1_block* top = &previous; if (y > start_y) - top = (const bc1_block*) input + (y - 1) * width_blocks + zigzag_x; // same zigzag position, row above + top = (const bc1_block*) input + ((y - 1) * width_blocks + zigzag_x) * block_stride; // same zigzag position, row above energy_previous += color_delta_energy(current, &previous); energy_top += color_delta_energy(current, top); @@ -1223,13 +1219,13 @@ void init_static_models(bcp_context* ctx, const void* input, uint32_t width_bloc // zig-zag pattern delta compression for colors uint32_t zigzag_x = (y&1) ? x : width_blocks - x - 1; - const bc1_block* current = (const bc1_block*) input + (y * width_blocks) + zigzag_x; + const bc1_block* current = (const bc1_block*) input + ((y * width_blocks) + zigzag_x) * block_stride; // first scanline of a strip is always predicted from the seed via the zigzag-previous // chain, after that a top-flagged strip is predicted from the row above, same zigzag position const bc1_block* color_reference = &previous; if (use_top && y > start_y) - color_reference = (const bc1_block*) input + (y - 1) * width_blocks + zigzag_x; + color_reference = (const bc1_block*) input + ((y - 1) * width_blocks + zigzag_x) * block_stride; color_hists_add(&hist, current, color_reference); @@ -1282,12 +1278,12 @@ void init_static_models(bcp_context* ctx, const void* input, uint32_t width_bloc // zig-zag pattern delta compression for colors uint32_t zigzag_x = (y&1) ? x : width_blocks - x - 1; - const bc1_block* current = (const bc1_block*) input + (y * width_blocks) + zigzag_x; + const bc1_block* current = (const bc1_block*) input + ((y * width_blocks) + zigzag_x) * block_stride; uint32_t position = (y - start_y) * width_blocks + x; const bc1_block* color_reference = &previous; if (use_top && y > start_y) - color_reference = (const bc1_block*) input + (y - 1) * width_blocks + zigzag_x; + color_reference = (const bc1_block*) input + ((y - 1) * width_blocks + zigzag_x) * block_stride; if (color_dict_match(&dict, position, color_pair(current)) > 0) { @@ -1337,16 +1333,6 @@ static inline void read_stream_align(byte_stream* stream, size_t power_two_align stream->pos = (stream->pos + mask) & ~(mask); } -//----------------------------------------------------------------------------------------------------------------------------- -// Adds to [count] the bits written to [stream] since the last call, and updates [bit_pos] to the current bit position. -// [bit_pos] must be initialized to 0 after le_begin_encode. -static inline void stream_bits_since(uint64_t* bit_pos, const le_stream* stream, uint64_t* count) -{ - const uint64_t pos = (uint64_t)stream->position * 8 + stream->bits_available; - *count += pos - *bit_pos; - *bit_pos = pos; -} - //----------------------------------------------------------------------------------------------------------------------------- static inline bool save_static_model(const le_model* model, byte_stream* stream) { @@ -1414,14 +1400,16 @@ static inline uint8_t delta_decode_wrap(uint8_t prev, uint8_t delta) //----------------------------------------------------------------------------------------------------------------------------- // Index context family from the endpoint range |e0 - e1| (unsigned, the true contrast of the 8 -// interpolated colors): range < 8 → family 0, range < 16 → family 1, else family 2 +// interpolated colors): range < 5 → family 0, range < 40 → family 1, else family 2. The index +// XOR distributions drift smoothly with the contrast, and these fitted thresholds minimize the +// index payload on the image suite (exp 21): 10/10 images improved, -0.24% stream over 8 / 16 static inline uint8_t bc4_index_family(uint8_t e0, uint8_t e1) { uint32_t range = (e0 >= e1) ? (uint32_t)(e0 - e1) : (uint32_t)(e1 - e0); - if (range < 8) + if (range < 5) return 0; - if (range < 16) + if (range < 40) return 1; return 2; } @@ -1478,7 +1466,7 @@ typedef struct bc4_hists // Fits the bc4/bc5 static models from [input]: the per-channel strip seed is the average of the // first block of each strip in chain order (as BC1), then one histogram pass with the exact // strip predictors of the encoder. there is no per-strip mode decision, so a single fit is final -static void bc4_init_models(bcp_context* ctx, const void* image, uint32_t width_blocks, uint32_t strip_width, uint32_t num_strips, uint32_t num_channels) +static void bc4_init_models(bcp_context* ctx, const void* image, uint32_t width_blocks, uint32_t strip_width, uint32_t num_strips, uint32_t num_channels, uint32_t block_stride) { bc4_hists hist[BCP_MAX_CHANNELS] = { 0 }; uint32_t sums[BCP_MAX_CHANNELS][2] = { 0 }; @@ -1492,7 +1480,7 @@ static void bc4_init_models(bcp_context* ctx, const void* image, uint32_t width_ for(uint32_t c=0; cendpoint[0]; sums[c][1] += b->endpoint[1]; @@ -1531,13 +1519,13 @@ static void bc4_init_models(bcp_context* ctx, const void* image, uint32_t width_ for(uint32_t c=0; cendpoint[0]; top_e[c][1] = t->endpoint[1]; } @@ -1584,22 +1572,18 @@ static void bc4_init_models(bcp_context* ctx, const void* image, uint32_t width_ //----------------------------------------------------------------------------------------------------------------------------- // Compresses a BC4 / BC5 texture: [image] holds (width/4) * (height/4) blocks, each // [num_channels] bc4_block (1 for BC4, red + green for BC5). shared by the two formats -static size_t bcp_compress_bc45(bcp_context* ctx, const void* image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips, uint32_t num_channels) +static size_t bcp_compress_bc45(bcp_context* ctx, const void* image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips, uint32_t num_channels, uint32_t block_stride) { if ((width < 16) || (height < 256) || !image || !ctx || !output || !num_strips || (num_channels < 1) || (num_channels > BCP_MAX_CHANNELS)) return 0; *num_strips = 64; // for now, could depend on the resolution in the future - // reset stats, they are only valid after a successful compression - ctx->bc4_endpoint_bits = 0; - ctx->bc4_index_bits = 0; - const uint32_t height_blocks = height / 4; const uint32_t width_blocks = width / 4; const uint32_t strip_width = height_blocks / *num_strips; - bc4_init_models(ctx, image, width_blocks, strip_width, *num_strips, num_channels); + bc4_init_models(ctx, image, width_blocks, strip_width, *num_strips, num_channels, block_stride); byte_stream stream = {.buffer = output, .length = output_length, .pos = 0}; @@ -1651,8 +1635,6 @@ static size_t bcp_compress_bc45(bcp_context* ctx, const void* image, uint32_t wi strips_offset[strip_index - 1] = (uint16_t)((stream.pos - previous_offset) / sizeof(uint32_t)); previous_offset = stream.pos; - uint64_t bit_pos = 0; // current bit position, used to measure the per-component stats - uint8_t prev_e[BCP_MAX_CHANNELS][2], top_e[BCP_MAX_CHANNELS][2], top_prev_e[BCP_MAX_CHANNELS][2]; for(uint32_t c=0; cendpoint[0]; top_e[c][1] = t->endpoint[1]; } @@ -1694,7 +1676,6 @@ static size_t bcp_compress_bc45(bcp_context* ctx, const void* image, uint32_t wi le_encode_symbol(&compressed_stream, &ctx->bc4_endpoint_model[c][e], delta_encode_wrap(pred, b->endpoint[e])); } - stream_bits_since(&bit_pos, &compressed_stream, &ctx->bc4_endpoint_bits); // index payload: the first index in raw 3 bits, then each index xored with its // intra-block prediction, contextual on the family and the prediction value @@ -1710,7 +1691,6 @@ static size_t bcp_compress_bc45(bcp_context* ctx, const void* image, uint32_t wi le_encode_symbol(&compressed_stream, &ctx->bc4_index_model[c][family * 8 + p], p ^ idx[i]); } - stream_bits_since(&bit_pos, &compressed_stream, &ctx->bc4_index_bits); if (!first_row) { @@ -1740,7 +1720,7 @@ static size_t bcp_compress_bc45(bcp_context* ctx, const void* image, uint32_t wi //----------------------------------------------------------------------------------------------------------------------------- // Decompresses one strip of a BC4 / BC5 stream: the mirror of bcp_compress_bc45, the strip-chain // references (top / top_previous) are read back from [output], which holds the decoded rows -static bool bcp_decompress_bc45(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index, uint32_t num_channels) +static bool bcp_decompress_bc45(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index, uint32_t num_channels, uint32_t block_stride) { if ((width < 16) || (height < 256) || !input || !ctx || !output || (num_strips == 0) || (strip_index >= num_strips) || (num_channels < 1) || (num_channels > BCP_MAX_CHANNELS)) return false; @@ -1817,12 +1797,12 @@ static bool bcp_decompress_bc45(bcp_context* ctx, const void* input, size_t inpu for(uint32_t c=0; cendpoint[0]; top_e[c][1] = t->endpoint[1]; } @@ -1892,19 +1872,15 @@ size_t packed_bc1_maxsize(uint32_t width, uint32_t height) } //----------------------------------------------------------------------------------------------------------------------------- -size_t bcp_compress_bc1(bcp_context* ctx, const void* bc1_image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips) +// [block_stride] is the distance between blocks in units of bc1_block: 1 for a plain BC1 grid, +// 2 for the color half of a BC3 grid (see bcp_compress_bc3) +static size_t bcp_compress_bc1_impl(bcp_context* ctx, const void* bc1_image, uint32_t block_stride, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips) { if (width < 16 || height < 256 || !bc1_image || !ctx || !output || !num_strips) return 0; *num_strips = 64; // for now, could depend on the resolution in the future - // reset stats, they are only valid after a successful compression - ctx->colors_bits = 0; - ctx->reference_bits = 0; - ctx->mask_bits = 0; - ctx->difference_bits = 0; - const uint32_t num_blocks = (width*height) / 16; const uint32_t height_blocks = height / 4; const uint32_t width_blocks = width / 4; @@ -1913,8 +1889,8 @@ size_t bcp_compress_bc1(bcp_context* ctx, const void* bc1_image, uint32_t width, if (ctx->hashmap == NULL) ctx->hashmap = ctx->mem.malloc_fn(sizeof(hashmap_entry) * HASHMAP_SIZE, ctx->mem.user); - build_top_table(ctx->hashmap, bc1_image, num_blocks, ctx->top_table, &ctx->top_table_size); - init_static_models(ctx, bc1_image, width_blocks, strip_width, *num_strips); + build_top_table(ctx->hashmap, bc1_image, num_blocks, block_stride, ctx->top_table, &ctx->top_table_size); + init_static_models(ctx, bc1_image, width_blocks, strip_width, *num_strips, block_stride); byte_stream stream = {.buffer = output, .length = output_length, .pos = 0}; @@ -1979,8 +1955,6 @@ size_t bcp_compress_bc1(bcp_context* ctx, const void* bc1_image, uint32_t width, strips_offset[strip_index - 1] = (uint16_t)((stream.pos - previous_offset) / sizeof(uint32_t)); previous_offset = stream.pos; - uint64_t bit_pos = 0; // current bit position, used to measure the per-component stats - bc1_block previous; unpack_strip_seed(ctx->strip_seed, &previous); color_dict dict; @@ -1995,7 +1969,7 @@ size_t bcp_compress_bc1(bcp_context* ctx, const void* bc1_image, uint32_t width, { // zig-zag pattern delta compression for colors uint32_t zigzag_x = (y&1) ? x : width_blocks - x - 1; - const bc1_block* current = (const bc1_block*) bc1_image + (y * width_blocks) + zigzag_x; + const bc1_block* current = (const bc1_block*) bc1_image + ((y * width_blocks) + zigzag_x) * block_stride; uint32_t position = (y - start_y) * width_blocks + x; uint32_t pair = color_pair(current); @@ -2016,7 +1990,7 @@ size_t bcp_compress_bc1(bcp_context* ctx, const void* bc1_image, uint32_t width, // chain, after that a top-flagged strip is predicted from the row above, same zigzag position const bc1_block* color_reference = &previous; if (use_top && y > start_y) - color_reference = (const bc1_block*) bc1_image + (y - 1) * width_blocks + zigzag_x; + color_reference = (const bc1_block*) bc1_image + ((y - 1) * width_blocks + zigzag_x) * block_stride; for(uint32_t j=0; j<2; ++j) { @@ -2043,12 +2017,9 @@ size_t bcp_compress_bc1(bcp_context* ctx, const void* bc1_image, uint32_t width, le_encode_symbol(&compressed_stream, &ctx->blue_model[j][chroma_context], dblue + COLOR_DELTA_OFFSET); } } - stream_bits_since(&bit_pos, &compressed_stream, &ctx->colors_bits); - uint8_t reference = nearest32(ctx->top_table, ctx->top_table_size, current->indices) & 0xff; le_encode_symbol(&compressed_stream, &ctx->table_reference_model, reference); - stream_bits_since(&bit_pos, &compressed_stream, &ctx->reference_bits); uint32_t difference = current->indices ^ ctx->top_table[reference]; uint32_t mask = 0; @@ -2058,12 +2029,10 @@ size_t bcp_compress_bc1(bcp_context* ctx, const void* bc1_image, uint32_t width, if ((difference & 0xFF000000) != 0) mask |= 8; le_encode_symbol(&compressed_stream, &ctx->mask_model, mask); - stream_bits_since(&bit_pos, &compressed_stream, &ctx->mask_bits); for(uint32_t j=0; j<4; ++j) if (mask & (1u << j)) le_encode_symbol(&compressed_stream, &ctx->table_difference_model, (difference >> (j*8)) & 0xff); - stream_bits_since(&bit_pos, &compressed_stream, &ctx->difference_bits); dict.entry[position & COLOR_DICT_MASK] = pair; previous = *current; @@ -2085,7 +2054,15 @@ size_t bcp_compress_bc1(bcp_context* ctx, const void* bc1_image, uint32_t width, } //----------------------------------------------------------------------------------------------------------------------------- -bool bcp_decompress_bc1(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index) +size_t bcp_compress_bc1(bcp_context* ctx, const void* bc1_image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips) +{ + return bcp_compress_bc1_impl(ctx, bc1_image, 1, width, height, output, output_length, num_strips); +} + +//----------------------------------------------------------------------------------------------------------------------------- +// [block_stride] is the distance between blocks in units of bc1_block: 1 for a plain BC1 grid, +// 2 for the color half of a BC3 grid (see bcp_decompress_bc3) +static bool bcp_decompress_bc1_impl(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index, uint32_t block_stride) { if (width < 16 || height < 256 || !input || !ctx || !output || strip_index >= num_strips) return false; @@ -2177,7 +2154,7 @@ bool bcp_decompress_bc1(bcp_context* ctx, const void* input, size_t input_length for(uint32_t x = 0; x < width_blocks; ++x) { uint32_t zigzag_x = (y&1) ? x : width_blocks - x - 1; - bc1_block* current = (bc1_block*) output + (y * width_blocks) + zigzag_x; + bc1_block* current = (bc1_block*) output + ((y * width_blocks) + zigzag_x) * block_stride; uint32_t position = (y - start_y) * width_blocks + x; @@ -2197,7 +2174,7 @@ bool bcp_decompress_bc1(bcp_context* ctx, const void* input, size_t input_length // after that a top-flagged strip is predicted from the row above, same zigzag position const bc1_block* color_reference = &previous; if (use_top && y > start_y) - color_reference = (const bc1_block*) output + (y - 1) * width_blocks + zigzag_x; + color_reference = (const bc1_block*) output + ((y - 1) * width_blocks + zigzag_x) * block_stride; for(uint32_t j=0; j<2; ++j) { @@ -2245,6 +2222,12 @@ bool bcp_decompress_bc1(bcp_context* ctx, const void* input, size_t input_length return compressed_stream.status == LE_OK; } +//----------------------------------------------------------------------------------------------------------------------------- +bool bcp_decompress_bc1(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index) +{ + return bcp_decompress_bc1_impl(ctx, input, input_length, width, height, num_strips, output, strip_index, 1); +} + //----------------------------------------------------------------------------------------------------------------------------- size_t packed_bc4_maxsize(uint32_t width, uint32_t height) { @@ -2262,59 +2245,100 @@ size_t packed_bc5_maxsize(uint32_t width, uint32_t height) //----------------------------------------------------------------------------------------------------------------------------- size_t bcp_compress_bc4(bcp_context* ctx, const void* bc4_image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips) { - return bcp_compress_bc45(ctx, bc4_image, width, height, output, output_length, num_strips, 1); + return bcp_compress_bc45(ctx, bc4_image, width, height, output, output_length, num_strips, 1, 1); } //----------------------------------------------------------------------------------------------------------------------------- size_t bcp_compress_bc5(bcp_context* ctx, const void* bc5_image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips) { - return bcp_compress_bc45(ctx, bc5_image, width, height, output, output_length, num_strips, 2); + return bcp_compress_bc45(ctx, bc5_image, width, height, output, output_length, num_strips, 2, 2); } //----------------------------------------------------------------------------------------------------------------------------- -void bcp_get_stats_bc5(bcp_context* ctx, bcp_stats_bc5* stats) +bool bcp_decompress_bc4(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index) { - *stats = (bcp_stats_bc5) - { - .endpoint_bytes = (uint32_t)((ctx->bc4_endpoint_bits + 7) / 8), - .pair_bytes = (uint32_t)((ctx->bc4_index_bits + 7) / 8), - .dict_bytes = 0 - }; + return bcp_decompress_bc45(ctx, input, input_length, width, height, num_strips, output, strip_index, 1, 1); } //----------------------------------------------------------------------------------------------------------------------------- -bool bcp_decompress_bc4(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index) +bool bcp_decompress_bc5(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index) { - return bcp_decompress_bc45(ctx, input, input_length, width, height, num_strips, output, strip_index, 1); + return bcp_decompress_bc45(ctx, input, input_length, width, height, num_strips, output, strip_index, 2, 2); } + //----------------------------------------------------------------------------------------------------------------------------- -bool bcp_decompress_bc5(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index) +// BC3 = a BC1 block + a BC4 block per 4x4 cell (the standard D3D layout, 16 bytes per block). +// The compressed stream is the composition of the two already validated pipelines: a +// little-endian u32 prefix with the size of the BC1 substream, then the BC1 stream of the +// color half, then the BC4 (1 channel) stream of the alpha half. Each substream is +// self-describing (models, strip seed, strip offsets, strips, as its standalone format) and +// ends 4-byte aligned, so no new payload logic, model, or context state is needed. +// The color half is the block at byte offset 0 of a 16-byte superblock and the alpha half the +// one at byte offset 8, so both views use a block stride of 2. + +//----------------------------------------------------------------------------------------------------------------------------- +size_t packed_bc3_maxsize(uint32_t width, uint32_t height) { - return bcp_decompress_bc45(ctx, input, input_length, width, height, num_strips, output, strip_index, 2); + // 2x the standard BC3 block size, the BC1 / BC4 convention, plus the 4-byte prefix + return packed_bc1_maxsize(width, height) + packed_bc4_maxsize(width, height) + sizeof(uint32_t); } +//----------------------------------------------------------------------------------------------------------------------------- +size_t bcp_compress_bc3(bcp_context* ctx, const void* bc3_image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips) +{ + if ((width < 16) || (height < 256) || !bc3_image || !ctx || !output || !num_strips || (output_length < sizeof(uint32_t))) + return 0; + + *num_strips = 64; // for now, could depend on the resolution in the future + + // prefix: the size of the BC1 substream, patched once it is known. The BC1 stream ends 4-byte + // aligned (each strip does), so the BC4 substream that follows is 4-byte aligned too. + uint32_t bc1_size = 0; + output[0] = (uint8_t)(bc1_size & 0xff); + output[1] = (uint8_t)((bc1_size >> 8) & 0xff); + output[2] = (uint8_t)((bc1_size >> 16) & 0xff); + output[3] = (uint8_t)((bc1_size >> 24) & 0xff); + + // color half: the BC1 pipeline on the blocks at byte offset 0, stride 2 + bc1_size = (uint32_t)bcp_compress_bc1_impl(ctx, bc3_image, 2, width, height, output + sizeof(uint32_t), output_length - sizeof(uint32_t), num_strips); + if (bc1_size == 0) + return 0; + + output[0] = (uint8_t)(bc1_size & 0xff); + output[1] = (uint8_t)((bc1_size >> 8) & 0xff); + output[2] = (uint8_t)((bc1_size >> 16) & 0xff); + output[3] = (uint8_t)((bc1_size >> 24) & 0xff); + + // alpha half: the single-channel BC4 pipeline on the blocks at byte offset 8, stride 2 + size_t bc4_size = bcp_compress_bc45(ctx, (const uint8_t*)bc3_image + 8, width, height, output + sizeof(uint32_t) + bc1_size, + output_length - sizeof(uint32_t) - bc1_size, num_strips, 1, 2); + if (bc4_size == 0) + return 0; + + return sizeof(uint32_t) + bc1_size + bc4_size; +} //----------------------------------------------------------------------------------------------------------------------------- -void bcp_get_stats(bcp_context* ctx, bcp_stats* stats) +bool bcp_decompress_bc3(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index) { - *stats = (bcp_stats) - { - // contextual models: report the zero-context (middle bucket) k, the one that carries most of the mass - .blue_k = { ctx->blue_model[0][COLOR_DELTA_CONTEXTS/2].k, ctx->blue_model[1][COLOR_DELTA_CONTEXTS/2].k }, - .difference_k = ctx->table_difference_model.k, - .green_k = { ctx->green_model[0].k, ctx->green_model[1].k }, - .red_k = { ctx->red_model[0][COLOR_DELTA_CONTEXTS/2].k, ctx->red_model[1][COLOR_DELTA_CONTEXTS/2].k }, - .mask_k = ctx->mask_model.k, - .reference_k = ctx->table_reference_model.k, - .top_table_size = ctx->top_table_size, - .colors_bytes = (uint32_t)((ctx->colors_bits + 7) / 8), - .reference_bytes = (uint32_t)((ctx->reference_bits + 7) / 8), - .mask_bytes = (uint32_t)((ctx->mask_bits + 7) / 8), - .difference_bytes = (uint32_t)((ctx->difference_bits + 7) / 8) - }; + const uint8_t* in = (const uint8_t*)input; + + if ((width < 16) || (height < 256) || !in || !ctx || !output || (num_strips == 0) || (strip_index >= num_strips) || (input_length < sizeof(uint32_t))) + return false; + + uint32_t bc1_size = (uint32_t)in[0] | ((uint32_t)in[1] << 8) | ((uint32_t)in[2] << 16) | ((uint32_t)in[3] << 24); + if ((size_t)bc1_size + sizeof(uint32_t) > input_length) + return false; + + // color half, then alpha half, each on its own substream of the same strip decomposition + bool ok = bcp_decompress_bc1_impl(ctx, in + sizeof(uint32_t), bc1_size, width, height, num_strips, output, strip_index, 2); + + return ok && bcp_decompress_bc45(ctx, in + sizeof(uint32_t) + bc1_size, input_length - sizeof(uint32_t) - bc1_size, width, height, num_strips, + (uint8_t*)output + 8, strip_index, 1, 2); } + //----------------------------------------------------------------------------------------------------------------------------- void bcp_terminate(bcp_context* ctx) { diff --git a/src/bc_packed.h b/src/bc_packed.h index 0edeb22..c609b83 100644 --- a/src/bc_packed.h +++ b/src/bc_packed.h @@ -26,19 +26,6 @@ typedef struct bcp_mem_interface typedef struct bcp_context bcp_context; -typedef struct bcp_stats -{ - uint32_t top_table_size; - uint8_t red_k[2], green_k[2], blue_k[2]; // one per endpoint: [0] = color[0], [1] = color[1] - uint8_t mask_k, reference_k, difference_k; - - // size in bytes of each range-coded component, valid after a successful bcp_compress_bc1 - uint32_t colors_bytes; // 6 color delta symbols per block - uint32_t reference_bytes; // top-table reference symbol per block - uint32_t mask_bytes; // index-residual mask symbol per block - uint32_t difference_bytes; // 0-4 index-residual bytes per block -} bcp_stats; - //---------------------------------------------------------------------------------------------------------------------------- // Creates a context. Returns NULL if the allocation fails. @@ -59,12 +46,6 @@ size_t packed_bc1_maxsize(uint32_t width, uint32_t height); // Returns the size of the compressed stream in bytes, or 0 on failure. size_t bcp_compress_bc1(bcp_context* ctx, const void* bc1_image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips); -//---------------------------------------------------------------------------------------------------------------------------- -// Extracts statistics after compression -// The *_bytes fields are valid after a successful bcp_compress_bc1, their sum is the range-coded payload size, -// the stream additionally contains the static header (models, top-table, strip offsets) -void bcp_get_stats(bcp_context* ctx, bcp_stats* stats); - //---------------------------------------------------------------------------------------------------------------------------- // CPU decompression, for unit tests and validation. // @@ -95,20 +76,6 @@ size_t packed_bc5_maxsize(uint32_t width, uint32_t height); size_t bcp_compress_bc4(bcp_context* ctx, const void* bc4_image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips); size_t bcp_compress_bc5(bcp_context* ctx, const void* bc5_image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips); -//---------------------------------------------------------------------------------------------------------------------------- -// Extracts statistics after a successful bcp_compress_bc5: the *_bytes fields are the size in -// bytes of each range-coded payload component, the stream additionally carries the static header. -// pair_bytes is the whole index payload; dict_bytes is reserved for a future index dictionary -// and is currently always 0. -typedef struct bcp_stats_bc5 -{ - uint32_t endpoint_bytes; // 4 endpoint delta symbols per block (2 endpoints x 2 channels) - uint32_t pair_bytes; // index payload: 3 raw bits + 15 contextual symbols per channel block - uint32_t dict_bytes; // reserved, always 0 for now -} bcp_stats_bc5; - -void bcp_get_stats_bc5(bcp_context* ctx, bcp_stats_bc5* stats); - //---------------------------------------------------------------------------------------------------------------------------- // CPU decompression, for unit tests and validation. // @@ -123,6 +90,36 @@ void bcp_get_stats_bc5(bcp_context* ctx, bcp_stats_bc5* stats); bool bcp_decompress_bc4(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index); bool bcp_decompress_bc5(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index); +//---------------------------------------------------------------------------------------------------------------------------- +// Maximum size of the compressed stream for a given BC3 texture, in bytes: +// packed_bc1_maxsize + packed_bc4_maxsize + 4 (the prefix, see the stream description below). +// The [output] buffer of bcp_compress_bc3 must be at least this large. +size_t packed_bc3_maxsize(uint32_t width, uint32_t height); + +//---------------------------------------------------------------------------------------------------------------------------- +// Compresses a BC3 texture (width x height pixels, must be a multiple of 4) into [output]. +// [bc3_image] holds (width/4) * (height/4) standard 16-byte BC3 blocks: the 8-byte BC1 color +// block followed by the 8-byte BC4 alpha block (the standard D3D layout). The compressed +// stream is [u32 size of the BC1 stream][BC1 stream][BC4 stream], the two substreams being +// exactly the bcp_compress_bc1 / bcp_compress_bc4 streams of the color and alpha halves, so +// both the BC1 and the BC4 model sets, strip offsets and strips are present in the stream. +// [num_strips] receives the number of strips used for the compressed stream. +// Returns the size of the compressed stream in bytes, or 0 on failure. +size_t bcp_compress_bc3(bcp_context* ctx, const void* bc3_image, uint32_t width, uint32_t height, uint8_t* output, size_t output_length, uint32_t* num_strips); + +//---------------------------------------------------------------------------------------------------------------------------- +// CPU decompression, for unit tests and validation. +// +// Decompress one strip of the image, for a complete image this function should be called [num_strips] times +// [input] compressed data (output of bcp_compress_bc3) +// [input_length] size in byte of the compressed data +// [width, height] dimensions of the output BC3 image +// [num_strips] number of strips, as returned by bcp_compress_bc3 +// [output] must be big enough to contain a BC3 image of width x height +// [strip_index] index of the strip, must be in [0; num_strips[) +// Returns false on error otherwise true +bool bcp_decompress_bc3(bcp_context* ctx, const void* input, size_t input_length, uint32_t width, uint32_t height, uint32_t num_strips, void* output, uint32_t strip_index); + #ifdef __cplusplus } #endif diff --git a/tests/image_tests.c b/tests/image_tests.c index bf5c725..e312271 100644 --- a/tests/image_tests.c +++ b/tests/image_tests.c @@ -44,6 +44,31 @@ FILE* open_hourly_stats_file(const char *folder_path) return fopen(full_path, "a"); } +//----------------------------------------------------------------------------------------------------------------------------- +// All image suites log to the same hourly CSV, each opening it in append mode, so the +// header is only written for a new file: after fseek to SEEK_END, ftell returns the size +static void ensure_stats_header(FILE* stats) +{ + fseek(stats, 0, SEEK_END); + if (ftell(stats) <= 0) + fprintf(stats, "format,filename,width,height,compression_ratio\n"); +} + +//----------------------------------------------------------------------------------------------------------------------------- +// one row per image and a comment line summarizing the suite, keeping the CSV self +// documenting across the bc1, bc4, bc5 and bc3 suites +static void log_image_stats(FILE* stats, const char* format, const char* file_path, uint32_t width, uint32_t height, float ratio) +{ + fprintf(stats, "%s,%s,%" PRIu32 ",%" PRIu32 ",%.6f\n", format, file_path, width, height, ratio); + fflush(stats); +} + +static void log_stats_summary(FILE* stats, const char* format, uint32_t num_images, float average_ratio) +{ + fprintf(stats, "\n\n# %s: %" PRIu32 " images, average compression ratio : %.4f:1\n\n", format, num_images, average_ratio); + fflush(stats); +} + //----------------------------------------------------------------------------------------------------------------------------- TEST test_all_images_in_folder(const char *dir_path) { @@ -73,7 +98,7 @@ TEST test_all_images_in_folder(const char *dir_path) uint8_t* decompressed_bc1 = malloc(max_image_size); ASSERT(original_bc1 != NULL && decompressed_bc1 != NULL); - fprintf(stats, "filename,width,height,compression_ratio, top_table_size, red0_k, red1_k, green0_k, green1_k, blue0_k, blue1_k, reference_k, mask_k, difference_k, colors_bytes, reference_bytes, mask_bytes, difference_bytes\n"); + ensure_stats_header(stats); float global_ratio = 0.f; uint32_t num_images = 0; @@ -115,17 +140,7 @@ TEST test_all_images_in_folder(const char *dir_path) const size_t image_size = (width/4)*(height/4)*sizeof(bc1_block); const float compression_ratio = (float) image_size / (float) compressed_buffer_length; - fprintf(stats, "%s,%" PRIu32 ",%" PRIu32 ",%.6f,", file_path, width, height, compression_ratio); - - bcp_stats cmp_stats; - bcp_get_stats(ctx, &cmp_stats); - - fprintf(stats, "%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 "\n", - cmp_stats.top_table_size, cmp_stats.red_k[0], cmp_stats.red_k[1], cmp_stats.green_k[0], cmp_stats.green_k[1], cmp_stats.blue_k[0], cmp_stats.blue_k[1], - cmp_stats.reference_k, cmp_stats.mask_k, cmp_stats.difference_k, - cmp_stats.colors_bytes, cmp_stats.reference_bytes, cmp_stats.mask_bytes, cmp_stats.difference_bytes); - - fflush(stats); + log_image_stats(stats, "bc1", file_path, width, height, compression_ratio); global_ratio += compression_ratio; num_images++; @@ -146,7 +161,7 @@ TEST test_all_images_in_folder(const char *dir_path) const float average_compression_ratio = global_ratio / (float) num_images; - fprintf(stats, "\n\n# %u images, average compression ratio : %2.4f:1\n\n", num_images, average_compression_ratio); + log_stats_summary(stats, "bc1", num_images, average_compression_ratio); closedir(dir); free(original_bc1); @@ -166,7 +181,8 @@ TEST test_all_images_in_folder(const char *dir_path) //----------------------------------------------------------------------------------------------------------------------------- // BC4 / BC5 image roundtrips on the hardcoded sets of images/bc4 (AO maps) and images/bc5 // (normal maps, red + green channels). the filenames are hardcoded, the folder is not scanned, -// and the per-image ratios are printed to stdout (no CSV): the average must stay above 1:1 +// the per-image ratios are logged to the hourly CSV and printed to stdout: the average must +// stay above 1:1 static const char* const bc4_image_filenames[] = { "images/bc4/grey_roof_tiles_02_ao_1k.png", @@ -185,12 +201,22 @@ static const char* const bc5_image_filenames[] = static greatest_test_res run_bc_image_suite(bool bc5) { + const char* format = bc5 ? "bc5" : "bc4"; const char* const* filenames = bc5 ? bc5_image_filenames : bc4_image_filenames; const uint32_t num_filenames = sizeof(bc4_image_filenames) / sizeof(bc4_image_filenames[0]); bcp_context* ctx = bcp_init(NULL); ASSERT(ctx != NULL); + FILE* stats = open_hourly_stats_file("./logs"); + if (!stats) + { + fprintf(stderr, "Unable to create stats file\n"); + FAIL(); + } + + ensure_stats_header(stats); + float global_ratio = 0.f; uint32_t num_images = 0; @@ -248,6 +274,7 @@ static greatest_test_res run_bc_image_suite(bool bc5) ASSERT(compressed_buffer_length <= compressed_buffer_size); const float compression_ratio = (float)(num_blocks * block_bytes) / (float)compressed_buffer_length; + log_image_stats(stats, format, filenames[i], width, height, compression_ratio); fprintf(stdout, "%s: %u x %u, %s ratio %.4f:1 (stream %zu bytes)\n", filenames[i], width, height, bc5 ? "bc5" : "bc4", compression_ratio, compressed_buffer_length); @@ -273,11 +300,14 @@ static greatest_test_res run_bc_image_suite(bool bc5) const float average_compression_ratio = global_ratio / (float)num_images; + log_stats_summary(stats, format, num_images, average_compression_ratio); + fclose(stats); + fprintf(stdout, "==> %s: %u images, average compression ratio : %2.4f:1\n\n", bc5 ? "bc5" : "bc4", num_images, average_compression_ratio); bcp_terminate(ctx); - ASSERT(average_compression_ratio >= 1.0f); + ASSERT(average_compression_ratio >= 1.05f); PASS(); } @@ -296,12 +326,133 @@ TEST bc5_images(void) return run_bc_image_suite(true); } +//----------------------------------------------------------------------------------------------------------------------------- +// BC3 image roundtrips on the hardcoded set of images/bc3 (albedo + alpha maps): the filenames +// are hardcoded, the folder is not scanned, the per-image ratios are logged to the hourly CSV +// and printed to stdout, and the average must stay above 1:1 +static const char* const bc3_image_filenames[] = +{ + "images/bc3/leak-stain_albedo.png", + "images/bc3/leaves.png" +}; + +TEST bc3_images(void) +{ + const uint32_t num_filenames = sizeof(bc3_image_filenames) / sizeof(bc3_image_filenames[0]); + + bcp_context* ctx = bcp_init(NULL); + ASSERT(ctx != NULL); + + FILE* stats = open_hourly_stats_file("./logs"); + if (!stats) + { + fprintf(stderr, "Unable to create stats file\n"); + FAIL(); + } + + ensure_stats_header(stats); + + float global_ratio = 0.f; + uint32_t num_images = 0; + + for(uint32_t i=0; i bc3: %u images, average compression ratio : %2.4f:1\n\n", num_images, average_compression_ratio); + + bcp_terminate(ctx); + + ASSERT(average_compression_ratio >= 2.7f); + + PASS(); +} + SUITE(suite_image) { RUN_TEST1(test_all_images_in_folder, "./images/"); RUN_TEST(bc4_images); RUN_TEST(bc5_images); + RUN_TEST(bc3_images); } diff --git a/tests/synthetic_tests.c b/tests/synthetic_tests.c index fdfed28..63e847f 100644 --- a/tests/synthetic_tests.c +++ b/tests/synthetic_tests.c @@ -190,66 +190,6 @@ TEST identical_colors(void) return roundtrip(image, TEST_IMAGE_WIDTH, TEST_IMAGE_HEIGHT); } -//----------------------------------------------------------------------------------------------------------------------------- -// Stats on a constant image. the assertions are cross-output invariants documented by the -// public API only: the stream fits the reported maxsize, and the sum of the reported -// payload components never exceeds the stream size (the stream additionally carries the -// static header). no assumption is made about how the payload is coded internally; -// the roundtrip must stay exact -TEST stats_constant_image(void) -{ - const uint32_t width = TEST_IMAGE_WIDTH; - const uint32_t height = TEST_IMAGE_HEIGHT; - const uint32_t num_blocks = (width / 4) * (height / 4); - - bc1_block image[TEST_IMAGE_SIZE]; - for(uint32_t i=0; i> 16)}, + .indices = r2 + }; + + image[i].alpha.endpoint[0] = (uint8_t)splitmix32(&rng_state); + image[i].alpha.endpoint[1] = (uint8_t)splitmix32(&rng_state); + for(uint32_t j=0; j<6; ++j) + image[i].alpha.indices[j] = (uint8_t)splitmix32(&rng_state); + } + + return bc3_roundtrip(image, TEST_BC45_WIDTH, TEST_BC45_HEIGHT); +} + + //----------------------------------------------------------------------------------------------------------------------------- SUITE(suite_synthetic) { @@ -746,11 +843,14 @@ SUITE(suite_synthetic) (void)bc45_checkerboard; (void)bc45_gradient; (void)bc45_random_splitmix32; + (void)bc3_flat; + (void)bc3_checkerboard; + (void)bc3_gradient; + (void)bc3_random_splitmix32; (void)gradient; (void)random_splitmix32; (void)color0_le_color1; (void)identical_colors; - (void)stats_constant_image; (void)strip_uniform_colors; (void)all_distinct_indices; (void)row_varying_colors; @@ -767,9 +867,12 @@ SUITE(suite_synthetic) RUN_TEST(bc45_checkerboard); RUN_TEST(bc45_gradient); RUN_TEST(bc45_random_splitmix32); + RUN_TEST(bc3_flat); + RUN_TEST(bc3_checkerboard); + RUN_TEST(bc3_gradient); + RUN_TEST(bc3_random_splitmix32); RUN_TEST(color0_le_color1); RUN_TEST(identical_colors); - RUN_TEST(stats_constant_image); RUN_TEST(strip_uniform_colors); RUN_TEST(variable_resolutions_random); RUN_TEST(all_distinct_indices); diff --git a/tests/test_helpers.h b/tests/test_helpers.h index 8e3b3c2..1867c97 100644 --- a/tests/test_helpers.h +++ b/tests/test_helpers.h @@ -31,6 +31,13 @@ typedef struct bc5_block bc4_block channel[2]; // [0] = red, [1] = green } bc5_block; +// the standard 16-byte BC3 block: the bc1_block color half followed by the bc4_block alpha half +typedef struct bc3_block +{ + bc1_block color; + bc4_block alpha; +} bc3_block; + //----------------------------------------------------------------------------------------------------------------------------- // SplitMix32 PRNG implementation static inline uint32_t splitmix32(uint32_t* state)