mirror of
https://github.com/BinomialLLC/basis_universal.git
synced 2026-07-12 17:19:19 +00:00
3644 lines
134 KiB
C++
3644 lines
134 KiB
C++
// 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
|