// File: basisu_xbc7_encode.cpp #include "basisu_xbc7_encode.h" #include "../transcoder/basisu_transcoder.h" #include "../zstd/zstd.h" #include "../transcoder/basisu_xbc7_decoder.h" #include "basisu_bc7e_scalar.h" using basist::fixed16_16; namespace basisu { namespace xbc7 { using namespace basist::xbc7; // shared XBC7 defs (DCT/syms/enums/eval) now live in basist::xbc7 class blob_stream_writer { public: blob_stream_writer() {} void clear() { m_blobs.clear(); } inline uint8_vec& get_blob_vec(uint32_t id) { assert(id < BLOB_STREAM_MAX_IDS); if (id >= m_blobs.size()) m_blobs.resize(id + 1); return m_blobs[id]; } // Hot path: append one byte to blob `id`. id must be < 128. inline void put_byte(uint32_t id, uint8_t b) { assert(id < BLOB_STREAM_MAX_IDS); if (id >= m_blobs.size()) m_blobs.resize(id + 1); m_blobs[id].push_back(b); } inline void put_bytes(uint32_t id, const void* p, size_t n) { assert(id < BLOB_STREAM_MAX_IDS); if (id >= m_blobs.size()) m_blobs.resize(id + 1); m_blobs[id].append(static_cast(p), n); } void append_bytes(uint32_t id, const uint8_vec& blob) { assert(id < BLOB_STREAM_MAX_IDS); if (id >= m_blobs.size()) m_blobs.resize(id + 1); m_blobs[id].append(blob); } // for the per-stream byte accounting / stats dashboard inline size_t get_blob_size(uint32_t id) const { return (id < m_blobs.size()) ? m_blobs[id].size() : 0; } inline uint32_t get_num_ids() const { return m_blobs.size_u32(); } inline const uint8_vec* get_blob_data(uint32_t id) const { return (id < m_blobs.size()) ? &m_blobs[id] : nullptr; } // Compress (where it helps) and serialize everything to `out`. // Returns false only on internal error (blob > 4GB etc). bool serialize(uint8_vec& out, int zstd_level = 19, uint32_t* pStored_sizes = nullptr) const // pStored_sizes: optional [BLOB_STREAM_MAX_IDS] array, receives stored (post-compression) payload bytes per id { out.resize(0); uint32_t num_stored = 0; for (uint32_t id = 0; id < m_blobs.size(); id++) if (m_blobs[id].size()) num_stored++; if (num_stored > 255) return false; out.push_back(BLOB_STREAM_MAGIC_BEGIN); out.push_back((uint8_t)num_stored); uint8_vec comp_buf; for (uint32_t id = 0; id < m_blobs.size(); id++) { const uint8_vec& blob = m_blobs[id]; if (!blob.size()) continue; if (blob.size() > UINT32_MAX) return false; const uint32_t uncomp_size = (uint32_t)blob.size(); // Try Zstd; keep it only if it's worth it. Small blobs keep any // win, but a LARGE blob must shrink by at least 1/400 (0.25%): // raw blobs are zero-copy at decode (no Zstd frame to walk, no // arena bytes), so a marginal win costs more than it saves. const uint8_t* pStored = blob.data(); uint32_t stored_size = 0; // 0 == raw const size_t bound = ZSTD_compressBound(blob.size()); comp_buf.resize(bound); const size_t comp_res = ZSTD_compress(comp_buf.data(), bound, blob.data(), blob.size(), zstd_level); if ((!ZSTD_isError(comp_res)) && (comp_res < blob.size())) { const uint32_t BLOB_RATIO_CHECK_MIN_SIZE = 128; // blobs <= this keep any win const uint32_t BLOB_MIN_SAVINGS_DENOM = 400; // larger blobs must save >= size/400 const uint64_t saved_bytes = (uint64_t)blob.size() - comp_res; if ((blob.size() <= BLOB_RATIO_CHECK_MIN_SIZE) || ((saved_bytes * BLOB_MIN_SAVINGS_DENOM) >= (uint64_t)blob.size())) { pStored = comp_buf.data(); stored_size = (uint32_t)comp_res; } } if (pStored_sizes) pStored_sizes[id] = stored_size ? stored_size : uncomp_size; // directory entry: id+flag byte, then varint size(s) out.push_back((uint8_t)(id | (stored_size ? 0x80u : 0u))); if (stored_size) { push_varint(out, uncomp_size); push_varint(out, stored_size); } else push_varint(out, uncomp_size); out.append(pStored, stored_size ? stored_size : uncomp_size); } out.push_back(BLOB_STREAM_MAGIC_END); return true; } private: basisu::vector m_blobs; static inline void push_varint(uint8_vec& v, uint32_t x) { while (x >= 0x80u) { v.push_back((uint8_t)(x | 0x80u)); x >>= 7; } v.push_back((uint8_t)x); } }; static const char* g_cand_names[cCandFirstXYDelta] = { "absolute", "left edge", "upper edge", "L+U blend", "reflect left", "reflect upper", "L+U avg", "L+U strong blend", "gradient", "damped gradient", "diag avg", "diag edge blend", "upper+diag edge blend", "MED", "GAB", "plane fit", "diag down-left", "diag down-right" }; // Post-quantization DCT coefficient statistics, collected from the WINNING // candidate of each non-solid block, per coded plane. Gives the picture the // run/coeff/sign stream design needs: where surviving ACs sit in zigzag // order, their amplitudes and signs, run lengths, and DC behavior. struct xbc7_dct_stats { uint64_t m_total_planes = 0; // DC (range is [-256, 256] by construction) uint64_t m_dc_zero = 0, m_dc_pos = 0, m_dc_neg = 0; uint64_t m_dc_sum_abs = 0; int m_dc_min = 257, m_dc_max = -257; uint64_t m_dc_mag_hist[10] = {}; // AC uint64_t m_total_acs = 0; uint64_t m_ac_pos = 0, m_ac_neg = 0; uint64_t m_ac_sum_abs = 0; int m_ac_min = 257, m_ac_max = -257; uint64_t m_ac_mag_hist[10] = {}; uint64_t m_ac_count_hist[16] = {}; // nonzero ACs per plane, 0..15 uint64_t m_zig_pos_hist[16] = {}; // zigzag position (1..15) of each nonzero AC uint64_t m_run_hist[16] = {}; // zero-run length coded before each nonzero AC uint64_t m_eob_markers = 0; // trailing-zeros records // |v| -> bucket: 1, 2, 3, 4, 5-8, 9-16, 17-32, 33-64, 65-128, 129+ static uint32_t mag_bucket(uint32_t m) { if (m <= 4) return m - 1; if (m <= 8) return 4; if (m <= 16) return 5; if (m <= 32) return 6; if (m <= 64) return 7; if (m <= 128) return 8; return 9; } void record_plane(const dct_syms& syms) { m_total_planes++; const int dc = syms.m_dc; if (!dc) m_dc_zero++; else if (dc > 0) m_dc_pos++; else m_dc_neg++; m_dc_sum_abs += (uint64_t)basisu::iabs(dc); m_dc_min = basisu::minimum(m_dc_min, dc); m_dc_max = basisu::maximum(m_dc_max, dc); if (dc) m_dc_mag_hist[mag_bucket((uint32_t)basisu::iabs(dc))]++; uint32_t num_acs = 0; uint32_t zig_idx = 1; for (uint32_t i = 0; i < syms.m_ac_vals.size_u32(); i++) { const uint32_t run_len = (uint32_t)syms.m_ac_vals[i].m_num_zeros; const int c = syms.m_ac_vals[i].m_coeff; if (c == INT16_MAX) { m_eob_markers++; break; } zig_idx += run_len; assert(zig_idx < 16); if (zig_idx >= 16) break; m_run_hist[basisu::minimum(run_len, 15)]++; m_zig_pos_hist[zig_idx]++; if (c > 0) m_ac_pos++; else m_ac_neg++; m_ac_sum_abs += (uint64_t)basisu::iabs(c); m_ac_min = basisu::minimum(m_ac_min, c); m_ac_max = basisu::maximum(m_ac_max, c); m_ac_mag_hist[mag_bucket((uint32_t)basisu::iabs(c))]++; num_acs++; zig_idx++; } m_total_acs += num_acs; m_ac_count_hist[basisu::minimum(num_acs, 15)]++; } // accumulate another collector (per-job stats -> totals); counters add, // extrema take min/max void merge(const xbc7_dct_stats& o) { m_total_planes += o.m_total_planes; m_dc_zero += o.m_dc_zero; m_dc_pos += o.m_dc_pos; m_dc_neg += o.m_dc_neg; m_dc_sum_abs += o.m_dc_sum_abs; m_dc_min = basisu::minimum(m_dc_min, o.m_dc_min); m_dc_max = basisu::maximum(m_dc_max, o.m_dc_max); for (uint32_t i = 0; i < 10; i++) m_dc_mag_hist[i] += o.m_dc_mag_hist[i]; m_total_acs += o.m_total_acs; m_ac_pos += o.m_ac_pos; m_ac_neg += o.m_ac_neg; m_ac_sum_abs += o.m_ac_sum_abs; m_ac_min = basisu::minimum(m_ac_min, o.m_ac_min); m_ac_max = basisu::maximum(m_ac_max, o.m_ac_max); for (uint32_t i = 0; i < 10; i++) m_ac_mag_hist[i] += o.m_ac_mag_hist[i]; for (uint32_t i = 0; i < 16; i++) { m_ac_count_hist[i] += o.m_ac_count_hist[i]; m_zig_pos_hist[i] += o.m_zig_pos_hist[i]; m_run_hist[i] += o.m_run_hist[i]; } m_eob_markers += o.m_eob_markers; } void print() const { static const char* s_mag_labels[10] = { "1", "2", "3", "4", "5-8", "9-16", "17-32", "33-64", "65-128", "129+" }; fmt_debug_printf("---- WEIGHTS: DCT coefficient statistics (winning candidates, per coded plane) ----\n"); const float inv_p = m_total_planes ? (100.0f / (float)m_total_planes) : 0.0f; const float inv_a = m_total_acs ? (100.0f / (float)m_total_acs) : 0.0f; fmt_debug_printf("Coded planes: {}, total nonzero ACs: {}, avg ACs/plane: {}\n", m_total_planes, m_total_acs, m_total_planes ? ((float)m_total_acs / (float)m_total_planes) : 0.0f); fmt_debug_printf("DC: zero: {} ({}%), pos: {} ({}%), neg: {} ({}%), range [{}, {}], mean |DC| (all planes): {}\n", m_dc_zero, (float)m_dc_zero * inv_p, m_dc_pos, (float)m_dc_pos * inv_p, m_dc_neg, (float)m_dc_neg * inv_p, m_dc_min, m_dc_max, m_total_planes ? ((float)m_dc_sum_abs / (float)m_total_planes) : 0.0f); fmt_debug_printf("DC magnitude histogram (nonzero DCs): "); for (uint32_t i = 0; i < 10; i++) if (m_dc_mag_hist[i]) fmt_debug_printf("{}:{} ", s_mag_labels[i], m_dc_mag_hist[i]); fmt_debug_printf("\n"); fmt_debug_printf("AC: pos: {} ({}%), neg: {} ({}%), range [{}, {}], mean |AC| (per nonzero AC): {}\n", m_ac_pos, (float)m_ac_pos * inv_a, m_ac_neg, (float)m_ac_neg * inv_a, m_ac_min, m_ac_max, m_total_acs ? ((float)m_ac_sum_abs / (float)m_total_acs) : 0.0f); fmt_debug_printf("AC magnitude histogram: "); for (uint32_t i = 0; i < 10; i++) if (m_ac_mag_hist[i]) fmt_debug_printf("{}:{} ({}%) ", s_mag_labels[i], m_ac_mag_hist[i], (float)m_ac_mag_hist[i] * inv_a); fmt_debug_printf("\n"); fmt_debug_printf("Nonzero ACs per plane: "); for (uint32_t i = 0; i < 16; i++) if (m_ac_count_hist[i]) fmt_debug_printf("{}:{} ({}%) ", i, m_ac_count_hist[i], (float)m_ac_count_hist[i] * inv_p); fmt_debug_printf("\n"); fmt_debug_printf("Zigzag position of nonzero ACs (1..15): "); for (uint32_t i = 1; i < 16; i++) fmt_debug_printf("{}:{} ", i, m_zig_pos_hist[i]); fmt_debug_printf("\n"); fmt_debug_printf("Zero-run length before each AC: "); for (uint32_t i = 0; i < 16; i++) if (m_run_hist[i]) fmt_debug_printf("{}:{} ({}%) ", i, m_run_hist[i], (float)m_run_hist[i] * inv_a); fmt_debug_printf("\n"); fmt_debug_printf("Trailing-zeros (EOB) markers: {} ({}% of planes)\n", m_eob_markers, (float)m_eob_markers * inv_p); } }; // ---- debug-image color palettes (encode-time visualization only) ---- // The command byte is split into three field-specific color images rather // than one hard-to-read grayscale; each image carries a drawn legend. // CORE command id (xbc7_command_id, command bits 0-2). static const color_rgba g_xbc7_command_vis_colors[8] = { color_rgba( 0, 0, 255, 255), // 0 RepeatLast - blue color_rgba( 0, 255, 255, 255), // 1 RepeatUpper - cyan color_rgba(128, 128, 128, 255), // 2 SolidDPCM - gray color_rgba(255, 0, 0, 255), // 3 NewConfig - red color_rgba( 0, 255, 0, 255), // 4 ReuseConfigLeft - green color_rgba(255, 255, 0, 255), // 5 ReuseConfigUpper - yellow color_rgba(255, 0, 255, 255), // 6 ReuseConfigLeftDiagonal - magenta color_rgba(255, 128, 0, 255), // 7 ReuseConfigRightDiagonal - orange }; // ENDPOINT prediction mode (xbc7_command_endpoint_mode, command bits 3-5). static const color_rgba g_xbc7_endpoint_mode_vis_colors[8] = { color_rgba(255, 0, 0, 255), // 0 Raw - red color_rgba( 0, 200, 0, 255), // 1 DPCM-Left - green color_rgba( 0, 128, 255, 255), // 2 DPCM-Up - blue color_rgba(255, 255, 0, 255), // 3 DPCM-LeftDiag - yellow color_rgba(255, 0, 255, 255), // 4 DPCM-RightDiag - magenta color_rgba( 0, 255, 255, 255), // 5 DPCM-Index - cyan color_rgba(255, 128, 0, 255), // 6 DPCM-Left-Sub1 - orange color_rgba(160, 0, 255, 255), // 7 DPCM-Up-Sub1 - purple }; // WEIGHT mode (xbc7_command_weight_mode, command bit 6). static const color_rgba g_xbc7_weight_mode_vis_colors[2] = { color_rgba( 0, 200, 0, 255), // 0 Raw/DPCM (lossless) - green color_rgba(255, 0, 0, 255), // 1 DCT (lossy) - red }; // Blocks with no endpoint/weight fields (fully predicted: RepeatLast/ // RepeatUpper/SolidDPCM) in the endpoint/weight-mode images. static const color_rgba g_xbc7_vis_na_color(40, 40, 40, 255); // AC-COUNT heatmap: number of nonzero DCT weight ACs in a block (DCT-coded // blocks only). Bucketed via xbc7_ac_count_bucket(). static const color_rgba g_xbc7_ac_count_vis_colors[6] = { color_rgba( 0, 0, 128, 255), // 0 ACs - dark blue (flat / DC only) color_rgba( 0, 128, 255, 255), // 1-2 ACs - blue color_rgba( 0, 200, 0, 255), // 3-5 ACs - green color_rgba(255, 255, 0, 255), // 6-10 ACs - yellow color_rgba(255, 128, 0, 255), // 11-20 ACs - orange color_rgba(255, 0, 0, 255), // 21+ ACs - red }; static inline uint32_t xbc7_ac_count_bucket(uint32_t num_acs) { if (num_acs == 0) return 0; if (num_acs <= 2) return 1; if (num_acs <= 5) return 2; if (num_acs <= 10) return 3; if (num_acs <= 20) return 4; return 5; } // PREDICTOR categories: the ~50 weight predictors (absolute, 17 synthetic, // 32 block references) collapsed into a handful of buckets; the 32 block // refs are split by their causal direction. enum xbc7_pred_category { cPredCatAbsolute = 0, // no prediction cPredCatSynthetic, // edge/gradient synthetic predictors cPredCatRefLeft, // block ref, same row (dy==0) cPredCatRefUp, // block ref, straight up (dx==0) cPredCatRefUpLeft, // block ref, up-and-left cPredCatRefUpRight, // block ref, up-and-right cNumPredCategories }; static const color_rgba g_xbc7_predictor_vis_colors[cNumPredCategories] = { color_rgba(255, 0, 0, 255), // Absolute - red color_rgba(255, 128, 0, 255), // Synthetic - orange color_rgba( 0, 200, 0, 255), // BlockRef Left - green color_rgba( 0, 128, 255, 255), // BlockRef Up - blue color_rgba( 0, 255, 255, 255), // BlockRef Up-Left - cyan color_rgba(255, 0, 255, 255), // BlockRef Up-Right - magenta }; // Map a (cand + amp*cTotalCandidates) predictor index to its category. static inline uint32_t xbc7_predictor_category(uint32_t pred_index) { const uint32_t cand = pred_index % cTotalCandidates; if (cand == cCandAbsolute) return cPredCatAbsolute; if (cand < cCandFirstXYDelta) return cPredCatSynthetic; const xbc7_xy_delta d = g_xbc7_xy_deltas[cand - cCandFirstXYDelta]; if (d.m_dy == 0) return cPredCatRefLeft; // dx<0, same row if (d.m_dx == 0) return cPredCatRefUp; // straight up return (d.m_dx < 0) ? cPredCatRefUpLeft : cPredCatRefUpRight; } // Weighted RGBA PSNR of a 16-pixel (4x4) block vs the source, using the // caller's per-channel weights (pack_options::m_weights), exactly mirroring // basisu_astc_ldr_encode.cpp's WSSE->PSNR: per-pixel WSSE via // color_rgba::get_weighted_dist2(), wmse = wsse / (sum_weights * num_pixels), // then 20*log10(255/sqrt(wmse)), capped for ~zero error. Used by the BC7 // alt-pack and endpoint-DPCM RDO passes. static inline float xbc7_block_wsse_psnr(const color_rgba* pOrig, const color_rgba* pDec, const uint32_t comp_weights[4]) { uint64_t wsse = 0; for (uint32_t i = 0; i < 16; i++) wsse += pDec[i].get_weighted_dist2(pOrig[i], comp_weights); const float total_comp_weights = (float)(comp_weights[0] + comp_weights[1] + comp_weights[2] + comp_weights[3]); const float wmse = (float)wsse / (total_comp_weights * 16.0f); return (wmse > 1e-5f) ? (20.0f * log10f(255.0f / sqrtf(wmse))) : 10000.0f; } // Standalone: recompute the optimal per-texel weights of a BC7 logical block // for its FIXED config + endpoints, ignoring its existing (possibly stale) // weights. out_blk receives in_blk's config + endpoints with freshly optimized // weights. comp_weights[4] are the per-channel RGBA error weights. No // dependency on pack_options, so this can be lifted into shared code later. // // Per plane: each texel's weight independently selects its interpolation // between the fixed endpoints, so setting all texels to the same value w and // decoding reveals every texel's value at w in one decode; sweeping w over all // 1<fill_box(bx * 4, by * 4, 4, 4, c); } // Color-fill a block across all debug images from its final command byte. // has_ep_wt is false for fully-predicted blocks (RepeatLast/RepeatUpper/ // SolidDPCM), which carry no endpoint/weight fields. pred_index is the weight // predictor (UINT32_MAX == none); ac_count is the nonzero DCT-weight AC count // (UINT32_MAX == not DCT-coded). UINT32_MAX maps to the n/a color. static inline void xbc7_vis_fill_command(const xbc7_debug_image_set& dbg, uint32_t bx, uint32_t by, uint8_t command_byte, bool has_ep_wt, uint32_t pred_index, uint32_t ac_count) { xbc7_vis_fill_block(dbg.m_pCommand, bx, by, g_xbc7_command_vis_colors[command_byte & 7]); if (has_ep_wt) { xbc7_vis_fill_block(dbg.m_pEndpoint_mode, bx, by, g_xbc7_endpoint_mode_vis_colors[(command_byte >> XBC7_COMMAND_ENDPOINT_MODE_SHIFT) & 7]); xbc7_vis_fill_block(dbg.m_pWeight_mode, bx, by, g_xbc7_weight_mode_vis_colors[(command_byte >> XBC7_COMMAND_WEIGHT_MODE_SHIFT) & 1]); } else { xbc7_vis_fill_block(dbg.m_pEndpoint_mode, bx, by, g_xbc7_vis_na_color); xbc7_vis_fill_block(dbg.m_pWeight_mode, bx, by, g_xbc7_vis_na_color); } xbc7_vis_fill_block(dbg.m_pPredictor, bx, by, (pred_index == UINT32_MAX) ? g_xbc7_vis_na_color : g_xbc7_predictor_vis_colors[xbc7_predictor_category(pred_index)]); xbc7_vis_fill_block(dbg.m_pAC_count, bx, by, (ac_count == UINT32_MAX) ? g_xbc7_vis_na_color : g_xbc7_ac_count_vis_colors[xbc7_ac_count_bucket(ac_count)]); } // ---- legend drawn into a strip at the bottom of each debug image ---- const uint32_t XBC7_VIS_LEGEND_ROW_H = 10; // px per entry (8px glyph + 2px gap) static inline uint32_t xbc7_vis_legend_height(uint32_t num_entries) { return num_entries * XBC7_VIS_LEGEND_ROW_H + 4; } struct xbc7_vis_legend_entry { color_rgba m_color; const char* m_pLabel; }; // Draws a swatch+label list into the BOTTOM strip of img, overwriting the // block pixels there (the image stays the source resolution). Everything // clips, so it's safe even if the strip is taller than the whole image. static void xbc7_vis_draw_legend(image& img, const xbc7_vis_legend_entry* pEntries, uint32_t num_entries) { const uint32_t legend_h = xbc7_vis_legend_height(num_entries); const uint32_t top_y = (img.get_height() >= legend_h) ? (img.get_height() - legend_h) : 0; img.fill_box(0, top_y, img.get_width(), img.get_height() - top_y, color_rgba(16, 16, 16, 255)); for (uint32_t i = 0; i < num_entries; i++) { const uint32_t y = top_y + 2 + i * XBC7_VIS_LEGEND_ROW_H; img.fill_box(2, y, 8, 8, pEntries[i].m_color); img.debug_text(14, y, 1, 1, color_rgba(255, 255, 255, 255), nullptr, false, "%s", pEntries[i].m_pLabel); } } // ---------------- encoder striping ---------------- // The main coding pass splits into 1..XBC7_MAX_ENCODER_STRIPES horizontal // stripes of whole block rows, one job per stripe. Each stripe is coded // causally from scratch (the encoder never emits references above a // stripe's first row), so thin stripes cost compression: small images stay // serial, and once striping kicks in every stripe gets at least // XBC7_MIN_STRIPE_BLOCK_ROWS rows. // (XBC7_MAX_ENCODER_STRIPES is shared with the decoder -- defined in // basisu_xbc7_decode.h.) // images shorter than this many TEXEL rows always encode as one stripe const uint32_t XBC7_MIN_IMAGE_TEXEL_ROWS_TO_STRIPE = 128; // minimum BLOCK rows per stripe once striping kicks in (bounds the // per-seam compression loss); 16 block rows == 64 texel rows const uint32_t XBC7_MIN_STRIPE_BLOCK_ROWS = 16; // Computes the encoder's stripe count (1..XBC7_MAX_ENCODER_STRIPES) and // each stripe's contiguous block-row range. Deliberately a function of // the image dimensions ONLY -- not of thread availability -- so the // encoded output is reproducible across machines; with fewer pool threads // than stripes the extra jobs simply queue. static uint32_t compute_encoder_stripes( uint32_t height_in_texels, uint32_t num_blocks_y, basisu::vector& stripes) { uint32_t num_stripes = 1; if (height_in_texels >= XBC7_MIN_IMAGE_TEXEL_ROWS_TO_STRIPE) { num_stripes = num_blocks_y / XBC7_MIN_STRIPE_BLOCK_ROWS; num_stripes = basisu::clamp(num_stripes, 1, XBC7_MAX_ENCODER_STRIPES); } num_stripes = basisu::minimum(num_stripes, basisu::maximum(1u, num_blocks_y)); compute_stripe_ranges(num_blocks_y, num_stripes, stripes); return num_stripes; } // Highest stripe count this image can carry without violating the striping // rules: at least XBC7_MIN_STRIPE_BLOCK_ROWS per stripe (so none is tiny or // empty), no more than XBC7_MAX_ENCODER_STRIPES, and 1 for images too short // to stripe usefully. Mirrors the bound compute_encoder_stripes uses. static uint32_t max_stripes_for_image(uint32_t height_in_texels, uint32_t num_blocks_y) { if (height_in_texels < XBC7_MIN_IMAGE_TEXEL_ROWS_TO_STRIPE) return 1; const uint32_t by_min_size = num_blocks_y / XBC7_MIN_STRIPE_BLOCK_ROWS; // each stripe >= min block rows return basisu::clamp(by_min_size, 1, XBC7_MAX_ENCODER_STRIPES); } uint32_t pack_options::set_num_stripes_for_image(const image& img, uint32_t desired_num_stripes) { const uint32_t height = img.get_height(); const uint32_t num_blocks_y = (height + 3) / 4; const uint32_t max_stripes = max_stripes_for_image(height, num_blocks_y); m_num_stripes = basisu::clamp(desired_num_stripes, 1, max_stripes); return m_num_stripes; } void pack_options::set_rdo_level(uint32_t rdo_level) { rdo_level = basisu::minimum(rdo_level, 100); if (!rdo_level) { // RDO fully disabled: flags off, all drops zero. m_bc7_alt_pack_enabled = false; m_endpoint_rdo_enabled = false; m_repeat_rdo_enabled = false; m_solid_rdo_enabled = false; m_bc7_alt_max_psnr_drop = 0.0f; m_endpoint_rdo_max_psnr_drop = 0.0f; m_repeat_rdo_max_psnr_drop = 0.0f; m_solid_rdo_max_psnr_drop = 0.0f; m_ac_trunc_rdo_max_psnr_drop = 0.0f; return; } const float frac = (float)rdo_level / 100.0f; // (0, 1] const float general_drop = 10.0f * frac; // BC7 alt-pack / endpoint-DPCM / AC-trunc -> up to 10 dB const float reuse_drop = 4.0f * frac; // repeat / solid block reuse -> up to 4 dB m_bc7_alt_pack_enabled = true; m_bc7_alt_max_psnr_drop = general_drop; m_endpoint_rdo_enabled = true; m_endpoint_rdo_max_psnr_drop = general_drop; m_ac_trunc_rdo_max_psnr_drop = general_drop; // no enable flag; >0 enables m_repeat_rdo_enabled = true; m_repeat_rdo_max_psnr_drop = reuse_drop; m_solid_rdo_enabled = true; m_solid_rdo_max_psnr_drop = reuse_drop; } // ------------------------- XBC7 encoding statistics ------------------------- // Populated inline during pack_image (negligible cost), printed when // opts.m_print_stats is set. Three layers: where the bytes go (per-blob // accounting), what the encoder chose (command/mode/predictor histograms), // and why (residual and coefficient distributions). struct xbc7_pack_stats { // context uint32_t m_width = 0, m_height = 0, m_total_blocks = 0, m_dct_q = 0; bool m_has_alpha = false; basisu::vector m_stripes; // choices uint64_t m_cmd_hist[8] = {}; uint64_t m_mode_hist[8] = {}; // coded (non-repeat, non-solid) blocks uint64_t m_pred_hist[cTotalCandidates * 4] = {}; uint64_t m_ep_raw_blocks = 0, m_ep_dpcm_blocks = 0, m_ep_dpcm_subsets = 0; // AC-truncation RDO uint64_t m_ac_trunc_pruned = 0; // weight-DCT AC coeffs zeroed uint64_t m_ac_trunc_blocks = 0; // blocks where >=1 coeff was pruned uint64_t m_ep_mode_hist[8] = {}; // xbc7_command_endpoint_mode of every coded block uint64_t m_ep_index_hist[NUM_XY_DELTAS] = {}; // XY-delta table index of every EP block reference uint64_t m_pbit_delta_bits = 0, m_pbit_delta_nonzero = 0; // distributions: EP residuals by [width class][stream channel slot], // wrapped bytes interpreted as int8 (the zero-peaked reading) uint64_t m_ep_resid_count[2][4] = {}; // [0]=fine, [1]=coarse uint64_t m_ep_resid_sum_abs[2][4] = {}; uint64_t m_ep_resid_zero[2][4] = {}; uint64_t m_solid_delta_count[4] = {}; uint64_t m_solid_delta_sum_abs[4] = {}; uint64_t m_solid_delta_zero[4] = {}; // weights: residual DCT vs lossless DPCM decision + tuning data uint32_t m_wt_alpha_pct = 0; uint64_t m_wt_dct_blocks = 0, m_wt_dpcm_blocks = 0; uint64_t m_wt_dct_est_bits_total = 0, m_wt_dpcm_est_bits_total = 0, m_wt_chosen_est_bits = 0; uint64_t m_wt_block_acs_hist_dct[33] = {}; // total ACs per block (clamped), split by which mode won uint64_t m_wt_block_acs_hist_dpcm[33] = {}; uint64_t m_wt_dpcm_pred_hist[cTotalCandidates * 4] = {}; uint64_t m_wt_resid_count[3] = {}, m_wt_resid_zero[3] = {}, m_wt_resid_sum_abs[3] = {}; // [weight_bits - 2] uint64_t m_wt_resid_mag_hist[3][9] = {}; // [weight_bits - 2][min(|centered resid|, 8)] xbc7_dct_stats m_dct_stats; // winning-candidate coefficient stats // where the bytes go (filled at serialize time) uint32_t m_blob_raw_size[BLOB_STREAM_MAX_IDS] = {}; uint32_t m_blob_stored_size[BLOB_STREAM_MAX_IDS] = {}; uint32_t m_total_file_size = 0; // order-0 byte histograms of the pre-Zstd blob contents uint64_t m_blob_hist[cBlobFirstUnused][256] = {}; void record_blob_bytes(uint32_t id, const uint8_vec* pBlob) { if ((id >= (uint32_t)cBlobFirstUnused) || (!pBlob)) return; for (size_t i = 0; i < pBlob->size(); i++) m_blob_hist[id][(*pBlob)[i]]++; } void record_ep_residuals(bool fine_class, const uint8_t* pResiduals, uint32_t num_residuals) { const uint32_t cls = fine_class ? 0 : 1; for (uint32_t i = 0; i < num_residuals; i++) { const uint32_t chan = i >> 1; const int v = (int8_t)pResiduals[i]; m_ep_resid_count[cls][chan]++; m_ep_resid_sum_abs[cls][chan] += (uint64_t)basisu::iabs(v); if (!v) m_ep_resid_zero[cls][chan]++; } } // Accumulate a per-stripe (per-job) collector into this one. Pure // counters only: the context fields (dims/Q/stripes/alpha) and the // blob size/histogram arrays are owned by the final stats object and // filled outside the jobs. void merge(const xbc7_pack_stats& o) { for (uint32_t i = 0; i < 8; i++) { m_cmd_hist[i] += o.m_cmd_hist[i]; m_mode_hist[i] += o.m_mode_hist[i]; m_ep_mode_hist[i] += o.m_ep_mode_hist[i]; } for (uint32_t i = 0; i < cTotalCandidates * 4; i++) { m_pred_hist[i] += o.m_pred_hist[i]; m_wt_dpcm_pred_hist[i] += o.m_wt_dpcm_pred_hist[i]; } m_ep_raw_blocks += o.m_ep_raw_blocks; m_ep_dpcm_blocks += o.m_ep_dpcm_blocks; m_ep_dpcm_subsets += o.m_ep_dpcm_subsets; m_ac_trunc_pruned += o.m_ac_trunc_pruned; m_ac_trunc_blocks += o.m_ac_trunc_blocks; for (uint32_t i = 0; i < NUM_XY_DELTAS; i++) m_ep_index_hist[i] += o.m_ep_index_hist[i]; m_pbit_delta_bits += o.m_pbit_delta_bits; m_pbit_delta_nonzero += o.m_pbit_delta_nonzero; for (uint32_t cls = 0; cls < 2; cls++) { for (uint32_t c = 0; c < 4; c++) { m_ep_resid_count[cls][c] += o.m_ep_resid_count[cls][c]; m_ep_resid_sum_abs[cls][c] += o.m_ep_resid_sum_abs[cls][c]; m_ep_resid_zero[cls][c] += o.m_ep_resid_zero[cls][c]; } } for (uint32_t c = 0; c < 4; c++) { m_solid_delta_count[c] += o.m_solid_delta_count[c]; m_solid_delta_sum_abs[c] += o.m_solid_delta_sum_abs[c]; m_solid_delta_zero[c] += o.m_solid_delta_zero[c]; } m_wt_dct_blocks += o.m_wt_dct_blocks; m_wt_dpcm_blocks += o.m_wt_dpcm_blocks; m_wt_dct_est_bits_total += o.m_wt_dct_est_bits_total; m_wt_dpcm_est_bits_total += o.m_wt_dpcm_est_bits_total; m_wt_chosen_est_bits += o.m_wt_chosen_est_bits; for (uint32_t i = 0; i <= 32; i++) { m_wt_block_acs_hist_dct[i] += o.m_wt_block_acs_hist_dct[i]; m_wt_block_acs_hist_dpcm[i] += o.m_wt_block_acs_hist_dpcm[i]; } for (uint32_t cls = 0; cls < 3; cls++) { m_wt_resid_count[cls] += o.m_wt_resid_count[cls]; m_wt_resid_zero[cls] += o.m_wt_resid_zero[cls]; m_wt_resid_sum_abs[cls] += o.m_wt_resid_sum_abs[cls]; for (uint32_t m = 0; m < 9; m++) m_wt_resid_mag_hist[cls][m] += o.m_wt_resid_mag_hist[cls][m]; } m_dct_stats.merge(o.m_dct_stats); } // fmt_variants rejects format specs on strings, so pad manually static std::string pad(const char* pStr, size_t n) { std::string s(pStr); if (s.size() < n) s.resize(n, ' '); return s; } void print() const { static const char* s_blob_names[cBlobFirstUnused] = { "header", "commands", "cfg: block_config", "cfg: partition2", "cfg: partition3", "wt: predictors", "wt-dct: dc_small", "wt-dct: dc_large", "wt-dct: ac_coeffs", "wt-dct: signs", "ep: pbits", "ep: fine_r", "ep: fine_g", "ep: fine_b", "ep: fine_a", "ep: coarse_r", "ep: coarse_g", "ep: coarse_b", "ep: coarse_a", "ep: raw", "ep: blk_index", "wt-dpcm: absolute", "solid: rgba_deltas", "wt-dpcm: resid_2", "wt-dpcm: resid_3", "wt-dpcm: resid_4", "seek_table" }; static const char* s_cmd_names[8] = { "repeat_last", "repeat_upper", "solid_dpcm", "new_config", "reuse_left", "reuse_upper", "reuse_ldiag", "reuse_rdiag" }; const float inv_blocks = m_total_blocks ? (100.0f / (float)m_total_blocks) : 0.0f; fmt_debug_printf("\n========== XBC7 pack stats ==========\n"); fmt_debug_printf("{}x{}, {} blocks, Q={}, has_alpha={}\n", m_width, m_height, m_total_blocks, m_dct_q, m_has_alpha); { std::string s(string_format("encoder stripes: %u", m_stripes.size_u32())); for (uint32_t i = 0; i < m_stripes.size_u32(); i++) s += string_format("%s block rows %u-%u", i ? "," : " --", m_stripes[i].m_first_block_row, m_stripes[i].m_first_block_row + m_stripes[i].m_num_block_rows - 1); fmt_debug_printf("{}\n", s); } // ---- per-blob byte accounting ---- fmt_debug_printf("\n---- blob accounting (raw -> stored after Zstd) ----\n"); fmt_debug_printf("{}{}{}{}{}{}\n", pad("blob", 20), pad("raw", 10), pad("stored", 10), pad("ratio", 8), pad("%file", 8), "bits/blk"); uint64_t total_raw = 0, total_stored = 0; for (uint32_t id = 0; id < (uint32_t)cBlobFirstUnused; id++) { if (!m_blob_raw_size[id]) continue; total_raw += m_blob_raw_size[id]; total_stored += m_blob_stored_size[id]; fmt_debug_printf("{}{}{}{}{}{.3}\n", pad(s_blob_names[id], 20), pad(string_format("%u", m_blob_raw_size[id]).c_str(), 10), pad(string_format("%u", m_blob_stored_size[id]).c_str(), 10), pad(string_format("%.3f", m_blob_stored_size[id] ? ((float)m_blob_raw_size[id] / (float)m_blob_stored_size[id]) : 0.0f).c_str(), 8), pad(string_format("%.2f", m_total_file_size ? ((float)m_blob_stored_size[id] * 100.0f / (float)m_total_file_size) : 0.0f).c_str(), 8), m_total_blocks ? ((float)m_blob_stored_size[id] * 8.0f / (float)m_total_blocks) : 0.0f); } const uint64_t overhead = (uint64_t)m_total_file_size - total_stored; fmt_debug_printf("{}{}{}\n", pad("TOTAL", 20), pad(string_format("%llu", (unsigned long long)total_raw).c_str(), 10), pad(string_format("%llu", (unsigned long long)total_stored).c_str(), 10)); fmt_debug_printf("container+dir overhead: {} bytes\n", overhead); fmt_debug_printf("file: {} bytes, {.3} bits/block, {.4} bpp\n", m_total_file_size, m_total_blocks ? ((float)m_total_file_size * 8.0f / (float)m_total_blocks) : 0.0f, (m_width && m_height) ? ((float)m_total_file_size * 8.0f / (float)(m_width * m_height)) : 0.0f); // ---- group rollup: where the bytes go, raw vs stored ---- { // m_super buckets the fine groups into the coarse summary: // 0 = commands+config, 1 = endpoints/pbits, 2 = weights, 3 = other struct group_def { const char* m_pName; uint8_t m_super; uint8_t m_ids[12]; uint32_t m_num_ids; }; static const group_def s_groups[] = { { "commands", 0, { cBlobCommands }, 1 }, { "config/partition", 0, { cBlobBC7BlockConfig, cBlobPartition2, cBlobPartition3 }, 3 }, { "endpoints/pbits", 1, { cBlobEPDeltaFineR, cBlobEPDeltaFineG, cBlobEPDeltaFineB, cBlobEPDeltaFineA, cBlobEPDeltaCoarseR, cBlobEPDeltaCoarseG, cBlobEPDeltaCoarseB, cBlobEPDeltaCoarseA, cBlobEPRaw, cBlobEPBlockIndex, cBlobPBits }, 11 }, { "wt predictors", 2, { cBlobWeightPredictors }, 1 }, { "wt dct", 2, { cBlobDCCoeffsSmall, cBlobDCCoeffsLarge, cBlobACCoeffs, cBlobCoeffSigns }, 4 }, { "wt dpcm", 2, { cBlobRawWeightBits, cBlobDPCMWeightResid2, cBlobDPCMWeightResid3, cBlobDPCMWeightResid4 }, 4 }, { "solid", 3, { cBlobSolidRGBADeltas }, 1 }, { "header", 3, { cBlobHeader }, 1 }, { "seek_table", 3, { cBlobStripeSeekTable }, 1 }, }; const uint32_t num_groups = (uint32_t)(sizeof(s_groups) / sizeof(s_groups[0])); const float inv_file = m_total_file_size ? (100.0f / (float)m_total_file_size) : 0.0f; fmt_debug_printf("\n---- group rollup ----\n"); fmt_debug_printf("{}{}{}{}\n", pad("group", 19), pad("raw", 10), pad("stored", 10), "%file"); uint64_t group_stored_total = 0; uint64_t super_raw[4] = {}, super_stored[4] = {}; for (uint32_t g = 0; g < num_groups; g++) { uint64_t raw = 0, stored = 0; for (uint32_t i = 0; i < s_groups[g].m_num_ids; i++) { raw += m_blob_raw_size[s_groups[g].m_ids[i]]; stored += m_blob_stored_size[s_groups[g].m_ids[i]]; } group_stored_total += stored; super_raw[s_groups[g].m_super] += raw; super_stored[s_groups[g].m_super] += stored; if (!raw) continue; fmt_debug_printf("{}{}{}{.2}\n", pad(s_groups[g].m_pName, 19), pad(string_format("%llu", (unsigned long long)raw).c_str(), 10), pad(string_format("%llu", (unsigned long long)stored).c_str(), 10), (float)stored * inv_file); } const uint64_t container_ovh = (uint64_t)m_total_file_size - group_stored_total; fmt_debug_printf("{}{}{}{.2}\n", pad("container", 19), pad("", 10), pad(string_format("%llu", (unsigned long long)container_ovh).c_str(), 10), (float)container_ovh * inv_file); fmt_debug_printf("{}{}{}{}\n", pad("TOTAL", 19), pad("", 10), pad(string_format("%u", m_total_file_size).c_str(), 10), "100.00"); // coarse three-bucket summary static const char* s_super_names[4] = { "= commands+config", "= endpoints/pbits", "= weights (all)", "= other" }; fmt_debug_printf("\n"); for (uint32_t s = 0; s < 4; s++) { if (!super_raw[s]) continue; fmt_debug_printf("{}{}{}{.2}\n", pad(s_super_names[s], 19), pad(string_format("%llu", (unsigned long long)super_raw[s]).c_str(), 10), pad(string_format("%llu", (unsigned long long)super_stored[s]).c_str(), 10), (float)super_stored[s] * inv_file); } } // ---- per-blob order-0 symbol stats ---- // H = Shannon entropy of the raw byte stream (bits/byte); ideal = the // order-0 entropy-coded size. stored/ideal < 1 means Zstd's matches + // higher-order context beat a memoryless coder; > 1 means an entropy // coder (FSE/rANS over this alphabet) would beat what Zstd achieves. fmt_debug_printf("\n---- blob symbol stats (order-0, pre-Zstd) ----\n"); fmt_debug_printf("{}{}{}{}{}{}{}\n", pad("blob", 20), pad("bytes", 10), pad("syms", 6), pad("H b/byte", 10), pad("ideal", 10), pad("stored", 10), "st/id"); for (uint32_t id = 0; id < (uint32_t)cBlobFirstUnused; id++) { uint64_t n = 0; uint32_t distinct = 0; for (uint32_t s = 0; s < 256; s++) { n += m_blob_hist[id][s]; if (m_blob_hist[id][s]) distinct++; } if (!n) continue; double entropy = 0.0; for (uint32_t s = 0; s < 256; s++) { if (!m_blob_hist[id][s]) continue; const double p = (double)m_blob_hist[id][s] / (double)n; entropy -= p * std::log2(p); } const uint64_t ideal_bytes = (uint64_t)std::ceil(entropy * (double)n / 8.0); fmt_debug_printf("{}{}{}{}{}{}{.3}\n", pad(s_blob_names[id], 20), pad(string_format("%llu", (unsigned long long)n).c_str(), 10), pad(string_format("%u", distinct).c_str(), 6), pad(string_format("%.3f", entropy).c_str(), 10), pad(string_format("%llu", (unsigned long long)ideal_bytes).c_str(), 10), pad(string_format("%u", m_blob_stored_size[id]).c_str(), 10), ideal_bytes ? ((float)m_blob_stored_size[id] / (float)ideal_bytes) : 0.0f); // condensed top symbols: up to 6, plus a count of the rest uint64_t hist[256]; memcpy(hist, m_blob_hist[id], sizeof(hist)); std::string tops(" top:"); uint32_t shown = 0; uint64_t shown_count = 0; for (; shown < 6; shown++) { uint32_t best_s = 0; uint64_t best_c = 0; for (uint32_t s = 0; s < 256; s++) { if (hist[s] > best_c) { best_c = hist[s]; best_s = s; } } if (!best_c) break; hist[best_s] = 0; shown_count += best_c; tops += string_format(" %u:%llu(%.1f%%)", best_s, (unsigned long long)best_c, (double)best_c * 100.0 / (double)n); } if (distinct > shown) tops += string_format(" +%u more (%.1f%%)", distinct - shown, (double)(n - shown_count) * 100.0 / (double)n); fmt_debug_printf("{}\n", tops); } // ---- commands / modes ---- fmt_debug_printf("\n---- COMMANDS: per-block command histogram ----\n"); for (uint32_t i = 0; i < 8; i++) if (m_cmd_hist[i]) fmt_debug_printf("{}: {} ({.2}%)\n", pad(s_cmd_names[i], 13), m_cmd_hist[i], (float)m_cmd_hist[i] * inv_blocks); fmt_debug_printf("\n---- CONFIG: BC7 modes of coded blocks ----\n"); for (uint32_t i = 0; i < 8; i++) if (m_mode_hist[i]) fmt_debug_printf("mode {}: {} ({.2}%)\n", i, m_mode_hist[i], (float)m_mode_hist[i] * inv_blocks); // ---- endpoints ---- fmt_debug_printf("\n---- ENDPOINTS: predictor modes + residuals ----\n"); fmt_debug_printf("raw blocks: {}, dpcm blocks: {} ({} subsets)\n", m_ep_raw_blocks, m_ep_dpcm_blocks, m_ep_dpcm_subsets); fmt_debug_printf("ep modes: raw {}, left {}, up {}, ldiag {}, rdiag {}, index {}, left_s1 {}, up_s1 {}\n", m_ep_mode_hist[0], m_ep_mode_hist[1], m_ep_mode_hist[2], m_ep_mode_hist[3], m_ep_mode_hist[4], m_ep_mode_hist[5], m_ep_mode_hist[6], m_ep_mode_hist[7]); if (m_ep_mode_hist[(uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMBlockIndex]) { std::string idx_line("ep index deltas:"); for (uint32_t i = 0; i < NUM_XY_DELTAS; i++) if (m_ep_index_hist[i]) idx_line += string_format(" (%i,%i):%llu", (int)g_xbc7_xy_deltas[i].m_dx, (int)g_xbc7_xy_deltas[i].m_dy, (unsigned long long)m_ep_index_hist[i]); fmt_debug_printf("{}\n", idx_line); } if (m_pbit_delta_bits) fmt_debug_printf("pbit deltas: {} bits, nonzero: {} ({.2}%)\n", m_pbit_delta_bits, m_pbit_delta_nonzero, (float)m_pbit_delta_nonzero * 100.0f / (float)m_pbit_delta_bits); static const char* s_class_names[2] = { "fine", "coarse" }; static const char* s_chan_names[4] = { "R", "G", "B", "A" }; for (uint32_t cls = 0; cls < 2; cls++) { for (uint32_t c = 0; c < 4; c++) { if (!m_ep_resid_count[cls][c]) continue; fmt_debug_printf("ep resid {} {}: n={}, mean|d|={.3}, zero={.2}%\n", pad(s_class_names[cls], 6), s_chan_names[c], m_ep_resid_count[cls][c], (float)m_ep_resid_sum_abs[cls][c] / (float)m_ep_resid_count[cls][c], (float)m_ep_resid_zero[cls][c] * 100.0f / (float)m_ep_resid_count[cls][c]); } } // ---- weight predictors (DCT-mode blocks; DPCM-mode predictors are in the next section) ---- fmt_debug_printf("\n---- WEIGHTS: DCT-mode predictors ----\n"); { uint64_t class_totals[3] = { 0, 0, 0 }; // absolute / synthetic / dictionary uint64_t amp_totals[4] = { 0, 0, 0, 0 }; uint64_t coded_blocks = 0; for (uint32_t c = 0; c < cTotalCandidates; c++) { for (uint32_t a = 0; a < 4; a++) { const uint64_t t = m_pred_hist[c + a * cTotalCandidates]; amp_totals[a] += t; coded_blocks += t; if (c == cCandAbsolute) class_totals[0] += t; else if (c >= cCandFirstXYDelta) class_totals[2] += t; else class_totals[1] += t; } } const float inv_coded = coded_blocks ? (100.0f / (float)coded_blocks) : 0.0f; fmt_debug_printf("classes: absolute {} ({.2}%), synthetic {} ({.2}%), dictionary {} ({.2}%)\n", class_totals[0], (float)class_totals[0] * inv_coded, class_totals[1], (float)class_totals[1] * inv_coded, class_totals[2], (float)class_totals[2] * inv_coded); fmt_debug_printf("amp codes: identity {}, flip {}, half {}, half-flip {}\n", amp_totals[0], amp_totals[1], amp_totals[2], amp_totals[3]); // top 12 joint (cand, amp) symbols fmt_debug_printf("top predictors:\n"); uint32_t idx[cTotalCandidates * 4]; for (uint32_t i = 0; i < cTotalCandidates * 4; i++) idx[i] = i; std::sort(idx, idx + cTotalCandidates * 4, [&](uint32_t a, uint32_t b) { return m_pred_hist[a] > m_pred_hist[b]; }); for (uint32_t r = 0; r < 12; r++) { const uint32_t i = idx[r]; if (!m_pred_hist[i]) break; const uint32_t cand = i % cTotalCandidates, amp = i / cTotalCandidates; static const char* s_amp_names[4] = { "", " (flip)", " (half)", " (half-flip)" }; std::string name; if (cand < cCandFirstXYDelta) name = g_cand_names[cand]; else { const xbc7_xy_delta& dlt = g_xbc7_xy_deltas[cand - cCandFirstXYDelta]; name = string_format("copy (%i,%i)", (int)dlt.m_dx, (int)dlt.m_dy); } name += s_amp_names[amp]; fmt_debug_printf(" {}: {} ({.2}%)\n", pad(name.c_str(), 28), m_pred_hist[i], (float)m_pred_hist[i] * inv_coded); } } // ---- weight coding mode ---- fmt_debug_printf("\n---- WEIGHTS: DCT vs lossless DPCM decision ----\n"); { const uint64_t wt_coded = m_wt_dct_blocks + m_wt_dpcm_blocks; const float inv_wt = wt_coded ? (100.0f / (float)wt_coded) : 0.0f; fmt_debug_printf("dct: {} ({.2}%), dpcm: {} ({.2}%) [decision: dpcm_bits*100 <= dct_bits*{}]\n", m_wt_dct_blocks, (float)m_wt_dct_blocks * inv_wt, m_wt_dpcm_blocks, (float)m_wt_dpcm_blocks * inv_wt, m_wt_alpha_pct); fmt_debug_printf("est bits -- all DCT: {}, all DPCM: {}, chosen: {}\n", m_wt_dct_est_bits_total / 8, m_wt_dpcm_est_bits_total / 8, m_wt_chosen_est_bits / 8); std::string l1("ACs/block when DCT won: "); std::string l2("ACs/block when DPCM won: "); for (uint32_t i = 0; i <= 32; i++) { if (m_wt_block_acs_hist_dct[i]) l1 += string_format("%u:%llu ", i, (unsigned long long)m_wt_block_acs_hist_dct[i]); if (m_wt_block_acs_hist_dpcm[i]) l2 += string_format("%u:%llu ", i, (unsigned long long)m_wt_block_acs_hist_dpcm[i]); } fmt_debug_printf("{}\n", l1); if (m_wt_dpcm_blocks) fmt_debug_printf("{}\n", l2); for (uint32_t cls = 0; cls < 3; cls++) { if (!m_wt_resid_count[cls]) continue; fmt_debug_printf("dpcm resid {}b: n={}, mean|d|={.3}, zero={.2}%\n", cls + 2, m_wt_resid_count[cls], (float)m_wt_resid_sum_abs[cls] / (float)m_wt_resid_count[cls], (float)m_wt_resid_zero[cls] * 100.0f / (float)m_wt_resid_count[cls]); std::string mh(string_format("dpcm resid %ub |mag| hist:", cls + 2)); for (uint32_t m = 0; m < 9; m++) if (m_wt_resid_mag_hist[cls][m]) mh += string_format(" %u:%.3f%%", m, (double)m_wt_resid_mag_hist[cls][m] * 100.0 / (double)m_wt_resid_count[cls]); fmt_debug_printf("{}\n", mh); } if (m_wt_dpcm_blocks) { uint32_t idx[cTotalCandidates * 4]; for (uint32_t i = 0; i < cTotalCandidates * 4; i++) idx[i] = i; std::sort(idx, idx + cTotalCandidates * 4, [&](uint32_t a, uint32_t b) { return m_wt_dpcm_pred_hist[a] > m_wt_dpcm_pred_hist[b]; }); std::string tops("top dpcm predictors:"); for (uint32_t r = 0; r < 8; r++) { const uint32_t i = idx[r]; if (!m_wt_dpcm_pred_hist[i]) break; const uint32_t cand = i % cTotalCandidates, amp = i / cTotalCandidates; static const char* s_amp_suffix[4] = { "", "/flip", "/half", "/half-flip" }; std::string name; if (cand < cCandFirstXYDelta) name = g_cand_names[cand]; else { const xbc7_xy_delta& dlt = g_xbc7_xy_deltas[cand - cCandFirstXYDelta]; name = string_format("copy(%i,%i)", (int)dlt.m_dx, (int)dlt.m_dy); } tops += string_format(" %s%s:%llu", name.c_str(), s_amp_suffix[amp], (unsigned long long)m_wt_dpcm_pred_hist[i]); } fmt_debug_printf("{}\n", tops); } } // ---- solid ---- if (m_cmd_hist[(uint32_t)xbc7_command_id::cCmdSolidDPCM]) { fmt_debug_printf("\n---- SOLID: color deltas vs neighbor edge prediction ----\n"); for (uint32_t c = 0; c < 4; c++) { if (!m_solid_delta_count[c]) continue; fmt_debug_printf("{}: n={}, mean|d|={.3}, zero={.2}%\n", s_chan_names[c], m_solid_delta_count[c], (float)m_solid_delta_sum_abs[c] / (float)m_solid_delta_count[c], (float)m_solid_delta_zero[c] * 100.0f / (float)m_solid_delta_count[c]); } } // ---- DCT coefficient stats ---- fmt_debug_printf("\n"); m_dct_stats.print(); fmt_debug_printf("=====================================\n\n"); } }; // NOTE: prev-emission coherence biasing of the predictor/mode sweeps was // tried here (prefer the previously-emitted symbol within a cost margin, // to cluster the predictor/command/index streams into Zstd-friendly runs) // and MEASURED WORSE at every margin including pure tie-breaking: the // existing earliest-candidate tie-break already concentrates ties onto a // few globally-frequent symbols, and that global skew compresses better // than local runs. Don't re-try without a new mechanism. // The mechanism that DOES work, generalized: a later sweep candidate must // beat the incumbent by more than this margin to displace it, so ties and // near-ties collapse onto the lowest-index candidates GLOBALLY (global // symbol skew is what Zstd prices). For the endpoint sweep the low-index // candidates are also the dedicated neighbor modes, which don't spend an // index byte. The weight-DCT sweep has its own long-standing equivalent // (ERR_SWITCH_MARGIN_PERMILLE). const uint32_t XBC7_SWEEP_SWITCH_MARGIN_PERMILLE = 30; // ---- effort-driven weight-predictor pruning ---------------------------- // The per-block weight-grid sweeps (pack_weights / pack_weights_dpcm) try // every (cand_index, amp_code) predictor; the DCT sweep re-runs a forward+ // inverse DCT per candidate, so it dominates encode time at Q < 100. The // effort knob prunes WHICH candidates are tried (not the iteration ORDER -- // the sweeps still walk cand_index 0..cTotalCandidates and merely SKIP the // disabled ones, so the earliest-candidate tie-break is preserved and effort // 10 reproduces the pre-knob output exactly). // // g_xbc7_pred_priority lists candidates most-valuable first, derived from // winning-predictor histograms across many images at Q=1/10/80 (opaque + // alpha). The dictionary "copy(dx,dy)" predictors collectively win ~2/3 of // blocks, so the high-value core deliberately mixes the strongest synthetic // predictors (left/upper edge, diag avg) with the nearest causal copies. // cCandAbsolute is first -- it's the no-prediction fallback and must always // be available. Any candidate not listed here is appended in natural order // by xbc7_build_pred_mask (so the table need only spell out the head). static const uint8_t g_xbc7_pred_priority[] = { cCandAbsolute, cCandLeftEdge, cCandUpperEdge, cCandFirstXYDelta + 0, cCandFirstXYDelta + 1, cCandDiagAvg, // <-- core 6 (effort 0) cCandFirstXYDelta + 6, cCandFirstXYDelta + 2, cCandFirstXYDelta + 7, cCandFirstXYDelta + 8, cCandReflectLeft, cCandFirstXYDelta + 4, cCandFirstXYDelta + 5, cCandReflectUpper, cCandPlaneFit, cCandFirstXYDelta + 3, cCandLUBlendStrong, cCandDDR, cCandDDL, cCandGradient, cCandGradientDamped, cCandLUBlend, cCandLUAvg, cCandUpperDiagEdgeBlend, cCandDiagEdgeBlend, cCandMED, cCandGAB, }; // Build the enabled-candidate bitmask for an effort level [0,10]. effort 0 // enables the core 6; the count ramps linearly to the full set at effort 10 // (where the mask is all-ones -> the sweeps are byte-for-byte unchanged). // cTotalCandidates < 64, so a single uint64_t holds the whole set. static uint64_t xbc7_build_pred_mask(uint32_t effort) { static_assert(cTotalCandidates <= 64, "predictor mask must fit in uint64_t"); effort = basisu::minimum(effort, 10); const uint32_t cCoreCands = 6; uint32_t count = cCoreCands + ((cTotalCandidates - cCoreCands) * effort + 5) / 10; count = basisu::minimum(count, cTotalCandidates); // priority head, then any remaining candidate in natural order -> a valid // permutation of [0, cTotalCandidates) regardless of what the head omits uint8_t order[cTotalCandidates]; bool used[cTotalCandidates] = { false }; uint32_t n = 0; for (uint32_t i = 0; i < sizeof(g_xbc7_pred_priority); i++) { const uint8_t c = g_xbc7_pred_priority[i]; assert((c < cTotalCandidates) && !used[c]); // head must be unique & in-range used[c] = true; order[n++] = c; } for (uint32_t c = 0; c < cTotalCandidates; c++) if (!used[c]) order[n++] = (uint8_t)c; assert(n == cTotalCandidates); uint64_t mask = 0; for (uint32_t i = 0; i < count; i++) mask |= (1ull << order[i]); assert(mask & (1ull << cCandAbsolute)); // fallback always present return mask; } // Amp codes the per-block sweeps try, by effort [0,10]. The amp loop runs in // descending win-share order (0=identity, 1=flip, 2=half-contrast, 3=half- // flip -- confirmed by histograms), so trying the first N keeps the most // valuable. effort 10 -> 4 (the full set -> output unchanged vs pre-knob). static uint32_t xbc7_amp_codes_for_effort(uint32_t effort) { static const uint8_t tbl[11] = { 1, 1, 1, 1, 2, 2, 3, 3, 4, 4, 4 }; return tbl[basisu::minimum(effort, 10)]; } bool pack_weights( uint32_t bx, uint32_t by, uint32_t num_blocks_x, const tile_bounds& tile, const color_rgba orig_block[16], const vector2D &log_blks, dct_syms best_syms[2], basist::bc7u::log_bc7_block &best_cand_log_blk, uint32_t& best_predictor_index, xbc7_weight_grid_dct_fixed &weight_grid_dct_fixed, fxvec &dct_work_fixed, const pack_options& opts, uint64_t enabled_pred_mask, uint32_t num_amp_codes) { best_predictor_index = 0; const uint32_t global_q = opts.m_dct_q; const uint64_t ERR_SWITCH_MARGIN_PERMILLE = 20; const uint32_t TOTAL_AMP_CODES = num_amp_codes; // 0=identity, 1=flip, 2=half contrast, 3=half of flip (effort-pruned) const basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by); best_syms[0].clear(); best_syms[1].clear(); best_cand_log_blk = log_blk; uint64_t best_err = UINT64_MAX; uint32_t best_num_ac_syms = UINT32_MAX; for (uint32_t amp_code = 0; amp_code < TOTAL_AMP_CODES; amp_code++) { for (uint32_t cand_index = 0; cand_index < cTotalCandidates; cand_index++) { if ((amp_code) && (cand_index == cCandAbsolute)) continue; // effort pruning: skip predictors not in this effort's enabled set // (mask is all-ones at effort 10, so this never fires there) if (!((enabled_pred_mask >> cand_index) & 1u)) continue; basist::bc7u::log_bc7_block cand_log_blk(log_blk); uint32_t cand_total_ac_syms = 0; dct_syms cand_syms[2]; cand_syms[0].clear(); cand_syms[1].clear(); for (uint32_t p = 0; p < cand_log_blk.m_num_planes; p++) { int weight_preds[16]; int* pWeight_predictions = nullptr; if (cand_index != cCandAbsolute) { bool eval_status = eval_weight_predictor( cand_index, amp_code, bx, by, num_blocks_x, tile, log_blks, p, weight_preds); if (!eval_status) goto skip_cand; pWeight_predictions = weight_preds; } dct_syms syms; weight_grid_dct_fixed.forward(basist::fixed16_16::from_int(global_q), p, pWeight_predictions, cand_log_blk, syms, dct_work_fixed); memset(cand_log_blk.m_weights[p], 0, 16); bool status = weight_grid_dct_fixed.inverse(basist::fixed16_16::from_int(global_q), p, pWeight_predictions, syms, cand_log_blk, dct_work_fixed); if (!status) { assert(0); return false; } uint32_t total_acs = syms.m_ac_vals.size_u32(); if (total_acs && (syms.m_ac_vals[total_acs - 1].m_coeff == INT16_MAX)) total_acs--; cand_total_ac_syms += total_acs; cand_syms[p] = syms; } // p // scoped so 'goto skip_cand' leaps over (not into) these inits -- required under /permissive- { color_rgba cand_block_pixels[16]; if (!basist::bc7u::unpack_bc7(cand_log_blk, (basist::color_rgba*)cand_block_pixels)) { assert(0); return false; } uint64_t cand_err = 0; for (uint32_t i = 0; i < 16; i++) cand_err += cand_block_pixels[i].get_weighted_dist2(orig_block[i], opts.m_weights); if ((cand_total_ac_syms < best_num_ac_syms) || ((cand_total_ac_syms == best_num_ac_syms) && ((cand_err * 1000) < (best_err * (1000 - ERR_SWITCH_MARGIN_PERMILLE))))) { best_cand_log_blk = cand_log_blk; best_predictor_index = cand_index + amp_code * cTotalCandidates; best_num_ac_syms = cand_total_ac_syms; best_err = cand_err; best_syms[0] = cand_syms[0]; best_syms[1] = cand_syms[1]; } } // end candidate-eval scope skip_cand: ; } // cand_index } // amp_code assert(best_syms[0].m_ac_vals.size()); return true; } // Estimated per-symbol coding costs in 1/8-bit units ("octobits"), measured // on the kodim corpus via the stats dashboard. // // DPCM weight residual cost, [weight_bits - 2][min(|centered resid|, 8)]: // -log2 of each symbol's probability (the magnitude's mass split across // +/-), measured at Q=100 where EVERY block codes DPCM, so the // distributions carry no mode-decision selection bias. Order-0 model; // Zstd lands within ~13% of it on these streams. static const uint32_t g_dpcm_resid_cost_obits[3][9] = { { 8, 17, 47, 47, 47, 47, 47, 47, 47 }, // 2-bit (centered range [-2,1]) { 12, 19, 27, 38, 63, 63, 63, 63, 63 }, // 3-bit (centered range [-4,3]) { 12, 25, 29, 33, 38, 44, 52, 61, 81 }, // 4-bit (centered range [-8,7]) }; // DCT stream bytes priced at their measured post-Zstd cost (Q 60..95): // DC bytes ~4.7 bits with the 6-bit DC quantization below (was ~6.4 at // 8-bit DC), AC run/magnitude stream bytes 3.1..3.8 bits (3.5 used). // Sign bits are incompressible noise: exactly 1 bit each. const uint32_t DCT_DC_BYTE_COST_OBITS = 38; const uint32_t DCT_AC_BYTE_COST_OBITS = 28; // Lossless residual DPCM weight coding: predict the dequantized [0,64] grid // with the shared predictor bank, quantize the prediction to the plane's bit // depth (quant_weight), then wrap-difference the quantized indices mod 2^n. // Reconstruction is exact, so the predictor choice affects ONLY compressed // size: pick the candidate minimizing the estimated emitted cost (octobit // table above) -- a content-adaptive estimate, since zero-heavy residual // grids are far cheaper than the stream average. cand 0 (absolute, // predictor index 0) is always available, emits the raw indices to a // separate blob, and is priced at its raw stored size. struct dpcm_weights { uint32_t m_pred_index = 0; // joint (cand + amp * cTotalCandidates) byte uint8_t m_syms[2][16] = {}; // wrapped n-bit symbols (raw indices if absolute), [plane][texel] uint64_t m_est_cost_obits = 0; // estimated emitted cost of the winning candidate, 1/8-bit units }; static void pack_weights_dpcm( uint32_t bx, uint32_t by, uint32_t num_blocks_x, const tile_bounds& tile, const vector2D& log_blks, dpcm_weights& result, uint64_t enabled_pred_mask, uint32_t num_amp_codes) { const uint32_t TOTAL_AMP_CODES = num_amp_codes; const basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by); uint64_t best_cost = UINT64_MAX; for (uint32_t amp_code = 0; amp_code < TOTAL_AMP_CODES; amp_code++) { for (uint32_t cand_index = 0; cand_index < cTotalCandidates; cand_index++) { if ((amp_code) && (cand_index == cCandAbsolute)) continue; // effort pruning (see pack_weights): all-ones at effort 10 if (!((enabled_pred_mask >> cand_index) & 1u)) continue; uint64_t cost = 0; uint8_t syms[2][16] = {}; // zeroed so single-plane blocks never copy indeterminate plane-1 bytes bool valid = true; for (uint32_t p = 0; p < log_blk.m_num_planes; p++) { const uint32_t num_bits = log_blk.m_weight_bits[p]; const uint32_t mask = (1u << num_bits) - 1; const int half = 1 << (num_bits - 1); int weight_preds[16]; int* pWeight_predictions = nullptr; if (cand_index != cCandAbsolute) { if (!eval_weight_predictor(cand_index, amp_code, bx, by, num_blocks_x, tile, log_blks, p, weight_preds)) { valid = false; break; } pWeight_predictions = weight_preds; } for (uint32_t i = 0; i < 16; i++) { const uint32_t pred_index = pWeight_predictions ? basist::bc7u::quant_weight(pWeight_predictions[i], num_bits) : 0; const uint32_t sym = (log_blk.m_weights[p][i] - pred_index) & mask; syms[p][i] = (uint8_t)sym; if (pWeight_predictions) { const int v = (sym >= (uint32_t)half) ? ((int)sym - (int)(mask + 1)) : (int)sym; cost += g_dpcm_resid_cost_obits[num_bits - 2][basisu::minimum(basisu::iabs(v), 8)]; } else { // absolute: raw indices at their stored size (2 bits, // or 4 for the nibble-expanded 3-bit class, or 4) cost += 8u * ((num_bits == 2) ? 2u : 4u); } } } // p if (!valid) continue; // later candidates must beat the incumbent by the switch margin // (UINT64_MAX guard: the multiply would wrap before any winner) if ((best_cost == UINT64_MAX) || ((cost * 1000) < (best_cost * (1000 - (uint64_t)XBC7_SWEEP_SWITCH_MARGIN_PERMILLE)))) { best_cost = cost; result.m_pred_index = cand_index + amp_code * cTotalCandidates; memcpy(result.m_syms, syms, sizeof(syms)); } } // cand_index } // amp_code result.m_est_cost_obits = best_cost; } // per-stripe (per-job) output state; merged in stripe order after the jobs join struct stripe_output { blob_stream_writer m_blob_writer; basisu::bitwise_coder m_coeff_signs; basisu::bitwise_coder m_pbits; basisu::bitwise_coder m_raw_endpoint_bits; xbc7_pack_stats m_stats; }; // Optional block-reuse RDO pre-pass: runs after the BC7 base pack, BEFORE the // endpoint RDO (once a whole block is reused there's no point trying cheaper // endpoints). In raster order per stripe (honoring boundaries), for each // non-solid block it tries the cheapest whole-block representations and adopts // the best acceptable one: a REPEAT (exact copy of the left/upper causal // neighbor -> 1-byte command; preferred) or SOLID (the block's mean color -> // cheap SolidDPCM). "Acceptable" = weighted RGBA PSNR >= the shared floor AND // within the per-mode tolerated drop vs the block's current PSNR. Modifies // log_blks in place; serial (causal). Returns false on a decode failure. static bool block_reuse_rdo_pass( const image& orig_img, basisu::vector2D& log_blks, uint32_t num_blocks_x, const basisu::vector& stripes, const pack_options& opts) { const uint32_t* comp_weights = opts.m_weights; const float min_block_psnr = opts.m_rdo_min_block_psnr; const bool has_alpha = orig_img.has_alpha(); uint32_t changed_repeat = 0, changed_solid = 0; for (uint32_t st = 0; st < stripes.size(); st++) { const uint32_t first_row = stripes[st].m_first_block_row; const uint32_t end_row = first_row + stripes[st].m_num_block_rows; for (uint32_t by = first_row; by < end_row; by++) { for (uint32_t bx = 0; bx < num_blocks_x; bx++) { basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by); // NOTE: solid blocks are NOT skipped -- a RepeatLast/RepeatUpper // command (1 byte) is cheaper than SolidDPCM, and pack_stripe checks // Repeat before Solid, so we aggressively reuse a neighbor even for a // solid block when it passes the PSNR test (VQ-style). color_rgba src_pixels[16]; orig_img.extract_block_clamped(src_pixels, bx * 4, by * 4, 4, 4); color_rgba dec[16]; if (!basist::bc7u::unpack_bc7(log_blk, (basist::color_rgba*)dec)) { assert(0); return false; } const float orig_psnr = xbc7_block_wsse_psnr(src_pixels, dec, comp_weights); // ---- Repeat: copy a causal neighbor wholesale (cheapest command) ---- if (opts.m_repeat_rdo_enabled) { const basist::bc7u::log_bc7_block* preds[2] = { nullptr, nullptr }; if (bx >= 1) preds[0] = &log_blks(bx - 1, by); // left -> RepeatLast if (by > first_row) preds[1] = &log_blks(bx, by - 1); // upper -> RepeatUpper const basist::bc7u::log_bc7_block* best = nullptr; float best_psnr = 0.0f; for (uint32_t i = 0; i < 2; i++) { if (!preds[i]) continue; if (!basist::bc7u::unpack_bc7(*preds[i], (basist::color_rgba*)dec)) { assert(0); return false; } const float p = xbc7_block_wsse_psnr(src_pixels, dec, comp_weights); if ((p >= min_block_psnr) && (p >= orig_psnr - opts.m_repeat_rdo_max_psnr_drop) && ((!best) || (p > best_psnr))) { best = preds[i]; best_psnr = p; } } if (best) { log_blk = *best; // bit-identical -> pack_stripe emits Repeat changed_repeat++; continue; // Repeat is the cheapest -- done with this block } } // ---- Solid: replace with the block's mean color (next cheapest) ---- // (skip if already solid -- it already codes cheaply as SolidDPCM) if (opts.m_solid_rdo_enabled && !basist::bc7u::is_solid_blk(log_blk)) { uint32_t sum[4] = { 0, 0, 0, 0 }; for (uint32_t i = 0; i < 16; i++) for (uint32_t c = 0; c < 4; c++) sum[c] += src_pixels[i][c]; color_rgba mean((sum[0] + 8) >> 4, (sum[1] + 8) >> 4, (sum[2] + 8) >> 4, (sum[3] + 8) >> 4); if (!has_alpha) mean.a = 255; basist::bc7u::log_bc7_block cand; cand.clear(); basist::bc7u::create_solid_blk(cand, (const basist::color_rgba&)mean); if (!basist::bc7u::unpack_bc7(cand, (basist::color_rgba*)dec)) { assert(0); return false; } const float p = xbc7_block_wsse_psnr(src_pixels, dec, comp_weights); if ((p >= min_block_psnr) && (p >= orig_psnr - opts.m_solid_rdo_max_psnr_drop)) { log_blk = cand; changed_solid++; } } } } } if (opts.m_debug_output) { uint32_t total_blocks = 0; for (uint32_t st = 0; st < stripes.size(); st++) total_blocks += num_blocks_x * stripes[st].m_num_block_rows; if (opts.m_repeat_rdo_enabled) fmt_debug_printf("repeat RDO: {} blocks changed ({.2}%)\n", changed_repeat, total_blocks ? (changed_repeat * 100.0f / (float)total_blocks) : 0.0f); if (opts.m_solid_rdo_enabled) fmt_debug_printf("solid RDO: {} blocks changed ({.2}%)\n", changed_solid, total_blocks ? (changed_solid * 100.0f / (float)total_blocks) : 0.0f); } return true; } // Optional endpoint-prediction RDO pre-pass: runs after the BC7 base pack but // BEFORE stripe coding. In raster order (per stripe, honoring its boundaries) // it tries forcing each block's endpoints to a causal neighbor's prediction // (left/upper/left-diag/right-diag) via a zero-residual endpoint DPCM, and // re-optimizes that candidate's weights for the new endpoints, and adopts the // best candidate when its weighted RGBA PSNR drops by no more than // max_psnr_drop dB. The matching zero residuals then cost almost nothing in // the coding pass (often collapsing to a Repeat). Solid blocks are skipped -- // they have their own cheaper SolidDPCM path. Modifies log_blks in place; // serial, since each block predicts from already-finalized causal neighbors. // Returns false on an (unexpected) decode failure. static bool endpoint_dpcm_rdo_pass( const image& orig_img, basisu::vector2D& log_blks, uint32_t num_blocks_x, const basisu::vector& stripes, const pack_options& opts) { const uint32_t* comp_weights = opts.m_weights; const float max_psnr_drop = opts.m_endpoint_rdo_max_psnr_drop; const float min_block_psnr = opts.m_rdo_min_block_psnr; uint32_t changed_blocks = 0; // blocks whose endpoints+weights were rewritten #if defined(DEBUG) || defined(_DEBUG) // sanity: re-optimizing a candidate's weights can never RAISE its WSSE // (the original weights are in the swept set); count any case where it does uint64_t dbg_weight_opt_cands = 0, dbg_weight_opt_worse = 0; #endif for (uint32_t st = 0; st < stripes.size(); st++) { const uint32_t first_row = stripes[st].m_first_block_row; const uint32_t end_row = first_row + stripes[st].m_num_block_rows; for (uint32_t by = first_row; by < end_row; by++) { for (uint32_t bx = 0; bx < num_blocks_x; bx++) { basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by); // solid blocks have a dedicated, cheaper command -- leave them alone if (basist::bc7u::is_solid_blk(log_blk)) continue; // already identical to a causal neighbor (e.g. from the block-reuse // RDO, or naturally) -- it'll code as a cheap Repeat, so don't // disturb its endpoints if (((bx >= 1) && basist::bc7u::compare_block_full(log_blk, log_blks(bx - 1, by))) || ((by > first_row) && basist::bc7u::compare_block_full(log_blk, log_blks(bx, by - 1)))) continue; color_rgba src_pixels[16]; orig_img.extract_block_clamped(src_pixels, bx * 4, by * 4, 4, 4); color_rgba orig_pixels[16]; if (!basist::bc7u::unpack_bc7(log_blk, (basist::color_rgba*)orig_pixels)) { assert(0); return false; } const float orig_psnr = xbc7_block_wsse_psnr(src_pixels, orig_pixels, comp_weights); // causal neighbor predictors valid within this stripe, in order: // left, upper, left-diag, right-diag (upper rows must stay in-stripe) const basist::bc7u::log_bc7_block* preds[4] = { nullptr, nullptr, nullptr, nullptr }; if (bx >= 1) preds[0] = &log_blks(bx - 1, by); if (by > first_row) preds[1] = &log_blks(bx, by - 1); if ((bx >= 1) && (by > first_row)) preds[2] = &log_blks(bx - 1, by - 1); if ((bx + 1 < num_blocks_x) && (by > first_row)) preds[3] = &log_blks(bx + 1, by - 1); basist::bc7u::log_bc7_block best_cand{}; // value-init silences a C4701 false positive (only read when have_cand) float best_cand_psnr = 0.0f; bool have_cand = false; for (uint32_t i = 0; i < 4; i++) { if (!preds[i]) continue; // copy keeps mode/config/weights; only endpoints+pbits change basist::bc7u::log_bc7_block cand(log_blk); for (uint32_t s = 0; s < cand.m_num_partitions; s++) { uint8_t residuals[8] = { 0 }; // zero residuals -> endpoints become the prediction uint8_t pbits[2] = { 0 }; uint32_t num_residuals = 0, num_pbits = 0; basist::bc7u::endpoint_dpcm(true, *preds[i], 0, cand, s, residuals, num_residuals, pbits, num_pbits); } #if defined(DEBUG) || defined(_DEBUG) // WSSE with the now-stale weights, before re-optimization uint64_t dbg_wsse_before = 0; { color_rgba pre[16]; if (!basist::bc7u::unpack_bc7(cand, (basist::color_rgba*)pre)) { assert(0); return false; } for (uint32_t k = 0; k < 16; k++) dbg_wsse_before += pre[k].get_weighted_dist2(src_pixels[k], comp_weights); } #endif // the slammed endpoints leave the copied weights stale -- recompute // the optimal weights for the new endpoints (can only help vs. stale) basist::bc7u::log_bc7_block cand_opt; if (!optimize_block_weights(cand, src_pixels, comp_weights, cand_opt)) return false; // already asserted inside cand = cand_opt; color_rgba cand_pixels[16]; if (!basist::bc7u::unpack_bc7(cand, (basist::color_rgba*)cand_pixels)) { assert(0); return false; } #if defined(DEBUG) || defined(_DEBUG) { uint64_t dbg_wsse_after = 0; for (uint32_t k = 0; k < 16; k++) dbg_wsse_after += cand_pixels[k].get_weighted_dist2(src_pixels[k], comp_weights); dbg_weight_opt_cands++; if (dbg_wsse_after > dbg_wsse_before) dbg_weight_opt_worse++; // weight re-optimization must never make WSSE worse } #endif const float cand_psnr = xbc7_block_wsse_psnr(src_pixels, cand_pixels, comp_weights); if ((!have_cand) || (cand_psnr > best_cand_psnr)) { have_cand = true; best_cand_psnr = cand_psnr; best_cand = cand; } } // adopt the best candidate if quality drops by no more than allowed // (a gain is of course also accepted) if (have_cand && (best_cand_psnr >= min_block_psnr) && (best_cand_psnr >= orig_psnr - max_psnr_drop)) { log_blk = best_cand; changed_blocks++; } } } } if (opts.m_debug_output) { uint32_t total_blocks = 0; for (uint32_t st = 0; st < stripes.size(); st++) total_blocks += num_blocks_x * stripes[st].m_num_block_rows; fmt_debug_printf("endpoint RDO: {} blocks changed ({.2}%)\n", changed_blocks, total_blocks ? (changed_blocks * 100.0f / (float)total_blocks) : 0.0f); } #if defined(DEBUG) || defined(_DEBUG) fmt_debug_printf("endpoint RDO weight re-opt: {} candidates, {} got WORSE (expected 0)\n", dbg_weight_opt_cands, dbg_weight_opt_worse); // assert(!dbg_weight_opt_worse); // intentionally NOT asserted -- probably too strong; observe via the stat above instead #endif return true; } // Optional weight-DCT AC-truncation RDO. Run ONCE on the winning DCT symbols // (after pack_weights, NOT inside its candidate sweep). Zeros the highest- // frequency weight-DCT AC coefficients one at a time in reverse zigzag order, // protecting the 2x2 low-freq corner (DC,(1,0),(0,1),(1,1) -> zigzag {0,1,2,4}), // while the decoded weighted RGBA PSNR stays within m_ac_trunc_rdo_max_psnr_drop // dB of the un-truncated block AND >= the shared floor. Updates best_syms // (pruned) and best_cand_log_blk (weights rebuilt via the same inverse() the // decoder uses, so it round-trips). Returns false on an unexpected decode failure. static bool ac_truncate_rdo( dct_syms best_syms[2], basist::bc7u::log_bc7_block& best_cand_log_blk, uint32_t best_predictor_index, const color_rgba orig_block[16], uint32_t bx, uint32_t by, uint32_t num_blocks_x, const tile_bounds& tile, const vector2D& log_blks, xbc7_weight_grid_dct_fixed& weight_grid_dct_fixed, fxvec& dct_work, const pack_options& opts, xbc7_pack_stats& stats) { const uint32_t num_planes = best_cand_log_blk.m_num_planes; // winning predictor -> per-plane predictions (pack_weights left untouched; // eval_weight_predictor asserts non-absolute, so absolute -> null preds) const uint32_t cand_index = best_predictor_index % cTotalCandidates; const uint32_t amp_code = best_predictor_index / cTotalCandidates; int preds[2][16]; const int* pPreds[2] = { nullptr, nullptr }; if (cand_index != cCandAbsolute) { for (uint32_t p = 0; p < num_planes; p++) { if (!eval_weight_predictor(cand_index, amp_code, bx, by, num_blocks_x, tile, log_blks, p, preds[p])) { assert(0); return false; } pPreds[p] = preds[p]; } } // baseline PSNR of the un-truncated DCT-coded block color_rgba dec[16]; if (!basist::bc7u::unpack_bc7(best_cand_log_blk, (basist::color_rgba*)dec)) { assert(0); return false; } const float orig_psnr = xbc7_block_wsse_psnr(orig_block, dec, opts.m_weights); int flat[2][16]; for (uint32_t p = 0; p < num_planes; p++) xbc7_syms_to_flat(best_syms[p], flat[p]); const float drop = opts.m_ac_trunc_rdo_max_psnr_drop; const float floor_psnr = opts.m_rdo_min_block_psnr; uint64_t pruned_here = 0; // reverse zigzag, skipping protected low-freq slots (zigzag 1,2,4; 0 = DC) for (uint32_t zz = 15; zz >= 1; zz--) { if ((zz == 1) || (zz == 2) || (zz == 4)) continue; const uint32_t nat = g_zigzag4x4_xy[zz][0] + g_zigzag4x4_xy[zz][1] * 4; // skip if this coefficient is already zero in every plane bool any_nonzero = false; for (uint32_t p = 0; p < num_planes; p++) if (flat[p][nat] != 0) any_nonzero = true; if (!any_nonzero) continue; int saved[2] = { 0, 0 }; for (uint32_t p = 0; p < num_planes; p++) { saved[p] = flat[p][nat]; flat[p][nat] = 0; } // rebuild the candidate from the tentatively-truncated grids basist::bc7u::log_bc7_block trial_blk = best_cand_log_blk; // config + endpoints kept dct_syms trial_syms[2]; for (uint32_t p = 0; p < num_planes; p++) { xbc7_flat_to_syms(flat[p], trial_syms[p]); memset(trial_blk.m_weights[p], 0, 16); if (!weight_grid_dct_fixed.inverse(basist::fixed16_16::from_int(opts.m_dct_q), p, pPreds[p], trial_syms[p], trial_blk, dct_work)) { assert(0); return false; } } if (!basist::bc7u::unpack_bc7(trial_blk, (basist::color_rgba*)dec)) { assert(0); return false; } const float psnr = xbc7_block_wsse_psnr(orig_block, dec, opts.m_weights); if ((psnr >= floor_psnr) && (psnr >= orig_psnr - drop)) { best_cand_log_blk = trial_blk; for (uint32_t p = 0; p < num_planes; p++) { best_syms[p] = trial_syms[p]; if (saved[p]) pruned_here++; } } else { for (uint32_t p = 0; p < num_planes; p++) flat[p][nat] = saved[p]; // revert this coefficient break; // stop at the first unacceptable truncation } } stats.m_ac_trunc_pruned += pruned_here; if (pruned_here) stats.m_ac_trunc_blocks++; return true; } // with every causal reference clamped to the stripe (the decoder mirrors // the one IMPLICIT cross-row prediction -- solid blocks -- via the header // stripe count; explicit references are simply never emitted across a // stripe boundary). All scratch state is local or lives in `out`. static bool pack_stripe( const image& orig_img, const pack_options& opts, bool has_alpha, uint32_t num_blocks_x, const stripe_range& stripe, vector2D& log_blks, vector2D* pCoded_log_blocks, const vector2D& base_used_lut, // per-block: 1 if the bc7e_scalar base pack used its endpoint LUTs (force DPCM) stripe_output& out, const xbc7_debug_image_set& dbg_imgs) // debug only; members null unless m_debug_images. Stripes write disjoint rows, so filling is thread-safe. { const uint32_t first_row = stripe.m_first_block_row; const uint32_t end_row = stripe.m_first_block_row + stripe.m_num_block_rows; // this stripe as an inclusive block AABB (full width, for now); ALL // causal references in this pass are gated through tile.contains() const tile_bounds tile = { 0, (int)first_row, (int)num_blocks_x - 1, (int)end_row - 1 }; blob_stream_writer& blob_writer = out.m_blob_writer; basisu::bitwise_coder& coeff_signs = out.m_coeff_signs; basisu::bitwise_coder& pbits = out.m_pbits; basisu::bitwise_coder& raw_endpoint_bits = out.m_raw_endpoint_bits; xbc7_pack_stats& stats = out.m_stats; xbc7_weight_grid_dct_fixed weight_grid_dct_fixed; weight_grid_dct_fixed.init(); fxvec dct_work_fixed; // hoisted so the per-block visualization calls compile out entirely when disabled const bool debug_images = opts.m_debug_images; // effort -> enabled weight-predictor set + amp-code count (constant for the // whole stripe; at effort 10 both are full so the sweeps are unchanged) const uint64_t enabled_pred_mask = xbc7_build_pred_mask(opts.m_effort_level); const uint32_t num_amp_codes = xbc7_amp_codes_for_effort(opts.m_effort_level); for (uint32_t by = first_row; by < end_row; by++) { for (uint32_t bx = 0; bx < num_blocks_x; bx++) { // causal neighbors, clamped to the tile AABB: blocks outside it // belong to another coding unit and are off limits const basist::bc7u::log_bc7_block* pLeft_log_blk = tile.contains((int)bx - 1, (int)by) ? &log_blks(bx - 1, by) : nullptr; const basist::bc7u::log_bc7_block* pUp_log_blk = tile.contains((int)bx, (int)by - 1) ? &log_blks(bx, by - 1) : nullptr; const basist::bc7u::log_bc7_block* pLeft_diag_log_blk = tile.contains((int)bx - 1, (int)by - 1) ? &log_blks(bx - 1, by - 1) : nullptr; const basist::bc7u::log_bc7_block* pRight_diag_log_blk = tile.contains((int)bx + 1, (int)by - 1) ? &log_blks(bx + 1, by - 1) : nullptr; basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by); if (pUp_log_blk && basist::bc7u::compare_block_full(log_blk, *pUp_log_blk)) { blob_writer.put_byte(cBlobCommands, (uint8_t)xbc7_command_id::cCmdRepeatUpper); stats.m_cmd_hist[(uint32_t)xbc7_command_id::cCmdRepeatUpper]++; if (debug_images) xbc7_vis_fill_command(dbg_imgs, bx, by, (uint8_t)xbc7_command_id::cCmdRepeatUpper, false, UINT32_MAX, UINT32_MAX); if (pCoded_log_blocks) { (*pCoded_log_blocks)(bx, by) = log_blk; } continue; } if (pLeft_log_blk && basist::bc7u::compare_block_full(log_blk, *pLeft_log_blk)) { blob_writer.put_byte(cBlobCommands, (uint8_t)xbc7_command_id::cCmdRepeatLast); stats.m_cmd_hist[(uint32_t)xbc7_command_id::cCmdRepeatLast]++; if (debug_images) xbc7_vis_fill_command(dbg_imgs, bx, by, (uint8_t)xbc7_command_id::cCmdRepeatLast, false, UINT32_MAX, UINT32_MAX); if (pCoded_log_blocks) { (*pCoded_log_blocks)(bx, by) = log_blk; } continue; } if (basist::bc7u::is_solid_blk(log_blk)) { int preds[4] = {}; int num_preds = 0; if (pLeft_log_blk) { for (uint32_t y = 0; y < 4; y++) { basist::color_rgba p; if (!basist::bc7u::unpack_bc7_texel(*pLeft_log_blk, p, 3, y)) { assert(0); return false; } preds[0] += p.r; preds[1] += p.g; preds[2] += p.b; preds[3] += p.a; } num_preds += 4; } if (pUp_log_blk) { for (uint32_t x = 0; x < 4; x++) { basist::color_rgba p; if (!basist::bc7u::unpack_bc7_texel(*pUp_log_blk, p, x, 3)) { assert(0); return false; } preds[0] += p.r; preds[1] += p.g; preds[2] += p.b; preds[3] += p.a; } num_preds += 4; } if (num_preds) { for (uint32_t c = 0; c < 4; c++) preds[c] = (preds[c] + (num_preds / 2)) / num_preds; } blob_writer.put_byte(cBlobCommands, (uint8_t)xbc7_command_id::cCmdSolidDPCM); stats.m_cmd_hist[(uint32_t)xbc7_command_id::cCmdSolidDPCM]++; if (debug_images) xbc7_vis_fill_command(dbg_imgs, bx, by, (uint8_t)xbc7_command_id::cCmdSolidDPCM, false, UINT32_MAX, UINT32_MAX); basist::color_rgba solid_color; if (!basist::bc7u::unpack_bc7_texel(log_blk, solid_color, 0, 0)) { assert(0); return false; } if (!has_alpha) solid_color.a = 255; // important in case a p-bit set a=254 for (uint32_t c = 0; c < (has_alpha ? 4u : 3u); c++) { const uint8_t delta = (uint8_t)(solid_color[c] - preds[c]); blob_writer.put_byte(cBlobSolidRGBADeltas, delta); stats.m_solid_delta_count[c]++; stats.m_solid_delta_sum_abs[c] += (uint64_t)basisu::iabs((int8_t)delta); if (!delta) stats.m_solid_delta_zero[c]++; } basist::bc7u::create_solid_blk(log_blk, solid_color); if (pCoded_log_blocks) { (*pCoded_log_blocks)(bx, by) = log_blk; } continue; } color_rgba orig_block[16]; orig_img.extract_block_clamped(orig_block, bx * 4, by * 4, 4, 4); uint8_t command_byte = 0; // Code config if ((pLeft_log_blk) && (basist::bc7u::compare_block_configs(log_blk, *pLeft_log_blk, false))) { command_byte = (uint8_t)xbc7_command_id::cCmdReuseConfigLeft; } else if ((pUp_log_blk) && (basist::bc7u::compare_block_configs(log_blk, *pUp_log_blk, false))) { command_byte = (uint8_t)xbc7_command_id::cCmdReuseConfigUpper; } else if ((pLeft_diag_log_blk) && (basist::bc7u::compare_block_configs(log_blk, *pLeft_diag_log_blk, false))) { command_byte = (uint8_t)xbc7_command_id::cCmdReuseConfigLeftDiagonal; } else if ((pRight_diag_log_blk) && (basist::bc7u::compare_block_configs(log_blk, *pRight_diag_log_blk, false))) { command_byte = (uint8_t)xbc7_command_id::cCmdReuseConfigRightDiagonal; } else { command_byte = (uint8_t)xbc7_command_id::cCmdNewConfig; uint32_t config_byte = log_blk.m_mode | (log_blk.m_dp_rotation_index << 3) | (log_blk.m_mode4_index_selector << 5); blob_writer.put_byte(cBlobBC7BlockConfig, (uint8_t)config_byte); } stats.m_cmd_hist[command_byte & 7]++; stats.m_mode_hist[log_blk.m_mode]++; // Code partition pattern index if (log_blk.m_num_partitions == 2) blob_writer.put_byte(cBlobPartition2, log_blk.m_pattern_index); else if (log_blk.m_num_partitions == 3) blob_writer.put_byte(cBlobPartition3, log_blk.m_pattern_index); // Endpoint coding: sweep every causal DPCM predictor -- the four // dedicated neighbor modes, then the 32 XY-delta block references -- // and keep the one minimizing the SSE of the residual bytes that // will actually be emitted (int8 reading, post G-decorrelation, // alpha bytes that the opaque mode 6 rule suppresses excluded). // Dedicated modes are evaluated first and replacement is strict // less-than, so an XY delta aliasing a dedicated neighbor can never // win a tie and waste an index byte. const basist::bc7u::log_bc7_block* pEP_pred_blk = nullptr; uint32_t ep_pred_mode = (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointRaw; uint32_t ep_pred_delta_index = 0; uint32_t ep_pred_subset = 0; { uint64_t best_ep_cost = UINT64_MAX; auto eval_ep_predictor = [&](const basist::bc7u::log_bc7_block& pred_blk, uint32_t pred_subset) -> uint64_t { uint64_t cost = 0; for (uint32_t s = 0; s < log_blk.m_num_partitions; s++) { uint8_t residuals[8]; uint32_t num_residuals; uint8_t residual_pbits[2]; uint32_t num_residual_pbits; basist::bc7u::endpoint_dpcm(false, pred_blk, pred_subset, // mirrors the emission below log_blk, s, residuals, num_residuals, residual_pbits, num_residual_pbits); uint32_t num_costed = num_residuals; if ((log_blk.m_mode == 6) && (!has_alpha)) num_costed = 6; // alpha residuals are never sent for (uint32_t i = 0; i < num_costed; i++) { const int v = (int8_t)residuals[i]; cost += (uint64_t)((int64_t)v * v); } // NOTE: residual_pbits are deliberately NOT costed. // Penalizing pbit mismatches was tried and measured // a wash at best (penalty 1: ~0%, penalty 2: +0.4% // WORSE): the delta stream is raw 1 bit/pbit either // way, so the only possible win is making its packed // bytes Zstd-compressible -- but at 8 deltas/byte // even a 30% nonzero rate leaves ~7 bits/byte of // entropy, and the residual bytes traded away to // get there cost more. } return cost; }; const basist::bc7u::log_bc7_block* neighbor_preds[4] = { pLeft_log_blk, pUp_log_blk, pLeft_diag_log_blk, pRight_diag_log_blk }; const xbc7_command_endpoint_mode neighbor_modes[4] = { xbc7_command_endpoint_mode::cCmdEndpointDPCMLeft, xbc7_command_endpoint_mode::cCmdEndpointDPCMUp, xbc7_command_endpoint_mode::cCmdEndpointDPCMLeftDiagonal, xbc7_command_endpoint_mode::cCmdEndpointDPCMRightDiagonal }; for (uint32_t i = 0; i < 4; i++) { if (!neighbor_preds[i]) continue; const uint64_t cost = eval_ep_predictor(*neighbor_preds[i], 0); if ((!pEP_pred_blk) || ((cost * 1000) < (best_ep_cost * (1000 - (uint64_t)XBC7_SWEEP_SWITCH_MARGIN_PERMILLE)))) { best_ep_cost = cost; pEP_pred_blk = neighbor_preds[i]; ep_pred_mode = (uint32_t)neighbor_modes[i]; ep_pred_subset = 0; } } // Subset-1 variants of left/up (EP modes 6/7), legal only when // the neighbor is partitioned; swept AFTER the subset-0 modes // so the switch margin keeps them losing ties. NOTE: a much // stricter dedicated margin was tried and measured WORSE -- // these modes' SSE wins are nearly always decisive, so the // filter barely fires; the (small) net cost on rare content // with hyper-skewed command streams is an SSE-vs-bytes // mismatch that only rate-based EP selection would fix. const basist::bc7u::log_bc7_block* s1_preds[2] = { pLeft_log_blk, pUp_log_blk }; const xbc7_command_endpoint_mode s1_modes[2] = { xbc7_command_endpoint_mode::cCmdEndpointDPCMLeftSubset1, xbc7_command_endpoint_mode::cCmdEndpointDPCMUpSubset1 }; for (uint32_t i = 0; i < 2; i++) { if ((!s1_preds[i]) || (s1_preds[i]->m_num_partitions < 2)) continue; const uint64_t cost = eval_ep_predictor(*s1_preds[i], 1); if ((!pEP_pred_blk) || ((cost * 1000) < (best_ep_cost * (1000 - (uint64_t)XBC7_SWEEP_SWITCH_MARGIN_PERMILLE)))) { best_ep_cost = cost; pEP_pred_blk = s1_preds[i]; ep_pred_mode = (uint32_t)s1_modes[i]; ep_pred_subset = 1; } } for (uint32_t i = 0; i < NUM_XY_DELTAS; i++) { const xbc7_xy_delta& delta = g_xbc7_xy_deltas[i]; const int nx = (int)bx + delta.m_dx; const int ny = (int)by + delta.m_dy; if (!tile.contains(nx, ny)) continue; const basist::bc7u::log_bc7_block& pred_blk = log_blks(nx, ny); const uint64_t cost = eval_ep_predictor(pred_blk, 0); if ((!pEP_pred_blk) || ((cost * 1000) < (best_ep_cost * (1000 - (uint64_t)XBC7_SWEEP_SWITCH_MARGIN_PERMILLE)))) { best_ep_cost = cost; pEP_pred_blk = &pred_blk; ep_pred_mode = (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMBlockIndex; ep_pred_delta_index = i; ep_pred_subset = 0; } } } if (pEP_pred_blk) { command_byte |= (uint8_t)(ep_pred_mode << XBC7_COMMAND_ENDPOINT_MODE_SHIFT); stats.m_ep_dpcm_blocks++; stats.m_ep_mode_hist[ep_pred_mode]++; if (ep_pred_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMBlockIndex) { blob_writer.put_byte(cBlobEPBlockIndex, (uint8_t)ep_pred_delta_index); stats.m_ep_index_hist[ep_pred_delta_index]++; } for (uint32_t s = 0; s < log_blk.m_num_partitions; s++) { uint8_t residuals[8]; uint32_t num_residuals; uint8_t residual_pbits[2]; uint32_t num_residual_pbits; basist::bc7u::endpoint_dpcm(false, *pEP_pred_blk, ep_pred_subset, // subset 1 for EP modes 6/7 log_blk, s, residuals, num_residuals, residual_pbits, num_residual_pbits); { basist::bc7u::log_bc7_block temp_log_blk(log_blk); memset(temp_log_blk.m_endpoints, 0, sizeof(temp_log_blk.m_endpoints)); basist::bc7u::endpoint_dpcm(true, *pEP_pred_blk, ep_pred_subset, // subset 1 for EP modes 6/7 temp_log_blk, s, residuals, num_residuals, residual_pbits, num_residual_pbits); #if defined(DEBUG) || defined(_DEBUG) for (uint32_t e = 0; e < 2; e++) { for (uint32_t c = 0; c < 4; c++) { if (log_blk.m_endpoints[s][e][c] != temp_log_blk.m_endpoints[s][e][c]) { fmt_error_printf("DPCM endpoint decomp failed"); assert(0); return false; } } } #endif } assert((num_residuals % 2) == 0); if ((log_blk.m_mode == 6) && (!has_alpha)) { assert(num_residuals == 8); if ((log_blk.m_endpoints[0][0][3] != 127) || (log_blk.m_endpoints[0][1][3] != 127)) { assert(0); return false; } #if 0 if ((residuals[6] != 0) || (residuals[7] != 0)) { assert(0); return false; } #endif num_residuals = 6; } stats.m_ep_dpcm_subsets++; stats.record_ep_residuals(log_blk.m_endpoint_bits[0] >= 6, residuals, num_residuals); if (log_blk.m_endpoint_bits[0] >= 6) { for (uint32_t i = 0; i < num_residuals; i += 2) { const uint32_t chan = i >> 1; blob_writer.put_byte(cBlobEPDeltaFineR + chan, residuals[i + 0]); blob_writer.put_byte(cBlobEPDeltaFineR + chan, residuals[i + 1]); } } else { for (uint32_t i = 0; i < num_residuals; i += 2) { const uint32_t chan = i >> 1; blob_writer.put_byte(cBlobEPDeltaCoarseR + chan, residuals[i + 0]); blob_writer.put_byte(cBlobEPDeltaCoarseR + chan, residuals[i + 1]); } } for (uint32_t p = 0; p < num_residual_pbits; p++) { pbits.put_bits(residual_pbits[p], 1); stats.m_pbit_delta_bits++; stats.m_pbit_delta_nonzero += residual_pbits[p]; } } } else { command_byte |= ((uint8_t)xbc7::xbc7_command_endpoint_mode::cCmdEndpointRaw << XBC7_COMMAND_ENDPOINT_MODE_SHIFT); stats.m_ep_raw_blocks++; stats.m_ep_mode_hist[(uint32_t)xbc7_command_endpoint_mode::cCmdEndpointRaw]++; for (uint32_t s = 0; s < log_blk.m_num_partitions; s++) { for (uint32_t c = 0; c < log_blk.get_num_comps(); c++) { for (uint32_t e = 0; e < 2; e++) { raw_endpoint_bits.put_bits(log_blk.m_endpoints[s][e][c], log_blk.m_endpoint_bits[c == 3]); } } } for (uint32_t p = 0; p < log_blk.m_num_pbits; p++) raw_endpoint_bits.put_bits(log_blk.m_pbits[p], 1); } // Weights: residual DCT vs lossless residual DPCM, chosen per // block by estimated emitted size (see decision below). Q >= 100 // is the lossless mode: weights are ALWAYS residual DPCM -- the // whole file is then exact relative to the input BC7 logical // blocks -- and the expensive DCT candidate search is skipped. // // We ALSO force lossless DPCM (and skip the DCT search) for any block // the bc7e_scalar base pack flagged as using its one-color endpoint // LUTs: those extreme endpoints only "land" with exact weights, so the // lossy weight DCT would decode them to "trap" artifacts. (LUTs are // already disabled at Q < 100; this also covers the always-LUT solid // path. At Q == 100 DPCM is forced anyway, so LUTs are harmless there.) const bool force_dpcm = (opts.m_dct_q >= 100) || (base_used_lut(bx, by) != 0); dct_syms best_syms[2]; // value-init silences a C4701 false positive: best_cand_log_blk is only // read on the DCT path (!force_dpcm), where pack_weights() assigns it // first; the DPCM path 'continue's before the read. basist::bc7u::log_bc7_block best_cand_log_blk{}; uint32_t best_predictor_index = 0; uint64_t dct_est_obits = 0; uint32_t block_total_acs = 0; if (!force_dpcm) { bool pack_status = pack_weights(bx, by, num_blocks_x, tile, orig_block, log_blks, best_syms, best_cand_log_blk, best_predictor_index, weight_grid_dct_fixed, dct_work_fixed, opts, enabled_pred_mask, num_amp_codes); if (!pack_status) return false; // Estimated DCT emission cost, mirroring the writes below: // each stream byte at its measured post-Zstd cost, plus the // exact (incompressible) sign bits. for (uint32_t p = 0; p < log_blk.m_num_planes; p++) { uint32_t num_acs = 0, num_eobs = 0; for (uint32_t c = 0; c < best_syms[p].m_ac_vals.size(); c++) { if (best_syms[p].m_ac_vals[c].m_coeff == INT16_MAX) num_eobs++; else num_acs++; } dct_est_obits += DCT_DC_BYTE_COST_OBITS + (2 * (uint64_t)DCT_AC_BYTE_COST_OBITS) * num_acs + (uint64_t)DCT_AC_BYTE_COST_OBITS * num_eobs + 8 * ((uint64_t)num_acs + ((best_predictor_index != cCandAbsolute) ? 1 : 0)); block_total_acs += num_acs; } stats.m_wt_dct_est_bits_total += dct_est_obits; } dpcm_weights dpcm; pack_weights_dpcm(bx, by, num_blocks_x, tile, log_blks, dpcm, enabled_pred_mask, num_amp_codes); stats.m_wt_dpcm_est_bits_total += dpcm.m_est_cost_obits; // Both estimates are in measured post-Zstd octobits, so 100 is // the neutral operating point. Ties go to DPCM: it's lossless, // so a tie is a free quality win. bool use_dpcm = force_dpcm || ((dpcm.m_est_cost_obits * 100) <= (dct_est_obits * opts.m_wt_dpcm_alpha_pct)); if (use_dpcm) { // the weight-mode command bit is cCmdWeightRaw == 0: nothing to OR in stats.m_wt_dpcm_blocks++; stats.m_wt_block_acs_hist_dpcm[basisu::minimum(block_total_acs, 32)]++; stats.m_wt_chosen_est_bits += dpcm.m_est_cost_obits; stats.m_wt_dpcm_pred_hist[dpcm.m_pred_index]++; blob_writer.put_byte(cBlobWeightPredictors, (uint8_t)dpcm.m_pred_index); const bool is_absolute = (dpcm.m_pred_index == (uint32_t)cCandAbsolute); for (uint32_t p = 0; p < log_blk.m_num_planes; p++) { const uint32_t num_bits = log_blk.m_weight_bits[p]; const uint32_t blob_id = is_absolute ? (uint32_t)cBlobRawWeightBits : ((num_bits == 2) ? (uint32_t)cBlobDPCMWeightResid2 : (num_bits == 3) ? (uint32_t)cBlobDPCMWeightResid3 : (uint32_t)cBlobDPCMWeightResid4); if (!is_absolute) { const uint32_t cls = num_bits - 2; const int half = 1 << (num_bits - 1); for (uint32_t i = 0; i < 16; i++) { const int sym = dpcm.m_syms[p][i]; const int v = (sym >= half) ? (sym - (1 << num_bits)) : sym; stats.m_wt_resid_count[cls]++; stats.m_wt_resid_sum_abs[cls] += (uint64_t)iabs(v); if (!v) stats.m_wt_resid_zero[cls]++; stats.m_wt_resid_mag_hist[cls][basisu::minimum(iabs(v), 8)]++; } } if (num_bits == 2) { // 4 per byte, LSB first for (uint32_t i = 0; i < 16; i += 4) { blob_writer.put_byte(blob_id, (uint8_t)(dpcm.m_syms[p][i] | (dpcm.m_syms[p][i + 1] << 2) | (dpcm.m_syms[p][i + 2] << 4) | (dpcm.m_syms[p][i + 3] << 6))); } } else { // 3-bit expanded to nibbles / 4-bit: 2 per byte, LSB first for (uint32_t i = 0; i < 16; i += 2) { blob_writer.put_byte(blob_id, (uint8_t)(dpcm.m_syms[p][i] | (dpcm.m_syms[p][i + 1] << 4))); } } } // p // lossless: reconstruction == input, no weight write-back needed if (pCoded_log_blocks) { (*pCoded_log_blocks)(bx, by) = log_blk; } if (debug_images) xbc7_vis_fill_command(dbg_imgs, bx, by, command_byte, true, dpcm.m_pred_index, UINT32_MAX); // lossless DPCM weights: no DCT ACs blob_writer.put_byte(cBlobCommands, command_byte); continue; } command_byte |= ((uint8_t)xbc7::xbc7_command_weight_mode::cCmdWeightDCT << XBC7_COMMAND_WEIGHT_MODE_SHIFT); // optional AC-truncation RDO: prune high-freq weight-DCT ACs from the // winning symbols (once per block), within the configured PSNR drop if (opts.m_ac_trunc_rdo_max_psnr_drop > 0.0f) { if (!ac_truncate_rdo(best_syms, best_cand_log_blk, best_predictor_index, orig_block, bx, by, num_blocks_x, tile, log_blks, weight_grid_dct_fixed, dct_work_fixed, opts, stats)) return false; // refresh the AC count for the histogram below (pruning changed it) block_total_acs = 0; for (uint32_t p = 0; p < log_blk.m_num_planes; p++) { uint32_t n = best_syms[p].m_ac_vals.size_u32(); if (n && (best_syms[p].m_ac_vals[n - 1].m_coeff == INT16_MAX)) n--; block_total_acs += n; } } stats.m_wt_dct_blocks++; stats.m_wt_block_acs_hist_dct[basisu::minimum(block_total_acs, 32)]++; stats.m_wt_chosen_est_bits += dct_est_obits; blob_writer.put_byte(cBlobWeightPredictors, (uint8_t)best_predictor_index); stats.m_pred_hist[best_predictor_index]++; for (uint32_t p = 0; p < log_blk.m_num_planes; p++) { stats.m_dct_stats.record_plane(best_syms[p]); assert(iabs(best_syms[p].m_dc) <= 255); // TODO: Always writing full precision DC blob_writer.put_byte(cBlobDCCoeffsSmall, (uint8_t)iabs(best_syms[p].m_dc)); if (best_predictor_index == cCandAbsolute) { assert(best_syms[p].m_dc >= 0); } else { coeff_signs.put_bits((best_syms[p].m_dc < 0) ? 1 : 0, 1); } assert(best_syms[p].m_ac_vals.size()); for (uint32_t c = 0; c < best_syms[p].m_ac_vals.size(); c++) { uint32_t num_zeros = best_syms[p].m_ac_vals[c].m_num_zeros; int ac_coeff = best_syms[p].m_ac_vals[c].m_coeff; if (ac_coeff == INT16_MAX) { blob_writer.put_byte(cBlobACCoeffs, 0xFF); } else { assert(iabs(ac_coeff) <= 255); blob_writer.put_byte(cBlobACCoeffs, (uint8_t)num_zeros); blob_writer.put_byte(cBlobACCoeffs, (uint8_t)iabs(ac_coeff)); coeff_signs.put_bits((ac_coeff < 0) ? 1 : 0, 1); } } // c } // p log_blk = best_cand_log_blk; if (pCoded_log_blocks) { (*pCoded_log_blocks)(bx, by) = best_cand_log_blk; } if (debug_images) xbc7_vis_fill_command(dbg_imgs, bx, by, command_byte, true, best_predictor_index, block_total_acs); // DCT weights blob_writer.put_byte(cBlobCommands, command_byte); } // bx } // by return true; } // Packs a single 4x4 RGBA block into a physical BC7 block using whichever BC7 // base encoder is selected in opts: cBC7F is the built-in fast real-time // packer; cBC7E_Scalar is the higher-quality scalar bc7e encoder (whose // per-level params must be prebuilt once into bc7e_params before threading). // Either way it returns a standard physical BC7 block, identical in form to // what bc7f produces, so the rest of the pipeline is unaffected. static inline void xbc7_pack_bc7_base_block( basist::bc7u::phys_bc7_block& phys_blk, const color_rgba* pPixels, const pack_options& opts, const bc7e_scalar::bc7e_compress_block_params& bc7e_params, bool& used_lut) { used_lut = false; if (opts.m_bc7_encoder == bc7_encoder_type::cBC7E_Scalar) { // bc7e writes via a uint64_t*; phys_bc7_block::m_bytes has no alignment // guarantee, so encode into an aligned local and copy the 16 bytes back. // pUsed_lut reports whether the winning encoding used bc7e's one-color // endpoint LUTs on any subset (a "this block may be weird" hint). uint64_t blk64[2]; uint8_t lut_flag = 0; bc7e_scalar::bc7e_compress_blocks(1, blk64, (const uint32_t*)pPixels, &bc7e_params, &lut_flag); memcpy(phys_blk.m_bytes, blk64, sizeof(phys_blk.m_bytes)); used_lut = (lut_flag != 0); } else { // bc7f does not use the one-color endpoint LUTs. basist::bc7f::fast_pack_bc7_auto_rgba(phys_blk.m_bytes, (basist::color_rgba*)pPixels, opts.m_bc7_pack_flags); } } bool pack_image( const image& orig_img, const pack_options& opts, uint8_vec &comp_bytes, vector2D &coded_log_blocks) { #if !BASISD_SUPPORT_KTX2_ZSTD // XBC7 is entirely zstd-based: the stream is zstd-compressed (serialize) and // decoding/validation requires zstd too. We do not support uncompressed XBC7 // streams, so without BASISD_SUPPORT_KTX2_ZSTD there is nothing we can do -- // fail the encode cleanly. (The zstd-using body below isn't compiled here, so // the encoder still links: serialize() ends up unreferenced.) BASISU_NOTE_UNUSED(orig_img); BASISU_NOTE_UNUSED(opts); BASISU_NOTE_UNUSED(comp_bytes); BASISU_NOTE_UNUSED(coded_log_blocks); fmt_error_printf("XBC7 pack_image: XBC7 requires zstd (BASISD_SUPPORT_KTX2_ZSTD=0) -- cannot encode\n"); return false; #else // Internal pointer alias for the caller-provided reference (guaranteed // non-null): pack_stripe() and the threaded lambdas below thread the coded // logical blocks through as a pointer. The encoder always populates these // and then validates the encoded stream decodes back to them exactly. vector2D* pCoded_log_blocks = &coded_log_blocks; const uint32_t width = orig_img.get_width(); const uint32_t height = orig_img.get_height(); if ((!width) || (width > UINT16_MAX) || (!height) || (height > UINT16_MAX)) { assert(0); return false; } if ((opts.m_dct_q < 1) || (opts.m_dct_q > 100)) { assert(0); return false; } if (!opts.m_pJob_pool) { assert(0); return false; } const bool use_threading = (opts.m_pJob_pool->get_total_threads() > 1); const uint32_t block_width = 4; const uint32_t block_height = 4; const uint32_t num_blocks_x = (width + block_width - 1) / block_width; const uint32_t num_blocks_y = (height + block_height - 1) / block_height; const uint32_t total_blocks = num_blocks_x * num_blocks_y; const bool has_alpha = orig_img.has_alpha(); if (opts.m_debug_output) fmt_debug_printf("XBC7 pack: {}x{} texels, alpha: {}, block: {}x{}, blocks: {}x{} = {} total\n", width, height, has_alpha, block_width, block_height, num_blocks_x, num_blocks_y, total_blocks); // Dump the full set of pack_options that were passed in. if (opts.m_debug_output) { fmt_debug_printf("---- XBC7 pack_options ----\n"); fmt_debug_printf(" dct_q: {}\n", opts.m_dct_q); { const uint64_t dbg_mask = xbc7_build_pred_mask(opts.m_effort_level); uint32_t dbg_num_preds = 0; for (uint32_t c = 0; c < cTotalCandidates; c++) dbg_num_preds += (uint32_t)((dbg_mask >> c) & 1u); fmt_debug_printf(" effort_level: {} (weight predictors: {}/{}, amp codes: {}/4)\n", opts.m_effort_level, dbg_num_preds, (uint32_t)cTotalCandidates, xbc7_amp_codes_for_effort(opts.m_effort_level)); } fmt_debug_printf(" weights[RGBA]: {} {} {} {}\n", opts.m_weights[0], opts.m_weights[1], opts.m_weights[2], opts.m_weights[3]); fmt_debug_printf(" optimize_weights_after_bc7f: {}\n", opts.m_optimize_weights_after_bc7f); fmt_debug_printf(" bc7_encoder: {} (bc7e_scalar level: {}, perceptual: {})\n", (opts.m_bc7_encoder == bc7_encoder_type::cBC7E_Scalar) ? "bc7e_scalar" : "bc7f", opts.m_bc7e_scalar_level, opts.m_perceptual); fmt_debug_printf(" self_validate: {}\n", opts.m_self_validate); fmt_debug_printf(" bc7_pack_flags: 0x{X}\n", opts.m_bc7_pack_flags); fmt_debug_printf(" bc7_alt_pack_enabled: {}, bc7_pack_flags_alt: 0x{X}, bc7_alt_max_psnr_drop: {.3}\n", opts.m_bc7_alt_pack_enabled, opts.m_bc7_pack_flags_alt, opts.m_bc7_alt_max_psnr_drop); fmt_debug_printf(" repeat_rdo_enabled: {}, repeat_rdo_max_psnr_drop: {.3}\n", opts.m_repeat_rdo_enabled, opts.m_repeat_rdo_max_psnr_drop); fmt_debug_printf(" solid_rdo_enabled: {}, solid_rdo_max_psnr_drop: {.3}\n", opts.m_solid_rdo_enabled, opts.m_solid_rdo_max_psnr_drop); fmt_debug_printf(" endpoint_rdo_enabled: {}, endpoint_rdo_max_psnr_drop: {.3}\n", opts.m_endpoint_rdo_enabled, opts.m_endpoint_rdo_max_psnr_drop); fmt_debug_printf(" ac_trunc_rdo_max_psnr_drop: {.3}\n", opts.m_ac_trunc_rdo_max_psnr_drop); fmt_debug_printf(" rdo_min_block_psnr: {.3}\n", opts.m_rdo_min_block_psnr); fmt_debug_printf(" wt_dpcm_alpha_pct: {}\n", opts.m_wt_dpcm_alpha_pct); fmt_debug_printf(" num_stripes (0=auto): {}\n", opts.m_num_stripes); fmt_debug_printf(" job_pool total threads: {}\n", opts.m_pJob_pool ? (uint32_t)opts.m_pJob_pool->get_total_threads() : 0); fmt_debug_printf(" print_stats: {}, debug_images: {}, debug_file_prefix: '{}'\n", opts.m_print_stats, opts.m_debug_images, opts.m_debug_file_prefix.c_str()); } vector2D log_blks(num_blocks_x, num_blocks_y); // Per-block flag: 1 if the bc7e_scalar base pack used its one-color endpoint // LUTs on the block (pack_stripe forces lossless DPCM for these). Function // scope so it survives from the base pack into the stripe coding pass. vector2D base_used_lut(num_blocks_x, num_blocks_y); if (opts.m_debug_output) fmt_debug_printf("XBC7 progress: BC7 base pack ({} blocks{})...\n", total_blocks, opts.m_bc7_alt_pack_enabled ? ", alt-pack RDO" : ""); // BC7 base pack + canonicalize. Every block is independent and writes // only its own pre-sized log_blks slot, so this pass parallelizes with // simple atomic row stealing; all per-block state lives on each job's // stack. The packer/unpack/canonicalize helpers are pure functions of // their inputs (shared state is const tables initialized at startup). { // Prebuild the bc7e_scalar params once (shared, read-only across the // pack jobs); only used when that encoder is selected. The level [0,6] // picks one of its speed/quality presets (0=ultrafast .. 6=slowest). // A one-time global table init is required before the first encode. bc7e_scalar::bc7e_compress_block_params bc7e_params; memset(&bc7e_params, 0, sizeof(bc7e_params)); if (opts.m_bc7_encoder == bc7_encoder_type::cBC7E_Scalar) { bc7e_scalar::bc7e_compress_block_init(); typedef void (*bc7e_params_init_func)(bc7e_scalar::bc7e_compress_block_params*, bool); static const bc7e_params_init_func s_bc7e_level_init[BC7E_SCALAR_MAX_LEVEL + 1] = { &bc7e_scalar::bc7e_compress_block_params_init_ultrafast, // 0 &bc7e_scalar::bc7e_compress_block_params_init_veryfast, // 1 &bc7e_scalar::bc7e_compress_block_params_init_fast, // 2 &bc7e_scalar::bc7e_compress_block_params_init_basic, // 3 &bc7e_scalar::bc7e_compress_block_params_init_slow, // 4 &bc7e_scalar::bc7e_compress_block_params_init_veryslow, // 5 &bc7e_scalar::bc7e_compress_block_params_init_slowest, // 6 }; const uint32_t lvl = clamp(opts.m_bc7e_scalar_level, BC7E_SCALAR_MIN_LEVEL, BC7E_SCALAR_MAX_LEVEL); // For perceptual (sRGB) sources, run bc7e in its built-in perceptual // error mode and leave its channel weights at the preset defaults. For // linear sources, run bc7e in linear mode and honor the explicit XBC7 // channel weights. (bc7f supports neither, so this only affects bc7e.) s_bc7e_level_init[lvl](&bc7e_params, opts.m_perceptual); if (!opts.m_perceptual) { for (uint32_t i = 0; i < 4; i++) bc7e_params.m_weights[i] = opts.m_weights[i]; } // bc7e's aggressive one-color endpoint LUTs place endpoints at extreme // positions that only "land" with exact weights. Only allow them at // lossless Q (m_dct_q == 100, weights coded losslessly); at lower Q the // lossy weight DCT can't reproduce them and the block decodes to a // "trap" artifact, so disable the LUTs there. bc7e_params.m_use_luts = (opts.m_dct_q >= 100); } image raw_bc7_debug_image; if (opts.m_debug_images || (opts.m_debug_output && opts.m_print_stats)) { raw_bc7_debug_image.resize(orig_img.get_width(), orig_img.get_height()); } std::atomic cur_row; cur_row.store(0); std::atomic pack_failed_flag; pack_failed_flag.store(false); std::atomic alt_changed_blocks; // BC7 alt-pack RDO: blocks where the alternate was kept alt_changed_blocks.store(0); std::atomic lut_block_count; // bc7e_scalar: blocks whose winning encoding used the endpoint LUTs lut_block_count.store(0); std::atomic lut_block_count_nonsolid; // same, but excluding solid blocks (all 16 source pixels equal) lut_block_count_nonsolid.store(0); auto pack_rows_func = [num_blocks_x, num_blocks_y, &opts, &cur_row, &pack_failed_flag, &alt_changed_blocks, &lut_block_count, &lut_block_count_nonsolid, &orig_img, &log_blks, &raw_bc7_debug_image, &bc7e_params, &base_used_lut]() { for ( ; ; ) { if (pack_failed_flag) return; const uint32_t by = cur_row.fetch_add(1); if (by >= num_blocks_y) break; for (uint32_t bx = 0; bx < num_blocks_x; bx++) { color_rgba orig_block[16]; orig_img.extract_block_clamped(orig_block, bx * 4, by * 4, 4, 4); basist::bc7u::phys_bc7_block phys_blk; bool used_lut = false; xbc7_pack_bc7_base_block(phys_blk, orig_block, opts, bc7e_params, used_lut); base_used_lut(bx, by) = used_lut ? (uint8_t)1 : (uint8_t)0; if (used_lut) { lut_block_count.fetch_add(1, std::memory_order_relaxed); // A block is "solid" if all 16 source pixels are equal. bool is_solid = true; for (uint32_t i = 1; i < 16; i++) { if ((orig_block[i].r != orig_block[0].r) || (orig_block[i].g != orig_block[0].g) || (orig_block[i].b != orig_block[0].b) || (orig_block[i].a != orig_block[0].a)) { is_solid = false; break; } } if (!is_solid) lut_block_count_nonsolid.fetch_add(1, std::memory_order_relaxed); } if (raw_bc7_debug_image.get_width()) { basist::color_rgba unpacked_block[16]; bool as = basist::bc7u::unpack_bc7(&phys_blk, unpacked_block); assert(as); BASISU_NOTE_UNUSED(as); raw_bc7_debug_image.set_block_clipped((color_rgba *)unpacked_block, bx * 4, by * 4, 4, 4); } // Optional poor-man's RDO: also pack with the alternate // (typically cheaper) flags and keep IT unless the primary's // RGBA PSNR is at least the threshold dB higher. if (opts.m_bc7_alt_pack_enabled) { basist::bc7u::phys_bc7_block phys_alt; basist::bc7f::fast_pack_bc7_auto_rgba(phys_alt.m_bytes, (basist::color_rgba*)orig_block, opts.m_bc7_pack_flags_alt); basist::color_rgba dec_primary[16], dec_alt[16]; if ((!basist::bc7u::unpack_bc7(phys_blk.m_bytes, dec_primary)) || (!basist::bc7u::unpack_bc7(phys_alt.m_bytes, dec_alt))) { assert(0); pack_failed_flag.store(true); return; } const float psnr_primary = xbc7_block_wsse_psnr(orig_block, (const color_rgba*)dec_primary, opts.m_weights); const float psnr_alt = xbc7_block_wsse_psnr(orig_block, (const color_rgba*)dec_alt, opts.m_weights); if ((psnr_alt >= opts.m_rdo_min_block_psnr) && ((psnr_primary - psnr_alt) < opts.m_bc7_alt_max_psnr_drop)) { phys_blk = phys_alt; // above the quality floor and drop within tolerance -- keep the cheaper block alt_changed_blocks.fetch_add(1, std::memory_order_relaxed); } } basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by); bool unpack_status = basist::bc7u::unpack_bc7(&phys_blk, log_blk); if (!unpack_status) { assert(0); pack_failed_flag.store(true); return; } #if defined(DEBUG) || defined(_DEBUG) { // sanity check canonicalization code basist::bc7u::log_bc7_block temp_log_blk(log_blk); basist::bc7u::canonicalize_endpoints(temp_log_blk); color_rgba ap[16], bp[16]; bool as = basist::bc7u::unpack_bc7(log_blk, (basist::color_rgba*)ap); assert(as); bool bs = basist::bc7u::unpack_bc7(temp_log_blk, (basist::color_rgba*)bp); assert(bs); if (memcmp(ap, bp, sizeof(color_rgba) * 16) != 0) { assert(0); pack_failed_flag.store(true); return; } } #endif basist::bc7u::canonicalize_endpoints(log_blk); // optional: re-optimize the weights for bc7f's endpoints. // bc7f is a fast real-time packer, so its weights aren't // per-texel optimal -- this exhaustively re-derives them // (slower, but threaded with the base pack). Endpoints/config // unchanged, so it can only hold or improve quality. if (opts.m_optimize_weights_after_bc7f) { basist::bc7u::log_bc7_block opt_blk; if (!optimize_block_weights(log_blk, orig_block, opts.m_weights, opt_blk)) { assert(0); pack_failed_flag.store(true); return; } log_blk = opt_blk; } } // bx } }; if ((use_threading) && (num_blocks_y > 1)) { const uint32_t num_threads = (uint32_t)opts.m_pJob_pool->get_total_threads(); // one job per worker; each drains rows via the atomic counter for (uint32_t job_index = 0; job_index < num_threads; job_index++) opts.m_pJob_pool->add_job(pack_rows_func); opts.m_pJob_pool->wait_for_all(); } else { pack_rows_func(); } if (pack_failed_flag) return false; if (opts.m_debug_output && opts.m_bc7_alt_pack_enabled) { const uint32_t n = alt_changed_blocks.load(); fmt_debug_printf("BC7 alt-pack RDO: {} blocks changed ({.2}%)\n", n, total_blocks ? (n * 100.0f / (float)total_blocks) : 0.0f); } if (opts.m_debug_output && (opts.m_bc7_encoder == bc7_encoder_type::cBC7E_Scalar)) { const uint32_t n = lut_block_count.load(); const uint32_t n_nonsolid = lut_block_count_nonsolid.load(); fmt_debug_printf("bc7e_scalar: {} of {} blocks used endpoint LUTs ({.2}%); {} excluding solid blocks ({.2}%) [m_use_luts={}]\n", n, total_blocks, total_blocks ? (n * 100.0f / (float)total_blocks) : 0.0f, n_nonsolid, total_blocks ? (n_nonsolid * 100.0f / (float)total_blocks) : 0.0f, bc7e_params.m_use_luts); } if (raw_bc7_debug_image.get_width()) { if (opts.m_debug_images) save_png(opts.m_debug_file_prefix + "vis_raw_bc7.png", raw_bc7_debug_image); if ((opts.m_debug_output) && (opts.m_print_stats)) { fmt_debug_printf("\nRaw packed BC7 image stats:\n"); print_image_metrics(orig_img, raw_bc7_debug_image); } } } // Stripe geometry for the main coding pass. The count is a pure function // of m_num_stripes (or the image dimensions when it's 0), NOT of the pool // size, so the output is reproducible regardless of thread count. Pass // m_num_stripes = 1 for the single-stripe (no seam cost, no seek table) // format. More stripes than pool threads simply queue. basisu::vector stripes; uint32_t num_stripes; if (opts.m_num_stripes) { // caller forced a specific count -- clamp to what the format and the // image can hold (can't exceed the block-row count or the max) num_stripes = basisu::clamp(opts.m_num_stripes, 1, basisu::minimum(num_blocks_y, XBC7_MAX_ENCODER_STRIPES)); compute_stripe_ranges(num_blocks_y, num_stripes, stripes); } else { num_stripes = compute_encoder_stripes(height, num_blocks_y, stripes); } // optional block-reuse RDO: replace whole blocks with a Repeat of a neighbor // or a solid color where quality allows -- runs FIRST (cheapest commands) if (opts.m_repeat_rdo_enabled || opts.m_solid_rdo_enabled) { if (opts.m_debug_output) fmt_debug_printf("XBC7 progress: block-reuse RDO pass (repeat/solid)...\n"); if (!block_reuse_rdo_pass(orig_img, log_blks, num_blocks_x, stripes, opts)) return false; } // optional endpoint-prediction RDO: rewrite block endpoints toward their // causal neighbors (zero-residual DPCM) + re-optimize weights where quality // allows, BEFORE coding if (opts.m_endpoint_rdo_enabled) { if (opts.m_debug_output) fmt_debug_printf("XBC7 progress: endpoint-DPCM RDO pass...\n"); if (!endpoint_dpcm_rdo_pass(orig_img, log_blks, num_blocks_x, stripes, opts)) return false; } if (pCoded_log_blocks) { pCoded_log_blocks->resize(num_blocks_x, num_blocks_y); for (uint32_t y = 0; y < num_blocks_y; y++) { for (uint32_t x = 0; x < num_blocks_x; x++) { (*pCoded_log_blocks)(x, y).clear(); (*pCoded_log_blocks)(x, y).m_mode = -1; } } } blob_stream_writer blob_writer; basisu::bitwise_coder coeff_signs; coeff_signs.reserve(1024); basisu::bitwise_coder pbits; pbits.reserve(1024); basisu::bitwise_coder raw_endpoint_bits; raw_endpoint_bits.reserve(1024); xbc7_pack_stats stats; stats.m_width = width; stats.m_height = height; stats.m_total_blocks = total_blocks; stats.m_dct_q = opts.m_dct_q; stats.m_has_alpha = has_alpha; stats.m_wt_alpha_pct = opts.m_wt_dpcm_alpha_pct; stats.m_stripes = stripes; // ---- main coding pass: one job per stripe ---- // Each stripe codes into its own stripe_output (disjoint vector // elements, pre-sized here, so the jobs never touch shared mutable // state outside their own log_blks/pCoded rows). basisu::vector stripe_outputs(num_stripes); // optional per-block debug visualizations (one 4x4 pixel block per BC7 // block), each with a drawn legend strip below the block grid. Shared // across stripe jobs, but each stripe fills disjoint rows. image vis_command_img, vis_ep_mode_img, vis_wt_mode_img, vis_ac_count_img, vis_predictor_img; xbc7_debug_image_set dbg_imgs; if (opts.m_debug_images) { // same resolution as the source image; the legend is drawn into the // bottom strip afterward (overwriting a few block rows -- all clipped) vis_command_img.resize(width, height); vis_ep_mode_img.resize(width, height); vis_wt_mode_img.resize(width, height); vis_ac_count_img.resize(width, height); vis_predictor_img.resize(width, height); dbg_imgs.m_pAC_count = &vis_ac_count_img; dbg_imgs.m_pPredictor = &vis_predictor_img; dbg_imgs.m_pCommand = &vis_command_img; dbg_imgs.m_pEndpoint_mode = &vis_ep_mode_img; dbg_imgs.m_pWeight_mode = &vis_wt_mode_img; } std::atomic stripe_failed_flag; stripe_failed_flag.store(false); if (opts.m_debug_output) fmt_debug_printf("XBC7 progress: main coding pass ({} stripe{}, {})...\n", num_stripes, (num_stripes == 1) ? "" : "s", ((use_threading) && (num_stripes > 1)) ? "threaded" : "serial"); if ((use_threading) && (num_stripes > 1)) { for (uint32_t stripe_index = 0; stripe_index < num_stripes; stripe_index++) { opts.m_pJob_pool->add_job([stripe_index, has_alpha, num_blocks_x, &stripes, &stripe_outputs, &stripe_failed_flag, &orig_img, &opts, &log_blks, pCoded_log_blocks, &base_used_lut, dbg_imgs] { if (!pack_stripe(orig_img, opts, has_alpha, num_blocks_x, stripes[stripe_index], log_blks, pCoded_log_blocks, base_used_lut, stripe_outputs[stripe_index], dbg_imgs)) { stripe_failed_flag.store(true); } }); } opts.m_pJob_pool->wait_for_all(); } else { for (uint32_t stripe_index = 0; stripe_index < num_stripes; stripe_index++) { if (!pack_stripe(orig_img, opts, has_alpha, num_blocks_x, stripes[stripe_index], log_blks, pCoded_log_blocks, base_used_lut, stripe_outputs[stripe_index], dbg_imgs)) { stripe_failed_flag.store(true); break; } } } if (stripe_failed_flag) return false; if (opts.m_debug_images) { const xbc7_vis_legend_entry cmd_legend[8] = { { g_xbc7_command_vis_colors[0], "0 RepeatLast" }, { g_xbc7_command_vis_colors[1], "1 RepeatUpper" }, { g_xbc7_command_vis_colors[2], "2 SolidDPCM" }, { g_xbc7_command_vis_colors[3], "3 NewConfig" }, { g_xbc7_command_vis_colors[4], "4 ReuseConfig Left" }, { g_xbc7_command_vis_colors[5], "5 ReuseConfig Up" }, { g_xbc7_command_vis_colors[6], "6 ReuseConfig LeftDiag" }, { g_xbc7_command_vis_colors[7], "7 ReuseConfig RightDiag" }, }; const xbc7_vis_legend_entry ep_legend[9] = { { g_xbc7_endpoint_mode_vis_colors[0], "0 Raw" }, { g_xbc7_endpoint_mode_vis_colors[1], "1 DPCM Left" }, { g_xbc7_endpoint_mode_vis_colors[2], "2 DPCM Up" }, { g_xbc7_endpoint_mode_vis_colors[3], "3 DPCM LeftDiag" }, { g_xbc7_endpoint_mode_vis_colors[4], "4 DPCM RightDiag" }, { g_xbc7_endpoint_mode_vis_colors[5], "5 DPCM BlockIndex" }, { g_xbc7_endpoint_mode_vis_colors[6], "6 DPCM Left Subset1" }, { g_xbc7_endpoint_mode_vis_colors[7], "7 DPCM Up Subset1" }, { g_xbc7_vis_na_color, "n/a (fully predicted block)" }, }; const xbc7_vis_legend_entry wt_legend[3] = { { g_xbc7_weight_mode_vis_colors[0], "0 Raw/DPCM weights (lossless)" }, { g_xbc7_weight_mode_vis_colors[1], "1 DCT weights (lossy)" }, { g_xbc7_vis_na_color, "n/a (fully predicted block)" }, }; const xbc7_vis_legend_entry ac_legend[7] = { { g_xbc7_ac_count_vis_colors[0], "0 ACs (flat / DC only)" }, { g_xbc7_ac_count_vis_colors[1], "1-2 ACs" }, { g_xbc7_ac_count_vis_colors[2], "3-5 ACs" }, { g_xbc7_ac_count_vis_colors[3], "6-10 ACs" }, { g_xbc7_ac_count_vis_colors[4], "11-20 ACs" }, { g_xbc7_ac_count_vis_colors[5], "21+ ACs" }, { g_xbc7_vis_na_color, "n/a (not DCT-coded)" }, }; const xbc7_vis_legend_entry pred_legend[7] = { { g_xbc7_predictor_vis_colors[cPredCatAbsolute], "Absolute (no prediction)" }, { g_xbc7_predictor_vis_colors[cPredCatSynthetic], "Synthetic edge/gradient" }, { g_xbc7_predictor_vis_colors[cPredCatRefLeft], "BlockRef Left" }, { g_xbc7_predictor_vis_colors[cPredCatRefUp], "BlockRef Up" }, { g_xbc7_predictor_vis_colors[cPredCatRefUpLeft], "BlockRef Up-Left" }, { g_xbc7_predictor_vis_colors[cPredCatRefUpRight], "BlockRef Up-Right" }, { g_xbc7_vis_na_color, "n/a (fully predicted block)" }, }; xbc7_vis_draw_legend(vis_command_img, cmd_legend, 8); xbc7_vis_draw_legend(vis_ep_mode_img, ep_legend, 9); xbc7_vis_draw_legend(vis_wt_mode_img, wt_legend, 3); xbc7_vis_draw_legend(vis_ac_count_img, ac_legend, 7); xbc7_vis_draw_legend(vis_predictor_img, pred_legend, 7); save_png(opts.m_debug_file_prefix + "vis_xbc7_command.png", vis_command_img); save_png(opts.m_debug_file_prefix + "vis_xbc7_endpoint_mode.png", vis_ep_mode_img); save_png(opts.m_debug_file_prefix + "vis_xbc7_weight_mode.png", vis_wt_mode_img); save_png(opts.m_debug_file_prefix + "vis_xbc7_weight_ac_count.png", vis_ac_count_img); save_png(opts.m_debug_file_prefix + "vis_xbc7_weight_predictor.png", vis_predictor_img); } if (opts.m_debug_output) fmt_debug_printf("XBC7 progress: merging stripe streams{}...\n", (num_stripes > 1) ? " + building seek table" : ""); // Per-stripe seek table: for each stripe, the start offset of its data // in every per-stripe stream id 1..25 -- a BYTE offset for byte blobs, // a BIT offset for the three bit blobs (signs/pbits/ep_raw, which stay // bit-merged with no padding). Recorded BEFORE each stripe is appended. // Built only when there's more than one stripe. const uint32_t SEEK_NUM_STREAMS = (uint32_t)cBlobStripeSeekTable - 1; // ids 1..25 basisu::vector> seek_table; // [stripe * SEEK_NUM_STREAMS + (id-1)], little-endian DELTAS from prev stripe if (num_stripes > 1) seek_table.resize(num_stripes * SEEK_NUM_STREAMS); // running start offset of the PREVIOUS stripe in each stream (indexed by // blob id), so each table entry can be stored as a small delta uint64_t prev_seek_ofs[(uint32_t)cBlobStripeSeekTable] = {}; // ---- merge the per-stripe streams, strictly in stripe (raster) order ---- for (uint32_t stripe_index = 0; stripe_index < num_stripes; stripe_index++) { const stripe_output& so = stripe_outputs[stripe_index]; if (num_stripes > 1) { for (uint32_t id = 1; id < (uint32_t)cBlobStripeSeekTable; id++) { uint64_t ofs; if (id == cBlobCoeffSigns) ofs = coeff_signs.get_total_bits(); // bit offset else if (id == cBlobPBits) ofs = pbits.get_total_bits(); // bit offset else if (id == cBlobEPRaw) ofs = raw_endpoint_bits.get_total_bits(); // bit offset else ofs = blob_writer.get_blob_size(id); // byte offset // store the delta from the previous stripe's start (stripe 0 -> 0). // offsets are monotonic, so the delta is non-negative and small; // packed_uint<4> keeps the low 32 bits, little-endian. seek_table[stripe_index * SEEK_NUM_STREAMS + (id - 1)] = ofs - prev_seek_ofs[id]; prev_seek_ofs[id] = ofs; } } for (uint32_t id = 0; id < BLOB_STREAM_MAX_IDS; id++) { const uint8_vec* pBlob = so.m_blob_writer.get_blob_data(id); if ((pBlob) && (pBlob->size())) blob_writer.append_bytes(id, *pBlob); } // bit streams: append() carries each stripe's unflushed partial bit // buffer, so the merged streams contain NO padding at the seams -- // the decoder still reads single continuous LSB-first streams, and // the seek table addresses stripe starts at BIT granularity coeff_signs.append(so.m_coeff_signs); pbits.append(so.m_pbits); raw_endpoint_bits.append(so.m_raw_endpoint_bits); stats.merge(so.m_stats); } if (raw_endpoint_bits.get_total_bits()) { raw_endpoint_bits.flush(); blob_writer.get_blob_vec(cBlobEPRaw) = raw_endpoint_bits.get_bytes(); } if (pbits.get_total_bits()) { pbits.flush(); blob_writer.get_blob_vec(cBlobPBits) = pbits.get_bytes(); } if (coeff_signs.get_total_bits()) { coeff_signs.flush(); blob_writer.get_blob_vec(cBlobCoeffSigns) = coeff_signs.get_bytes(); } // emit the seek table, byte-plane (SoA) transposed: all entries' byte 0 // first, then every byte 1, then byte 2, then byte 3. The deltas are // small, so the upper planes are near-all-zero and Zstd crushes them -- // much better than the interleaved little-endian layout. Lossless // reordering; the decoder reverses it before reading the deltas. if (num_stripes > 1) { const uint32_t num_entries = (uint32_t)seek_table.size(); // num_stripes * SEEK_NUM_STREAMS const uint8_t* pSrc = (const uint8_t*)seek_table.data(); // little-endian, 4 bytes/entry uint8_vec planed(num_entries * 4); for (uint32_t plane = 0; plane < 4; plane++) for (uint32_t e = 0; e < num_entries; e++) planed[plane * num_entries + e] = pSrc[e * 4 + plane]; blob_writer.put_bytes(cBlobStripeSeekTable, planed.data(), planed.size()); } xbc7_header hdr; clear_obj(hdr); hdr.m_dct_q = (uint8_t)opts.m_dct_q; hdr.m_flags = 0; if (has_alpha) hdr.m_flags = hdr.m_flags | (uint8_t)XBC7_FLAG_HAS_ALPHA; hdr.m_width_in_texels = orig_img.get_width(); hdr.m_height_in_texels = orig_img.get_height(); hdr.m_num_stripes = (uint8_t)num_stripes; blob_writer.put_bytes(cBlobHeader, &hdr, sizeof(hdr)); for (uint32_t id = 0; id < BLOB_STREAM_MAX_IDS; id++) { stats.m_blob_raw_size[id] = (uint32_t)blob_writer.get_blob_size(id); stats.record_blob_bytes(id, blob_writer.get_blob_data(id)); } if (opts.m_debug_output) fmt_debug_printf("XBC7 progress: serializing + Zstd compressing blobs...\n"); if (!blob_writer.serialize(comp_bytes, 19, stats.m_blob_stored_size)) return false; stats.m_total_file_size = comp_bytes.size_u32(); if (opts.m_debug_output && (opts.m_ac_trunc_rdo_max_psnr_drop > 0.0f)) fmt_debug_printf("AC-trunc RDO: {} coeffs pruned across {} blocks ({.2}% of blocks)\n", stats.m_ac_trunc_pruned, stats.m_ac_trunc_blocks, total_blocks ? (stats.m_ac_trunc_blocks * 100.0f / (float)total_blocks) : 0.0f); // m_debug_output is the master gate; m_print_stats selects the dashboard if (opts.m_debug_output && opts.m_print_stats) stats.print(); if (pCoded_log_blocks) { for (uint32_t by = 0; by < num_blocks_y; by++) { for (uint32_t bx = 0; bx < num_blocks_x; bx++) { if (!basist::bc7u::compare_block_full((*pCoded_log_blocks)(bx, by), log_blks(bx, by))) { assert(0); return false; } } } } if (opts.m_debug_output) fmt_debug_printf("XBC7 progress: tiny-mip size check...\n"); // Tiny-mip fallback: for the smallest levels the blob container's fixed // overhead can exceed a raw packed-BC7 encoding. If a tiny-mip stream -- // [marker][num_blocks_x:u8][num_blocks_y:u8] followed by 16 bytes per BC7 // block -- is smaller (and the block dims fit a byte), emit that instead. // The decoder distinguishes streams by the leading byte: 0xB7 = blob, // 0xB8 = tiny-mip without alpha, 0xB9 = tiny-mip with alpha (so the // has_alpha bit rides in the marker). The packed blocks are the FINAL // logical blocks (identical to pCoded_log_blocks), so the decode round- // trips exactly (pack<->unpack of a logical block is lossless). if ((num_blocks_x <= 255) && (num_blocks_y <= 255)) { const size_t tiny_size = 3 + (size_t)total_blocks * 16; const uint32_t blob_size = comp_bytes.size_u32(); if (tiny_size < (size_t)blob_size) { comp_bytes.resize(0); comp_bytes.reserve((uint32_t)tiny_size); comp_bytes.push_back(has_alpha ? (uint8_t)0xB9 : (uint8_t)0xB8); // marker carries the alpha bit comp_bytes.push_back((uint8_t)num_blocks_x); comp_bytes.push_back((uint8_t)num_blocks_y); for (uint32_t by = 0; by < num_blocks_y; by++) { for (uint32_t bx = 0; bx < num_blocks_x; bx++) { basist::bc7u::phys_bc7_block phys; if (!basist::bc7u::pack_bc7(log_blks(bx, by), &phys)) { assert(0); return false; } comp_bytes.append(phys.m_bytes, sizeof(phys.m_bytes)); // The tiny-mip decoder reconstructs each block by UNPACKING // the stored physical block. BC7 pack<->unpack is pixel- // lossless but can re-canonicalize the LOGICAL fields // (endpoint swap + weight complement -> identical pixels, // different struct). So the coded reference must be the // unpacked form -- exactly what the decoder will yield -- // not the original log_blks, or a per-block logical compare // would spuriously fail. if (pCoded_log_blocks) { if (!basist::bc7u::unpack_bc7(&phys, (*pCoded_log_blocks)(bx, by))) { assert(0); return false; } } } } assert(comp_bytes.size() == tiny_size); if (opts.m_debug_output) fmt_debug_printf("XBC7 tiny-mip selected: {} blocks, {} bytes (blob path was {} bytes)\n", total_blocks, comp_bytes.size_u32(), blob_size); } } if (opts.m_debug_output) fmt_debug_printf("XBC7 output: {} bytes, {} bits/pixel\n", comp_bytes.size_u32(), ((float)comp_bytes.size() * 8.0f) / (float)orig_img.get_total_pixels()); // Self-validation (opts.m_self_validate, ON by default): decode the stream we // just produced through the transcoder and verify EVERY logical BC7 block // round-trips exactly to the coded reference (coded_log_blocks). This // guarantees that every emitted XBC7 stream is valid and unpacks correctly // during encoding. It exercises whichever stream format was written above -- // the blob format or the tiny-mip format (coded_log_blocks already holds the // form the decoder yields in each case, including the tiny-mip's // re-canonicalized blocks). if (opts.m_self_validate) { if (opts.m_debug_output) fmt_debug_printf("XBC7 progress: decode self-validation...\n"); struct verify_ctx { const vector2D* m_pRef; uint32_t m_expected_nbx, m_expected_nby; bool m_dims_ok; bool m_blocks_ok; } vctx; vctx.m_pRef = &coded_log_blocks; vctx.m_expected_nbx = num_blocks_x; vctx.m_expected_nby = num_blocks_y; vctx.m_dims_ok = true; vctx.m_blocks_ok = true; auto verify_init_cb = [](uint32_t nbx, uint32_t nby, uint32_t, uint32_t, uint32_t, bool, void* pData) -> bool { verify_ctx& c = *(verify_ctx*)pData; if ((nbx != c.m_expected_nbx) || (nby != c.m_expected_nby)) { c.m_dims_ok = false; return false; // abort decode: dimensions disagree with what we encoded } return true; }; auto verify_block_cb = [](uint32_t bx, uint32_t by, const basist::bc7u::log_bc7_block& decoded_blk, void* pData) -> bool { verify_ctx& c = *(verify_ctx*)pData; if (!basist::bc7u::compare_block_full((*c.m_pRef)(bx, by), decoded_blk)) { c.m_blocks_ok = false; return false; // abort decode: a block did not round-trip } return true; }; const bool decode_ok = basist::xbc7::unpack_image( basist::xbc7::byte_span(comp_bytes), verify_init_cb, &vctx, verify_block_cb, &vctx); if ((!decode_ok) || (!vctx.m_dims_ok) || (!vctx.m_blocks_ok)) { assert(0 && "XBC7 pack_image: encoded stream failed decode self-validation"); fmt_error_printf("XBC7 pack_image: SELF-VALIDATION FAILED -- the encoded stream did not decode back to the expected BC7 logical blocks (decode_ok={}, dims_ok={}, blocks_ok={})\n", decode_ok, vctx.m_dims_ok, vctx.m_blocks_ok); return false; } if (opts.m_debug_output) fmt_debug_printf("XBC7 self-validation: decode round-trip OK ({} blocks)\n", total_blocks); } return true; #endif // BASISD_SUPPORT_KTX2_ZSTD } } // namespace xbc7 } // namespace basisu