mirror of
https://github.com/BinomialLLC/basis_universal.git
synced 2026-07-12 08:58:52 +00:00
1468 lines
50 KiB
C++
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
|