new files
diff --git a/transcoder/basisu_xbc7_decoder.h b/transcoder/basisu_xbc7_decoder.h
new file mode 100644
index 0000000..2136199
--- /dev/null
+++ b/transcoder/basisu_xbc7_decoder.h
@@ -0,0 +1,1462 @@
+// 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, WT = DCT blocks only.
+		// 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-bits (1 or 2 per subset per the mode's pbit
+		// shape), raw. 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, 4 interleaved wrapped bytes (G, R-G, B-G, A) 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
diff --git a/transcoder/basisu_xbc7_decoder.inl b/transcoder/basisu_xbc7_decoder.inl
new file mode 100644
index 0000000..08bac94
--- /dev/null
+++ b/transcoder/basisu_xbc7_decoder.inl
@@ -0,0 +1,1515 @@
+// File: basisu_xbc7_decoder.inl
+// XBC7 decoder implementation. #included at the END of basisu_transcoder.cpp
+// (the transcoder stays a single .cpp). Declarations live in
+// basisu_xbc7_decoder.h. Decoding REQUIRES zstd (BASISD_SUPPORT_KTX2_ZSTD); when
+// that is disabled the public entry points fail with an error.
+namespace basist {
+namespace xbc7 {
+
+	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, // every block reference is clamped to this AABB (encoder: the job's tile; decoder: whole image)
+		const vector2D<basist::bc7u::log_bc7_block>& log_blks,
+		uint32_t p, int pOut_preds[16])
+	{
+		BASISU_NOTE_UNUSED(num_blocks_x); // bounds now come from the tile
+		assert(cand_index != cCandAbsolute);
+		assert(cand_index < cTotalCandidates);
+		assert(amp_code < 4);
+		assert(tile.contains((int)bx, (int)by));
+
+		auto fetch_w = [](const basist::bc7u::log_bc7_block* pBlk, uint32_t plane, uint32_t w) -> int
+			{
+				const uint32_t sp = pBlk->is_dual_plane() ? plane : 0;
+				return basist::bc7u::dequant_weight(pBlk->m_weights[sp][w], pBlk->m_weight_bits[sp]);
+			};
+
+		const basist::bc7u::log_bc7_block* pLeft_diag_log_blk = tile.contains((int)bx - 1, (int)by - 1) ? &log_blks(bx - 1, by - 1) : nullptr;
+		const basist::bc7u::log_bc7_block* pRight_diag_log_blk = tile.contains((int)bx + 1, (int)by - 1) ? &log_blks(bx + 1, by - 1) : nullptr;
+		const basist::bc7u::log_bc7_block* pUp_log_blk = tile.contains((int)bx, (int)by - 1) ? &log_blks(bx, by - 1) : nullptr;
+		const basist::bc7u::log_bc7_block* pLeft_log_blk = tile.contains((int)bx - 1, (int)by) ? &log_blks(bx - 1, by) : nullptr;
+
+		const basist::bc7u::log_bc7_block* pCand_log_blk = nullptr;
+
+		if (cand_index == cCandAbsolute)
+		{
+
+		}
+		else if (cand_index >= cCandFirstXYDelta)
+		{
+			// generic causal block reference (copy); subsumes the old
+			// left/up/left-diag/right-diag copy candidates
+			const xbc7_xy_delta& delta = g_xbc7_xy_deltas[cand_index - cCandFirstXYDelta];
+			const int nx = (int)bx + delta.m_dx;
+			const int ny = (int)by + delta.m_dy;
+
+			if (tile.contains(nx, ny))
+				pCand_log_blk = &log_blks(nx, ny);
+
+			if (!pCand_log_blk)
+				return false;
+		}
+		else
+		{
+			if (cand_index == cCandLeftEdge)
+				pCand_log_blk = pLeft_log_blk; // left edge
+			else if (cand_index == cCandUpperEdge)
+				pCand_log_blk = pUp_log_blk; // upper edge
+			else if (cand_index == cCandLUBlend)
+				pCand_log_blk = (pLeft_log_blk && pUp_log_blk) ? pLeft_log_blk : nullptr; // left+upper edge blend
+			else if (cand_index == cCandReflectLeft)
+				pCand_log_blk = pLeft_log_blk; // reflect left
+			else if (cand_index == cCandReflectUpper)
+				pCand_log_blk = pUp_log_blk; // reflect upper
+			else if (cand_index == cCandLUAvg)
+				pCand_log_blk = (pLeft_log_blk && pUp_log_blk) ? pLeft_log_blk : nullptr; // left+upper edge avg
+			else if (cand_index == cCandLUBlendStrong)
+				pCand_log_blk = (pLeft_log_blk && pUp_log_blk) ? pLeft_log_blk : nullptr; // left+upper edge stronger distance blend
+			else if (cand_index == cCandGradient)
+				pCand_log_blk = (pLeft_log_blk && pUp_log_blk && pLeft_diag_log_blk) ? pLeft_log_blk : nullptr; // gradient
+			else if (cand_index == cCandGradientDamped)
+				pCand_log_blk = (pLeft_log_blk && pUp_log_blk && pLeft_diag_log_blk) ? pLeft_log_blk : nullptr; // damped gradient
+			else if (cand_index == cCandDiagAvg)
+				pCand_log_blk = (pLeft_diag_log_blk && pRight_diag_log_blk) ? pLeft_diag_log_blk : nullptr; // left/right diagonal avg
+			else if (cand_index == cCandDiagEdgeBlend)
+				pCand_log_blk = (pLeft_diag_log_blk && pRight_diag_log_blk) ? pLeft_diag_log_blk : nullptr; // diagonal edge blend
+			else if (cand_index == cCandUpperDiagEdgeBlend)
+				pCand_log_blk = (pUp_log_blk && pLeft_diag_log_blk && pRight_diag_log_blk) ? pLeft_diag_log_blk : nullptr; // upper + diagonal edge blend
+			else if ((cand_index == cCandMED) || (cand_index == cCandGAB))
+				pCand_log_blk = (pLeft_log_blk && pUp_log_blk && pLeft_diag_log_blk) ? pLeft_log_blk : nullptr; // MED / gradient-adaptive blend
+			else if (cand_index == cCandPlaneFit)
+				pCand_log_blk = (pLeft_log_blk && pUp_log_blk) ? pLeft_log_blk : nullptr; // LS plane fit
+			else if ((cand_index == cCandDDL) && pUp_log_blk && pRight_diag_log_blk)
+				pCand_log_blk = pUp_log_blk;
+			else if (cand_index == cCandDDR)
+				pCand_log_blk = (pLeft_log_blk && pUp_log_blk && pLeft_diag_log_blk) ? pLeft_log_blk : nullptr;
+
+			if (!pCand_log_blk)
+				return false;
+		}
+
+		int* pWeight_predictions = nullptr;
+
+		int weight_preds[16];
+		if (pCand_log_blk)
+		{
+			for (uint32_t w = 0; w < 16; w++)
+			{
+				weight_preds[w] = fetch_w(pCand_log_blk, p, w);
+			}
+
+			int orig_weight_preds[16];
+			memcpy(orig_weight_preds, weight_preds, sizeof(orig_weight_preds));
+
+			if (cand_index == cCandLeftEdge)
+			{
+				// left edge
+				for (uint32_t y = 0; y < 4; y++)
+					for (uint32_t x = 0; x < 4; x++)
+						weight_preds[index_from_xy(x, y)] = orig_weight_preds[index_from_xy(3, y)];
+			}
+			else if (cand_index == cCandUpperEdge)
+			{
+				// upper edge
+				for (uint32_t y = 0; y < 4; y++)
+					for (uint32_t x = 0; x < 4; x++)
+						weight_preds[index_from_xy(x, y)] = orig_weight_preds[index_from_xy(x, 3)];
+			}
+			else if ((cand_index == cCandLUBlend) || (cand_index == cCandLUAvg) || (cand_index == cCandLUBlendStrong))
+			{
+				// left+upper edge blend variants.
+				// pCand_log_blk is pLeft_log_blk here, so orig_weight_preds contains the left block.
+				// Pull upper edge directly from pUp_log_blk.
+
+				int upper_edge[4];
+
+				for (uint32_t x = 0; x < 4; x++)
+				{
+					const uint32_t w = index_from_xy(x, 3); // upper block's bottom edge
+
+					upper_edge[x] = fetch_w(pUp_log_blk, p, w);
+				}
+
+				for (uint32_t y = 0; y < 4; y++)
+				{
+					const int left_val = orig_weight_preds[index_from_xy(3, y)]; // left block's right edge
+
+					for (uint32_t x = 0; x < 4; x++)
+					{
+						const int upper_val = upper_edge[x];
+						int pred;
+
+						if (cand_index == cCandLUBlend)
+						{
+							// Existing distance-weighted blend.
+							const int wl = 4 - static_cast<int>(x); // 4,3,2,1
+							const int wu = 4 - static_cast<int>(y); // 4,3,2,1
+							const int den = wl + wu;
+
+							pred = (wl * left_val + wu * upper_val + (den >> 1)) / den;
+						}
+						else if (cand_index == cCandLUAvg)
+						{
+							// Simple average.
+							pred = (left_val + upper_val + 1) >> 1;
+						}
+						else // cCandLUBlendStrong
+						{
+							// Stronger distance weighting: trust the nearest edge more.
+							const int dx = 4 - static_cast<int>(x); // 4,3,2,1
+							const int dy = 4 - static_cast<int>(y); // 4,3,2,1
+							const int wl = dx * dx; // 16,9,4,1
+							const int wu = dy * dy; // 16,9,4,1
+							const int den = wl + wu;
+
+							pred = (wl * left_val + wu * upper_val + (den >> 1)) / den;
+						}
+
+						weight_preds[index_from_xy(x, y)] = pred;
+					}
+				}
+			}
+			else if (cand_index == cCandReflectLeft)
+			{
+				// reflect left
+				for (uint32_t y = 0; y < 4; y++)
+					for (uint32_t x = 0; x < 4; x++)
+						weight_preds[index_from_xy(x, y)] = orig_weight_preds[index_from_xy(3 - x, y)];
+			}
+			else if (cand_index == cCandReflectUpper)
+			{
+				// reflect upper
+				for (uint32_t y = 0; y < 4; y++)
+					for (uint32_t x = 0; x < 4; x++)
+						weight_preds[index_from_xy(x, y)] = orig_weight_preds[index_from_xy(x, 3 - y)];
+			}
+			else if ((cand_index == cCandGradient) || (cand_index == cCandGradientDamped) || (cand_index == cCandMED) || (cand_index == cCandGAB))
+			{
+				int upper_edge[4];
+
+				for (uint32_t x = 0; x < 4; x++)
+				{
+					const uint32_t w = index_from_xy(x, 3); // upper block's bottom edge
+
+					upper_edge[x] = fetch_w(pUp_log_blk, p, w);
+				}
+
+				const uint32_t corner_w = index_from_xy(3, 3); // upper-left block's bottom-right
+
+				int corner_val;
+				corner_val = fetch_w(pLeft_diag_log_blk, p, corner_w);
+
+				for (uint32_t y = 0; y < 4; y++)
+				{
+					const int left_val = orig_weight_preds[index_from_xy(3, y)]; // left block's right edge
+
+					for (uint32_t x = 0; x < 4; x++)
+					{
+						const int upper_val = upper_edge[x];
+
+						if (cand_index == cCandGradient)
+						{
+							int grad = left_val + upper_val - corner_val;
+							grad = basisu::clamp<int>(grad, 0, 64);
+
+							weight_preds[index_from_xy(x, y)] = grad;
+						}
+						else if (cand_index == cCandGradientDamped)
+						{
+							int grad = left_val + upper_val - corner_val;
+							grad = basisu::clamp<int>(grad, 0, 64);
+
+							// Damped gradient: blend gradient with the proven #7 predictor.
+							const int wl = 4 - static_cast<int>(x);
+							const int wu = 4 - static_cast<int>(y);
+							const int den = wl + wu;
+							const int blend7 = (wl * left_val + wu * upper_val + (den >> 1)) / den;
+
+							weight_preds[index_from_xy(x, y)] = (grad + blend7 + 1) >> 1;
+						}
+						else if (cand_index == cCandMED)
+						{
+							// MED (Median Edge Detector, JPEG-LS / LOCO-I).
+							// Plane predictor wrapped in a per-sample edge switch:
+							// if the corner is the local extreme, an edge passes
+							// between the neighbors -- predict from the neighbor on
+							// the current sample's side instead of extrapolating
+							// through the edge (which is #12's overshoot failure).
+							const int mn = basisu::minimum(left_val, upper_val);
+							const int mx = basisu::maximum(left_val, upper_val);
+
+							int pred;
+							if (corner_val >= mx)
+								pred = mn;
+							else if (corner_val <= mn)
+								pred = mx;
+							else
+								pred = left_val + upper_val - corner_val; // in (mn, mx) here, cannot overshoot
+
+							weight_preds[index_from_xy(x, y)] = basisu::clamp<int>(pred, 0, 64);
+						}
+						else // cCandGAB
+						{
+							// GAB: gradient-adaptive blend (CALIC-spirit, 3 samples).
+							// |left-corner| large => a horizontal edge crossed between
+							// the corner row and this row, so the left sample is on the
+							// current sample's side of it: trust it more. Symmetrically
+							// for |upper-corner| and vertical edges. A self-normalizing
+							// soft MED whose weights come from the data, not position.
+							const int wl = basisu::iabs(left_val - corner_val) + 1;
+							const int wu = basisu::iabs(upper_val - corner_val) + 1;
+							const int den = wl + wu;
+
+							const int pred = (wl * left_val + wu * upper_val + (den >> 1)) / den;
+
+							weight_preds[index_from_xy(x, y)] = pred; // convex blend of in-range values: already in [0,64]
+						}
+					}
+				}
+			}
+			else if (cand_index == cCandDiagAvg)
+			{
+				// Average upper-left and upper-right diagonal blocks.
+				// pCand_log_blk is pLeft_diag_log_blk here, so orig_weight_preds contains upper-left.
+				// Pull upper-right directly from pRight_diag_log_blk.
+
+				for (uint32_t w = 0; w < 16; w++)
+				{
+					int right_diag_val;
+
+					right_diag_val = fetch_w(pRight_diag_log_blk, p, w);
+
+					weight_preds[w] = (orig_weight_preds[w] + right_diag_val + 1) >> 1;
+				}
+			}
+			else if (cand_index == cCandDiagEdgeBlend)
+			{
+				// Blend upper-left block's right edge with upper-right block's left edge.
+				// pCand_log_blk is pLeft_diag_log_blk, so orig_weight_preds contains upper-left.
+				// Pull upper-right left edge directly from pRight_diag_log_blk.
+				//
+				// For each row y:
+				//   L = upper-left[3,y]
+				//   R = upper-right[0,y]
+				// Then interpolate across x.
+
+				int right_diag_left_edge[4];
+
+				for (uint32_t y = 0; y < 4; y++)
+				{
+					const uint32_t w = index_from_xy(0, y); // upper-right block's left edge
+
+					right_diag_left_edge[y] = fetch_w(pRight_diag_log_blk, p, w);
+				}
+
+				for (uint32_t y = 0; y < 4; y++)
+				{
+					const int left_val = orig_weight_preds[index_from_xy(3, y)]; // upper-left right edge
+					const int right_val = right_diag_left_edge[y];                // upper-right left edge
+
+					for (uint32_t x = 0; x < 4; x++)
+					{
+						// x=0 mostly left_val, x=3 mostly right_val.
+						// Use 4-sample interpolation: 3/0, 2/1, 1/2, 0/3.
+						const int pred = ((3 - static_cast<int>(x)) * left_val +
+							static_cast<int>(x) * right_val + 1) / 3;
+
+						weight_preds[index_from_xy(x, y)] = pred;
+					}
+				}
+			}
+			else if (cand_index == cCandUpperDiagEdgeBlend)
+			{
+				// Blend upper edge predictor with diagonal edge blend.
+				//
+				// upper_edge[x] = upper block's bottom edge
+				// diag_blend[x,y] = horizontal interpolation between:
+				//   upper-left block's right edge and upper-right block's left edge
+				//
+				// This combines direct top continuation with previous-row lateral structure.
+
+				int upper_edge[4];
+				int right_diag_left_edge[4];
+
+				for (uint32_t x = 0; x < 4; x++)
+				{
+					const uint32_t up_w = index_from_xy(x, 3); // upper block's bottom edge
+
+					upper_edge[x] = fetch_w(pUp_log_blk, p, up_w);
+				}
+
+				for (uint32_t y = 0; y < 4; y++)
+				{
+					const uint32_t rd_w = index_from_xy(0, y); // upper-right block's left edge
+
+					right_diag_left_edge[y] = fetch_w(pRight_diag_log_blk, p, rd_w);
+				}
+
+				for (uint32_t y = 0; y < 4; y++)
+				{
+					const int left_diag_right_val = orig_weight_preds[index_from_xy(3, y)]; // upper-left right edge
+					const int right_diag_left_val = right_diag_left_edge[y];
+
+					for (uint32_t x = 0; x < 4; x++)
+					{
+						// Same as #15: lateral predictor from upper-left/right diagonal edges.
+						const int diag_blend =
+							((3 - static_cast<int>(x)) * left_diag_right_val +
+								static_cast<int>(x) * right_diag_left_val + 1) / 3;
+
+						// Same as #6: direct upper edge replicated downward.
+						const int up_val = upper_edge[x];
+
+						// Trust upper edge more near y=0, trust diagonal lateral structure more lower in the block.
+						const int wu = 4 - static_cast<int>(y); // 4,3,2,1
+						const int wd = 1 + static_cast<int>(y); // 1,2,3,4
+						const int den = wu + wd;                // always 5
+
+						weight_preds[index_from_xy(x, y)] =
+							(wu * up_val + wd * diag_blend + (den >> 1)) / den;
+					}
+				}
+			}
+			else if (cand_index == cCandPlaneFit)
+			{
+				// LS plane fit through left + upper edges.
+				// pCand_log_blk is pLeft_log_blk, so orig_weight_preds holds the left block.
+				int upper_edge[4];
+
+				for (uint32_t x = 0; x < 4; x++)
+				{
+					const uint32_t w = index_from_xy(x, 3); // upper block's bottom edge
+
+					upper_edge[x] = fetch_w(pUp_log_blk, p, w);
+				}
+
+				int left_edge[4];
+				for (uint32_t y = 0; y < 4; y++)
+					left_edge[y] = orig_weight_preds[index_from_xy(3, y)]; // left block's right edge
+
+				const int sum_u = upper_edge[0] + upper_edge[1] + upper_edge[2] + upper_edge[3];
+				const int sum_l = left_edge[0] + left_edge[1] + left_edge[2] + left_edge[3];
+
+				// LS slopes * 10
+				const int gx10 = -3 * upper_edge[0] - upper_edge[1] + upper_edge[2] + 3 * upper_edge[3];
+				const int gy10 = -3 * left_edge[0] - left_edge[1] + left_edge[2] + 3 * left_edge[3];
+
+				const int base = 5 * (sum_u + sum_l); // mean*40, anchored at edge centroid (.25,.25)
+
+				for (uint32_t y = 0; y < 4; y++)
+				{
+					for (uint32_t x = 0; x < 4; x++)
+					{
+						const int num = base + gx10 * (4 * (int)x - 1) + gy10 * (4 * (int)y - 1);
+
+						// round-half-up = floor((num + 20) / 40); numerator can be negative
+						// (negative slopes), and C++ '/' truncates toward zero, so force floor.
+						const int t = num + 20;
+						const int pred_unclamped = (t >= 0) ? (t / 40) : -((-t + 39) / 40);
+
+						weight_preds[index_from_xy(x, y)] = basisu::clamp<int>(pred_unclamped, 0, 64);
+					}
+				}
+			}
+			else if (cand_index == cCandDDL)
+			{
+				// Diagonal-down-left (H.264 4x4 intra mode 3 analog): propagate the
+				// extended top row (upper block's bottom edge + upper-RIGHT block's
+				// bottom edge) down-left at 45 degrees, with 1-2-1 smoothing along the
+				// diagonal. Captures 45-degree edges/stripes entering from the top-right
+				// quadrant -- a direction nothing else in the bank can represent.
+				// pCand_log_blk is pUp_log_blk; requires pRight_diag_log_blk.
+				int T[8];
+
+				for (uint32_t x = 0; x < 4; x++)
+				{
+					const uint32_t w = index_from_xy(x, 3);
+
+					T[x] = fetch_w(pUp_log_blk, p, w);
+
+					T[4 + x] = fetch_w(pRight_diag_log_blk, p, w);
+				}
+
+				for (uint32_t y = 0; y < 4; y++)
+				{
+					for (uint32_t x = 0; x < 4; x++)
+					{
+						const uint32_t d = x + y; // 0..6
+						int pred;
+
+						if (d == 6)
+							pred = (T[6] + 3 * T[7] + 2) >> 2;
+						else
+							pred = (T[d] + 2 * T[d + 1] + T[d + 2] + 2) >> 2;
+
+						weight_preds[index_from_xy(x, y)] = pred; // smoothed avg of [0,64] values: in range
+					}
+				}
+			}
+			else if (cand_index == cCandDDR)
+			{
+				// Diagonal-down-right (H.264 4x4 intra mode 4 analog): propagate at 45
+				// degrees from the top-left, sourcing the left column, the corner, and
+				// the top row, 1-2-1 smoothed. Captures 45-degree structure entering
+				// from the top-left quadrant.
+				// pCand_log_blk is pLeft_log_blk; requires pUp_log_blk and pLeft_diag_log_blk.
+				// A[0..8]: A[0..3] = left column bottom-to-top, A[4] = corner, A[5..8] = top row.
+				int A[9];
+
+				for (uint32_t y = 0; y < 4; y++)
+					A[3 - y] = orig_weight_preds[index_from_xy(3, y)]; // left block's right edge, reversed
+
+				{
+					const uint32_t cw = index_from_xy(3, 3);
+					A[4] = fetch_w(pLeft_diag_log_blk, p, cw);
+				}
+
+				for (uint32_t x = 0; x < 4; x++)
+				{
+					const uint32_t w = index_from_xy(x, 3);
+					A[5 + x] = fetch_w(pUp_log_blk, p, w);
+				}
+
+				for (uint32_t y = 0; y < 4; y++)
+				{
+					for (uint32_t x = 0; x < 4; x++)
+					{
+						const int d = 4 + (int)x - (int)y; // 1..7; texels on diagonal k share A-index k
+						const int pred = (A[d - 1] + 2 * A[d] + A[d + 1] + 2) >> 2;
+
+						weight_preds[index_from_xy(x, y)] = pred;
+					}
+				}
+			}
+
+			pWeight_predictions = weight_preds;
+		}
+
+		if ((amp_code) && (pWeight_predictions))
+		{
+			// Amplitude code: generalization of the old inversion flag.
+			// All transforms are about the prediction's own mean (per
+			// plane), so they negate/scale the ACs while leaving the DC
+			// for the DC coefficient -- the old 64-w flip also
+			// complemented the DC, forcing a DC correction symbol
+			// whenever the content mean wasn't complementary.
+			int sum = 0;
+			for (uint32_t i = 0; i < 16; i++)
+				sum += pWeight_predictions[i];
+			const int mean = (sum + 8) >> 4;
+
+			for (uint32_t i = 0; i < 16; i++)
+			{
+				const int w = pWeight_predictions[i];
+				int v;
+
+				if (amp_code == 1)
+				{
+					// flip about mean (s = -1): pure AC negation
+					v = basisu::clamp<int>(2 * mean - w, 0, 64);
+					//v = 64 - w;
+				}
+				else if (amp_code == 2)
+				{
+					// half contrast (s = +1/2): mean + (w - mean)/2, half-up
+					v = (w + mean + 1) >> 1; // closed in [0,64], no clamp needed
+				}
+				else // amp_code == 3
+				{
+					// half contrast of the flip (s = -1/2): compose 1 then 2
+					const int f = basisu::clamp<int>(2 * mean - w, 0, 64);
+					v = (f + mean + 1) >> 1;
+				}
+
+				pWeight_predictions[i] = v;
+			}
+		}
+
+		if (!pWeight_predictions)
+			return false;
+
+		memcpy(pOut_preds, pWeight_predictions, 16 * sizeof(int));
+		return true;
+	}
+
+	static inline int unpack_coeff_b(uint32_t v)
+	{
+		assert(v <= 255);
+		return (int)v;
+	}
+
+	// LSB-first bit reader matching basisu::bitwise_coder's packing exactly
+	// (first bit written lands in bit 0 of byte 0; values may cross byte
+	// boundaries). Total: reads past the end return false.
+	struct lsb_bit_reader
+	{
+		const uint8_t* m_p = nullptr;
+		uint64_t m_total_bits = 0;
+		uint64_t m_bit_ofs = 0;
+
+		void init(const uint8_t* p, size_t size_bytes) { m_p = p; m_total_bits = (uint64_t)size_bytes * 8; m_bit_ofs = 0; }
+
+		// seek to a bit range [start_bit, end_bit) within the blob -- used for
+		// per-stripe decoding (the stripe's bits occupy exactly this range)
+		void init_range(const uint8_t* p, uint64_t start_bit, uint64_t end_bit) { m_p = p; m_bit_ofs = start_bit; m_total_bits = end_bit; }
+
+		bool get_bits(uint32_t n, uint32_t& v)
+		{
+			assert(n <= 32);
+			v = 0;
+			if (n > (m_total_bits - m_bit_ofs))
+				return false;
+			if (!n)
+				return true;
+
+			const uint64_t byte_idx = m_bit_ofs >> 3;
+			const uint32_t bit_idx = (uint32_t)(m_bit_ofs & 7);
+			const uint32_t num_bytes = (bit_idx + n + 7) >> 3; // 1..5
+
+			uint64_t bits = 0;
+			for (uint32_t i = 0; i < num_bytes; i++)
+				bits |= ((uint64_t)m_p[byte_idx + i]) << (i * 8);
+
+			v = (uint32_t)((bits >> bit_idx) & ((1ull << n) - 1ull));
+			m_bit_ofs += n;
+			return true;
+		}
+
+		// at most 7 zero pad bits may remain
+		bool is_fully_consumed() const { return (m_total_bits - m_bit_ofs) < 8; }
+	};
+
+	// forward byte cursor over one blob; total
+	struct byte_cursor
+	{
+		const uint8_t* m_p = nullptr;
+		uint32_t m_size = 0, m_ofs = 0;
+
+		void init(const blob_stream_reader& rdr, uint32_t id) { m_p = rdr.get_ptr(id); m_size = rdr.get_size(id); m_ofs = 0; }
+		// seek to a byte range [start, end) within the blob -- per-stripe decode
+		void init_range(const blob_stream_reader& rdr, uint32_t id, uint32_t start, uint32_t end) { m_p = rdr.get_ptr(id); m_ofs = start; m_size = end; }
+		bool get(uint8_t& b) { if (m_ofs >= m_size) return false; b = m_p[m_ofs++]; return true; }
+		bool is_fully_consumed() const { return m_ofs == m_size; }
+	};
+
+	bool blob_stream_reader::init_internal(const void* pData, size_t data_size, uint64_t max_total_uncomp)
+		{
+			clear();
+
+			const uint8_t* pBytes = static_cast<const uint8_t*>(pData);
+
+			if ((!pBytes) || (data_size < 3)) // magic + count + magic minimum
+				return false;
+
+			uint64_t ofs = 0;
+			if (pBytes[ofs++] != BLOB_STREAM_MAGIC_BEGIN)
+				return false;
+			const uint32_t num_blobs = pBytes[ofs++];
+
+			// pass 1: walk + validate the directory, total the arena size
+			struct entry { uint32_t id, uncomp_size, stored_size; uint64_t data_ofs; };
+			entry entries[255];
+			uint64_t total_arena = 0;
+
+			for (uint32_t i = 0; i < num_blobs; i++)
+			{
+				if (ofs >= data_size)
+					return false;
+
+				entry& e = entries[i];
+				const uint8_t id_flag = pBytes[ofs++];
+				e.id = id_flag & 0x7Fu;
+				const bool compressed = (id_flag & 0x80u) != 0;
+
+				if (!read_varint(pBytes, data_size, ofs, e.uncomp_size))
+					return false;
+				e.stored_size = 0;
+				if (compressed)
+				{
+					if (!read_varint(pBytes, data_size, ofs, e.stored_size))
+						return false;
+					if (!e.stored_size)
+						return false;
+				}
+				e.data_ofs = ofs;
+
+				if (!e.uncomp_size)
+					return false; // empty blobs are never stored
+
+				if (m_sizes[e.id] || m_ptrs[e.id])
+					return false; // duplicate id
+
+				const uint64_t stored_bytes = e.stored_size ? e.stored_size : e.uncomp_size;
+				if ((ofs + stored_bytes) > data_size)
+					return false;
+				ofs += stored_bytes;
+
+				if (e.stored_size)
+				{
+					if (e.stored_size >= e.uncomp_size)
+						return false; // compressed must be strictly smaller (writer guarantees it)
+					total_arena += e.uncomp_size;
+					if (total_arena > max_total_uncomp)
+						return false;
+				}
+
+				// mark id as seen (real ptr/size set in pass 2)
+				m_sizes[e.id] = e.uncomp_size;
+				m_ptrs[e.id] = pBytes; // placeholder, nonzero for dup detection
+			}
+
+			// end marker must be the exact final byte: rejects truncation,
+			// trailing garbage, and directory/data length disagreements
+			if ((ofs != (data_size - 1)) || (pBytes[ofs] != BLOB_STREAM_MAGIC_END))
+				return false;
+
+			// pass 2: single arena allocation, decompress, wire up pointers
+			if (!m_arena.try_resize((size_t)total_arena))
+				return false;
+
+			uint64_t arena_ofs = 0;
+			for (uint32_t i = 0; i < num_blobs; i++)
+			{
+				const entry& e = entries[i];
+
+				if (!e.stored_size)
+				{
+					m_ptrs[e.id] = pBytes + e.data_ofs; // raw: zero copy into input
+				}
+				else
+				{
+					uint8_t* pDst = m_arena.data() + arena_ofs;
+
+#if BASISD_SUPPORT_KTX2_ZSTD
+					const size_t res = ZSTD_decompress(pDst, e.uncomp_size, pBytes + e.data_ofs, e.stored_size);
+					if (ZSTD_isError(res) || (res != e.uncomp_size))
+						return false;
+#else
+					BASISU_NOTE_UNUSED(pDst);
+					return false; // zstd disabled at compile time
+#endif
+
+					m_ptrs[e.id] = pDst;
+					arena_ofs += e.uncomp_size;
+				}
+			}
+
+			return true;
+		}
+
+	bool image_unpacker::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)
+	{
+		m_initialized = false; // set true ONLY on full success below; every failure path leaves it false
+		m_tiny_mip = false;
+		m_block_cb = pBlock_callback;
+		m_block_data = pBlock_callback_data;
+
+#if !BASISD_SUPPORT_KTX2_ZSTD
+		BASISU_DEVEL_ERROR("xbc7::unpack_image: XBC7 decoding requires zstd (BASISD_SUPPORT_KTX2_ZSTD is 0)");
+		return false;
+#endif
+
+		// ---- format dispatch on the leading byte ----
+		// 0xB7 = blob container (blob_stream_reader validates this marker itself).
+		// 0xB8/0xB9 = tiny-mip ([marker][num_blocks_x:u8][num_blocks_y:u8] then 16
+		//        bytes per packed BC7 block); 0xB8 = no alpha, 0xB9 = has alpha
+		//        (the has_alpha bit rides in the marker). Anything else: not XBC7.
+		if (comp.size() < 1)
+			return false;
+
+#ifndef NDEBUG
+		// Debug builds: report the input buffer size and the stream type.
+		{
+			const char* pType = "regular (blob)";
+			if (comp[0] == 0xB8)
+				pType = "tiny-mip (no alpha)";
+			else if (comp[0] == 0xB9)
+				pType = "tiny-mip (alpha)";
+			else if (comp[0] != BLOB_STREAM_MAGIC_BEGIN)
+				pType = "unknown / invalid marker";
+			fmt_debug_printf("XBC7 decode: input {} bytes, type: {}\n", (uint64_t)comp.size(), pType);
+		}
+#endif
+
+		if ((comp[0] == 0xB8) || (comp[0] == 0xB9))
+			return init_tiny_mip(comp, comp[0] == 0xB9, pInit_callback, pInit_callback_data);
+		if (comp[0] != BLOB_STREAM_MAGIC_BEGIN)
+			return false; // unknown format marker
+
+		if (!m_rdr.init(comp.data(), comp.size()))
+			return false;
+
+		// ---- header ----
+		if (m_rdr.get_size(cBlobHeader) != sizeof(xbc7_header))
+			return false;
+
+		xbc7_header hdr;
+		memcpy((void *)&hdr, m_rdr.get_ptr(cBlobHeader), sizeof(hdr));
+
+		const uint32_t width = hdr.m_width_in_texels;
+		const uint32_t height = hdr.m_height_in_texels;
+		m_global_q = hdr.m_dct_q;
+
+		if ((!width) || (!height))
+			return false;
+		const uint32_t XBC7_MAX_SUPPORTED_DIM = 16384;
+		if ((width > XBC7_MAX_SUPPORTED_DIM) || (height > XBC7_MAX_SUPPORTED_DIM))
+			return false;
+		if ((m_global_q < 1) || (m_global_q > 100))
+			return false;
+		if (hdr.m_flags & ~XBC7_FLAG_HAS_ALPHA)
+			return false; // unknown flags
+
+		m_width = width;
+		m_height = height;
+		m_has_alpha = (hdr.m_flags & XBC7_FLAG_HAS_ALPHA) != 0;
+		m_num_blocks_x = (width + 3) / 4;
+		m_num_blocks_y = (height + 3) / 4;
+		const uint32_t total_blocks = m_num_blocks_x * m_num_blocks_y; // <= 16384^2, no overflow
+
+		// ---- stripe geometry (also governs the implicit solid-block clamp) ----
+		m_num_stripes = hdr.m_num_stripes;
+		if ((!m_num_stripes) || (m_num_stripes > m_num_blocks_y))
+			return false;
+		if (m_num_stripes > XBC7_MAX_ENCODER_STRIPES)
+			return false;
+		compute_stripe_ranges(m_num_blocks_y, m_num_stripes, m_stripes);
+
+		// ---- paranoia gate: structural size check BEFORE any allocation ----
+		// One command byte per block makes this a necessary condition and
+		// transitively bounds every allocation, killing the huge-dims DoS.
+		if (m_rdr.get_size(cBlobCommands) != total_blocks)
+			return false;
+
+		// ---- per-stripe seek table ----
+		// m_seek(id, s) = start offset of stripe s in blob id (a BYTE offset,
+		// or a BIT offset for the three bit blobs); row m_num_stripes holds the
+		// end sentinel (blob byte size, or total bits). Stripe 0 starts at 0.
+		// Single-stripe files carry no table -- the whole blob is stripe 0.
+		const uint32_t SEEK_NUM_STREAMS = (uint32_t)cBlobStripeSeekTable - 1; // ids 1..25
+		m_seek.resize((uint32_t)cBlobStripeSeekTable, m_num_stripes + 1); // (id) x (stripe)
+		m_seek.set_all(0);
+		for (uint32_t id = 1; id < (uint32_t)cBlobStripeSeekTable; id++)
+		{
+			const bool bit_blob = (id == cBlobCoeffSigns) || (id == cBlobPBits) || (id == cBlobEPRaw);
+			m_seek(id, m_num_stripes) = bit_blob ? ((uint64_t)m_rdr.get_size(id) * 8) : (uint64_t)m_rdr.get_size(id);
+		}
+
+		if (m_num_stripes > 1)
+		{
+			if (m_rdr.get_size(cBlobStripeSeekTable) != m_num_stripes * SEEK_NUM_STREAMS * 4)
+				return false;
+
+			// Entries are DELTAS from the previous stripe's start, stripe-major,
+			// stored byte-plane (SoA) transposed: byte b of entry e lives at
+			// pT[b * num_entries + e] (the encoder's inverse of this layout).
+			// Reconstruct absolute offsets with a running prefix sum, validating
+			// as we go (all BEFORE any block decode): stripe 0's delta must be 0,
+			// and each running offset must stay within [0, blob_end] (blob_end =
+			// byte size, or total bits for the three bit blobs). Monotonicity is
+			// automatic -- deltas are unsigned, so the running sum never
+			// decreases. The range test rejects a corrupted table immediately; a
+			// wild byte/bit offset can never reach the cursor seek in decode_stripe.
+			const uint8_t* pT = m_rdr.get_ptr(cBlobStripeSeekTable);
+			const uint32_t num_entries = m_num_stripes * SEEK_NUM_STREAMS;
+			for (uint32_t id = 1; id < (uint32_t)cBlobStripeSeekTable; id++)
+			{
+				const uint64_t blob_end = m_seek(id, m_num_stripes); // sentinel set above
+				uint64_t running = 0;
+				for (uint32_t st = 0; st < m_num_stripes; st++)
+				{
+					const uint32_t e = st * SEEK_NUM_STREAMS + (id - 1);
+					const uint64_t delta = (uint64_t)pT[e] | ((uint64_t)pT[num_entries + e] << 8) |
+						((uint64_t)pT[2u * num_entries + e] << 16) | ((uint64_t)pT[3u * num_entries + e] << 24);
+
+					if ((st == 0) && (delta != 0))
+						return false; // stripe 0 must start at offset 0
+
+					running += delta; // unsigned add: offsets are monotonic by construction
+					if (running > blob_end)
+						return false; // offset past end of blob
+
+					m_seek(id, st) = running;
+				}
+			}
+		}
+
+		// ---- decode state + callbacks ----
+		// The decoder always maintains its own full-image logical-block buffer:
+		// causal prediction (endpoint DPCM / repeat / weight predictors) reads
+		// already-decoded neighbors. Finished blocks are streamed to the caller
+		// via the block callback; the caller owns any persistent storage/packing.
+		m_log_blks.resize(m_num_blocks_x, m_num_blocks_y);
+
+		// Hand the caller the header geometry/metadata exactly once, before any
+		// block, so it can validate and allocate. A false return aborts.
+		if (pInit_callback)
+		{
+			if (!pInit_callback(m_num_blocks_x, m_num_blocks_y, m_width, m_height, m_global_q, m_has_alpha, pInit_callback_data))
+				return false;
+		}
+
+		m_initialized = true;
+		return true;
+	}
+
+	bool image_unpacker::init_tiny_mip(
+		const byte_span& comp, bool has_alpha,
+		decode_init_callback_ptr pInit_callback, void* pInit_callback_data)
+	{
+		if (comp.size() < 3)
+			return false;
+
+		const uint32_t nbx = comp[1];
+		const uint32_t nby = comp[2];
+		if ((!nbx) || (!nby))
+			return false;
+
+		// exact-size tripwire: the 3-byte header plus exactly the BC7 blocks
+		const uint64_t expected = 3ull + (uint64_t)nbx * (uint64_t)nby * 16ull;
+		if ((uint64_t)comp.size() != expected)
+			return false;
+
+		m_tiny_mip = true;
+		// checked region pointer to all the BC7 blocks (validated once); aliases
+		// the span's underlying buffer, which must outlive the decoder
+		m_tiny_blocks = comp.checked_ptr(3, (size_t)nbx * (size_t)nby * 16); // -> first block
+		m_num_blocks_x = nbx;
+		m_num_blocks_y = nby;
+		m_num_stripes = 1;
+		m_width = nbx * 4;   // block-aligned; tiny-mip stores no exact texel dims
+		m_height = nby * 4;
+		m_global_q = 0;      // no DCT in tiny-mip
+		m_has_alpha = has_alpha; // from the stream marker (0xB8 = no alpha, 0xB9 = alpha)
+
+		if (pInit_callback)
+		{
+			if (!pInit_callback(m_num_blocks_x, m_num_blocks_y, m_width, m_height, m_global_q, m_has_alpha, pInit_callback_data))
+				return false;
+		}
+
+		m_initialized = true;
+		return true;
+	}
+
+	bool image_unpacker::decode_stripe(uint32_t stripe_index)
+	{
+		// must follow a successful init() -- guard in ALL builds (assert traps
+		// the bug in debug; the return false keeps release total)
+		assert(m_initialized && "decode_stripe() called without a successful init()");
+		if (!m_initialized)
+			return false;
+
+		assert(stripe_index < m_num_stripes);
+		if (stripe_index >= m_num_stripes)
+			return false;
+
+		// tiny-mip is a single conceptual stripe (m_num_stripes == 1): decode all
+		// its blocks straight from the raw BC7 data, no prediction/blobs involved.
+		if (m_tiny_mip)
+			return decode_tiny_mip();
+
+		const uint32_t first_row = m_stripes[stripe_index].m_first_block_row;
+		const uint32_t end_row = first_row + m_stripes[stripe_index].m_num_block_rows;
+
+		// every reference is clamped to this stripe AABB (mirrors the encoder)
+		const tile_bounds stripe_tile = { 0, (int)first_row, (int)m_num_blocks_x - 1, (int)end_row - 1 };
+
+		// aliases so the per-block body below resolves to member state
+		blob_stream_reader& rdr = m_rdr;
+		const uint32_t num_blocks_x = m_num_blocks_x;
+		const uint32_t global_q = m_global_q;
+		const bool has_alpha = m_has_alpha;
+		vector2D<basist::bc7u::log_bc7_block>& log_blks = m_log_blks;
+
+		// cursors seeked to this stripe's range [seek(id,s), seek(id,s+1))
+		const uint32_t s = stripe_index;
+		byte_cursor commands;		commands.init_range(rdr, cBlobCommands, (uint32_t)m_seek(cBlobCommands, s), (uint32_t)m_seek(cBlobCommands, s + 1));
+		byte_cursor configs;		configs.init_range(rdr, cBlobBC7BlockConfig, (uint32_t)m_seek(cBlobBC7BlockConfig, s), (uint32_t)m_seek(cBlobBC7BlockConfig, s + 1));
+		byte_cursor partitions2;	partitions2.init_range(rdr, cBlobPartition2, (uint32_t)m_seek(cBlobPartition2, s), (uint32_t)m_seek(cBlobPartition2, s + 1));
+		byte_cursor partitions3;	partitions3.init_range(rdr, cBlobPartition3, (uint32_t)m_seek(cBlobPartition3, s), (uint32_t)m_seek(cBlobPartition3, s + 1));
+		byte_cursor predictors;		predictors.init_range(rdr, cBlobWeightPredictors, (uint32_t)m_seek(cBlobWeightPredictors, s), (uint32_t)m_seek(cBlobWeightPredictors, s + 1));
+		byte_cursor dc_coeffs;		dc_coeffs.init_range(rdr, cBlobDCCoeffsSmall, (uint32_t)m_seek(cBlobDCCoeffsSmall, s), (uint32_t)m_seek(cBlobDCCoeffsSmall, s + 1)); // encoder writes ALL DC here
+		byte_cursor ac_coeffs;		ac_coeffs.init_range(rdr, cBlobACCoeffs, (uint32_t)m_seek(cBlobACCoeffs, s), (uint32_t)m_seek(cBlobACCoeffs, s + 1));
+		byte_cursor solid_deltas;	solid_deltas.init_range(rdr, cBlobSolidRGBADeltas, (uint32_t)m_seek(cBlobSolidRGBADeltas, s), (uint32_t)m_seek(cBlobSolidRGBADeltas, s + 1));
+		byte_cursor ep_block_index;	ep_block_index.init_range(rdr, cBlobEPBlockIndex, (uint32_t)m_seek(cBlobEPBlockIndex, s), (uint32_t)m_seek(cBlobEPBlockIndex, s + 1));
+		byte_cursor raw_weights;	raw_weights.init_range(rdr, cBlobRawWeightBits, (uint32_t)m_seek(cBlobRawWeightBits, s), (uint32_t)m_seek(cBlobRawWeightBits, s + 1));
+
+		byte_cursor wt_resid[3];	// DPCM weight residuals by bit width: [0]=2, [1]=3, [2]=4
+		wt_resid[0].init_range(rdr, cBlobDPCMWeightResid2, (uint32_t)m_seek(cBlobDPCMWeightResid2, s), (uint32_t)m_seek(cBlobDPCMWeightResid2, s + 1));
+		wt_resid[1].init_range(rdr, cBlobDPCMWeightResid3, (uint32_t)m_seek(cBlobDPCMWeightResid3, s), (uint32_t)m_seek(cBlobDPCMWeightResid3, s + 1));
+		wt_resid[2].init_range(rdr, cBlobDPCMWeightResid4, (uint32_t)m_seek(cBlobDPCMWeightResid4, s), (uint32_t)m_seek(cBlobDPCMWeightResid4, s + 1));
+
+		byte_cursor ep_deltas[8]; // [0..3] = Fine R,G,B,A; [4..7] = Coarse R,G,B,A
+		for (uint32_t i = 0; i < 4; i++)
+		{
+			ep_deltas[i].init_range(rdr, cBlobEPDeltaFineR + i, (uint32_t)m_seek(cBlobEPDeltaFineR + i, s), (uint32_t)m_seek(cBlobEPDeltaFineR + i, s + 1));
+			ep_deltas[4 + i].init_range(rdr, cBlobEPDeltaCoarseR + i, (uint32_t)m_seek(cBlobEPDeltaCoarseR + i, s), (uint32_t)m_seek(cBlobEPDeltaCoarseR + i, s + 1));
+		}
+
+		lsb_bit_reader coeff_signs;	coeff_signs.init_range(rdr.get_ptr(cBlobCoeffSigns), m_seek(cBlobCoeffSigns, s), m_seek(cBlobCoeffSigns, s + 1));
+		lsb_bit_reader pbits;		pbits.init_range(rdr.get_ptr(cBlobPBits), m_seek(cBlobPBits, s), m_seek(cBlobPBits, s + 1));
+		lsb_bit_reader ep_raw;		ep_raw.init_range(rdr.get_ptr(cBlobEPRaw), m_seek(cBlobEPRaw, s), m_seek(cBlobEPRaw, s + 1));
+
+		xbc7::xbc7_weight_grid_dct_fixed weight_grid_dct_fixed;
+		weight_grid_dct_fixed.init();
+		xbc7::fxvec dct_work_fixed;
+
+		for (uint32_t by = first_row; by < end_row; by++)
+		{
+			for (uint32_t bx = 0; bx < num_blocks_x; bx++)
+			{
+				const basist::bc7u::log_bc7_block* pLeft_log_blk = stripe_tile.contains((int)bx - 1, (int)by) ? &log_blks(bx - 1, by) : nullptr;
+				const basist::bc7u::log_bc7_block* pUp_log_blk = stripe_tile.contains((int)bx, (int)by - 1) ? &log_blks(bx, by - 1) : nullptr;
+				const basist::bc7u::log_bc7_block* pLeft_diag_log_blk = stripe_tile.contains((int)bx - 1, (int)by - 1) ? &log_blks(bx - 1, by - 1) : nullptr;
+				const basist::bc7u::log_bc7_block* pRight_diag_log_blk = stripe_tile.contains((int)bx + 1, (int)by - 1) ? &log_blks(bx + 1, by - 1) : nullptr;
+
+				basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by);
+
+				uint8_t cmd_byte;
+				if (!commands.get(cmd_byte))
+					return false;
+
+				const uint32_t cmd = cmd_byte & 7;
+				const uint32_t ep_mode = (cmd_byte >> XBC7_COMMAND_ENDPOINT_MODE_SHIFT) & 7;
+				const uint32_t wt_mode = (cmd_byte >> XBC7_COMMAND_WEIGHT_MODE_SHIFT) & 1;
+
+				if (cmd_byte & 0x80)
+					return false; // P-frame flag: reserved in v0
+
+				// ---- simple commands: the whole byte is the command id ----
+				if (cmd <= (uint32_t)xbc7_command_id::cCmdSolidDPCM)
+				{
+					if (cmd_byte != cmd)
+						return false; // canonical: simple commands carry no EP/WT bits
+
+					if (cmd == (uint32_t)xbc7_command_id::cCmdRepeatLast)
+					{
+						if (!pLeft_log_blk)
+							return false;
+						log_blk = *pLeft_log_blk;
+					}
+					else if (cmd == (uint32_t)xbc7_command_id::cCmdRepeatUpper)
+					{
+						if (!pUp_log_blk)
+							return false;
+						log_blk = *pUp_log_blk;
+					}
+					else // cCmdSolidDPCM
+					{
+						// neighbor edge-average prediction in decoded-pixel space;
+						// mirrors the encoder exactly -- INCLUDING its stripe
+						// seam clamp, since this prediction is implicit (the
+						// encoder cannot read the stripe above, so neither may we)
+						int preds[4] = { 0, 0, 0, 0 };
+						int num_preds = 0;
+
+						if (pLeft_log_blk)
+						{
+							for (uint32_t y = 0; y < 4; y++)
+							{
+								basist::color_rgba px;
+								if (!basist::bc7u::unpack_bc7_texel(*pLeft_log_blk, px, 3, y))
+									return false;
+								preds[0] += px.r; preds[1] += px.g; preds[2] += px.b; preds[3] += px.a;
+							}
+							num_preds += 4;
+						}
+
+						if (pUp_log_blk) // pUp is already null above the stripe's first row
+						{
+							for (uint32_t x = 0; x < 4; x++)
+							{
+								basist::color_rgba px;
+								if (!basist::bc7u::unpack_bc7_texel(*pUp_log_blk, px, x, 3))
+									return false;
+								preds[0] += px.r; preds[1] += px.g; preds[2] += px.b; preds[3] += px.a;
+							}
+							num_preds += 4;
+						}
+
+						if (num_preds)
+						{
+							for (uint32_t c = 0; c < 4; c++)
+								preds[c] = (preds[c] + (num_preds / 2)) / num_preds;
+						}
+
+						basist::color_rgba solid_color;
+						for (uint32_t c = 0; c < (has_alpha ? 4u : 3u); c++)
+						{
+							uint8_t delta;
+							if (!solid_deltas.get(delta))
+								return false;
+							solid_color[c] = (uint8_t)(delta + preds[c]);
+						}
+						if (!has_alpha)
+							solid_color.a = 255;
+
+						basist::bc7u::create_solid_blk(log_blk, solid_color);
+					}
+				}
+				else
+				{
+					// ---- config ----
+					if (cmd == (uint32_t)xbc7_command_id::cCmdNewConfig)
+					{
+						uint8_t config_byte;
+						if (!configs.get(config_byte))
+							return false;
+						if (config_byte & 0xC0)
+							return false; // reserved bits
+
+						const uint32_t mode = config_byte & 7;
+						const uint32_t rot = (config_byte >> 3) & 3;
+						const uint32_t sel = (config_byte >> 5) & 1;
+
+						basist::bc7u::init_log_blk(log_blk, mode);
+
+						if (log_blk.m_num_planes == 2)
+							log_blk.m_dp_rotation_index = (uint8_t)rot;
+						else if (rot)
+							return false; // rotation on a non-dual-plane mode
+
+						if (mode == 4)
+							log_blk.m_mode4_index_selector = (uint8_t)sel;
+						else if (sel)
+							return false; // selector outside mode 4
+					}
+					else // reuse config from a neighbor
+					{
+						const basist::bc7u::log_bc7_block* pSrc = nullptr;
+
+						if (cmd == (uint32_t)xbc7_command_id::cCmdReuseConfigLeft)
+							pSrc = pLeft_log_blk;
+						else if (cmd == (uint32_t)xbc7_command_id::cCmdReuseConfigUpper)
+							pSrc = pUp_log_blk;
+						else if (cmd == (uint32_t)xbc7_command_id::cCmdReuseConfigLeftDiagonal)
+							pSrc = pLeft_diag_log_blk;
+						else // cCmdReuseConfigRightDiagonal
+							pSrc = pRight_diag_log_blk;
+
+						if (!pSrc)
+							return false;
+
+						basist::bc7u::init_log_blk(log_blk, pSrc->m_mode);
+						log_blk.m_dp_rotation_index = pSrc->m_dp_rotation_index;
+						log_blk.m_mode4_index_selector = pSrc->m_mode4_index_selector;
+					}
+
+					// ---- partition index (always sent for partitioned modes) ----
+					if (log_blk.m_num_partitions == 2)
+					{
+						uint8_t pat;
+						if (!partitions2.get(pat))
+							return false;
+						if (pat >= 64)
+							return false;
+						log_blk.m_pattern_index = pat;
+					}
+					else if (log_blk.m_num_partitions == 3)
+					{
+						uint8_t pat;
+						if (!partitions3.get(pat))
+							return false;
+						if (pat >= (1u << log_blk.m_pattern_bits))
+							return false; // mode 0: index < 16
+						log_blk.m_pattern_index = pat;
+					}
+
+					const basist::bc7u::endpoint_format& fmt = basist::bc7u::g_endpoint_formats[log_blk.m_mode];
+					const uint32_t num_comps = log_blk.get_num_comps();
+
+					// ---- endpoints ----
+					if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointRaw)
+					{
+						for (uint32_t subset = 0; subset < log_blk.m_num_partitions; subset++)
+						{
+							for (uint32_t c = 0; c < num_comps; c++)
+							{
+								for (uint32_t e = 0; e < 2; e++)
+								{
+									uint32_t v;
+									if (!ep_raw.get_bits(log_blk.m_endpoint_bits[c == 3], v))
+										return false;
+									log_blk.m_endpoints[subset][e][c] = (uint8_t)v;
+								}
+							}
+						}
+
+						for (uint32_t pb = 0; pb < log_blk.m_num_pbits; pb++)
+						{
+							uint32_t v;
+							if (!ep_raw.get_bits(1, v))
+								return false;
+							if (pb < 6) // to shut up gcc bogus warning
+								log_blk.m_pbits[pb] = (uint8_t)v;
+						}
+					}
+					else
+					{
+						// resolve the endpoint predictor block (mirrors the encoder
+						// sweep); ep_mode is 3 bits, and 1..7 are all DPCM modes
+						const basist::bc7u::log_bc7_block* pEP_pred_blk = nullptr;
+						uint32_t ep_pred_subset = 0;
+
+						if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMLeft)
+							pEP_pred_blk = pLeft_log_blk;
+						else if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMUp)
+							pEP_pred_blk = pUp_log_blk;
+						else if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMLeftDiagonal)
+							pEP_pred_blk = pLeft_diag_log_blk;
+						else if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMRightDiagonal)
+							pEP_pred_blk = pRight_diag_log_blk;
+						else if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMLeftSubset1)
+						{
+							pEP_pred_blk = pLeft_log_blk;
+							ep_pred_subset = 1;
+						}
+						else if (ep_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMUpSubset1)
+						{
+							pEP_pred_blk = pUp_log_blk;
+							ep_pred_subset = 1;
+						}
+						else // cCmdEndpointDPCMBlockIndex
+						{
+							uint8_t delta_index;
+							if (!ep_block_index.get(delta_index))
+								return false;
+							if (delta_index >= NUM_XY_DELTAS)
+								return false; // top 3 bits reserved-zero
+
+							const xbc7_xy_delta& delta = g_xbc7_xy_deltas[delta_index];
+							const int nx = (int)bx + delta.m_dx;
+							const int ny = (int)by + delta.m_dy;
+
+							// must reference a block inside this stripe (the encoder
+							// never emits a cross-stripe reference); also keeps a
+							// worker from reading another stripe's rows
+							if (!stripe_tile.contains(nx, ny))
+								return false; // reference outside the stripe: malformed stream
+
+							pEP_pred_blk = &log_blks(nx, ny);
+						}
+
+						if (!pEP_pred_blk)
+							return false;
+
+						// subset-1 references require a partitioned predictor
+						if ((ep_pred_subset) && (pEP_pred_blk->m_num_partitions < 2))
+							return false;
+
+						const bool fine = (log_blk.m_endpoint_bits[0] >= 6);
+
+						for (uint32_t subset = 0; subset < log_blk.m_num_partitions; subset++)
+						{
+							uint8_t residuals[8];
+							uint32_t num_residuals = num_comps * 2;
+							if ((!has_alpha) && (log_blk.m_mode == 6))
+							{
+								assert(num_residuals == 8);
+								num_residuals = 6;
+								residuals[6] = 0;
+								residuals[7] = 0;
+							}
+
+							for (uint32_t i = 0; i < num_residuals; i += 2)
+							{
+								const uint32_t chan = i >> 1;
+								byte_cursor& strm = ep_deltas[(fine ? 0 : 4) + chan];
+								if (!strm.get(residuals[i + 0]))
+									return false;
+								if (!strm.get(residuals[i + 1]))
+									return false;
+							}
+
+							uint8_t residual_pbits[2] = { 0, 0 };
+							for (uint32_t pb = 0; pb < fmt.m_num_pbits; pb++)
+							{
+								uint32_t v;
+								if (!pbits.get_bits(1, v))
+									return false;
+								residual_pbits[pb] = (uint8_t)v;
+							}
+
+							uint32_t num_residuals_out, num_residual_pbits_out;
+							basist::bc7u::endpoint_dpcm(true,
+								*pEP_pred_blk, ep_pred_subset, // mirrors the encoder (subset 1 for EP modes 6/7)
+								log_blk, subset,
+								residuals, num_residuals_out, residual_pbits, num_residual_pbits_out);
+							if ((!has_alpha) && (log_blk.m_mode == 6))
+							{
+								log_blk.m_endpoints[0][0][3] = 127;
+								log_blk.m_endpoints[0][1][3] = 127;
+							}
+						}
+					}
+
+					// ---- weights: residual DCT (wt bit 1) or lossless residual DPCM (wt bit 0) ----
+					uint8_t pred_byte;
+					if (!predictors.get(pred_byte))
+						return false;
+
+					if (pred_byte >= cTotalCandidates * 4)
+						return false;
+
+					const uint32_t cand_index = pred_byte % cTotalCandidates;
+					const uint32_t amp_code = pred_byte / cTotalCandidates;
+
+					if ((amp_code) && (cand_index == cCandAbsolute))
+						return false;
+
+					int weight_preds[16];
+					int* pWeight_predictions = nullptr;
+
+					for (uint32_t p = 0; p < log_blk.m_num_planes; p++)
+					{
+						if (cand_index != cCandAbsolute)
+						{
+							// the decoder may reference ANY causal block (whole-image
+							// tile): the encoder simply never emits a reference
+							// that crosses one of its tile boundaries
+							if (!eval_weight_predictor(cand_index, amp_code, bx, by, num_blocks_x, stripe_tile, log_blks, p, weight_preds))
+								return false; // candidate unavailable here: malformed stream
+							pWeight_predictions = weight_preds;
+						}
+
+						if (wt_mode != (uint32_t)xbc7_command_weight_mode::cCmdWeightDCT)
+						{
+							// ---- lossless residual DPCM weights ----
+							// Predictions are quantized to the plane's bit depth and
+							// the wrapped n-bit index residuals (or, for the absolute
+							// predictor, the raw indices) are read back. Exact.
+							const uint32_t num_bits = log_blk.m_weight_bits[p];
+							const uint32_t mask = (1u << num_bits) - 1;
+
+							byte_cursor& strm = (cand_index == cCandAbsolute) ? raw_weights : wt_resid[num_bits - 2];
+
+							uint8_t syms[16];
+							if (num_bits == 2)
+							{
+								for (uint32_t i = 0; i < 16; i += 4)
+								{
+									uint8_t b;
+									if (!strm.get(b))
+										return false;
+									syms[i + 0] = b & 3;
+									syms[i + 1] = (b >> 2) & 3;
+									syms[i + 2] = (b >> 4) & 3;
+									syms[i + 3] = (uint8_t)(b >> 6);
+								}
+							}
+							else
+							{
+								for (uint32_t i = 0; i < 16; i += 2)
+								{
+									uint8_t b;
+									if (!strm.get(b))
+										return false;
+									const uint8_t lo = b & 0xF, hi = (uint8_t)(b >> 4);
+									if ((num_bits == 3) && ((lo > 7) || (hi > 7)))
+										return false; // nibble bit 3 reserved-zero
+									syms[i + 0] = lo;
+									syms[i + 1] = hi;
+								}
+							}
+
+							for (uint32_t i = 0; i < 16; i++)
+							{
+								const uint32_t pred_index = pWeight_predictions ?
+									basist::bc7u::quant_weight(pWeight_predictions[i], num_bits) : 0;
+								log_blk.m_weights[p][i] = (uint8_t)((syms[i] + pred_index) & mask);
+							}
+
+							continue; // next plane
+						}
+
+						// ---- DC ----
+						uint8_t dc_byte;
+						if (!dc_coeffs.get(dc_byte))
+							return false;
+
+						int dc = unpack_coeff_b(dc_byte);
+
+						if (pred_byte != cCandAbsolute) // joint index 0 == absolute: DC is unsigned
+						{
+							uint32_t sign;
+							if (!coeff_signs.get_bits(1, sign))
+								return false;
+							if (sign)
+								dc = -dc;
+						}
+
+						// ---- ACs ----
+						xbc7::dct_syms syms;
+						syms.clear();
+						syms.m_dc = (int16_t)dc;
+
+						uint32_t zig_idx = 1;
+
+						while (zig_idx < 16)
+						{
+							uint8_t b;
+							if (!ac_coeffs.get(b))
+								return false;
+
+							if (b == 0xFF)
+							{
+								// trailing zeros to the end of the scan
+								xbc7::coeff cf;
+								cf.m_num_zeros = (int16_t)(16 - zig_idx);
+								cf.m_coeff = INT16_MAX;
+								syms.m_ac_vals.push_back(cf);
+								break;
+							}
+
+							const uint32_t run = b;
+							if ((zig_idx + run) > 15)
+								return false; // a real coefficient must land at position <= 15
+
+							uint8_t mag_byte;
+							if (!ac_coeffs.get(mag_byte))
+								return false;
+
+							const int mag = unpack_coeff_b(mag_byte);
+							if (!mag)
+								return false; // zero coefficients are never coded
+
+							uint32_t sign;
+							if (!coeff_signs.get_bits(1, sign))
+								return false;
+
+							xbc7::coeff cf;
+							cf.m_num_zeros = (int16_t)run;
+							cf.m_coeff = (int16_t)(sign ? -mag : mag);
+							syms.m_ac_vals.push_back(cf);
+
+							zig_idx += run + 1;
+						}
+
+						bool status = weight_grid_dct_fixed.inverse(
+							basist::fixed16_16::from_int(global_q), p, pWeight_predictions, syms, log_blk, dct_work_fixed);
+						if (!status)
+							return false;
+
+					} // p
+				}
+								
+				// ---- single emit point: every block path (repeat-last,
+				// repeat-upper, solid, config) falls through to here, so the
+				// caller's block callback is invoked EXACTLY ONCE per block,
+				// after the logical block is stored in m_log_blks. The caller may
+				// pack a physical BC7 block, store it, compare it vs a reference,
+				// transcode it, etc. A false return aborts the decode. ----
+				if (m_block_cb && !m_block_cb(bx, by, log_blk, m_block_data))
+					return false;
+
+			} // bx
+		} // by
+
+		// ---- per-stripe desync tripwires: each stream consumed exactly its
+		// range (cursors were bounded to [seek(s), seek(s+1))) ----
+		if (!commands.is_fully_consumed()) return false;
+		if (!configs.is_fully_consumed()) return false;
+		if (!partitions2.is_fully_consumed()) return false;
+		if (!partitions3.is_fully_consumed()) return false;
+		if (!predictors.is_fully_consumed()) return false;
+		if (!dc_coeffs.is_fully_consumed()) return false;
+		if (!ac_coeffs.is_fully_consumed()) return false;
+		if (!solid_deltas.is_fully_consumed()) return false;
+		if (!ep_block_index.is_fully_consumed()) return false;
+		if (!raw_weights.is_fully_consumed()) return false;
+		for (uint32_t i = 0; i < 3; i++)
+			if (!wt_resid[i].is_fully_consumed()) return false;
+		for (uint32_t i = 0; i < 8; i++)
+			if (!ep_deltas[i].is_fully_consumed()) return false;
+		if (!coeff_signs.is_fully_consumed()) return false;
+		if (!pbits.is_fully_consumed()) return false;
+		if (!ep_raw.is_fully_consumed()) return false;
+
+		return true;
+	} // decode_stripe
+
+	bool image_unpacker::decode_tiny_mip()
+	{
+		for (uint32_t by = 0; by < m_num_blocks_y; by++)
+		{
+			for (uint32_t bx = 0; bx < m_num_blocks_x; bx++)
+			{
+				const uint8_t* p = m_tiny_blocks + ((size_t)by * m_num_blocks_x + bx) * 16;
+
+				basist::bc7u::log_bc7_block log_blk;
+				if (!basist::bc7u::unpack_bc7(p, log_blk))
+					return false;
+
+				if (m_block_cb && !m_block_cb(bx, by, log_blk, m_block_data))
+					return false;
+			}
+		}
+		return true;
+	}
+
+	bool image_unpacker::decode_all()
+	{
+		// guard here too: with no successful init(), m_num_stripes is 0 and the
+		// loop would otherwise return true (false success) without decoding
+		assert(m_initialized && "decode_all() called without a successful init()");
+		if (!m_initialized)
+			return false;
+
+		for (uint32_t s = 0; s < m_num_stripes; s++)
+			if (!decode_stripe(s))
+				return false;
+		return true;
+	}
+
+	// Single-threaded one-shot: prep + serial decode.
+	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)
+	{
+#if !BASISD_SUPPORT_KTX2_ZSTD
+		BASISU_NOTE_UNUSED(comp); BASISU_NOTE_UNUSED(pInit_callback); BASISU_NOTE_UNUSED(pInit_callback_data);
+		BASISU_NOTE_UNUSED(pBlock_callback); BASISU_NOTE_UNUSED(pBlock_callback_data);
+		BASISU_DEVEL_ERROR("xbc7::unpack_image: XBC7 decoding requires zstd (BASISD_SUPPORT_KTX2_ZSTD is 0)");
+		return false;
+#else
+		image_unpacker dec;
+		if (!dec.init(comp, pInit_callback, pInit_callback_data, pBlock_callback, pBlock_callback_data))
+			return false;
+		return dec.decode_all();
+#endif
+	}
+
+	// Threaded: caller owns `dec` (it must outlive the spawned jobs). init + spawn
+	// one job per stripe via the spawner; returns false only on init failure and
+	// does NOT wait. The 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)
+	{
+#if !BASISD_SUPPORT_KTX2_ZSTD
+		BASISU_NOTE_UNUSED(dec); BASISU_NOTE_UNUSED(comp); BASISU_NOTE_UNUSED(spawner);
+		BASISU_NOTE_UNUSED(pInit_callback); BASISU_NOTE_UNUSED(pInit_callback_data);
+		BASISU_NOTE_UNUSED(pBlock_callback); BASISU_NOTE_UNUSED(pBlock_callback_data);
+		BASISU_DEVEL_ERROR("xbc7::unpack_image_threaded: XBC7 decoding requires zstd (BASISD_SUPPORT_KTX2_ZSTD is 0)");
+		return false;
+#else
+		if (!dec.init(comp, pInit_callback, pInit_callback_data, pBlock_callback, pBlock_callback_data))
+			return false;
+		const uint32_t num_stripes = dec.get_num_stripes();
+		for (uint32_t stripe_index = 0; stripe_index < num_stripes; stripe_index++)
+			spawner.spawn_job(dec, stripe_index);
+		return true;
+#endif
+	}
+
+} // namespace xbc7
+} // namespace basist