| // 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<const uint8_t*>(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<uint8_vec> 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<uint32_t>(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<uint32_t>(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<<m_weight_bits[plane] values and keeping each texel's lowest WSSE yields | |
| // the per-texel optimum. Error is summed ONLY over the channels this plane | |
| // drives (get_decoded_channel_weight_plane) -- critical for dual-plane modes, | |
| // where planes 0/1 control disjoint channels (with mode 4/5 rotation/CCS). | |
| [[maybe_unused]] static bool optimize_block_weights(const basist::bc7u::log_bc7_block& in_blk, const color_rgba orig_pixels[16], const uint32_t comp_weights[4], basist::bc7u::log_bc7_block& out_blk) | |
| { | |
| out_blk = in_blk; // same config + endpoints; weights overwritten below | |
| for (uint32_t p = 0; p < in_blk.m_num_planes; p++) | |
| { | |
| const uint32_t num_weight_vals = in_blk.get_num_weight_vals(p); // 4, 8, or 16 | |
| basist::bc7u::log_bc7_block scratch(in_blk); | |
| // neutralize the OTHER plane so the decode is well-defined; the | |
| // channel masking below means it can't affect this plane's WSSE anyway | |
| if (in_blk.is_dual_plane()) | |
| memset(scratch.m_weights[1u - p], 0, sizeof(scratch.m_weights[0])); | |
| uint64_t best_err[16]; | |
| uint8_t best_weight[16]; | |
| for (uint32_t i = 0; i < 16; i++) | |
| { | |
| best_err[i] = UINT64_MAX; | |
| best_weight[i] = 0; | |
| } | |
| for (uint32_t w = 0; w < num_weight_vals; w++) | |
| { | |
| for (uint32_t i = 0; i < 16; i++) | |
| scratch.m_weights[p][i] = (uint8_t)w; | |
| basist::color_rgba dec[16]; | |
| if (!basist::bc7u::unpack_bc7(scratch, dec)) | |
| { | |
| assert(0); // a valid config+endpoints block must always decode | |
| return false; | |
| } | |
| for (uint32_t i = 0; i < 16; i++) | |
| { | |
| uint64_t err = 0; | |
| for (uint32_t c = 0; c < 4; c++) | |
| { | |
| if (in_blk.get_decoded_channel_weight_plane(c) != p) | |
| continue; // only the channels this plane drives | |
| const int d = (int)dec[i][c] - (int)orig_pixels[i][c]; | |
| err += (uint64_t)(d * d) * comp_weights[c]; | |
| } | |
| if (err < best_err[i]) | |
| { | |
| best_err[i] = err; | |
| best_weight[i] = (uint8_t)w; | |
| } | |
| } | |
| } | |
| for (uint32_t i = 0; i < 16; i++) | |
| out_blk.m_weights[p][i] = best_weight[i]; | |
| } | |
| return true; | |
| } | |
| // Encode-time visualization targets (debug only). Each member is null unless | |
| // m_debug_images is set. One 4x4 pixel block per BC7 block; stripes fill | |
| // disjoint block rows, so writing these from stripe jobs is thread-safe. | |
| struct xbc7_debug_image_set | |
| { | |
| image* m_pCommand = nullptr; // core command id (bits 0-2) | |
| image* m_pEndpoint_mode = nullptr; // endpoint pred mode (bits 3-5); n/a for predicted blocks | |
| image* m_pWeight_mode = nullptr; // weight mode (bit 6): DCT vs lossless; n/a for predicted blocks | |
| image* m_pAC_count = nullptr; // # nonzero DCT weight ACs; n/a unless DCT-coded | |
| image* m_pPredictor = nullptr; // weight predictor category; n/a for predicted blocks | |
| }; | |
| // Fill one BC7 block's 4x4 region in a (possibly null) debug image. | |
| static inline void xbc7_vis_fill_block(image* pImg, uint32_t bx, uint32_t by, const color_rgba& c) | |
| { | |
| if (pImg) | |
| pImg->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<stripe_range>& 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<uint32_t>(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<uint32_t>(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<uint32_t>(desired_num_stripes, 1, max_stripes); | |
| return m_num_stripes; | |
| } | |
| void pack_options::set_rdo_level(uint32_t rdo_level) | |
| { | |
| rdo_level = basisu::minimum<uint32_t>(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<stripe_range> 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<uint32_t>(effort, 10); | |
| const uint32_t cCoreCands = 6; | |
| uint32_t count = cCoreCands + ((cTotalCandidates - cCoreCands) * effort + 5) / 10; | |
| count = basisu::minimum<uint32_t>(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<uint32_t>(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<basist::bc7u::log_bc7_block> &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<basist::bc7u::log_bc7_block>& 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<int>(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<basist::bc7u::log_bc7_block>& log_blks, | |
| uint32_t num_blocks_x, | |
| const basisu::vector<stripe_range>& 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<basist::bc7u::log_bc7_block>& log_blks, | |
| uint32_t num_blocks_x, | |
| const basisu::vector<stripe_range>& 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<basist::bc7u::log_bc7_block>& 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<basist::bc7u::log_bc7_block>& log_blks, | |
| vector2D<basist::bc7u::log_bc7_block>* pCoded_log_blocks, | |
| const vector2D<uint8_t>& 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<uint32_t>(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<int>(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<uint32_t>(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<basist::bc7u::log_bc7_block> &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<basist::bc7u::log_bc7_block>* 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<basist::bc7u::log_bc7_block> 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<uint8_t> 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<uint32_t>(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<uint32_t> cur_row; | |
| cur_row.store(0); | |
| std::atomic<bool> pack_failed_flag; | |
| pack_failed_flag.store(false); | |
| std::atomic<uint32_t> alt_changed_blocks; // BC7 alt-pack RDO: blocks where the alternate was kept | |
| alt_changed_blocks.store(0); | |
| std::atomic<uint32_t> lut_block_count; // bc7e_scalar: blocks whose winning encoding used the endpoint LUTs | |
| lut_block_count.store(0); | |
| std::atomic<uint32_t> 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<stripe_range> 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<uint32_t>(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_output> 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<bool> 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<basisu::packed_uint<4>> 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<basist::bc7u::log_bc7_block>* 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 |