Files
basis_universal/transcoder/basisu_xbc7_decoder.h
Richard Geldreich 929f80198d fixing comments
2026-07-02 01:32:34 -04:00

1468 lines
50 KiB
C++

// File: basisu_xbc7_decoder.h
// XBC7 decoder: shared definitions + decoder API. Lives in the transcoder so the
// decode path (XUBC7 -> BC7/etc.) can use it; the xbc7 encoder also includes this
// header for the shared DCT/symbol/enum definitions. The implementation is in
// basisu_xbc7_decoder.inl (included at the end of basisu_transcoder.cpp). Depends
// only on transcoder headers.
#pragma once
#include "basisu.h"
#include "basisu_containers.h"
#include "basisu_transcoder_internal.h"
namespace basist {
namespace xbc7 {
using basist::fixed16_16;
using basisu::uint8_vec; // these basisu containers/util are used unqualified below
using basisu::vector2D;
using basisu::fmt_debug_printf;
using basisu::minimum;
using basisu::maximum;
using basisu::clamp;
using basisu::iabs;
// dct2fx: 2D orthonormal DCT-II (forward) / DCT-III (inverse) on fixed16_16.
// Port of the float dct2f class. Pure integer end to end, INCLUDING table
// generation at init: libm cosf/sqrtf are not bit-identical across platforms,
// which would silently break cross-platform determinism. Tables are built
// with an integer Q30 cosine (range reduction + nested Taylor, error < 1e-8)
// and the deterministic integer sqrt, then quantized once to Q15.16.
//
// Differences from the float original (all intentional):
// - The alpha(u)/alpha(v) scale factors are FOLDED into the cos tables at
// init (entry = alpha(u)*cos(...)). One multiply and one rounding fewer
// per output; the m_a_col/m_a_row members are gone.
// - Dot products accumulate raw 64-bit products (mul_wide) and round ONCE
// per output (from_sum) -- the fast pattern, and in fixed point it makes
// inverse() and inverse_check() bit-identical (int64 addition commutes,
// unlike float).
// - The inverse zero-skip is pSrc[...].v == 0 -- no type-punning needed.
// - The stale BASISU_NOTE_UNUSED(src_stride/dst_stride) lines are gone:
// the strides were actually used right below them.
//
// Range guidance (Q15.16, asserts catch violations in debug): worst-case
// gain of the 2D transform is sqrt(rows*cols) <= 12, so inputs bounded by
// ~2700 in magnitude can never overflow any intermediate or output.
//
// Precision (measured, 12x12 worst case, inputs +-2000): forward abs error
// vs a double reference <= ~0.5, round-trip error <= ~0.2. Dominated by the
// 2^-17 quantization of the Q15.16 tables (error scales ~ |input|*N*2^-17
// per pass, x sqrt(N) through the second pass) -- the same effect the float
// original has at 2^-24 scale.
typedef basisu::vector<fixed16_16> fxvec;
namespace dct_detail
{
// cos(pi * k / n) in Q30, pure integer, deterministic. n in [1, 48].
// Error < 1e-8 (Taylor truncation ~2e-11, arithmetic ~few * 2^-30),
// far below the Q15.16 table quantization of 2^-17.
constexpr int64_t cos_pi_frac_q30(uint32_t k, uint32_t n)
{
const int64_t Q30 = int64_t(1) << 30;
// range-reduce: period 2n, fold to [0, n], then to [0, n/2] + sign
uint32_t m = k % (2u * n);
if (m > n) m = 2u * n - m; // cos(2pi - t) = cos t
bool neg = false;
if (2u * m > n) { m = n - m; neg = true; } // cos(pi - t) = -cos t
// theta = pi*m/n in Q30, theta <= pi/2
const int64_t PI_Q30 = 3373259426ll; // round(pi * 2^30)
const int64_t th = (PI_Q30 * int64_t(m)) / int64_t(n);
const int64_t x2 = (th * th) >> 30; // theta^2, Q30
// cos t = 1 - x2/2*(1 - x2/12*(1 - x2/30*(1 - x2/56*(1 - x2/90*
// (1 - x2/132*(1 - x2/182)))))) (nested Taylor)
const int dens[7] = { 182, 132, 90, 56, 30, 12, 2 };
int64_t r = Q30;
for (int i = 0; i < 7; i++)
r = Q30 - ((x2 * r) >> 30) / dens[i];
return neg ? -r : r;
}
// sqrt(1/n) and sqrt(2/n) in Q30, via the exact integer sqrt
constexpr int64_t alpha0_q30(uint32_t n)
{
return int64_t(basist::fixed_detail::isqrt_floor((uint64_t(1) << 60) / n));
}
constexpr int64_t alpha_q30(uint32_t n)
{
return int64_t(basist::fixed_detail::isqrt_floor((uint64_t(1) << 61) / n));
}
// Q30 * Q30 -> Q15.16, rounded half away from zero
constexpr int32_t q60_to_q16(int64_t p)
{
const int64_t h = int64_t(1) << 43;
return int32_t(p >= 0 ? ((p + h) >> 44) : -(((-p) + h) >> 44));
}
}
// Fixed-point 4-sample orthonormal DCT-II / IDCT-III (radix-2 butterfly).
// Overloads for fixed16_16 (the float template needs T(double), which fixed
// deliberately lacks). Constants are the EXACT Q15.16 quantizations of the
// alpha*cos table entries the general dct2fx path uses, and partial sums are
// kept wide (int64) with ONE rounding per output -- int64 sums commute, so
// these butterflies are bit-identical to the general matrix product. That
// lets dct2fx dispatch 4x4 to them with zero behavioral change.
namespace dct4
{
namespace fxk
{
typedef fixed16_16 fx;
// alpha(k)*cos(pi*(2n+1)k/8) quantized exactly as dct2fx's tables
constexpr int64_t A0 = dct_detail::alpha0_q30(4); // 1/2
constexpr int64_t A = dct_detail::alpha_q30(4); // 1/sqrt(2)
constexpr fx HALF = fx::from_raw(dct_detail::q60_to_q16(A0 * dct_detail::cos_pi_frac_q30(0, 8)));
constexpr fx C1 = fx::from_raw(dct_detail::q60_to_q16(A * dct_detail::cos_pi_frac_q30(1, 8)));
constexpr fx C3 = fx::from_raw(dct_detail::q60_to_q16(A * dct_detail::cos_pi_frac_q30(3, 8)));
static_assert(HALF.v == 32768, ""); // 0.5 exact
static_assert(C1.v == 42813, ""); // cos(pi/8) /sqrt(2) ~ 0.653281
static_assert(C3.v == 17734, ""); // cos(3pi/8)/sqrt(2) ~ 0.270598
}
inline void forward_ortho(const fixed16_16 x[4], fixed16_16 y[4])
{
using namespace fxk;
const fx a0 = x[0] + x[3];
const fx a1 = x[1] + x[2];
const fx a2 = x[1] - x[2];
const fx a3 = x[0] - x[3];
y[0] = fx::from_sum(a0.mul_wide(HALF) + a1.mul_wide(HALF));
y[1] = fx::from_sum(a3.mul_wide(C1) + a2.mul_wide(C3));
y[2] = fx::from_sum(a0.mul_wide(HALF) - a1.mul_wide(HALF));
y[3] = fx::from_sum(a3.mul_wide(C3) - a2.mul_wide(C1));
}
inline void inverse_ortho(const fixed16_16 y[4], fixed16_16 x[4])
{
using namespace fxk;
// shared partial sums stay wide; each output rounded once
const int64_t b0 = y[0].mul_wide(HALF) + y[2].mul_wide(HALF);
const int64_t b1 = y[0].mul_wide(HALF) - y[2].mul_wide(HALF);
const int64_t t0 = y[1].mul_wide(C1) + y[3].mul_wide(C3);
const int64_t t1 = y[1].mul_wide(C3) - y[3].mul_wide(C1);
x[0] = fx::from_sum(b0 + t0);
x[3] = fx::from_sum(b0 - t0);
x[1] = fx::from_sum(b1 + t1);
x[2] = fx::from_sum(b1 - t1);
}
inline void forward_ortho_inplace(fixed16_16 x[4])
{
fixed16_16 y[4];
forward_ortho(x, y);
x[0] = y[0]; x[1] = y[1]; x[2] = y[2]; x[3] = y[3];
}
inline void inverse_ortho_inplace(fixed16_16 x[4])
{
fixed16_16 y[4];
inverse_ortho(x, y);
x[0] = y[0]; x[1] = y[1]; x[2] = y[2]; x[3] = y[3];
}
}
class dct2fx
{
enum { cMaxSize = 12 };
public:
typedef fixed16_16 fx;
dct2fx() : m_rows(0u), m_cols(0u) {}
// call with grid_height/grid_width (INVERTED)
bool init(uint32_t rows, uint32_t cols)
{
if ((rows < 2u) || (rows > cMaxSize) ||
(cols < 2u) || (cols > cMaxSize))
{
assert(0);
return false;
}
m_rows = rows;
m_cols = cols;
m_c_col.assign(m_rows * m_rows, fx());
m_c_row.assign(m_cols * m_cols, fx());
// tables with alpha folded in: entry = alpha(u) * cos(pi*(2x+1)*u / (2*rows))
for (uint32_t u = 0; u < m_rows; ++u)
for (uint32_t x = 0; x < m_rows; ++x)
m_c_col[u * m_rows + x] = table_entry(u, x, m_rows);
for (uint32_t v = 0; v < m_cols; ++v)
for (uint32_t y = 0; y < m_cols; ++y)
m_c_row[v * m_cols + y] = table_entry(v, y, m_cols);
#ifndef NDEBUG
// one-time sanity check (debug builds): hash the table entries of ALL
// legal sizes 2..12 against a golden constant. Any platform/compiler
// generating different bits trips this assert immediately.
static const bool s_tables_ok = check_tables();
assert(s_tables_ok && "dct2fx: table generation differs from golden hash");
#endif
return true;
}
uint32_t rows() const { return m_rows; }
uint32_t cols() const { return m_cols; }
void forward(const fx* pSrc, fx* pDst, fxvec& work) const
{
forward(pSrc, m_cols, pDst, m_cols, work);
}
void inverse(const fx* pSrc, fx* pDst, fxvec& work) const
{
inverse(pSrc, m_cols, pDst, m_cols, work);
}
void inverse_check(const fx* pSrc, fx* pDst, fxvec& work) const
{
inverse_check(pSrc, m_cols, pDst, m_cols, work);
}
void forward(const fx* pSrc, uint32_t src_stride,
fx* pDst, uint32_t dst_stride, fxvec& work) const
{
assert(m_rows && m_cols);
work.resize(m_rows * m_cols);
forward(pSrc, src_stride, pDst, dst_stride, &work[0]);
}
void forward(const fx* pSrc, uint32_t src_stride,
fx* pDst, uint32_t dst_stride, fx* pWork) const
{
assert(m_rows && m_cols);
if ((m_rows == 4u) && (m_cols == 4u))
{
forward_4x4(pSrc, src_stride, pDst, dst_stride);
return;
}
const uint32_t m = m_rows, n = m_cols;
// horizontal
for (uint32_t x = 0; x < m; ++x)
{
const fx* pRowIn = pSrc + x * src_stride;
fx* pRowT = pWork + x * n;
for (uint32_t v = 0; v < n; ++v)
{
const fx* pCv = &m_c_row[v * n];
int64_t acc = 0;
for (uint32_t y = 0; y < n; ++y)
acc += pRowIn[y].mul_wide(pCv[y]);
pRowT[v] = fx::from_sum(acc); // alpha already folded in
}
}
// vertical
for (uint32_t v = 0; v < n; ++v)
{
for (uint32_t u = 0; u < m; ++u)
{
const fx* pCu = &m_c_col[u * m];
int64_t acc = 0;
for (uint32_t x = 0; x < m; ++x)
acc += pWork[x * n + v].mul_wide(pCu[x]);
pDst[u * dst_stride + v] = fx::from_sum(acc);
}
}
}
void inverse(const fx* pSrc, uint32_t src_stride,
fx* pDst, uint32_t dst_stride, fxvec& work) const
{
assert(m_rows && m_cols);
if ((m_rows == 4u) && (m_cols == 4u))
{
inverse_4x4(pSrc, src_stride, pDst, dst_stride);
return;
}
work.resize(m_rows * m_cols);
const uint32_t m = m_rows, n = m_cols;
fx* pWork = &work[0];
// vertical
for (uint32_t v = 0; v < n; ++v) // cols
{
int64_t sums[cMaxSize] = { 0 };
for (uint32_t u = 0; u < m; ++u) // rows
{
const fx yU = pSrc[u * src_stride + v];
if (yU.v == 0) // most coeffs will be 0
continue;
const fx* pCu = &m_c_col[u * m];
for (uint32_t x = 0; x < m; ++x)
sums[x] += yU.mul_wide(pCu[x]);
} // u
for (uint32_t x = 0; x < m; ++x)
pWork[x * n + v] = fx::from_sum(sums[x]);
} // v
// horizontal
for (uint32_t x = 0; x < m; ++x) // rows
{
const fx* pRowT = pWork + x * n;
fx* pRowOut = pDst + x * dst_stride;
for (uint32_t y = 0; y < n; ++y) // cols
{
int64_t acc = 0;
for (uint32_t v = 0; v < n; ++v) // cols
acc += pRowT[v].mul_wide(m_c_row[v * n + y]);
pRowOut[y] = fx::from_sum(acc);
}
}
}
void inverse_check(const fx* pSrc, uint32_t src_stride,
fx* pDst, uint32_t dst_stride, fxvec& work) const
{
assert(m_rows && m_cols);
work.resize(m_rows * m_cols);
const uint32_t m = m_rows, n = m_cols;
fx* pWork = &work[0];
// vertical
for (uint32_t v = 0; v < n; ++v)
{
for (uint32_t x = 0; x < m; ++x)
{
int64_t acc = 0;
for (uint32_t u = 0; u < m; ++u)
acc += pSrc[u * src_stride + v].mul_wide(m_c_col[u * m + x]);
pWork[x * n + v] = fx::from_sum(acc);
}
}
// horizontal
for (uint32_t x = 0; x < m; ++x) // rows
{
const fx* pRowT = pWork + x * n;
fx* pRowOut = pDst + x * dst_stride;
for (uint32_t y = 0; y < n; ++y) // cols
{
int64_t acc = 0;
for (uint32_t v = 0; v < n; ++v) // cols
acc += pRowT[v].mul_wide(m_c_row[v * n + y]);
pRowOut[y] = fx::from_sum(acc);
}
}
}
private:
// Specialized 4x4 path via the dct4 butterflies. Bit-identical to the
// general matrix path (same quantized constants, same wide sums, one
// rounding per output), just ~2.5x fewer multiplies. inverse_check is
// deliberately NOT dispatched: it stays the independent reference.
void forward_4x4(const fx* pSrc, uint32_t src_stride,
fx* pDst, uint32_t dst_stride) const
{
fx t[16];
for (uint32_t x = 0; x < 4; ++x) // horizontal
dct4::forward_ortho(pSrc + x * src_stride, t + x * 4);
for (uint32_t v = 0; v < 4; ++v) // vertical
{
const fx col[4] = { t[v], t[4 + v], t[8 + v], t[12 + v] };
fx out[4];
dct4::forward_ortho(col, out);
for (uint32_t u = 0; u < 4; ++u)
pDst[u * dst_stride + v] = out[u];
}
}
void inverse_4x4(const fx* pSrc, uint32_t src_stride,
fx* pDst, uint32_t dst_stride) const
{
fx t[16];
for (uint32_t v = 0; v < 4; ++v) // vertical
{
const fx col[4] = { pSrc[v], pSrc[src_stride + v],
pSrc[2 * src_stride + v], pSrc[3 * src_stride + v] };
fx out[4];
dct4::inverse_ortho(col, out);
for (uint32_t x = 0; x < 4; ++x)
t[x * 4 + v] = out[x];
}
for (uint32_t x = 0; x < 4; ++x) // horizontal
dct4::inverse_ortho(t + x * 4, pDst + x * dst_stride);
}
static fx table_entry(uint32_t u, uint32_t x, uint32_t n)
{
const int64_t a = u ? dct_detail::alpha_q30(n) : dct_detail::alpha0_q30(n);
const int64_t c = dct_detail::cos_pi_frac_q30((2u * x + 1u) * u, 2u * n);
return fx::from_raw(dct_detail::q60_to_q16(a * c));
}
#ifndef NDEBUG
static bool check_tables() // FNV-1a, same constant the offline test bakes
{
uint64_t h = 1469598103934665603ull;
for (uint32_t r = 2; r <= cMaxSize; r++)
for (uint32_t c = 2; c <= cMaxSize; c++)
for (int pass = 0; pass < 2; pass++) {
const uint32_t n = pass ? c : r;
for (uint32_t u = 0; u < n; ++u)
for (uint32_t x = 0; x < n; ++x) {
const uint32_t raw = uint32_t(table_entry(u, x, n).v);
for (int b = 0; b < 4; b++) { h ^= uint8_t(raw >> (b * 8)); h *= 1099511628211ull; }
}
}
return h == 0x013A49075AF22067ull;
}
#endif
uint32_t m_rows, m_cols;
fxvec m_c_col; // alpha(u) * cos, [u*m_rows + x]
fxvec m_c_row; // alpha(v) * cos, [v*m_cols + y]
};
inline constexpr uint8_t g_zigzag4x4_xy[16][2] = // [index][X,Y]
{
{ 0, 0 },
{ 1, 0 },
{ 0, 1 },
{ 0, 2 },
{ 1, 1 },
{ 2, 0 },
{ 3, 0 },
{ 2, 1 },
{ 1, 2 },
{ 0, 3 },
{ 1, 3 },
{ 2, 2 },
{ 3, 1 },
{ 3, 2 },
{ 2, 3 },
{ 3, 3 }
};
inline constexpr fixed16_16 g_base_4x4_quant[16] =
{
fixed16_16::from_float_and_raw(1.0f, 65536), fixed16_16::from_float_and_raw(3.5f, 229376), fixed16_16::from_float_and_raw(24.0f, 1572864), fixed16_16::from_float_and_raw(51.0f, 3342336),
fixed16_16::from_float_and_raw(3.5f, 229376), fixed16_16::from_float_and_raw(12.0f, 786432), fixed16_16::from_float_and_raw(40.0f, 2621440), fixed16_16::from_float_and_raw(78.0f, 5111808),
fixed16_16::from_float_and_raw(24.0f, 1572864), fixed16_16::from_float_and_raw(40.0f, 2621440), fixed16_16::from_float_and_raw(68.0f, 4456448), fixed16_16::from_float_and_raw(103.0f, 6750208),
fixed16_16::from_float_and_raw(51.0f, 3342336), fixed16_16::from_float_and_raw(78.0f, 5111808), fixed16_16::from_float_and_raw(103.0f, 6750208), fixed16_16::from_float_and_raw(120.0f, 7864320)
};
static inline void compute_quant_table_fixed(fixed16_16 q, fixed16_16 level_scale, int* dct_quant_tab)
{
const uint32_t grid_width = 4, grid_height = 4;
assert(q > fixed16_16());
dct_quant_tab[0] = 1;
if (q >= fixed16_16::from_int(100))
{
for (uint32_t y = 0; y < grid_height; y++)
{
for (uint32_t x = 0; x < grid_width; x++)
{
if (x || y)
{
dct_quant_tab[x + y * grid_width] = 1;
}
}
}
return;
}
for (uint32_t y = 0; y < grid_height; y++)
{
for (uint32_t x = y ? y : 1; x < grid_width; x++)
{
assert(x || y);
fixed16_16 base = g_base_4x4_quant[x + y * 4];
//int quant_scale = (base * level_scale).round_to_int();
int quant_scale = base.mul_round_to_int(level_scale);
quant_scale = basisu::maximum<int>(1, quant_scale);
if ((x + y) == 1)
{
const int MAX_QUANT_SCALE_AC_1_1 = 73; // 73
quant_scale = minimum(quant_scale, MAX_QUANT_SCALE_AC_1_1);
}
dct_quant_tab[x + y * grid_width] = quant_scale;
dct_quant_tab[y + x * grid_width] = quant_scale;
} // x
} // y
}
struct coeff
{
int16_t m_num_zeros; // number of zero AC coefficients before this one
int16_t m_coeff; // both sign and mag, [-255,255], or INT16_MAX if last
void clear()
{
m_num_zeros = 0;
m_coeff = 0;
}
};
typedef basisu::vector<coeff> coeff_vec;
struct dct_syms
{
int16_t m_dc; // [-255,255]
coeff_vec m_ac_vals;
void clear()
{
m_dc = 0;
m_ac_vals.resize(0);
}
};
// ---- standalone (de)serialization of dct_syms <-> a flat 4x4 quantized-coeff
// grid, used ONLY by the optional AC-truncation RDO. forward()/inverse() are
// left untouched; these mirror their exact run-length format so a re-packed
// array is always canonical & correctly terminated. Natural index = x + y*4.
// Unpack a (valid) dct_syms AC run-length list into flat[16] (DC at [0]).
static inline void xbc7_syms_to_flat(const dct_syms& syms, int flat[16])
{
for (uint32_t i = 0; i < 16; i++)
flat[i] = 0;
flat[0] = syms.m_dc;
uint32_t zig_idx = 1;
for (uint32_t i = 0; i < syms.m_ac_vals.size(); i++)
{
zig_idx += (uint32_t)syms.m_ac_vals[i].m_num_zeros;
if (zig_idx >= 16)
break; // EOB / end: remaining slots stay zero
if (syms.m_ac_vals[i].m_coeff == INT16_MAX)
break; // defensive (shouldn't occur with zig_idx < 16)
flat[g_zigzag4x4_xy[zig_idx][0] + g_zigzag4x4_xy[zig_idx][1] * 4] = syms.m_ac_vals[i].m_coeff;
zig_idx++;
}
}
// Re-pack flat[16] into a canonical dct_syms (DC + RLE ACs + trailing EOB),
// identical in form to forward()'s emission.
static inline void xbc7_flat_to_syms(const int flat[16], dct_syms& syms)
{
syms.clear();
syms.m_dc = basisu::safe_cast_int16(flat[0]);
int total_zeros = 0;
for (uint32_t i = 1; i < 16; i++)
{
const int ac = flat[g_zigzag4x4_xy[i][0] + g_zigzag4x4_xy[i][1] * 4];
if (!ac)
{
total_zeros++;
continue;
}
coeff cf;
cf.m_num_zeros = basisu::safe_cast_int16(total_zeros);
cf.m_coeff = basisu::safe_cast_int16(ac);
syms.m_ac_vals.push_back(cf);
total_zeros = 0;
}
if (total_zeros)
{
coeff cf;
cf.m_num_zeros = basisu::safe_cast_int16(total_zeros);
cf.m_coeff = INT16_MAX;
syms.m_ac_vals.push_back(cf);
}
}
inline constexpr fixed16_16 DEADZONE_ALPHA_FIXED = fixed16_16::from_float_and_raw(0.5f, 32768);
inline constexpr fixed16_16 g_scale_quant_steps_fixed[3] =
{
fixed16_16::from_float_and_raw(1.35588217f, 88859), // 4 (2-bits)
fixed16_16::from_float_and_raw(1.24573100f, 81640), // 8 (3-bits)
fixed16_16::from_float_and_raw(1.15431654f, 75649), // 16 (4-bits)
};
static inline uint32_t get_weight_size_index_from_bits(uint32_t num_weight_bits)
{
switch (num_weight_bits)
{
case 2: return 0;
case 3: return 1;
case 4: return 2;
default:
assert(0);
return 0;
}
}
// When true, weight-grid DC coefficients are uniformly quantized by
// XBC7_DC_QUANT (6-bit magnitude + sign instead of 8). The orthonormal
// 4x4 DC spans [-256, 256] but the weights themselves only span [0, 64],
// so a step of 4 costs at most +-2 DC == +-0.5 of one weight step spread
// across the whole block -- visually negligible, while the DC stream is
// one of the largest in the file. NOTE: format-affecting and not (yet)
// signalled in the stream: encoder and decoder must be built alike.
inline bool g_xbc7_quantize_dc = true;
inline constexpr int XBC7_DC_QUANT = 4;
// When ALSO true, the DC precision scales with the plane's weight depth:
// (weight_bits + 2) magnitude bits, i.e. quant step 2^(6 - weight_bits)
// (2-bit: 16, 3-bit: 8, 4-bit: 4). Rationale: an n-bit plane's
// reconstruction snaps to a 2^n-level grid (step ~64/(2^n - 1) in [0,64]
// space), so coarse planes tolerate proportionally coarser DC. Same
// build-alike caveat as above.
static bool g_xbc7_dc_quant_per_weight_bits = true;
static inline int get_xbc7_dc_quant(uint32_t num_weight_bits)
{
if (!g_xbc7_dc_quant_per_weight_bits)
return XBC7_DC_QUANT;
assert((num_weight_bits >= 2) && (num_weight_bits <= 4));
return 1 << (6 - num_weight_bits);
}
class xbc7_weight_grid_dct_fixed
{
public:
typedef basist::fixed16_16 fx;
xbc7_weight_grid_dct_fixed()
{}
void init()
{
m_dct.init(BLOCK_HEIGHT, BLOCK_WIDTH);
}
void forward(
fx global_q, uint32_t plane_index,
const int* pWeight_predictions, // may be nullptr
const basist::bc7u::log_bc7_block& log_blk,
dct_syms& syms,
fxvec& dct_work)
{
syms.clear();
fx orig_weights[16];
for (uint32_t i = 0; i < 16; i++)
{
const int predicted_weight = pWeight_predictions ? pWeight_predictions[i] : 0;
assert((predicted_weight >= 0) && (predicted_weight <= 64));
orig_weights[i] = fx::from_int(
basist::bc7u::dequant_weight(log_blk.m_weights[plane_index][i], log_blk.m_weight_bits[plane_index]) - predicted_weight);
}
fx dct_weights[16];
m_dct.forward(orig_weights, dct_weights, dct_work);
const fx span_len = get_max_span_len(log_blk, plane_index);
const fx level_scale = compute_level_scale(global_q, span_len, log_blk.m_weight_bits[plane_index]);
int dct_quant_tab[16];
compute_quant_table_fixed(global_q, level_scale, dct_quant_tab);
int dct_coeffs[16];
for (uint32_t y = 0; y < 4; y++)
{
for (uint32_t x = 0; x < 4; x++)
{
if (!x && !y)
{
int dc = basisu::clamp<int>(dct_weights[0].round_to_int(), -255, 255);
if (g_xbc7_quantize_dc)
{
// plain uniform quantizer (no deadzone), round half
// away from zero, mirrored by inverse(). The full
// quantized range [-256/q, 256/q] fits a magnitude
// byte at every weight depth, so no clipping at the
// extremes (the old -1 was a byte-range vestige).
const int q = get_xbc7_dc_quant(log_blk.m_weight_bits[plane_index]);
const int max_mag = 256 / q;
dc = (dc >= 0) ? ((dc + (q / 2)) / q) : -(((-dc) + (q / 2)) / q);
dc = basisu::clamp<int>(dc, -max_mag, max_mag);
}
dct_coeffs[0] = dc;
continue;
}
const int levels = dct_quant_tab[x + y * 4];
const fx d = dct_weights[x + y * 4];
const int id = quantize_deadzone(d, levels, DEADZONE_ALPHA_FIXED, x, y);
dct_coeffs[x + y * 4] = basisu::clamp<int>(id, -255, 255); // clamping to [-255,255] not 256
} // x
} // y
syms.m_dc = basisu::safe_cast_int16(dct_coeffs[0]);
syms.m_ac_vals.reserve(17);
int total_zeros = 0;
for (uint32_t i = 1; i < 16; i++)
{
const uint32_t dct_idx = g_zigzag4x4_xy[i][0] + (g_zigzag4x4_xy[i][1] * 4);
assert(dct_idx);
int ac_coeff = dct_coeffs[dct_idx];
if (!ac_coeff)
{
total_zeros++;
continue;
}
coeff cf;
cf.m_num_zeros = basisu::safe_cast_int16(total_zeros);
cf.m_coeff = basisu::safe_cast_int16(ac_coeff);
syms.m_ac_vals.push_back(cf);
total_zeros = 0;
}
if (total_zeros)
{
coeff cf;
cf.m_num_zeros = basisu::safe_cast_int16(total_zeros);
cf.m_coeff = INT16_MAX;
syms.m_ac_vals.push_back(cf);
}
}
bool inverse(
fx global_q, uint32_t plane_index,
const int* pWeight_predictions, // may be nullptr
const dct_syms& syms,
basist::bc7u::log_bc7_block& log_blk,
fxvec& dct_work)
{
const fx span_len = get_max_span_len(log_blk, plane_index);
const fx level_scale = compute_level_scale(global_q, span_len, log_blk.m_weight_bits[plane_index]);
int dct_quant_tab[16];
compute_quant_table_fixed(global_q, level_scale, dct_quant_tab);
fx dct_weights[16];
for (uint32_t i = 0; i < 16; i++)
dct_weights[i] = fx();
// hostile streams can carry any byte here; *16 worst case (255*16 =
// 4080) is still inside the 4x4 IDCT's safe input range (~8191 at
// gain 4), so the decoder remains total
dct_weights[0] = fx::from_int(g_xbc7_quantize_dc ?
((int)syms.m_dc * get_xbc7_dc_quant(log_blk.m_weight_bits[plane_index])) : (int)syms.m_dc);
uint32_t zig_idx = 1;
uint32_t coeff_ofs = 0;
while (coeff_ofs < syms.m_ac_vals.size())
{
const uint32_t run_len = syms.m_ac_vals[coeff_ofs].m_num_zeros;
const int coeff = syms.m_ac_vals[coeff_ofs].m_coeff;
coeff_ofs++;
if ((run_len + zig_idx) > 16)
return false;
zig_idx += run_len;
if (zig_idx >= 16)
break;
// INT16_MAX is impossible in a valid stream. The float class
// asserts here; a deterministic decoder must instead behave
// IDENTICALLY on every input in every build (debug included),
// so malformed streams are rejected, never trapped.
if (coeff == INT16_MAX)
return false;
const int x = g_zigzag4x4_xy[zig_idx][0];
const int y = g_zigzag4x4_xy[zig_idx][1];
const int dct_idx = x + (y * 4);
const int quant = dct_quant_tab[dct_idx];
dct_weights[dct_idx] = dequant_deadzone(coeff, quant, DEADZONE_ALPHA_FIXED, x, y);
zig_idx++;
}
fx idct_weights[16];
m_dct.inverse(dct_weights, idct_weights, dct_work);
for (uint32_t i = 0; i < 16; i++)
{
const int pred = pWeight_predictions ? pWeight_predictions[i] : 0;
log_blk.m_weights[plane_index][i] = basist::bc7u::quant_weight(
basisu::clamp<int>((idct_weights[i] + fx::from_int(pred)).round_to_int(), 0, 64),
log_blk.m_weight_bits[plane_index]);
}
return true;
}
private:
static const uint32_t BLOCK_WIDTH = 4;
static const uint32_t BLOCK_HEIGHT = 4;
dct2fx m_dct;
// sqrt of a plain non-negative integer, result Q15.16, round-to-nearest.
// Bypasses fixed::sqrt because the input (sum of squares, <= 260100)
// exceeds the Q15.16 VALUE range; only the result (<= 510) must fit.
static fx isqrt_to_fixed(uint32_t ssq)
{
const uint64_t x = uint64_t(ssq) << 32;
uint64_t f = basist::fixed_detail::isqrt_floor(x);
f += (x - f * f > f); // round to nearest
return fx::from_raw((int32_t)f);
}
// Needed by AQ. Endpoint values are 8-bit ints: accumulate the sum of
// squares EXACTLY in integer math, one deterministic sqrt at the end.
fx get_max_span_len(const basist::bc7u::log_bc7_block& log_blk, uint32_t plane_index) const
{
uint32_t max_ssq = 0;
if (log_blk.is_dual_plane())
{
basist::color_rgba ep[2];
basist::bc7u::unpack_endpoints(log_blk, ep, 0);
const basist::color_rgba& l = ep[0];
const basist::color_rgba& h = ep[1];
for (uint32_t c = 0; c < 4; c++)
{
// get the weight plane used by this endpoint channel (NOT the decoded
// pixel channel, which is after any mode 4/5 channel swapping/rotation)
const uint32_t endpoint_chan_plane = log_blk.get_endpoint_channel_weight_plane(c);
if (endpoint_chan_plane == plane_index)
{
const int d = (int)h[c] - (int)l[c];
max_ssq += (uint32_t)(d * d);
}
}
}
else
{
assert(!plane_index);
for (uint32_t i = 0; i < log_blk.m_num_partitions; i++)
{
basist::color_rgba ep[2];
basist::bc7u::unpack_endpoints(log_blk, ep, i);
const basist::color_rgba& l = ep[0];
const basist::color_rgba& h = ep[1];
uint32_t ssq = 0;
for (uint32_t c = 0; c < 4; c++)
{
const int d = (int)h[c] - (int)l[c];
ssq += (uint32_t)(d * d);
}
// sqrt is monotonic: max of the roots == root of the max
max_ssq = basisu::maximum(max_ssq, ssq);
}
}
return isqrt_to_fixed(max_ssq);
}
// Adaptive quantization (all-integer port of the float version; the
// comments there still apply)
fx compute_level_scale(fx q, fx span_len, uint32_t num_weight_bits) const
{
const uint32_t weight_size_index = get_weight_size_index_from_bits(num_weight_bits);
// Standard JPEG quality factor calcs
q = basisu::clamp(q, fx::from_int(1), fx::from_int(100));
fx level_scale = (q < fx::from_int(50))
? fx::from_int(5000) / q
: fx::from_int(200) - q * 2;
level_scale = level_scale / 100; // because JPEG's quant table is scaled by 100
const fx span_floor = fx::from_int(14);
fx adaptive_factor = fx::from_int(64) / basisu::maximum(span_len, span_floor);
adaptive_factor = adaptive_factor * g_scale_quant_steps_fixed[weight_size_index];
return level_scale * adaptive_factor;
}
int quantize_deadzone(fx d, int L, fx alpha, uint32_t x, uint32_t y) const
{
assert((x < BLOCK_WIDTH) && (y < BLOCK_HEIGHT));
if (((x == 1) && (y == 0)) ||
((x == 0) && (y == 1)))
{
return (d / L).round_to_int();
}
if (L <= 0)
return 0;
const fx s = d.abs();
const fx tau = alpha * L; // half-width of the zero band
if (s <= tau)
return 0; // inside dead-zone
// s > tau, so the quotient is positive: round_to_int (half away)
// equals the float version's floor(qf + 0.5) here
const int q = ((s - tau) / L).round_to_int();
return (d < fx()) ? -q : q;
}
// int64 + saturation: hostile/corrupt syms (huge |q|) times a low-quality
// L can exceed Q15.16, and a DECODER must be total -- no trap, wrap, or
// assert on ANY input, debug builds included. Valid encoder streams can
// never produce a dequantized coefficient beyond ~768 (a nonzero coeff
// requires |d| > tau, bounding tau + |q|*L by ~|d| + L/2 <= ~512), so
// saturating at +-2048 is invisible to legal bitstreams while keeping
// every IDCT intermediate (gain <= 4) safely inside Q15.16.
static fx sat_raw(int64_t raw)
{
const int64_t lim = int64_t(2048) * fx::ONE;
return fx::from_raw((int32_t)basisu::clamp<int64_t>(raw, -lim, lim));
}
fx dequant_deadzone(int q, int L, fx alpha, uint32_t x, uint32_t y) const
{
assert((x < BLOCK_WIDTH) && (y < BLOCK_HEIGHT));
if (((x == 1) && (y == 0)) ||
((x == 0) && (y == 1)))
{
return sat_raw((int64_t)q * L * fx::ONE);
}
if (q == 0 || L <= 0)
return fx();
const int64_t aq = (q < 0) ? -(int64_t)q : (int64_t)q;
// center of the (nonzero) bin: tau + |q|*L, computed wide
const int64_t mag_raw = (int64_t)alpha.v * L + aq * L * fx::ONE;
return (q < 0) ? sat_raw(-mag_raw) : sat_raw(mag_raw);
}
};
// KISS tagged-blob container for the XBC7 Zstd profile.
//
// Encoder: blob_stream_writer -- append bytes to blobs by ID, then
// serialize() everything to one uint8_vec. Each blob is Zstd compressed,
// UNLESS that doesn't shrink it, in which case it's stored raw (this handles
// "AC sign bits are noise" automatically -- no caller flags needed).
//
// Decoder: blob_stream_reader -- point it at the serialized bytes, init()
// scans the directory, validates everything, and decompresses all compressed
// blobs into ONE arena allocation (raw blobs are zero-copy pointers into the
// input). Queries are then O(1) table lookups. Total decoder allocations:
// exactly one (zero if nothing was compressed).
//
// Serialized format (deterministic across platforms; sizes are LEB128
// varints -- 7 bits per byte, high bit = continue, max 5 bytes for uint32):
// [uint8 0xB7] (begin marker)
// [uint8 num_blobs] (only non-empty blobs are stored)
// repeated num_blobs times:
// [uint8 id_and_flag] (low 7 bits = blob id, so ids must be
// < 128; high bit set == Zstd
// compressed, clear == stored RAW)
// if RAW:
// [varint size] (never 0; size raw bytes follow)
// if COMPRESSED:
// [varint uncompressed_size] (never 0)
// [varint stored_size] (never 0, strictly < uncompressed;
// stored_size bytes follow)
// [blob data]
// [uint8 0x6A] (end marker; must land exactly at the
// final byte -- trailing garbage and
// truncation both fail validation)
// Per-blob overhead: typically 3 bytes raw / 5 bytes compressed (sizes under
// 16KB take 2 varint bytes). ~20 blobs -> ~80 bytes per mipmap level.
//
// IMPORTANT (decoder): raw blobs alias the input buffer. The serialized data
// passed to init() must outlive the reader.
inline constexpr uint32_t BLOB_STREAM_MAX_IDS = 128; // low 7 bits of the entry byte
inline constexpr uint8_t BLOB_STREAM_MAGIC_BEGIN = 0xB7;
inline constexpr uint8_t BLOB_STREAM_MAGIC_END = 0x6A;
static inline uint32_t index_from_xy(uint32_t x, uint32_t y) { assert((x < 4) && (y < 4)); return x + y * 4; }
// XBC7 weight predictor candidates. The old plain-copy candidates
// (left/up/left-diag/right-diag) are subsumed by the generic XY-delta block
// references at the end of the enum.
enum xbc7_cand_t : uint32_t
{
cCandAbsolute = 0, // no prediction (residual == signal)
// synthetic predictors
cCandLeftEdge, // left block's right edge replicated
cCandUpperEdge, // upper block's bottom edge replicated
cCandLUBlend, // left+upper edge distance blend
cCandReflectLeft, // left block mirrored about the shared edge
cCandReflectUpper, // upper block mirrored about the shared edge
cCandLUAvg, // left+upper edge simple average
cCandLUBlendStrong, // left+upper edge squared-distance blend
cCandGradient, // L + U - C plane gradient
cCandGradientDamped, // gradient blended with cCandLUBlend
cCandDiagAvg, // upper-left/upper-right block average
cCandDiagEdgeBlend, // upper-left right edge <-> upper-right left edge
cCandUpperDiagEdgeBlend, // upper edge blended with diagonal lateral structure
cCandMED, // JPEG-LS median edge detector
cCandGAB, // gradient-adaptive blend (CALIC-spirit)
cCandPlaneFit, // LS plane fit through left+upper edges
cCandDDL, // 45-degree diagonal-down-left propagation
cCandDDR, // 45-degree diagonal-down-right propagation
// generic causal block references (copies); amp codes apply as usual
cCandFirstXYDelta,
cCandLastXYDelta = cCandFirstXYDelta + 31,
cTotalCandidates
};
// Causal block reference deltas (same layout as astc_hdr_6x6::g_reuse_xy_deltas).
// All entries are causal by construction: dy < 0, or dy == 0 and dx < 0.
struct xbc7_xy_delta { int8_t m_dx, m_dy; };
inline constexpr uint32_t NUM_XY_DELTAS = 32;
inline constexpr xbc7_xy_delta g_xbc7_xy_deltas[NUM_XY_DELTAS] =
{
{ -1, 0 }, { -2, 0 }, { -3, 0 }, { -4, 0 },
{ 3, -1 }, { 2, -1 }, { 1, -1 }, { 0, -1 }, { -1, -1 }, { -2, -1 }, { -3, -1 }, { -4, -1 },
{ 3, -2 }, { 2, -2 }, { 1, -2 }, { 0, -2 }, { -1, -2 }, { -2, -2 }, { -3, -2 }, { -4, -2 },
{ 3, -3 }, { 2, -3 }, { 1, -3 }, { 0, -3 }, { -1, -3 }, { -2, -3 }, { -3, -3 }, { -4, -3 },
{ 3, -4 }, { 2, -4 }, { 1, -4 }, { 0, -4 }
};
inline constexpr uint32_t XBC7_FLAG_HAS_ALPHA = 1;
#pragma pack(push, 1)
struct xbc7_header
{
basisu::packed_uint<2> m_width_in_texels;
basisu::packed_uint<2> m_height_in_texels;
uint8_t m_dct_q;
uint8_t m_flags;
// Encoder stripe count (>= 1). The decoder needs it because the
// solid-block prediction is IMPLICIT (derived from neighbors on both
// sides), so its upper-neighbor clamp at stripe seams must mirror the
// encoder's. All EXPLICIT references remain valid anywhere causal.
uint8_t m_num_stripes;
};
#pragma pack(pop)
enum xbc7_blob_id : uint8_t
{
// File-level metadata: dims, version, global Q, flags. Always first
// logically; readers locate it by ID, not position.
cBlobHeader = 0,
// One command byte per block, raster order. Drives all other streams.
cBlobCommands = 1,
// Config bytes (CMD = new-config only): mode in bits 0-2, mode 4/5
// component rotation in bits 3-4, mode 4 index selector in bit 5,
// bits 6-7 reserved (writer zeros; decoder rejects nonzero).
cBlobBC7BlockConfig = 2,
// Partition indices, one byte each, split by subset count because the
// two tables are disjoint vocabularies (same byte value = unrelated
// geometry). Present only when the just-parsed mode is partitioned.
cBlobPartition2 = 3, // modes 1, 3, 7 (64 patterns)
cBlobPartition3 = 4, // modes 0, 2 (mode 0: index < 16, else reject)
// Joint (candidate, amp code) predictor byte, one per full-block
// command (CMD = new-config/reuse-config), consumed by BOTH weight
// modes (DPCM and DCT).
// value = cand_index + amp_code * cTotalCandidates; >= 200 rejects.
cBlobWeightPredictors = 5,
// DC values, one per coded plane of every WT = DCT block (dual-plane
// = two). Lattice-coded magnitude; sign is conditional (absolute-
// predictor planes are unsigned by construction -- no sign emitted).
cBlobDCCoeffsSmall = 6, // 2/3-bit weight modes
cBlobDCCoeffsLarge = 7, // 4-bit weight modes
cBlobACCoeffs = 8,
// Bit-packed raw sign bits: AC signs, plus DC signs where present.
cBlobCoeffSigns = 9,
// Bit-packed endpoint p-bit RESIDUALS for the DPCM endpoint modes
// (1 or 2 per subset per the mode's pbit shape), stored raw. The
// EP = raw escape path's p-bits travel in cBlobEPRaw instead. Own
// stream so the accounting can price them.
cBlobPBits = 10,
cBlobEPDeltaFineR = 11, // >= 6 bits
cBlobEPDeltaFineG = 12,
cBlobEPDeltaFineB = 13,
cBlobEPDeltaFineA = 14,
cBlobEPDeltaCoarseR = 15, // < 6 bits
cBlobEPDeltaCoarseG = 16,
cBlobEPDeltaCoarseB = 17,
cBlobEPDeltaCoarseA = 18,
// Raw endpoints (EP = raw escape)
cBlobEPRaw = 19,
// EP = indexed-DPCM block references: one byte per reference holding
// the 5-bit delta-table index (top 3 bits reserved-zero).
cBlobEPBlockIndex = 20,
// WT = DPCM with the absolute predictor: the plane's quantized weight
// indices, byte-packed (2-bit: 4 per byte LSB-first; 3-bit: expanded to
// nibbles, 2 per byte; 4-bit: 2 per byte). Each plane is a whole number
// of bytes, so planes never straddle a byte.
cBlobRawWeightBits = 21,
// CMD = solid: per-solid-block DPCM residual vs the neighbor edge
// prediction, one wrapped byte per channel in R,G,B,A order (3 bytes
// when the file has no alpha, 4 with alpha) in 8-bit PIXEL space --
// distinct domain and predictor from the endpoint deltas, so its own
// stream and FSE context. Interleaved rather than planar; revisit if
// UI-class corpora make this blob dominant.
cBlobSolidRGBADeltas = 22,
// WT = DPCM with a real predictor: wrapped n-bit weight index residuals,
// byte-packed exactly like cBlobRawWeightBits, split by bit width
// (disjoint vocabularies -- same byte value, unrelated statistics).
cBlobDPCMWeightResid2 = 23,
cBlobDPCMWeightResid3 = 24,
cBlobDPCMWeightResid4 = 25,
// Per-stripe seek table (present only when num_stripes > 1): 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 (coeff_signs, pbits, ep_raw). Lets the decoder seek each
// stripe directly and decode them independently (in parallel). Stored
// as little-endian packed_uint<4>, stripe-major, as DELTAS from the
// previous stripe's start (stripe 0's delta is always 0) -- the small
// non-monotonic values compress far better than absolute offsets. The
// decoder reconstructs absolute offsets with a running prefix sum.
cBlobStripeSeekTable = 26,
// 27..127 reserved for future streams (P-frame motion, temporal
// references, optional tables). IDs >= 128 are invalid (the blob
// container uses bit 7 as its compression flag).
//
// NOTE: IDs freeze permanently at the FIRST golden mint, not before.
cBlobFirstUnused = 27
};
enum class xbc7_command_id : uint8_t
{
cCmdRepeatLast = 0,
cCmdRepeatUpper = 1,
cCmdSolidDPCM = 2,
cCmdNewConfig = 3,
cCmdReuseConfigLeft = 4,
cCmdReuseConfigUpper = 5,
cCmdReuseConfigLeftDiagonal = 6,
cCmdReuseConfigRightDiagonal = 7
};
enum class xbc7_command_endpoint_mode : uint8_t
{
cCmdEndpointRaw = 0,
cCmdEndpointDPCMLeft = 1,
cCmdEndpointDPCMUp = 2,
cCmdEndpointDPCMLeftDiagonal = 3,
cCmdEndpointDPCMRightDiagonal = 4,
cCmdEndpointDPCMBlockIndex = 5,
// like Left/Up but predicting from the neighbor's SECOND subset --
// useful when a partitioned neighbor's other half matches better.
// The decoder REJECTS these when the referenced block has fewer than
// 2 subsets.
cCmdEndpointDPCMLeftSubset1 = 6,
cCmdEndpointDPCMUpSubset1 = 7
};
enum class xbc7_command_weight_mode : uint8_t
{
cCmdWeightRaw = 0,
cCmdWeightDCT = 1
};
inline constexpr uint32_t XBC7_COMMAND_ENDPOINT_MODE_SHIFT = 3;
inline constexpr uint32_t XBC7_COMMAND_WEIGHT_MODE_SHIFT = 6;
// Format-level max stripe count. The decoder REJECTS stripe counts above
// this, and the encoder clamps to it, so it's shared by both sides; raising
// it later is a format-affecting change. (The encoder-only sizing thresholds
// XBC7_MIN_IMAGE_TEXEL_ROWS_TO_STRIPE / XBC7_MIN_STRIPE_BLOCK_ROWS stay in
// basisu_xbc7_encode.cpp.)
inline constexpr uint32_t XBC7_MAX_ENCODER_STRIPES = 16;
struct stripe_range
{
uint32_t m_first_block_row = 0;
uint32_t m_num_block_rows = 0;
};
// Inclusive 2D bounding box (in BC7 logical block coords) that a coding
// unit may reference. Generalizes the stripe row-clamp: EVERY causal
// predictor access -- neighbor/diagonal blocks, the XY-delta block
// references, and the weight predictor bank -- is gated through
// contains(), so the encoder can never read a block outside its tile.
// Initially each tile is a full-width stripe { 0, first_row,
// num_blocks_x-1, last_row }, so the AABB test is identical to the old
// row clamp and the emitted bytes don't change; later, narrower tiles
// enable 2D-parallel encode. The decoder passes a whole-image tile, so
// it stays fully permissive and decoding is unaffected for now.
struct tile_bounds
{
int m_bx0 = 0, m_by0 = 0, m_bx1 = 0, m_by1 = 0; // inclusive
bool contains(int bx, int by) const
{
return (bx >= m_bx0) && (bx <= m_bx1) && (by >= m_by0) && (by <= m_by1);
}
};
// Splits num_blocks_y rows as evenly as possible into num_stripes
// contiguous ranges (the first num_blocks_y % num_stripes stripes carry
// one extra row). Shared by the encoder AND the decoder: the decoder
// rebuilds the same geometry from the header's stripe count, because the
// solid-block prediction is implicit and must clamp identically on both
// sides.
[[maybe_unused]] static void compute_stripe_ranges(uint32_t num_blocks_y, uint32_t num_stripes, basisu::vector<stripe_range>& stripes)
{
assert((num_stripes >= 1) && (num_stripes <= num_blocks_y));
stripes.resize(num_stripes);
const uint32_t base_rows = num_blocks_y / num_stripes;
const uint32_t extra_rows = num_blocks_y % num_stripes;
uint32_t cur_row = 0;
for (uint32_t i = 0; i < num_stripes; i++)
{
stripes[i].m_first_block_row = cur_row;
stripes[i].m_num_block_rows = base_rows + ((i < extra_rows) ? 1 : 0);
cur_row += stripes[i].m_num_block_rows;
}
assert(cur_row == num_blocks_y);
}
// eval_weight_predictor: reconstruct the 16 weight predictions for predictor
// (cand_index, amp_code) at block (bx,by). SHARED by the encoder (predictor
// search) and the decoder (reconstruction); DEFINED in
// basisu_xbc7_decode.cpp so both sides link a single copy. Returns false for
// an invalid / out-of-tile candidate.
bool eval_weight_predictor(
uint32_t cand_index, uint32_t amp_code,
uint32_t bx, uint32_t by, uint32_t num_blocks_x,
const tile_bounds& tile,
const vector2D<basist::bc7u::log_bc7_block>& log_blks,
uint32_t p, int pOut_preds[16]);
// ----------------------------- decoder API -----------------------------
// Bounds-checked read-only byte view that the decoder takes as input (the
// decoder no longer accepts a uint8_vec -- it's a low-level API over a
// pointer+size). Unlike std::span it TRAPS on any access outside [0,size):
// assert() in debug, and a safe sentinel (the first byte, or 0 if empty) in
// all builds -- so a malformed stream or a decoder bug degrades to a
// controlled, reproducible value instead of reading bogus memory or crashing.
// The viewed buffer must outlive every decode call that uses the span.
struct byte_span
{
const uint8_t* m_p = nullptr;
size_t m_size = 0;
byte_span() = default;
byte_span(const uint8_t* p, size_t size) : m_p(p), m_size(size) {}
// convenience for callers that hold a uint8_vec (e.g. basisu_tool)
byte_span(const uint8_vec& v) : m_p(v.data()), m_size(v.size()) {}
const uint8_t* data() const { return m_p; }
size_t size() const { return m_size; }
bool empty() const { return m_size == 0; }
uint8_t first() const { return m_size ? m_p[0] : 0; } // safe sentinel
// Checked single-byte read. Out of range -> assert + first byte.
uint8_t operator[](size_t i) const
{
if (i < m_size)
return m_p[i];
assert(!"byte_span: index out of range");
return first();
}
// Checked pointer to the [offset, offset+len) region, so a caller can
// read a whole run directly (no per-byte overhead). Out of range (either
// end) -> assert + m_p clamped to the start, so the read stays inside the
// buffer rather than walking off it. (offset==m_size, len==0 is valid.)
const uint8_t* checked_ptr(size_t offset, size_t len) const
{
if ((offset <= m_size) && (len <= m_size - offset)) // len<=m_size-offset avoids overflow
return m_p + offset;
assert(!"byte_span: region out of bounds");
return m_p;
}
};
// Callback-streaming decoder (same shape as the transcoder's XUASTC LDR
// path). The decoder owns NO output image: it hands each decoded LOGICAL
// BC7 block to the caller, who decides what to do -- pack to physical BC7,
// store the logical block, compare vs a reference, transcode, etc. Context
// flows through the opaque pData pointer (so captureless lambdas work as
// callbacks with zero allocation).
//
// init: fired ONCE, after the header is parsed/validated and before any
// block, so the caller can validate geometry and allocate. (block dims are
// always 4x4 for BC7.) Return false to abort.
typedef bool (*decode_init_callback_ptr)(
uint32_t num_blocks_x, uint32_t num_blocks_y,
uint32_t width_in_texels, uint32_t height_in_texels,
uint32_t dct_q, bool has_alpha, void* pData);
// block: fired once per decoded block. In unpack_image_threaded() it may be
// invoked CONCURRENTLY from multiple worker threads, but always for DISTINCT
// (bx,by) blocks (each stripe is a disjoint block-row range), and never in
// global raster order. Return false to abort the decode.
typedef bool (*decode_block_callback_ptr)(
uint32_t bx, uint32_t by, const basist::bc7u::log_bc7_block& log_blk, void* pData);
// ------------------------------ decoder internals ------------------------------
// Tagged-blob reader (decoder side). Trivial queries are inline; the heavy
// init_internal() (Zstd) is defined in basisu_xbc7_decoder.inl.
class blob_stream_reader
{
public:
blob_stream_reader() { clear(); }
void clear() { memset(m_ptrs, 0, sizeof(m_ptrs)); memset(m_sizes, 0, sizeof(m_sizes)); m_arena.clear(); }
bool init(const void* pData, size_t data_size, uint64_t max_total_uncomp = 1ULL << 30)
{
if (!init_internal(pData, data_size, max_total_uncomp)) { clear(); return false; }
return true;
}
inline bool has(uint32_t id) const { return (id < BLOB_STREAM_MAX_IDS) && (m_sizes[id] != 0); }
inline uint32_t get_size(uint32_t id) const { return (id < BLOB_STREAM_MAX_IDS) ? m_sizes[id] : 0; }
inline const uint8_t* get_ptr(uint32_t id) const { return (id < BLOB_STREAM_MAX_IDS) ? m_ptrs[id] : nullptr; }
private:
bool init_internal(const void* pData, size_t data_size, uint64_t max_total_uncomp); // basisu_xbc7_decoder.inl
const uint8_t* m_ptrs[BLOB_STREAM_MAX_IDS];
uint32_t m_sizes[BLOB_STREAM_MAX_IDS];
uint8_vec m_arena; // the single decoder allocation
// bounds-checked LEB128; rejects encodings past 5 bytes / 32 bits
static inline bool read_varint(const uint8_t* pBytes, size_t data_size, uint64_t& ofs, uint32_t& result)
{
uint32_t v = 0;
for (uint32_t shift = 0; shift < 35; shift += 7)
{
if (ofs >= data_size) return false;
const uint8_t b = pBytes[ofs++];
if ((shift == 28) && (b > 0x0Fu)) return false;
v |= (uint32_t)(b & 0x7Fu) << shift;
if (!(b & 0x80u)) { result = v; return true; }
}
return false;
}
};
// Stateful XBC7 image decoder. init() does the one-time prep; each stripe is
// decoded by decode_stripe() (self-contained, safe to run concurrently across
// distinct stripes). Impl in basisu_xbc7_decoder.inl.
class image_unpacker
{
public:
bool init(const byte_span& comp,
decode_init_callback_ptr pInit_callback, void* pInit_callback_data,
decode_block_callback_ptr pBlock_callback, void* pBlock_callback_data);
uint32_t get_num_stripes() const { return m_num_stripes; }
bool decode_stripe(uint32_t stripe_index);
bool decode_all();
private:
bool init_tiny_mip(const byte_span& comp, bool has_alpha,
decode_init_callback_ptr pInit_callback, void* pInit_callback_data);
bool decode_tiny_mip();
bool m_initialized = false;
blob_stream_reader m_rdr;
uint32_t m_width = 0, m_height = 0, m_global_q = 0;
bool m_has_alpha = false;
uint32_t m_num_blocks_x = 0, m_num_blocks_y = 0, m_num_stripes = 0;
basisu::vector<stripe_range> m_stripes;
basisu::vector2D<uint64_t> m_seek;
vector2D<basist::bc7u::log_bc7_block> m_log_blks;
decode_block_callback_ptr m_block_cb = nullptr;
void* m_block_data = nullptr;
bool m_tiny_mip = false;
const uint8_t* m_tiny_blocks = nullptr;
};
// Caller-provided job spawner: replaces the encoder's job_pool so the threaded
// decode can live in the transcoder. spawn_job() schedules
// dec.decode_stripe(stripe_index) on the caller's own threads and must NOT
// block; the caller waits for all spawned jobs itself, then inspects results.
struct job_spawner
{
virtual ~job_spawner() {}
virtual void spawn_job(image_unpacker& dec, uint32_t stripe_index) = 0;
};
// ------------------------------- decoder API -------------------------------
// Single-threaded one-shot. Returns false on any malformed stream (total over
// hostile input). Either callback may be null. Requires zstd.
bool unpack_image(const byte_span& comp,
decode_init_callback_ptr pInit_callback, void* pInit_callback_data,
decode_block_callback_ptr pBlock_callback, void* pBlock_callback_data);
// Threaded: caller owns `dec` (must outlive the spawned jobs). init + spawn one
// job per stripe via the spawner; returns false only on init failure and does
// NOT wait. Caller waits on its own pool, then inspects results.
bool unpack_image_threaded(image_unpacker& dec, const byte_span& comp, job_spawner& spawner,
decode_init_callback_ptr pInit_callback, void* pInit_callback_data,
decode_block_callback_ptr pBlock_callback, void* pBlock_callback_data);
} // namespace xbc7
} // namespace basist