From 3cfdd2240bb08e36e4881a8c4d0f0c7d7bd99e6d Mon Sep 17 00:00:00 2001 From: Richard Geldreich Date: Wed, 1 Jul 2026 13:20:12 -0400 Subject: [PATCH] new files for v2.5 --- encoder/basisu_astc_ldr_fencode.cpp | 5423 +++++++++++++++++++++ encoder/basisu_astc_ldr_fencode.h | 440 ++ encoder/basisu_astc_ldr_pseudoinv_tab.inl | 298 ++ encoder/basisu_bc15_spmd.cpp | 441 ++ encoder/basisu_bc15_spmd.h | 73 + encoder/basisu_bc15_spmd_kernels.inl | 428 ++ encoder/basisu_bc15_spmd_sse.cpp | 22 + encoder/basisu_bc7e_scalar.cpp | 5015 +++++++++++++++++++ encoder/basisu_bc7e_scalar.h | 94 + encoder/basisu_dds_export.cpp | 569 +++ encoder/basisu_dds_export.h | 123 + encoder/basisu_xbc7_encode.cpp | 3643 ++++++++++++++ encoder/basisu_xbc7_encode.h | 200 + 13 files changed, 16769 insertions(+) create mode 100644 encoder/basisu_astc_ldr_fencode.cpp create mode 100644 encoder/basisu_astc_ldr_fencode.h create mode 100644 encoder/basisu_astc_ldr_pseudoinv_tab.inl create mode 100644 encoder/basisu_bc15_spmd.cpp create mode 100644 encoder/basisu_bc15_spmd.h create mode 100644 encoder/basisu_bc15_spmd_kernels.inl create mode 100644 encoder/basisu_bc15_spmd_sse.cpp create mode 100644 encoder/basisu_bc7e_scalar.cpp create mode 100644 encoder/basisu_bc7e_scalar.h create mode 100644 encoder/basisu_dds_export.cpp create mode 100644 encoder/basisu_dds_export.h create mode 100644 encoder/basisu_xbc7_encode.cpp create mode 100644 encoder/basisu_xbc7_encode.h diff --git a/encoder/basisu_astc_ldr_fencode.cpp b/encoder/basisu_astc_ldr_fencode.cpp new file mode 100644 index 0000000..0ff4054 --- /dev/null +++ b/encoder/basisu_astc_ldr_fencode.cpp @@ -0,0 +1,5423 @@ +// basisu_astc_ldr_fencode.cpp +#include "basisu_astc_ldr_fencode.h" +#include "../transcoder/basisu_astc_helpers.h" +#include "basisu_astc_ldr_encode.h" +#include "basisu_enc.h" +#include + +namespace basisu +{ +namespace astc_ldrf +{ + +// cem: 4 bits, ofs 0 +// w: 4 bits, ofs 4 +// h: 4 bits, ofs 8 +// e: 5 bits, ofs 12 +// w: 4 bits, ofs 17 +// dp: 1 bit, ofs 21 + +// cems 6,8,10,12 only, gw>=gh (opposite case of gw=gh (opposite case of gw= src_img.m_width)); + + const uint32_t row_byte_pitch = src_img.m_row_pitch_in_texels * sizeof(uint32_t); + + const uint8_t* pSrc_row = src_img.m_pPixels + ((y_ofs * height) * src_img.m_row_pitch_in_texels + (x_ofs * width)) * sizeof(uint32_t); + + if (num_comps == 3) + { + for (uint32_t y = 0; y < height; y++, pSrc_row += row_byte_pitch) + { + const uint8_t* pSrc = pSrc_row; + + for (uint32_t x = 0; x < width; x++, pSrc += 4) + { + pixelbuf_set_comp(pDst_pixel_buf, x, y, cR, (float)pSrc[0]); + pixelbuf_set_comp(pDst_pixel_buf, x, y, cG, (float)pSrc[1]); + pixelbuf_set_comp(pDst_pixel_buf, x, y, cB, (float)pSrc[2]); + pixelbuf_set_comp(pDst_pixel_buf, x, y, cA, 255.0f); + } + } + } + else + { + for (uint32_t y = 0; y < height; y++, pSrc_row += row_byte_pitch) + { + const uint8_t* pSrc = pSrc_row; + + for (uint32_t x = 0; x < width; x++, pSrc += 4) + { + pixelbuf_set_comp(pDst_pixel_buf, x, y, cR, (float)pSrc[0]); + pixelbuf_set_comp(pDst_pixel_buf, x, y, cG, (float)pSrc[1]); + pixelbuf_set_comp(pDst_pixel_buf, x, y, cB, (float)pSrc[2]); + pixelbuf_set_comp(pDst_pixel_buf, x, y, cA, (float)pSrc[3]); + } + } + } +} + +static void pixelbuf_set_comp_to_val(pixelbuf &pbuf, uint32_t comp, float val) +{ + const uint32_t width = pbuf.m_width, height = pbuf.m_height; + float* pDst_pixel_buf = pbuf.m_pBuf; + + for (uint32_t y = 0; y < height; y++) + for (uint32_t x = 0; x < width; x++) + pixelbuf_set_comp(pDst_pixel_buf, x, y, comp, val); +} + +static void pixelbuf_swap_comp_with_alpha(pixelbuf &pbuf, uint32_t comp) +{ + const uint32_t width = pbuf.m_width, height = pbuf.m_height; + float* pDst_pixel_buf = pbuf.m_pBuf; + + assert((comp >= 0) && (comp <= 3)); + + if (comp == cR) + { + for (uint32_t y = 0; y < height; y++) + { + for (uint32_t x = 0; x < width; x++) + { + const float r = pixelbuf_get_comp(pDst_pixel_buf, x, y, cR); + const float a = pixelbuf_get_comp(pDst_pixel_buf, x, y, cA); + pixelbuf_set_comp(pDst_pixel_buf, x, y, cR, a); + pixelbuf_set_comp(pDst_pixel_buf, x, y, cA, r); + } + } + } + else if (comp == cG) + { + for (uint32_t y = 0; y < height; y++) + { + for (uint32_t x = 0; x < width; x++) + { + const float g = pixelbuf_get_comp(pDst_pixel_buf, x, y, cG); + const float a = pixelbuf_get_comp(pDst_pixel_buf, x, y, cA); + pixelbuf_set_comp(pDst_pixel_buf, x, y, cG, a); + pixelbuf_set_comp(pDst_pixel_buf, x, y, cA, g); + } + } + } + else if (comp == cB) + { + for (uint32_t y = 0; y < height; y++) + { + for (uint32_t x = 0; x < width; x++) + { + const float b = pixelbuf_get_comp(pDst_pixel_buf, x, y, cB); + const float a = pixelbuf_get_comp(pDst_pixel_buf, x, y, cA); + pixelbuf_set_comp(pDst_pixel_buf, x, y, cB, a); + pixelbuf_set_comp(pDst_pixel_buf, x, y, cA, b); + } + } + } +} + +static inline bool does_cem_have_alpha(uint32_t cem) +{ + return (cem == 4) || (cem >= 10); +} + +static inline uint32_t get_num_cem_chans(uint32_t cem) +{ + return ((cem == 4) || (cem >= 10)) ? 4 : 3; +} + +// l and la +[[maybe_unused]] static inline bool is_cem_0_or_4(uint32_t cem) +{ + return (cem == 0) || (cem == 4); +} + +// base+scale and base+scale plus 2 a +static inline bool is_cem_6_or_10(uint32_t cem) +{ + return (cem == 6) || (cem == 10); +} + +// rgb(a) direct +static inline bool is_cem_8_or_12(uint32_t cem) +{ + return (cem == 8) || (cem == 12); +} + +// rgb(a) base+ofs +static inline bool is_cem_9_or_13(uint32_t cem) +{ + return (cem == 9) || (cem == 13); +} + +void convert_rank_lblock_to_ise(astc_helpers::log_astc_block& log_blk) +{ + if ((log_blk.m_solid_color_flag_ldr) || (is_lblock_ise(log_blk))) + return; + + const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(log_blk.m_endpoint_ise_range).m_rank_to_ISE; + + const uint32_t num_endpoint_vals = astc_helpers::get_total_endpoint_vals(log_blk); + for (uint32_t i = 0; i < num_endpoint_vals; i++) + log_blk.m_endpoints[i] = (uint8_t)endpoint_tab[log_blk.m_endpoints[i]]; + + const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(log_blk.m_weight_ise_range).m_rank_to_ISE; + + const uint32_t num_weight_vals = astc_helpers::get_total_weights(log_blk); + for (uint32_t i = 0; i < num_weight_vals; i++) + log_blk.m_weights[i] = (uint8_t)weight_tab[log_blk.m_weights[i]]; + + log_blk.m_user_mode = (uint8_t)cUserModeISEValues; +} + +void convert_ise_lblock_to_rank(astc_helpers::log_astc_block& log_blk) +{ + if ((log_blk.m_solid_color_flag_ldr) || (!is_lblock_ise(log_blk))) + return; + + const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(log_blk.m_endpoint_ise_range).m_ISE_to_rank; + + const uint32_t num_endpoint_vals = astc_helpers::get_total_endpoint_vals(log_blk); + for (uint32_t i = 0; i < num_endpoint_vals; i++) + log_blk.m_endpoints[i] = (uint8_t)endpoint_tab[log_blk.m_endpoints[i]]; + + const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(log_blk.m_weight_ise_range).m_ISE_to_rank; + + const uint32_t num_weight_vals = astc_helpers::get_total_weights(log_blk); + for (uint32_t i = 0; i < num_weight_vals; i++) + log_blk.m_weights[i] = (uint8_t)weight_tab[log_blk.m_weights[i]]; + + log_blk.m_user_mode = (uint8_t)cUserModeRankValues; +} + +static double compute_block_error(const astc_helpers::log_astc_block& lblk, const pixelbuf& src_block, const single_subset_enc_context& ctx) +{ + const astc_helpers::log_astc_block* pBlock = &lblk; + + astc_helpers::log_astc_block lblk_temp; + + if (!is_lblock_ise(lblk)) + { + lblk_temp = lblk; + convert_rank_lblock_to_ise(lblk_temp); + pBlock = &lblk_temp; + } + + astc_helpers::color_rgba block_pixels[astc_helpers::MAX_BLOCK_PIXELS]; + bool status = astc_helpers::decode_block_xuastc_ldr(*pBlock, block_pixels, ctx.m_block_width, ctx.m_block_height, ctx.m_astc_decode_mode); + assert(status); + if (!status) + return DBL_MAX; + + const float wr = (float)ctx.m_chan_weights[0], wg = (float)ctx.m_chan_weights[1], wb = (float)ctx.m_chan_weights[2], wa = (float)ctx.m_chan_weights[3]; + + double wsse = 0; + + const astc_helpers::color_rgba* pBlock_pixel = block_pixels; + + for (uint32_t y = 0; y < ctx.m_block_height; y++) + { + for (uint32_t x = 0; x < ctx.m_block_width; x++) + { + wsse += squaref((float)pBlock_pixel->m_r - pixelbuf_get_comp(src_block.m_pBuf, x, y, 0)) * wr; + wsse += squaref((float)pBlock_pixel->m_g - pixelbuf_get_comp(src_block.m_pBuf, x, y, 1)) * wg; + wsse += squaref((float)pBlock_pixel->m_b - pixelbuf_get_comp(src_block.m_pBuf, x, y, 2)) * wb; + wsse += squaref((float)pBlock_pixel->m_a - pixelbuf_get_comp(src_block.m_pBuf, x, y, 3)) * wa; + + ++pBlock_pixel; + } + } + + return wsse; +} + +// Scatter/covariance matrix values +// Symmetric matrix form: +// 0 1 2 3 +// 1 4 5 6 +// 2 5 7 8 +// 3 6 8 9 +enum +{ + cCovarRR = 0, cCovarRG = 1, cCovarRB = 2, cCovarRA = 3, + cCovarGG = 4, cCovarGB = 5, cCovarGA = 6, + cCovarBB = 7, cCovarBA = 8, + cCovarAA = 9, + cTotalCovar = 10, +}; + +// Columns of the symmetric 4x4 covariance matrix +static const uint8_t s_covar_col_indices[4][4] = +{ + { cCovarRR, cCovarRG, cCovarRB, cCovarRA }, + { cCovarRG, cCovarGG, cCovarGB, cCovarGA }, + { cCovarRB, cCovarGB, cCovarBB, cCovarBA }, + { cCovarRA, cCovarGA, cCovarBA, cCovarAA } +}; + +// Channel pair Pearson correlation coefficients +enum +{ + cCorrRG = 0, cCorrRB = 1, cCorrRA = 2, + cCorrGB = 3, cCorrGA = 4, + cCorrBA = 5, + cTotalCorr = 6 +}; + +static void compute_block_moments( + float pMoments[cTotalCovar], float pSums[4], + const pixelbuf& pixel_buf, + uint32_t num_comps, + bool zero_centered) +{ + assert((num_comps == 3) || (num_comps == 4)); + + if (zero_centered) + { + if (num_comps == 3) + { + for (uint32_t y = 0; y < pixel_buf.m_height; y++) + { + for (uint32_t x = 0; x < pixel_buf.m_width; x++) + { + const float r = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cR); + const float g = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cG); + const float b = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cB); + + pMoments[cCovarRR] += r * r; pMoments[cCovarRG] += r * g; pMoments[cCovarRB] += r * b; + pMoments[cCovarGG] += g * g; pMoments[cCovarGB] += g * b; + pMoments[cCovarBB] += b * b; + } // x + } // y + } + else + { + assert(num_comps == 4); + + for (uint32_t y = 0; y < pixel_buf.m_height; y++) + { + for (uint32_t x = 0; x < pixel_buf.m_width; x++) + { + const float r = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cR); + const float g = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cG); + const float b = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cB); + const float a = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cA); + + pMoments[cCovarRR] += r * r; pMoments[cCovarRG] += r * g; pMoments[cCovarRB] += r * b; + pMoments[cCovarGG] += g * g; pMoments[cCovarGB] += g * b; + pMoments[cCovarBB] += b * b; + + pMoments[cCovarRA] += r * a; pMoments[cCovarGA] += g * a; pMoments[cCovarBA] += b * a; pMoments[cCovarAA] += a * a; + } // x + } // y + } + } + else + { + if (num_comps == 3) + { + for (uint32_t y = 0; y < pixel_buf.m_height; y++) + { + for (uint32_t x = 0; x < pixel_buf.m_width; x++) + { + const float r = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cR); + const float g = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cG); + const float b = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cB); + + pSums[cR] += r; pSums[cG] += g; pSums[cB] += b; + + pMoments[cCovarRR] += r * r; pMoments[cCovarRG] += r * g; pMoments[cCovarRB] += r * b; + pMoments[cCovarGG] += g * g; pMoments[cCovarGB] += g * b; + pMoments[cCovarBB] += b * b; + } // x + } // y + } + else + { + assert(num_comps == 4); + + for (uint32_t y = 0; y < pixel_buf.m_height; y++) + { + for (uint32_t x = 0; x < pixel_buf.m_width; x++) + { + const float r = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cR); + const float g = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cG); + const float b = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cB); + const float a = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cA); + + pSums[cR] += r; pSums[cG] += g; pSums[cB] += b; pSums[cA] += a; + + pMoments[cCovarRR] += r * r; pMoments[cCovarRG] += r * g; pMoments[cCovarRB] += r * b; + pMoments[cCovarGG] += g * g; pMoments[cCovarGB] += g * b; + pMoments[cCovarBB] += b * b; + + pMoments[cCovarRA] += r * a; pMoments[cCovarGA] += g * a; pMoments[cCovarBA] += b * a; pMoments[cCovarAA] += a * a; + } // x + } // y + } + } +} + +static inline void calc_unnormalized_covariance( + const float pMoments[cTotalCovar], const float pSums[4], + float pDst_covar[cTotalCovar], + uint32_t num_comps, + uint32_t total_pixels, + bool zero_centered) +{ + assert((num_comps == 3) || (num_comps == 4)); + assert(total_pixels > 0); + + if (zero_centered) + { + memcpy(pDst_covar, pMoments, sizeof(float) * cTotalCovar); + return; + } + + const float oo_total_texels = 1.0f / (float)total_pixels; + + const float sum_r = pSums[cR], sum_g = pSums[cG], sum_b = pSums[cB]; + + pDst_covar[cCovarRR] = pMoments[cCovarRR] - (sum_r * sum_r * oo_total_texels); + pDst_covar[cCovarRG] = pMoments[cCovarRG] - (sum_r * sum_g * oo_total_texels); + pDst_covar[cCovarRB] = pMoments[cCovarRB] - (sum_r * sum_b * oo_total_texels); + pDst_covar[cCovarGG] = pMoments[cCovarGG] - (sum_g * sum_g * oo_total_texels); + pDst_covar[cCovarGB] = pMoments[cCovarGB] - (sum_g * sum_b * oo_total_texels); + pDst_covar[cCovarBB] = pMoments[cCovarBB] - (sum_b * sum_b * oo_total_texels); + + pDst_covar[cCovarRA] = 0; pDst_covar[cCovarGA] = 0; pDst_covar[cCovarBA] = 0; pDst_covar[cCovarAA] = 0; + if (num_comps == 4) + { + const float sum_a = pSums[cA]; + pDst_covar[cCovarRA] = pMoments[cCovarRA] - (sum_r * sum_a * oo_total_texels); + pDst_covar[cCovarGA] = pMoments[cCovarGA] - (sum_g * sum_a * oo_total_texels); + pDst_covar[cCovarBA] = pMoments[cCovarBA] - (sum_b * sum_a * oo_total_texels); + pDst_covar[cCovarAA] = pMoments[cCovarAA] - (sum_a * sum_a * oo_total_texels); + } +} + +// corr [-1,1]: 0=RG, 1=RB, 2=RA, 3=GB, 4=GA, 5=BA +// note not clamped +// xy = cross term +// xx = first chan +// xy = second chan +static inline float corr_pair(float xy, float xx, float yy) +{ + float d = xx * yy; + if (d <= TINY_EPS) + return 1.0f; // one or both channels aren't active; returning 1.0f, not 0.0f, to simplify channel correlation checks later + + float r = xy / sqrtf(d); // note not clamped + return r; +} + +// Pearson correlation coefficients (assumed mean removed covar) +static inline void corr_from_covar(float corr[6], const float covar[10], uint32_t num_comps) +{ + assert((num_comps == 3) || (num_comps == 4)); + + corr[cCorrRG] = corr_pair(covar[cCovarRG], covar[cCovarRR], covar[cCovarGG]); // RG + corr[cCorrRB] = corr_pair(covar[cCovarRB], covar[cCovarRR], covar[cCovarBB]); // RB + corr[cCorrGB] = corr_pair(covar[cCovarGB], covar[cCovarGG], covar[cCovarBB]); // GB + + corr[cCorrRA] = corr[cCorrGA] = corr[cCorrBA] = 1.0f; // chan pairs with A default to 1.0f, not 0 + + if (num_comps == 4) + { + corr[cCorrRA] = corr_pair(covar[cCovarRA], covar[cCovarRR], covar[cCovarAA]); // RA + corr[cCorrGA] = corr_pair(covar[cCovarGA], covar[cCovarGG], covar[cCovarAA]); // GA + corr[cCorrBA] = corr_pair(covar[cCovarBA], covar[cCovarBB], covar[cCovarAA]); // BA + } +} + +// computes unnormalized mean or zero centered 4D covar (really scatter) and block mean (covar matrix elements are NOT divided by the total # of texels in the block) +// mean will be 0 if zero_centered is true +static inline void compute_covariance(float pDst_covar[cTotalCovar], float mean[4], const pixelbuf& block, uint32_t num_comps, bool zero_centered) +{ + assert((num_comps == 3) || (num_comps == 4)); + + float moments[cTotalCovar]; + + memset(moments, 0, sizeof(moments)); + memset(mean, 0, sizeof(float) * 4); + + compute_block_moments(moments, mean, block, num_comps, zero_centered); + + const uint32_t total_pixels = block.m_width * block.m_height; + calc_unnormalized_covariance(moments, mean, pDst_covar, num_comps, total_pixels, zero_centered); + + if (!zero_centered) + { + const float one_over_total = 1.0f / (float)total_pixels; + for (uint32_t c = 0; c < num_comps; c++) + mean[c] *= one_over_total; + } +} + +// 4x4 * 4x1 = 4x1 +// Computes covar_matrix * vec4. +static inline void covar_mul_vec4(float pDst[4], const float pCovar[cTotalCovar], const float pSrc[4]) +{ + const float x = pSrc[0], y = pSrc[1], z = pSrc[2], w = pSrc[3]; + + pDst[0] = pCovar[cCovarRR] * x + pCovar[cCovarRG] * y + pCovar[cCovarRB] * z + pCovar[cCovarRA] * w; + pDst[1] = pCovar[cCovarRG] * x + pCovar[cCovarGG] * y + pCovar[cCovarGB] * z + pCovar[cCovarGA] * w; + pDst[2] = pCovar[cCovarRB] * x + pCovar[cCovarGB] * y + pCovar[cCovarBB] * z + pCovar[cCovarBA] * w; + pDst[3] = pCovar[cCovarRA] * x + pCovar[cCovarGA] * y + pCovar[cCovarBA] * z + pCovar[cCovarAA] * w; +} + +// 3x3 * 3x1 = 3x1 +// Computes covar_matrix * vec3. +// pDst[3] is explicitly cleared so callers can safely treat pDst as vec4. +static inline void covar_mul_vec3(float pDst[4], const float pCovar[cTotalCovar], const float pSrc[4]) +{ + const float x = pSrc[0], y = pSrc[1], z = pSrc[2]; + + pDst[0] = pCovar[cCovarRR] * x + pCovar[cCovarRG] * y + pCovar[cCovarRB] * z; + pDst[1] = pCovar[cCovarRG] * x + pCovar[cCovarGG] * y + pCovar[cCovarGB] * z; + pDst[2] = pCovar[cCovarRB] * x + pCovar[cCovarGB] * y + pCovar[cCovarBB] * z; + pDst[3] = 0.0f; +} + +static inline void get_initial_axis_from_largest_diag(float v[4], const float covar[cTotalCovar], uint32_t num_comps) +{ + assert((num_comps == 3) || (num_comps == 4)); + + [[maybe_unused]] static const uint8_t s_diag_indices[4] = { cCovarRR, cCovarGG, cCovarBB, cCovarAA }; + + uint32_t best_col = 0; + float best_diag = covar[cCovarRR]; + + if (covar[cCovarGG] > best_diag) + { + best_diag = covar[cCovarGG]; + best_col = 1; + } + + if (covar[cCovarBB] > best_diag) + { + best_diag = covar[cCovarBB]; + best_col = 2; + } + + if ((num_comps == 4) && (covar[cCovarAA] > best_diag)) + { + best_diag = covar[cCovarAA]; + best_col = 3; + } + + const uint8_t* pCol = s_covar_col_indices[best_col]; + + v[0] = covar[pCol[0]]; + v[1] = covar[pCol[1]]; + v[2] = covar[pCol[2]]; + v[3] = (num_comps == 4) ? covar[pCol[3]] : 0.0f; +} + +// all 4 elements of pAxis set +static void compute_principle_axis(float pAxis[4], const float pCovar[cTotalCovar], uint32_t max_pair_iters, uint32_t num_comps) +{ + assert(max_pair_iters); + assert((num_comps == 3) || (num_comps == 4)); + + //vec4_set(pAxis, 1.0f, 1.0f, 1.0f, (num_comps == 4) ? 1.0f : 0.0f); + get_initial_axis_from_largest_diag(pAxis, pCovar, num_comps); + + //vec_normalize(pAxis, num_comps); + float s = maximum(fabs(pAxis[0]), fabs(pAxis[1]), fabs(pAxis[2]), fabs(pAxis[3])); + if (s > TINY_EPS) + vec4_scale(pAxis, 1.0f / s); + + float temp_vec[4], prev_axis[4]; + + for (uint32_t i = 0; i < max_pair_iters; ++i) + { + vec_copy(prev_axis, pAxis, 4); + + if (num_comps == 3) + { + covar_mul_vec3(temp_vec, pCovar, pAxis); + covar_mul_vec3(pAxis, pCovar, temp_vec); + } + else + { + covar_mul_vec4(temp_vec, pCovar, pAxis); + covar_mul_vec4(pAxis, pCovar, temp_vec); + } + + const float total_sq = vec_normalize(pAxis, num_comps); + + assert(!std::isinf(total_sq)); + + if (total_sq < TINY_EPS) + { + // should be very rare + vec4_set(pAxis, 1.0f, 1.0f, 1.0f, (num_comps == 4) ? 1.0f : 0.0f); + vec_normalize(pAxis, num_comps); + break; + } + + if (i) + { + float d = vec4_dot(pAxis, prev_axis); + const float DOT_THRESH = 0.9997f; + if (d >= DOT_THRESH) + break; + } + } +} + +struct block_stats +{ + float m_covar[cTotalCovar]; // unnormalized covar + float m_corr[cTotalCorr]; // Pearson's but unclamped, defaults to 1.0 (see corr_pair()) + float m_mean[4]; + float m_axis[4]; // 3D or 4D normalized +}; + +// endpoint values returned in direct ASTC endpoint order: lr hr lg hg lb hb la ha +static void calc_initial_cem_endpoints(const pixelbuf& block, float pCEM_values[8], uint32_t num_comps, block_stats *pOut_stats, bool zero_centered) +{ + //static inline void compute_covariance(float pDst_covar[cTotalCovar], float* pMean, const pixelbuf & block, uint32_t num_comps, bool zero_centered) + float cov[cTotalCovar]; + float block_mean[4]; + + compute_covariance(cov, block_mean, block, num_comps, zero_centered); + + if (pOut_stats) + { + memcpy(pOut_stats->m_covar, cov, sizeof(float) * cTotalCovar); + + corr_from_covar(pOut_stats->m_corr, cov, num_comps); + + // will be 0 if zero_centered + memcpy(pOut_stats->m_mean, block_mean, sizeof(float) * 4); + } + + cov[cCovarRR] += SMALL_EPS; + cov[cCovarGG] += SMALL_EPS; + cov[cCovarBB] += SMALL_EPS; + if (num_comps == 4) + cov[cCovarAA] += SMALL_EPS; + + float axis[4]; + + const uint32_t MAX_POWER_ITER_PAIRS = 5; + compute_principle_axis(axis, cov, MAX_POWER_ITER_PAIRS, num_comps); + + if (pOut_stats) + memcpy(pOut_stats->m_axis, axis, sizeof(float) * 4); + + float span[2] = { 1e+30f, -1e+30f }; + for (uint32_t y = 0; y < block.m_height; y++) + { + for (uint32_t x = 0; x < block.m_width; x++) + { + float d = 0; + for (uint32_t c = 0; c < num_comps; c++) + d += (pixelbuf_get_comp(block.m_pBuf, x, y, c) - block_mean[c]) * axis[c]; + + span[0] = basisu::minimum(span[0], d); + span[1] = basisu::maximum(span[1], d); + } + } + + // degenerate span check + if ((span[0] + 1.0f) > span[1]) + { + span[0] -= 0.5f; + span[1] += 0.5f; + } + + for (uint32_t e = 0; e < 2; e++) + { + for (uint32_t c = 0; c < num_comps; c++) + pCEM_values[c * 2 + e] = clamp(axis[c] * span[e] + block_mean[c], 0.0f, 255.0f); + + if (num_comps == 3) + { + pCEM_values[3 * 2 + 0] = 0; + pCEM_values[3 * 2 + 1] = 255; + } + } +} + +// 8 bytes - used for shortlist generation +struct astc_unpacked_config +{ + uint8_t m_cem; + uint8_t m_grid_width; + uint8_t m_grid_height; + uint8_t m_endpoint_range; + + uint8_t m_weight_range; + uint8_t m_dual_plane; + uint8_t m_ccs_index; + uint8_t m_unused; +}; + +struct single_subset_shortlist_state +{ + uint32_t m_block_width, m_block_height; + uint32_t m_max_candidates; + + uint32_t m_num_src_block_comps; // whether the original src block has 3 or 4 active components + bool m_src_is_luma_only; + bool m_should_include_dual_plane; + + pixelbuf m_pbuf; + + float m_block_pixels[PIXELBUF_SIZE_IN_FLOATS]; + + // 2x2 to 12x12, [h - 2][w - 2], lost high frequency pixel energy (SSE) for RGBA + float m_downsample_sse[11][11]; + + // 0=CEM 6: RGB Base+Scale (3D PCA, zero RGB mean, A=255), CCS can be [0,2] + // 1=CEM 8: RGB Direct (3D PCA, A=255), CCS can be [0,2] + // 2=CEM 10: RGB Base+Scale+Two A (3D PCA, zero RGB mean, A=direct), CCS can be [0,3] + // 3=CEM 12: RGBA Direct (4D PCA, A=direct), CCS can be [0,3] + + // Single Plane + // [CEM index][chan] + float m_sp_spans[cCEMTotalIndices][4]; + float m_sp_slam_to_line_error[cCEMTotalIndices]; + bool m_sp_valid[cCEMTotalIndices]; + + // Dual Plane + // [CEM index][CCS Index][chan] + float m_dp_spans[cCEMTotalIndices][4][4]; + float m_dp_slam_to_line_error[cCEMTotalIndices][4]; + bool m_dp_valid[cCEMTotalIndices][4]; + + // mean centered block stats + // unclamped Pearson, order RG, RB, GB, RA, GA, BA; beware if a channel is invalid it's 1 not 0 + // normalized 3D or 4D (depending on if the block had alpha or not) + block_stats m_stats; + + float m_best_sse[MAX_CANDIDATES]; + astc_unpacked_config m_best_configs[MAX_CANDIDATES]; +}; + +static void fit_and_measure( + const pixelbuf &block, + uint32_t num_comps, // 3 (RGB only) or 4 (RGBA) + bool zero_centered, // true => line through origin + float pOut_span[4], // written for [0..num_chans-1]; rest untouched + float* pOut_ortho_error, // slam to line error + block_stats *pOut_stats, + float pOut_ortho_error_rgba[4]) // per-channel slam to line error (only num_comps channels valid) +{ + float endpoints[8]; + calc_initial_cem_endpoints(block, endpoints, num_comps, pOut_stats, zero_centered); + + pOut_span[3] = 0.0f; + + float endpoint_org[4] = { 0, 0, 0, 0 }; + float endpoint_dir[4] = { 0, 0, 0, 0 }; + for (uint32_t c = 0; c < num_comps; c++) + { + endpoint_org[c] = endpoints[c * 2 + 0]; + endpoint_dir[c] = endpoints[c * 2 + 1] - endpoints[c * 2 + 0]; + pOut_span[c] = endpoint_dir[c]; // signed; squared in quant formula + } + + const float inv_dir_sq_len = 1.0f / (vec_get_squared_len(endpoint_dir, num_comps) + TINY_EPS); + + float total_ortho_error_c[4] = { }; + + for (uint32_t y = 0; y < block.m_height; y++) + { + for (uint32_t x = 0; x < block.m_width; x++) + { + float d = 0.0f; + for (uint32_t c = 0; c < num_comps; c++) + { + float v = pixelbuf_get_comp(block.m_pBuf, x, y, c); + d += (v - endpoint_org[c]) * endpoint_dir[c]; + } + + const float dist_along = d * inv_dir_sq_len; + + for (uint32_t c = 0; c < num_comps; c++) + { + const float block_val = pixelbuf_get_comp(block.m_pBuf, x, y, c); + const float nearest_val = endpoint_dir[c] * dist_along + endpoint_org[c]; + total_ortho_error_c[c] += square(block_val - nearest_val); + } + } + } + + float total_ortho_error = 0; + + for (uint32_t c = 0; c < num_comps; c++) + total_ortho_error += total_ortho_error_c[c]; + + *pOut_ortho_error = total_ortho_error; + + if (pOut_ortho_error_rgba) + vec4_copy(pOut_ortho_error_rgba, total_ortho_error_c); +} + +static void fit_and_measure_2D( + const pixelbuf& block, uint32_t chan_to_zero, + bool zero_centered, // true => line through origin + float pOut_span[4], // written for [0..num_chans-1]; rest untouched + float* pOut_ortho_error, + block_stats* pOut_stats, + float *pOut_ortho_error_rgba) +{ + float temp_pixels[PIXELBUF_SIZE_IN_FLOATS]; + memcpy(temp_pixels, block.m_pBuf, PIXELBUF_SIZE_IN_FLOATS * sizeof(float)); + + pixelbuf temp_pbuf(block); + temp_pbuf.m_pBuf = temp_pixels; + + pixelbuf_set_comp_to_val(temp_pbuf, chan_to_zero, 0.0f); + + const int num_chans = 3; + return fit_and_measure(temp_pbuf, num_chans, zero_centered, pOut_span, pOut_ortho_error, pOut_stats, pOut_ortho_error_rgba); +} + +static void fit_and_measure_1D(const pixelbuf &block, uint32_t comp_index, float* pOut_span, float* pOut_slam_to_255_sse) +{ + float lo = 1e+30f, hi = -1e+30f, a_err = 0.0f; + + for (uint32_t y = 0; y < block.m_height; y++) + { + for (uint32_t x = 0; x < block.m_width; x++) + { + const float v = pixelbuf_get_comp(block.m_pBuf, x, y, comp_index); + + lo = basisu::minimum(lo, v); + hi = basisu::maximum(hi, v); + + a_err += square(v - 255.0f); + } + } + + if (pOut_span) + *pOut_span = hi - lo; + + if (pOut_slam_to_255_sse) + *pOut_slam_to_255_sse = a_err; +} + +#if 0 +static float compute_ac_energy_from_dct(uint32_t block_width, uint32_t block_height, float* pDCT) +{ + pixel + float total_energy = 0.0f; + for (uint32_t i = 1; i < num_texels; i++) + { + const float v = square(pDCT[i]); + pDCT[i] = v; + total_energy += v; + } + + pDCT[0] = 0.0f; + return total_energy; +} +#endif + +#define BASISU_USE_ENERGY_PREFIX_SUM (1) +typedef double prefix_sum_t; // probably not really necessary, but due to the amount of summation involved double is useful here (and still way faster than not using a prefix sum table) + +// Build 2D prefix sums over a row-major block of energies. +// prefix[x + y * block_width] contains the sum over [0..x] x [0..y], inclusive. +// Slams DC energy to 0, returns total_ac_energy. +static inline prefix_sum_t prepare_dct_energy_prefix_table(uint32_t block_width, uint32_t block_height, float* pEnergy, prefix_sum_t* pPrefix) +{ + pixelbuf_set_comp(pEnergy, 0, 0, 0, 0.0f); // ignore DC + + prefix_sum_t total_ac_energy = 0.0f; + + for (uint32_t y = 0; y < block_height; y++) + { + prefix_sum_t row_sum = 0.0f; + + for (uint32_t x = 0; x < block_width; x++) + { + const float ac_energy = pixelbuf_get_comp(pEnergy, x, y, 0); + + total_ac_energy += ac_energy; + + row_sum += ac_energy; + + const prefix_sum_t val_above = y ? pixelbuf_get_comp(pPrefix, x, y - 1, 0) : 0.0f; + + pixelbuf_set_comp(pPrefix, x, y, 0, row_sum + val_above); + } // x + } // y + + return total_ac_energy; +} + +// Sum of the origin-anchored rectangle [0, grid_w) x [0, grid_h). +static inline prefix_sum_t query_dct_energy_prefix_sum(uint32_t block_width, uint32_t block_height, const prefix_sum_t* pPrefix, uint32_t grid_w, uint32_t grid_h) +{ + BASISU_NOTE_UNUSED(block_width); + BASISU_NOTE_UNUSED(block_height); + + assert((grid_w >= 1) && (grid_w <= block_width)); + assert((grid_h >= 1) && (grid_h <= block_height)); + + return pixelbuf_get_comp(pPrefix, grid_w - 1, grid_h - 1, 0); +} + +static inline prefix_sum_t compute_lost_dct_energy_prefix_sum(uint32_t block_width, uint32_t block_height, const prefix_sum_t* pEnergy_prefix_sum, uint32_t grid_w, uint32_t grid_h, prefix_sum_t total_ac_energy) +{ + const prefix_sum_t kept_energy = query_dct_energy_prefix_sum(block_width, block_height, pEnergy_prefix_sum, grid_w, grid_h); + + return maximum(total_ac_energy - kept_energy, 0.0f); +} + +static bool should_include_dual_plane(const single_subset_shortlist_state& shortlist_state, bool has_a) +{ + const block_stats& stats = shortlist_state.m_stats; + + const float var_r = stats.m_covar[cCovarRR], var_g = stats.m_covar[cCovarGG], var_b = stats.m_covar[cCovarBB]; + const bool has_r = (var_r != 0.0f), has_g = (var_g != 0.0f), has_b = (var_b != 0.0f); + + const uint32_t total_active_chans = has_r + has_g + has_b + has_a; + + if (total_active_chans <= 1) + return false; + + //const float MIN_A_DP_CORR = .995f; + //const float MIN_RGB_DP_CORR = .985f; + + const float MIN_A_DP_CORR = .9999f; + const float MIN_RGB_DP_CORR = .9999f; + + if (has_a) + { + float min_corr_vs_a = basisu::minimum(basisu::minimum(fabsf(stats.m_corr[cCorrRA]), fabsf(stats.m_corr[cCorrGA])), fabsf(stats.m_corr[cCorrBA])); + if (min_corr_vs_a < MIN_A_DP_CORR) + return true; + } + + const float rg_corr = fabs(stats.m_corr[cCorrRG]); + const float rb_corr = fabs(stats.m_corr[cCorrRB]); + const float gb_corr = fabs(stats.m_corr[cCorrGB]); + + float min_p = basisu::minimum(rg_corr, rb_corr, gb_corr); + + if (min_p < MIN_RGB_DP_CORR) + return true; + + return false; +} + +// num_comps==4 if block has alpha and cems 4/10/12 are valid +// if num_comps=3 only stats for cem 0/6/8 are valid (if the block doesn't have alpha, it makes no sense to use cem 4/10/12) +static void compute_block_metrics(single_subset_shortlist_state&state, uint32_t num_comps, const basist::astc_ldr_t::dct2f &dct, const single_subset_enc_context &ctx) +{ + float temp_buf[PIXELBUF_SIZE_IN_FLOATS]; + memcpy(temp_buf, state.m_block_pixels, PIXELBUF_SIZE_IN_FLOATS * sizeof(float)); + + pixelbuf temp_pbuf(state.m_block_width, state.m_block_height, temp_buf); + + memset(state.m_sp_spans, 0, sizeof(state.m_sp_spans)); + memset(state.m_sp_slam_to_line_error, 0, sizeof(state.m_sp_slam_to_line_error)); + memset(state.m_sp_valid, 0, sizeof(state.m_sp_valid)); + + memset(state.m_dp_spans, 0, sizeof(state.m_dp_spans)); + memset(state.m_dp_slam_to_line_error, 0, sizeof(state.m_dp_slam_to_line_error)); + memset(state.m_dp_valid, 0, sizeof(state.m_dp_valid)); + + float chan_ranges[4], a_slam_to_255_sse = 0; + + for (uint32_t c = 0; c < 4; c++) + fit_and_measure_1D(temp_pbuf, c, &chan_ranges[c], (c == 3) ? &a_slam_to_255_sse : nullptr); + + // Single Plane + float cem6_ortho_error_rgba[4] = { }; + float cem10_ortho_error_rgba[4] = { }; + + // CEM 6: RGB Base+Scale, A=always 255 + fit_and_measure(temp_pbuf, 3, true, state.m_sp_spans[cCEM6], &state.m_sp_slam_to_line_error[cCEM6], nullptr, cem6_ortho_error_rgba); + state.m_sp_valid[cCEM6] = true; + + if (num_comps == 4) + { + // CEM 8: RGB Direct: A=always 255 + fit_and_measure(temp_pbuf, 3, false, state.m_sp_spans[cCEM8], &state.m_sp_slam_to_line_error[cCEM8], nullptr, nullptr); + state.m_sp_valid[cCEM8] = true; + + // CEM 10: RGB Base+Scale Plus Two A + fit_and_measure(temp_pbuf, 4, true, state.m_sp_spans[cCEM10], &state.m_sp_slam_to_line_error[cCEM10], nullptr, cem10_ortho_error_rgba); + state.m_sp_valid[cCEM10] = true; + + // CEM 12: RGBA Direct - also general block stats + fit_and_measure(temp_pbuf, 4, false, state.m_sp_spans[cCEM12], &state.m_sp_slam_to_line_error[cCEM12], &state.m_stats, nullptr); + state.m_sp_valid[cCEM12] = true; + } + else + { + // CEM 8: RGB Direct: A=always 255 + fit_and_measure(temp_pbuf, 3, false, state.m_sp_spans[cCEM8], &state.m_sp_slam_to_line_error[cCEM8], &state.m_stats, nullptr); + state.m_sp_valid[cCEM8] = true; + } + + // Dual Plane + const float CEM6_10_DP_RGB_CHAN_ORTHO_WEIGHT = .25f; + + for (uint32_t comp = 0; comp < num_comps; comp++) + { + pixelbuf_swap_comp_with_alpha(temp_pbuf, comp); + + // CCS must be [0,2] for CEM 6/8 dual plane (they can't encode alpha, it's set to 255) + if (comp < 3) + { + // CEM 6: Although R,G, or B are on a separate plane, they still share the same Scale factor. A=always 255. + state.m_dp_spans[cCEM6][comp][0] = state.m_sp_spans[cCEM6][0]; + state.m_dp_spans[cCEM6][comp][1] = state.m_sp_spans[cCEM6][1]; + state.m_dp_spans[cCEM6][comp][2] = state.m_sp_spans[cCEM6][2]; + state.m_dp_slam_to_line_error[cCEM6][comp] = a_slam_to_255_sse; + for (uint32_t c = 0; c < 3; c++) + { + const float chan_weight = (c == comp) ? CEM6_10_DP_RGB_CHAN_ORTHO_WEIGHT : 1.0f; + state.m_dp_slam_to_line_error[cCEM6][comp] += cem6_ortho_error_rgba[c] * chan_weight; + } + state.m_dp_valid[cCEM6][comp] = true; + + // CEM 8: RGB Direct, R,G,B on a separate plane, A=always 255. + // Source A has been rotated somewhere into RGB, so we zero that out, so it's only 2D PCA on the remaining channels on weight plane 0. + fit_and_measure_2D(temp_pbuf, comp, false, state.m_dp_spans[cCEM8][comp], &state.m_dp_slam_to_line_error[cCEM8][comp], nullptr, nullptr); + state.m_dp_spans[cCEM8][comp][comp] = 0; // force it to 0 in case forcing channel comp to 0 made the block entire solid, causing PCA to return the gray axis + + state.m_dp_spans[cCEM8][comp][3] = chan_ranges[comp]; + std::swap(state.m_dp_spans[cCEM8][comp][3], state.m_dp_spans[cCEM8][comp][comp]); + state.m_dp_slam_to_line_error[cCEM8][comp] += a_slam_to_255_sse; + state.m_dp_valid[cCEM8][comp] = true; + } + + if (num_comps == 4) + { + // CEM 10: RGB Base+Scale + Two A, R,G,B or A on a separate plane + if (comp == 3) + { + // This is just sp CEM6 + entirely separate alpha, so slam to line error is just RGB + state.m_dp_spans[cCEM10][comp][0] = state.m_sp_spans[cCEM6][0]; + state.m_dp_spans[cCEM10][comp][1] = state.m_sp_spans[cCEM6][1]; + state.m_dp_spans[cCEM10][comp][2] = state.m_sp_spans[cCEM6][2]; + state.m_dp_spans[cCEM10][comp][3] = chan_ranges[3]; + state.m_dp_slam_to_line_error[cCEM10][comp] = state.m_sp_slam_to_line_error[cCEM6]; + } + else + { + // this is sp CEM10 but one RGB channel is given its own weight plane after encoding, so slam to line error is reduced a bit on the ccs channel + state.m_dp_spans[cCEM10][comp][0] = state.m_sp_spans[cCEM10][0]; + state.m_dp_spans[cCEM10][comp][1] = state.m_sp_spans[cCEM10][1]; + state.m_dp_spans[cCEM10][comp][2] = state.m_sp_spans[cCEM10][2]; + state.m_dp_spans[cCEM10][comp][3] = state.m_sp_spans[cCEM10][3]; + + state.m_dp_slam_to_line_error[cCEM10][comp] = 0.0f; + for (uint32_t c = 0; c < 4; c++) + { + const float chan_weight = (c == comp) ? CEM6_10_DP_RGB_CHAN_ORTHO_WEIGHT : 1.0f; + state.m_dp_slam_to_line_error[cCEM10][comp] += cem10_ortho_error_rgba[c] * chan_weight; + } + } + state.m_dp_valid[cCEM10][comp] = true; + + // CEM 12: RGBA Direct, R,G,B, or A on a separate plane + fit_and_measure(temp_pbuf, 3, false, state.m_dp_spans[cCEM12][comp], &state.m_dp_slam_to_line_error[cCEM12][comp], nullptr, nullptr); + state.m_dp_spans[cCEM12][comp][3] = chan_ranges[comp]; + std::swap(state.m_dp_spans[cCEM12][comp][3], state.m_dp_spans[cCEM12][comp][comp]); + state.m_dp_valid[cCEM12][comp] = true; + } + + pixelbuf_swap_comp_with_alpha(temp_pbuf, comp); + + } // comp + + // Add slam alpha to 255 penalties for CEM6/CEM8. + state.m_sp_slam_to_line_error[cCEM6] += a_slam_to_255_sse; + state.m_sp_slam_to_line_error[cCEM8] += a_slam_to_255_sse; + + assert(dct.cols() == (int)state.m_block_width); + assert(dct.rows() == (int)state.m_block_height); + + // Apply Parseval's theorem to rapidly estimate SSE due to weight grid downsampling. + float dct_temp[12 * 12]; + + for (uint32_t c = 0; c < num_comps; c++) + dct.forward(temp_buf + PIXELBUF_COMP_PITCH * c, PIXELBUF_ROW_PITCH, temp_buf + PIXELBUF_COMP_PITCH * c, PIXELBUF_ROW_PITCH, dct_temp); + + memset(state.m_downsample_sse, 0, sizeof(state.m_downsample_sse)); + + for (uint32_t y = 0; y < state.m_block_height; y++) + { + for (uint32_t x = 0; x < state.m_block_width; x++) + { + float total_comp_energy = 0; + for (uint32_t c = 0; c < num_comps; c++) + total_comp_energy += square(pixelbuf_get_comp(temp_buf, x, y, c)); + + pixelbuf_set_comp(temp_buf, x, y, 0, total_comp_energy); + } // x + } // y + +#if BASISU_USE_ENERGY_PREFIX_SUM + prefix_sum_t prefix_sum_buf[PIXELBUF_COMP_PITCH]; // only 1 component is used + const prefix_sum_t total_ac_energy = prepare_dct_energy_prefix_table(state.m_block_width, state.m_block_height, temp_buf, prefix_sum_buf); +#endif + + for (uint32_t grid_h = 2; grid_h <= state.m_block_height; grid_h++) + { + for (uint32_t grid_w = 2; grid_w <= state.m_block_width; grid_w++) + { + // check for valid grid size + if ((grid_w * grid_h) > 64) + continue; + +#if BASISU_USE_ENERGY_PREFIX_SUM + const prefix_sum_t sse = compute_lost_dct_energy_prefix_sum(state.m_block_width, state.m_block_height, prefix_sum_buf, grid_w, grid_h, total_ac_energy); + state.m_downsample_sse[grid_h - 2][grid_w - 2] = (float)sse; +#else + float sse = 0; + for (uint32_t y = 0; y < state.m_block_height; y++) + { + for (uint32_t x = 0; x < state.m_block_width; x++) + { + if ((y < grid_h) && (x < grid_w)) + continue; + sse += pixelbuf_get_comp(temp_buf, x, y, 0); + } // x + } // y + state.m_downsample_sse[grid_h - 2][grid_w - 2] = sse; +#endif // BASISU_USE_ENERGY_PREFIX_SUM + + } // grid_w + } // grid_h + + //---- + + assert(state.m_num_src_block_comps == num_comps); + + if (state.m_num_src_block_comps == 3) + { + assert(state.m_stats.m_mean[3] == 0.0f); + assert(state.m_stats.m_covar[3] == 0.0f); + } + else + { + assert(state.m_stats.m_mean[3] < 255.0f); + } + + state.m_should_include_dual_plane = false; + if (!ctx.m_disable_dual_plane) + state.m_should_include_dual_plane = should_include_dual_plane(state, num_comps == 4); +} + +// ab_sum = (2.0f * float(w_levels) - 1.0f) / (3.0f * (float(w_levels) - 1.0f)), for [2,32] (entries 0,1 invalid) +static const float g_ab_sum_tab[33] = // [0,32] +{ + 0.0f, 0.0f, // 0-1 invalid + 1.000000f, 0.833333f, 0.777778f, 0.750000f, // 2-32 + 0.733333f, 0.722222f, 0.714286f, 0.708333f, + 0.703704f, 0.700000f, 0.696970f, 0.694444f, + 0.692308f, 0.690476f, 0.688889f, 0.687500f, + 0.686275f, 0.685185f, 0.684211f, 0.683333f, + 0.682540f, 0.681818f, 0.681159f, 0.680556f, + 0.680000f, 0.679487f, 0.679012f, 0.678571f, + 0.678161f, 0.677778f, 0.677419f +}; + +// 1/(levels-1) +static const float g_intervals_recip[257] = // [0,256] +{ + 0.0f, 0.0f, 1.000000f, 0.500000f, 0.333333f, 0.250000f, 0.200000f, 0.166667f, // levels 0-1 invalid + 0.142857f, 0.125000f, 0.111111f, 0.100000f, 0.090909f, 0.083333f, 0.076923f, 0.071429f, + 0.066667f, 0.062500f, 0.058824f, 0.055556f, 0.052632f, 0.050000f, 0.047619f, 0.045455f, + 0.043478f, 0.041667f, 0.040000f, 0.038462f, 0.037037f, 0.035714f, 0.034483f, 0.033333f, + 0.032258f, 0.031250f, 0.030303f, 0.029412f, 0.028571f, 0.027778f, 0.027027f, 0.026316f, + 0.025641f, 0.025000f, 0.024390f, 0.023810f, 0.023256f, 0.022727f, 0.022222f, 0.021739f, + 0.021277f, 0.020833f, 0.020408f, 0.020000f, 0.019608f, 0.019231f, 0.018868f, 0.018519f, + 0.018182f, 0.017857f, 0.017544f, 0.017241f, 0.016949f, 0.016667f, 0.016393f, 0.016129f, + 0.015873f, 0.015625f, 0.015385f, 0.015152f, 0.014925f, 0.014706f, 0.014493f, 0.014286f, + 0.014085f, 0.013889f, 0.013699f, 0.013514f, 0.013333f, 0.013158f, 0.012987f, 0.012821f, + 0.012658f, 0.012500f, 0.012346f, 0.012195f, 0.012048f, 0.011905f, 0.011765f, 0.011628f, + 0.011494f, 0.011364f, 0.011236f, 0.011111f, 0.010989f, 0.010870f, 0.010753f, 0.010638f, + 0.010526f, 0.010417f, 0.010309f, 0.010204f, 0.010101f, 0.010000f, 0.009901f, 0.009804f, + 0.009709f, 0.009615f, 0.009524f, 0.009434f, 0.009346f, 0.009259f, 0.009174f, 0.009091f, + 0.009009f, 0.008929f, 0.008850f, 0.008772f, 0.008696f, 0.008621f, 0.008547f, 0.008475f, + 0.008403f, 0.008333f, 0.008264f, 0.008197f, 0.008130f, 0.008065f, 0.008000f, 0.007937f, + 0.007874f, 0.007812f, 0.007752f, 0.007692f, 0.007634f, 0.007576f, 0.007519f, 0.007463f, + 0.007407f, 0.007353f, 0.007299f, 0.007246f, 0.007194f, 0.007143f, 0.007092f, 0.007042f, + 0.006993f, 0.006944f, 0.006897f, 0.006849f, 0.006803f, 0.006757f, 0.006711f, 0.006667f, + 0.006623f, 0.006579f, 0.006536f, 0.006494f, 0.006452f, 0.006410f, 0.006369f, 0.006329f, + 0.006289f, 0.006250f, 0.006211f, 0.006173f, 0.006135f, 0.006098f, 0.006061f, 0.006024f, + 0.005988f, 0.005952f, 0.005917f, 0.005882f, 0.005848f, 0.005814f, 0.005780f, 0.005747f, + 0.005714f, 0.005682f, 0.005650f, 0.005618f, 0.005587f, 0.005556f, 0.005525f, 0.005495f, + 0.005464f, 0.005435f, 0.005405f, 0.005376f, 0.005348f, 0.005319f, 0.005291f, 0.005263f, + 0.005236f, 0.005208f, 0.005181f, 0.005155f, 0.005128f, 0.005102f, 0.005076f, 0.005051f, + 0.005025f, 0.005000f, 0.004975f, 0.004950f, 0.004926f, 0.004902f, 0.004878f, 0.004854f, + 0.004831f, 0.004808f, 0.004785f, 0.004762f, 0.004739f, 0.004717f, 0.004695f, 0.004673f, + 0.004651f, 0.004630f, 0.004608f, 0.004587f, 0.004566f, 0.004545f, 0.004525f, 0.004505f, + 0.004484f, 0.004464f, 0.004444f, 0.004425f, 0.004405f, 0.004386f, 0.004367f, 0.004348f, + 0.004329f, 0.004310f, 0.004292f, 0.004274f, 0.004255f, 0.004237f, 0.004219f, 0.004202f, + 0.004184f, 0.004167f, 0.004149f, 0.004132f, 0.004115f, 0.004098f, 0.004082f, 0.004065f, + 0.004049f, 0.004032f, 0.004016f, 0.004000f, 0.003984f, 0.003968f, 0.003953f, 0.003937f, + 0.003922f +}; + +// Note: This uses 1/w_levels, not 1/(w_levels - 1), for the effective weight quantization step. +// Although the representable weights are spaced by 1/(w_levels - 1), the encoder +// refits endpoints after weight selection, so low-level weight modes behave more +// like scalar clustering with w_levels reconstruction bins. This is especially +// important for 2-level weights, where the fixed-endpoint model overestimates +// the weight error term by 4x. + +#if 1 +static inline float analytical_quant_est_sse(uint32_t astc_cem, uint32_t e_levels, uint32_t w_levels, const float spans[4], uint32_t num_pixels) +{ + assert((e_levels >= 2) && (e_levels <= 256) && (w_levels >= 2) && (w_levels <= 32)); + assert(spans); + assert((astc_cem == 0) || (astc_cem == 4) || (astc_cem == 6) || (astc_cem == 8) || (astc_cem == 10) || (astc_cem == 12)); + + const float Dep = g_intervals_recip[e_levels]; // endpoint quant step + //const float Dw = g_intervals_recip[w_levels]; // weight quant step + const float Dw = g_intervals_recip[w_levels + 1]; // weight quant step, adjusted to approximately factor in endpoint LS fit (especially less pessimestic for 2-3 level configs) + const float ab_sum = g_ab_sum_tab[w_levels]; + + // sanity checks + assert(fabs(Dep - (1.0f / (float)(e_levels - 1))) < .000125f); + //assert(fabs(Dw - (1.0f / (float)(w_levels - 1))) < .000125f); + assert(fabs(Dw - (1.0f / (float)(w_levels))) < .000125f); + assert(fabs(ab_sum - (2.0f * float(w_levels) - 1.0f) / (3.0f * (float(w_levels) - 1.0f))) < .000125f); + + //const int num_chans = ((astc_cem == 6) || (astc_cem == 8)) ? 3 : 4; + //const int num_chans = (astc_cem <= 8) ? 3 : 4; + const int num_chans = get_num_cem_chans(astc_cem); + + // For num_chans = 3 we know the decoder always outputs 255 for Alpha, so there isn't any endpoint quant error there, and spans[3] should always be 0. + if (num_chans == 3) + { + assert(spans[3] == 0.0f); + } + + // 5418.75f = ((255.0f * 255.0f) / 12.0f) + float pixel_sse = (e_levels == 256) ? 0.0f : ((Dep * Dep) * ab_sum * 5418.75f * (float)num_chans); + + const float k = (Dw * Dw) * (1.0f / 12.0f); + + float t = (spans[0] * spans[0] + spans[1] * spans[1] + spans[2] * spans[2]); + if (num_chans == 4) //(astc_cem > 8) + t += spans[3] * spans[3]; + + pixel_sse += k * t; + + return pixel_sse * float(num_pixels); +} +#else +static inline float analytical_quant_est_sse(uint32_t astc_cem, uint32_t e_levels, uint32_t w_levels, const float spans[4], uint32_t num_pixels) +{ + assert((e_levels >= 2) && (e_levels <= 256) && (w_levels >= 2) && (w_levels <= 32)); + assert(spans); + assert((astc_cem == 0) || (astc_cem == 4) || (astc_cem == 6) || (astc_cem == 8) || (astc_cem == 10) || (astc_cem == 12)); + + const float Dep = 1.0f / (float)(e_levels - 1); // endpoint quant step + //const float Dw = 1.0f / (float)(w_levels - 1); // weight quant step + const float Dw = 1.0f / (float)(w_levels); // weight quant step, adjusted to approximately factor in endpoint LS fit (especially less pessimestic for 2-3 level configs) + + assert(fabs(g_intervals_recip[w_levels + 1] - Dw) < TINY_EPS); + + // TODO: precompute + const float N = float(w_levels); + const float ab_sum = (2.0f * N - 1.0f) / (3.0f * (N - 1.0f)); // simple model, assumes uniform weights, estimates how much endpoint quantization error survives after interpolation, averaged over all weight levels. + + //const int num_chans = ((astc_cem == 6) || (astc_cem == 8)) ? 3 : 4; + const int num_chans = get_num_cem_chans(astc_cem); + + // For num_chans = 3 we know the decoder always outputs 255 for Alpha, so there isn't any endpoint quant error there, and spans[3] should always be 0. + if (num_chans == 3) + { + assert(spans[3] == 0.0f); + } + + float pixel_sse = (e_levels == 256) ? 0.0f : ((Dep * Dep) * ((1.0f / 12.0f) * ab_sum * (255.0f * 255.0f)) * (float)num_chans); + + const float k = (Dw * Dw) * (1.0f / 12.0f); + for (int i = 0; i < num_chans; i++) + pixel_sse += k * (float)(spans[i] * spans[i]); + + return pixel_sse * float(num_pixels); +} +#endif + +const float DEF_SCALE_WEIGHT = 1.0f; // 1.0-3.0 seems reasonable +const float DEF_QUANT_WEIGHT = 1.0f; + +// Does NOT include downsample SSE. +static inline float estimate_base_config_sse(single_subset_shortlist_state&state, const astc_unpacked_config& cfg) +{ + const uint32_t astc_cem = cfg.m_cem; + //assert((astc_cem >= 6) && (astc_cem <= 12)); + assert(is_cem_0_or_4(astc_cem) || is_cem_6_or_10(astc_cem) || is_cem_8_or_12(astc_cem)); + + //float error = 0.0f; + + uint32_t cem_index = 0; // = (astc_cem - 6) >> 1; + if (astc_cem >= 6) + cem_index = (astc_cem - 6) >> 1; + else if (astc_cem == 0) + cem_index = cCEM8; + else + { + assert(astc_cem == 4); + cem_index = cCEM12; + } + + const int num_endpoint_levels = astc_helpers::get_ise_levels(cfg.m_endpoint_range); + const int num_weight_levels = astc_helpers::get_ise_levels(cfg.m_weight_range); + + const bool dual_plane = cfg.m_dual_plane; + const uint32_t ccs_index = cfg.m_ccs_index; + + // sanity check, decoded A will always be 255 here, A span should be 0 + if ((dual_plane) && (ccs_index == 3)) + { + // forbidden combo, useless + assert((astc_cem != 0) && (astc_cem != 6) && (astc_cem != 8)); + } + + const int num_block_pixels = state.m_block_height * state.m_block_width; + + // sanity check + assert(dual_plane ? state.m_dp_valid[cem_index][ccs_index] : state.m_sp_valid[cem_index]); + + const float quant_error = analytical_quant_est_sse(astc_cem, num_endpoint_levels, num_weight_levels, + dual_plane ? state.m_dp_spans[cem_index][ccs_index] : state.m_sp_spans[cem_index], num_block_pixels); + + const float ortho_error = dual_plane ? state.m_dp_slam_to_line_error[cem_index][ccs_index] : state.m_sp_slam_to_line_error[cem_index]; + + return quant_error * DEF_QUANT_WEIGHT + ortho_error; +} + +// also adds in downsample SSE +static inline float estimate_full_config_sse(single_subset_shortlist_state& state, const astc_unpacked_config& cfg, float scale_weight) +{ + return state.m_downsample_sse[cfg.m_grid_height - 2][cfg.m_grid_width - 2] * scale_weight + estimate_base_config_sse(state, cfg); +} + +static inline float estimate_full_config_sse(single_subset_shortlist_state& state, const astc_unpacked_config& cfg, float base_sse, float scale_weight) +{ + return state.m_downsample_sse[cfg.m_grid_height - 2][cfg.m_grid_width - 2] * scale_weight + base_sse; +} + +[[maybe_unused]] static inline void unpack_config(astc_unpacked_config& cfg, uint32_t packed_config) +{ + cfg.m_cem = (uint8_t)extract_bits(packed_config, 0, 4); + cfg.m_grid_width = (uint8_t)extract_bits(packed_config, 4, 4); + cfg.m_grid_height = (uint8_t)extract_bits(packed_config, 8, 4); + cfg.m_endpoint_range = (uint8_t)extract_bits(packed_config, 12, 5); + cfg.m_weight_range = (uint8_t)extract_bits(packed_config, 17, 4); + cfg.m_dual_plane = (uint8_t)extract_bits(packed_config, 21, 1) != 0; + cfg.m_ccs_index = 0; + cfg.m_unused = 0; +} + +static inline void estimate_and_add_config( + single_subset_shortlist_state& shortlist_state, + float sse, const astc_unpacked_config&cfg, + float &max_candidate_sse, uint32_t &num_candidates, uint32_t max_candidates) +{ + if (num_candidates < max_candidates) + { + assert((num_candidates >= 0) && (num_candidates < std::size(shortlist_state.m_best_configs))); + + memcpy(&shortlist_state.m_best_configs[num_candidates], &cfg, sizeof(cfg)); + shortlist_state.m_best_sse[num_candidates] = sse; + + max_candidate_sse = maximum(max_candidate_sse, sse); + + num_candidates++; + + return; + } + + if (sse >= max_candidate_sse) + return; + + assert(num_candidates == max_candidates); + + astc_unpacked_config worst_cfg(cfg); + float worst_sse = sse; + + float prev_max_candidate_sse = max_candidate_sse; + max_candidate_sse = 0; + + for (uint32_t i = 0; i < num_candidates; i++) + { + if (shortlist_state.m_best_sse[i] > worst_sse) + { + std::swap(shortlist_state.m_best_sse[i], worst_sse); + std::swap(shortlist_state.m_best_configs[i], worst_cfg); + } + + max_candidate_sse = basisu::maximum(max_candidate_sse, shortlist_state.m_best_sse[i]); + } + + assert(worst_sse == prev_max_candidate_sse); + BASISU_NOTE_UNUSED(prev_max_candidate_sse); +} + +static void init_single_subset_shortlist_state( + const rgba32_image& src_block_rgba32, + const single_subset_enc_context& context, + single_subset_shortlist_state& shortlist_state, + bool src_is_luma_only, + uint32_t num_src_block_comps) +{ + shortlist_state.m_block_width = context.m_block_width; + shortlist_state.m_block_height = context.m_block_height; + shortlist_state.m_max_candidates = context.m_max_candidates; + shortlist_state.m_num_src_block_comps = num_src_block_comps; + shortlist_state.m_src_is_luma_only = src_is_luma_only; + + // TODO + pixelbuf& pbuf = shortlist_state.m_pbuf; + pbuf.m_width = context.m_block_width; + pbuf.m_height = context.m_block_height; + pbuf.m_pBuf = shortlist_state.m_block_pixels; + + pixelbuf_load_block(pbuf, src_block_rgba32, 0, 0, num_src_block_comps); + + compute_block_metrics(shortlist_state, num_src_block_comps, context.m_dct, context); + +#if defined(DEBUG) || defined(_DEBUG) + { + float min_a = 255.0f; + for (uint32_t y = 0; y < pbuf.m_height; y++) + for (uint32_t x = 0; x < pbuf.m_width; x++) + min_a = minimum(min_a, pixelbuf_get_comp(pbuf.m_pBuf, x, y, 3)); + + if (min_a == 255.0f) + { + assert(num_src_block_comps == 3); + } + else + { + assert(num_src_block_comps == 4); + } + } +#endif +} + +static uint32_t generate_single_subset_shortlist( + uint32_t total_configs, const uint32_t *pPacked_configs, + const single_subset_enc_context &context, + [[maybe_unused]] const rgba32_image &src_block_rgba32, + [[maybe_unused]] bool src_is_luma_only, + uint32_t num_src_block_comps, // 3 or 4, determined by caller + single_subset_shortlist_state& shortlist_state, + float scale_weight, uint32_t max_candidates) +{ + static_assert(sizeof(astc_unpacked_config) == sizeof(uint64_t), "sizeof(astc_unpacked_config) != sizeof(uint64_t)"); + assert((num_src_block_comps == 3) || (num_src_block_comps == 4)); + assert(max_candidates <= MAX_CANDIDATES); + + const bool has_alpha = (num_src_block_comps == 4); + const uint32_t num_actual_block_chans = has_alpha ? 4 : 3; + + uint32_t num_candidates = 0; + float max_candidate_sse = 0; + + astc_unpacked_config cfg; + clear_obj(cfg); + + for (uint32_t id = 0; id < total_configs; id++) + { + const uint32_t packed_config = pPacked_configs[id]; + + cfg.m_cem = (uint8_t)extract_bits(packed_config, 0, 4); + + if ((num_actual_block_chans < 4) && (does_cem_have_alpha(cfg.m_cem))) + continue; + + cfg.m_grid_width = (uint8_t)extract_bits(packed_config, 4, 4); + cfg.m_grid_height = (uint8_t)extract_bits(packed_config, 8, 4); + + assert(cfg.m_grid_width >= cfg.m_grid_height); + + const bool wh_flag = (cfg.m_grid_width <= context.m_block_width) && (cfg.m_grid_height <= context.m_block_height); + if (!wh_flag) + { + // hw_flag cannot be true, grid_width is >= grid_height, and block_width >= block_height, so swapping GW/GH isn't going to help. + assert(!((cfg.m_grid_height <= context.m_block_width) && (cfg.m_grid_width <= context.m_block_height))); + continue; + } + + const bool hw_flag = (cfg.m_grid_width != cfg.m_grid_height) && (cfg.m_grid_height <= context.m_block_width) && (cfg.m_grid_width <= context.m_block_height); + + cfg.m_endpoint_range = (uint8_t)extract_bits(packed_config, 12, 5); + cfg.m_weight_range = (uint8_t)extract_bits(packed_config, 17, 4); + cfg.m_dual_plane = (uint8_t)extract_bits(packed_config, 21, 1) != 0; + +#if 0 + // HACK HACK + //if (!cfg.m_dual_plane) + // continue; + if ((cfg.m_cem != 0) && (cfg.m_cem != 8) && (cfg.m_cem != 12)) + continue; + //if (cfg.m_cem == 6) + // continue; + //if (!cfg.m_dual_plane) + // continue; +#endif + + if (cfg.m_dual_plane) + { + assert(cfg.m_cem != 0); + + if (!shortlist_state.m_should_include_dual_plane) + continue; + + uint32_t ccs_first = 0, ccs_last = num_actual_block_chans - 1; + + if (!does_cem_have_alpha(cfg.m_cem)) + { + ccs_last = 2; + } + else if (cfg.m_cem == 4) + { + assert(num_actual_block_chans == 4); + + ccs_first = 3; + } + + if (hw_flag) + { + astc_unpacked_config alt_cfg(cfg); + std::swap(alt_cfg.m_grid_width, alt_cfg.m_grid_height); + + for (uint32_t ccs_index = ccs_first; ccs_index <= ccs_last; ccs_index++) + { + cfg.m_ccs_index = (uint8_t)ccs_index; + + const float base_sse = estimate_base_config_sse(shortlist_state, cfg); + + const float wh_sse = estimate_full_config_sse(shortlist_state, cfg, base_sse, scale_weight); + estimate_and_add_config(shortlist_state, wh_sse, cfg, max_candidate_sse, num_candidates, max_candidates); + + alt_cfg.m_ccs_index = (uint8_t)ccs_index; + const float hw_sse = estimate_full_config_sse(shortlist_state, alt_cfg, base_sse, scale_weight); + estimate_and_add_config(shortlist_state, hw_sse, alt_cfg, max_candidate_sse, num_candidates, max_candidates); + + assert(equal_abs_tol(wh_sse, estimate_full_config_sse(shortlist_state, cfg, scale_weight), TINY_EPS)); + assert(equal_abs_tol(hw_sse, estimate_full_config_sse(shortlist_state, alt_cfg, scale_weight), TINY_EPS)); + } + } + else + { + for (uint32_t ccs_index = ccs_first; ccs_index <= ccs_last; ccs_index++) + { + cfg.m_ccs_index = (uint8_t)ccs_index; + estimate_and_add_config(shortlist_state, estimate_full_config_sse(shortlist_state, cfg, scale_weight), cfg, max_candidate_sse, num_candidates, max_candidates); + } + } + } + else + { + cfg.m_ccs_index = 0; + + if (hw_flag) + { + const float base_sse = estimate_base_config_sse(shortlist_state, cfg); + + const float wh_sse = estimate_full_config_sse(shortlist_state, cfg, base_sse, scale_weight); + assert(equal_abs_tol(wh_sse, estimate_full_config_sse(shortlist_state, cfg, scale_weight), TINY_EPS)); + estimate_and_add_config(shortlist_state, wh_sse, cfg, max_candidate_sse, num_candidates, max_candidates); + + std::swap(cfg.m_grid_width, cfg.m_grid_height); + const float hw_sse = estimate_full_config_sse(shortlist_state, cfg, base_sse, scale_weight); + assert(equal_abs_tol(hw_sse, estimate_full_config_sse(shortlist_state, cfg, scale_weight), TINY_EPS)); + estimate_and_add_config(shortlist_state, hw_sse, cfg, max_candidate_sse, num_candidates, max_candidates); + } + else + { + estimate_and_add_config(shortlist_state, estimate_full_config_sse(shortlist_state, cfg, scale_weight), cfg, max_candidate_sse, num_candidates, max_candidates); + } + } + + } // id + + assert((num_candidates > 0) && (num_candidates <= max_candidates)); + + return num_candidates; +} + +// Each subtable is the pseudoinverse of the corresponding 1D ASTC bilinear +// upsample matrix T, computed as Tplus = inverse(transpose(T) * T) * transpose(T). +// For a block size B and grid size G, the subtable contains B*G coefficients. +// The natural matrix shape is Tplus[G][B] ([row][col]) (destination major), but the downsample coefficients are stored +// transposed/source-major as filter[b * G + g] = Tplus[g][b]. +// T=1D ASTC bilinear upsample matrix +// Tplus = (T^T * T) ^ -1 * T^T (T^T=T transposed) +// Input: +// T: block_size x grid_size +// Output: +// Tplus: grid_size x block_size +static const float s_pseudoinverse_coeffs[1585] = +{ + #include "basisu_astc_ldr_pseudoinv_tab.inl" +}; + +// offsets into s_pseudoinverse_coeffs[] for each possible 1D downsampling scenario +static int16_t g_pseudoinverse_coeff_offsets[9][11] = // [block_size-4][grid_size-2] +{ + // dest grid width range: 2-12 source block texel size + { 0, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // 4 + { 20, 30, 45, -1, -1, -1, -1, -1, -1, -1, -1 }, // 5 + { 65, 77, 95, 119, -1, -1, -1, -1, -1, -1, -1 }, // 6 + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // 7 + { 149, 165, 189, 221, 261, 309, -1, -1, -1, -1, -1 }, // 8 + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // 9 + { 365, 385, 415, 455, 505, 565, 635, 715, -1, -1, -1 }, // 10 + { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // 11 + { 805, 829, 865, 913, 973, 1045, 1129, 1225, 1333, 1453, -1 } // 12 +}; + +static void pseudoinverse_block_to_grid(const pixelbuf& src, pixelbuf& dst, uint32_t num_comps) +{ + assert((num_comps == 1) || (num_comps == 3) || (num_comps == 4)); + + const uint32_t src_width = src.m_width, src_height = src.m_height; + const uint32_t dst_width = dst.m_width, dst_height = dst.m_height; + + // sanity checks + assert(&src != &dst); + + assert((src_width >= 4) && (src_width <= 12)); + assert((src_height >= 4) && (src_height <= 12)); + + assert((src_width != 7) && (src_width != 9) && (src_width != 11)); + assert((src_height != 7) && (src_height != 9) && (src_height != 11)); + + assert((dst_width >= 2) && (dst_width <= src_width)); + assert((dst_height >= 2) && (dst_height <= src_height)); + assert((dst_width * dst_height) <= 64); // ASTC max grid size limitation + + if ((src_width == dst_width) && (src_height == dst_height)) + { + memcpy(dst.m_pBuf, src.m_pBuf, PIXELBUF_SIZE_IN_FLOATS * sizeof(float)); + return; + } + + const int h_ofs = g_pseudoinverse_coeff_offsets[src_width - 4][dst_width - 2]; // across X (left/right) + assert((h_ofs < 0) || (h_ofs + src_width * dst_width) <= std::size(s_pseudoinverse_coeffs)); + const float* pH_coeffs = (h_ofs < 0) ? nullptr : &s_pseudoinverse_coeffs[h_ofs]; + + const int v_ofs = g_pseudoinverse_coeff_offsets[src_height - 4][dst_height - 2]; // across Y (up/down) + assert((v_ofs < 0) || (v_ofs + src_height * dst_height) <= std::size(s_pseudoinverse_coeffs)); + const float* pV_coeffs = (v_ofs < 0) ? nullptr : &s_pseudoinverse_coeffs[v_ofs]; + + float temp_buf[12][4]; // src_width x 1 + + // TODO: compute # of ops to do vertical vs. horizontal first, use minimum + for (uint32_t dst_y = 0; dst_y < dst_height; dst_y++) + { + // first filter vertically, outputs src_width x 1 samples + if (src_height == dst_height) + { + assert(v_ofs == -1); + + for (uint32_t src_x = 0; src_x < src_width; src_x++) + { + for (uint32_t c = 0; c < num_comps; c++) + temp_buf[src_x][c] = pixelbuf_get_comp(src.m_pBuf, src_x, dst_y, c); + } // src_x + } + else + { + assert(v_ofs != -1); + + if (num_comps == 1) + { + for (uint32_t src_x = 0; src_x < src_width; src_x++) + { + float r = 0; + + for (uint32_t src_y = 0; src_y < src_height; src_y++) + { + const float w = pV_coeffs[src_y * dst_height + dst_y]; + + r += pixelbuf_get_comp(src.m_pBuf, src_x, src_y, 0) * w; + } + + temp_buf[src_x][0] = r; + } + } + else + { + for (uint32_t src_x = 0; src_x < src_width; src_x++) + { + float r = 0, g = 0, b = 0, a = 0; + + for (uint32_t src_y = 0; src_y < src_height; src_y++) + { + const float w = pV_coeffs[src_y * dst_height + dst_y]; + + r += pixelbuf_get_comp(src.m_pBuf, src_x, src_y, 0) * w; + g += pixelbuf_get_comp(src.m_pBuf, src_x, src_y, 1) * w; + b += pixelbuf_get_comp(src.m_pBuf, src_x, src_y, 2) * w; + if (num_comps == 4) + a += pixelbuf_get_comp(src.m_pBuf, src_x, src_y, 3) * w; + } + + temp_buf[src_x][0] = r; + temp_buf[src_x][1] = g; + temp_buf[src_x][2] = b; + if (num_comps == 4) + temp_buf[src_x][3] = a; + } + } + } + + // input is now src_width x 1 in temp_buf + // filter horizontally, outputs dst_width x 1 + if (src_width == dst_width) + { + assert(h_ofs == -1); + + for (uint32_t dst_x = 0; dst_x < dst_width; dst_x++) + { + for (uint32_t c = 0; c < num_comps; c++) + pixelbuf_set_comp(dst.m_pBuf, dst_x, dst_y, c, temp_buf[dst_x][c]); + } // dst_x + } + else + { + assert(h_ofs != -1); + + if (num_comps == 1) + { + for (uint32_t dst_x = 0; dst_x < dst_width; dst_x++) + { + float r = 0; + + for (uint32_t src_x = 0; src_x < src_width; src_x++) + { + const float w = pH_coeffs[src_x * dst_width + dst_x]; + + r += temp_buf[src_x][0] * w; + + } // src_x + + pixelbuf_set_comp(dst.m_pBuf, dst_x, dst_y, 0, r); + + } // dst_x + } + else + { + for (uint32_t dst_x = 0; dst_x < dst_width; dst_x++) + { + float r = 0, g = 0, b = 0, a = 0; + + for (uint32_t src_x = 0; src_x < src_width; src_x++) + { + const float w = pH_coeffs[src_x * dst_width + dst_x]; + + r += temp_buf[src_x][0] * w; + g += temp_buf[src_x][1] * w; + b += temp_buf[src_x][2] * w; + if (num_comps == 4) + a += temp_buf[src_x][3] * w; + + } // src_x + + pixelbuf_set_comp(dst.m_pBuf, dst_x, dst_y, 0, r); + pixelbuf_set_comp(dst.m_pBuf, dst_x, dst_y, 1, g); + pixelbuf_set_comp(dst.m_pBuf, dst_x, dst_y, 2, b); + if (num_comps == 4) + pixelbuf_set_comp(dst.m_pBuf, dst_x, dst_y, 3, a); + + } // dst_x + } + } + } // dst_y +} + +// pEndpoints[8] decode order two vec4F's = LR LG LB LA, HR HG HB HA +// pCEM_vals_orig[] in rank space (not ISE) +static void cem_decode(uint32_t cem, const uint8_t pCEM_vals_quantized[8], uint32_t endpoint_range, float pEndpoints[8], float *pActual_scale) +{ + assert(astc_helpers::is_cem_ldr(cem)); + + const uint8_t* pCEM_vals = pCEM_vals_quantized; + + uint8_t dequantized_cem_vals[8]; // ISE 20 + + if (endpoint_range < astc_helpers::BISE_256_LEVELS) + { + pCEM_vals = dequantized_cem_vals; + + const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(endpoint_range); + + const uint32_t num_vals = astc_helpers::get_num_cem_values(cem); + for (uint32_t i = 0; i < num_vals; i++) + dequantized_cem_vals[i] = (uint8_t)endpoint_tab.get_rank_to_val(pCEM_vals_quantized[i]); + } + + int v0 = pCEM_vals[0], v1 = pCEM_vals[1], v2 = pCEM_vals[2], v3 = pCEM_vals[3]; + + switch (cem) + { + case 0: + case 4: + { + // 0 or 4 + pEndpoints[0] = (float)v0; + pEndpoints[1] = (float)v0; + pEndpoints[2] = (float)v0; + pEndpoints[3] = 0xFF; + + pEndpoints[4] = (float)v1; + pEndpoints[5] = (float)v1; + pEndpoints[6] = (float)v1; + pEndpoints[7] = 0xFF; + + if (cem == 4) + { + pEndpoints[3] = (float)v2; + pEndpoints[7] = (float)v3; + } + + break; + } + case 6: + case 10: + { + // 6 or 10 + pEndpoints[0] = (float)((v0 * v3) >> 8); + pEndpoints[1] = (float)((v1 * v3) >> 8); + pEndpoints[2] = (float)((v2 * v3) >> 8); + pEndpoints[3] = 0xFF; + + pEndpoints[4] = (float)v0; + pEndpoints[5] = (float)v1; + pEndpoints[6] = (float)v2; + pEndpoints[7] = 0xFF; + + if (cem == 10) + { + pEndpoints[3] = pCEM_vals[4]; + pEndpoints[7] = pCEM_vals[5]; + } + + if (pActual_scale) + *pActual_scale = (float)v3 * (1.0f / 256.0f); + + break; + } + case 8: + case 12: + { + // 8 or 12 + int v4 = pCEM_vals[4], v5 = pCEM_vals[5], v6 = 255, v7 = 255; + + if (cem == 12) + { + v6 = pCEM_vals[6]; + v7 = pCEM_vals[7]; + } + + if ((v1 + v3 + v5) >= (v0 + v2 + v4)) + { + pEndpoints[0] = (float)v0; + pEndpoints[1] = (float)v2; + pEndpoints[2] = (float)v4; + pEndpoints[3] = (float)v6; + + pEndpoints[4] = (float)v1; + pEndpoints[5] = (float)v3; + pEndpoints[6] = (float)v5; + pEndpoints[7] = (float)v7; + } + else + { + astc_helpers::blue_contract(v0, v2, v4); + astc_helpers::blue_contract(v1, v3, v5); + + pEndpoints[0] = (float)v1; + pEndpoints[1] = (float)v3; + pEndpoints[2] = (float)v5; + pEndpoints[3] = (float)v7; + + pEndpoints[4] = (float)v0; + pEndpoints[5] = (float)v2; + pEndpoints[6] = (float)v4; + pEndpoints[7] = (float)v6; + } + + if (pActual_scale) + *pActual_scale = 0; + + break; + } + default: + { + assert(cem <= 13); // LDR check + + int dec_endpoints[4][2]; // [c][l/h] + astc_helpers::decode_endpoint(cem, dec_endpoints, pCEM_vals); + + pEndpoints[0] = (float)dec_endpoints[0][0]; + pEndpoints[1] = (float)dec_endpoints[1][0]; + pEndpoints[2] = (float)dec_endpoints[2][0]; + pEndpoints[3] = (float)dec_endpoints[3][0]; + + pEndpoints[4] = (float)dec_endpoints[0][1]; + pEndpoints[5] = (float)dec_endpoints[1][1]; + pEndpoints[6] = (float)dec_endpoints[2][1]; + pEndpoints[7] = (float)dec_endpoints[3][1]; + + if (pActual_scale) + *pActual_scale = 0; + + break; + } + } + +#if defined(DEBUG) || defined(_DEBUG) + { + // sanity check + int dec_endpoints[4][2]; // [c][l/h] + astc_helpers::decode_endpoint(cem, dec_endpoints, pCEM_vals); + for (uint32_t i = 0; i < 4; i++) + { + assert((float)dec_endpoints[i][0] == pEndpoints[i]); + assert((float)dec_endpoints[i][1] == pEndpoints[4 + i]); + } + } +#endif +} + +// values in decoded L RGBA H RGBA order (not cem encode order) +static float calc_shortest_endpoint_dist(const float a[8], const float b[8], uint32_t num_comps) +{ + assert((num_comps == 3) || (num_comps == 4)); + float dist0, dist1; + + if (num_comps == 3) + { + dist0 = vec3_squared_dist(a + 0, b + 0) + vec3_squared_dist(a + 4, b + 4); + dist1 = vec3_squared_dist(a + 0, b + 4) + vec3_squared_dist(a + 4, b + 0); + } + else + { + dist0 = vec4_squared_dist(a + 0, b + 0) + vec4_squared_dist(a + 4, b + 4); + dist1 = vec4_squared_dist(a + 0, b + 4) + vec4_squared_dist(a + 4, b + 0); + } + + return minimum(dist0, dist1); +} + +static inline int quant_endpoint_val_to_rank(float value, uint32_t range, uint32_t num_levels) +{ + (void)range; + + if (basisu::is_pow2(num_levels)) + { + return clamp((int)(value * (1.0f / 255.0f) * (num_levels - 1) + 0.5f), 0, num_levels - 1); + } + else + { + // TODO: Compute optimal rounding tables + value = clamp(value, 0.0f, 255.0f); + int v = (int)value; + + const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(range); + + int r0 = endpoint_tab.get_val_to_rank(v); + float v0 = fabs((float)endpoint_tab.get_rank_to_val(r0) - value); + + int rp = minimum(r0 + 1, num_levels - 1); + float vp = fabs((float)endpoint_tab.get_rank_to_val(rp) - value); + + return (v0 < vp) ? r0 : rp; + } +} + +// returns [0,255] +static inline int dequant_endpoint_rank_to_val(uint32_t rank, uint32_t range) +{ + return astc_helpers::g_dequant_tables.get_endpoint_tab(range).get_rank_to_val(rank); +} + +// returns [0,64] +[[maybe_unused]] static inline int dequant_weight_rank_to_val(uint32_t rank, uint32_t range) +{ + assert((range <= astc_helpers::BISE_32_LEVELS) || (range == astc_helpers::BISE_64_LEVELS)); + + if (range == astc_helpers::BISE_64_LEVELS) + return rank; + else + return astc_helpers::g_dequant_tables.get_weight_tab(range).get_rank_to_val(rank); +} + +[[maybe_unused]] static inline int apply_delta_to_rank_value(int rank_val, int delta, uint32_t num_levels) +{ + return clamp(rank_val + delta, 0, num_levels - 1); +} + +static inline int compute_endpoint_sum(const uint8_t pCEM_values[6], uint32_t endpoint_ise_range) +{ + const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(endpoint_ise_range); + + int sum = 0; + + for (uint32_t i = 0; i < 6; i++) + { + int v = endpoint_tab.get_rank_to_val(pCEM_values[i]); + + if ((i & 1) == 0) // low endpoints subtract + v = -v; + + sum += v; + } + + return sum; +} + +static inline void cem_bc_encode(uint32_t cem_index, uint32_t endpoint_ise_index, uint8_t pCEM_values[8], uint32_t num_levels, bool use_bc) +{ + assert((cem_index == 8) || (cem_index == 9) || (cem_index == 12) || (cem_index == 13)); + + int cur_sum = compute_endpoint_sum(pCEM_values, endpoint_ise_index); + if (cur_sum == 0) + { + for (uint32_t i = 0; i < 6; i++) + { + int dir = (i & 1) ? 1 : -1; + + int cur_r = pCEM_values[i]; + int new_r = clamp(cur_r + dir, 0, num_levels - 1); + if (new_r == cur_r) + continue; + + pCEM_values[i] = (uint8_t)new_r; + + cur_sum = compute_endpoint_sum(pCEM_values, endpoint_ise_index); + if (cur_sum != 0) + break; + } + } + + assert(cur_sum != 0); + + bool cur_bc = cur_sum < 0; + + if (cur_bc != use_bc) + { + const uint32_t num_comps = (cem_index >= 12) ? 4 : 3; + + for (uint32_t i = 0; i < num_comps; i++) + std::swap(pCEM_values[2 * i + 0], pCEM_values[2 * i + 1]); + } + +#if defined(DEBUG) || defined(_DEBUG) + { + uint8_t cem_ise_vals[8]; + for (uint32_t i = 0; i < astc_helpers::get_num_cem_values(cem_index); i++) + cem_ise_vals[i] = astc_helpers::g_dequant_tables.get_endpoint_tab(endpoint_ise_index).m_rank_to_ISE[pCEM_values[i]]; + + const bool check_bc = astc_helpers::used_blue_contraction(cem_index, cem_ise_vals, endpoint_ise_index); + // should never happen, but if it does, it's not fatal (hurts quality especially in single shot mode) + assert(check_bc == use_bc); + } +#endif +} + +// pEndpoints[] = ASTC direct order: LR HR LG HG LB HB LA HA +// true if BC was used +static bool encode_cem_8_12(uint32_t cem_index, uint8_t* pCEM_values, uint32_t endpoint_ise_range, const float pEndpoints[8], bool allow_bc) +{ + const uint32_t num_endpoint_levels = astc_helpers::get_ise_levels(endpoint_ise_range); + const uint32_t num_endpoint_vals = (cem_index >= 12) ? 8 : 6; + + // don't bother with BC if 256 levels, pointless + bool use_bc = allow_bc && (num_endpoint_levels < 256); + float enc_endpoints[8]; + + if (use_bc) + { + for (uint32_t i = 0; i < 2; i++) + { + float r = pEndpoints[0 + i], g = pEndpoints[2 + i], b = pEndpoints[4 + i]; + + r = r * 2 - b; + g = g * 2 - b; + + float clamped_r = clamp(r, 0, 255); + float clamped_g = clamp(g, 0, 255); + + if ((r != clamped_r) || (g != clamped_g)) + { + use_bc = false; + break; + } + + enc_endpoints[0 + i] = clamped_r; + enc_endpoints[2 + i] = clamped_g; + enc_endpoints[4 + i] = b; + } + } + + if (!use_bc) + vec_copy(enc_endpoints, pEndpoints, 6); + + if (cem_index == 12) + { + enc_endpoints[6] = pEndpoints[6]; + enc_endpoints[7] = pEndpoints[7]; + } + + for (uint32_t i = 0; i < num_endpoint_vals; i++) + pCEM_values[i] = (uint8_t)quant_endpoint_val_to_rank(enc_endpoints[i], endpoint_ise_range, num_endpoint_levels); + + cem_bc_encode(cem_index, endpoint_ise_range, pCEM_values, num_endpoint_levels, use_bc); + + return use_bc; +} + +// pEndpoints[] = ASTC direct order: LR HR LG HG LB HB LA HA +// scale derived from L endpoint +static void encode_cem_6_10(uint32_t cem_index, uint8_t* pCEM_values, uint32_t endpoint_ise_range, const float pEndpoints[8]) +{ + assert((cem_index == 6) || (cem_index == 10)); + + const uint32_t num_endpoint_levels = astc_helpers::get_ise_levels(endpoint_ise_range); + + float h[3]; + + for (uint32_t c = 0; c < 3; c++) + { + pCEM_values[c] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[c * 2 + 1], endpoint_ise_range, num_endpoint_levels); + + h[c] = (float)dequant_endpoint_rank_to_val(pCEM_values[c], endpoint_ise_range); + } + + float l[3] = { pEndpoints[0], pEndpoints[2], pEndpoints[4] }; + + float hh_dot = vec3_dot(h, h); + float lh_dot = vec3_dot(l, h); + + float scale = (256.0f * lh_dot) / (hh_dot + TINY_EPS); + + pCEM_values[3] = (uint8_t)quant_endpoint_val_to_rank(scale, endpoint_ise_range, num_endpoint_levels); + + if (cem_index == 10) + { + pCEM_values[4] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[6], endpoint_ise_range, num_endpoint_levels); + pCEM_values[5] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[7], endpoint_ise_range, num_endpoint_levels); + } +} + +// pEndpoints[] = ASTC direct order: LR HR LG HG LB HB LA HA +static void encode_cem_0_4(uint32_t cem_index, uint8_t* pCEM_values, uint32_t endpoint_ise_range, const float pEndpoints[8]) +{ + assert((cem_index == 0) || (cem_index == 4)); + + const uint32_t num_endpoint_levels = astc_helpers::get_ise_levels(endpoint_ise_range); + + pCEM_values[0] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[0], endpoint_ise_range, num_endpoint_levels); + pCEM_values[1] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[1], endpoint_ise_range, num_endpoint_levels); + + if (cem_index == 4) + { + pCEM_values[2] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[6], endpoint_ise_range, num_endpoint_levels); + pCEM_values[3] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[7], endpoint_ise_range, num_endpoint_levels); + } +} + +// pEndpoints[] = ASTC direct order: LR HR LG HG LB HB LA HA +// returns false if base+ofs encode clamped +static bool encode_cem_9_13(uint32_t cem_index, uint8_t* pCEM_values, uint32_t endpoint_ise_range, const float pEndpoints[8], bool allow_bc) +{ + assert(is_cem_9_or_13(cem_index)); + + const uint32_t num_chans = get_num_cem_chans(cem_index); + + basist::color_rgba e[2]; + + e[0].a = 255; + e[1].a = 255; + + for (uint32_t c = 0; c < num_chans; c++) + { + e[0][c] = (uint8_t)clamp(fast_roundf_int(pEndpoints[c * 2 + 0]), 0, 255); + e[1][c] = (uint8_t)clamp(fast_roundf_int(pEndpoints[c * 2 + 1]), 0, 255); + } // c + + bool bc_clamped_flag, base_ofs_clamped_flag, endpoints_swapped_flag; + + bool status = basist::astc_ldr_t::pack_base_offset(cem_index, endpoint_ise_range, pCEM_values, e[0], e[1], allow_bc, true, bc_clamped_flag, base_ofs_clamped_flag, endpoints_swapped_flag); + BASISU_NOTE_UNUSED(status); + assert(status); + + return !base_ofs_clamped_flag; +} + +// source pEndpoints[] = ASTC direct order: LR HR LG HG LB HB LA HA +// output are quantized ASTC CEM values (rank space) +// true is CEM specific, for 9/13 it means the base+ofs pack didn't clamp +bool cem_encode(uint32_t cem_index, const float pEndpoints[8], uint32_t endpoint_ise_range, uint8_t* pCEM_values, bool allow_bc, bool high_effort) +{ + bool status = true; + + switch (cem_index) + { + case 0: + case 4: + { + encode_cem_0_4(cem_index, pCEM_values, endpoint_ise_range, pEndpoints); + break; + } + case 6: + case 10: + { + encode_cem_6_10(cem_index, pCEM_values, endpoint_ise_range, pEndpoints); + break; + } + case 8: + case 12: + { + const bool used_bc = encode_cem_8_12(cem_index, pCEM_values, endpoint_ise_range, pEndpoints, allow_bc); + + if ((high_effort) && (used_bc)) + { + // not necessary - small to tiny gain + float ep_bc[8]; + cem_decode(cem_index, pCEM_values, endpoint_ise_range, ep_bc, nullptr); + + uint8_t CEM_values_plain[8]; + + const bool used_bc2 = encode_cem_8_12(cem_index, CEM_values_plain, endpoint_ise_range, pEndpoints, false); + BASISU_NOTE_UNUSED(used_bc2); + assert(!used_bc2); + + float ep_plain[8]; + cem_decode(cem_index, CEM_values_plain, endpoint_ise_range, ep_plain, nullptr); + + const uint32_t num_comps = get_num_cem_chans(cem_index); + + float desired_endpoints[8]; + + for (uint32_t c = 0; c < num_comps; c++) + { + desired_endpoints[c + 0] = pEndpoints[c * 2 + 0]; + desired_endpoints[c + 4] = pEndpoints[c * 2 + 1]; + } + + const float dist_bc = calc_shortest_endpoint_dist(desired_endpoints, ep_bc, num_comps); + const float dist_plain = calc_shortest_endpoint_dist(desired_endpoints, ep_plain, num_comps); + + if (dist_plain < dist_bc) + { + memcpy(pCEM_values, CEM_values_plain, astc_helpers::get_num_cem_values(cem_index)); + } + } + + break; + } + case 9: + case 13: + { + status = encode_cem_9_13(cem_index, pCEM_values, endpoint_ise_range, pEndpoints, allow_bc); + break; + } + default: + { + assert(0); + memset(pCEM_values, 0, astc_helpers::get_num_cem_values(cem_index)); + status = false; + break; + } + } + + return status; +} + +static void eval_weights_first_plane( + const pixelbuf& pixels, + uint8_t* pWeights, uint32_t weight_ise_range, + uint32_t cem, const uint8_t *pCEM_values, uint32_t endpoint_ise_range, + uint32_t num_comps, + int chan_to_swap = -1) +{ + assert((num_comps == 3) || (num_comps == 4)); + + float dec_endpoints[8]; + cem_decode(cem, pCEM_values, endpoint_ise_range, dec_endpoints, nullptr); + + if (chan_to_swap >= 0) + { + assert(chan_to_swap <= 3); + std::swap(dec_endpoints[0 + chan_to_swap], dec_endpoints[0 + 3]); + std::swap(dec_endpoints[4 + chan_to_swap], dec_endpoints[4 + 3]); + } + + float dir[4] = { + dec_endpoints[4] - dec_endpoints[0], + dec_endpoints[5] - dec_endpoints[1], + dec_endpoints[6] - dec_endpoints[2], + (num_comps == 4) ? (dec_endpoints[7] - dec_endpoints[3]) : 0.0f }; + + uint32_t num_weight_levels = astc_helpers::get_ise_levels(weight_ise_range); + if (num_weight_levels == 64) + num_weight_levels = 65; // [0,64] (special case for raw ASTC weight mode, used for intermediate calcs) + + const uint32_t num_weight_levels_minus_one = num_weight_levels - 1; + + float dir_len2 = vec4_dot(dir, dir); + + if (dir_len2 < TINY_EPS) + { + memset(pWeights, 0, pixels.m_width * pixels.m_height); + return; + } + + vec_scale(dir, (float)num_weight_levels_minus_one / dir_len2, num_comps); + + const float w_bias = 0.5f - vec4_dot(dec_endpoints, dir); // 0.5f=rounding + + uint8_t* pDst_weights = pWeights; + + if (num_comps == 4) + { + for (uint32_t y = 0; y < pixels.m_height; y++) + { + for (uint32_t x = 0; x < pixels.m_width; x++) + { + float v[4]; + pixelbuf_get_pixel4(pixels.m_pBuf, x, y, v); + + float w = (v[0] * dir[0]) + (v[1] * dir[1]) + (v[2] * dir[2]) + (v[3] * dir[3]) + w_bias; + + int qw = (int)(w); + + if ((uint32_t)qw > num_weight_levels_minus_one) + qw = ((~qw) >> 31) & num_weight_levels_minus_one; + + *pDst_weights++ = (uint8_t)qw; + + } // x + } // y + } + else + { + for (uint32_t y = 0; y < pixels.m_height; y++) + { + for (uint32_t x = 0; x < pixels.m_width; x++) + { + float v[3]; + pixelbuf_get_pixel3(pixels.m_pBuf, x, y, v); + + float w = (v[0] * dir[0]) + (v[1] * dir[1]) + (v[2] * dir[2]) + w_bias; + + int qw = (int)(w); + + if ((uint32_t)qw > num_weight_levels_minus_one) + qw = ((~qw) >> 31) & num_weight_levels_minus_one; + + *pDst_weights++ = (uint8_t)qw; + + } // x + } // y + } +} + +static void eval_weights_for_plane( + const pixelbuf& pixels, + uint32_t weight_stride, // 1 or 2 + uint8_t* pWeights, uint32_t weight_ise_range, + uint32_t cem, const uint8_t* pCEM_values, uint32_t endpoint_ise_range, + uint32_t active_chans_mask, + int block_chan_to_swap_with_alpha) // only impacts reads from pixels +{ + float dec_endpoints[8]; + cem_decode(cem, pCEM_values, endpoint_ise_range, dec_endpoints, nullptr); + + float dir[4]; + vec4_sub(dir, dec_endpoints + 4, dec_endpoints); + + const uint32_t num_comps = 4; + + for (uint32_t i = 0; i < 4; i++) + if ((active_chans_mask & (1 << i)) == 0) + dir[i] = 0; + + uint32_t num_weight_levels = astc_helpers::get_ise_levels(weight_ise_range); + if (num_weight_levels == 64) + num_weight_levels = 65; // [0,64] (special case for raw ASTC weight mode, used for intermediate calcs) + + const uint32_t num_weight_levels_minus_one = num_weight_levels - 1; + + float dir_len2 = vec4_dot(dir, dir); + + if (dir_len2 < TINY_EPS) + { + const uint32_t total_pixels = pixels.m_width * pixels.m_height; + for (uint32_t i = 0; i < total_pixels; i++) + pWeights[i * weight_stride] = 0; + return; + } + + vec_scale(dir, (float)num_weight_levels_minus_one / dir_len2, num_comps); + + const float w_bias = 0.5f - vec4_dot(dec_endpoints, dir); // 0.5f=rounding + + uint8_t* pDst_weights = pWeights; + + for (uint32_t y = 0; y < pixels.m_height; y++) + { + for (uint32_t x = 0; x < pixels.m_width; x++) + { + float v[4]; + pixelbuf_get_pixel4(pixels.m_pBuf, x, y, v); + + if (block_chan_to_swap_with_alpha >= 0) + std::swap(v[3], v[block_chan_to_swap_with_alpha]); + + float w = (v[0] * dir[0]) + (v[1] * dir[1]) + (v[2] * dir[2]) + (v[3] * dir[3]) + w_bias; + + int qw = (int)(w); + + if ((uint32_t)qw > num_weight_levels_minus_one) + qw = ((~qw) >> 31) & num_weight_levels_minus_one; + + *pDst_weights = (uint8_t)qw; + pDst_weights += weight_stride; + + } // x + } // y +} + +[[maybe_unused]] static bool refine_endpoints_given_weights_cem_6_or_10_method2( + const pixelbuf& pixels, uint32_t cem, + const uint8_t *pWeights, uint32_t weight_ise_range, + const uint8_t* pCEM_values, uint32_t endpoint_ise_range, + float pNew_CEM_vals[6], // always writes 6 values + uint32_t num_comps) +{ + assert(is_cem_6_or_10(cem)); + assert((num_comps == 3) || (num_comps == 4)); + + float dec_endpoints[8], actual_scale = 0.0f; + cem_decode(cem, pCEM_values, endpoint_ise_range, dec_endpoints, &actual_scale); + + float actual_high[3] = { dec_endpoints[4], dec_endpoints[5], dec_endpoints[6] }; + + // not valid if weight_ise_range is 64 levels (which is a special case for [0,64] or 65 actual levels) + const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(minimum(astc_helpers::BISE_32_LEVELS, weight_ise_range)); + + float Pa[3], Pb[3]; + vec3_zero(Pa); + vec3_zero(Pb); + + float A = 0.0f, B = 0.0f, C = 0.0f; + + const uint8_t* pSrc_weights = pWeights; + for (uint32_t y = 0; y < pixels.m_height; y++) + { + for (uint32_t x = 0; x < pixels.m_width; x++) + { + float px[3]; + pixelbuf_get_pixel3(pixels.m_pBuf, x, y, px); + + const int qw = *pSrc_weights++; + const int iw = (weight_ise_range == astc_helpers::BISE_64_LEVELS) ? qw : weight_tab.get_rank_to_val(qw); + assert(iw <= 64); + + float t = (float)iw * (1.0f / 64.0f); + float bi = t, ai = 1.0f - t; + + vec3_scale_add(Pa, px, ai, Pa); + vec3_scale_add(Pb, px, bi, Pb); + + A += ai * ai; + B += ai * bi; + C += bi * bi; + } // x + } // y + + const float MAX_S = 255.0f / 256.0f; + + bool did_clamp = false; + + float new_high[3]; + vec3_copy(new_high, actual_high); + float new_scale = actual_scale; + + float h2 = vec3_dot(actual_high, actual_high); + if ((h2 > TINY_EPS) && (A > TINY_EPS)) + { + new_scale = (vec3_dot(Pa, actual_high) / h2 - B) / A; + new_scale = clamp(new_scale, 0.0f, MAX_S); // not setting did_clamp on intermediate new_scale + } + + const float den = A * new_scale * new_scale + 2.0f * B * new_scale + C; + if (den > TINY_EPS) + { + vec3_scale_add(new_high, Pa, new_scale, Pb); + vec3_div(new_high, den); + for (uint32_t i = 0; i < 3; i++) + new_high[i] = clamp_flag(new_high[i], 0.0f, 255.0f, did_clamp); + } + + h2 = vec3_dot(new_high, new_high); + if ((h2 > TINY_EPS) && (A > TINY_EPS)) + { + new_scale = (vec3_dot(Pa, new_high) / h2 - B) / A; + new_scale = clamp_flag(new_scale, 0.0f, MAX_S, did_clamp); + } + + for (uint32_t c = 0; c < 3; c++) + { + pNew_CEM_vals[c * 2 + 0] = new_scale * new_high[c]; + pNew_CEM_vals[c * 2 + 1] = new_high[c]; + } + + pNew_CEM_vals[6] = 255.0f; + pNew_CEM_vals[7] = 255.0f; + + if (num_comps == 4) + { + float z00 = 0, z01 = 0, z10 = 0, z11 = 0; + float q00_a = 0, q10_a = 0, t_a = 0; + + pSrc_weights = pWeights; + for (uint32_t y = 0; y < pixels.m_height; y++) + { + for (uint32_t x = 0; x < pixels.m_width; x++) + { + const float a = pixelbuf_get_comp(pixels.m_pBuf, x, y, 3); + + const uint32_t qw = *pSrc_weights++; + const int dw = (weight_ise_range <= astc_helpers::BISE_32_LEVELS) ? weight_tab.get_rank_to_val(qw) : qw; + const float w = dw * (1.0f / 64.0f); + + z00 += w * w; + z10 += (1.0f - w) * w; + z11 += (1.0f - w) * (1.0f - w); + + q00_a += w * a; + t_a += a; + } // x + } // y + + q10_a = t_a - q00_a; + z01 = z10; + + float xl, xh; + + float det = z00 * z11 - z01 * z10; + //if (fabs(det) >= TINY_EPS) + if (det >= TINY_EPS) + { + det = 1.0f / det; + + float iz00 = z11 * det; + float iz01 = -z01 * det; + float iz10 = -z10 * det; + float iz11 = z00 * det; + + xl = clamp_flag(iz10 * q00_a + iz11 * q10_a, 0.0f, 255.0f, did_clamp); + xh = clamp_flag(iz00 * q00_a + iz01 * q10_a, 0.0f, 255.0f, did_clamp); + } + else + { + xl = t_a / (float)(pixels.m_width * pixels.m_height); + xh = xl; + } + + pNew_CEM_vals[6] = xl; + pNew_CEM_vals[7] = xh; + } + + return did_clamp; +} + +static bool refine_endpoints_given_weights_cem_6_or_10_method3( + const pixelbuf& pixels, + uint32_t cem, + const uint8_t* pWeights, uint32_t weight_ise_range, + float pNew_CEM_vals[6], // always writes 6 values + uint32_t num_comps) +{ + BASISU_NOTE_UNUSED(cem); + assert(is_cem_6_or_10(cem)); + + assert((num_comps == 3) || (num_comps == 4)); + const uint32_t pixel_count = pixels.m_width * pixels.m_height; + const float pixel_count_f = (float)pixel_count; + + bool did_clamp = false; + + const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(minimum(astc_helpers::BISE_32_LEVELS, weight_ise_range)); + + float sum_w = 0.0f, sum_w2 = 0.0f; + float rgb_sum[3] = { }; + float weighted_rgb_sum[3] = { }; + + int min_dw = 256, max_dw = 0; + + const uint8_t* pSrc_weights = pWeights; + for (uint32_t y = 0; y < pixels.m_height; y++) + { + for (uint32_t x = 0; x < pixels.m_width; x++) + { + float p[3]; + pixelbuf_get_pixel(pixels.m_pBuf, x, y, p, 3); + + const uint32_t qw = *pSrc_weights++; + + const int dw = (weight_ise_range <= astc_helpers::BISE_32_LEVELS) ? weight_tab.get_rank_to_val(qw) : qw; + assert(dw <= 64); + + min_dw = minimum(min_dw, dw); + max_dw = maximum(max_dw, dw); + + const float w = dw * (1.0f / 64.0f); + + sum_w += w; + sum_w2 += w * w; + + vec3_add(rgb_sum, p); + vec3_scale_add(weighted_rgb_sum, p, w, weighted_rgb_sum); + + } // x + } // y + + float rgb_mean[3]; + vec3_div(rgb_mean, rgb_sum, pixel_count_f); + + const float A = pixel_count_f - 2.0f * sum_w + sum_w2; + const float B = sum_w - sum_w2; + const float C = sum_w2; + + float Pb[3]; + vec3_copy(Pb, weighted_rgb_sum); + + float Pa[3]; + vec3_sub(Pa, rgb_sum, weighted_rgb_sum); + + const float kMaxScale01 = 255.0f / 256.0f; + + float best_scale01 = kMaxScale01; + float best_obj = -1.0f; // objective is always >= 0 when valid + + auto try_scale = [&](float s) + { + // Reject out-of-range scales. Do not clamp roots into endpoint candidates. + if ((s < 0.0f) || (s > kMaxScale01)) + return; + + const float den = A * s * s + 2.0f * B * s + C; + if (den < TINY_EPS) + return; + + float v[3]; + vec3_scale_add(v, Pa, s, Pb); + + const float obj = vec3_dot(v, v) / den; + + if (obj > best_obj) + { + best_obj = obj; + best_scale01 = s; + } + }; + + float out_high_rgb[3]; + vec3_copy(out_high_rgb, rgb_mean); + float out_scale = kMaxScale01; + + if (max_dw == min_dw) + { + // weights all equal + const float w = (float)min_dw * (1.0f / 64.0f); + + const float scale01 = kMaxScale01; + + // Decoder effective multiplier: + // recon = lerp(scale01 * high, high, w) + // = high * (scale01 * (1 - w) + w) + // = high * (scale01 + w * (1 - scale01)) + const float f = scale01 + w * (1.0f - scale01); + + const float max_mean = fmaxf(rgb_mean[0], fmaxf(rgb_mean[1], rgb_mean[2])); + + if ((f > TINY_EPS) && (max_mean <= 255.0f * f)) + { + // Exact constant-color reconstruction without high endpoint clamp. + vec3_div(out_high_rgb, f); + } + // else: keep out_high_rgb = rgb_mean to avoid high endpoint clamp. + + vec3_clamp_flag(out_high_rgb, 0.0f, 255.0f, did_clamp); + + out_scale = scale01; + } + else + { + const float n2 = vec3_dot(Pa, Pa); + const float n1 = vec3_dot(Pa, Pb); + const float n0 = vec3_dot(Pb, Pb); + + const float q1 = n2 * C - n0 * A; + const float q0 = n1 * C - n0 * B; + const float q2 = n2 * B - n1 * A; + +#if 0 + if ((q2 > TINY_EPS) || (q2 < -TINY_EPS)) + { + //const double disc = (double)q1 * (double)q1 - 4.0f * (double)q2 * (double)q0; + const float disc = q1 * q1 - 4.0f * q2 * q0; + + if (disc >= 0.0f) + { + //const float sqrt_disc = (float)sqrt(disc); + const float sqrt_disc = sqrtf(disc); + + const float inv_2q2 = 0.5f / q2; + + try_scale((-q1 - sqrt_disc) * inv_2q2); + try_scale((-q1 + sqrt_disc) * inv_2q2); + } + } + else if ((q1 > TINY_EPS) || (q1 < -TINY_EPS)) + { + try_scale(-q0 / q1); + } +#else + const float aq0 = fabsf(q0); + const float aq1 = fabsf(q1); + const float aq2 = fabsf(q2); + + const float m = fmaxf(aq0, fmaxf(aq1, aq2)); + + if (m > 0.0f) + { + const float s = 1.0f / m; + + const float a = q2 * s; + const float b = q1 * s; + const float c = q0 * s; + + if (fabsf(a) > TINY_EPS) + { + const float disc = b * b - 4.0f * a * c; + + if (disc >= 0.0f) + { + const float sqrt_disc = sqrtf(disc); + + const float t = -0.5f * (b + copysignf(sqrt_disc, b)); + + if (t != 0.0f) + { + try_scale(t / a); + try_scale(c / t); + } + else + { + try_scale(-b * (0.5f / a)); + } + } + } + else if (fabsf(b) > TINY_EPS) + { + try_scale(-c / b); + } + } +#endif + + // If no valid stationary root exists, or weights are flat, + // use conservative fallback scales. + if (best_obj < 0.0f) + { + try_scale(kMaxScale01); + try_scale(0.875f); + try_scale(0.75f); + try_scale(0.5f); + try_scale(0.25f); + } + + if (best_obj >= 0.0f) + { + const float scale01 = best_scale01; + + const float high_den = + A * scale01 * scale01 + + 2.0f * B * scale01 + + C; + + if (high_den > TINY_EPS) + { + vec3_scale_add(out_high_rgb, Pa, scale01, Pb); + vec3_div(out_high_rgb, high_den); + } + + vec3_clamp_flag(out_high_rgb, 0.0f, 255.0f, did_clamp); + + out_scale = clamp_flag(scale01, 0.0f, kMaxScale01, did_clamp); + } + + } // if (max_dw == min_dw) + + for (uint32_t c = 0; c < 3; c++) + { + pNew_CEM_vals[c * 2 + 0] = out_scale * out_high_rgb[c]; + pNew_CEM_vals[c * 2 + 1] = out_high_rgb[c]; + } + + pNew_CEM_vals[6] = 255.0f; + pNew_CEM_vals[7] = 255.0f; + + if (num_comps == 4) + { + float z00 = 0, z01 = 0, z10 = 0, z11 = 0; + float q00_a = 0, q10_a = 0, t_a = 0; + +#if 0 + // TODO + z00 = sum_w2; + z10 = B; + z11 = A; + ... + q00_a = sum w * a + t_a = sum a + q10_a = t_a - q00_a + ... + const float z00 = C; + const float z01 = B; + const float z10 = B; + const float z11 = A; +#endif + + pSrc_weights = pWeights; + for (uint32_t y = 0; y < pixels.m_height; y++) + { + for (uint32_t x = 0; x < pixels.m_width; x++) + { + const float a = pixelbuf_get_comp(pixels.m_pBuf, x, y, 3); + + const uint32_t qw = *pSrc_weights++; + const int dw = (weight_ise_range <= astc_helpers::BISE_32_LEVELS) ? weight_tab.get_rank_to_val(qw) : qw; + const float w = dw * (1.0f / 64.0f); + + // TODO: some of these values we've already computed above + z00 += w * w; + z10 += (1.0f - w) * w; + z11 += (1.0f - w) * (1.0f - w); + + q00_a += w * a; + t_a += a; + } // x + } // y + + q10_a = t_a - q00_a; + z01 = z10; + + float xl, xh; + + float det = z00 * z11 - z01 * z10; + //if (fabs(det) >= TINY_EPS) // todo should always be pos? + if (det >= TINY_EPS) // todo should always be pos? + { + det = 1.0f / det; + + float iz00 = z11 * det; + float iz01 = -z01 * det; + float iz10 = -z10 * det; + float iz11 = z00 * det; + + xl = clamp_flag(iz10 * q00_a + iz11 * q10_a, 0.0f, 255.0f, did_clamp); + xh = clamp_flag(iz00 * q00_a + iz01 * q10_a, 0.0f, 255.0f, did_clamp); + } + else + { + xl = t_a / (float)(pixels.m_width * pixels.m_height); + xh = xl; + } + + pNew_CEM_vals[6] = xl; + pNew_CEM_vals[7] = xh; + } + + return did_clamp; +} + +static bool refine_endpoints_given_weights_cem_8_9_12_or_13( + const pixelbuf& pixels, + uint32_t cem, + const uint8_t* pWeights, uint32_t weight_ise_range, + float pNew_CEM_vals[8], // always writes 8 values + uint32_t num_comps) +{ + BASISU_NOTE_UNUSED(cem); + assert(is_cem_8_or_12(cem) || is_cem_9_or_13(cem)); + assert((num_comps == 3) || (num_comps == 4)); + + float z00 = 0, z01 = 0, z10 = 0, z11 = 0; + float q00_a[4] = { }, t_a[4] = { }; + + bool did_clamp = false; + + const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(minimum(astc_helpers::BISE_32_LEVELS, weight_ise_range)); + + const uint8_t *pSrc_weights = pWeights; + for (uint32_t y = 0; y < pixels.m_height; y++) + { + for (uint32_t x = 0; x < pixels.m_width; x++) + { + const uint32_t qw = *pSrc_weights++; + const int dw = (weight_ise_range <= astc_helpers::BISE_32_LEVELS) ? weight_tab.get_rank_to_val(qw) : qw; + assert(dw <= 64); + + const float w = dw * (1.0f / 64.0f); + + z00 += w * w; + z10 += (1.0f - w) * w; + z11 += (1.0f - w) * (1.0f - w); + + for (uint32_t c = 0; c < num_comps; c++) + { + const float a = pixelbuf_get_comp(pixels.m_pBuf, x, y, c); + + q00_a[c] += w * a; + t_a[c] += a; + } + } // x + } // y + + float q10_a[4]; + for (uint32_t c = 0; c < num_comps; c++) + q10_a[c] = t_a[c] - q00_a[c]; + + z01 = z10; + + float det = z00 * z11 - z01 * z10; // should be non-negative mathematically + + //if (fabs(det) >= TINY_EPS) + if (det >= TINY_EPS) + { + det = 1.0f / det; + + float iz00 = z11 * det; + float iz01 = -z01 * det; + float iz10 = -z10 * det; + float iz11 = z00 * det; + + for (uint32_t c = 0; c < num_comps; c++) + { + pNew_CEM_vals[c * 2 + 0] = clamp_flag(iz10 * q00_a[c] + iz11 * q10_a[c], 0.0f, 255.0f, did_clamp); + pNew_CEM_vals[c * 2 + 1] = clamp_flag(iz00 * q00_a[c] + iz01 * q10_a[c], 0.0f, 255.0f, did_clamp); + } + } + else + { + const float one_over_total_pixels = 1.0f / (float)(pixels.m_width * pixels.m_height); + for (uint32_t c = 0; c < num_comps; c++) + { + pNew_CEM_vals[c * 2 + 0] = t_a[c] * one_over_total_pixels; + pNew_CEM_vals[c * 2 + 1] = pNew_CEM_vals[c * 2 + 0]; + } + } + + if (num_comps == 3) + { + pNew_CEM_vals[3 * 2 + 0] = 255.0f; + pNew_CEM_vals[3 * 2 + 1] = 255.0f; + } + + return did_clamp; +} + +static bool refine_endpoints_given_weights_cem_0_or_4( + const pixelbuf& pixels, + uint32_t cem, + const uint8_t* pWeights, uint32_t weight_ise_range, + float pNew_CEM_vals[8], // always writes 8 values + uint32_t num_comps) +{ + BASISU_NOTE_UNUSED(cem); + BASISU_NOTE_UNUSED(num_comps); + assert(is_cem_0_or_4(cem)); + assert((num_comps == 3) || (num_comps == 4)); + + float z00 = 0, z01 = 0, z10 = 0, z11 = 0; + float q00_a[2] = { }, t_a[2] = { }; + + bool did_clamp = false; + + const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(minimum(astc_helpers::BISE_32_LEVELS, weight_ise_range)); + + const uint32_t num_actual_comps = (cem == 4) ? 2 : 1; + + const uint8_t* pSrc_weights = pWeights; + for (uint32_t y = 0; y < pixels.m_height; y++) + { + for (uint32_t x = 0; x < pixels.m_width; x++) + { + const uint32_t qw = *pSrc_weights++; + const int dw = (weight_ise_range <= astc_helpers::BISE_32_LEVELS) ? weight_tab.get_rank_to_val(qw) : qw; + const float w = dw * (1.0f / 64.0f); + + z00 += w * w; + z10 += (1.0f - w) * w; + z11 += (1.0f - w) * (1.0f - w); + + for (uint32_t c = 0; c < num_actual_comps; c++) + { + const float a = pixelbuf_get_comp(pixels.m_pBuf, x, y, c ? 3 : 0); + + q00_a[c] += w * a; + t_a[c] += a; + } + } // x + } // y + + float q10_a[2]; + for (uint32_t c = 0; c < num_actual_comps; c++) + q10_a[c] = t_a[c] - q00_a[c]; + + z01 = z10; + + // default A bounds for cem 0 + pNew_CEM_vals[6] = 255.0f; + pNew_CEM_vals[7] = 255.0f; + + float det = z00 * z11 - z01 * z10; // should be non-negative mathematically + //if (fabs(det) >= TINY_EPS) + if (det >= TINY_EPS) + { + det = 1.0f / det; + + float iz00 = z11 * det; + float iz01 = -z01 * det; + float iz10 = -z10 * det; + float iz11 = z00 * det; + + for (uint32_t c = 0; c < num_actual_comps; c++) + { + float l = clamp_flag(iz10 * q00_a[c] + iz11 * q10_a[c], 0.0f, 255.0f, did_clamp); + float h = clamp_flag(iz00 * q00_a[c] + iz01 * q10_a[c], 0.0f, 255.0f, did_clamp); + + if (c == 0) + { + pNew_CEM_vals[0] = l; + pNew_CEM_vals[1] = h; + } + else + { + pNew_CEM_vals[6] = l; + pNew_CEM_vals[7] = h; + } + } + } + else + { + const float one_over_total_pixels = 1.0f / (float)(pixels.m_width * pixels.m_height); + + for (uint32_t c = 0; c < num_actual_comps; c++) + { + float l = t_a[c] * one_over_total_pixels; + float h = l; + + if (c == 0) + { + pNew_CEM_vals[0] = l; + pNew_CEM_vals[1] = h; + } + else + { + pNew_CEM_vals[6] = l; + pNew_CEM_vals[7] = h; + } + } + } + + // set G and B to R bounds + pNew_CEM_vals[2] = pNew_CEM_vals[0]; + pNew_CEM_vals[3] = pNew_CEM_vals[1]; + + pNew_CEM_vals[4] = pNew_CEM_vals[0]; + pNew_CEM_vals[5] = pNew_CEM_vals[1]; + + return did_clamp; +} + +// true if clamping occured on outputs +static bool refine_endpoints_given_weights( + const pixelbuf& pixels, + uint32_t cem, const uint8_t* pWeights, uint32_t weight_ise_range, + [[maybe_unused]] const uint8_t* pCEM_values, [[maybe_unused]] uint32_t endpoint_ise_range, + float pNew_CEM_vals[8], + uint32_t num_comps) +{ + assert(is_cem_0_or_4(cem) || is_cem_6_or_10(cem) || is_cem_8_or_12(cem)); + + switch (cem) + { + case 0: + case 4: + { + assert(is_cem_0_or_4(cem)); + + return refine_endpoints_given_weights_cem_0_or_4( + pixels, cem, + pWeights, weight_ise_range, + pNew_CEM_vals, + num_comps); + } + case 6: + case 10: + { +#if 1 + // root finding, creates new endpoints + return refine_endpoints_given_weights_cem_6_or_10_method3( + pixels, cem, + pWeights, weight_ise_range, + pNew_CEM_vals, + num_comps); +#else + // coordinate descent, needs current endpoints + return refine_endpoints_given_weights_cem_6_or_10_method2( + pixels, cem, + pWeights, weight_ise_range, pCEM_values, endpoint_ise_range, + pNew_CEM_vals, + num_comps); +#endif + } + case 8: + case 9: + case 12: + case 13: + { + return refine_endpoints_given_weights_cem_8_9_12_or_13( + pixels, cem, + pWeights, weight_ise_range, + pNew_CEM_vals, + num_comps); + } + default: + assert(0); + break; + } + + return false; +} + +// ccs component was swizzled into A +static void eval_weights_second_plane( + const pixelbuf& pixels, + uint32_t cem, uint32_t ccs_index, + uint8_t* pWeights0, uint8_t* pWeights1, uint32_t weight_ise_range, + float desired_cem_endpoints[8], // in CEM order + const uint8_t* pCEM_values, uint32_t endpoint_ise_range, + uint8_t* pNew_CEM_values, + uint32_t num_pca_comps, bool ccs_chan_rotated_flag, + const single_subset_enc_context& ctx) // true if channel ccs_index was rotated vs. alpha, ccs_index should be <= 2 +{ + assert(!is_cem_9_or_13(cem)); + BASISU_NOTE_UNUSED(num_pca_comps); + + float lo_v = 1e+30f, hi_v = -1e+30f; + + // ccs_chan_rotated_flag ccs chan was rotated into alpha, otherwise there was no ccs channel swapping + const uint32_t actual_src_ccs_chan = ccs_chan_rotated_flag ? 3 : ccs_index; + + for (uint32_t y = 0; y < pixels.m_height; y++) + { + for (uint32_t x = 0; x < pixels.m_width; x++) + { + float v = pixelbuf_get_comp(pixels.m_pBuf, x, y, actual_src_ccs_chan); + lo_v = minimum(lo_v, v); + hi_v = maximum(hi_v, v); + } // x + } // y + + // be aware lo_v/hi_v are not clamped + + if (ccs_index == 3) + { + // second plane controls A: insert alpha range directly into the encoded CEM values and we're done + int lo_v_q = quant_endpoint_val_to_rank(lo_v, endpoint_ise_range, astc_helpers::get_ise_levels(endpoint_ise_range)); + int hi_v_q = quant_endpoint_val_to_rank(hi_v, endpoint_ise_range, astc_helpers::get_ise_levels(endpoint_ise_range)); + + if (cem == 10) + { + if (pNew_CEM_values != pCEM_values) + memcpy(pNew_CEM_values, pCEM_values, 4); + + pNew_CEM_values[4] = (uint8_t)lo_v_q; + pNew_CEM_values[5] = (uint8_t)hi_v_q; + + desired_cem_endpoints[4] = lo_v; + desired_cem_endpoints[5] = hi_v; + } + else if (cem == 12) + { + if (pNew_CEM_values != pCEM_values) + memcpy(pNew_CEM_values, pCEM_values, 6); + + pNew_CEM_values[6] = (uint8_t)lo_v_q; + pNew_CEM_values[7] = (uint8_t)hi_v_q; + + desired_cem_endpoints[6] = lo_v; + desired_cem_endpoints[7] = hi_v; + } + else if (cem == 4) + { + if (pNew_CEM_values != pCEM_values) + memcpy(pNew_CEM_values, pCEM_values, 2); + + pNew_CEM_values[2] = (uint8_t)lo_v_q; + pNew_CEM_values[3] = (uint8_t)hi_v_q; + + desired_cem_endpoints[2] = lo_v; + desired_cem_endpoints[3] = hi_v; + } + else + { + assert(0); + } + } + else if (is_cem_8_or_12(cem)) //((cem == 8) || (cem == 12)) + { + assert(num_pca_comps == 3); + + // ccs_index is [0,2], cem is 8/12 + // ccs was swizzled with A, undo that, insert pixel bounds of ccs channel into correct endpoint channel + assert(ccs_chan_rotated_flag); + + desired_cem_endpoints[3 * 2 + 0] = lo_v; + desired_cem_endpoints[3 * 2 + 1] = hi_v; + + std::swap(desired_cem_endpoints[3 * 2 + 0], desired_cem_endpoints[ccs_index * 2 + 0]); + std::swap(desired_cem_endpoints[3 * 2 + 1], desired_cem_endpoints[ccs_index * 2 + 1]); + + cem_encode(cem, desired_cem_endpoints, endpoint_ise_range, pNew_CEM_values, true, ctx.m_higher_effort_bc); + } + else + { + assert(is_cem_6_or_10(cem)); + + // ccs_index is [0,2], cem is 6/10 - no change to endpoints, we're just going to split out one RGB channel into a sep weight plane + if (pNew_CEM_values != pCEM_values) + memcpy(pNew_CEM_values, pCEM_values, astc_helpers::get_num_cem_values(cem)); + + // no change to desired_cem_endpoints + } + + float final_endpoints[8]; + cem_decode(cem, pNew_CEM_values, endpoint_ise_range, final_endpoints, nullptr); + + const uint32_t num_weight_levels = astc_helpers::get_ise_levels(weight_ise_range); + assert(num_weight_levels <= 32); + + const float l = final_endpoints[0 + ccs_index]; + const float h = final_endpoints[4 + ccs_index]; + const float scale = (float)(num_weight_levels - 1) / ((h - l) + TINY_EPS); + + uint8_t* pDst_weights = pWeights1; + for (uint32_t y = 0; y < pixels.m_height; y++) + { + for (uint32_t x = 0; x < pixels.m_width; x++) + { + const float v = pixelbuf_get_comp(pixels.m_pBuf, x, y, actual_src_ccs_chan); + + const int iw = clamp((int)((v - l) * scale + .5f), 0, num_weight_levels - 1); + + *pDst_weights++ = (uint8_t)iw; + } // x + } // y + + if ((ccs_index < 3) && (is_cem_8_or_12(cem))) + { + assert(ccs_chan_rotated_flag); + + // CEM 8 or 12. + // We re-encoded the endpoints, so update the weights in case they changed. + // Final endpoints are not CCS swapped (rotated), so we'll need eval_weights_first_plane() to swap them on + // endpoint decode to match the input pixels here which are swapped with A. + eval_weights_first_plane(pixels, + pWeights0, weight_ise_range, cem, + pNew_CEM_values, endpoint_ise_range, + 3, ccs_index); + } +} + +static void try_base_ofs_cem( + uint32_t cem_index, + const pixelbuf& block, + astc_helpers::log_astc_block& log_blk, + [[maybe_unused]] const single_subset_enc_context& ctx, + [[maybe_unused]] uint32_t num_block_comps, // 3 or 4, for 3 alpha=255 + float desired_cem_endpoints[8], + bool ccs_chan_rotated_flag) // if true, a and the ccs chan are swapped in block's pixels +{ + assert(!is_lblock_ise(log_blk)); + + if (is_cem_9_or_13(log_blk.m_color_endpoint_modes[0])) + return; + + assert(is_cem_8_or_12(cem_index)); + + if (log_blk.m_endpoint_ise_range == astc_helpers::BISE_256_LEVELS) + return; + + // try base+ofs, very rare win (~1-3% vs. direct) + uint8_t CEM_values_base_ofs[8]; + + const uint32_t cem_index_base_ofs = (cem_index == 8) ? 9 : 13; + const uint32_t num_comps = get_num_cem_chans(cem_index); + const uint32_t num_base_ofs_cem_vals = astc_helpers::get_num_cem_values(cem_index_base_ofs); + + const bool base_ofs_fit = encode_cem_9_13(cem_index_base_ofs, CEM_values_base_ofs, log_blk.m_endpoint_ise_range, desired_cem_endpoints, true); + if (!base_ofs_fit) + return; + + const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(log_blk.m_endpoint_ise_range); + for (uint32_t i = 0; i < num_base_ofs_cem_vals; i++) + CEM_values_base_ofs[i] = endpoint_tab.m_ISE_to_rank[CEM_values_base_ofs[i]]; + + float ep_base_ofs[8]; + cem_decode(cem_index_base_ofs, CEM_values_base_ofs, log_blk.m_endpoint_ise_range, ep_base_ofs, nullptr); + + float ep_direct[8]; + cem_decode(cem_index, log_blk.m_endpoints, log_blk.m_endpoint_ise_range, ep_direct, nullptr); + + float desired_endpoints_lh_rgba[8]; + + for (uint32_t c = 0; c < num_comps; c++) + { + desired_endpoints_lh_rgba[c + 0] = desired_cem_endpoints[c * 2 + 0]; + desired_endpoints_lh_rgba[c + 4] = desired_cem_endpoints[c * 2 + 1]; + } + + const float dist_base_ofs = calc_shortest_endpoint_dist(desired_endpoints_lh_rgba, ep_base_ofs, num_comps); + const float dist_direct = calc_shortest_endpoint_dist(desired_endpoints_lh_rgba, ep_direct, num_comps); + + if (dist_base_ofs < dist_direct) + { + log_blk.m_color_endpoint_modes[0] = (uint8_t)cem_index_base_ofs; + memcpy(log_blk.m_endpoints, CEM_values_base_ofs, num_base_ofs_cem_vals); + + const uint32_t weight1_chan_mask = log_blk.m_dual_plane ? (1u << log_blk.m_color_component_selector) : 0; + const uint32_t weight0_chan_mask = ((1 << num_comps) - 1) ^ weight1_chan_mask; + + eval_weights_for_plane( + block, + log_blk.m_dual_plane ? 2 : 1, + log_blk.m_weights, log_blk.m_weight_ise_range, + log_blk.m_color_endpoint_modes[0], log_blk.m_endpoints, log_blk.m_endpoint_ise_range, + weight0_chan_mask, ccs_chan_rotated_flag ? log_blk.m_color_component_selector : -1); + + if (log_blk.m_dual_plane) + { + eval_weights_for_plane( + block, + 2, + log_blk.m_weights + 1, log_blk.m_weight_ise_range, + log_blk.m_color_endpoint_modes[0], log_blk.m_endpoints, log_blk.m_endpoint_ise_range, + weight1_chan_mask, ccs_chan_rotated_flag ? log_blk.m_color_component_selector : -1); + } + } +} + +// PCA comps +// single plane: +// cem 0: 3D (R=G=B, really 1D) +// cem 4: 4D (R=G=B + A, really 2D) +// cem 6: 3D, zero centered +// cem 8: 3D +// cem 10: 4D, zero centered +// cem 12: 4D +// dual plane, CCS=3: +// cem 0: invalid +// cem 4: 3D (R=G=B + A, really 1D) +// cem 6: invalid +// cem 8: invalid +// cem 10: 3D, zero centered +// cem 12: 3D +// dual plane, CCS=[0,2] - for CEM 6/10, solve as SP, then introduce 2nd weight plane but don't change the encoded RGBA endpoints +// cem 0: invalid +// cem 4: invalid +// cem 6: 3D zero centered, unrotated pixels (i.e. ccs channel NOT swapped with A) +// cem 8: 3D, rotated pixels (essentially 2D PCA as all A=255) +// cem 10: 4D, zero centered, unrotated pixels (exceptional case) +// cem 12: 3D, rotated pixels +static inline void get_pca_config(const astc_unpacked_config& cfg, uint32_t& num_pca_comps, bool& ccs_chan_rotated_flag) +{ + num_pca_comps = 3; + ccs_chan_rotated_flag = false; + + if (cfg.m_dual_plane) + { + assert(cfg.m_cem != 0); + + if (cfg.m_cem == 4) + { + assert(cfg.m_ccs_index == 3); + //num_pca_comps = 3; + } + else if (cfg.m_ccs_index < 3) + { + num_pca_comps = (cfg.m_cem == 10) ? 4 : 3; + ccs_chan_rotated_flag = is_cem_8_or_12(cfg.m_cem); // ((cfg.m_cem & 3) == 0); // cem 8 or 12, otherwise not ccs channel rotated with A + } + } + else + { + num_pca_comps = ((cfg.m_cem == 4) || (cfg.m_cem >= 10)) ? 4 : 3; + } +} + +static void create_single_subset_block( + const pixelbuf& block, + astc_helpers::log_astc_block& log_blk, + const single_subset_enc_context& ctx, + uint32_t num_block_comps, // 3 or 4, for 3 alpha=255 + uint32_t num_pca_comps, // 3D or 4D + bool ccs_chan_rotated_flag, + bool try_base_ofs) // ccs_chan_rotated_flag is true for dual plane if the CCS channel has been swapped/rotated with alpha +{ + const uint32_t cem_index = log_blk.m_color_endpoint_modes[0]; + + assert(is_cem_0_or_4(cem_index) || is_cem_6_or_10(cem_index) || is_cem_8_or_12(cem_index)); + + const bool cem_6_or_10 = is_cem_6_or_10(cem_index); // (cem_index & 3) == 2; + //const bool cem_has_alpha = does_cem_have_alpha(cem_index); // only true when the block has alpha, too + const bool dual_plane = log_blk.m_dual_plane; + + if (num_block_comps == 3) + { + assert((cem_index == 0) || (cem_index == 6) || (cem_index == 8)); + } + + if (dual_plane) + { + if (log_blk.m_color_component_selector == 3) + { + assert((cem_index == 4) || (cem_index >= 10)); + } + else + { + assert(!is_cem_0_or_4(cem_index)); + } + } + + float desired_cem_endpoints[8]; + calc_initial_cem_endpoints(block, desired_cem_endpoints, num_pca_comps, nullptr, cem_6_or_10); + + cem_encode(cem_index, desired_cem_endpoints, log_blk.m_endpoint_ise_range, log_blk.m_endpoints, true, ctx.m_higher_effort_bc); + + eval_weights_first_plane(block, log_blk.m_weights, log_blk.m_weight_ise_range, + cem_index, log_blk.m_endpoints, log_blk.m_endpoint_ise_range, + num_pca_comps); + + for (uint32_t i = 0; i < ctx.m_num_ls_iterations; i++) + { + refine_endpoints_given_weights(block, cem_index, log_blk.m_weights, log_blk.m_weight_ise_range, log_blk.m_endpoints, log_blk.m_endpoint_ise_range, desired_cem_endpoints, num_pca_comps); + + cem_encode(cem_index, desired_cem_endpoints, log_blk.m_endpoint_ise_range, log_blk.m_endpoints, true, ctx.m_higher_effort_bc); + + eval_weights_first_plane(block, log_blk.m_weights, log_blk.m_weight_ise_range, + cem_index, log_blk.m_endpoints, log_blk.m_endpoint_ise_range, + num_pca_comps); + } // i + + if (dual_plane) + { + uint8_t weights[2][astc_helpers::MAX_GRID_WEIGHTS]; + + eval_weights_second_plane( + block, + cem_index, log_blk.m_color_component_selector, + log_blk.m_weights, weights[1], log_blk.m_weight_ise_range, + desired_cem_endpoints, + log_blk.m_endpoints, log_blk.m_endpoint_ise_range, + log_blk.m_endpoints, + num_pca_comps, ccs_chan_rotated_flag, ctx); + + const uint32_t num_grid_samples = log_blk.m_grid_width * log_blk.m_grid_height; + memcpy(weights[0], log_blk.m_weights, num_grid_samples); + + for (uint32_t i = 0; i < num_grid_samples; i++) + { + log_blk.m_weights[i * 2 + 0] = weights[0][i]; + log_blk.m_weights[i * 2 + 1] = weights[1][i]; + } // i + } + + if ((try_base_ofs) && ((cem_index == 8) || (cem_index == 12))) + { + try_base_ofs_cem(cem_index, + block, + log_blk, + ctx, + num_block_comps, + desired_cem_endpoints, + ccs_chan_rotated_flag); + } +} + +static void encode_single_subset_block( + const single_subset_enc_context& ctx, + const pixelbuf& src_block, uint32_t num_src_block_comps, + const astc_unpacked_config& cfg, + astc_helpers::log_astc_block& log_blk, + bool try_base_ofs) +{ + assert(cfg.m_grid_width <= ctx.m_block_width); + assert(cfg.m_grid_height <= ctx.m_block_height); + + log_blk.clear(); + log_blk.m_grid_width = cfg.m_grid_width; + log_blk.m_grid_height = cfg.m_grid_height; + log_blk.m_endpoint_ise_range = cfg.m_endpoint_range; + log_blk.m_weight_ise_range = cfg.m_weight_range; + log_blk.m_dual_plane = cfg.m_dual_plane; + log_blk.m_color_component_selector = cfg.m_ccs_index; + log_blk.m_num_partitions = 1; + log_blk.m_color_endpoint_modes[0] = cfg.m_cem; + log_blk.m_user_mode = cUserModeRankValues; + + if (num_src_block_comps == 3) + { + //assert(cfg.m_cem <= 8); + assert((cfg.m_cem == 0) || (cfg.m_cem == 6) || (cfg.m_cem == 8)); + } + + uint32_t num_pca_comps; + bool ccs_chan_rotated_flag; + get_pca_config(cfg, num_pca_comps, ccs_chan_rotated_flag); + + float grid_pixels[PIXELBUF_SIZE_IN_FLOATS]; + pixelbuf grid_pbuf(cfg.m_grid_width, cfg.m_grid_height, grid_pixels); + + const uint32_t num_block_chans_to_encode = minimum(get_num_cem_chans(cfg.m_cem), num_src_block_comps); // always 3 or 4 + + pseudoinverse_block_to_grid(src_block, grid_pbuf, num_block_chans_to_encode); + + if (num_block_chans_to_encode == 3) + pixelbuf_set_comp_to_val(grid_pbuf, 3, 255.0f); + + if (ccs_chan_rotated_flag) + pixelbuf_swap_comp_with_alpha(grid_pbuf, cfg.m_ccs_index); + + create_single_subset_block(grid_pbuf, log_blk, ctx, num_block_chans_to_encode, num_pca_comps, ccs_chan_rotated_flag, try_base_ofs); +} + +bool init_single_subset_context( + single_subset_enc_context& ctx, + uint32_t block_width, uint32_t block_height, + astc_helpers::decode_mode astc_decode_mode, + const uint32_t chan_weights[4], + uint32_t max_candidates, uint32_t num_ls_iterations, bool disable_dual_plane, bool has_alpha, bool weight_polishing) +{ + const int idx = astc_helpers::find_astc_block_size_index(block_width, block_height); + assert(idx != -1); + if (idx == -1) + { + assert(0); + return false; + } + + if (max_candidates > MAX_CANDIDATES) + { + assert(0); + return false; + } + + ctx.m_block_width = block_width; + ctx.m_block_height = block_height; + + ctx.m_block_size_index = idx; + ctx.m_total_block_pixels = block_width * block_height; + + ctx.m_astc_decode_mode = astc_decode_mode; + + memcpy(ctx.m_chan_weights, chan_weights, sizeof(ctx.m_chan_weights)); + + ctx.m_max_candidates = max_candidates; + ctx.m_num_ls_iterations = num_ls_iterations; + ctx.m_weight_polishing = weight_polishing; + + ctx.m_disable_dual_plane = disable_dual_plane; + ctx.m_has_alpha = has_alpha; + + ctx.m_try_base_ofs = true; + ctx.m_higher_effort_bc = true; + + return ctx.m_dct.init(block_height, block_width); // rows=block height, cols=block width +} + +static inline float calc_weighted_error(const astc_helpers::color_rgba& c, float r, float g, float b, float a, const single_subset_enc_context& state) +{ + return (square((float)c.m_r - r) * (float)state.m_chan_weights[0]) + + (square((float)c.m_g - g) * (float)state.m_chan_weights[1]) + + (square((float)c.m_b - b) * (float)state.m_chan_weights[2]) + + (square((float)c.m_a - a) * (float)state.m_chan_weights[3]); +} + +// 1-3 subsets +// returns true if lblock_to_refine was changed +static bool weight_polish( + const pixelbuf& src_block, + astc_helpers::log_astc_block& lblock_to_refine, + const single_subset_enc_context& enc_state, + const basist::astc_ldr_t::astc_block_grid_data* pGrid_data, + double& best_lblock_error, astc_helpers::log_astc_block& best_lblock, + astc_lblock_vec* pAll_candidates) +{ + assert((lblock_to_refine.m_num_partitions >= 1) && (lblock_to_refine.m_num_partitions <= 3)); + + const uint32_t block_width = enc_state.m_block_width, block_height = enc_state.m_block_height; + [[maybe_unused]] const uint32_t total_block_pixels = block_width * block_height; + + [[maybe_unused]] const uint32_t num_cem_endpoint_vals = astc_helpers::get_num_cem_values(lblock_to_refine.m_color_endpoint_modes[0]); + [[maybe_unused]] const uint32_t total_grid_weights = lblock_to_refine.m_grid_width * lblock_to_refine.m_grid_height; + + [[maybe_unused]] const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(lblock_to_refine.m_endpoint_ise_range); + const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(lblock_to_refine.m_weight_ise_range); + + const uint32_t num_weight_levels = astc_helpers::get_ise_levels(lblock_to_refine.m_weight_ise_range); + + [[maybe_unused]] const uint32_t num_partitions = lblock_to_refine.m_num_partitions; + + astc_helpers::log_astc_block refined_lblock_ise(lblock_to_refine); + convert_rank_lblock_to_ise(refined_lblock_ise); // xuastc_ldr_block_decoder expects ISE blocks + + bool changed_flag = false; + + const uint32_t grid_width = refined_lblock_ise.m_grid_width, grid_height = refined_lblock_ise.m_grid_height; + + astc_helpers::xuastc_ldr_block_decoder block_decoder; + block_decoder.init(refined_lblock_ise, block_width, block_height, enc_state.m_astc_decode_mode, pGrid_data->m_upsample_weights.get_ptr()); + +#if defined(DEBUG) || defined(_DEBUG) + const double error1 = compute_block_error(refined_lblock_ise, src_block, enc_state); +#endif + + for (uint32_t y = 0; y < grid_height; y++) + { + for (uint32_t x = 0; x < grid_width; x++) + { + const uint32_t idx = x + y * grid_width; + + const basisu::uint16_vec& influenced_texels = pGrid_data->m_grid_to_texel_influence_list[idx]; + + int cur_rank = weight_tab.m_ISE_to_rank[refined_lblock_ise.m_weights[idx]]; + + const uint8_t orig_weight_ise = astc_helpers::get_weight(refined_lblock_ise, 0, idx); + + double best_err = 0.0f; + + for (uint32_t j = 0; j < influenced_texels.size(); j++) + { + const uint32_t packed_texel_index = influenced_texels[j]; + const uint32_t texel_x = packed_texel_index & 0xFF; + const uint32_t texel_y = packed_texel_index >> 8; + + astc_helpers::color_rgba dec_c; + block_decoder.decode_texel(texel_x, texel_y, dec_c); + + best_err += calc_weighted_error(dec_c, pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 0), pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 1), pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 2), pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 3), enc_state); + } + + if (best_err == 0.0f) + continue; + + uint8_t best_weight_ise = orig_weight_ise; + + for (int idir = -1; idir <= 1; idir += 2) + { + const int new_rank = clamp(cur_rank + idir, 0, (int)num_weight_levels - 1); + const uint32_t new_ise = weight_tab.m_rank_to_ISE[new_rank]; + + // change current weight + astc_helpers::get_weight(refined_lblock_ise, 0, idx) = (uint8_t)new_ise; + + double trial_err = 0.0f; + for (uint32_t j = 0; j < influenced_texels.size(); j++) + { + const uint32_t packed_texel_index = influenced_texels[j]; + const uint32_t texel_x = packed_texel_index & 0xFF; + const uint32_t texel_y = packed_texel_index >> 8; + //const uint32_t texel_index = texel_x + texel_y * block_width; + + astc_helpers::color_rgba dec_c; + block_decoder.decode_texel(texel_x, texel_y, dec_c); + + trial_err += calc_weighted_error(dec_c, pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 0), pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 1), pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 2), pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 3), enc_state); + } + + astc_helpers::get_weight(refined_lblock_ise, 0, idx) = orig_weight_ise; + + if (trial_err < best_err) + { + // accept + best_err = trial_err; + best_weight_ise = (uint8_t)new_ise; + changed_flag = true; + if (best_err == 0.0f) + break; + } + + } // idir + + astc_helpers::get_weight(refined_lblock_ise, 0, idx) = best_weight_ise; + + } // x + } //y + +#if defined(DEBUG) || defined(_DEBUG) + const double error2 = compute_block_error(refined_lblock_ise, src_block, enc_state); + assert(error2 <= error1); +#endif + + convert_ise_lblock_to_rank(refined_lblock_ise); + // refined_lblock_ise is now in rank space + + if (changed_flag) + { + const double refined_error = compute_block_error(refined_lblock_ise, src_block, enc_state); + if (refined_error < best_lblock_error) + { + best_lblock_error = refined_error; + memcpy(&best_lblock, &refined_lblock_ise, sizeof(best_lblock)); + + } + + if (pAll_candidates) + pAll_candidates->push_back(refined_lblock_ise); + + lblock_to_refine = refined_lblock_ise; + } + + return changed_flag; +} + +static double compress_single_subset_internal( + const single_subset_enc_context& ctx, + const uint8_t* pBlock_pixels, + astc_helpers::log_astc_block& best_lblock, + astc_lblock_vec* pAll_candidates, + single_subset_shortlist_state& shortlist_state, + bool always_compute_error, float scale_weight) +{ +#if 0 + { + basisu::rand rnd; + rnd.seed(1115); + for (; ; ) + { + float endpoints[6]; + for (uint32_t i = 0; i < 6; i++) + endpoints[i] = rnd.frand(-4.0f, 255.0f + 4); + + int range = rnd.irand(astc_helpers::FIRST_VALID_ENDPOINT_ISE_RANGE, astc_helpers::LAST_VALID_ENDPOINT_ISE_RANGE); + + uint8_t cem_vals[6]; + encode_cem_8_12(8, cem_vals, range, endpoints); + } + } +#endif + + assert(ctx.m_block_height <= 12); + assert(ctx.m_block_width <= 12); + assert(ctx.m_total_block_pixels == (ctx.m_block_width * ctx.m_block_height)); + assert(ctx.m_max_candidates && (ctx.m_max_candidates <= MAX_CANDIDATES)); + + const uint32_t* pBlock_pixels_u32 = reinterpret_cast(pBlock_pixels); + + const uint32_t first_pixel = pBlock_pixels_u32[0]; + + const int last_pixel_index = ctx.m_total_block_pixels - 1; + + if (pBlock_pixels_u32[last_pixel_index] == first_pixel) + { + int i; + for (i = 1; i < last_pixel_index; i++) + if (pBlock_pixels_u32[i] != first_pixel) + break; + + if (i == last_pixel_index) + { + astc_helpers::set_ldr_solid_block(best_lblock, pBlock_pixels[0], pBlock_pixels[1], pBlock_pixels[2], pBlock_pixels[3]); + + if (pAll_candidates) + pAll_candidates->push_back(best_lblock); + + return 0.0f; + } + } + + rgba32_image block_img; + block_img.m_pPixels = pBlock_pixels; + block_img.m_width = ctx.m_block_width; + block_img.m_height = ctx.m_block_height; + block_img.m_row_pitch_in_texels = ctx.m_block_width; + + uint32_t num_src_block_comps = 3; + bool src_is_luma_only = true; + + if (ctx.m_has_alpha) + { + for (uint32_t i = 0; i < ctx.m_total_block_pixels; i++) + { + const uint8_t r = pBlock_pixels[i * 4 + 0]; + const uint8_t g = pBlock_pixels[i * 4 + 1]; + const uint8_t b = pBlock_pixels[i * 4 + 2]; + const uint8_t a = pBlock_pixels[i * 4 + 3]; + + if ((r != g) || (r != b)) + src_is_luma_only = false; + + if (a != 255) + num_src_block_comps = 4; + } + } + else + { + for (uint32_t i = 0; i < ctx.m_total_block_pixels; i++) + { + const uint8_t r = pBlock_pixels[i * 4 + 0]; + const uint8_t g = pBlock_pixels[i * 4 + 1]; + const uint8_t b = pBlock_pixels[i * 4 + 2]; + + if ((r != g) || (r != b)) + { + src_is_luma_only = false; + break; + } + } + } + + uint32_t total_packed_configs = TOTAL_SINGLE_SUBSET_CONFIGS_RGBA; + const uint32_t* pPacked_configs = g_single_subset_configs_rgba; + + if (src_is_luma_only) + { + total_packed_configs = TOTAL_SINGLE_SUBSET_CONFIGS_LA; + pPacked_configs = g_single_subset_configs_la; + } + + init_single_subset_shortlist_state(block_img, ctx, shortlist_state, src_is_luma_only, num_src_block_comps); + + const uint32_t total_candidates = generate_single_subset_shortlist(total_packed_configs, pPacked_configs, ctx, block_img, src_is_luma_only, + num_src_block_comps, shortlist_state, scale_weight, ctx.m_max_candidates); + + if (!total_candidates) + { + astc_helpers::set_ldr_solid_block(best_lblock, 0xFF, 0, 0xFF, 0xFF); + + if (pAll_candidates) + pAll_candidates->push_back(best_lblock); + + assert(0); + return DBL_MAX; + } + + double best_err = DBL_MAX; + + for (uint32_t cand_index = 0; cand_index < total_candidates; cand_index++) + { + astc_helpers::log_astc_block trial_lblock; + + encode_single_subset_block(ctx, shortlist_state.m_pbuf, num_src_block_comps, shortlist_state.m_best_configs[cand_index], trial_lblock, ctx.m_try_base_ofs); + + if (pAll_candidates) + pAll_candidates->push_back(trial_lblock); + + if (always_compute_error || (total_candidates > 1)) + { + double err = compute_block_error(trial_lblock, shortlist_state.m_pbuf, ctx); + if (err < best_err) + { + best_err = err; + best_lblock = trial_lblock; + } + } + else + { + best_err = -1.0f; + best_lblock = trial_lblock; + } + } + + if (ctx.m_weight_polishing) + { + const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data( + ctx.m_block_width, ctx.m_block_height, best_lblock.m_grid_width, best_lblock.m_grid_height); + + weight_polish(shortlist_state.m_pbuf, best_lblock, ctx, pGrid_data, best_err, best_lblock, pAll_candidates); + } + + convert_rank_lblock_to_ise(best_lblock); + + return best_err; +} + +double compress_single_subset( + single_subset_enc_context& ctx, + const uint8_t* pBlock_pixels, + astc_helpers::log_astc_block& best_lblock, + astc_lblock_vec* pAll_candidates, + bool always_compute_error) +{ + single_subset_shortlist_state shortlist_state; + + return compress_single_subset_internal(ctx, pBlock_pixels, best_lblock, pAll_candidates, shortlist_state, always_compute_error, DEF_SCALE_WEIGHT); +} + +// unpack_config() compatible +// cems 6,8,10,12 only, gw>=gh (opposite case of gw= 1) && (max_subsets <= 3)); + + assert(pPart_data_p2 && pPart_data_p2->m_part_lhs_map.is_valid()); + assert(pPart_data_p3 && pPart_data_p3->m_part_lhs_map.is_valid()); + + assert(astc_helpers::is_valid_block_size(ctx.m_block_width, ctx.m_block_height) && + (astc_helpers::find_astc_block_size_index(ctx.m_block_width, ctx.m_block_height) == (int)ctx.m_block_size_index)); + + assert(ctx.m_total_block_pixels == (ctx.m_block_width * ctx.m_block_height)); + + ctx.m_max_subsets = max_subsets; + ctx.m_num_carrier_candidates = num_carrier_candidates; + ctx.m_num_pattern_candidates = num_pattern_candidates; + ctx.m_two_subset_var_thresh = two_subset_var_thresh; + ctx.m_three_subset_var_thresh = three_subset_var_thresh; + ctx.m_two_subset_dot_thresh_fract_index = two_subset_dot_thresh_fract_index; + + const uint32_t num_two_subset_unique_pats = basist::astc_ldr_t::get_total_unique_patterns(ctx.m_block_size_index, 2); + + ctx.m_num_unique_two_subset_pats = num_two_subset_unique_pats; + ctx.m_pUnique_two_subset_pats = basist::astc_ldr_t::g_unique_index_to_astc_part_seed[0][ctx.m_block_size_index]; + + ctx.m_use_method1 = true; + ctx.m_use_method2 = true; + + ctx.m_pPart_data_p2 = pPart_data_p2; + ctx.m_pPart_data_p3 = pPart_data_p3; + + for (uint32_t unique_index = 0; unique_index < num_two_subset_unique_pats; unique_index++) + { + const uint32_t seed_id = ctx.m_pUnique_two_subset_pats[unique_index]; + assert(seed_id == basist::astc_ldr_t::unique_pat_index_to_part_seed(ctx.m_block_size_index, 2, unique_index)); + + bitmask192 pat_bitmask(0, 0, 0); + uint32_t bit_ofs = 0; + + for (uint32_t y = 0; y < ctx.m_block_height; y++) + { + for (uint32_t x = 0; x < ctx.m_block_width; x++) + { + uint64_t s = astc_helpers::get_precomputed_texel_partition(ctx.m_block_width, ctx.m_block_height, seed_id, x, y, 2); + + if (s) + { + bitmask192 b(0, 0, 0); + b.set_bit(bit_ofs); + + pat_bitmask |= b; + } + + ++bit_ofs; + } // x + } // y + + ctx.m_two_subset_pat_bitmask[unique_index] = pat_bitmask; + + } // unique_index + + return true; +} + +// gradient descent: refines weights given current 2 subset endpoints +// returns true if weight grid changed +static bool weight_gradient_descent( + const pixelbuf& src_block, + astc_helpers::log_astc_block& lblock_to_refine, + const pixelbuf subset_pbuf[3], uint32_t subset_pixel_indices[3][256], + const astc_unpacked_config& best_config, + const subset_enc_context& enc_state, + const basist::astc_ldr_t::astc_block_grid_data* pGrid_data, + double& best_lblock_error, astc_helpers::log_astc_block& best_lblock, + astc_lblock_vec* pAll_candidates) +{ + assert(!is_lblock_ise(lblock_to_refine)); + assert((lblock_to_refine.m_num_partitions >= 2) && (lblock_to_refine.m_num_partitions <= 3)); + + const uint32_t block_width = enc_state.m_block_width, block_height = enc_state.m_block_height; + const uint32_t total_block_pixels = block_width * block_height; + + const uint32_t num_cem_endpoint_vals = astc_helpers::get_num_cem_values(best_config.m_cem); + const uint32_t total_grid_weights = best_config.m_grid_width * best_config.m_grid_height; + + [[maybe_unused]] const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(best_config.m_endpoint_range); + const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(best_config.m_weight_range); + + [[maybe_unused]] const uint32_t num_weight_levels = astc_helpers::get_ise_levels(best_config.m_weight_range); + + const uint32_t num_partitions = lblock_to_refine.m_num_partitions; + + // compute the weights we want (the "ideal" weights) minus the upsampled weights weights we have, then project that residual back to weight grid res + + // compute ideal [0,64] block weights given the current endpoints + uint8_t ideal_block_weight_vals[astc_helpers::MAX_BLOCK_PIXELS]; // at block res, ranks, [0,64] dequantized values + +#if defined(DEBUG) || defined(_DEBUG) + memset(ideal_block_weight_vals, 0xFF, astc_helpers::MAX_BLOCK_PIXELS); +#endif + + for (uint32_t s = 0; s < num_partitions; s++) + { + uint8_t weights[astc_helpers::MAX_BLOCK_PIXELS]; + + eval_weights_first_plane(subset_pbuf[s], + weights, astc_helpers::BISE_64_LEVELS, + best_config.m_cem, lblock_to_refine.m_endpoints + s * num_cem_endpoint_vals, best_config.m_endpoint_range, + get_num_cem_chans(best_config.m_cem)); + + for (uint32_t j = 0; j < subset_pbuf[s].m_width; j++) + { + const uint32_t pixel_index = subset_pixel_indices[s][j]; + assert(pixel_index < total_block_pixels); + + ideal_block_weight_vals[pixel_index] = weights[j]; // [0,64] weight value space, or in rank space + } // j + + } // s + +#if defined(DEBUG) || defined(_DEBUG) + for (uint32_t i = 0; i < total_block_pixels; i++) + { + assert(ideal_block_weight_vals[i] != 0xFF); + } +#endif + + // upsample the current weight grid + + uint8_t dequant_grid_weights[astc_helpers::MAX_GRID_WEIGHTS]; // grid res, [0,64] + + for (uint32_t i = 0; i < total_grid_weights; i++) + dequant_grid_weights[i] = (uint8_t)weight_tab.get_rank_to_val(lblock_to_refine.m_weights[i]); + + uint8_t dequant_block_weights[astc_helpers::MAX_BLOCK_PIXELS]; // block res, [0,64] + if ((lblock_to_refine.m_grid_width == (int)block_width) && (lblock_to_refine.m_grid_height == (int)block_height)) + memcpy(dequant_block_weights, dequant_grid_weights, total_block_pixels); + else + astc_helpers::upsample_weight_grid_xuastc_ldr(block_width, block_height, lblock_to_refine.m_grid_width, lblock_to_refine.m_grid_height, dequant_grid_weights, dequant_block_weights, nullptr, nullptr); + + // now compute the residual at block res + + int weight_block_raw_residuals[astc_helpers::MAX_BLOCK_PIXELS]; // block res, [0,64] + + for (uint32_t i = 0; i < total_block_pixels; i++) + weight_block_raw_residuals[i] = ideal_block_weight_vals[i] - dequant_block_weights[i]; + + // downsample the residuals to grid res + + const basisu::vector& unweighted_downsample_matrix = pGrid_data->m_unweighted_downsample_matrix; + const basisu::vector& one_over_diag_AtA = pGrid_data->m_one_over_diag_AtA; + + float weight_grid_residuals_downsampled[astc_helpers::MAX_GRID_WEIGHTS]; // grid res, [0,64] + + basisu::astc_ldr::downsample_weight_residual_grid( + unweighted_downsample_matrix.get_ptr(), + block_width, block_height, // source/from dimension (block size) + best_config.m_grid_width, best_config.m_grid_height, // dest/to dimension (grid size) + weight_block_raw_residuals, // these are dequantized weights, NOT ISE symbols, [by][bx] + weight_grid_residuals_downsampled); // [wy][wx] + + for (uint32_t i = 0; i < total_grid_weights; i++) + weight_grid_residuals_downsampled[i] *= one_over_diag_AtA[i]; + + // Apply the residuals at grid res and quantize + const float Q = 1.0f; + + astc_helpers::log_astc_block refined_lblock(lblock_to_refine); + + bool changed_flag = false; + + for (uint32_t i = 0; i < total_grid_weights; i++) + { + float v = (float)weight_tab.get_rank_to_val(lblock_to_refine.m_weights[i]) + weight_grid_residuals_downsampled[i] * Q; + + const int iv = clamp((int)std::roundf(v), 0, 64); + + uint8_t new_weight = (uint8_t)weight_tab.get_val_to_rank(iv); + + if (refined_lblock.m_weights[i] != new_weight) + { + refined_lblock.m_weights[i] = new_weight; + changed_flag = true; + } + } + + if (changed_flag) + { + const double refined_error = compute_block_error(refined_lblock, src_block, enc_state); + + if (refined_error < best_lblock_error) + { + best_lblock_error = refined_error; + memcpy(&best_lblock, &refined_lblock, sizeof(best_lblock)); + } + + if (pAll_candidates) + pAll_candidates->push_back(refined_lblock); + + lblock_to_refine = refined_lblock; + } + + return changed_flag; +} + +// given current weight grid, refine subset endpoints +// returns true if improved +static bool refine_endpoints_with_current_weights( + const pixelbuf &src_block, + astc_helpers::log_astc_block& lblock_to_refine, + const pixelbuf subset_pbuf[3], uint32_t subset_pixel_indices[3][256], + const astc_unpacked_config& best_config, + const subset_enc_context& enc_state, + double& best_lblock_error, astc_helpers::log_astc_block& best_lblock, + astc_lblock_vec* pAll_candidates) +{ + assert(!is_lblock_ise(lblock_to_refine)); + assert((lblock_to_refine.m_num_partitions >= 2) && (lblock_to_refine.m_num_partitions <= 3)); + + const uint32_t block_width = enc_state.m_block_width, block_height = enc_state.m_block_height; + const uint32_t total_block_pixels = block_width * block_height; + + const uint32_t num_cem_endpoint_vals = astc_helpers::get_num_cem_values(best_config.m_cem); + //const uint32_t num_weight_levels = get_levels(best_config.m_weight_range); + + const uint32_t total_grid_weights = best_config.m_grid_width * best_config.m_grid_height; + + const uint32_t num_partitions = lblock_to_refine.m_num_partitions; + + [[maybe_unused]] const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(best_config.m_endpoint_range); + const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(best_config.m_weight_range); + + astc_helpers::log_astc_block trial_block(lblock_to_refine); + + uint8_t dequant_grid_weights[astc_helpers::MAX_GRID_WEIGHTS]; // grid res, [0,64] + + for (uint32_t i = 0; i < total_grid_weights; i++) + { + dequant_grid_weights[i] = (uint8_t)weight_tab.get_rank_to_val(trial_block.m_weights[i]); + } + + uint8_t dequant_block_weights[astc_helpers::MAX_BLOCK_PIXELS]; // block res, [0,64] + + if ((trial_block.m_grid_width == (int)block_width) && (trial_block.m_grid_height == (int)block_height)) + memcpy(dequant_block_weights, dequant_grid_weights, total_block_pixels); + else + astc_helpers::upsample_weight_grid_xuastc_ldr(block_width, block_height, trial_block.m_grid_width, trial_block.m_grid_height, dequant_grid_weights, dequant_block_weights, nullptr, nullptr); + + const uint32_t num_cem_chans = get_num_cem_chans(best_config.m_cem); + + for (uint32_t subset_index = 0; subset_index < num_partitions; subset_index++) + { + const uint32_t num_subset_pixels = subset_pbuf[subset_index].m_width; + assert(num_subset_pixels); + + uint8_t subset_weights[astc_helpers::MAX_BLOCK_PIXELS]; + for (uint32_t i = 0; i < num_subset_pixels; i++) + { + const uint32_t subset_pixel_index = subset_pixel_indices[subset_index][i]; + + subset_weights[i] = dequant_block_weights[subset_pixel_index]; + } // i + + float new_cem_valsf[8]; + + refine_endpoints_given_weights(subset_pbuf[subset_index], best_config.m_cem, subset_weights, astc_helpers::BISE_64_LEVELS, + lblock_to_refine.m_endpoints + num_cem_endpoint_vals * subset_index, best_config.m_endpoint_range, new_cem_valsf, num_cem_chans); + + uint8_t new_cem_vals[8]; + cem_encode(best_config.m_cem, new_cem_valsf, best_config.m_endpoint_range, new_cem_vals, true, enc_state.m_higher_effort_bc); + + for (uint32_t i = 0; i < num_cem_endpoint_vals; i++) + trial_block.m_endpoints[num_cem_endpoint_vals * subset_index + i] = new_cem_vals[i]; + + } // subset_index + + const double trial_error = compute_block_error(trial_block, src_block, enc_state); + bool status = false; + + if (trial_error < best_lblock_error) + { + best_lblock_error = trial_error; + memcpy(&best_lblock, &trial_block, sizeof(best_lblock)); + status = true; + } + + if (pAll_candidates) + pAll_candidates->push_back(trial_block); + + lblock_to_refine = trial_block; + + return status; +} + +static void compress_block_2subsets_internal( + const subset_enc_context& enc_context, + const uint8_t* pBlock_pixels, + double& best_lblock_error, astc_helpers::log_astc_block& best_lblock, + astc_lblock_vec* pAll_candidates, + single_subset_shortlist_state &shortlist_state) +{ + assert(enc_context.m_use_method1 || enc_context.m_use_method2); + + const uint32_t block_width = enc_context.m_block_width, block_height = enc_context.m_block_height; + const uint32_t total_block_pixels = enc_context.m_total_block_pixels; + + const uint32_t num_src_block_chans = shortlist_state.m_num_src_block_comps; + [[maybe_unused]] const bool has_a = (num_src_block_chans == 4); + + rgba32_image block_img; + block_img.m_pPixels = pBlock_pixels; + block_img.m_width = block_width; + block_img.m_height = block_height; + block_img.m_row_pitch_in_texels = block_width; + + const float SUBSET_SCALE_WEIGHT = 2.0f; + + const uint32_t total_candidates = generate_single_subset_shortlist( + TOTAL_TWO_SUBSET_CONFIGS_RGBA, g_two_subset_configs_rgba, + enc_context, + block_img, + shortlist_state.m_src_is_luma_only, + shortlist_state.m_num_src_block_comps, + shortlist_state, SUBSET_SCALE_WEIGHT, enc_context.m_num_carrier_candidates); + + if (!total_candidates) + { + assert(0); + return; + } + +#if 0 + encode_single_subset_block(enc_context, shortlist_state.m_pbuf, shortlist_state.m_num_src_block_comps, shortlist_state.m_best_configs[0], best_lblock, false); + + best_lblock.m_color_endpoint_modes[1] = best_lblock.m_color_endpoint_modes[0]; + + uint32_t num_endpoint_vals = astc_helpers::get_num_cem_values(best_lblock.m_color_endpoint_modes[0]); + memcpy(best_lblock.m_endpoints + num_endpoint_vals, best_lblock.m_endpoints, num_endpoint_vals); + + best_lblock.m_num_partitions = 2; + best_lblock.m_partition_id = 1; + + convert_rank_lblock_to_ise(best_lblock); + return; +#endif + + static const uint8_t s_num_dot_thresh_fracs[NUM_DOT_THRESH_FRACTS] = { 1, 3, 5, 9, 11, 15 }; + + static const float s_dot_thresh_fracs15[15] = { -.55f, -0.45f, -0.35f, -0.25f, -0.15f, -0.10f, -0.075f, 0.0f, 0.075f, 0.10f, 0.15f, 0.25f, 0.35f, 0.45f, 0.55f }; + static const float s_dot_thresh_fracs11[11] = { -0.45f, -0.35f, -0.25f, -0.15f, -0.075f, 0.0f, 0.075f, 0.15f, 0.25f, 0.35f, 0.45f }; + static const float s_dot_thresh_fracs9[9] = { -0.35f, -0.25f, -0.15f, -0.075f, 0.0f, 0.075f, 0.15f, 0.25f, 0.35f }; + static const float s_dot_thresh_fracs5[5] = { -.2f, -0.1f, 0.0f, .1f, .2f }; + static const float s_dot_thresh_fracs3[3] = { -0.1f, 0.0f, .1f }; + static const float s_dot_thresh_fracs1[1] = { 0.0f }; + static const float* s_pDot_thresh_fracts[NUM_DOT_THRESH_FRACTS] = { s_dot_thresh_fracs1, s_dot_thresh_fracs3, s_dot_thresh_fracs5, s_dot_thresh_fracs9, s_dot_thresh_fracs11, s_dot_thresh_fracs15 }; + + const uint32_t dt_index = minimum(NUM_DOT_THRESH_FRACTS - 1, enc_context.m_two_subset_dot_thresh_fract_index); + const uint32_t num_dot_thresh_fracts = s_num_dot_thresh_fracs[dt_index]; + const float* const pDot_thresh_fracts = s_pDot_thresh_fracts[dt_index]; + + float dot_range = 0; + + if (num_dot_thresh_fracts > 1) + { + const block_stats& stats = shortlist_state.m_stats; + + const float mean_r = stats.m_mean[0], mean_g = stats.m_mean[1], mean_b = stats.m_mean[2], mean_a = stats.m_mean[3]; + const float axis_r = stats.m_axis[0], axis_g = stats.m_axis[1], axis_b = stats.m_axis[2], axis_a = stats.m_axis[3]; + + float min_dot = FLT_MAX; + float max_dot = -FLT_MAX; + + const uint8_t* pSrc_pixels = pBlock_pixels; + + for (uint32_t y = 0; y < block_height; y++) + { + for (uint32_t x = 0; x < block_width; x++) + { + const float r = (float)pSrc_pixels[0] - mean_r; + const float g = (float)pSrc_pixels[1] - mean_g; + const float b = (float)pSrc_pixels[2] - mean_b; + const float a = (float)pSrc_pixels[3] - mean_a; + pSrc_pixels += 4; + + const float dot = r * axis_r + g * axis_g + b * axis_b + a * axis_a; + + min_dot = minimum(min_dot, dot); + max_dot = maximum(max_dot, dot); + } + } + + dot_range = max_dot - min_dot; + } + + for (uint32_t config_cand_index = 0; config_cand_index < total_candidates; config_cand_index++) + { + const astc_unpacked_config& best_config = shortlist_state.m_best_configs[config_cand_index]; + + const bool cem_has_a = does_cem_have_alpha(best_config.m_cem); + const uint32_t num_cem_comps = get_num_cem_chans(best_config.m_cem); + + const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, best_config.m_grid_width, best_config.m_grid_height); + + astc_helpers::log_astc_block carrier_lblock; // carrier block in rank space + + encode_single_subset_block(enc_context, shortlist_state.m_pbuf, shortlist_state.m_num_src_block_comps, best_config, carrier_lblock, false); + + // base+ofs is so marginal that it's not worth the effort/extra complexity here + assert(!is_cem_9_or_13(carrier_lblock.m_color_endpoint_modes[0])); + + convert_ise_lblock_to_rank(carrier_lblock); + + for (uint32_t dot_thresh_fract_iter = 0; dot_thresh_fract_iter < num_dot_thresh_fracts; dot_thresh_fract_iter++) + { + const float dot_thresh_fract = pDot_thresh_fracts[dot_thresh_fract_iter]; + + bitmask192 desired_bitmask(0, 0, 0); + + { + const block_stats& stats = shortlist_state.m_stats; + + const float mean_r = stats.m_mean[0], mean_g = stats.m_mean[1], mean_b = stats.m_mean[2], mean_a = stats.m_mean[3]; + const float axis_r = stats.m_axis[0], axis_g = stats.m_axis[1], axis_b = stats.m_axis[2], axis_a = stats.m_axis[3]; + + uint32_t bit_ofs = 0; + const uint8_t* pSrc_pixels = pBlock_pixels; + + const float subset_dot_thresh = dot_thresh_fract * dot_range; + + for (uint32_t y = 0; y < block_height; y++) + { + for (uint32_t x = 0; x < block_width; x++) + { + const float r = ((float)pSrc_pixels[0] - mean_r); + const float g = ((float)pSrc_pixels[1] - mean_g); + const float b = ((float)pSrc_pixels[2] - mean_b); + const float a = ((float)pSrc_pixels[3] - mean_a); + pSrc_pixels += 4; + + const float dot = (r * axis_r) + (g * axis_g) + (b * axis_b) + (a * axis_a); + + const uint32_t s = (dot > subset_dot_thresh) ? 1 : 0; + desired_bitmask.set_bit(bit_ofs, s); + + bit_ofs++; + } // x + } // y + } + + const bitmask192 active_bitmask(bitmask192::lsb_mask(total_block_pixels)); + const bitmask192 desired_bitmask_inverted = (~desired_bitmask) & active_bitmask; + + assert(desired_bitmask <= active_bitmask); + assert(desired_bitmask_inverted <= active_bitmask); + + uint32_t best_diffs[MAX_UNIQUE_2SUBSET_PATS]; + assert(enc_context.m_num_unique_two_subset_pats <= MAX_UNIQUE_2SUBSET_PATS); + + if (enc_context.m_num_pattern_candidates == 1) + { + uint32_t best_diff = UINT32_MAX, best_diff_inverted = UINT32_MAX; + + for (uint32_t i = 0; i < enc_context.m_num_unique_two_subset_pats; i++) + { + const bitmask192 pat_bitmask = enc_context.m_two_subset_pat_bitmask[i]; + + const uint32_t diff = (popcount192(pat_bitmask ^ desired_bitmask) << 16) | i; + const uint32_t diff_inverted = (popcount192(pat_bitmask ^ desired_bitmask_inverted) << 16) | i; + + best_diff = minimum(diff, best_diff); + best_diff_inverted = minimum(diff_inverted, best_diff_inverted); + } + + best_diffs[0] = minimum(best_diff, best_diff_inverted); + } + else + { + for (uint32_t i = 0; i < enc_context.m_num_unique_two_subset_pats; i++) + { + const bitmask192 pat_bitmask = enc_context.m_two_subset_pat_bitmask[i]; + assert(pat_bitmask <= active_bitmask); + + const uint32_t diff = popcount192(pat_bitmask ^ desired_bitmask); + const uint32_t diff_inverted = popcount192(pat_bitmask ^ desired_bitmask_inverted); + + best_diffs[i] = (minimum(diff, diff_inverted) << 16) + i; + } + + std::sort(best_diffs, best_diffs + enc_context.m_num_unique_two_subset_pats); + } + + for (uint32_t pat_cand_iter = 0; pat_cand_iter < enc_context.m_num_pattern_candidates; pat_cand_iter++) + { + const uint32_t best_unique_pat_index = best_diffs[pat_cand_iter] & 1023; + assert(best_unique_pat_index < enc_context.m_num_unique_two_subset_pats); + + const bitmask192 best_pat_bitmask = enc_context.m_two_subset_pat_bitmask[best_unique_pat_index]; + + const uint32_t best_seed_id = enc_context.m_pUnique_two_subset_pats[best_unique_pat_index]; + + // [3] but we only use [2] here + + float subset_pixels[3][PIXELBUF_SIZE_IN_FLOATS]; + + pixelbuf subset_pbuf[3]; + subset_pbuf[0].m_pBuf = subset_pixels[0]; + subset_pbuf[0].m_width = 0; + subset_pbuf[0].m_height = 1; + + subset_pbuf[1].m_pBuf = subset_pixels[1]; + subset_pbuf[1].m_width = 0; + subset_pbuf[1].m_height = 1; + + uint32_t subset_pixel_indices[3][256]; + + const uint8_t* pSrc_pixels = pBlock_pixels; + uint32_t bit_ofs = 0; + + for (uint32_t y = 0; y < block_height; y++) + { + for (uint32_t x = 0; x < block_width; x++) + { + uint32_t s = (uint32_t)(best_pat_bitmask.is_bit_set(bit_ofs)); + bit_ofs++; + + const float r = (float)pSrc_pixels[0]; + const float g = (float)pSrc_pixels[1]; + const float b = (float)pSrc_pixels[2]; + const float a = cem_has_a ? (float)pSrc_pixels[3] : 255; + pSrc_pixels += 4; + + const uint32_t cur_width = subset_pbuf[s].m_width; + + subset_pixel_indices[s][cur_width] = x + y * block_width; + + pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 0, r); + pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 1, g); + pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 2, b); + pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 3, a); + + subset_pbuf[s].m_width = cur_width + 1; + + } // x + + } // y + + assert(subset_pbuf[0].m_width && subset_pbuf[1].m_width); + + // ------- + + astc_helpers::log_astc_block final_lblock; + final_lblock.clear(); + final_lblock.m_user_mode = cUserModeRankValues; + + final_lblock.m_grid_width = carrier_lblock.m_grid_width; + final_lblock.m_grid_height = carrier_lblock.m_grid_height; + final_lblock.m_endpoint_ise_range = best_config.m_endpoint_range; + final_lblock.m_weight_ise_range = best_config.m_weight_range; + final_lblock.m_num_partitions = 2; + final_lblock.m_partition_id = safe_cast_uint16(best_seed_id); + final_lblock.m_color_endpoint_modes[0] = best_config.m_cem; + final_lblock.m_color_endpoint_modes[1] = best_config.m_cem; + + // ------- + + const uint32_t num_cem_endpoint_vals = astc_helpers::get_num_cem_values(best_config.m_cem); + const uint32_t total_grid_weights = carrier_lblock.m_grid_width * carrier_lblock.m_grid_height; + + //const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(best_config.m_endpoint_range); + const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(best_config.m_weight_range); + + [[maybe_unused]] const uint32_t num_weight_levels = astc_helpers::get_ise_levels(best_config.m_weight_range); + + // ------- method 1 + if (enc_context.m_use_method1) + { + memcpy(final_lblock.m_weights, carrier_lblock.m_weights, total_grid_weights); + memcpy(final_lblock.m_endpoints, carrier_lblock.m_endpoints, num_cem_endpoint_vals); + memcpy(final_lblock.m_endpoints + num_cem_endpoint_vals, carrier_lblock.m_endpoints, num_cem_endpoint_vals); + + const uint32_t NUM_M1_PASSES = 2; + + for (uint32_t pass = 0; pass < NUM_M1_PASSES; pass++) + { + refine_endpoints_with_current_weights(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context, + best_lblock_error, best_lblock, nullptr); + + bool changed_flag; + + if (pass == (NUM_M1_PASSES - 1)) + { + changed_flag = weight_polish(shortlist_state.m_pbuf, final_lblock, enc_context, pGrid_data, + best_lblock_error, best_lblock, nullptr); + } + else + { + changed_flag = weight_gradient_descent(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context, pGrid_data, + best_lblock_error, best_lblock, nullptr); + } + + if (!changed_flag) + { + break; + } + } + + if (pAll_candidates) + { + pAll_candidates->push_back(final_lblock); + } + } + + double best_pat_lblock_error = DBL_MAX; + astc_helpers::log_astc_block best_pat_lblock; + + // ------- method 2 + if (enc_context.m_use_method2) + { + uint8_t desired_block_weights[astc_helpers::MAX_BLOCK_PIXELS]; // at block res, ranks + +#if defined(DEBUG) || defined(_DEBUG) + memset(desired_block_weights, 0xFF, astc_helpers::MAX_BLOCK_PIXELS); +#endif + for (uint32_t s = 0; s < 2; s++) + { + float initial_endpoints[8]; + calc_initial_cem_endpoints(subset_pbuf[s], initial_endpoints, num_cem_comps, nullptr, is_cem_6_or_10(best_config.m_cem)); + + uint8_t* pSubset_CEM_vals = final_lblock.m_endpoints + s * num_cem_endpoint_vals; + cem_encode(best_config.m_cem, initial_endpoints, best_config.m_endpoint_range, pSubset_CEM_vals, true, enc_context.m_higher_effort_bc); + + uint8_t subset_weights[astc_helpers::MAX_BLOCK_PIXELS]; + eval_weights_first_plane(subset_pbuf[s], subset_weights, best_config.m_weight_range, + best_config.m_cem, pSubset_CEM_vals, best_config.m_endpoint_range, + num_cem_comps); + + for (uint32_t i = 0; i < enc_context.m_num_ls_iterations; i++) + { + float refined_endpoints[8]; + refine_endpoints_given_weights(subset_pbuf[s], best_config.m_cem, + subset_weights, best_config.m_weight_range, pSubset_CEM_vals, best_config.m_endpoint_range, refined_endpoints, num_cem_comps); + + cem_encode(best_config.m_cem, refined_endpoints, best_config.m_endpoint_range, pSubset_CEM_vals, true, enc_context.m_higher_effort_bc); + + eval_weights_first_plane(subset_pbuf[s], subset_weights, best_config.m_weight_range, + best_config.m_cem, pSubset_CEM_vals, best_config.m_endpoint_range, + num_cem_comps); + } // i + + // base+ofs is so rarely a win (< ~1%) that it's not worth the trouble + + for (uint32_t j = 0; j < subset_pbuf[s].m_width; j++) + { + const uint32_t pixel_index = subset_pixel_indices[s][j]; + assert(pixel_index < total_block_pixels); + + desired_block_weights[pixel_index] = subset_weights[j]; + } + + } // s + +#if defined(DEBUG) || defined(_DEBUG) + for (uint32_t i = 0; i < total_block_pixels; i++) + { + assert(desired_block_weights[i] != 0xFF); + } +#endif + + // now downsample the ideal weights to grid res, quantize + // desired_block_weights[] is either [0,64] (dequantized weight values) or in the carrier's weight rank space depending on solve_weights_dequantized + + if ((block_width == best_config.m_grid_width) && (block_height == best_config.m_grid_height)) + { + assert(total_block_pixels == total_grid_weights); + +memcpy(final_lblock.m_weights, desired_block_weights, total_block_pixels); + } + else + { + // dequantize ranks to [0,64] values + for (uint32_t i = 0; i < total_block_pixels; i++) + desired_block_weights[i] = (uint8_t)weight_tab.get_rank_to_val(desired_block_weights[i]); + + // downsample from block to grid res, [0,64] values + uint8_t downsampled_weights[astc_helpers::MAX_GRID_WEIGHTS]; + +#if 0 + basisu::downsample_weight_grid(pGrid_data->m_downsample_matrix.get_ptr(), + block_width, block_height, + best_config.m_grid_width, best_config.m_grid_height, + desired_block_weights, + downsampled_weights); +#else + float src_temp[PIXELBUF_COMP_PITCH]; + pixelbuf src_pbuf(block_width, block_height, src_temp); + for (uint32_t y = 0; y < block_height; y++) + for (uint32_t x = 0; x < block_width; x++) + pixelbuf_set_comp(src_pbuf, x, y, 0, desired_block_weights[x + y * block_width]); + + float dst_temp[PIXELBUF_COMP_PITCH]; + pixelbuf dst_pbuf(best_config.m_grid_width, best_config.m_grid_height, dst_temp); + + pseudoinverse_block_to_grid(src_pbuf, dst_pbuf, 1); + for (uint32_t y = 0; y < best_config.m_grid_height; y++) + for (uint32_t x = 0; x < best_config.m_grid_width; x++) + downsampled_weights[x + y * best_config.m_grid_width] = (uint8_t)clamp((int)std::round(pixelbuf_get_comp(dst_pbuf, x, y, 0)), 0, 64); +#endif + + // quantize downsampled weights + for (uint32_t i = 0; i < total_grid_weights; i++) + final_lblock.m_weights[i] = (uint8_t)weight_tab.get_val_to_rank(downsampled_weights[i]); + } + + { + // convert from rank to ISE space + const double subset_error = compute_block_error(final_lblock, shortlist_state.m_pbuf, enc_context); + + if (subset_error < best_pat_lblock_error) + { + best_pat_lblock_error = subset_error; + best_pat_lblock = final_lblock; + } + } + + const uint32_t NUM_M2_PASSES = 2; + + for (uint32_t pass = 0; pass < NUM_M2_PASSES; pass++) + { + refine_endpoints_with_current_weights(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context, + best_pat_lblock_error, best_pat_lblock, nullptr); + + bool changed_flag; + + if (pass == (NUM_M2_PASSES - 1)) + { + changed_flag = weight_polish(shortlist_state.m_pbuf, final_lblock, enc_context, pGrid_data, + best_pat_lblock_error, best_pat_lblock, nullptr); + } + else + { + changed_flag = weight_gradient_descent(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context, pGrid_data, + best_pat_lblock_error, best_pat_lblock, nullptr); + } + + if (!changed_flag) + { + break; + } + } + + assert(best_pat_lblock_error != DBL_MAX); + if (best_pat_lblock_error < best_lblock_error) + { + best_lblock_error = best_pat_lblock_error; + best_lblock = best_pat_lblock; + } + + if (pAll_candidates) + { + pAll_candidates->push_back(best_pat_lblock); + } + } + + } // pat_cand_iter + + } // ds + + } // config_cand_index + + convert_rank_lblock_to_ise(best_lblock); +} + +static bool create_desired_partitions_3subsets( + const block_stats& stats, + uint32_t total_block_pixels, + const uint8_t *pBlock_pixels, + uint8_t *pDesired_part) +{ + memset(pDesired_part, 0, total_block_pixels); + + const uint32_t NUM_SUBSETS = 3; + + const float mean_r = stats.m_mean[0], mean_g = stats.m_mean[1], mean_b = stats.m_mean[2], mean_a = stats.m_mean[3]; + const float axis_r = stats.m_axis[0], axis_g = stats.m_axis[1], axis_b = stats.m_axis[2], axis_a = stats.m_axis[3]; + + float cluster_centroids[NUM_SUBSETS][4]; + clear_obj(cluster_centroids); + + float brightest_inten = -BIG_FLOAT_VAL, darkest_inten = BIG_FLOAT_VAL; + + const uint8_t* pSrc_pixels = pBlock_pixels; + for (uint32_t i = 0; i < total_block_pixels; i++, pSrc_pixels += 4) + { + float v[4]; + vec4_load_u8(v, pSrc_pixels); + + const float inten = + ((v[0] - mean_r) * axis_r) + ((v[1] - mean_g) * axis_g) + + ((v[2] - mean_b) * axis_b) + ((v[3] - mean_a) * axis_a); + + if (inten < darkest_inten) + { + darkest_inten = inten; + vec4_copy(cluster_centroids[0], v); + } + + if (inten > brightest_inten) + { + brightest_inten = inten; + vec4_copy(cluster_centroids[1], v); + } + + } // i + + float furthest_dist2 = 0.0f; + + vec4_copy(cluster_centroids[2], cluster_centroids[0]); + + pSrc_pixels = pBlock_pixels; + for (uint32_t i = 0; i < total_block_pixels; i++, pSrc_pixels += 4) + { + float v[4]; + vec4_set(v, (float)pSrc_pixels[0], (float)pSrc_pixels[1], (float)pSrc_pixels[2], (float)pSrc_pixels[3]); + + float dist_a = vec4_squared_dist(v, cluster_centroids[0]); + if (dist_a == 0.0f) + continue; + + float dist_b = vec4_squared_dist(v, cluster_centroids[1]); + if (dist_b == 0.0f) + continue; + + float dist2 = dist_a + dist_b; + if (dist2 > furthest_dist2) + { + furthest_dist2 = dist2; + vec4_copy(cluster_centroids[2], v); + } + } + + if (vec4_compare(cluster_centroids[0], cluster_centroids[1]) || + vec4_compare(cluster_centroids[0], cluster_centroids[2]) || + vec4_compare(cluster_centroids[1], cluster_centroids[2])) + { + return false; + } + + uint32_t num_cluster_pixels[NUM_SUBSETS]; + + const uint32_t NUM_ITERS = 4; + + for (uint32_t s = 0; s < NUM_ITERS; s++) + { + float new_cluster_means[NUM_SUBSETS][4]; + + clear_obj(num_cluster_pixels); + clear_obj(new_cluster_means); + + pSrc_pixels = pBlock_pixels; + + bool changed_flag = false; + + for (uint32_t i = 0; i < total_block_pixels; i++, pSrc_pixels += 4) + { + float v[4]; + vec4_set(v, (float)pSrc_pixels[0], (float)pSrc_pixels[1], (float)pSrc_pixels[2], (float)pSrc_pixels[3]); + + const float d0 = vec4_squared_dist(v, cluster_centroids[0]); + const float d1 = vec4_squared_dist(v, cluster_centroids[1]); + const float d2 = vec4_squared_dist(v, cluster_centroids[2]); + + uint32_t idx = 0; float md = d0; + if (d1 < md) { md = d1; idx = 1; } + if (d2 < md) { idx = 2; } + + if (idx != pDesired_part[i]) + changed_flag = true; + + pDesired_part[i] = (uint8_t)idx; + + vec4_add(new_cluster_means[idx], v); + + num_cluster_pixels[idx]++; + } // i + + if (!num_cluster_pixels[0] || !num_cluster_pixels[1] || !num_cluster_pixels[2]) + return false; + + if (!changed_flag) + break; + + if (s < (NUM_ITERS - 1)) + { + for (uint32_t j = 0; j < NUM_SUBSETS; j++) + vec4_scale(cluster_centroids[j], new_cluster_means[j], 1.0f / (float)num_cluster_pixels[j]); + } + + } // s + + return true; +} + +static void compress_block_3subsets_internal( + const subset_enc_context& enc_context, + subset_enc_thread_context& enc_thread_context, + const uint8_t* pBlock_pixels, + double& best_lblock_error, astc_helpers::log_astc_block& best_lblock, + astc_lblock_vec* pAll_candidates, + single_subset_shortlist_state& shortlist_state) +{ + const uint32_t block_width = enc_context.m_block_width, block_height = enc_context.m_block_height; + const uint32_t total_block_pixels = enc_context.m_total_block_pixels; + + const uint32_t num_src_block_chans = shortlist_state.m_num_src_block_comps; + [[maybe_unused]] const bool has_a = (num_src_block_chans == 4); + + rgba32_image block_img; + block_img.m_pPixels = pBlock_pixels; + block_img.m_width = block_width; + block_img.m_height = block_height; + block_img.m_row_pitch_in_texels = block_width; + + const float SUBSET_SCALE_WEIGHT = 2.0f; + + const uint32_t total_candidates = generate_single_subset_shortlist( + TOTAL_THREE_SUBSET_CONFIGS_RGBA, g_three_subset_configs_rgba, + enc_context, + block_img, + shortlist_state.m_src_is_luma_only, + shortlist_state.m_num_src_block_comps, + shortlist_state, SUBSET_SCALE_WEIGHT, enc_context.m_num_carrier_candidates); + + if (!total_candidates) + { + assert(0); + return; + } + +#if 0 + encode_single_subset_block(enc_context, shortlist_state.m_pbuf, shortlist_state.m_num_src_block_comps, shortlist_state.m_best_configs[0], best_lblock, false); + + best_lblock.m_color_endpoint_modes[1] = best_lblock.m_color_endpoint_modes[0]; + best_lblock.m_color_endpoint_modes[2] = best_lblock.m_color_endpoint_modes[0]; + + const uint32_t num_endpoint_vals = astc_helpers::get_num_cem_values(best_lblock.m_color_endpoint_modes[0]); + memcpy(best_lblock.m_endpoints + num_endpoint_vals, best_lblock.m_endpoints, num_endpoint_vals); + memcpy(best_lblock.m_endpoints + num_endpoint_vals * 2, best_lblock.m_endpoints, num_endpoint_vals); + + best_lblock.m_num_partitions = 3; + best_lblock.m_partition_id = 1; + + convert_rank_lblock_to_ise(best_lblock); + return; +#endif + + enc_thread_context.m_pat_vec.init(enc_context.m_block_width, enc_context.m_block_height); + + if (!create_desired_partitions_3subsets(shortlist_state.m_stats, total_block_pixels, pBlock_pixels, enc_thread_context.m_pat_vec.m_parts)) + return; + + astc_ldr::partitions_data* pPart_data = enc_context.m_pPart_data_p3; + + uint32_t cand_patterns[MAX_UNIQUE_3SUBSET_PATS]; + assert(enc_context.m_num_pattern_candidates <= MAX_UNIQUE_3SUBSET_PATS); + const uint32_t num_pat_candidates = pPart_data->m_part_lhs_map.find(enc_thread_context.m_pat_vec, cand_patterns, enc_context.m_num_pattern_candidates, false); + + if (!num_pat_candidates) + return; + + for (uint32_t config_cand_index = 0; config_cand_index < total_candidates; config_cand_index++) + { + const astc_unpacked_config& best_config = shortlist_state.m_best_configs[config_cand_index]; + + const bool cem_has_a = does_cem_have_alpha(best_config.m_cem); + const uint32_t num_cem_comps = get_num_cem_chans(best_config.m_cem); + + const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, best_config.m_grid_width, best_config.m_grid_height); + + astc_helpers::log_astc_block carrier_lblock; // carrier block in rank space + + encode_single_subset_block(enc_context, shortlist_state.m_pbuf, shortlist_state.m_num_src_block_comps, best_config, carrier_lblock, false); + + // base+ofs is so marginal that it's not worth the effort/extra complexity here + assert(!is_cem_9_or_13(carrier_lblock.m_color_endpoint_modes[0])); + + convert_ise_lblock_to_rank(carrier_lblock); + + for (uint32_t pat_cand_iter = 0; pat_cand_iter < num_pat_candidates; pat_cand_iter++) + { + const uint32_t best_unique_pat_index = cand_patterns[pat_cand_iter]; + assert(best_unique_pat_index < pPart_data->m_total_unique_patterns); + + const uint32_t best_seed_id = pPart_data->m_unique_index_to_part_seed[best_unique_pat_index]; + assert((int)best_unique_pat_index == pPart_data->m_part_seed_to_unique_index[best_seed_id]); + + float subset_pixels[3][PIXELBUF_SIZE_IN_FLOATS]; + + pixelbuf subset_pbuf[3]; + subset_pbuf[0].m_pBuf = subset_pixels[0]; + subset_pbuf[0].m_width = 0; + subset_pbuf[0].m_height = 1; + + subset_pbuf[1].m_pBuf = subset_pixels[1]; + subset_pbuf[1].m_width = 0; + subset_pbuf[1].m_height = 1; + + subset_pbuf[2].m_pBuf = subset_pixels[2]; + subset_pbuf[2].m_width = 0; + subset_pbuf[2].m_height = 1; + + uint32_t subset_pixel_indices[3][256]; + + const uint8_t* pSrc_pixels = pBlock_pixels; + [[maybe_unused]] uint32_t bit_ofs = 0; + + for (uint32_t y = 0; y < block_height; y++) + { + for (uint32_t x = 0; x < block_width; x++) + { + const uint32_t s = pPart_data->m_partition_pats[best_unique_pat_index](x, y); + assert(s < 3); + + const float r = (float)pSrc_pixels[0]; + const float g = (float)pSrc_pixels[1]; + const float b = (float)pSrc_pixels[2]; + const float a = cem_has_a ? (float)pSrc_pixels[3] : 255; + pSrc_pixels += 4; + + const uint32_t cur_width = subset_pbuf[s].m_width; + + subset_pixel_indices[s][cur_width] = x + y * block_width; + + pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 0, r); + pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 1, g); + pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 2, b); + pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 3, a); + + subset_pbuf[s].m_width = cur_width + 1; + + } // x + + } // y + + assert(subset_pbuf[0].m_width && subset_pbuf[1].m_width && subset_pbuf[2].m_width); + + // ------- + + astc_helpers::log_astc_block final_lblock; + final_lblock.clear(); + final_lblock.m_user_mode = cUserModeRankValues; + + final_lblock.m_grid_width = carrier_lblock.m_grid_width; + final_lblock.m_grid_height = carrier_lblock.m_grid_height; + final_lblock.m_endpoint_ise_range = best_config.m_endpoint_range; + final_lblock.m_weight_ise_range = best_config.m_weight_range; + final_lblock.m_num_partitions = 3; + final_lblock.m_partition_id = safe_cast_uint16(best_seed_id); + final_lblock.m_color_endpoint_modes[0] = best_config.m_cem; + final_lblock.m_color_endpoint_modes[1] = best_config.m_cem; + final_lblock.m_color_endpoint_modes[2] = best_config.m_cem; + + // ------- + + const uint32_t num_cem_endpoint_vals = astc_helpers::get_num_cem_values(best_config.m_cem); + const uint32_t total_grid_weights = carrier_lblock.m_grid_width * carrier_lblock.m_grid_height; + + //const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(best_config.m_endpoint_range); + const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(best_config.m_weight_range); + + [[maybe_unused]] const uint32_t num_weight_levels = astc_helpers::get_ise_levels(best_config.m_weight_range); + + // ------- method 1 + if (enc_context.m_use_method1) + { + memcpy(final_lblock.m_weights, carrier_lblock.m_weights, total_grid_weights); + memcpy(final_lblock.m_endpoints, carrier_lblock.m_endpoints, num_cem_endpoint_vals); + memcpy(final_lblock.m_endpoints + num_cem_endpoint_vals, carrier_lblock.m_endpoints, num_cem_endpoint_vals); + memcpy(final_lblock.m_endpoints + 2 * num_cem_endpoint_vals, carrier_lblock.m_endpoints, num_cem_endpoint_vals); + + const uint32_t NUM_M1_PASSES = 2; + + for (uint32_t pass = 0; pass < NUM_M1_PASSES; pass++) + { + refine_endpoints_with_current_weights(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context, + best_lblock_error, best_lblock, nullptr); + + bool changed_flag; + + if (pass == (NUM_M1_PASSES - 1)) + { + changed_flag = weight_polish(shortlist_state.m_pbuf, final_lblock, enc_context, pGrid_data, + best_lblock_error, best_lblock, nullptr); + } + else + { + changed_flag = weight_gradient_descent(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context, pGrid_data, + best_lblock_error, best_lblock, nullptr); + } + + if (!changed_flag) + { + break; + } + } + + if (pAll_candidates) + { + pAll_candidates->push_back(final_lblock); + } + } + + double best_pat_lblock_error = DBL_MAX; + astc_helpers::log_astc_block best_pat_lblock; + + // ------- method 2 + if (enc_context.m_use_method2) + { + uint8_t desired_block_weights[astc_helpers::MAX_BLOCK_PIXELS]; // at block res, ranks + +#if defined(DEBUG) || defined(_DEBUG) + memset(desired_block_weights, 0xFF, astc_helpers::MAX_BLOCK_PIXELS); +#endif + for (uint32_t s = 0; s < 3; s++) + { + float initial_endpoints[8]; + calc_initial_cem_endpoints(subset_pbuf[s], initial_endpoints, num_cem_comps, nullptr, is_cem_6_or_10(best_config.m_cem)); + + uint8_t* pSubset_CEM_vals = final_lblock.m_endpoints + s * num_cem_endpoint_vals; + cem_encode(best_config.m_cem, initial_endpoints, best_config.m_endpoint_range, pSubset_CEM_vals, true, enc_context.m_higher_effort_bc); + + uint8_t subset_weights[astc_helpers::MAX_BLOCK_PIXELS]; + eval_weights_first_plane(subset_pbuf[s], subset_weights, best_config.m_weight_range, + best_config.m_cem, pSubset_CEM_vals, best_config.m_endpoint_range, + num_cem_comps); + + for (uint32_t i = 0; i < enc_context.m_num_ls_iterations; i++) + { + float refined_endpoints[8]; + refine_endpoints_given_weights(subset_pbuf[s], best_config.m_cem, + subset_weights, best_config.m_weight_range, pSubset_CEM_vals, best_config.m_endpoint_range, refined_endpoints, num_cem_comps); + + cem_encode(best_config.m_cem, refined_endpoints, best_config.m_endpoint_range, pSubset_CEM_vals, true, enc_context.m_higher_effort_bc); + + eval_weights_first_plane(subset_pbuf[s], subset_weights, best_config.m_weight_range, + best_config.m_cem, pSubset_CEM_vals, best_config.m_endpoint_range, + num_cem_comps); + } // i + + // base+ofs is so rarely a win (< ~1%) that it's not worth the trouble + + for (uint32_t j = 0; j < subset_pbuf[s].m_width; j++) + { + const uint32_t pixel_index = subset_pixel_indices[s][j]; + assert(pixel_index < total_block_pixels); + + desired_block_weights[pixel_index] = subset_weights[j]; + } + + } // s + +#if defined(DEBUG) || defined(_DEBUG) + for (uint32_t i = 0; i < total_block_pixels; i++) + { + assert(desired_block_weights[i] != 0xFF); + } +#endif + + // now downsample the ideal weights to grid res, quantize + // desired_block_weights[] is either [0,64] (dequantized weight values) or in the carrier's weight rank space depending on solve_weights_dequantized + + if ((block_width == best_config.m_grid_width) && (block_height == best_config.m_grid_height)) + { + assert(total_block_pixels == total_grid_weights); + + memcpy(final_lblock.m_weights, desired_block_weights, total_block_pixels); + } + else + { + // dequantize ranks to [0,64] values + for (uint32_t i = 0; i < total_block_pixels; i++) + desired_block_weights[i] = (uint8_t)weight_tab.get_rank_to_val(desired_block_weights[i]); + + // downsample from block to grid res, [0,64] values + uint8_t downsampled_weights[astc_helpers::MAX_GRID_WEIGHTS]; + +#if 0 + basisu::downsample_weight_grid(pGrid_data->m_downsample_matrix.get_ptr(), + block_width, block_height, + best_config.m_grid_width, best_config.m_grid_height, + desired_block_weights, + downsampled_weights); +#else + float src_temp[PIXELBUF_COMP_PITCH]; + pixelbuf src_pbuf(block_width, block_height, src_temp); + for (uint32_t y = 0; y < block_height; y++) + for (uint32_t x = 0; x < block_width; x++) + pixelbuf_set_comp(src_pbuf, x, y, 0, desired_block_weights[x + y * block_width]); + + float dst_temp[PIXELBUF_COMP_PITCH]; + pixelbuf dst_pbuf(best_config.m_grid_width, best_config.m_grid_height, dst_temp); + + pseudoinverse_block_to_grid(src_pbuf, dst_pbuf, 1); + for (uint32_t y = 0; y < best_config.m_grid_height; y++) + for (uint32_t x = 0; x < best_config.m_grid_width; x++) + downsampled_weights[x + y * best_config.m_grid_width] = (uint8_t)clamp((int)std::round(pixelbuf_get_comp(dst_pbuf, x, y, 0)), 0, 64); +#endif + + // quantize downsampled weights + for (uint32_t i = 0; i < total_grid_weights; i++) + final_lblock.m_weights[i] = (uint8_t)weight_tab.get_val_to_rank(downsampled_weights[i]); + } + + { + // convert from rank to ISE space + const double subset_error = compute_block_error(final_lblock, shortlist_state.m_pbuf, enc_context); + + if (subset_error < best_pat_lblock_error) + { + best_pat_lblock_error = subset_error; + best_pat_lblock = final_lblock; + } + } + + const uint32_t NUM_M2_PASSES = 2; + + for (uint32_t pass = 0; pass < NUM_M2_PASSES; pass++) + { + refine_endpoints_with_current_weights(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context, + best_pat_lblock_error, best_pat_lblock, nullptr); + + bool changed_flag; + + if (pass == (NUM_M2_PASSES - 1)) + { + changed_flag = weight_polish(shortlist_state.m_pbuf, final_lblock, enc_context, pGrid_data, + best_pat_lblock_error, best_pat_lblock, nullptr); + } + else + { + changed_flag = weight_gradient_descent(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context, pGrid_data, + best_pat_lblock_error, best_pat_lblock, nullptr); + } + + if (!changed_flag) + { + break; + } + } + + assert(best_pat_lblock_error != DBL_MAX); + if (best_pat_lblock_error < best_lblock_error) + { + best_lblock_error = best_pat_lblock_error; + best_lblock = best_pat_lblock; + } + + if (pAll_candidates) + { + pAll_candidates->push_back(best_pat_lblock); + } + } + + } // pat_cand_iter + + } // config_cand_index + + convert_rank_lblock_to_ise(best_lblock); +} + +double compress_block_subsets( + const subset_enc_context& enc_context, + subset_enc_thread_context& enc_thread_context, + const uint8_t* pBlock_pixels, + astc_helpers::log_astc_block& best_lblock, astc_lblock_vec* pAll_candidates) +{ + assert(enc_context.m_use_method1 || enc_context.m_use_method2); + + const bool FORCE_SUBSETS = false; + + single_subset_shortlist_state shortlist_state; + double best_lblock_error = compress_single_subset_internal(enc_context, pBlock_pixels, best_lblock, pAll_candidates, shortlist_state, true, DEF_SCALE_WEIGHT); + + if ((best_lblock.m_solid_color_flag_ldr) || (enc_context.m_max_subsets == 1)) + return best_lblock_error; + + assert(is_lblock_ise(best_lblock)); + + if (FORCE_SUBSETS) + best_lblock_error = DBL_MAX; + + const float max_chan_var = FORCE_SUBSETS ? FLT_MAX : (basisu::maximum( + shortlist_state.m_stats.m_covar[cCovarRR], shortlist_state.m_stats.m_covar[cCovarGG], + shortlist_state.m_stats.m_covar[cCovarBB], shortlist_state.m_stats.m_covar[cCovarAA]) / (float)enc_context.m_total_block_pixels); // TODO: divide + + if (max_chan_var >= enc_context.m_two_subset_var_thresh) + { + compress_block_2subsets_internal( + enc_context, + pBlock_pixels, + best_lblock_error, best_lblock, + pAll_candidates, shortlist_state); + } + + if ((enc_context.m_max_subsets == 3) && (max_chan_var >= enc_context.m_three_subset_var_thresh)) + { + compress_block_3subsets_internal( + enc_context, enc_thread_context, + pBlock_pixels, + best_lblock_error, best_lblock, + pAll_candidates, shortlist_state); + } + + return best_lblock_error; +} + +} // namespace astc_ldrf +} // namespace basisu diff --git a/encoder/basisu_astc_ldr_fencode.h b/encoder/basisu_astc_ldr_fencode.h new file mode 100644 index 0000000..035c9fa --- /dev/null +++ b/encoder/basisu_astc_ldr_fencode.h @@ -0,0 +1,440 @@ +// basisu_astc_ldr_fencode.h +#pragma once +#include "../transcoder/basisu.h" +#include "../transcoder/basisu_transcoder_internal.h" +#include "basisu_astc_ldr_common.h" + +namespace basisu +{ + +namespace astc_ldrf +{ + +BASISU_FORCE_INLINE int popcount64(uint64_t x) +{ +#if defined(__cplusplus) && (__cplusplus >= 202002L) && defined(__cpp_lib_bitops) + return static_cast(std::popcount(x)); + +#elif defined(__EMSCRIPTEN__) || defined(__clang__) || defined(__GNUC__) + return __builtin_popcountll(static_cast(x)); + +#elif defined(_MSC_VER) && defined(_M_X64) + return static_cast(__popcnt64(x)); + +#elif defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_ARM64) || defined(_M_ARM)) + return static_cast( + __popcnt(static_cast(x)) + + __popcnt(static_cast(x >> 32)) + ); + +#else + int count = 0; + while (x) + { + x &= (x - 1); + ++count; + } + return count; +#endif +} + +struct bitmask192 +{ + uint64_t m_a, m_b, m_c; // a=lowest qword (bits 0-63), b=middle (64-127), c=highest (128-191) + + inline bitmask192() {} + + constexpr inline bitmask192(const bitmask192& o) noexcept + : m_a(o.m_a), m_b(o.m_b), m_c(o.m_c) + { + } + + inline bitmask192& operator=(const bitmask192& o) noexcept + { + m_a = o.m_a; + m_b = o.m_b; + m_c = o.m_c; + return *this; + } + + constexpr inline bitmask192(uint64_t a, uint64_t b, uint64_t c) noexcept + : m_a(a), m_b(b), m_c(c) + { + } + + static constexpr inline bitmask192 zero() noexcept + { + return bitmask192(0, 0, 0); + } + + static constexpr inline bitmask192 all() noexcept + { + return bitmask192(UINT64_MAX, UINT64_MAX, UINT64_MAX); + } + + inline void clear() noexcept + { + m_a = 0; + m_b = 0; + m_c = 0; + } + + inline void set_all() noexcept + { + m_a = UINT64_MAX; + m_b = UINT64_MAX; + m_c = UINT64_MAX; + } + + inline bool is_zero() const noexcept + { + return (m_a | m_b | m_c) == 0; + } + + inline bool any() const noexcept + { + return (m_a | m_b | m_c) != 0; + } + + inline bool none() const noexcept + { + return !any(); + } + + inline bool all_set() const noexcept + { + return (m_a == UINT64_MAX) && (m_b == UINT64_MAX) && (m_c == UINT64_MAX); + } + + inline uint32_t popcount() const noexcept + { + return popcount64(m_a) + popcount64(m_b) + popcount64(m_c); + } + + constexpr inline bitmask192 operator~() const noexcept + { + return bitmask192(~m_a, ~m_b, ~m_c); + } + + constexpr inline bitmask192 operator&(const bitmask192& rhs) const noexcept + { + return bitmask192(m_a & rhs.m_a, m_b & rhs.m_b, m_c & rhs.m_c); + } + + constexpr inline bitmask192 operator|(const bitmask192& rhs) const noexcept + { + return bitmask192(m_a | rhs.m_a, m_b | rhs.m_b, m_c | rhs.m_c); + } + + constexpr inline bitmask192 operator^(const bitmask192& rhs) const noexcept + { + return bitmask192(m_a ^ rhs.m_a, m_b ^ rhs.m_b, m_c ^ rhs.m_c); + } + + inline bitmask192& operator&=(const bitmask192& rhs) noexcept + { + m_a &= rhs.m_a; + m_b &= rhs.m_b; + m_c &= rhs.m_c; + return *this; + } + + inline bitmask192& operator|=(const bitmask192& rhs) noexcept + { + m_a |= rhs.m_a; + m_b |= rhs.m_b; + m_c |= rhs.m_c; + return *this; + } + + inline bitmask192& operator^=(const bitmask192& rhs) noexcept + { + m_a ^= rhs.m_a; + m_b ^= rhs.m_b; + m_c ^= rhs.m_c; + return *this; + } + + constexpr inline bool operator==(const bitmask192& rhs) const noexcept + { + return (m_a == rhs.m_a) && (m_b == rhs.m_b) && (m_c == rhs.m_c); + } + + constexpr inline bool operator!=(const bitmask192& rhs) const noexcept + { + return !(*this == rhs); + } + + // m_a is the lowest qword, m_b highest + constexpr inline bool operator<(const bitmask192& rhs) const noexcept + { + if (m_c != rhs.m_c) return m_c < rhs.m_c; + if (m_b != rhs.m_b) return m_b < rhs.m_b; + return m_a < rhs.m_a; + } + + constexpr inline bool operator>(const bitmask192& rhs) const noexcept + { + return rhs < *this; + } + + constexpr inline bool operator<=(const bitmask192& rhs) const noexcept + { + return !(rhs < *this); + } + + constexpr inline bool operator>=(const bitmask192& rhs) const noexcept + { + return !(*this < rhs); + } + + explicit constexpr inline operator bool() const noexcept + { + return (m_a | m_b | m_c) != 0; + } + + inline bool intersects(const bitmask192& rhs) const noexcept + { + return ((m_a & rhs.m_a) | (m_b & rhs.m_b) | (m_c & rhs.m_c)) != 0; + } + + inline bool disjoint(const bitmask192& rhs) const noexcept + { + return !intersects(rhs); + } + + constexpr inline bool contains_all(const bitmask192& rhs) const noexcept + { + return ((m_a & rhs.m_a) == rhs.m_a) && + ((m_b & rhs.m_b) == rhs.m_b) && + ((m_c & rhs.m_c) == rhs.m_c); + } + + constexpr inline bool is_subset_of(const bitmask192& rhs) const noexcept + { + return rhs.contains_all(*this); + } + + inline bool contains_any(const bitmask192& rhs) const noexcept + { + return intersects(rhs); + } + + constexpr inline uint64_t low64() const noexcept + { + return m_a; + } + + constexpr inline uint64_t mid64() const noexcept + { + return m_b; + } + + constexpr inline uint64_t high64() const noexcept + { + return m_c; + } + + inline void set_bit(uint32_t index) noexcept + { + assert(index < 192); + + if (index < 64) + m_a |= uint64_t(1) << index; + else if (index < 128) + m_b |= uint64_t(1) << (index - 64); + else + m_c |= uint64_t(1) << (index - 128); + } + + inline void set_bit(uint32_t index, uint32_t bit_val) noexcept + { + assert(index < 192); + assert(bit_val <= 1); + + if (index < 64) + { + m_a &= ~(uint64_t(1) << index); + m_a |= uint64_t(bit_val) << index; + } + else if (index < 128) + { + m_b &= ~(uint64_t(1) << (index - 64)); + m_b |= uint64_t(bit_val) << (index - 64); + } + else + { + m_c &= ~(uint64_t(1) << (index - 128)); + m_c |= uint64_t(bit_val) << (index - 128); + } + } + + inline bool is_bit_set(uint32_t index) const noexcept + { + assert(index < 192); + + if (index < 64) + return ((m_a >> index) & 1) != 0; + else if (index < 128) + return ((m_b >> (index - 64)) & 1) != 0; + else + return ((m_c >> (index - 128)) & 1) != 0; + } + + static inline bitmask192 lsb_mask(uint32_t num_bits) noexcept + { + assert(num_bits <= 192); + + if (num_bits == 0) + return bitmask192(0, 0, 0); + + if (num_bits < 64) + return bitmask192((uint64_t(1) << num_bits) - 1, 0, 0); + + if (num_bits == 64) + return bitmask192(UINT64_MAX, 0, 0); + + if (num_bits < 128) + { + return bitmask192( + UINT64_MAX, + (uint64_t(1) << (num_bits - 64)) - 1, + 0); + } + + if (num_bits == 128) + return bitmask192(UINT64_MAX, UINT64_MAX, 0); + + if (num_bits < 192) + { + return bitmask192( + UINT64_MAX, + UINT64_MAX, + (uint64_t(1) << (num_bits - 128)) - 1); + } + + return bitmask192(UINT64_MAX, UINT64_MAX, UINT64_MAX); + } +}; + +inline uint32_t popcount192(const bitmask192& a) { return a.popcount(); } + +const uint32_t MAX_CANDIDATES = 512; + +struct rgba32_image +{ + const uint8_t* m_pPixels; + uint32_t m_width; + uint32_t m_height; + uint32_t m_row_pitch_in_texels; // pitch in pixels/texels, not bytes +}; + +struct single_subset_enc_context +{ + uint32_t m_block_width, m_block_height; + uint32_t m_block_size_index; + uint32_t m_total_block_pixels; + + uint32_t m_max_candidates; + uint32_t m_num_ls_iterations; + + uint32_t m_chan_weights[4]; + + basist::astc_ldr_t::dct2f m_dct; + + astc_helpers::decode_mode m_astc_decode_mode; + bool m_disable_dual_plane; + bool m_weight_polishing; + + bool m_has_alpha; + + bool m_try_base_ofs; + bool m_higher_effort_bc; +}; + +const uint32_t MAX_UNIQUE_2SUBSET_PATS = 838; +const uint32_t MAX_UNIQUE_3SUBSET_PATS = 626; + +typedef basisu::vector astc_lblock_vec; + +// source pEndpoints[] = ASTC direct order: LR HR LG HG LB HB LA HA +bool cem_encode(uint32_t cem_index, const float pEndpoints[8], uint32_t endpoint_ise_range, uint8_t* pCEM_values, bool allow_bc = true, bool high_effort = true); + +bool init_single_subset_context( + single_subset_enc_context& ctx, + uint32_t block_width, uint32_t block_height, + astc_helpers::decode_mode astc_decode_mode, + const uint32_t chan_weights[4], + uint32_t max_candidates, uint32_t num_ls_iterations, bool disable_dual_plane, bool has_alpha, bool weight_polishing); + +// best_lblock will be in ise space (see is_lblock_ise() below), elements in array pointed to by pAll_candidates (which may be null) will be in rank space +double compress_single_subset( + single_subset_enc_context& ctx, + const uint8_t* pBlock_pixels, + astc_helpers::log_astc_block& best_lblock, + astc_lblock_vec* pAll_candidates, + bool always_compute_error); + +static const uint32_t NUM_DOT_THRESH_FRACTS = 6; + +struct subset_enc_context : single_subset_enc_context +{ + subset_enc_context() {} + + uint32_t m_max_subsets; + + uint32_t m_num_carrier_candidates; + uint32_t m_num_pattern_candidates; + float m_two_subset_var_thresh; + float m_three_subset_var_thresh; + uint32_t m_two_subset_dot_thresh_fract_index; + + bitmask192 m_two_subset_pat_bitmask[MAX_UNIQUE_2SUBSET_PATS]; + + uint32_t m_num_unique_two_subset_pats; + const uint16_t* m_pUnique_two_subset_pats; + + bool m_use_method1; + bool m_use_method2; + + astc_ldr::partitions_data* m_pPart_data_p2; + astc_ldr::partitions_data* m_pPart_data_p3; +}; + +// must have first called init_single_subset_context() on ctx +bool init_multi_subset_context( + subset_enc_context& ctx, + uint32_t max_subsets, + uint32_t num_carrier_candidates, uint32_t num_pattern_candidates, + float two_subset_var_thresh, uint32_t two_subset_dot_thresh_fract_index, + float three_subset_var_thresh, + astc_ldr::partitions_data* pPart_data_p2, astc_ldr::partitions_data* pPart_data_p3); + +struct subset_enc_thread_context +{ + astc_ldr::partition_pattern_vec m_pat_vec; +}; + +double compress_block_subsets( + const subset_enc_context& enc_context, + subset_enc_thread_context& enc_thread_context, + const uint8_t* pBlock_pixels, + astc_helpers::log_astc_block& best_lblock, astc_lblock_vec* pAll_candidates); + +enum +{ + cUserModeISEValues = 0, + cUserModeRankValues = 1 +}; + +static inline bool is_lblock_ise(const astc_helpers::log_astc_block& log_blk) +{ + // the default user mode value == ISE, 1 = ranks (which is the exceptional case for astc_helpers) + return log_blk.m_user_mode == cUserModeISEValues; +} + +void convert_rank_lblock_to_ise(astc_helpers::log_astc_block& log_blk); +void convert_ise_lblock_to_rank(astc_helpers::log_astc_block& log_blk); + +} // namespace astc_ldr_f +} // namespace basisu diff --git a/encoder/basisu_astc_ldr_pseudoinv_tab.inl b/encoder/basisu_astc_ldr_pseudoinv_tab.inl new file mode 100644 index 0000000..d106198 --- /dev/null +++ b/encoder/basisu_astc_ldr_pseudoinv_tab.inl @@ -0,0 +1,298 @@ +0.6883562f, -0.1883562f, 0.4143836f, 0.0856164f, 0.0856164f, 0.4143836f, -0.1883562f, 0.6883562f, +0.9555160f, -0.2272727f, 0.0444840f, 0.1423488f, 0.7272727f, -0.1423488f, -0.1423488f, 0.7272727f, + +0.1423488f, 0.0444840f, -0.2272727f, 0.9555160f, 0.6000000f, -0.2000000f, 0.4000000f, 0.0000000f, +0.2000000f, 0.2000000f, 0.0000000f, 0.4000000f, -0.2000000f, 0.6000000f, 0.8285714f, -0.1428571f, + +0.0285714f, 0.3428572f, 0.2857143f, -0.0571429f, -0.1428571f, 0.7142857f, -0.1428571f, -0.0571429f, +0.2857143f, 0.3428572f, 0.0285714f, -0.1428571f, 0.8285714f, 0.9857143f, -0.2523810f, 0.0809524f, + +-0.0142857f, 0.0571429f, 1.0095239f, -0.3238095f, 0.0571429f, -0.0857143f, 0.4857143f, 0.4857143f, +-0.0857143f, 0.0571429f, -0.3238095f, 1.0095239f, 0.0571429f, -0.0142857f, 0.0809524f, -0.2523810f, + +0.9857143f, 0.5107527f, -0.1774193f, 0.3817204f, -0.0483871f, 0.2526882f, 0.0806452f, 0.0806452f, +0.2526882f, -0.0483871f, 0.3817204f, -0.1774193f, 0.5107527f, 0.7542282f, -0.1948819f, 0.0528584f, + +0.3983119f, 0.1476378f, -0.0400442f, -0.0169237f, 0.5472441f, -0.1484306f, -0.1484306f, 0.5472441f, +-0.0169237f, -0.0400442f, 0.1476378f, 0.3983119f, 0.0528584f, -0.1948819f, 0.7542282f, 0.9212349f, + +-0.2166766f, 0.0636154f, -0.0130717f, 0.2100402f, 0.5778043f, -0.1696410f, 0.0348577f, -0.1641219f, +0.7987264f, -0.0538284f, 0.0110606f, 0.0110606f, -0.0538284f, 0.7987264f, -0.1641219f, 0.0348577f, + +-0.1696410f, 0.5778043f, 0.2100402f, -0.0130717f, 0.0636154f, -0.2166766f, 0.9212349f, 0.9969320f, +-0.2099229f, 0.0692308f, -0.0208463f, 0.0030679f, 0.0163624f, 1.1195889f, -0.3692308f, 0.1111804f, + +-0.0163624f, -0.0354519f, 0.2408908f, 0.8000000f, -0.2408908f, 0.0354519f, 0.0354519f, -0.2408908f, +0.8000000f, 0.2408908f, -0.0354519f, -0.0163624f, 0.1111804f, -0.3692308f, 1.1195889f, 0.0163624f, + +0.0030679f, -0.0208463f, 0.0692308f, -0.2099229f, 0.9969320f, 0.4159091f, -0.1659091f, 0.3431818f, +-0.0931818f, 0.2340909f, 0.0159091f, 0.1613636f, 0.0886364f, 0.0886364f, 0.1613636f, 0.0159091f, + +0.2340909f, -0.0931818f, 0.3431818f, -0.1659091f, 0.4159091f, 0.6538065f, -0.1721698f, 0.0584577f, +0.3956889f, 0.0400943f, -0.0136134f, 0.1891948f, 0.2099057f, -0.0712703f, -0.0689228f, 0.4221698f, + +-0.1433414f, -0.1433414f, 0.4221698f, -0.0689228f, -0.0712703f, 0.2099057f, 0.1891948f, -0.0136134f, +0.0400943f, 0.3956889f, 0.0584577f, -0.1721698f, 0.6538065f, 0.8053632f, -0.2047127f, 0.0614056f, + +-0.0163868f, 0.3634550f, 0.2204598f, -0.0661291f, 0.0176474f, -0.0784532f, 0.6456324f, -0.1936639f, +0.0516816f, -0.1215507f, 0.4554813f, 0.0815266f, -0.0217564f, -0.0217564f, 0.0815266f, 0.4554813f, + +-0.1215507f, 0.0516816f, -0.1936639f, 0.6456324f, -0.0784532f, 0.0176474f, -0.0661291f, 0.2204598f, +0.3634550f, -0.0163868f, 0.0614056f, -0.2047127f, 0.8053632f, 0.8815933f, -0.2045389f, 0.0750646f, + +-0.0215593f, 0.0044532f, 0.2706439f, 0.4675174f, -0.1715762f, 0.0492785f, -0.0101788f, -0.1695884f, +0.8210233f, -0.1598191f, 0.0459017f, -0.0094813f, -0.0123115f, 0.0596032f, 0.7563307f, -0.2172260f, + +0.0448696f, 0.0448696f, -0.2172260f, 0.7563307f, 0.0596032f, -0.0123115f, -0.0094813f, 0.0459017f, +-0.1598191f, 0.8210233f, -0.1695884f, -0.0101788f, 0.0492785f, -0.1715762f, 0.4675174f, 0.2706439f, + +0.0044532f, -0.0215593f, 0.0750646f, -0.2045389f, 0.8815933f, 0.9672751f, -0.2873513f, 0.0769021f, +-0.0186696f, 0.0054320f, -0.0009586f, 0.1047195f, 0.9195243f, -0.2460869f, 0.0597428f, -0.0173824f, + +0.0030675f, -0.1279904f, 0.6539148f, 0.3007728f, -0.0730190f, 0.0212451f, -0.0037491f, 0.0649557f, +-0.3318644f, 1.0073659f, -0.1058330f, 0.0307925f, -0.0054340f, -0.0067231f, 0.0343492f, -0.1042661f, + +0.9963973f, -0.2899053f, 0.0511598f, -0.0051125f, 0.0261200f, -0.0792867f, 0.3231576f, 0.5710128f, +-0.1007670f, 0.0038343f, -0.0195900f, 0.0594650f, -0.2423682f, 0.9050737f, 0.0755752f, -0.0009586f, + +0.0048975f, -0.0148663f, 0.0605921f, -0.2262684f, 0.9811062f, 0.9997144f, -0.1402052f, 0.0544243f, +-0.0244578f, 0.0084673f, -0.0019448f, 0.0002094f, 0.0022848f, 1.1216413f, -0.4353943f, 0.1956628f, + +-0.0677386f, 0.0155583f, -0.0016755f, -0.0063974f, 0.0594046f, 1.2191039f, -0.5478558f, 0.1896682f, +-0.0435634f, 0.0046914f, 0.0100531f, -0.0933501f, 0.3699796f, 0.8609163f, -0.2980500f, 0.0684567f, + +-0.0073723f, -0.0100531f, 0.0933501f, -0.3699796f, 0.9168615f, 0.2980500f, -0.0684567f, 0.0073723f, +0.0058643f, -0.0544542f, 0.2158214f, -0.5348359f, 1.1594708f, 0.0399331f, -0.0043005f, -0.0016755f, + +0.0155583f, -0.0616633f, 0.1528102f, -0.3312774f, 1.1314477f, 0.0012287f, 0.0002094f, -0.0019448f, +0.0077079f, -0.0191013f, 0.0414097f, -0.1414310f, 0.9998464f, 0.3539683f, -0.1539683f, 0.2904762f, + +-0.0904762f, 0.2269841f, -0.0269841f, 0.1952381f, 0.0047619f, 0.1317460f, 0.0682540f, 0.0682540f, +0.1317460f, 0.0047619f, 0.1952381f, -0.0269841f, 0.2269841f, -0.0904762f, 0.2904762f, -0.1539683f, + +0.3539683f, 0.5617812f, -0.1572600f, 0.0543796f, 0.3820210f, -0.0145911f, 0.0050455f, 0.2472007f, +0.0924105f, -0.0319550f, 0.0674404f, 0.2350793f, -0.0812891f, -0.0673798f, 0.3420810f, -0.1182897f, + +-0.1308050f, 0.3438689f, -0.0590948f, -0.0911226f, 0.2395491f, 0.0666980f, -0.0382127f, 0.1004561f, +0.2344217f, 0.0146972f, -0.0386369f, 0.4021455f, 0.0543796f, -0.1429567f, 0.5279383f, 0.6755751f, + +-0.1416472f, 0.0300853f, -0.0082323f, 0.4201931f, 0.0643851f, -0.0136751f, 0.0037420f, 0.1137348f, +0.3116238f, -0.0661876f, 0.0181110f, -0.1416472f, 0.5176560f, -0.1099481f, 0.0300853f, -0.0879808f, + +0.3215297f, 0.0861782f, -0.0235811f, -0.0235811f, 0.0861782f, 0.3215297f, -0.0879808f, 0.0300853f, +-0.1099481f, 0.5176560f, -0.1416472f, 0.0181110f, -0.0661876f, 0.3116238f, 0.1137348f, 0.0037420f, + +-0.0136751f, 0.0643851f, 0.4201931f, -0.0082323f, 0.0300853f, -0.1416472f, 0.6755751f, 0.8069659f, +-0.2107186f, 0.0594210f, -0.0171096f, 0.0045659f, 0.3617290f, 0.2269277f, -0.0639919f, 0.0184257f, + +-0.0049171f, -0.0835080f, 0.6645741f, -0.1874048f, 0.0539610f, -0.0144002f, -0.1263000f, 0.4732779f, +0.1065327f, -0.0306749f, 0.0081860f, 0.0087699f, -0.0328629f, 0.6332502f, -0.1823370f, 0.0486588f, + +0.0402884f, -0.1509708f, 0.5632743f, 0.0167249f, -0.0044632f, 0.0068062f, -0.0255047f, 0.0951583f, +0.4646113f, -0.1239872f, -0.0144002f, 0.0539610f, -0.2013295f, 0.6602938f, -0.0823658f, -0.0049171f, + +0.0184257f, -0.0687466f, 0.2254662f, 0.3621190f, 0.0045659f, -0.0171096f, 0.0638362f, -0.2093614f, +0.8066038f, 0.8810968f, -0.2021351f, 0.0666954f, -0.0202770f, 0.0058238f, -0.0012029f, 0.2717788f, + +0.4620232f, -0.1524466f, 0.0463474f, -0.0133115f, 0.0027496f, -0.1685313f, 0.8159056f, -0.1420003f, +0.0431715f, -0.0123993f, 0.0025612f, -0.0173142f, 0.0838225f, 0.6720048f, -0.2043056f, 0.0586787f, + +-0.0121205f, 0.0449523f, -0.2176261f, 0.7577241f, -0.0067165f, 0.0019290f, -0.0003985f, -0.0039697f, +0.0192183f, -0.0669137f, 0.7472055f, -0.2146051f, 0.0443283f, -0.0121205f, 0.0586787f, -0.2043056f, + +0.5968578f, 0.1054055f, -0.0217723f, 0.0025612f, -0.0123993f, 0.0431715f, -0.1261211f, 0.8113450f, +-0.1675893f, 0.0027496f, -0.0133115f, 0.0463474f, -0.1353993f, 0.4571270f, 0.2727902f, -0.0012029f, + +0.0058238f, -0.0202770f, 0.0592372f, -0.1999931f, 0.8806543f, 0.9554276f, -0.2268213f, 0.0425861f, +-0.0101123f, 0.0019080f, -0.0005029f, 0.0001033f, 0.1426315f, 0.7258282f, -0.1362755f, 0.0323593f, + +-0.0061055f, 0.0016092f, -0.0003307f, -0.1426315f, 0.7287173f, 0.1362755f, -0.0323593f, 0.0061055f, +-0.0016092f, 0.0003307f, 0.0425861f, -0.2175763f, 0.9147495f, -0.2172119f, 0.0409829f, -0.0108020f, + +0.0022196f, 0.0063560f, -0.0324732f, 0.1365260f, 0.7274374f, -0.1372507f, 0.0361755f, -0.0074333f, +-0.0063560f, 0.0324732f, -0.1365260f, 0.7271081f, 0.1372507f, -0.0361755f, 0.0074333f, 0.0019080f, + +-0.0097479f, 0.0409829f, -0.2182660f, 0.9193873f, -0.2423252f, 0.0497929f, 0.0002505f, -0.0012798f, +0.0053808f, -0.0286570f, 0.1207099f, 0.8116162f, -0.1667705f, -0.0002756f, 0.0014078f, -0.0059189f, + +0.0315227f, -0.1327809f, 0.7072222f, 0.1834475f, 0.0001033f, -0.0005279f, 0.0022196f, -0.0118210f, +0.0497929f, -0.2652083f, 0.9312072f, 0.9899751f, -0.2765257f, 0.0928675f, -0.0243293f, 0.0065427f, + +-0.0019937f, 0.0006807f, -0.0001201f, 0.0400995f, 1.1061027f, -0.3714699f, 0.0973173f, -0.0261706f, +0.0079748f, -0.0027227f, 0.0004805f, -0.0687420f, 0.3895382f, 0.6368056f, -0.1668297f, 0.0448639f, + +-0.0136711f, 0.0046674f, -0.0008237f, 0.0562435f, -0.3187130f, 0.9335227f, 0.1364970f, -0.0367068f, +0.0111854f, -0.0038188f, 0.0006739f, -0.0204703f, 0.1159986f, -0.3397642f, 1.1149904f, -0.1461748f, + +0.0445429f, -0.0152074f, 0.0026837f, 0.0026837f, -0.0152074f, 0.0445429f, -0.1461748f, 1.1149904f, +-0.3397642f, 0.1159986f, -0.0204703f, 0.0006739f, -0.0038188f, 0.0111854f, -0.0367068f, 0.1364970f, + +0.9335227f, -0.3187130f, 0.0562435f, -0.0008237f, 0.0046674f, -0.0136711f, 0.0448639f, -0.1668297f, +0.6368056f, 0.3895382f, -0.0687420f, 0.0004805f, -0.0027227f, 0.0079748f, -0.0261706f, 0.0973173f, + +-0.3714699f, 1.1061027f, 0.0400995f, -0.0001201f, 0.0006807f, -0.0019937f, 0.0065427f, -0.0243293f, +0.0928675f, -0.2765257f, 0.9899751f, 0.9999861f, -0.1427280f, 0.0322982f, -0.0127173f, 0.0061198f, + +-0.0030193f, 0.0010721f, -0.0002462f, 0.0000265f, 0.0001113f, 1.1418240f, -0.2583854f, 0.1017386f, +-0.0489583f, 0.0241542f, -0.0085767f, 0.0019699f, -0.0002121f, -0.0005192f, 0.0048215f, 1.2057984f, + +-0.4747804f, 0.2284722f, -0.1127197f, 0.0400247f, -0.0091929f, 0.0009900f, 0.0013500f, -0.0125358f, +0.0649243f, 1.2344289f, -0.5940278f, 0.2930712f, -0.1040642f, 0.0239017f, -0.0025740f, -0.0021214f, + +0.0196992f, -0.1020238f, 0.3458974f, 0.9334723f, -0.4605404f, 0.1635294f, -0.0375598f, 0.0040449f, +0.0021214f, -0.0196992f, 0.1020238f, -0.3458974f, 0.8443055f, 0.4605404f, -0.1635294f, 0.0375598f, + +-0.0040449f, -0.0014850f, 0.0137894f, -0.0714167f, 0.2421282f, -0.5910138f, 1.2776217f, 0.1144706f, +-0.0262918f, 0.0028314f, 0.0007425f, -0.0068947f, 0.0357083f, -0.1210641f, 0.2955069f, -0.6388109f, + +1.2760980f, 0.0131459f, -0.0014157f, -0.0002121f, 0.0019699f, -0.0102024f, 0.0345897f, -0.0844306f, +0.1825174f, -0.3645994f, 1.1391011f, 0.0004045f, 0.0000265f, -0.0002462f, 0.0012753f, -0.0043237f, + +0.0105538f, -0.0228147f, 0.0455749f, -0.1423876f, 0.9999495f, 0.2845912f, -0.1179245f, 0.2594340f, +-0.0927673f, 0.2091195f, -0.0424528f, 0.1839623f, -0.0172956f, 0.1336478f, 0.0330189f, 0.1084906f, + +0.0581761f, 0.0581761f, 0.1084906f, 0.0330189f, 0.1336478f, -0.0172956f, 0.1839623f, -0.0424528f, +0.2091195f, -0.0927673f, 0.2594340f, -0.1179245f, 0.2845912f, 0.4784868f, -0.1190476f, 0.0453227f, + +0.3664491f, -0.0380952f, 0.0145033f, 0.2544114f, 0.0428571f, -0.0163162f, 0.1423737f, 0.1238095f, +-0.0471356f, 0.0303360f, 0.2047619f, -0.0779550f, -0.0817017f, 0.2857143f, -0.1087745f, -0.1087745f, + +0.2857143f, -0.0817017f, -0.0779550f, 0.2047619f, 0.0303360f, -0.0471356f, 0.1238095f, 0.1423737f, +-0.0163162f, 0.0428571f, 0.2544114f, 0.0145033f, -0.0380952f, 0.3664491f, 0.0453227f, -0.1190476f, + +0.4784868f, 0.6098023f, -0.1552631f, 0.0402324f, -0.0131765f, 0.4185360f, 0.0020702f, -0.0005364f, +0.0001757f, 0.1794530f, 0.1987367f, -0.0514975f, 0.0168660f, -0.0118133f, 0.3560700f, -0.0922663f, + +0.0302182f, -0.1430446f, 0.4367628f, -0.0855359f, 0.0280139f, -0.0819523f, 0.2502276f, 0.1009994f, +-0.0330784f, -0.0330784f, 0.1009994f, 0.2502276f, -0.0819523f, 0.0280139f, -0.0855359f, 0.4367628f, + +-0.1430446f, 0.0302182f, -0.0922663f, 0.3560700f, -0.0118133f, 0.0168660f, -0.0514975f, 0.1987367f, +0.1794530f, 0.0001757f, -0.0005364f, 0.0020702f, 0.4185360f, -0.0131765f, 0.0402324f, -0.1552631f, + +0.6098023f, 0.7383369f, -0.1727899f, 0.0494924f, -0.0124893f, 0.0036259f, 0.3966643f, 0.1151932f, +-0.0329949f, 0.0083262f, -0.0024173f, 0.0549918f, 0.4031764f, -0.1154822f, 0.0291417f, -0.0084605f, + +-0.1588972f, 0.5473127f, -0.1175973f, 0.0296755f, -0.0086155f, -0.0755414f, 0.2601981f, 0.1996616f, +-0.0503842f, 0.0146277f, 0.0078145f, -0.0269165f, 0.5169204f, -0.1304439f, 0.0378708f, 0.0378708f, + +-0.1304439f, 0.5169204f, -0.0269165f, 0.0078145f, 0.0146277f, -0.0503842f, 0.1996616f, 0.2601981f, +-0.0755414f, -0.0086155f, 0.0296755f, -0.1175973f, 0.5473127f, -0.1588972f, -0.0084605f, 0.0291417f, + +-0.1154822f, 0.4031764f, 0.0549918f, -0.0024173f, 0.0083262f, -0.0329949f, 0.1151932f, 0.3966643f, +0.0036259f, -0.0124893f, 0.0494924f, -0.1727899f, 0.7383369f, 0.7979735f, -0.1758341f, 0.0515405f, + +-0.0146323f, 0.0039688f, -0.0009159f, 0.3719327f, 0.2344454f, -0.0687206f, 0.0195097f, -0.0052917f, +0.0012212f, -0.1149711f, 0.7033362f, -0.2061619f, 0.0585292f, -0.0158751f, 0.0036635f, -0.0905686f, + +0.3924640f, 0.1691985f, -0.0480354f, 0.0130288f, -0.0030066f, 0.0089078f, -0.0386003f, 0.6271625f, +-0.1780511f, 0.0482933f, -0.0111446f, 0.0349973f, -0.1516549f, 0.5591316f, 0.0305291f, -0.0082805f, + +0.0019109f, 0.0019109f, -0.0082805f, 0.0305291f, 0.5591316f, -0.1516549f, 0.0349973f, -0.0111446f, +0.0482933f, -0.1780511f, 0.6271625f, -0.0386003f, 0.0089078f, -0.0030066f, 0.0130288f, -0.0480354f, + +0.1691985f, 0.3924640f, -0.0905686f, 0.0036635f, -0.0158751f, 0.0585292f, -0.2061619f, 0.7033362f, +-0.1149711f, 0.0012212f, -0.0052917f, 0.0195097f, -0.0687206f, 0.2344454f, 0.3719327f, -0.0009159f, + +0.0039688f, -0.0146323f, 0.0515405f, -0.1758341f, 0.7979735f, 0.8748942f, -0.1721068f, 0.0498959f, +-0.0156577f, 0.0051759f, -0.0015183f, 0.0003136f, 0.2859561f, 0.3933870f, -0.1140478f, 0.0357889f, + +-0.0118306f, 0.0034705f, -0.0007168f, -0.1582316f, 0.7660421f, -0.1602371f, 0.0502835f, -0.0166220f, +0.0048760f, -0.0010072f, -0.0333551f, 0.1614811f, 0.5716645f, -0.1793921f, 0.0593008f, -0.0173957f, + +0.0035932f, 0.0376046f, -0.1820540f, 0.7985786f, -0.0774646f, 0.0256071f, -0.0075117f, 0.0015516f, +0.0007307f, -0.0035377f, 0.0155179f, 0.6876691f, -0.2273195f, 0.0666833f, -0.0137739f, -0.0104493f, + +0.0505878f, -0.2219028f, 0.6858636f, 0.0164518f, -0.0048261f, 0.0009969f, 0.0012696f, -0.0061464f, +0.0269613f, -0.0833326f, 0.8178639f, -0.2399172f, 0.0495567f, 0.0026656f, -0.0129047f, 0.0566062f, + +-0.1749600f, 0.5587704f, 0.1561008f, -0.0322438f, -0.0006815f, 0.0032996f, -0.0144735f, 0.0447351f, +-0.1428707f, 0.7886099f, -0.1628932f, -0.0007168f, 0.0034705f, -0.0152231f, 0.0470521f, -0.1502703f, + +0.4469841f, 0.2748853f, 0.0003136f, -0.0015183f, 0.0066601f, -0.0205853f, 0.0657433f, -0.1955555f, +0.8797377f, 0.9262632f, -0.2411474f, 0.0556399f, -0.0149728f, 0.0047774f, -0.0011027f, 0.0002986f, + +-0.0000614f, 0.1966316f, 0.6430596f, -0.1483731f, 0.0399274f, -0.0127397f, 0.0029406f, -0.0007962f, +0.0001636f, -0.1669505f, 0.8124927f, 0.0469259f, -0.0126278f, 0.0040292f, -0.0009300f, 0.0002518f, + +-0.0000517f, 0.0370907f, -0.1805080f, 0.9206194f, -0.2477401f, 0.0790471f, -0.0182455f, 0.0049403f, +-0.0010151f, 0.0159203f, -0.0774786f, 0.2861435f, 0.5380877f, -0.1716891f, 0.0396290f, -0.0107303f, + +0.0022049f, -0.0112696f, 0.0548455f, -0.2025551f, 0.8775509f, -0.0836501f, 0.0193080f, -0.0052280f, +0.0010742f, 0.0010742f, -0.0052280f, 0.0193080f, -0.0836501f, 0.8775509f, -0.2025551f, 0.0548455f, + +-0.0112696f, 0.0022049f, -0.0107303f, 0.0396290f, -0.1716891f, 0.5380877f, 0.2861435f, -0.0774786f, +0.0159203f, -0.0010151f, 0.0049403f, -0.0182455f, 0.0790471f, -0.2477401f, 0.9206194f, -0.1805080f, + +0.0370907f, -0.0000517f, 0.0002518f, -0.0009300f, 0.0040292f, -0.0126278f, 0.0469259f, 0.8124927f, +-0.1669505f, 0.0001636f, -0.0007962f, 0.0029406f, -0.0127397f, 0.0399274f, -0.1483731f, 0.6430596f, + +0.1966316f, -0.0000614f, 0.0002986f, -0.0011027f, 0.0047774f, -0.0149728f, 0.0556399f, -0.2411474f, +0.9262632f, 0.9815653f, -0.2288699f, 0.0695333f, -0.0149811f, 0.0048350f, -0.0011356f, 0.0003056f, + +-0.0000889f, 0.0000157f, 0.0737389f, 0.9154796f, -0.2781330f, 0.0599244f, -0.0193400f, 0.0045422f, +-0.0012223f, 0.0003556f, -0.0000628f, -0.0983185f, 0.5571383f, 0.3708440f, -0.0798992f, 0.0257866f, + +-0.0060563f, 0.0016297f, -0.0004742f, 0.0000837f, 0.0536868f, -0.3042253f, 1.0456146f, -0.0337669f, +0.0108979f, -0.0025595f, 0.0006888f, -0.0002004f, 0.0000354f, -0.0096989f, 0.0549607f, -0.1888986f, + +0.9982641f, -0.3221796f, 0.0756679f, -0.0203621f, 0.0059244f, -0.0010455f, -0.0025960f, 0.0147109f, +-0.0505609f, 0.1878900f, 0.8000000f, -0.1878900f, 0.0505609f, -0.0147109f, 0.0025960f, 0.0025960f, + +-0.0147109f, 0.0505609f, -0.1878900f, 0.8000000f, 0.1878900f, -0.0505609f, 0.0147109f, -0.0025960f, +-0.0010455f, 0.0059244f, -0.0203621f, 0.0756679f, -0.3221796f, 0.9982641f, -0.1888986f, 0.0549607f, + +-0.0096989f, 0.0000354f, -0.0002004f, 0.0006888f, -0.0025595f, 0.0108979f, -0.0337669f, 1.0456146f, +-0.3042253f, 0.0536868f, 0.0000837f, -0.0004742f, 0.0016297f, -0.0060563f, 0.0257866f, -0.0798992f, + +0.3708440f, 0.5571383f, -0.0983185f, -0.0000628f, 0.0003556f, -0.0012223f, 0.0045422f, -0.0193400f, +0.0599244f, -0.2781330f, 0.9154796f, 0.0737389f, 0.0000157f, -0.0000889f, 0.0003056f, -0.0011356f, + +0.0048350f, -0.0149811f, 0.0695333f, -0.2288699f, 0.9815653f, 0.9974228f, -0.2132571f, 0.0803035f, +-0.0276114f, 0.0056224f, -0.0012128f, 0.0003452f, -0.0001335f, 0.0000402f, -0.0000059f, 0.0137454f, + +1.1373711f, -0.4282856f, 0.1472608f, -0.0299864f, 0.0064685f, -0.0018411f, 0.0007119f, -0.0002144f, +0.0000315f, -0.0297817f, 0.2023627f, 0.9279522f, -0.3190651f, 0.0649705f, -0.0140151f, 0.0039891f, + +-0.0015424f, 0.0004644f, -0.0000684f, 0.0330908f, -0.2248475f, 0.7467198f, 0.3545168f, -0.0721895f, +0.0155723f, -0.0044323f, 0.0017138f, -0.0005161f, 0.0000759f, -0.0193029f, 0.1311610f, -0.4355866f, + +1.1265318f, 0.0421105f, -0.0090838f, 0.0025855f, -0.0009997f, 0.0003010f, -0.0000443f, 0.0051952f, +-0.0353010f, 0.1172349f, -0.3031976f, 1.0652362f, -0.1495209f, 0.0425580f, -0.0164556f, 0.0049550f, + +-0.0007292f, -0.0003584f, 0.0024355f, -0.0080883f, 0.0209182f, -0.0734929f, 1.1395742f, -0.3243563f, +0.1254161f, -0.0377645f, 0.0055578f, -0.0000443f, 0.0003010f, -0.0009997f, 0.0025855f, -0.0090838f, + +0.0488706f, 1.1246078f, -0.4348426f, 0.1309370f, -0.0192700f, 0.0000759f, -0.0005161f, 0.0017138f, +-0.0044323f, 0.0155723f, -0.0837781f, 0.3578153f, 0.7454444f, -0.2244634f, 0.0330342f, -0.0000684f, + +0.0004644f, -0.0015424f, 0.0039891f, -0.0140151f, 0.0754003f, -0.3220338f, 0.9291000f, 0.2020171f, +-0.0297308f, 0.0000315f, -0.0002144f, 0.0007119f, -0.0018411f, 0.0064685f, -0.0348001f, 0.1486310f, + +-0.4288154f, 1.1375306f, 0.0137219f, -0.0000059f, 0.0000402f, -0.0001335f, 0.0003452f, -0.0012128f, +0.0065250f, -0.0278683f, 0.0804029f, -0.2132870f, 0.9974272f, 0.9999995f, -0.0666571f, 0.0153275f, + +-0.0049158f, 0.0024855f, -0.0011966f, 0.0005915f, -0.0002124f, 0.0000571f, -0.0000096f, 0.0000006f, +0.0000089f, 1.0665138f, -0.2452399f, 0.0786531f, -0.0397674f, 0.0191453f, -0.0094634f, 0.0033982f, + +-0.0009139f, 0.0001529f, -0.0000089f, -0.0000446f, 0.0007645f, 1.2261996f, -0.3932655f, 0.1988369f, +-0.0957265f, 0.0473169f, -0.0169909f, 0.0045696f, -0.0007645f, 0.0000446f, 0.0001450f, -0.0024845f, + +0.0148512f, 1.2781130f, -0.6462200f, 0.3111111f, -0.1537800f, 0.0552203f, -0.0148512f, 0.0024845f, +-0.0001450f, -0.0002900f, 0.0049690f, -0.0297024f, 0.1104406f, 1.2924401f, -0.6222222f, 0.3075600f, + +-0.1104406f, 0.0297024f, -0.0049690f, 0.0002900f, 0.0004143f, -0.0070986f, 0.0424320f, -0.1577723f, +0.4393714f, 0.8888889f, -0.4393714f, 0.1577723f, -0.0424320f, 0.0070986f, -0.0004143f, -0.0004143f, + +0.0070986f, -0.0424320f, 0.1577723f, -0.4393714f, 0.8888889f, 0.4393714f, -0.1577723f, 0.0424320f, +-0.0070986f, 0.0004143f, 0.0002900f, -0.0049690f, 0.0297024f, -0.1104406f, 0.3075600f, -0.6222222f, + +1.2924401f, 0.1104406f, -0.0297024f, 0.0049690f, -0.0002900f, -0.0001450f, 0.0024845f, -0.0148512f, +0.0552203f, -0.1537800f, 0.3111111f, -0.6462200f, 1.2781130f, 0.0148512f, -0.0024845f, 0.0001450f, + +0.0000446f, -0.0007645f, 0.0045696f, -0.0169909f, 0.0473169f, -0.0957265f, 0.1988369f, -0.3932655f, +1.2261996f, 0.0007645f, -0.0000446f, -0.0000089f, 0.0001529f, -0.0009139f, 0.0033982f, -0.0094634f, + +0.0191453f, -0.0397674f, 0.0786531f, -0.2452399f, 1.0665138f, 0.0000089f, 0.0000006f, -0.0000096f, +0.0000571f, -0.0002124f, 0.0005915f, -0.0011966f, 0.0024855f, -0.0049158f, 0.0153275f, -0.0666571f, + +0.9999995f diff --git a/encoder/basisu_bc15_spmd.cpp b/encoder/basisu_bc15_spmd.cpp new file mode 100644 index 0000000..4bac1c1 --- /dev/null +++ b/encoder/basisu_bc15_spmd.cpp @@ -0,0 +1,441 @@ +// basisu_bc15_spmd.cpp -- standalone opaque 4-color BC1 encoder (see basisu_bc15_spmd.h). +// +// This TU holds the public API, the solid-color omatch tables, and the scalar reference encoder. The SSE4.1 +// cppspmd kernel lives in basisu_bc15_spmd_kernels.inl, compiled by basisu_bc15_spmd_sse.cpp; we only declare + +// call its wrapper here (gated on g_cpu_supports_sse41), with the scalar path as the fallback. + +#include "basisu_bc15_spmd.h" +#include "basisu_enc.h" +#include + +namespace basisu +{ + extern bool g_cpu_supports_sse41; // set by detect_sse41() in basisu_encoder_init() + + namespace bc_spmd + { + // Baked-in benchmarked defaults (not exposed as parameters by design). + static const int kLsRounds = 2; // least-squares alternation rounds + static const int kAvgVar = 16; // try the avg-solid 2nd seed when (rng_r+rng_g+rng_b) < kAvgVar + +#if BASISU_SUPPORT_SSE + // Defined in basisu_bc15_spmd_kernels.inl (the SSE4.1 TU). Encodes up to 4 blocks at once, writing num_write + // of them directly at pOut + j*out_stride (no temp buffer). + void encode_bc1_blocks_4_sse41(const color_rgba* pBlocks, uint8_t* pOut, + int* om5lo, int* om5hi, int* om6lo, int* om6hi, int ls_rounds, int avgvar, + uint32_t out_stride, uint32_t num_write); + + // BC4: encodes up to 4 single-channel blocks at once, writing num_write of them at pOut + j*out_stride. + // do_ls!=0 adds the 1-D least-squares refit. src_stride = byte stride between texel values in pBlocks (1 or 4). + void encode_bc4_blocks_4_sse41(const uint8_t* pBlocks, uint8_t* pOut, uint32_t out_stride, uint32_t num_write, uint32_t do_ls, uint32_t src_stride); +#endif + + // ---- solid-color omatch tables (stb_dxt/basisu 3% span penalty), as int[256] for the kernel's gather ---- + static int s_om5_lo[256], s_om5_hi[256], s_om6_lo[256], s_om6_hi[256]; + + static void build_omatch_tables() + { + for (int bits = 5; bits <= 6; bits++) + { + const int levels = 1 << bits; + int* lo = (bits == 5) ? s_om5_lo : s_om6_lo; + int* hi = (bits == 5) ? s_om5_hi : s_om6_hi; + for (int target = 0; target < 256; target++) + { + int best = 0x7FFFFFFF, bc0 = 0, bc1 = 0; + for (int c0 = 0; c0 < levels; c0++) + { + const int e0 = (bits == 5) ? ((c0 << 3) | (c0 >> 2)) : ((c0 << 2) | (c0 >> 4)); + for (int c1 = 0; c1 < levels; c1++) // FULL cross product (matches encode_bc1's prepare_bc1_single_color_table) + { + const int e1 = (bits == 5) ? ((c1 << 3) | (c1 >> 2)) : ((c1 << 2) | (c1 >> 4)); + const int interp = (2 * e0 + e1) / 3; // selector-2 (2/3 toward c0), truncated + int err = (interp > target) ? (interp - target) : (target - interp); + err += ((e0 > e1 ? e0 - e1 : e1 - e0) * 3) / 100; // 3% linear span penalty (abs: e0 may be < e1 now) + if (err < best) { best = err; bc0 = c0; bc1 = c1; } + } + } + lo[target] = bc0; hi[target] = bc1; + } + } + } + + // Thread-safe one-time table init: C++11 guarantees a function-local static's initializer runs exactly once, + // race-free, even under concurrent first calls (no double-checked-locking bug). init() just forces it. + static inline void ensure_init() { static const bool s_built = (build_omatch_tables(), true); (void)s_built; } + void init() { ensure_init(); } + + // ---- scalar reference encoder (the oracle) -- identical algorithm/op-order to the SPMD kernel ---- + static inline void unpack565(uint32_t c, int& r, int& g, int& b) + { + r = (c >> 11) & 31; g = (c >> 5) & 63; b = c & 31; + r = (r << 3) | (r >> 2); g = (g << 2) | (g >> 4); b = (b << 3) | (b >> 2); + } + static inline uint32_t f_to_565(float r, float g, float b) + { + const float cr = (r < 0.0f) ? 0.0f : (r > 255.0f ? 255.0f : r); + const float cg = (g < 0.0f) ? 0.0f : (g > 255.0f ? 255.0f : g); + const float cb = (b < 0.0f) ? 0.0f : (b > 255.0f ? 255.0f : b); + return ((int)rintf(cr * (31.0f / 255.0f)) << 11) | ((int)rintf(cg * (63.0f / 255.0f)) << 5) | (int)rintf(cb * (31.0f / 255.0f)); + } + static inline int thresh_sel(const int* pr, const int* pg, const int* pb, int r, int g, int b) + { + const int ar = pr[1] - pr[0], ag = pg[1] - pg[0], ab = pb[1] - pb[0]; + const int dp0 = pr[0] * ar + pg[0] * ag + pb[0] * ab; + const int dp2 = pr[2] * ar + pg[2] * ag + pb[2] * ab; + const int dp3 = pr[3] * ar + pg[3] * ag + pb[3] * ab; + const int dp1 = pr[1] * ar + pg[1] * ag + pb[1] * ab; + const int t0 = dp0 + dp2, t1 = dp2 + dp3, t2 = dp3 + dp1; + const int d = r * (ar + ar) + g * (ag + ag) + b * (ab + ab); + return (d <= t0) ? 0 : (d < t1) ? 2 : (d < t2) ? 3 : 1; + } + static void ls_round(const color_rgba* blk, uint32_t& c0, uint32_t& c1) + { + int r0, g0, b0, r1, g1, b1; + unpack565(c0, r0, g0, b0); unpack565(c1, r1, g1, b1); + const int pr[4] = { r0, r1, (2 * r0 + r1) / 3, (r0 + 2 * r1) / 3 }; + const int pg[4] = { g0, g1, (2 * g0 + g1) / 3, (g0 + 2 * g1) / 3 }; + const int pb[4] = { b0, b1, (2 * b0 + b1) / 3, (b0 + 2 * b1) / 3 }; + float m00 = 0, m01 = 0, m11 = 0, a0r = 0, a0g = 0, a0b = 0, a1r = 0, a1g = 0, a1b = 0; + for (uint32_t i = 0; i < 16; i++) + { + const int sel = thresh_sel(pr, pg, pb, blk[i].r, blk[i].g, blk[i].b); + const float tb = (sel == 0) ? 0.0f : (sel == 1) ? 1.0f : (sel == 2) ? (1.0f / 3.0f) : (2.0f / 3.0f); + const float ta = 1.0f - tb; + m00 += ta * ta; m01 += ta * tb; m11 += tb * tb; + const float cr = (float)blk[i].r, cg = (float)blk[i].g, cb = (float)blk[i].b; + a0r += ta * cr; a0g += ta * cg; a0b += ta * cb; + a1r += tb * cr; a1g += tb * cg; a1b += tb * cb; + } + const float det = m00 * m11 - m01 * m01; + const float inv = 1.0f / basisu::maximum(det, 1e-6f); + const float e0r = (a0r * m11 - a1r * m01) * inv, e0g = (a0g * m11 - a1g * m01) * inv, e0b = (a0b * m11 - a1b * m01) * inv; + const float e1r = (a1r * m00 - a0r * m01) * inv, e1g = (a1g * m00 - a0g * m01) * inv, e1b = (a1b * m00 - a0b * m01) * inv; + uint32_t nc0 = f_to_565(e0r, e0g, e0b), nc1 = f_to_565(e1r, e1g, e1b); + { const uint32_t hi = basisu::maximum(nc0, nc1), lo = basisu::minimum(nc0, nc1); nc0 = hi; nc1 = lo; } + if (det > 1e-6f) { c0 = nc0; c1 = nc1; } + } + static int eval_error(const color_rgba* blk, uint32_t c0, uint32_t c1) + { + int r0, g0, b0, r1, g1, b1; + unpack565(c0, r0, g0, b0); unpack565(c1, r1, g1, b1); + const int pr[4] = { r0, r1, (2 * r0 + r1) / 3, (r0 + 2 * r1) / 3 }; + const int pg[4] = { g0, g1, (2 * g0 + g1) / 3, (g0 + 2 * g1) / 3 }; + const int pb[4] = { b0, b1, (2 * b0 + b1) / 3, (b0 + 2 * b1) / 3 }; + int err = 0; + for (uint32_t i = 0; i < 16; i++) + { + const int sel = thresh_sel(pr, pg, pb, blk[i].r, blk[i].g, blk[i].b); + const int dr = blk[i].r - pr[sel], dg = blk[i].g - pg[sel], db = blk[i].b - pb[sel]; + err += dr * dr + dg * dg + db * db; + } + return err; + } + static void encode_block_scalar(const color_rgba* blk, uint8_t out[8]) + { + int mnr = 255, mng = 255, mnb = 255, mxr = 0, mxg = 0, mxb = 0, sum_r = 0, sum_g = 0, sum_b = 0; + int sum_rr = 0, sum_gg = 0, sum_bb = 0, sum_rg = 0, sum_rb = 0, sum_gb = 0; + for (uint32_t i = 0; i < 16; i++) + { + const int r = blk[i].r, g = blk[i].g, b = blk[i].b; + mnr = basisu::minimum(mnr, r); mng = basisu::minimum(mng, g); mnb = basisu::minimum(mnb, b); + mxr = basisu::maximum(mxr, r); mxg = basisu::maximum(mxg, g); mxb = basisu::maximum(mxb, b); + sum_r += r; sum_g += g; sum_b += b; + sum_rr += r * r; sum_gg += g * g; sum_bb += b * b; + sum_rg += r * g; sum_rb += r * b; sum_gb += g * b; + } + + // Solid block: clone of basist::encode_bc1_solid_block -- omatch endpoints from the (full-cross-product) + // single-color tables, then the same swap + degenerate handling that GUARANTEES a 4-color block + // (color0 > color1), never 3-color (required so this is safe for BC3/BC5 color blocks). + if ((mnr == mxr) && (mng == mxg) && (mnb == mxb)) + { + const int ar = (sum_r + 8) >> 4, ag = (sum_g + 8) >> 4, ab = (sum_b + 8) >> 4; + uint32_t max16 = (s_om5_lo[ar] << 11) | (s_om6_lo[ag] << 5) | s_om5_lo[ab]; // lo[] = 2x-weighted endpoint (encode_bc1 m_hi) + uint32_t min16 = (s_om5_hi[ar] << 11) | (s_om6_hi[ag] << 5) | s_om5_hi[ab]; // hi[] = 1x endpoint (encode_bc1 m_lo) + uint32_t mask = 0xAA; + if (min16 == max16) + { + mask = 0; // selector 0 = color0 directly; force max16 > min16 so the block stays 4-color + if (min16 > 0) min16--; + else { max16 = 1; min16 = 0; mask = 0x55; } // l == h == 0 + } + if (max16 < min16) { const uint32_t t = max16; max16 = min16; min16 = t; mask ^= 0x55; } // selector 2<->3 on swap + out[0] = (uint8_t)max16; out[1] = (uint8_t)(max16 >> 8); out[2] = (uint8_t)min16; out[3] = (uint8_t)(min16 >> 8); + out[4] = out[5] = out[6] = out[7] = (uint8_t)mask; + return; + } + + // PCA seed (METHOD 2, on-axis, sqrt-free). + const float cxx = (float)(16 * sum_rr - sum_r * sum_r), cxy = (float)(16 * sum_rg - sum_r * sum_g), cxz = (float)(16 * sum_rb - sum_r * sum_b); + const float cyy = (float)(16 * sum_gg - sum_g * sum_g), cyz = (float)(16 * sum_gb - sum_g * sum_b), czz = (float)(16 * sum_bb - sum_b * sum_b); + float ax = (float)(mxr - mnr), ay = (float)(mxg - mng), az = (float)(mxb - mnb); + for (int it = 0; it < 4; it++) + { + const float nr = ax * cxx + ay * cxy + az * cxz; + const float ng = ax * cxy + ay * cyy + az * cyz; + const float nb = ax * cxz + ay * cyz + az * czz; + ax = nr; ay = ng; az = nb; + } + const float kk = basisu::maximum(basisu::maximum(fabsf(ax), fabsf(ay)), fabsf(az)); + const float mm = 1024.0f / basisu::maximum(kk, 1e-3f); + ax *= mm; ay *= mm; az *= mm; + const float inv_len2 = 1.0f / (ax * ax + ay * ay + az * az + 0.0000125f); + const float meanx = (float)sum_r * (1.0f / 16.0f), meany = (float)sum_g * (1.0f / 16.0f), meanz = (float)sum_b * (1.0f / 16.0f); + float minp = 1e30f, maxp = -1e30f; + for (uint32_t i = 0; i < 16; i++) + { + const float prj = ((float)blk[i].r - meanx) * ax + ((float)blk[i].g - meany) * ay + ((float)blk[i].b - meanz) * az; + minp = basisu::minimum(minp, prj); maxp = basisu::maximum(maxp, prj); + } + const float tlo = minp * inv_len2, thi = maxp * inv_len2; + uint32_t color0 = f_to_565(meanx + tlo * ax, meany + tlo * ay, meanz + tlo * az); + uint32_t color1 = f_to_565(meanx + thi * ax, meany + thi * ay, meanz + thi * az); + { const uint32_t hi = basisu::maximum(color0, color1), lo = basisu::minimum(color0, color1); color0 = hi; color1 = lo; } + if (color0 == color1) { if (color1 != 0) color1--; else color0++; } + + for (int r = 0; r < kLsRounds; r++) ls_round(blk, color0, color1); + if (color0 == color1) { if (color1 != 0) color1--; else color0++; } + + // Low-variance avg-solid 2nd seed (keep-better). + const int var_proxy = (mxr - mnr) + (mxg - mng) + (mxb - mnb); + if (var_proxy < kAvgVar) + { + const int ar = (sum_r + 8) >> 4, ag = (sum_g + 8) >> 4, ab = (sum_b + 8) >> 4; + uint32_t c0b = (s_om5_lo[ar] << 11) | (s_om6_lo[ag] << 5) | s_om5_lo[ab]; + uint32_t c1b = (s_om5_hi[ar] << 11) | (s_om6_hi[ag] << 5) | s_om5_hi[ab]; + { const uint32_t hi = basisu::maximum(c0b, c1b), lo = basisu::minimum(c0b, c1b); c0b = hi; c1b = lo; } + if (c0b == c1b) { if (c1b != 0) c1b--; else c0b++; } + ls_round(blk, c0b, c1b); + if (c0b == c1b) { if (c1b != 0) c1b--; else c0b++; } + if (eval_error(blk, c0b, c1b) < eval_error(blk, color0, color1)) { color0 = c0b; color1 = c1b; } + } + + int r0, g0, b0, r1, g1, b1; + unpack565(color0, r0, g0, b0); unpack565(color1, r1, g1, b1); + const int pr[4] = { r0, r1, (2 * r0 + r1) / 3, (r0 + 2 * r1) / 3 }; + const int pg[4] = { g0, g1, (2 * g0 + g1) / 3, (g0 + 2 * g1) / 3 }; + const int pb[4] = { b0, b1, (2 * b0 + b1) / 3, (b0 + 2 * b1) / 3 }; + uint32_t sw = 0; + for (uint32_t i = 0; i < 16; i++) + sw |= ((uint32_t)thresh_sel(pr, pg, pb, blk[i].r, blk[i].g, blk[i].b)) << (i * 2); + out[0] = (uint8_t)color0; out[1] = (uint8_t)(color0 >> 8); + out[2] = (uint8_t)color1; out[3] = (uint8_t)(color1 >> 8); + out[4] = (uint8_t)sw; out[5] = (uint8_t)(sw >> 8); out[6] = (uint8_t)(sw >> 16); out[7] = (uint8_t)(sw >> 24); + } + + // ---- public API ---- + void encode_bc1_scalar(void* pBlocks, const color_rgba* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride) + { + ensure_init(); + uint8_t* pOut = (uint8_t*)pBlocks; + const uint32_t out_stride = 8 * block_stride; + for (uint32_t b = 0; b < num_blocks; b++) + encode_block_scalar(pSrc_pixels + b * 16, pOut + b * out_stride); + } + + void encode_bc1_spmd(void* pBlocks, const color_rgba* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride) + { + ensure_init(); + uint8_t* pOut = (uint8_t*)pBlocks; + (void)pOut; + const uint32_t out_stride = 8 * block_stride; + (void)out_stride; +#if BASISU_SUPPORT_SSE + if (g_cpu_supports_sse41) + { + // Full groups of 4: the kernel writes all 4 blocks directly at the (possibly strided) output. + uint32_t base = 0; + for (; base + 4 <= num_blocks; base += 4) + encode_bc1_blocks_4_sse41(pSrc_pixels + base * 16, pOut + base * out_stride, s_om5_lo, s_om5_hi, s_om6_lo, s_om6_hi, kLsRounds, kAvgVar, out_stride, 4); + + // Final partial group: pad the input to 4, but write only the n valid blocks (still direct, no temp). + const uint32_t n = num_blocks - base; // 0..3 + if (n) + { + color_rgba grp[64]; + for (uint32_t gi = 0; gi < 4; gi++) + memcpy(grp + gi * 16, pSrc_pixels + (base + ((gi < n) ? gi : (n - 1))) * 16, 16 * sizeof(color_rgba)); + encode_bc1_blocks_4_sse41(grp, pOut + base * out_stride, s_om5_lo, s_om5_hi, s_om6_lo, s_om6_hi, kLsRounds, kAvgVar, out_stride, n); + } + return; + } +#endif + encode_bc1_scalar(pBlocks, pSrc_pixels, num_blocks, block_stride); // scalar fallback (no SSE4.1) + } + + // ================================ BC4 (RGTC1, single channel) ================================ + + // Scalar reference (oracle) for one BC4 block -- clone of basist::encode_bc4 (raw bbox min/max, exact + // nearest-of-8 integer-threshold selector), but the solid case is forced to 8-value mode (red0 > red1) so + // every block is 8-color. v = 16 contiguous channel bytes (texel i = y*4+x). out = 8 bytes. + // Compute the nearest-of-8 selectors + the resulting decoded SSE for an 8-value endpoint pair (r0 > r1). + // Optionally accumulates the 1-D least-squares moments (rank = interpolation weight, 0..7). + static int bc4_eval(const uint8_t* v, int r0, int r1, uint64_t& sel_out, int* pSw, int* pSww, int* pSv, int* pSwv, uint32_t src_stride) + { + const int delta = r0 - r1; + const int t0 = delta * 13, t1 = delta * 11, t2 = delta * 9, t3 = delta * 7, t4 = delta * 5, t5 = delta * 3, t6 = delta; + const int bias = 4 - r1 * 14; + uint64_t sel = 0; int sse = 0, Sw = 0, Sww = 0, Sv = 0, Swv = 0; + for (int i = 0; i < 16; i++) + { + const int val = v[i * src_stride]; + const int sv = val * 14 + bias; + const int rank = (sv >= t0) + (sv >= t1) + (sv >= t2) + (sv >= t3) + (sv >= t4) + (sv >= t5) + (sv >= t6); + const int idx = (rank == 7) ? 0 : (rank == 0) ? 1 : (8 - rank); + sel |= (uint64_t)idx << (i * 3); + const int recon = (rank * r0 + (7 - rank) * r1) / 7; // == decoded value for this selector (matches unpack_bc4) + const int d = val - recon; sse += d * d; + Sw += rank; Sww += rank * rank; Sv += val; Swv += rank * val; + } + sel_out = sel; + if (pSw) { *pSw = Sw; *pSww = Sww; *pSv = Sv; *pSwv = Swv; } + return sse; + } + + static void encode_bc4_block(const uint8_t* v, uint8_t out[8], bool do_ls, uint32_t src_stride) + { + int mn = v[0], mx = v[0]; + for (int i = 1; i < 16; i++) { const int x = v[i * src_stride]; mn = basisu::minimum(mn, x); mx = basisu::maximum(mx, x); } + + // Solid -> force an 8-value block (red0 > red1) that still reconstructs the exact value. + if (mn == mx) + { + int r0, r1, idx; + if (mx > 0) { r0 = mx; r1 = mx - 1; idx = 0; } // all selectors index 0 = red0 = value + else { r0 = 1; r1 = 0; idx = 1; } // value 0: all selectors index 1 = red1 = 0 + out[0] = (uint8_t)r0; out[1] = (uint8_t)r1; + uint64_t sel = 0; for (int i = 0; i < 16; i++) sel |= (uint64_t)idx << (i * 3); + for (int i = 0; i < 6; i++) out[2 + i] = (uint8_t)(sel >> (i * 8)); + return; + } + + // Candidate A: raw bbox endpoints (== basist::encode_bc4) + (for HQ) its LS moments. + int Sw, Sww, Sv, Swv; uint64_t sel; + int r0 = mx, r1 = mn; + int sse = bc4_eval(v, r0, r1, sel, &Sw, &Sww, &Sv, &Swv, src_stride); + + // Candidate B (HQ only): one 1-D least-squares endpoint refit (value ~= a + (rank/7)*b), adopted only if + // it lowers SSE -- monotonic, can never regress below bbox/encode_bc4. Always 8-value (require nr0 > nr1). + if (do_ls) + { + const int detI = 16 * Sww - Sw * Sw; + if (detI != 0) + { + const float b = 7.0f * (float)(16 * Swv - Sw * Sv) / (float)detI; // slope = red0 - red1 + const float bsw7 = b * (float)Sw * (1.0f / 7.0f); + const float a = ((float)Sv - bsw7) / 16.0f; // intercept = red1 + const int nr1 = basisu::clamp((int)rintf(a), 0, 255); + const int nr0 = basisu::clamp((int)rintf(a + b), 0, 255); + if (nr0 > nr1) + { + uint64_t selB; + const int sseB = bc4_eval(v, nr0, nr1, selB, nullptr, nullptr, nullptr, nullptr, src_stride); + if (sseB < sse) { r0 = nr0; r1 = nr1; sel = selB; sse = sseB; } + } + } + } + + out[0] = (uint8_t)r0; out[1] = (uint8_t)r1; + for (int i = 0; i < 6; i++) out[2 + i] = (uint8_t)(sel >> (i * 8)); + } + + void encode_bc4_scalar(void* pBlocks, const uint8_t* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride, bool high_quality, uint32_t src_stride) + { + uint8_t* pOut = (uint8_t*)pBlocks; + const uint32_t out_stride = 8 * block_stride; + const uint32_t in_block = 16 * src_stride; // bytes between consecutive blocks in the source + for (uint32_t b = 0; b < num_blocks; b++) + encode_bc4_block(pSrc_pixels + (size_t)b * in_block, pOut + b * out_stride, high_quality, src_stride); + } + + void encode_bc4_spmd(void* pBlocks, const uint8_t* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride, bool high_quality, uint32_t src_stride) + { + uint8_t* pOut = (uint8_t*)pBlocks; + (void)pOut; + const uint32_t out_stride = 8 * block_stride; + (void)out_stride; + const uint32_t in_block = 16 * src_stride; + (void)in_block; +#if BASISU_SUPPORT_SSE + if (g_cpu_supports_sse41) + { + const uint32_t do_ls = high_quality ? 1 : 0; + // Full groups read straight from the (possibly strided) source -- no copy. + uint32_t base = 0; + for (; base + 4 <= num_blocks; base += 4) + encode_bc4_blocks_4_sse41(pSrc_pixels + (size_t)base * in_block, pOut + base * out_stride, out_stride, 4, do_ls, src_stride); + + // Tail (<4): gather the channel into a tightly-packed 64-byte stack buffer (pad with last block), then + // encode with src_stride=1. Only the tail copies; the bulk above is zero-copy. + const uint32_t n = num_blocks - base; // 0..3 + if (n) + { + uint8_t grp[64]; + for (uint32_t gi = 0; gi < 4; gi++) + { + const uint8_t* sp = pSrc_pixels + (size_t)(base + ((gi < n) ? gi : (n - 1))) * in_block; + for (uint32_t t = 0; t < 16; t++) grp[gi * 16 + t] = sp[t * src_stride]; + } + encode_bc4_blocks_4_sse41(grp, pOut + base * out_stride, out_stride, n, do_ls, 1); + } + return; + } +#endif + encode_bc4_scalar(pBlocks, pSrc_pixels, num_blocks, block_stride, high_quality, src_stride); // scalar fallback + } + + // ================================ High-level RGBA format helpers ================================ + // NO allocations, NO channel-extraction copies: the BC4 path reads the requested channel straight out of the + // RGBA pixels via src_stride=4 (the channel pointer is &pPixels->r/g/b/a). encode_bc4_* handles the <4 tail + // without overrunning the (possibly strided) output. + + void encode_bc1(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd) + { + if (use_spmd) encode_bc1_spmd(pBlocks, pPixels, num_blocks, 1); + else encode_bc1_scalar(pBlocks, pPixels, num_blocks, 1); + } + + void encode_bc4(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd, bool high_quality) + { + const uint8_t* pR = &pPixels[0].r; // R channel, stride 4, contiguous output + if (use_spmd) encode_bc4_spmd(pBlocks, pR, num_blocks, 1, high_quality, 4); + else encode_bc4_scalar(pBlocks, pR, num_blocks, 1, high_quality, 4); + } + + void encode_bc5(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd, bool high_quality) + { + uint8_t* pOut = (uint8_t*)pBlocks; // [0..7] = BC4 of R, [8..15] = BC4 of G (each block_stride 2) + const uint8_t* pR = &pPixels[0].r, * pG = &pPixels[0].g; + if (use_spmd) { encode_bc4_spmd(pOut, pR, num_blocks, 2, high_quality, 4); encode_bc4_spmd(pOut + 8, pG, num_blocks, 2, high_quality, 4); } + else { encode_bc4_scalar(pOut, pR, num_blocks, 2, high_quality, 4); encode_bc4_scalar(pOut + 8, pG, num_blocks, 2, high_quality, 4); } + } + + void encode_bc3(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd, bool high_quality) + { + uint8_t* pOut = (uint8_t*)pBlocks; // [0..7] = BC4 alpha block, [8..15] = BC1 color block + const uint8_t* pA = &pPixels[0].a; // A channel, stride 4 + if (use_spmd) { encode_bc4_spmd(pOut, pA, num_blocks, 2, high_quality, 4); encode_bc1_spmd(pOut + 8, pPixels, num_blocks, 2); } + else { encode_bc4_scalar(pOut, pA, num_blocks, 2, high_quality, 4); encode_bc1_scalar(pOut + 8, pPixels, num_blocks, 2); } + } + + void encode_bc2(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd) + { + uint8_t* pOut = (uint8_t*)pBlocks; // [0..7] = explicit 4-bit alpha (scalar), [8..15] = BC1 color block + for (uint32_t b = 0; b < num_blocks; b++) + { + uint8_t* o = pOut + b * 16; + const color_rgba* p = pPixels + b * 16; + for (uint32_t y = 0; y < 4; y++) + { + uint32_t row = 0; // one little-endian 16-bit word per row; texel x in nibble x + for (uint32_t x = 0; x < 4; x++) { const int a8 = p[y * 4 + x].a; const int a4 = (a8 * 2 + 17) / 34; row |= (uint32_t)a4 << (x * 4); } // round(a8/17) + o[y * 2] = (uint8_t)row; o[y * 2 + 1] = (uint8_t)(row >> 8); + } + } + if (use_spmd) encode_bc1_spmd(pOut + 8, pPixels, num_blocks, 2); + else encode_bc1_scalar(pOut + 8, pPixels, num_blocks, 2); + } + + } // namespace bc_spmd +} // namespace basisu diff --git a/encoder/basisu_bc15_spmd.h b/encoder/basisu_bc15_spmd.h new file mode 100644 index 0000000..4507610 --- /dev/null +++ b/encoder/basisu_bc15_spmd.h @@ -0,0 +1,73 @@ +// basisu_bc15_spmd.h +// Standalone opaque 4-color BC1 (DXT1) encoder for the Basis Universal encoder library. +// +// Two entry points share the SAME algorithm (omatch-solid fast path -> PCA endpoint seed -> 2x integer-threshold +// least-squares -> low-variance avg-solid 2nd seed (keep-better) -> integer-threshold selectors): +// - encode_bc1_scalar : portable scalar reference ("oracle"), no SIMD. +// - encode_bc1_spmd : SSE4.1 cppspmd kernel, 4 blocks per vector; falls back to the scalar path when SSE4.1 +// is unavailable (e.g. WASM without SIMD). +// Benchmarked (vs basist::encode_bc1): ~3.4-4.7x faster on photos, ~+0.06 dB average, and it beats encode_bc1 +// on low-dynamic-range / gradient blocks. Opaque 4-color only (no 3-color / punchthrough). +#pragma once + +#include "basisu_enc.h" // basisu::color_rgba + +namespace basisu +{ + namespace bc_spmd + { + // Build the solid-color omatch tables. Call once before encoding (idempotent). Not safe to call + // concurrently with itself or the encoders on first use -- call it once at startup (e.g. right after + // basisu_encoder_init(), which also performs the CPU-feature detection encode_bc1_spmd dispatches on). + void init(); + + // Encode num_blocks opaque BC1 blocks. + // pSrc_pixels : num_blocks * 16 color_rgba, block-contiguous; texel index within a block = y*4 + x. + // pBlocks : output; BC1 block b is written at (uint8_t*)pBlocks + b * 8 * block_stride. + // num_blocks : any count; counts not divisible by 4 are handled internally (no caller padding needed). + // block_stride : output spacing in units of 8-byte BC1 blocks. 1 = contiguous BC1. 2 = write into every + // other 8-byte slot, e.g. the color half of 16-byte BC3/BC5 blocks (pass pBlocks already + // offset to that half). Input layout is unaffected. + // The caller is responsible for extracting the 16 pixels per block from its source image. + void encode_bc1_scalar(void* pBlocks, const color_rgba* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride = 1); + void encode_bc1_spmd(void* pBlocks, const color_rgba* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride = 1); + + // Encode num_blocks single-channel BC4 (RGTC1) blocks. Clone of basist::encode_bc4's algorithm: raw bbox + // min/max endpoints + exact nearest-of-8 integer-threshold selector, ALWAYS 8-value mode (red0 > red1) -- + // including a forced-8-value solid-block path so we NEVER emit a 6-value block (safe for BC3/BC5). Quality + // matches encode_bc4 (bit-exact on non-solid blocks; identical decode on solid). (Least-squares endpoint + // refinement may be layered on later.) + // pSrc_pixels : num_blocks * 16 uint8_t, block-contiguous; texel index within a block = y*4 + x. The + // caller extracts the 16 single-channel values per block itself (e.g. one channel of RGBA). + // pBlocks : output; BC4 block b is written at (uint8_t*)pBlocks + b * 8 * block_stride. + // num_blocks : any count; the <4 tail is handled internally. + // block_stride : output spacing in units of 8-byte BC4 blocks. 1 = contiguous BC4. 2 = write into every + // other 8-byte slot, e.g. the alpha half of a 16-byte BC3 block or one half of a BC5 pair. + // high_quality : false (default) = fast bbox encoder, matches encode_bc4 quality, ~1.4x faster. true = add a + // 1-D least-squares endpoint refit (keep-best, monotonic): ~+0.8 dB on photos but ~0.6x speed. + // src_stride : BYTE stride between consecutive texel values in pSrc_pixels. 1 (default) = tightly packed + // single channel. 4 = one channel of color_rgba (point pSrc_pixels at the desired channel + // byte, e.g. &rgba[0].g) -- lets callers encode a channel in place with no extraction copy. + void encode_bc4_scalar(void* pBlocks, const uint8_t* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride = 1, bool high_quality = false, uint32_t src_stride = 1); + void encode_bc4_spmd(void* pBlocks, const uint8_t* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride = 1, bool high_quality = false, uint32_t src_stride = 1); + + // ---------------- High-level format helpers (RGBA in -> complete GPU blocks out) ---------------- + // Convenience wrappers over the low-level encoders. Each takes pPixels = num_blocks * 16 color_rgba, + // block-contiguous (texel index = y*4 + x), and writes complete blocks. num_blocks may be ANY count >= 1 + // (not necessarily a multiple of 4); the <4 tail is handled internally with NO output overrun. + // use_spmd : true = SSE4.1 SPMD path (auto-falls back to scalar when SSE4.1 is unavailable); false = scalar. + // high_quality : enables the BC4 channel least-squares refit for the alpha/red/green block(s) (BC3/BC4/BC5). + // Output block sizes / layout (matches the transcoder's bcu unpackers): + // BC1: 8 bytes/block -- RGB, opaque 4-color. + // BC2: 16 bytes/block -- explicit 4-bit alpha [0..7] (scalar) + BC1 color [8..15]. + // BC3: 16 bytes/block -- BC4 alpha block [0..7] + BC1 color [8..15]. + // BC4: 8 bytes/block -- single channel = R. + // BC5: 16 bytes/block -- BC4 of R [0..7] + BC4 of G [8..15]. + void encode_bc1(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd = true); + void encode_bc2(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd = true); + void encode_bc3(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd = true, bool high_quality = false); + void encode_bc4(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd = true, bool high_quality = false); + void encode_bc5(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd = true, bool high_quality = false); + + } // namespace bc_spmd +} // namespace basisu diff --git a/encoder/basisu_bc15_spmd_kernels.inl b/encoder/basisu_bc15_spmd_kernels.inl new file mode 100644 index 0000000..e6f80d2 --- /dev/null +++ b/encoder/basisu_bc15_spmd_kernels.inl @@ -0,0 +1,428 @@ +// basisu_bc15_spmd_kernels.inl -- Do NOT directly include. +// +// SSE4.1 cppspmd kernel for the standalone BC1 encoder. Included by basisu_bc15_spmd_sse.cpp from file scope after +// cppspmd_sse.h + "using namespace CPPSPMD;". LANES = BLOCKS: 4 independent BC1 blocks per vector, one per lane; +// each lane runs the same scalar-looking program on its block's 16 texels. Mirrors encoder/basisu_kernels_imp.h. + +namespace bc_spmd_kern +{ + // Opaque 4-color BC1 encode of 4 blocks at once (one block per lane). pBlocks = 4 blocks x 16 texels, + // block-contiguous; pOut = 4*8 bytes. om5*/om6* = solid-color omatch tables (int[256]); ls_rounds, avgvar tune. + struct encode_bc1_4color_blocks : spmd_kernel + { + inline vint div3(const vint& x) { return VUINT_SHIFT_RIGHT(x * vint(43691), 17); } // exact floor(x/3), x small + inline vint expand5(const vint& c) { return VINT_SHIFT_LEFT(c, 3) | VUINT_SHIFT_RIGHT(c, 2); } + inline vint expand6(const vint& c) { return VINT_SHIFT_LEFT(c, 2) | VUINT_SHIFT_RIGHT(c, 4); } + + // Round per-lane float RGB (0..255) STRAIGHT into 5/6/5-bit space (single round, like encode_bc1's + // *31/255+0.5) -- avoids the double-rounding (float->8bit->565) that biases endpoints inward. + inline vint f_to_565(const vfloat& r, const vfloat& g, const vfloat& b) + { + const vint r5 = vint(round_nearest(min(max(r, vfloat(0.0f)), vfloat(255.0f)) * vfloat(31.0f / 255.0f))); + const vint g6 = vint(round_nearest(min(max(g, vfloat(0.0f)), vfloat(255.0f)) * vfloat(63.0f / 255.0f))); + const vint b5 = vint(round_nearest(min(max(b, vfloat(0.0f)), vfloat(255.0f)) * vfloat(31.0f / 255.0f))); + return VINT_SHIFT_LEFT(r5, 11) | VINT_SHIFT_LEFT(g6, 5) | b5; + } + + // Load texel i of all 4 blocks into per-channel vints (lane j = block j's texel i). + inline void load_texel(const int32_t* p, int i, vint& r, vint& g, vint& b) + { + vint rgba; rgba.m_value = _mm_setr_epi32(p[i], p[16 + i], p[32 + i], p[48 + i]); + r = rgba & vint(0xFF); + g = VINT_SHIFT_RIGHT(rgba, 8) & vint(0xFF); + b = VINT_SHIFT_RIGHT(rgba, 16) & vint(0xFF); + // Opaque 4-color encoder: alpha is ignored entirely. + } + + // Integer-threshold selector (encode_bc1's bc1_find_sels scheme): 1 dot + 3 compares/texel. The asymmetric + // (d<=t0) biases extreme texels onto the OUTER selectors so the LS step reaches wider endpoints. + // Palette order is [pal0,pal2,pal3,pal1] along the c0->c1 axis; lut4={1,3,2,0} via the ternary cascade. + struct thresholds { vint t0, t1, t2, ar2, ag2, ab2; }; + inline thresholds make_thresholds( + const vint& pr0, const vint& pg0, const vint& pb0, const vint& pr1, const vint& pg1, const vint& pb1, + const vint& pr2, const vint& pg2, const vint& pb2, const vint& pr3, const vint& pg3, const vint& pb3) + { + const vint ar = pr1 - pr0, ag = pg1 - pg0, ab = pb1 - pb0; + const vint dp0 = pr0 * ar + pg0 * ag + pb0 * ab; + const vint dp2 = pr2 * ar + pg2 * ag + pb2 * ab; + const vint dp3 = pr3 * ar + pg3 * ag + pb3 * ab; + const vint dp1 = pr1 * ar + pg1 * ag + pb1 * ab; + thresholds T; + T.t0 = dp0 + dp2; T.t1 = dp2 + dp3; T.t2 = dp3 + dp1; + T.ar2 = ar + ar; T.ag2 = ag + ag; T.ab2 = ab + ab; + return T; + } + inline vint thresh_sel(const thresholds& T, const vint& r, const vint& g, const vint& b) + { + const vint d = r * T.ar2 + g * T.ag2 + b * T.ab2; + return spmd_ternaryi(d <= T.t0, 0, spmd_ternaryi(d < T.t1, 2, spmd_ternaryi(d < T.t2, 3, 1))); + } + + // One least-squares round (per lane): assign integer-threshold selectors for the current endpoints, refit + // the endpoint line via the 2x2 normal equations (Cramer), adopt only where well-conditioned (det). + inline void ls_round(const vint* R, const vint* G, const vint* B, vint& c0, vint& c1) + { + const vint pr0 = expand5(VINT_SHIFT_RIGHT(c0, 11) & vint(31)), pg0 = expand6(VINT_SHIFT_RIGHT(c0, 5) & vint(63)), pb0 = expand5(c0 & vint(31)); + const vint pr1 = expand5(VINT_SHIFT_RIGHT(c1, 11) & vint(31)), pg1 = expand6(VINT_SHIFT_RIGHT(c1, 5) & vint(63)), pb1 = expand5(c1 & vint(31)); + const vint pr2 = div3(vint(2) * pr0 + pr1), pg2 = div3(vint(2) * pg0 + pg1), pb2 = div3(vint(2) * pb0 + pb1); + const vint pr3 = div3(pr0 + vint(2) * pr1), pg3 = div3(pg0 + vint(2) * pg1), pb3 = div3(pb0 + vint(2) * pb1); + + const thresholds T = make_thresholds(pr0, pg0, pb0, pr1, pg1, pb1, pr2, pg2, pb2, pr3, pg3, pb3); + vfloat m00(0.0f), m01(0.0f), m11(0.0f); + vfloat a0r(0.0f), a0g(0.0f), a0b(0.0f), a1r(0.0f), a1g(0.0f), a1b(0.0f); + for (int i = 0; i < 16; i++) + { + const vint& r = R[i]; const vint& g = G[i]; const vint& b = B[i]; + const vint sel = thresh_sel(T, r, g, b); + // fraction toward c1 (the LS weight b): {0, 1, 1/3, 2/3}[sel]; a = 1 - b. + const vfloat tb = spmd_ternaryf(sel == vint(0), vfloat(0.0f), spmd_ternaryf(sel == vint(1), vfloat(1.0f), spmd_ternaryf(sel == vint(2), vfloat(1.0f / 3.0f), vfloat(2.0f / 3.0f)))); + const vfloat ta = vfloat(1.0f) - tb; + m00 = m00 + ta * ta; m01 = m01 + ta * tb; m11 = m11 + tb * tb; + const vfloat cr = (vfloat)r, cg = (vfloat)g, cb = (vfloat)b; + a0r = a0r + ta * cr; a0g = a0g + ta * cg; a0b = a0b + ta * cb; + a1r = a1r + tb * cr; a1g = a1g + tb * cg; a1b = a1b + tb * cb; + } + const vfloat det = m00 * m11 - m01 * m01; + const vfloat inv = vfloat(1.0f) / max(det, vfloat(1e-6f)); // clamp not bias: exact 1/det for real blocks, never /0 + const vfloat e0r = (a0r * m11 - a1r * m01) * inv, e0g = (a0g * m11 - a1g * m01) * inv, e0b = (a0b * m11 - a1b * m01) * inv; + const vfloat e1r = (a1r * m00 - a0r * m01) * inv, e1g = (a1g * m00 - a0g * m01) * inv, e1b = (a1b * m00 - a0b * m01) * inv; + vint nc0 = f_to_565(e0r, e0g, e0b), nc1 = f_to_565(e1r, e1g, e1b); + { const vint hi = max(nc0, nc1), lo = min(nc0, nc1); nc0 = hi; nc1 = lo; } + const vbool ok = det > vfloat(1e-6f); // only adopt a well-conditioned refit; degenerate lane keeps current + c0 = spmd_ternaryi(ok, nc0, c0); + c1 = spmd_ternaryi(ok, nc1, c1); + } + + // Block RGB SSE for an endpoint pair (integer-threshold selectors + squared distance). For keep-better. + inline vint eval_error(const vint* R, const vint* G, const vint* B, const vint& c0, const vint& c1) + { + const vint pr0 = expand5(VINT_SHIFT_RIGHT(c0, 11) & vint(31)), pg0 = expand6(VINT_SHIFT_RIGHT(c0, 5) & vint(63)), pb0 = expand5(c0 & vint(31)); + const vint pr1 = expand5(VINT_SHIFT_RIGHT(c1, 11) & vint(31)), pg1 = expand6(VINT_SHIFT_RIGHT(c1, 5) & vint(63)), pb1 = expand5(c1 & vint(31)); + const vint pr2 = div3(vint(2) * pr0 + pr1), pg2 = div3(vint(2) * pg0 + pg1), pb2 = div3(vint(2) * pb0 + pb1); + const vint pr3 = div3(pr0 + vint(2) * pr1), pg3 = div3(pg0 + vint(2) * pg1), pb3 = div3(pb0 + vint(2) * pb1); + const thresholds T = make_thresholds(pr0, pg0, pb0, pr1, pg1, pb1, pr2, pg2, pb2, pr3, pg3, pb3); + vint err(0); + for (int i = 0; i < 16; i++) + { + const vint& r = R[i]; const vint& g = G[i]; const vint& b = B[i]; + const vint sel = thresh_sel(T, r, g, b); + const vint psr = spmd_ternaryi(sel == vint(0), pr0, spmd_ternaryi(sel == vint(1), pr1, spmd_ternaryi(sel == vint(2), pr2, pr3))); + const vint psg = spmd_ternaryi(sel == vint(0), pg0, spmd_ternaryi(sel == vint(1), pg1, spmd_ternaryi(sel == vint(2), pg2, pg3))); + const vint psb = spmd_ternaryi(sel == vint(0), pb0, spmd_ternaryi(sel == vint(1), pb1, spmd_ternaryi(sel == vint(2), pb2, pb3))); + const vint dr = r - psr, dg = g - psg, db = b - psb; + err = err + dr * dr + dg * dg + db * db; + } + return err; + } + + void _call(const basisu::color_rgba* pBlocks, uint8_t* pOut, + int* om5lo, int* om5hi, int* om6lo, int* om6hi, int ls_rounds, int avgvar, + uint32_t out_stride, uint32_t num_write) + { + const int32_t* p = (const int32_t*)pBlocks; + + // Stage source pixels to SoA ONCE (lane = block): R[i]/G[i]/B[i] hold texel i of all 4 blocks. + vint R[16], G[16], B[16]; + for (int i = 0; i < 16; i++) load_texel(p, i, R[i], G[i], B[i]); + + // Pass 1: per-lane bounding box + channel sums + integer moments for covariance. + vint mnr(255), mng(255), mnb(255), mxr(0), mxg(0), mxb(0), sum_r(0), sum_g(0), sum_b(0); + vint sum_rr(0), sum_gg(0), sum_bb(0), sum_rg(0), sum_rb(0), sum_gb(0); + for (int i = 0; i < 16; i++) + { + const vint& r = R[i]; const vint& g = G[i]; const vint& b = B[i]; + mnr = min(mnr, r); mng = min(mng, g); mnb = min(mnb, b); + mxr = max(mxr, r); mxg = max(mxg, g); mxb = max(mxb, b); + sum_r = sum_r + r; sum_g = sum_g + g; sum_b = sum_b + b; + sum_rr = sum_rr + r * r; sum_gg = sum_gg + g * g; sum_bb = sum_bb + b * b; + sum_rg = sum_rg + r * g; sum_rb = sum_rb + r * b; sum_gb = sum_gb + g * b; + } + + vint color0(0), color1(0), sel_word(0); + + // SOLID lanes -> omatch endpoints; all-solid groups skip the non-solid path via SPMD_SELSE's any() check. + const vbool solid = (mnr == mxr) && (mng == mxg) && (mnb == mxb); + SPMD_SIF(solid) + { + // Clone of basist::encode_bc1_solid_block: omatch endpoints from the (full-cross-product) single-color + // tables, then the swap + degenerate handling that GUARANTEES 4-color (color0 > color1), never 3-color. + const vint ar = VUINT_SHIFT_RIGHT(sum_r + vint(8), 4), ag = VUINT_SHIFT_RIGHT(sum_g + vint(8), 4), ab = VUINT_SHIFT_RIGHT(sum_b + vint(8), 4); + vint max16 = VINT_SHIFT_LEFT(load_all(ar[om5lo]), 11) | VINT_SHIFT_LEFT(load_all(ag[om6lo]), 5) | load_all(ab[om5lo]); // 2x-weighted (m_hi) + vint min16 = VINT_SHIFT_LEFT(load_all(ar[om5hi]), 11) | VINT_SHIFT_LEFT(load_all(ag[om6hi]), 5) | load_all(ab[om5hi]); // 1x (m_lo) + vint mask = vint(0xAA); + SPMD_SIF(min16 == max16) // force max16 > min16 (stay 4-color, never 3-color) + { + store(mask, vint(0)); + SPMD_SIF(min16 > vint(0)) { store(min16, min16 - vint(1)); } + SPMD_SELSE(min16 > vint(0)) { store(max16, vint(1)); store(min16, vint(0)); store(mask, vint(0x55)); } + SPMD_SENDIF + } + SPMD_SENDIF + SPMD_SIF(max16 < min16) // ensure color0 > color1; selector 2<->3 flips with the swap + { + const vint a = max16, b = min16; + store(max16, b); store(min16, a); + store(mask, mask ^ vint(0x55)); + } + SPMD_SENDIF + store(color0, max16); store(color1, min16); + store(sel_word, mask | VINT_SHIFT_LEFT(mask, 8) | VINT_SHIFT_LEFT(mask, 16) | VINT_SHIFT_LEFT(mask, 24)); + } + SPMD_SELSE(solid) + { + // PCA seed (METHOD 2, on-axis, sqrt-free): integer-moment covariance -> no-renorm power iteration + // (bbox-diagonal seed, can't overflow at 4 iters) -> scale 1024/max -> endpoints = mean +- extent. + const vfloat cxx = (vfloat)(vint(16) * sum_rr - sum_r * sum_r), cxy = (vfloat)(vint(16) * sum_rg - sum_r * sum_g), cxz = (vfloat)(vint(16) * sum_rb - sum_r * sum_b); + const vfloat cyy = (vfloat)(vint(16) * sum_gg - sum_g * sum_g), cyz = (vfloat)(vint(16) * sum_gb - sum_g * sum_b), czz = (vfloat)(vint(16) * sum_bb - sum_b * sum_b); + vfloat ax = (vfloat)(mxr - mnr), ay = (vfloat)(mxg - mng), az = (vfloat)(mxb - mnb); + // Overflow-safe: covariance entries <= 4.16e6, 4 iters -> |v| <= ~6.2e30 << float max. Do not exceed 5 iters. + for (int it = 0; it < 4; it++) + { + const vfloat nr = ax * cxx + ay * cxy + az * cxz; + const vfloat ng = ax * cxy + ay * cyy + az * cyz; + const vfloat nb = ax * cxz + ay * cyz + az * czz; + ax = nr; ay = ng; az = nb; + } + const vfloat kk = max(max(abs(ax), abs(ay)), abs(az)); + const vfloat mm = vfloat(1024.0f) / max(kk, vfloat(1e-3f)); // clamp guards the near-degenerate lane + ax = ax * mm; ay = ay * mm; az = az * mm; + const vfloat inv_len2 = vfloat(1.0f) / (ax * ax + ay * ay + az * az + vfloat(0.0000125f)); // tiny bias: never /0 + const vfloat meanx = (vfloat)sum_r * vfloat(1.0f / 16.0f), meany = (vfloat)sum_g * vfloat(1.0f / 16.0f), meanz = (vfloat)sum_b * vfloat(1.0f / 16.0f); + vfloat minp(1e30f), maxp(-1e30f); + for (int i = 0; i < 16; i++) + { + const vfloat pr = ((vfloat)R[i] - meanx) * ax + ((vfloat)G[i] - meany) * ay + ((vfloat)B[i] - meanz) * az; + minp = min(minp, pr); maxp = max(maxp, pr); + } + const vfloat tlo = minp * inv_len2, thi = maxp * inv_len2; + vint c0 = f_to_565(meanx + tlo * ax, meany + tlo * ay, meanz + tlo * az); + vint c1 = f_to_565(meanx + thi * ax, meany + thi * ay, meanz + thi * az); + { const vint hi = max(c0, c1), lo = min(c0, c1); c0 = hi; c1 = lo; } + SPMD_SIF(c0 == c1) // 4-color needs color0 > color1; nudge the degenerate lanes + { + SPMD_SIF(c1 != vint(0)) { store(c1, c1 - vint(1)); } + SPMD_SELSE(c1 != vint(0)) { store(c0, c0 + vint(1)); } + SPMD_SENDIF + } + SPMD_SENDIF + + // Least-squares refinement (the big quality lever). + for (int r = 0; r < ls_rounds; r++) ls_round(R, G, B, c0, c1); + SPMD_SIF(c0 == c1) + { + SPMD_SIF(c1 != vint(0)) { store(c1, c1 - vint(1)); } + SPMD_SELSE(c1 != vint(0)) { store(c0, c0 + vint(1)); } + SPMD_SENDIF + } + SPMD_SENDIF + + // Low-variance blocks (ramps) collapse the PCA seed: try an omatch-solid-of-average 2nd seed (which + // survives the collapse), 1 LS round, and keep whichever has lower error. Gated to where it's needed. + const vint var_proxy = (mxr - mnr) + (mxg - mng) + (mxb - mnb); + SPMD_SIF(var_proxy < vint(avgvar)) + { + const vint ar = VUINT_SHIFT_RIGHT(sum_r + vint(8), 4), ag = VUINT_SHIFT_RIGHT(sum_g + vint(8), 4), ab = VUINT_SHIFT_RIGHT(sum_b + vint(8), 4); + vint c0b = VINT_SHIFT_LEFT(load_all(ar[om5lo]), 11) | VINT_SHIFT_LEFT(load_all(ag[om6lo]), 5) | load_all(ab[om5lo]); + vint c1b = VINT_SHIFT_LEFT(load_all(ar[om5hi]), 11) | VINT_SHIFT_LEFT(load_all(ag[om6hi]), 5) | load_all(ab[om5hi]); + { const vint hi = max(c0b, c1b), lo = min(c0b, c1b); c0b = hi; c1b = lo; } + SPMD_SIF(c0b == c1b) + { + SPMD_SIF(c1b != vint(0)) { store(c1b, c1b - vint(1)); } + SPMD_SELSE(c1b != vint(0)) { store(c0b, c0b + vint(1)); } + SPMD_SENDIF + } + SPMD_SENDIF + ls_round(R, G, B, c0b, c1b); + SPMD_SIF(c0b == c1b) + { + SPMD_SIF(c1b != vint(0)) { store(c1b, c1b - vint(1)); } + SPMD_SELSE(c1b != vint(0)) { store(c0b, c0b + vint(1)); } + SPMD_SENDIF + } + SPMD_SENDIF + const vbool use_avg = eval_error(R, G, B, c0b, c1b) < eval_error(R, G, B, c0, c1); + store(c0, spmd_ternaryi(use_avg, c0b, c0)); + store(c1, spmd_ternaryi(use_avg, c1b, c1)); + } + SPMD_SENDIF + + // Final palette + integer-threshold selector pass. + const vint pr0 = expand5(VINT_SHIFT_RIGHT(c0, 11) & vint(31)), pg0 = expand6(VINT_SHIFT_RIGHT(c0, 5) & vint(63)), pb0 = expand5(c0 & vint(31)); + const vint pr1 = expand5(VINT_SHIFT_RIGHT(c1, 11) & vint(31)), pg1 = expand6(VINT_SHIFT_RIGHT(c1, 5) & vint(63)), pb1 = expand5(c1 & vint(31)); + const vint pr2 = div3(vint(2) * pr0 + pr1), pg2 = div3(vint(2) * pg0 + pg1), pb2 = div3(vint(2) * pb0 + pb1); + const vint pr3 = div3(pr0 + vint(2) * pr1), pg3 = div3(pg0 + vint(2) * pg1), pb3 = div3(pb0 + vint(2) * pb1); + const thresholds T = make_thresholds(pr0, pg0, pb0, pr1, pg1, pb1, pr2, pg2, pb2, pr3, pg3, pb3); + vint sw(0); + for (int i = 0; i < 16; i++) + { + const vint bestsel = thresh_sel(T, R[i], G[i], B[i]); + sw = sw | (bestsel << (i * 2)); + } + store(color0, c0); store(color1, c1); store(sel_word, sw); + } + SPMD_SENDIF + + // Emit one BC1 block per lane directly to the (possibly strided) output -- no temp buffer. num_write < 4 + // for the final partial group so the padded lanes aren't written; out_stride lets BC3/BC5 interleave. + CPPSPMD_DECL(int, c0a[4]); CPPSPMD_DECL(int, c1a[4]); CPPSPMD_DECL(int, swa[4]); + storeu_linear_all(c0a, color0); + storeu_linear_all(c1a, color1); + storeu_linear_all(swa, sel_word); + for (uint32_t j = 0; j < num_write; j++) + { + uint8_t* o = pOut + j * out_stride; + const uint32_t c0 = (uint32_t)c0a[j], c1 = (uint32_t)c1a[j], sw = (uint32_t)swa[j]; + o[0] = (uint8_t)c0; o[1] = (uint8_t)(c0 >> 8); + o[2] = (uint8_t)c1; o[3] = (uint8_t)(c1 >> 8); + o[4] = (uint8_t)sw; o[5] = (uint8_t)(sw >> 8); o[6] = (uint8_t)(sw >> 16); o[7] = (uint8_t)(sw >> 24); + } + } + }; +} // namespace bc_spmd_kern + +namespace bc_spmd_kern +{ + // BC4 (RGTC1) -- one single-channel block per lane, 4 blocks/call. Clone of basist::encode_bc4: raw bbox + // min/max, exact nearest-of-8 integer-threshold selector, ALWAYS 8-value mode (solid forced to red0>red1). + // All-integer, GATHER-FREE: texels via manual load; rank->index by arithmetic (no s_tran gather); the 48-bit + // selector packed via uniform shifts into two vints (texels 0-9 in lo, 10-15 in hi), combined with <<30 in scalar. + struct encode_bc4_blocks : spmd_kernel + { + // Nearest-of-8 selectors + decoded SSE for an 8-value endpoint pair (r0 > r1). Packs texels 0-9 into slo, + // 10-15 into shi. Optionally accumulates the 1-D least-squares moments (rank = interpolation weight 0..7). + inline vint bc4_eval(const vint* A, const vint& r0, const vint& r1, vint& slo, vint& shi, vint* pSw, vint* pSww, vint* pSv, vint* pSwv) + { + const vint delta = r0 - r1; + const vint bias = vint(4) - r1 * vint(14); + const vint t0 = delta * vint(13), t1 = delta * vint(11), t2 = delta * vint(9), t3 = delta * vint(7), t4 = delta * vint(5), t5 = delta * vint(3), t6 = delta; + vint sse(0), Sw(0), Sww(0), Sv(0), Swv(0); + slo = vint(0); shi = vint(0); + for (int i = 0; i < 16; i++) + { + const vint val = A[i]; + const vint sv = val * vint(14) + bias; + const vint rank = spmd_ternaryi(sv >= t0, 1, 0) + spmd_ternaryi(sv >= t1, 1, 0) + spmd_ternaryi(sv >= t2, 1, 0) + + spmd_ternaryi(sv >= t3, 1, 0) + spmd_ternaryi(sv >= t4, 1, 0) + spmd_ternaryi(sv >= t5, 1, 0) + spmd_ternaryi(sv >= t6, 1, 0); + const vint idx = spmd_ternaryi(rank == vint(7), vint(0), spmd_ternaryi(rank == vint(0), vint(1), vint(8) - rank)); + if (i < 10) slo = slo | (idx << (i * 3)); + else shi = shi | (idx << ((i - 10) * 3)); + const vint num = rank * r0 + (vint(7) - rank) * r1; + const vint recon = VUINT_SHIFT_RIGHT(num * vint(9363), 16); // == num/7 floor for num<=1785 (matches scalar /7) + const vint d = val - recon; sse = sse + d * d; + Sw = Sw + rank; Sww = Sww + rank * rank; Sv = Sv + val; Swv = Swv + rank * val; + } + if (pSw) { *pSw = Sw; *pSww = Sww; *pSv = Sv; *pSwv = Swv; } + return sse; + } + + void _call(const uint8_t* pBlocks, uint8_t* pOut, uint32_t out_stride, uint32_t num_write, uint32_t do_ls, uint32_t src_stride) + { + // Stage 16 channel values to SoA: A[i] = texel i of the 4 lane-blocks. Source values are src_stride bytes + // apart (1 = packed channel, 4 = one channel of RGBA); blocks are 16*src_stride bytes apart. + const uint32_t S = src_stride; + vint A[16]; + for (int i = 0; i < 16; i++) + A[i].m_value = _mm_setr_epi32(pBlocks[i * S], pBlocks[(16 + i) * S], pBlocks[(32 + i) * S], pBlocks[(48 + i) * S]); + + vint mn = A[0], mx = A[0]; + for (int i = 1; i < 16; i++) { mn = min(mn, A[i]); mx = max(mx, A[i]); } + + vint color0(0), color1(0), sel_lo(0), sel_hi(0); + + const vbool solid = (mx == mn); + SPMD_SIF(solid) + { + // Force 8-value solid: value>0 -> (max, max-1, all idx 0); value==0 -> (1, 0, all idx 1). + const vbool pos = (mx > vint(0)); + store(color0, spmd_ternaryi(pos, mx, vint(1))); + store(color1, spmd_ternaryi(pos, mx - vint(1), vint(0))); + const vint idx = spmd_ternaryi(pos, vint(0), vint(1)); // replicated into every 3-bit field + store(sel_lo, idx * vint(0x9249249)); // 10 fields (texels 0-9) all = idx + store(sel_hi, idx * vint(0x9249)); // 6 fields (texels 10-15) all = idx + } + SPMD_SELSE(solid) + { + if (do_ls) + { + // HQ: candidate A = raw bbox (== encode_bc4) + LS moments; candidate B = 1-D LS endpoint refit + // (value ~= a + (rank/7)*b). detf guards /0 for degenerate lanes (refit then rejected). Keep-best. + vint sloA, shiA, Sw, Sww, Sv, Swv; + const vint sseA = bc4_eval(A, mx, mn, sloA, shiA, &Sw, &Sww, &Sv, &Swv); + const vint detI = vint(16) * Sww - Sw * Sw; + const vfloat detf = (vfloat)spmd_ternaryi(detI == vint(0), vint(1), detI); + const vfloat b = vfloat(7.0f) * (vfloat)(vint(16) * Swv - Sw * Sv) / detf; // slope = red0 - red1 + const vfloat bsw7 = b * (vfloat)Sw * vfloat(1.0f / 7.0f); + const vfloat a = ((vfloat)Sv - bsw7) / vfloat(16.0f); // intercept = red1 + vint nr1 = vint(round_nearest(a)); nr1 = min(max(nr1, vint(0)), vint(255)); + vint nr0 = vint(round_nearest(a + b)); nr0 = min(max(nr0, vint(0)), vint(255)); + vint sloB, shiB; + const vint sseB = bc4_eval(A, nr0, nr1, sloB, shiB, nullptr, nullptr, nullptr, nullptr); + const vbool adopt = ((detI != vint(0)) && (nr0 > nr1)) && (sseB < sseA); + store(color0, spmd_ternaryi(adopt, nr0, mx)); + store(color1, spmd_ternaryi(adopt, nr1, mn)); + store(sel_lo, spmd_ternaryi(adopt, sloB, sloA)); + store(sel_hi, spmd_ternaryi(adopt, shiB, shiA)); + } + else + { + // FAST: raw bbox endpoints + nearest-of-8 selector only (== basist::encode_bc4 quality, ~1.4x). + store(color0, mx); store(color1, mn); + const vint delta = mx - mn; + const vint bias = vint(4) - mn * vint(14); + const vint t0 = delta * vint(13), t1 = delta * vint(11), t2 = delta * vint(9), t3 = delta * vint(7), t4 = delta * vint(5), t5 = delta * vint(3), t6 = delta; + vint slo(0), shi(0); + for (int i = 0; i < 16; i++) + { + const vint sv = A[i] * vint(14) + bias; + const vint rank = spmd_ternaryi(sv >= t0, 1, 0) + spmd_ternaryi(sv >= t1, 1, 0) + spmd_ternaryi(sv >= t2, 1, 0) + + spmd_ternaryi(sv >= t3, 1, 0) + spmd_ternaryi(sv >= t4, 1, 0) + spmd_ternaryi(sv >= t5, 1, 0) + spmd_ternaryi(sv >= t6, 1, 0); + const vint idx = spmd_ternaryi(rank == vint(7), vint(0), spmd_ternaryi(rank == vint(0), vint(1), vint(8) - rank)); + if (i < 10) slo = slo | (idx << (i * 3)); + else shi = shi | (idx << ((i - 10) * 3)); + } + store(sel_lo, slo); store(sel_hi, shi); + } + } + SPMD_SENDIF + + CPPSPMD_DECL(int, c0a[4]); CPPSPMD_DECL(int, c1a[4]); CPPSPMD_DECL(int, sloa[4]); CPPSPMD_DECL(int, shia[4]); + storeu_linear_all(c0a, color0); + storeu_linear_all(c1a, color1); + storeu_linear_all(sloa, sel_lo); + storeu_linear_all(shia, sel_hi); + for (uint32_t j = 0; j < num_write; j++) + { + uint8_t* o = pOut + j * out_stride; + o[0] = (uint8_t)c0a[j]; o[1] = (uint8_t)c1a[j]; + const uint64_t W = (uint64_t)(uint32_t)sloa[j] | ((uint64_t)(uint32_t)shia[j] << 30); // 48-bit selector word + o[2] = (uint8_t)W; o[3] = (uint8_t)(W >> 8); o[4] = (uint8_t)(W >> 16); + o[5] = (uint8_t)(W >> 24); o[6] = (uint8_t)(W >> 32); o[7] = (uint8_t)(W >> 40); + } + } + }; +} // namespace bc_spmd_kern + +namespace basisu +{ + namespace bc_spmd + { + void encode_bc4_blocks_4_sse41(const uint8_t* pBlocks, uint8_t* pOut, uint32_t out_stride, uint32_t num_write, uint32_t do_ls, uint32_t src_stride) + { + spmd_call(pBlocks, pOut, out_stride, num_write, do_ls, src_stride); + } + } +} + +// ISA wrapper -- encode 4 blocks at once. Defined here (inside the SSE4.1 TU), declared in basisu_bc15_spmd.cpp. +namespace basisu +{ + namespace bc_spmd + { + void encode_bc1_blocks_4_sse41(const color_rgba* pBlocks, uint8_t* pOut, + int* om5lo, int* om5hi, int* om6lo, int* om6hi, int ls_rounds, int avgvar, + uint32_t out_stride, uint32_t num_write) + { + spmd_call(pBlocks, pOut, om5lo, om5hi, om6lo, om6hi, ls_rounds, avgvar, out_stride, num_write); + } + } +} diff --git a/encoder/basisu_bc15_spmd_sse.cpp b/encoder/basisu_bc15_spmd_sse.cpp new file mode 100644 index 0000000..2caeeff --- /dev/null +++ b/encoder/basisu_bc15_spmd_sse.cpp @@ -0,0 +1,22 @@ +// basisu_bc15_spmd_sse.cpp -- SSE4.1 ISA translation unit for the standalone BC1 encoder. +// +// Mirrors basisu_kernels_sse.cpp's pattern: select the ISA, include the cppspmd framework, then pull in the +// kernel .inl ("do not directly include"). Multiple TUs may include the framework because cppspmd_math.h's +// print_* helpers are inline. + +#include "basisu_enc.h" // basisu::color_rgba + BASISU_SUPPORT_SSE + +#if BASISU_SUPPORT_SSE + +#define CPPSPMD_SSE2 0 +#ifdef _MSC_VER +#include +#endif +#include "cppspmd_sse.h" +#include "cppspmd_type_aliases.h" + +using namespace CPPSPMD; + +#include "basisu_bc15_spmd_kernels.inl" + +#endif // BASISU_SUPPORT_SSE diff --git a/encoder/basisu_bc7e_scalar.cpp b/encoder/basisu_bc7e_scalar.cpp new file mode 100644 index 0000000..de312ca --- /dev/null +++ b/encoder/basisu_bc7e_scalar.cpp @@ -0,0 +1,5015 @@ +// bc7e_scalar.cpp - Pure scalar C++17 port of bc7e.ispc (auto-derived, then +// hand-fixed). De-SIMD'd: the single SPMD foreach over blocks becomes a plain +// for loop; uniform/varying are stripped (one lane/block, no gang), and the +// ISPC stdlib surface is provided by ispc_compat.h. Encodes one 4x4 BC7 block +// per iteration. Logic/math and the public API are preserved verbatim. +#include "basisu_bc7e_scalar.h" +#include +#include // memset, memcpy +#include +#include +#include + +// --- ISPC compatibility shim (formerly ispc_compat.h, inlined here so this .cpp +// --- plus basisu_bc7e_scalar.h are fully self-contained). Supplies the small ISPC +// --- stdlib surface the de-SIMD'd code uses, the base integer type aliases, and +// --- the coherent control-flow keyword macros. Kept in the .cpp (not the public +// --- header) so these global templates/macros don't leak into the rest of the project. + +// ISPC base integer type aliases (the fixed-width *_t names come from ). +typedef int8_t int8; +typedef int16_t int16; +typedef int32_t int32; +typedef int64_t int64; + +// ISPC "coherent" control-flow keywords -> plain C++ control flow (no-op at gang width 1). +#define cif if +#define cfor for +#define cwhile while + +// Mark an intentionally-unused variable/parameter (silences C4100/C4189). +#define NOTE_UNUSED(x) (void)(x) + +// Range-checked cast for a float that is known to hold a small non-negative integral +// value (e.g. an 8-bit channel min/max). Asserts the value is in [0,255] in debug, then +// narrows to uint32_t. Used at the few float->uint sites so the truncation is explicit +// and guarded rather than an implicit (and portability-warning-triggering) conversion. +static inline uint32_t float_to_uint8(float v) { assert((v >= 0.0f) && (v <= 255.0f)); return (uint32_t)v; } + +// ISPC stdlib scalar equivalents. min/max match minss/maxss semantics (the second +// operand is returned on an unordered/NaN comparison), and clamp() is defined as +// min(max(v,lo),hi) -- NOT a direct ternary -- so clamp(NaN,lo,hi) yields lo just as +// it does under ISPC (a direct ternary would yield NaN, which casts to INT_MIN and +// becomes an out-of-bounds index). +template static inline T min(T a, T b) { return a < b ? a : b; } +template static inline T max(T a, T b) { return a > b ? a : b; } +template static inline T clamp(T v, T lo, T hi) { return min(max(v, lo), hi); } +template static inline T select(bool c, T a, T b) { return c ? a : b; } +template static inline bool all(T v) { return (bool)v; } +template static inline bool any(T v) { return (bool)v; } +template static inline bool none(T v) { return !(bool)v; } +template static inline T abs(T v) { return v < 0 ? -v : v; } +// floor()/sqrt() on floats resolve to the global float overloads provides under MSVC. + +namespace bc7e_scalar { + +// TEMP debug checkpoint (single-threaded runs only). Prints the source line so +// the last value before a crash pinpoints the failing statement. Remove once verified. +#define HERE() do { fprintf(stderr, "L%d\n", __LINE__); fflush(stderr); } while(0) + +// Tiny bias added to data-dependent denominators that can legitimately be zero +// (flat blocks / flat channels -> zero endpoint deltas). The original SIMD code +// let the divide produce +/-Inf/NaN and relied on a later min/max clamp to +// sanitize it; in scalar code an unguarded 0/0 NaN can become an INT_MIN array +// index. Biasing keeps the divisor finite and nonzero with negligible effect on +// the result (denominators are otherwise >= 1 in these spots). +static const float BC7E_DENOM_BIAS = 0.0000125f; + +// bc7e.ispc - Fast high quality SIMD BC7 encoder +// Copyright (C) 2018-2020 Binomial LLC, All rights reserved. Apache 2.0 license - see LICENSE. +// Typically compiled as: ispc -g -O2 "%(Filename).ispc" -o "$(TargetDir)%(Filename).obj" -h "$(ProjectDir)%(Filename)_ispc.h" --target=sse2,sse4,avx,avx2 --opt=fast-math --opt=disable-assertions +// --opt=fast-math is optional (doesn't make much if any measurable difference). +// Thanks to ArasP for the determinism fix. + +#define BC7E_NON_DETERMINISTIC (0) +#define BC7E_2SUBSET_CHECKERBOARD_PARTITION_INDEX (34) +#define BC7E_BLOCK_SIZE (16) +#define BC7E_MAX_PARTITIONS0 (16) +#define BC7E_MAX_PARTITIONS1 (64) +#define BC7E_MAX_PARTITIONS2 (64) +#define BC7E_MAX_PARTITIONS3 (64) +#define BC7E_MAX_PARTITIONS7 (64) +#define BC7E_MAX_UBER_LEVEL (4) + +// endpoint_err::m_error is a real uint16_t (was a 64-bit alias inherited from the ISPC source). +// Verified safe to narrow: all stores are single-channel squared errors (k-c)^2 <= 65025, +// guarded by safe_cast_uint16(); reads only compare or sum 3-4 of them (integer-promoted to +// int, max ~260100), so 16 bits is sufficient and no 64-bit width is relied upon. + +#ifndef UINT16_MAX +#define UINT16_MAX (0xFFFF) +#endif + +#ifndef UINT_MAX +#define UINT_MAX (0xFFFFFFFFU) +#endif + +#ifndef UINT64_MAX +#define UINT64_MAX (0xFFFFFFFFFFFFFFFFULL) +#endif + +#ifndef INT64_MAX +#define INT64_MAX (0x7FFFFFFFFFFFFFFFULL) +#endif + + +static inline int32_t clampi( int32_t value, int32_t low, int32_t high) { return clamp(value, low, high); } +[[maybe_unused]] static inline uint32_t clampu( uint32_t value, uint32_t low, uint32_t high) { return clamp(value, low, high); } +static inline float clampf( float value, float low, float high) { return clamp(value, low, high); } + +static inline float saturate( float value) { return clampf(value, 0, 1.0f); } +[[maybe_unused]] static inline float saturate255( float value) { return clampf(value, 0, 255.0f); } + +[[maybe_unused]] static inline uint8_t minimumub( uint8_t a, uint8_t b) { return min(a, b); } +static inline int32_t minimumi( int32_t a, int32_t b) { return min(a, b); } +static inline uint32_t minimumu( uint32_t a, uint32_t b) { return min(a, b); } +[[maybe_unused]] static inline uint64_t minimumu64( uint64_t a, uint64_t b) { return min(a, b); } +static inline float minimumf( float a, float b) { return min(a, b); } + +[[maybe_unused]] static inline uint8_t maximumub( uint8_t a, uint8_t b) { return max(a, b); } +static inline int32_t maximumi( int32_t a, int32_t b) { return max(a, b); } +static inline uint32_t maximumu( uint32_t a, uint32_t b) { return max(a, b); } +static inline float maximumf( float a, float b) { return max(a, b); } + +static inline int32_t iabs32( int32_t v) { uint32_t msk = v >> 31; return (v ^ msk) - msk; } + +[[maybe_unused]] static inline void swapub( uint8_t * a, uint8_t * b) { uint8_t t = *a; *a = *b; *b = t; } +static inline void swapu( uint32_t * a, uint32_t * b) { uint32_t t = *a; *a = *b; *b = t; } +static inline void swapi( int32_t * a, int32_t * b) { int32_t t = *a; *a = *b; *b = t; } +static inline void swapf( float * a, float * b) { float t = *a; *a = *b; *b = t; } + +static inline float square(float s) { return s * s; } +[[maybe_unused]] static inline int square(int s) { return s * s; } + +struct color_quad_u8 +{ + uint8_t m_c[4]; +}; + +struct color_quad_i +{ + int32_t m_c[4]; +}; + +struct color_quad_f +{ + float m_c[4]; +}; + +[[maybe_unused]] static inline color_quad_i component_min_rgb(const color_quad_i * pA, const color_quad_i * pB) +{ + color_quad_i res; + res.m_c[0] = minimumi(pA->m_c[0], pB->m_c[0]); + res.m_c[1] = minimumi(pA->m_c[1], pB->m_c[1]); + res.m_c[2] = minimumi(pA->m_c[2], pB->m_c[2]); + res.m_c[3] = 255; + return res; +} + +[[maybe_unused]] static inline color_quad_i component_max_rgb(const color_quad_i * pA, const color_quad_i * pB) +{ + color_quad_i res; + res.m_c[0] = maximumi(pA->m_c[0], pB->m_c[0]); + res.m_c[1] = maximumi(pA->m_c[1], pB->m_c[1]); + res.m_c[2] = maximumi(pA->m_c[2], pB->m_c[2]); + res.m_c[3] = 255; + return res; +} + +static inline color_quad_i *color_quad_i_set_clamped( color_quad_i * pRes, int32_t r, int32_t g, int32_t b, int32_t a) +{ + pRes->m_c[0] = clampi(r, 0, 255); + pRes->m_c[1] = clampi(g, 0, 255); + pRes->m_c[2] = clampi(b, 0, 255); + pRes->m_c[3] = clampi(a, 0, 255); + return pRes; +} + +static inline color_quad_i *color_quad_i_set( color_quad_i * pRes, int32_t r, int32_t g, int32_t b, int32_t a) +{ + pRes->m_c[0] = r; + pRes->m_c[1] = g; + pRes->m_c[2] = b; + pRes->m_c[3] = a; + return pRes; +} + +static inline bool color_quad_i_equals(const color_quad_i * pLHS, const color_quad_i * pRHS) +{ + return (pLHS->m_c[0] == pRHS->m_c[0]) && (pLHS->m_c[1] == pRHS->m_c[1]) && (pLHS->m_c[2] == pRHS->m_c[2]) && (pLHS->m_c[3] == pRHS->m_c[3]); +} + +static inline bool color_quad_i_notequals(const color_quad_i * pLHS, const color_quad_i * pRHS) +{ + return !color_quad_i_equals(pLHS, pRHS); +} + +struct vec4F +{ + float m_c[4]; +}; + +static inline vec4F * vec4F_set_scalar( vec4F * pV, float x) +{ + pV->m_c[0] = x; + pV->m_c[1] = x; + pV->m_c[2] = x; + pV->m_c[3] = x; + return pV; +} + +static inline vec4F * vec4F_set( vec4F * pV, float x, float y, float z, float w) +{ + pV->m_c[0] = x; + pV->m_c[1] = y; + pV->m_c[2] = z; + pV->m_c[3] = w; + return pV; +} + +static inline vec4F * vec4F_saturate_in_place( vec4F * pV) +{ + pV->m_c[0] = saturate(pV->m_c[0]); + pV->m_c[1] = saturate(pV->m_c[1]); + pV->m_c[2] = saturate(pV->m_c[2]); + pV->m_c[3] = saturate(pV->m_c[3]); + return pV; +} + +static inline vec4F vec4F_saturate(const vec4F * pV) +{ + vec4F res; + res.m_c[0] = saturate(pV->m_c[0]); + res.m_c[1] = saturate(pV->m_c[1]); + res.m_c[2] = saturate(pV->m_c[2]); + res.m_c[3] = saturate(pV->m_c[3]); + return res; +} + +static inline vec4F vec4F_from_color(const color_quad_i * pC) +{ + vec4F res; + vec4F_set(&res, (float)pC->m_c[0], (float)pC->m_c[1], (float)pC->m_c[2], (float)pC->m_c[3]); + return res; +} + +static inline vec4F vec4F_add(const vec4F * pLHS, const vec4F * pRHS) +{ + vec4F res; + vec4F_set(&res, pLHS->m_c[0] + pRHS->m_c[0], pLHS->m_c[1] + pRHS->m_c[1], pLHS->m_c[2] + pRHS->m_c[2], pLHS->m_c[3] + pRHS->m_c[3]); + return res; +} + +static inline vec4F vec4F_sub(const vec4F * pLHS, const vec4F * pRHS) +{ + vec4F res; + vec4F_set(&res, pLHS->m_c[0] - pRHS->m_c[0], pLHS->m_c[1] - pRHS->m_c[1], pLHS->m_c[2] - pRHS->m_c[2], pLHS->m_c[3] - pRHS->m_c[3]); + return res; +} + +static inline float vec4F_dot(const vec4F * pLHS, const vec4F * pRHS) +{ + return pLHS->m_c[0] * pRHS->m_c[0] + pLHS->m_c[1] * pRHS->m_c[1] + pLHS->m_c[2] * pRHS->m_c[2] + pLHS->m_c[3] * pRHS->m_c[3]; +} + +static inline vec4F vec4F_mul(const vec4F * pLHS, float s) +{ + vec4F res; + vec4F_set(&res, pLHS->m_c[0] * s, pLHS->m_c[1] * s, pLHS->m_c[2] * s, pLHS->m_c[3] * s); + return res; +} + +static inline vec4F *vec4F_normalize_in_place( vec4F * pV) +{ + float s = pV->m_c[0] * pV->m_c[0] + pV->m_c[1] * pV->m_c[1] + pV->m_c[2] * pV->m_c[2] + pV->m_c[3] * pV->m_c[3]; + + if (s != 0.0f) + { + s = 1.0f / sqrt(s); + + pV->m_c[0] *= s; + pV->m_c[1] *= s; + pV->m_c[2] *= s; + pV->m_c[3] *= s; + } + + return pV; +} + +static const uint32_t g_bc7_weights2[4] = { 0, 21, 43, 64 }; +static const uint32_t g_bc7_weights3[8] = { 0, 9, 18, 27, 37, 46, 55, 64 }; +static const uint32_t g_bc7_weights4[16] = { 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64 }; + +// Precomputed weight constants used during least fit determination. For each entry in g_bc7_weights[]: w * w, (1.0f - w) * w, (1.0f - w) * (1.0f - w), w +static const float g_bc7_weights2x[4 * 4] = { 0.000000f, 0.000000f, 1.000000f, 0.000000f, 0.107666f, 0.220459f, 0.451416f, 0.328125f, 0.451416f, 0.220459f, 0.107666f, 0.671875f, 1.000000f, 0.000000f, 0.000000f, 1.000000f }; +static const float g_bc7_weights3x[8 * 4] = { 0.000000f, 0.000000f, 1.000000f, 0.000000f, 0.019775f, 0.120850f, 0.738525f, 0.140625f, 0.079102f, 0.202148f, 0.516602f, 0.281250f, 0.177979f, 0.243896f, 0.334229f, 0.421875f, 0.334229f, 0.243896f, 0.177979f, 0.578125f, 0.516602f, 0.202148f, + 0.079102f, 0.718750f, 0.738525f, 0.120850f, 0.019775f, 0.859375f, 1.000000f, 0.000000f, 0.000000f, 1.000000f }; +static const float g_bc7_weights4x[16 * 4] = { 0.000000f, 0.000000f, 1.000000f, 0.000000f, 0.003906f, 0.058594f, 0.878906f, 0.062500f, 0.019775f, 0.120850f, 0.738525f, 0.140625f, 0.041260f, 0.161865f, 0.635010f, 0.203125f, 0.070557f, 0.195068f, 0.539307f, 0.265625f, 0.107666f, 0.220459f, + 0.451416f, 0.328125f, 0.165039f, 0.241211f, 0.352539f, 0.406250f, 0.219727f, 0.249023f, 0.282227f, 0.468750f, 0.282227f, 0.249023f, 0.219727f, 0.531250f, 0.352539f, 0.241211f, 0.165039f, 0.593750f, 0.451416f, 0.220459f, 0.107666f, 0.671875f, 0.539307f, 0.195068f, 0.070557f, 0.734375f, + 0.635010f, 0.161865f, 0.041260f, 0.796875f, 0.738525f, 0.120850f, 0.019775f, 0.859375f, 0.878906f, 0.058594f, 0.003906f, 0.937500f, 1.000000f, 0.000000f, 0.000000f, 1.000000f }; + +static const int g_bc7_partition1[16] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }; + +static const int g_bc7_partition2[64 * 16] = +{ + 0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,1, 0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1, 0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1, 0,0,0,1,0,0,1,1,0,0,1,1,0,1,1,1, 0,0,0,0,0,0,0,1,0,0,0,1,0,0,1,1, 0,0,1,1,0,1,1,1,0,1,1,1,1,1,1,1, 0,0,0,1,0,0,1,1,0,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,1,0,0,1,1,0,1,1,1, + 0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,1, 0,0,1,1,0,1,1,1,1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,1,0,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,1, 0,0,0,1,0,1,1,1,1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1, 0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1, + 0,0,0,0,1,0,0,0,1,1,1,0,1,1,1,1, 0,1,1,1,0,0,0,1,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,1,0,0,0,1,1,1,0, 0,1,1,1,0,0,1,1,0,0,0,1,0,0,0,0, 0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0, 0,0,0,0,1,0,0,0,1,1,0,0,1,1,1,0, 0,0,0,0,0,0,0,0,1,0,0,0,1,1,0,0, 0,1,1,1,0,0,1,1,0,0,1,1,0,0,0,1, + 0,0,1,1,0,0,0,1,0,0,0,1,0,0,0,0, 0,0,0,0,1,0,0,0,1,0,0,0,1,1,0,0, 0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0, 0,0,1,1,0,1,1,0,0,1,1,0,1,1,0,0, 0,0,0,1,0,1,1,1,1,1,1,0,1,0,0,0, 0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0, 0,1,1,1,0,0,0,1,1,0,0,0,1,1,1,0, 0,0,1,1,1,0,0,1,1,0,0,1,1,1,0,0, + 0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1, 0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1, 0,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0, 0,0,1,1,0,0,1,1,1,1,0,0,1,1,0,0, 0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0, 0,1,0,1,0,1,0,1,1,0,1,0,1,0,1,0, 0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1, 0,1,0,1,1,0,1,0,1,0,1,0,0,1,0,1, + 0,1,1,1,0,0,1,1,1,1,0,0,1,1,1,0, 0,0,0,1,0,0,1,1,1,1,0,0,1,0,0,0, 0,0,1,1,0,0,1,0,0,1,0,0,1,1,0,0, 0,0,1,1,1,0,1,1,1,1,0,1,1,1,0,0, 0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0, 0,0,1,1,1,1,0,0,1,1,0,0,0,0,1,1, 0,1,1,0,0,1,1,0,1,0,0,1,1,0,0,1, 0,0,0,0,0,1,1,0,0,1,1,0,0,0,0,0, + 0,1,0,0,1,1,1,0,0,1,0,0,0,0,0,0, 0,0,1,0,0,1,1,1,0,0,1,0,0,0,0,0, 0,0,0,0,0,0,1,0,0,1,1,1,0,0,1,0, 0,0,0,0,0,1,0,0,1,1,1,0,0,1,0,0, 0,1,1,0,1,1,0,0,1,0,0,1,0,0,1,1, 0,0,1,1,0,1,1,0,1,1,0,0,1,0,0,1, 0,1,1,0,0,0,1,1,1,0,0,1,1,1,0,0, 0,0,1,1,1,0,0,1,1,1,0,0,0,1,1,0, + 0,1,1,0,1,1,0,0,1,1,0,0,1,0,0,1, 0,1,1,0,0,0,1,1,0,0,1,1,1,0,0,1, 0,1,1,1,1,1,1,0,1,0,0,0,0,0,0,1, 0,0,0,1,1,0,0,0,1,1,1,0,0,1,1,1, 0,0,0,0,1,1,1,1,0,0,1,1,0,0,1,1, 0,0,1,1,0,0,1,1,1,1,1,1,0,0,0,0, 0,0,1,0,0,0,1,0,1,1,1,0,1,1,1,0, 0,1,0,0,0,1,0,0,0,1,1,1,0,1,1,1 +}; + +static const int g_bc7_table_anchor_index_second_subset[64] = +{ + 15,15,15,15,15,15,15,15, 15,15,15,15,15,15,15,15, 15, 2, 8, 2, 2, 8, 8,15, 2, 8, 2, 2, 8, 8, 2, 2, 15,15, 6, 8, 2, 8,15,15, 2, 8, 2, 2, 2,15,15, 6, 6, 2, 6, 8,15,15, 2, 2, 15,15,15,15,15, 2, 2,15 +}; + +static const int g_bc7_partition3[64 * 16] = +{ + 0,0,1,1,0,0,1,1,0,2,2,1,2,2,2,2, 0,0,0,1,0,0,1,1,2,2,1,1,2,2,2,1, 0,0,0,0,2,0,0,1,2,2,1,1,2,2,1,1, 0,2,2,2,0,0,2,2,0,0,1,1,0,1,1,1, 0,0,0,0,0,0,0,0,1,1,2,2,1,1,2,2, 0,0,1,1,0,0,1,1,0,0,2,2,0,0,2,2, 0,0,2,2,0,0,2,2,1,1,1,1,1,1,1,1, 0,0,1,1,0,0,1,1,2,2,1,1,2,2,1,1, + 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2, 0,0,0,0,1,1,1,1,1,1,1,1,2,2,2,2, 0,0,0,0,1,1,1,1,2,2,2,2,2,2,2,2, 0,0,1,2,0,0,1,2,0,0,1,2,0,0,1,2, 0,1,1,2,0,1,1,2,0,1,1,2,0,1,1,2, 0,1,2,2,0,1,2,2,0,1,2,2,0,1,2,2, 0,0,1,1,0,1,1,2,1,1,2,2,1,2,2,2, 0,0,1,1,2,0,0,1,2,2,0,0,2,2,2,0, + 0,0,0,1,0,0,1,1,0,1,1,2,1,1,2,2, 0,1,1,1,0,0,1,1,2,0,0,1,2,2,0,0, 0,0,0,0,1,1,2,2,1,1,2,2,1,1,2,2, 0,0,2,2,0,0,2,2,0,0,2,2,1,1,1,1, 0,1,1,1,0,1,1,1,0,2,2,2,0,2,2,2, 0,0,0,1,0,0,0,1,2,2,2,1,2,2,2,1, 0,0,0,0,0,0,1,1,0,1,2,2,0,1,2,2, 0,0,0,0,1,1,0,0,2,2,1,0,2,2,1,0, + 0,1,2,2,0,1,2,2,0,0,1,1,0,0,0,0, 0,0,1,2,0,0,1,2,1,1,2,2,2,2,2,2, 0,1,1,0,1,2,2,1,1,2,2,1,0,1,1,0, 0,0,0,0,0,1,1,0,1,2,2,1,1,2,2,1, 0,0,2,2,1,1,0,2,1,1,0,2,0,0,2,2, 0,1,1,0,0,1,1,0,2,0,0,2,2,2,2,2, 0,0,1,1,0,1,2,2,0,1,2,2,0,0,1,1, 0,0,0,0,2,0,0,0,2,2,1,1,2,2,2,1, + 0,0,0,0,0,0,0,2,1,1,2,2,1,2,2,2, 0,2,2,2,0,0,2,2,0,0,1,2,0,0,1,1, 0,0,1,1,0,0,1,2,0,0,2,2,0,2,2,2, 0,1,2,0,0,1,2,0,0,1,2,0,0,1,2,0, 0,0,0,0,1,1,1,1,2,2,2,2,0,0,0,0, 0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,0, 0,1,2,0,2,0,1,2,1,2,0,1,0,1,2,0, 0,0,1,1,2,2,0,0,1,1,2,2,0,0,1,1, + 0,0,1,1,1,1,2,2,2,2,0,0,0,0,1,1, 0,1,0,1,0,1,0,1,2,2,2,2,2,2,2,2, 0,0,0,0,0,0,0,0,2,1,2,1,2,1,2,1, 0,0,2,2,1,1,2,2,0,0,2,2,1,1,2,2, 0,0,2,2,0,0,1,1,0,0,2,2,0,0,1,1, 0,2,2,0,1,2,2,1,0,2,2,0,1,2,2,1, 0,1,0,1,2,2,2,2,2,2,2,2,0,1,0,1, 0,0,0,0,2,1,2,1,2,1,2,1,2,1,2,1, + 0,1,0,1,0,1,0,1,0,1,0,1,2,2,2,2, 0,2,2,2,0,1,1,1,0,2,2,2,0,1,1,1, 0,0,0,2,1,1,1,2,0,0,0,2,1,1,1,2, 0,0,0,0,2,1,1,2,2,1,1,2,2,1,1,2, 0,2,2,2,0,1,1,1,0,1,1,1,0,2,2,2, 0,0,0,2,1,1,1,2,1,1,1,2,0,0,0,2, 0,1,1,0,0,1,1,0,0,1,1,0,2,2,2,2, 0,0,0,0,0,0,0,0,2,1,1,2,2,1,1,2, + 0,1,1,0,0,1,1,0,2,2,2,2,2,2,2,2, 0,0,2,2,0,0,1,1,0,0,1,1,0,0,2,2, 0,0,2,2,1,1,2,2,1,1,2,2,0,0,2,2, 0,0,0,0,0,0,0,0,0,0,0,0,2,1,1,2, 0,0,0,2,0,0,0,1,0,0,0,2,0,0,0,1, 0,2,2,2,1,2,2,2,0,2,2,2,1,2,2,2, 0,1,0,1,2,2,2,2,2,2,2,2,2,2,2,2, 0,1,1,1,2,0,1,1,2,2,0,1,2,2,2,0, +}; + +static const int g_bc7_table_anchor_index_third_subset_1[64] = +{ + 3, 3,15,15, 8, 3,15,15, 8, 8, 6, 6, 6, 5, 3, 3, 3, 3, 8,15, 3, 3, 6,10, 5, 8, 8, 6, 8, 5,15,15, 8,15, 3, 5, 6,10, 8,15, 15, 3,15, 5,15,15,15,15, 3,15, 5, 5, 5, 8, 5,10, 5,10, 8,13,15,12, 3, 3 +}; + +static const int g_bc7_table_anchor_index_third_subset_2[64] = +{ + 15, 8, 8, 3,15,15, 3, 8, 15,15,15,15,15,15,15, 8, 15, 8,15, 3,15, 8,15, 8, 3,15, 6,10,15,15,10, 8, 15, 3,15,10,10, 8, 9,10, 6,15, 8,15, 3, 6, 6, 8, 15, 3,15,15,15,15,15,15, 15,15,15,15, 3,15,15, 8 +}; + +static const int g_bc7_num_subsets[8] = { 3, 2, 3, 2, 1, 1, 1, 2 }; +static const int g_bc7_partition_bits[8] = { 4, 6, 6, 6, 0, 0, 0, 6 }; +[[maybe_unused]] static const int g_bc7_rotation_bits[8] = { 0, 0, 0, 0, 2, 2, 0, 0 }; +static const int g_bc7_color_index_bitcount[8] = { 3, 3, 2, 2, 2, 2, 4, 2 }; +static int get_bc7_color_index_size( int mode, int index_selection_bit) { return g_bc7_color_index_bitcount[mode] + index_selection_bit; } +static int g_bc7_alpha_index_bitcount[8] = { 0, 0, 0, 0, 3, 2, 4, 2 }; +static int get_bc7_alpha_index_size( int mode, int index_selection_bit) { return g_bc7_alpha_index_bitcount[mode] - index_selection_bit; } +static const int g_bc7_mode_has_p_bits[8] = { 1, 1, 0, 1, 0, 0, 1, 1 }; +static const int g_bc7_mode_has_shared_p_bits[8] = { 0, 1, 0, 0, 0, 0, 0, 0 }; +static const int g_bc7_color_precision_table[8] = { 4, 6, 5, 7, 5, 7, 7, 5 }; +[[maybe_unused]] static const int g_bc7_color_precision_plus_pbit_table[8] = { 5, 7, 5, 8, 5, 7, 8, 6 }; +static const int g_bc7_alpha_precision_table[8] = { 0, 0, 0, 0, 6, 8, 7, 5 }; +[[maybe_unused]] static const int g_bc7_alpha_precision_plus_pbit_table[8] = { 0, 0, 0, 0, 6, 8, 8, 6 }; +static bool get_bc7_mode_has_seperate_alpha_selectors( int mode) { return (mode == 4) || (mode == 5); } + +struct endpoint_err +{ + uint16_t m_error; + uint8_t m_lo; + uint8_t m_hi; +}; + +// Guarded store into endpoint_err::m_error (a real uint16_t). The stored quantity +// is always a single-channel squared error (k-c)^2 with k,c in [0,255], so it is +// <= 255^2 = 65025 <= UINT16_MAX by construction. This assert verifies that +// invariant on every store; if it ever fires (e.g. a weighted error gets routed +// here), the value would silently truncate at 16 bits, so the assert must hold. +static inline uint16_t safe_cast_uint16(int v) +{ + assert(v >= 0 && v <= (int)UINT16_MAX); + return (uint16_t)v; +} + +static endpoint_err g_bc7_mode_1_optimal_endpoints[256][2]; // [c][pbit] +const uint32_t BC7E_MODE_1_OPTIMAL_INDEX = 2; + +static endpoint_err g_bc7_mode_7_optimal_endpoints[256][2][2]; // [c][pbit][hp][lp] +const uint32_t BC7E_MODE_7_OPTIMAL_INDEX = 1; + +static endpoint_err g_bc7_mode_6_optimal_endpoints[256][2][2]; // [c][hp][lp] +const uint32_t BC7E_MODE_6_OPTIMAL_INDEX = 5; + +static uint32_t g_bc7_mode_4_optimal_endpoints3[256]; // [c] +static uint32_t g_bc7_mode_4_optimal_endpoints2[256]; // [c] +const uint32_t BC7E_MODE_4_OPTIMAL_INDEX3 = 2; +const uint32_t BC7E_MODE_4_OPTIMAL_INDEX2 = 1; + +static uint32_t g_bc7_mode_5_optimal_endpoints[256]; // [c] +const uint32_t BC7E_MODE_5_OPTIMAL_INDEX = 1; + +static endpoint_err g_bc7_mode_0_optimal_endpoints[256][2][2]; // [c][hp][lp] +const uint32_t BC7E_MODE_0_OPTIMAL_INDEX = 2; + +static bool g_codec_initialized; + + void bc7e_compress_block_init() +{ + if (g_codec_initialized) + return; + + // Mode 0: 444.1 + for ( int c = 0; c < 256; c++) + { + for ( uint32_t hp = 0; hp < 2; hp++) + { + for ( uint32_t lp = 0; lp < 2; lp++) + { + endpoint_err best; + best.m_error = safe_cast_uint16(UINT16_MAX); + + for ( uint32_t l = 0; l < 16; l++) + { + uint32_t low = ((l << 1) | lp) << 3; + low |= (low >> 5); + + for ( uint32_t h = 0; h < 16; h++) + { + uint32_t high = ((h << 1) | hp) << 3; + high |= (high >> 5); + + const int k = (low * (64 - g_bc7_weights3[BC7E_MODE_0_OPTIMAL_INDEX]) + high * g_bc7_weights3[BC7E_MODE_0_OPTIMAL_INDEX] + 32) >> 6; + + const int err = (k - c) * (k - c); + if (err < best.m_error) + { + best.m_error = safe_cast_uint16(err); + best.m_lo = (uint8_t)l; + best.m_hi = (uint8_t)h; + } + } // h + } // l + + g_bc7_mode_0_optimal_endpoints[c][hp][lp] = best; + } // lp + } // hp + } // c + + // Mode 1: 666.1 + for ( int c = 0; c < 256; c++) + { + for ( uint32_t lp = 0; lp < 2; lp++) + { + endpoint_err best; + best.m_error = safe_cast_uint16(UINT16_MAX); + + for ( uint32_t l = 0; l < 64; l++) + { + uint32_t low = ((l << 1) | lp) << 1; + low |= (low >> 7); + + for ( uint32_t h = 0; h < 64; h++) + { + uint32_t high = ((h << 1) | lp) << 1; + high |= (high >> 7); + + const int k = (low * (64 - g_bc7_weights3[BC7E_MODE_1_OPTIMAL_INDEX]) + high * g_bc7_weights3[BC7E_MODE_1_OPTIMAL_INDEX] + 32) >> 6; + + const int err = (k - c) * (k - c); + if (err < best.m_error) + { + best.m_error = safe_cast_uint16(err); + best.m_lo = (uint8_t)l; + best.m_hi = (uint8_t)h; + } + } // h + } // l + + g_bc7_mode_1_optimal_endpoints[c][lp] = best; + } // lp + } // c + + // Mode 6: 777.1 4-bit indices + for ( int c = 0; c < 256; c++) + { + for ( uint32_t hp = 0; hp < 2; hp++) + { + for ( uint32_t lp = 0; lp < 2; lp++) + { + endpoint_err best; + best.m_error = safe_cast_uint16(UINT16_MAX); + + for ( uint32_t l = 0; l < 128; l++) + { + uint32_t low = (l << 1) | lp; + + for ( uint32_t h = 0; h < 128; h++) + { + uint32_t high = (h << 1) | hp; + + const int k = (low * (64 - g_bc7_weights4[BC7E_MODE_6_OPTIMAL_INDEX]) + high * g_bc7_weights4[BC7E_MODE_6_OPTIMAL_INDEX] + 32) >> 6; + + const int err = (k - c) * (k - c); + if (err < best.m_error) + { + best.m_error = safe_cast_uint16(err); + best.m_lo = (uint8_t)l; + best.m_hi = (uint8_t)h; + } + } // h + } // l + + g_bc7_mode_6_optimal_endpoints[c][hp][lp] = best; + } // lp + } // hp + } // c + + // Mode 5: 777 2-bit indices + for ( int c = 0; c < 256; c++) + { + endpoint_err best; + best.m_error = safe_cast_uint16(UINT16_MAX); + best.m_lo = 0; + best.m_hi = 0; + + for ( uint32_t l = 0; l < 128; l++) + { + uint32_t low = l << 1; + low |= (low >> 7); + + for ( uint32_t h = 0; h < 128; h++) + { + uint32_t high = h << 1; + high |= (high >> 7); + + const int k = (low * (64 - g_bc7_weights2[BC7E_MODE_5_OPTIMAL_INDEX]) + high * g_bc7_weights2[BC7E_MODE_5_OPTIMAL_INDEX] + 32) >> 6; + + const int err = (k - c) * (k - c); + if (err < best.m_error) + { + best.m_error = safe_cast_uint16(err); + best.m_lo = (uint8_t)l; + best.m_hi = (uint8_t)h; + } + } // h + } // l + + g_bc7_mode_5_optimal_endpoints[c] = (uint32_t)best.m_lo | (((uint32_t)best.m_hi) << 8); + + } // c + + + //Mode 4: 555 3-bit indices + for ( int c = 0; c < 256; c++) + { + endpoint_err best; + best.m_error = safe_cast_uint16(UINT16_MAX); + best.m_lo = 0; + best.m_hi = 0; + + for ( uint32_t l = 0; l < 32; l++) + { + uint32_t low = l << 3; + low |= (low >> 5); + + for ( uint32_t h = 0; h < 32; h++) + { + uint32_t high = h << 3; + high |= (high >> 5); + + const int k = (low * (64 - g_bc7_weights3[BC7E_MODE_4_OPTIMAL_INDEX3]) + high * g_bc7_weights3[BC7E_MODE_4_OPTIMAL_INDEX3] + 32) >> 6; + + const int err = (k - c) * (k - c); + if (err < best.m_error) + { + best.m_error = safe_cast_uint16(err); + best.m_lo = (uint8_t)l; + best.m_hi = (uint8_t)h; + } + } // h + } // l + + g_bc7_mode_4_optimal_endpoints3[c] = (uint32_t)best.m_lo | (((uint32_t)best.m_hi) << 8); + + } // c + + // Mode 4: 555 2-bit indices + for ( int c = 0; c < 256; c++) + { + endpoint_err best; + best.m_error = safe_cast_uint16(UINT16_MAX); + best.m_lo = 0; + best.m_hi = 0; + + for ( uint32_t l = 0; l < 32; l++) + { + uint32_t low = l << 3; + low |= (low >> 5); + + for ( uint32_t h = 0; h < 32; h++) + { + uint32_t high = h << 3; + high |= (high >> 5); + + const int k = (low * (64 - g_bc7_weights2[BC7E_MODE_4_OPTIMAL_INDEX2]) + high * g_bc7_weights2[BC7E_MODE_4_OPTIMAL_INDEX2] + 32) >> 6; + + const int err = (k - c) * (k - c); + if (err < best.m_error) + { + best.m_error = safe_cast_uint16(err); + best.m_lo = (uint8_t)l; + best.m_hi = (uint8_t)h; + } + } // h + } // l + + g_bc7_mode_4_optimal_endpoints2[c] = (uint32_t)best.m_lo | (((uint32_t)best.m_hi) << 8); + + } // c + + // Mode 7: 555.1 2-bit indices + for ( int c = 0; c < 256; c++) + { + endpoint_err best; + best.m_error = safe_cast_uint16(UINT16_MAX); + best.m_lo = 0; + best.m_hi = 0; + + for ( uint32_t hp = 0; hp < 2; hp++) + { + for ( uint32_t lp = 0; lp < 2; lp++) + { + for ( uint32_t l = 0; l < 32; l++) + { + uint32_t low = ((l << 1) | lp) << 2; + low |= (low >> 6); + + for ( uint32_t h = 0; h < 32; h++) + { + uint32_t high = ((h << 1) | hp) << 2; + high |= (high >> 6); + + const int k = (low * (64 - g_bc7_weights2[BC7E_MODE_7_OPTIMAL_INDEX]) + high * g_bc7_weights2[BC7E_MODE_7_OPTIMAL_INDEX] + 32) >> 6; + + const int err = (k - c) * (k - c); + if (err < best.m_error) + { + best.m_error = safe_cast_uint16(err); + best.m_lo = (uint8_t)l; + best.m_hi = (uint8_t)h; + } + } // h + } // l + + g_bc7_mode_7_optimal_endpoints[c][hp][lp] = best; + + } // hp + + } // lp + + } // c + + g_codec_initialized = true; +} + +static void compute_least_squares_endpoints_rgba(uint32_t N, const int * pSelectors, const vec4F * pSelector_weights, vec4F * pXl, vec4F * pXh, const color_quad_i * pColors) +{ + // Least squares using normal equations: http://www.cs.cornell.edu/~bindel/class/cs3220-s12/notes/lec10.pdf + // I did this in matrix form first, expanded out all the ops, then optimized it a bit. + float z00 = 0.0f, z01 = 0.0f, z10 = 0.0f, z11 = 0.0f; + float q00_r = 0.0f, q10_r = 0.0f, t_r = 0.0f; + float q00_g = 0.0f, q10_g = 0.0f, t_g = 0.0f; + float q00_b = 0.0f, q10_b = 0.0f, t_b = 0.0f; + float q00_a = 0.0f, q10_a = 0.0f, t_a = 0.0f; + for ( uint32_t i = 0; i < N; i++) + { + const uint32_t sel = pSelectors[i]; + + z00 += pSelector_weights[sel].m_c[0]; + z10 += pSelector_weights[sel].m_c[1]; + z11 += pSelector_weights[sel].m_c[2]; + + float w = pSelector_weights[sel].m_c[3]; + + q00_r += w * (int)pColors[i].m_c[0]; t_r += (int)pColors[i].m_c[0]; + q00_g += w * (int)pColors[i].m_c[1]; t_g += (int)pColors[i].m_c[1]; + q00_b += w * (int)pColors[i].m_c[2]; t_b += (int)pColors[i].m_c[2]; + q00_a += w * (int)pColors[i].m_c[3]; t_a += (int)pColors[i].m_c[3]; + } + + q10_r = t_r - q00_r; + q10_g = t_g - q00_g; + q10_b = t_b - q00_b; + q10_a = t_a - q00_a; + + z01 = z10; + + float det = z00 * z11 - z01 * z10; + if (det != 0.0f) + det = 1.0f / det; + + float iz00, iz01, iz10, iz11; + iz00 = z11 * det; + iz01 = -z01 * det; + iz10 = -z10 * det; + iz11 = z00 * det; + + pXl->m_c[0] = (float)(iz00 * q00_r + iz01 * q10_r); pXh->m_c[0] = (float)(iz10 * q00_r + iz11 * q10_r); + pXl->m_c[1] = (float)(iz00 * q00_g + iz01 * q10_g); pXh->m_c[1] = (float)(iz10 * q00_g + iz11 * q10_g); + pXl->m_c[2] = (float)(iz00 * q00_b + iz01 * q10_b); pXh->m_c[2] = (float)(iz10 * q00_b + iz11 * q10_b); + pXl->m_c[3] = (float)(iz00 * q00_a + iz01 * q10_a); pXh->m_c[3] = (float)(iz10 * q00_a + iz11 * q10_a); +} + +static void compute_least_squares_endpoints_rgb(uint32_t N, const int * pSelectors, const vec4F * pSelector_weights, vec4F * pXl, vec4F * pXh, const color_quad_i * pColors) +{ + // Least squares using normal equations: http://www.cs.cornell.edu/~bindel/class/cs3220-s12/notes/lec10.pdf + // I did this in matrix form first, expanded out all the ops, then optimized it a bit. + float z00 = 0.0f, z01 = 0.0f, z10 = 0.0f, z11 = 0.0f; + float q00_r = 0.0f, q10_r = 0.0f, t_r = 0.0f; + float q00_g = 0.0f, q10_g = 0.0f, t_g = 0.0f; + float q00_b = 0.0f, q10_b = 0.0f, t_b = 0.0f; + for ( uint32_t i = 0; i < N; i++) + { + const uint32_t sel = pSelectors[i]; + + z00 += pSelector_weights[sel].m_c[0]; + z10 += pSelector_weights[sel].m_c[1]; + z11 += pSelector_weights[sel].m_c[2]; + float w = pSelector_weights[sel].m_c[3]; + + q00_r += w * (int)pColors[i].m_c[0]; t_r += (int)pColors[i].m_c[0]; + q00_g += w * (int)pColors[i].m_c[1]; t_g += (int)pColors[i].m_c[1]; + q00_b += w * (int)pColors[i].m_c[2]; t_b += (int)pColors[i].m_c[2]; + } + + q10_r = t_r - q00_r; + q10_g = t_g - q00_g; + q10_b = t_b - q00_b; + + z01 = z10; + + float det = z00 * z11 - z01 * z10; + if (det != 0.0f) + det = 1.0f / det; + + float iz00, iz01, iz10, iz11; + iz00 = z11 * det; + iz01 = -z01 * det; + iz10 = -z10 * det; + iz11 = z00 * det; + + pXl->m_c[0] = (float)(iz00 * q00_r + iz01 * q10_r); pXh->m_c[0] = (float)(iz10 * q00_r + iz11 * q10_r); + pXl->m_c[1] = (float)(iz00 * q00_g + iz01 * q10_g); pXh->m_c[1] = (float)(iz10 * q00_g + iz11 * q10_g); + pXl->m_c[2] = (float)(iz00 * q00_b + iz01 * q10_b); pXh->m_c[2] = (float)(iz10 * q00_b + iz11 * q10_b); +} + +static void compute_least_squares_endpoints_a(uint32_t N, const int * pSelectors, const vec4F * pSelector_weights, float * pXl, float * pXh, const color_quad_i * pColors) +{ + // Least squares using normal equations: http://www.cs.cornell.edu/~bindel/class/cs3220-s12/notes/lec10.pdf + // I did this in matrix form first, expanded out all the ops, then optimized it a bit. + float z00 = 0.0f, z01 = 0.0f, z10 = 0.0f, z11 = 0.0f; + float q00_a = 0.0f, q10_a = 0.0f, t_a = 0.0f; + for ( uint32_t i = 0; i < N; i++) + { + const uint32_t sel = pSelectors[i]; + + z00 += pSelector_weights[sel].m_c[0]; + z10 += pSelector_weights[sel].m_c[1]; + z11 += pSelector_weights[sel].m_c[2]; + float w = pSelector_weights[sel].m_c[3]; + + q00_a += w * (int)pColors[i].m_c[3]; t_a += (int)pColors[i].m_c[3]; + } + + q10_a = t_a - q00_a; + + z01 = z10; + + float det = z00 * z11 - z01 * z10; + if (det != 0.0f) + det = 1.0f / det; + + float iz00, iz01, iz10, iz11; + iz00 = z11 * det; + iz01 = -z01 * det; + iz10 = -z10 * det; + iz11 = z00 * det; + + *pXl = (float)(iz00 * q00_a + iz01 * q10_a); *pXh = (float)(iz10 * q00_a + iz11 * q10_a); +} + +struct color_cell_compressor_params +{ + uint32_t m_num_selector_weights; + const uint32_t * m_pSelector_weights; + const vec4F * m_pSelector_weightsx; + uint32_t m_comp_bits; + uint32_t m_weights[4]; + bool m_has_alpha; + bool m_has_pbits; + bool m_endpoints_share_pbit; + bool m_perceptual; +}; + +static inline void color_cell_compressor_params_clear( color_cell_compressor_params * p) +{ + p->m_num_selector_weights = 0; + p->m_pSelector_weights = NULL; + p->m_pSelector_weightsx = NULL; + p->m_comp_bits = 0; + p->m_perceptual = false; + p->m_weights[0] = 1; + p->m_weights[1] = 1; + p->m_weights[2] = 1; + p->m_weights[3] = 1; + p->m_has_alpha = false; + p->m_has_pbits = false; + p->m_endpoints_share_pbit = false; +} + +struct color_cell_compressor_results +{ + uint64_t m_best_overall_err; + color_quad_i m_low_endpoint; + color_quad_i m_high_endpoint; + uint32_t m_pbits[2]; + int *m_pSelectors; + int *m_pSelectors_temp; + // True if this subset's result was produced by the precomputed "one color" + // optimal-endpoint lookup tables (the solid/allSame path or the average-color + // candidate). Those place endpoints at extreme positions that only land at one + // fixed weight -- a hint to callers that this subset is "weird" (fragile under + // lossy weight recoding). + bool m_used_lut; +}; + +static inline color_quad_i scale_color(const color_quad_i * pC, const color_cell_compressor_params * pParams) +{ + color_quad_i results; + + const uint32_t n = pParams->m_comp_bits + (pParams->m_has_pbits ? 1 : 0); + assert((n >= 4) && (n <= 8)); + + for ( uint32_t i = 0; i < 4; i++) + { + uint32_t v = pC->m_c[i] << (8 - n); + v |= (v >> n); + assert(v <= 255); + results.m_c[i] = v; + } + + return results; +} + +static const float pr_weight = (.5f / (1.0f - .2126f)) * (.5f / (1.0f - .2126f)); +static const float pb_weight = (.5f / (1.0f - .0722f)) * (.5f / (1.0f - .0722f)); + +static inline uint64_t compute_color_distance_rgb(const color_quad_i * pE1, const color_quad_i * pE2, bool perceptual, const uint32_t weights[4]) +{ + if (perceptual) + { + const float l1 = pE1->m_c[0] * .2126f + pE1->m_c[1] * .7152f + pE1->m_c[2] * .0722f; + const float cr1 = pE1->m_c[0] - l1; + const float cb1 = pE1->m_c[2] - l1; + + const float l2 = pE2->m_c[0] * .2126f + pE2->m_c[1] * .7152f + pE2->m_c[2] * .0722f; + const float cr2 = pE2->m_c[0] - l2; + const float cb2 = pE2->m_c[2] - l2; + + float dl = l1 - l2; + float dcr = cr1 - cr2; + float dcb = cb1 - cb2; + + return (int64_t)(weights[0] * (dl * dl) + weights[1] * pr_weight * (dcr * dcr) + weights[2] * pb_weight * (dcb * dcb)); + } + else + { + float dr = (float)pE1->m_c[0] - (float)pE2->m_c[0]; + float dg = (float)pE1->m_c[1] - (float)pE2->m_c[1]; + float db = (float)pE1->m_c[2] - (float)pE2->m_c[2]; + + return (int64_t)(weights[0] * dr * dr + weights[1] * dg * dg + weights[2] * db * db); + } +} + +static inline uint64_t compute_color_distance_rgba(const color_quad_i * pE1, const color_quad_i * pE2, bool perceptual, const uint32_t weights[4]) +{ + float da = (float)pE1->m_c[3] - (float)pE2->m_c[3]; + float a_err = weights[3] * (da * da); + + if (perceptual) + { + const float l1 = pE1->m_c[0] * .2126f + pE1->m_c[1] * .7152f + pE1->m_c[2] * .0722f; + const float cr1 = pE1->m_c[0] - l1; + const float cb1 = pE1->m_c[2] - l1; + + const float l2 = pE2->m_c[0] * .2126f + pE2->m_c[1] * .7152f + pE2->m_c[2] * .0722f; + const float cr2 = pE2->m_c[0] - l2; + const float cb2 = pE2->m_c[2] - l2; + + float dl = l1 - l2; + float dcr = cr1 - cr2; + float dcb = cb1 - cb2; + + return (int64_t)(weights[0] * (dl * dl) + weights[1] * pr_weight * (dcr * dcr) + weights[2] * pb_weight * (dcb * dcb) + a_err); + } + else + { + float dr = (float)pE1->m_c[0] - (float)pE2->m_c[0]; + float dg = (float)pE1->m_c[1] - (float)pE2->m_c[1]; + float db = (float)pE1->m_c[2] - (float)pE2->m_c[2]; + + return (int64_t)(weights[0] * dr * dr + weights[1] * dg * dg + weights[2] * db * db + a_err); + } +} + +static uint64_t pack_mode1_to_one_color(const color_cell_compressor_params * pParams, color_cell_compressor_results * pResults, uint32_t r, uint32_t g, uint32_t b, + int * pSelectors, uint32_t num_pixels, const color_quad_i * pPixels) +{ + uint32_t best_err = UINT_MAX; + uint32_t best_p = 0; + + for ( uint32_t p = 0; p < 2; p++) + { + uint32_t err = g_bc7_mode_1_optimal_endpoints[r][p].m_error + g_bc7_mode_1_optimal_endpoints[g][p].m_error + g_bc7_mode_1_optimal_endpoints[b][p].m_error; + if (err < best_err) + { + best_err = err; + best_p = p; + } + } + + const endpoint_err *pEr = &g_bc7_mode_1_optimal_endpoints[r][best_p]; + const endpoint_err *pEg = &g_bc7_mode_1_optimal_endpoints[g][best_p]; + const endpoint_err *pEb = &g_bc7_mode_1_optimal_endpoints[b][best_p]; + + color_quad_i_set(&pResults->m_low_endpoint, pEr->m_lo, pEg->m_lo, pEb->m_lo, 0); + color_quad_i_set(&pResults->m_high_endpoint, pEr->m_hi, pEg->m_hi, pEb->m_hi, 0); + pResults->m_pbits[0] = best_p; + pResults->m_pbits[1] = 0; + + for ( uint32_t i = 0; i < num_pixels; i++) + pSelectors[i] = BC7E_MODE_1_OPTIMAL_INDEX; + + color_quad_i p; + + for ( uint32_t i = 0; i < 3; i++) + { + uint32_t low = ((pResults->m_low_endpoint.m_c[i] << 1) | pResults->m_pbits[0]) << 1; + low |= (low >> 7); + + uint32_t high = ((pResults->m_high_endpoint.m_c[i] << 1) | pResults->m_pbits[0]) << 1; + high |= (high >> 7); + + p.m_c[i] = (low * (64 - g_bc7_weights3[BC7E_MODE_1_OPTIMAL_INDEX]) + high * g_bc7_weights3[BC7E_MODE_1_OPTIMAL_INDEX] + 32) >> 6; + } + + p.m_c[3] = 255; + + uint64_t total_err = 0; + for ( uint32_t i = 0; i < num_pixels; i++) + total_err += compute_color_distance_rgb(&p, &pPixels[i], pParams->m_perceptual, pParams->m_weights); + + pResults->m_best_overall_err = total_err; + + return total_err; +} + +static uint64_t pack_mode24_to_one_color(const color_cell_compressor_params * pParams, color_cell_compressor_results * pResults, uint32_t r, uint32_t g, uint32_t b, + int * pSelectors, uint32_t num_pixels, const color_quad_i * pPixels) +{ + uint32_t er, eg, eb; + + if (pParams->m_num_selector_weights == 8) + { + er = g_bc7_mode_4_optimal_endpoints3[r]; + eg = g_bc7_mode_4_optimal_endpoints3[g]; + eb = g_bc7_mode_4_optimal_endpoints3[b]; + } + else + { + er = g_bc7_mode_4_optimal_endpoints2[r]; + eg = g_bc7_mode_4_optimal_endpoints2[g]; + eb = g_bc7_mode_4_optimal_endpoints2[b]; + } + + color_quad_i_set(&pResults->m_low_endpoint, er & 0xFF, eg & 0xFF, eb & 0xFF, 0); + color_quad_i_set(&pResults->m_high_endpoint, er >> 8, eg >> 8, eb >> 8, 0); + + for ( uint32_t i = 0; i < num_pixels; i++) + pSelectors[i] = (pParams->m_num_selector_weights == 8) ? BC7E_MODE_4_OPTIMAL_INDEX3 : BC7E_MODE_4_OPTIMAL_INDEX2; + + color_quad_i p; + + for ( uint32_t i = 0; i < 3; i++) + { + uint32_t low = pResults->m_low_endpoint.m_c[i] << 3; + low |= (low >> 5); + + uint32_t high = pResults->m_high_endpoint.m_c[i] << 3; + high |= (high >> 5); + + if (pParams->m_num_selector_weights == 8) + p.m_c[i] = (low * (64 - g_bc7_weights3[BC7E_MODE_4_OPTIMAL_INDEX3]) + high * g_bc7_weights3[BC7E_MODE_4_OPTIMAL_INDEX3] + 32) >> 6; + else + p.m_c[i] = (low * (64 - g_bc7_weights2[BC7E_MODE_4_OPTIMAL_INDEX2]) + high * g_bc7_weights2[BC7E_MODE_4_OPTIMAL_INDEX2] + 32) >> 6; + } + + p.m_c[3] = 255; + + uint64_t total_err = 0; + for ( uint32_t i = 0; i < num_pixels; i++) + total_err += compute_color_distance_rgb(&p, &pPixels[i], pParams->m_perceptual, pParams->m_weights); + + pResults->m_best_overall_err = total_err; + + return total_err; +} + +static uint64_t pack_mode0_to_one_color(const color_cell_compressor_params * pParams, color_cell_compressor_results * pResults, uint32_t r, uint32_t g, uint32_t b, + int * pSelectors, uint32_t num_pixels, const color_quad_i * pPixels) +{ + uint32_t best_err = UINT_MAX; + uint32_t best_p = 0; + + for ( uint32_t p = 0; p < 4; p++) + { + uint32_t err = g_bc7_mode_0_optimal_endpoints[r][p >> 1][p & 1].m_error + g_bc7_mode_0_optimal_endpoints[g][p >> 1][p & 1].m_error + g_bc7_mode_0_optimal_endpoints[b][p >> 1][p & 1].m_error; + if (err < best_err) + { + best_err = err; + best_p = p; + } + } + + const endpoint_err *pEr = &g_bc7_mode_0_optimal_endpoints[r][best_p >> 1][best_p & 1]; + const endpoint_err *pEg = &g_bc7_mode_0_optimal_endpoints[g][best_p >> 1][best_p & 1]; + const endpoint_err *pEb = &g_bc7_mode_0_optimal_endpoints[b][best_p >> 1][best_p & 1]; + + color_quad_i_set(&pResults->m_low_endpoint, pEr->m_lo, pEg->m_lo, pEb->m_lo, 0); + + color_quad_i_set(&pResults->m_high_endpoint, pEr->m_hi, pEg->m_hi, pEb->m_hi, 0); + + pResults->m_pbits[0] = best_p & 1; + pResults->m_pbits[1] = best_p >> 1; + + for ( uint32_t i = 0; i < num_pixels; i++) + pSelectors[i] = BC7E_MODE_0_OPTIMAL_INDEX; + + color_quad_i p; + + for ( uint32_t i = 0; i < 3; i++) + { + uint32_t low = ((pResults->m_low_endpoint.m_c[i] << 1) | pResults->m_pbits[0]) << 3; + low |= (low >> 5); + + uint32_t high = ((pResults->m_high_endpoint.m_c[i] << 1) | pResults->m_pbits[1]) << 3; + high |= (high >> 5); + + p.m_c[i] = (low * (64 - g_bc7_weights3[BC7E_MODE_0_OPTIMAL_INDEX]) + high * g_bc7_weights3[BC7E_MODE_0_OPTIMAL_INDEX] + 32) >> 6; + } + + p.m_c[3] = 255; + + uint64_t total_err = 0; + for ( uint32_t i = 0; i < num_pixels; i++) + total_err += compute_color_distance_rgb(&p, &pPixels[i], pParams->m_perceptual, pParams->m_weights); + + pResults->m_best_overall_err = total_err; + + return total_err; +} + +static uint64_t pack_mode6_to_one_color(const color_cell_compressor_params * pParams, color_cell_compressor_results * pResults, uint32_t r, uint32_t g, uint32_t b, uint32_t a, + int * pSelectors, uint32_t num_pixels, const color_quad_i * pPixels) +{ + uint32_t best_err = UINT_MAX; + uint32_t best_p = 0; + + for ( uint32_t p = 0; p < 4; p++) + { + uint32_t hi_p = p >> 1; + uint32_t lo_p = p & 1; + uint32_t err = g_bc7_mode_6_optimal_endpoints[r][hi_p][lo_p].m_error + g_bc7_mode_6_optimal_endpoints[g][hi_p][lo_p].m_error + g_bc7_mode_6_optimal_endpoints[b][hi_p][lo_p].m_error + g_bc7_mode_6_optimal_endpoints[a][hi_p][lo_p].m_error; + if (err < best_err) + { + best_err = err; + best_p = p; + } + } + + uint32_t best_hi_p = best_p >> 1; + uint32_t best_lo_p = best_p & 1; + + const endpoint_err *pEr = &g_bc7_mode_6_optimal_endpoints[r][best_hi_p][best_lo_p]; + const endpoint_err *pEg = &g_bc7_mode_6_optimal_endpoints[g][best_hi_p][best_lo_p]; + const endpoint_err *pEb = &g_bc7_mode_6_optimal_endpoints[b][best_hi_p][best_lo_p]; + const endpoint_err *pEa = &g_bc7_mode_6_optimal_endpoints[a][best_hi_p][best_lo_p]; + + color_quad_i_set(&pResults->m_low_endpoint, pEr->m_lo, pEg->m_lo, pEb->m_lo, pEa->m_lo); + + color_quad_i_set(&pResults->m_high_endpoint, pEr->m_hi, pEg->m_hi, pEb->m_hi, pEa->m_hi); + + pResults->m_pbits[0] = best_lo_p; + pResults->m_pbits[1] = best_hi_p; + + for ( uint32_t i = 0; i < num_pixels; i++) + pSelectors[i] = BC7E_MODE_6_OPTIMAL_INDEX; + + color_quad_i p; + + for ( uint32_t i = 0; i < 4; i++) + { + uint32_t low = (pResults->m_low_endpoint.m_c[i] << 1) | pResults->m_pbits[0]; + uint32_t high = (pResults->m_high_endpoint.m_c[i] << 1) | pResults->m_pbits[1]; + + p.m_c[i] = (low * (64 - g_bc7_weights4[BC7E_MODE_6_OPTIMAL_INDEX]) + high * g_bc7_weights4[BC7E_MODE_6_OPTIMAL_INDEX] + 32) >> 6; + } + + uint64_t total_err = 0; + for ( uint32_t i = 0; i < num_pixels; i++) + total_err += compute_color_distance_rgba(&p, &pPixels[i], pParams->m_perceptual, pParams->m_weights); + + pResults->m_best_overall_err = total_err; + + return total_err; +} + +static uint64_t pack_mode7_to_one_color(const color_cell_compressor_params * pParams, color_cell_compressor_results * pResults, uint32_t r, uint32_t g, uint32_t b, uint32_t a, + int * pSelectors, uint32_t num_pixels, const color_quad_i * pPixels) +{ + uint32_t best_err = UINT_MAX; + uint32_t best_p = 0; + + for ( uint32_t p = 0; p < 4; p++) + { + uint32_t hi_p = p >> 1; + uint32_t lo_p = p & 1; + uint32_t err = g_bc7_mode_7_optimal_endpoints[r][hi_p][lo_p].m_error + g_bc7_mode_7_optimal_endpoints[g][hi_p][lo_p].m_error + g_bc7_mode_7_optimal_endpoints[b][hi_p][lo_p].m_error + g_bc7_mode_7_optimal_endpoints[a][hi_p][lo_p].m_error; + if (err < best_err) + { + best_err = err; + best_p = p; + } + } + + uint32_t best_hi_p = best_p >> 1; + uint32_t best_lo_p = best_p & 1; + + const endpoint_err *pEr = &g_bc7_mode_7_optimal_endpoints[r][best_hi_p][best_lo_p]; + const endpoint_err *pEg = &g_bc7_mode_7_optimal_endpoints[g][best_hi_p][best_lo_p]; + const endpoint_err *pEb = &g_bc7_mode_7_optimal_endpoints[b][best_hi_p][best_lo_p]; + const endpoint_err *pEa = &g_bc7_mode_7_optimal_endpoints[a][best_hi_p][best_lo_p]; + + color_quad_i_set(&pResults->m_low_endpoint, pEr->m_lo, pEg->m_lo, pEb->m_lo, pEa->m_lo); + + color_quad_i_set(&pResults->m_high_endpoint, pEr->m_hi, pEg->m_hi, pEb->m_hi, pEa->m_hi); + + pResults->m_pbits[0] = best_lo_p; + pResults->m_pbits[1] = best_hi_p; + + for ( uint32_t i = 0; i < num_pixels; i++) + pSelectors[i] = BC7E_MODE_7_OPTIMAL_INDEX; + + color_quad_i p; + + for ( uint32_t i = 0; i < 4; i++) + { + uint32_t low = (pResults->m_low_endpoint.m_c[i] << 1) | pResults->m_pbits[0]; + uint32_t high = (pResults->m_high_endpoint.m_c[i] << 1) | pResults->m_pbits[1]; + + p.m_c[i] = (low * (64 - g_bc7_weights2[BC7E_MODE_7_OPTIMAL_INDEX]) + high * g_bc7_weights2[BC7E_MODE_7_OPTIMAL_INDEX] + 32) >> 6; + } + + uint64_t total_err = 0; + for ( uint32_t i = 0; i < num_pixels; i++) + total_err += compute_color_distance_rgba(&p, &pPixels[i], pParams->m_perceptual, pParams->m_weights); + + pResults->m_best_overall_err = total_err; + + return total_err; +} + +static uint64_t evaluate_solution(const color_quad_i * pLow, const color_quad_i * pHigh, const uint32_t * pbits, + const color_cell_compressor_params * pParams, color_cell_compressor_results * pResults, uint32_t num_pixels, const color_quad_i * pPixels) +{ + color_quad_i quantMinColor = *pLow; + color_quad_i quantMaxColor = *pHigh; + + if (pParams->m_has_pbits) + { + uint32_t minPBit, maxPBit; + + if (pParams->m_endpoints_share_pbit) + maxPBit = minPBit = pbits[0]; + else + { + minPBit = pbits[0]; + maxPBit = pbits[1]; + } + + quantMinColor.m_c[0] = (pLow->m_c[0] << 1) | minPBit; + quantMinColor.m_c[1] = (pLow->m_c[1] << 1) | minPBit; + quantMinColor.m_c[2] = (pLow->m_c[2] << 1) | minPBit; + quantMinColor.m_c[3] = (pLow->m_c[3] << 1) | minPBit; + + quantMaxColor.m_c[0] = (pHigh->m_c[0] << 1) | maxPBit; + quantMaxColor.m_c[1] = (pHigh->m_c[1] << 1) | maxPBit; + quantMaxColor.m_c[2] = (pHigh->m_c[2] << 1) | maxPBit; + quantMaxColor.m_c[3] = (pHigh->m_c[3] << 1) | maxPBit; + } + + color_quad_i actualMinColor = scale_color(&quantMinColor, pParams); + color_quad_i actualMaxColor = scale_color(&quantMaxColor, pParams); + + const uint32_t N = pParams->m_num_selector_weights; + const uint32_t nc = pParams->m_has_alpha ? 4 : 3; + + float total_errf = 0; + + float wr = (float)(pParams->m_weights[0]); + float wg = (float)(pParams->m_weights[1]); + float wb = (float)(pParams->m_weights[2]); + float wa = (float)(pParams->m_weights[3]); + + color_quad_f weightedColors[16]; + weightedColors[0].m_c[0] = (float)(actualMinColor.m_c[0]); + weightedColors[0].m_c[1] = (float)(actualMinColor.m_c[1]); + weightedColors[0].m_c[2] = (float)(actualMinColor.m_c[2]); + weightedColors[0].m_c[3] = (float)(actualMinColor.m_c[3]); + + weightedColors[N - 1].m_c[0] = (float)(actualMaxColor.m_c[0]); + weightedColors[N - 1].m_c[1] = (float)(actualMaxColor.m_c[1]); + weightedColors[N - 1].m_c[2] = (float)(actualMaxColor.m_c[2]); + weightedColors[N - 1].m_c[3] = (float)(actualMaxColor.m_c[3]); + + for ( uint32_t i = 1; i < (N - 1); i++) + for ( uint32_t j = 0; j < nc; j++) + weightedColors[i].m_c[j] = floor((weightedColors[0].m_c[j] * (64.0f - pParams->m_pSelector_weights[i]) + weightedColors[N - 1].m_c[j] * pParams->m_pSelector_weights[i] + 32) * (1.0f / 64.0f)); + + if (!pParams->m_perceptual) + { + if (!pParams->m_has_alpha) + { + if (N == 16) + { + float lr = (float)(actualMinColor.m_c[0]); + float lg = (float)(actualMinColor.m_c[1]); + float lb = (float)(actualMinColor.m_c[2]); + + float dr = actualMaxColor.m_c[0] - lr; + float dg = actualMaxColor.m_c[1] - lg; + float db = actualMaxColor.m_c[2] - lb; + + const float f = N / (dr * dr + dg * dg + db * db + BC7E_DENOM_BIAS); + + lr *= -dr; + lg *= -dg; + lb *= -db; + + cfor ( uint32_t i = 0; i < num_pixels; i++) + { + const color_quad_i * pC = &pPixels[i]; + float r = (float)(pC->m_c[0]); + float g = (float)(pC->m_c[1]); + float b = (float)(pC->m_c[2]); + + float best_sel = floor(((r * dr + lr) + (g * dg + lg) + (b * db + lb)) * f + .5f); + best_sel = clamp(best_sel, (float)1, (float)(N - 1)); + + float best_sel0 = best_sel - 1; + + float dr0 = weightedColors[(int)best_sel0].m_c[0] - r; + + float dg0 = weightedColors[(int)best_sel0].m_c[1] - g; + + float db0 = weightedColors[(int)best_sel0].m_c[2] - b; + + float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0; + + float dr1 = weightedColors[(int)best_sel].m_c[0] - r; + + float dg1 = weightedColors[(int)best_sel].m_c[1] - g; + + float db1 = weightedColors[(int)best_sel].m_c[2] - b; + + float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1; + + float min_err = min(err0, err1); + total_errf += min_err; + pResults->m_pSelectors_temp[i] = (int)select(min_err == err0, best_sel0, best_sel); + } + } + else if (N == 8) + { + cfor ( uint32_t i = 0; i < num_pixels; i++) + { + float pr = (float)pPixels[i].m_c[0]; + float pg = (float)pPixels[i].m_c[1]; + float pb = (float)pPixels[i].m_c[2]; + + float best_err; + int best_sel; + + { + float dr0 = weightedColors[0].m_c[0] - pr; + float dg0 = weightedColors[0].m_c[1] - pg; + float db0 = weightedColors[0].m_c[2] - pb; + float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0; + + float dr1 = weightedColors[1].m_c[0] - pr; + float dg1 = weightedColors[1].m_c[1] - pg; + float db1 = weightedColors[1].m_c[2] - pb; + float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1; + + float dr2 = weightedColors[2].m_c[0] - pr; + float dg2 = weightedColors[2].m_c[1] - pg; + float db2 = weightedColors[2].m_c[2] - pb; + float err2 = wr * dr2 * dr2 + wg * dg2 * dg2 + wb * db2 * db2; + + float dr3 = weightedColors[3].m_c[0] - pr; + float dg3 = weightedColors[3].m_c[1] - pg; + float db3 = weightedColors[3].m_c[2] - pb; + float err3 = wr * dr3 * dr3 + wg * dg3 * dg3 + wb * db3 * db3; + + best_err = min(min(min(err0, err1), err2), err3); + + best_sel = select(best_err == err1, 1, 0); + best_sel = select(best_err == err2, 2, best_sel); + best_sel = select(best_err == err3, 3, best_sel); + } + + { + float dr0 = weightedColors[4].m_c[0] - pr; + float dg0 = weightedColors[4].m_c[1] - pg; + float db0 = weightedColors[4].m_c[2] - pb; + float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0; + + float dr1 = weightedColors[5].m_c[0] - pr; + float dg1 = weightedColors[5].m_c[1] - pg; + float db1 = weightedColors[5].m_c[2] - pb; + float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1; + + float dr2 = weightedColors[6].m_c[0] - pr; + float dg2 = weightedColors[6].m_c[1] - pg; + float db2 = weightedColors[6].m_c[2] - pb; + float err2 = wr * dr2 * dr2 + wg * dg2 * dg2 + wb * db2 * db2; + + float dr3 = weightedColors[7].m_c[0] - pr; + float dg3 = weightedColors[7].m_c[1] - pg; + float db3 = weightedColors[7].m_c[2] - pb; + float err3 = wr * dr3 * dr3 + wg * dg3 * dg3 + wb * db3 * db3; + + best_err = min(best_err, min(min(min(err0, err1), err2), err3)); + + best_sel = select(best_err == err0, 4, best_sel); + best_sel = select(best_err == err1, 5, best_sel); + best_sel = select(best_err == err2, 6, best_sel); + best_sel = select(best_err == err3, 7, best_sel); + } + + total_errf += best_err; + + pResults->m_pSelectors_temp[i] = best_sel; + } + } + else // if (N == 4) + { + cfor ( uint32_t i = 0; i < num_pixels; i++) + { + float pr = (float)pPixels[i].m_c[0]; + float pg = (float)pPixels[i].m_c[1]; + float pb = (float)pPixels[i].m_c[2]; + + float dr0 = weightedColors[0].m_c[0] - pr; + float dg0 = weightedColors[0].m_c[1] - pg; + float db0 = weightedColors[0].m_c[2] - pb; + float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0; + + float dr1 = weightedColors[1].m_c[0] - pr; + float dg1 = weightedColors[1].m_c[1] - pg; + float db1 = weightedColors[1].m_c[2] - pb; + float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1; + + float dr2 = weightedColors[2].m_c[0] - pr; + float dg2 = weightedColors[2].m_c[1] - pg; + float db2 = weightedColors[2].m_c[2] - pb; + float err2 = wr * dr2 * dr2 + wg * dg2 * dg2 + wb * db2 * db2; + + float dr3 = weightedColors[3].m_c[0] - pr; + float dg3 = weightedColors[3].m_c[1] - pg; + float db3 = weightedColors[3].m_c[2] - pb; + float err3 = wr * dr3 * dr3 + wg * dg3 * dg3 + wb * db3 * db3; + + float best_err = min(min(min(err0, err1), err2), err3); + + int best_sel = select(best_err == err1, 1, 0); + best_sel = select(best_err == err2, 2, best_sel); + best_sel = select(best_err == err3, 3, best_sel); + + total_errf += best_err; + + pResults->m_pSelectors_temp[i] = best_sel; + } + } + } + else + { + // alpha + if (N == 16) + { + float lr = (float)(actualMinColor.m_c[0]); + float lg = (float)(actualMinColor.m_c[1]); + float lb = (float)(actualMinColor.m_c[2]); + float la = (float)(actualMinColor.m_c[3]); + + float dr = actualMaxColor.m_c[0] - lr; + float dg = actualMaxColor.m_c[1] - lg; + float db = actualMaxColor.m_c[2] - lb; + float da = actualMaxColor.m_c[3] - la; + + const float f = N / (dr * dr + dg * dg + db * db + da * da + BC7E_DENOM_BIAS); + + lr *= -dr; + lg *= -dg; + lb *= -db; + la *= -da; + + cfor ( uint32_t i = 0; i < num_pixels; i++) + { + const color_quad_i * pC = &pPixels[i]; + float r = (float)(pC->m_c[0]); + float g = (float)(pC->m_c[1]); + float b = (float)(pC->m_c[2]); + float a = (float)(pC->m_c[3]); + + float best_sel = floor(((r * dr + lr) + (g * dg + lg) + (b * db + lb) + (a * da + la)) * f + .5f); + best_sel = clamp(best_sel, (float)1, (float)(N - 1)); + + float best_sel0 = best_sel - 1; + + float dr0 = weightedColors[(int)best_sel0].m_c[0] - r; + float dg0 = weightedColors[(int)best_sel0].m_c[1] - g; + float db0 = weightedColors[(int)best_sel0].m_c[2] - b; + float da0 = weightedColors[(int)best_sel0].m_c[3] - a; + float err0 = (wr * dr0 * dr0) + (wg * dg0 * dg0) + (wb * db0 * db0) + (wa * da0 * da0); + + float dr1 = weightedColors[(int)best_sel].m_c[0] - r; + float dg1 = weightedColors[(int)best_sel].m_c[1] - g; + float db1 = weightedColors[(int)best_sel].m_c[2] - b; + float da1 = weightedColors[(int)best_sel].m_c[3] - a; + + float err1 = (wr * dr1 * dr1) + (wg * dg1 * dg1) + (wb * db1 * db1) + (wa * da1 * da1); + + float min_err = min(err0, err1); + total_errf += min_err; + pResults->m_pSelectors_temp[i] = (int)select(min_err == err0, best_sel0, best_sel); + } + } + else if (N == 8) + { + cfor ( uint32_t i = 0; i < num_pixels; i++) + { + float pr = (float)pPixels[i].m_c[0]; + float pg = (float)pPixels[i].m_c[1]; + float pb = (float)pPixels[i].m_c[2]; + float pa = (float)pPixels[i].m_c[3]; + + float best_err; + int best_sel; + + { + float dr0 = weightedColors[0].m_c[0] - pr; + float dg0 = weightedColors[0].m_c[1] - pg; + float db0 = weightedColors[0].m_c[2] - pb; + float da0 = weightedColors[0].m_c[3] - pa; + float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0 + wa * da0 * da0; + + float dr1 = weightedColors[1].m_c[0] - pr; + float dg1 = weightedColors[1].m_c[1] - pg; + float db1 = weightedColors[1].m_c[2] - pb; + float da1 = weightedColors[1].m_c[3] - pa; + float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1 + wa * da1 * da1; + + float dr2 = weightedColors[2].m_c[0] - pr; + float dg2 = weightedColors[2].m_c[1] - pg; + float db2 = weightedColors[2].m_c[2] - pb; + float da2 = weightedColors[2].m_c[3] - pa; + float err2 = wr * dr2 * dr2 + wg * dg2 * dg2 + wb * db2 * db2 + wa * da2 * da2; + + float dr3 = weightedColors[3].m_c[0] - pr; + float dg3 = weightedColors[3].m_c[1] - pg; + float db3 = weightedColors[3].m_c[2] - pb; + float da3 = weightedColors[3].m_c[3] - pa; + float err3 = wr * dr3 * dr3 + wg * dg3 * dg3 + wb * db3 * db3 + wa * da3 * da3; + + best_err = min(min(min(err0, err1), err2), err3); + + best_sel = select(best_err == err1, 1, 0); + best_sel = select(best_err == err2, 2, best_sel); + best_sel = select(best_err == err3, 3, best_sel); + } + + { + float dr0 = weightedColors[4].m_c[0] - pr; + float dg0 = weightedColors[4].m_c[1] - pg; + float db0 = weightedColors[4].m_c[2] - pb; + float da0 = weightedColors[4].m_c[3] - pa; + float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0 + wa * da0 * da0; + + float dr1 = weightedColors[5].m_c[0] - pr; + float dg1 = weightedColors[5].m_c[1] - pg; + float db1 = weightedColors[5].m_c[2] - pb; + float da1 = weightedColors[5].m_c[3] - pa; + float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1 + wa * da1 * da1; + + float dr2 = weightedColors[6].m_c[0] - pr; + float dg2 = weightedColors[6].m_c[1] - pg; + float db2 = weightedColors[6].m_c[2] - pb; + float da2 = weightedColors[6].m_c[3] - pa; + float err2 = wr * dr2 * dr2 + wg * dg2 * dg2 + wb * db2 * db2 + wa * da2 * da2; + + float dr3 = weightedColors[7].m_c[0] - pr; + float dg3 = weightedColors[7].m_c[1] - pg; + float db3 = weightedColors[7].m_c[2] - pb; + float da3 = weightedColors[7].m_c[3] - pa; + float err3 = wr * dr3 * dr3 + wg * dg3 * dg3 + wb * db3 * db3 + wa * da3 * da3; + + best_err = min(best_err, min(min(min(err0, err1), err2), err3)); + + best_sel = select(best_err == err0, 4, best_sel); + best_sel = select(best_err == err1, 5, best_sel); + best_sel = select(best_err == err2, 6, best_sel); + best_sel = select(best_err == err3, 7, best_sel); + } + + total_errf += best_err; + + pResults->m_pSelectors_temp[i] = best_sel; + } + } + else // if (N == 4) + { + cfor ( uint32_t i = 0; i < num_pixels; i++) + { + float pr = (float)pPixels[i].m_c[0]; + float pg = (float)pPixels[i].m_c[1]; + float pb = (float)pPixels[i].m_c[2]; + float pa = (float)pPixels[i].m_c[3]; + + float dr0 = weightedColors[0].m_c[0] - pr; + float dg0 = weightedColors[0].m_c[1] - pg; + float db0 = weightedColors[0].m_c[2] - pb; + float da0 = weightedColors[0].m_c[3] - pa; + float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0 + wa * da0 * da0; + + float dr1 = weightedColors[1].m_c[0] - pr; + float dg1 = weightedColors[1].m_c[1] - pg; + float db1 = weightedColors[1].m_c[2] - pb; + float da1 = weightedColors[1].m_c[3] - pa; + float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1 + wa * da1 * da1; + + float dr2 = weightedColors[2].m_c[0] - pr; + float dg2 = weightedColors[2].m_c[1] - pg; + float db2 = weightedColors[2].m_c[2] - pb; + float da2 = weightedColors[2].m_c[3] - pa; + float err2 = wr * dr2 * dr2 + wg * dg2 * dg2 + wb * db2 * db2 + wa * da2 * da2; + + float dr3 = weightedColors[3].m_c[0] - pr; + float dg3 = weightedColors[3].m_c[1] - pg; + float db3 = weightedColors[3].m_c[2] - pb; + float da3 = weightedColors[3].m_c[3] - pa; + float err3 = wr * dr3 * dr3 + wg * dg3 * dg3 + wb * db3 * db3 + wa * da3 * da3; + + float best_err = min(min(min(err0, err1), err2), err3); + + int best_sel = select(best_err == err1, 1, 0); + best_sel = select(best_err == err2, 2, best_sel); + best_sel = select(best_err == err3, 3, best_sel); + + total_errf += best_err; + + pResults->m_pSelectors_temp[i] = best_sel; + } + } + } + } + else + { + wg *= pr_weight; + wb *= pb_weight; + + float weightedColorsY[16], weightedColorsCr[16], weightedColorsCb[16]; + + for ( uint32_t i = 0; i < N; i++) + { + float r = weightedColors[i].m_c[0]; + float g = weightedColors[i].m_c[1]; + float b = weightedColors[i].m_c[2]; + + float y = r * .2126f + g * .7152f + b * .0722f; + + weightedColorsY[i] = y; + weightedColorsCr[i] = r - y; + weightedColorsCb[i] = b - y; + } + + if (pParams->m_has_alpha) + { + for ( uint32_t i = 0; i < num_pixels; i++) + { + float r = (float)(pPixels[i].m_c[0]); + float g = (float)(pPixels[i].m_c[1]); + float b = (float)(pPixels[i].m_c[2]); + float a = (float)(pPixels[i].m_c[3]); + + float y = r * .2126f + g * .7152f + b * .0722f; + float cr = r - y; + float cb = b - y; + + float best_err = 1e+10f; + int32_t best_sel = 0; + + for ( uint32_t j = 0; j < N; j++) + { + float dl = y - weightedColorsY[j]; + float dcr = cr - weightedColorsCr[j]; + float dcb = cb - weightedColorsCb[j]; + float da = a - weightedColors[j].m_c[3]; + + float err = (wr * dl * dl) + (wg * dcr * dcr) + (wb * dcb * dcb) + (wa * da * da); + if (err < best_err) + { + best_err = err; + best_sel = j; + } + } + + total_errf += best_err; + + pResults->m_pSelectors_temp[i] = best_sel; + } + } + else + { + for ( uint32_t i = 0; i < num_pixels; i++) + { + float r = (float)(pPixels[i].m_c[0]); + float g = (float)(pPixels[i].m_c[1]); + float b = (float)(pPixels[i].m_c[2]); + + float y = r * .2126f + g * .7152f + b * .0722f; + float cr = r - y; + float cb = b - y; + + float best_err = 1e+10f; + int32_t best_sel = 0; + + for ( uint32_t j = 0; j < N; j++) + { + float dl = y - weightedColorsY[j]; + float dcr = cr - weightedColorsCr[j]; + float dcb = cb - weightedColorsCb[j]; + + float err = (wr * dl * dl) + (wg * dcr * dcr) + (wb * dcb * dcb); + if (err < best_err) + { + best_err = err; + best_sel = j; + } + } + + total_errf += best_err; + + pResults->m_pSelectors_temp[i] = best_sel; + } + } + } + + uint64_t total_err = (int64)total_errf; + + if (total_err < pResults->m_best_overall_err) + { + pResults->m_best_overall_err = total_err; + + pResults->m_low_endpoint = *pLow; + pResults->m_high_endpoint = *pHigh; + + pResults->m_pbits[0] = pbits[0]; + pResults->m_pbits[1] = pbits[1]; + + for ( uint32_t i = 0; i < num_pixels; i++) + pResults->m_pSelectors[i] = pResults->m_pSelectors_temp[i]; + } + + return total_err; +} + +static void fixDegenerateEndpoints( uint32_t mode, color_quad_i * pTrialMinColor, color_quad_i * pTrialMaxColor, const vec4F * pXl, const vec4F * pXh, uint32_t iscale) +{ + if ((mode == 1) || (mode == 4)) // also mode 2 + { + // fix degenerate case where the input collapses to a single colorspace voxel, and we loose all freedom (test with grayscale ramps) + for ( uint32_t i = 0; i < 3; i++) + { + if (pTrialMinColor->m_c[i] == pTrialMaxColor->m_c[i]) + { + if (abs(pXl->m_c[i] - pXh->m_c[i]) > 0.0f) + { + if (pTrialMinColor->m_c[i] > (int)(iscale >> 1)) + { + if (pTrialMinColor->m_c[i] > 0) + pTrialMinColor->m_c[i]--; + else + if (pTrialMaxColor->m_c[i] < (int)iscale) + pTrialMaxColor->m_c[i]++; + } + else + { + if (pTrialMaxColor->m_c[i] < (int)iscale) + pTrialMaxColor->m_c[i]++; + else if (pTrialMinColor->m_c[i] > 0) + pTrialMinColor->m_c[i]--; + } + + if (mode == 4) + { + if (pTrialMinColor->m_c[i] > (int)(iscale >> 1)) + { + if (pTrialMaxColor->m_c[i] < (int)iscale) + pTrialMaxColor->m_c[i]++; + else if (pTrialMinColor->m_c[i] > 0) + pTrialMinColor->m_c[i]--; + } + else + { + if (pTrialMinColor->m_c[i] > 0) + pTrialMinColor->m_c[i]--; + else if (pTrialMaxColor->m_c[i] < (int)iscale) + pTrialMaxColor->m_c[i]++; + } + } + } + } + } + } +} + +static uint64_t find_optimal_solution( uint32_t mode, vec4F * pXl, vec4F * pXh, const color_cell_compressor_params * pParams, color_cell_compressor_results * pResults, + bool pbit_search, uint32_t num_pixels, const color_quad_i * pPixels) +{ + vec4F xl = *pXl; + vec4F xh = *pXh; + + vec4F_saturate_in_place(&xl); + vec4F_saturate_in_place(&xh); + + if (pParams->m_has_pbits) + { + if (pbit_search) + { + // compensated rounding+pbit search + const int iscalep = (1 << (pParams->m_comp_bits + 1)) - 1; + const float scalep = (float)iscalep; + + const int32_t totalComps = pParams->m_has_alpha ? 4 : 3; + NOTE_UNUSED(totalComps); + + if (!pParams->m_endpoints_share_pbit) + { + color_quad_i lo[2], hi[2]; + + for ( int p = 0; p < 2; p++) + { + color_quad_i xMinColor, xMaxColor; + + // Notes: The pbit controls which quantization intervals are selected. + // total_levels=2^(comp_bits+1), where comp_bits=4 for mode 0, etc. + // pbit 0: v=(b*2)/(total_levels-1), pbit 1: v=(b*2+1)/(total_levels-1) where b is the component bin from [0,total_levels/2-1] and v is the [0,1] component value + // rearranging you get for pbit 0: b=floor(v*(total_levels-1)/2+.5) + // rearranging you get for pbit 1: b=floor((v*(total_levels-1)-1)/2+.5) + for ( uint32_t c = 0; c < 4; c++) + { + xMinColor.m_c[c] = (int)((xl.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p; + xMinColor.m_c[c] = clamp(xMinColor.m_c[c], p, iscalep - 1 + p); + + xMaxColor.m_c[c] = (int)((xh.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p; + xMaxColor.m_c[c] = clamp(xMaxColor.m_c[c], p, iscalep - 1 + p); + } + + lo[p] = xMinColor; + hi[p] = xMaxColor; + + for ( int c = 0; c < 4; c++) + { + lo[p].m_c[c] >>= 1; + hi[p].m_c[c] >>= 1; + } + } + + fixDegenerateEndpoints(mode, &lo[0], &hi[0], &xl, &xh, iscalep >> 1); + fixDegenerateEndpoints(mode, &lo[1], &hi[1], &xl, &xh, iscalep >> 1); + + uint32_t pbits[2]; + + pbits[0] = 0; pbits[1] = 0; + evaluate_solution(&lo[0], &hi[0], pbits, pParams, pResults, num_pixels, pPixels); + + pbits[0] = 0; pbits[1] = 1; + evaluate_solution(&lo[0], &hi[1], pbits, pParams, pResults, num_pixels, pPixels); + + pbits[0] = 1; pbits[1] = 0; + evaluate_solution(&lo[1], &hi[0], pbits, pParams, pResults, num_pixels, pPixels); + + pbits[0] = 1; pbits[1] = 1; + evaluate_solution(&lo[1], &hi[1], pbits, pParams, pResults, num_pixels, pPixels); + } + else + { + // Endpoints share pbits + color_quad_i lo[2], hi[2]; + + for ( int p = 0; p < 2; p++) + { + color_quad_i xMinColor, xMaxColor; + + for ( uint32_t c = 0; c < 4; c++) + { + xMinColor.m_c[c] = (int)((xl.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p; + xMinColor.m_c[c] = clamp(xMinColor.m_c[c], p, iscalep - 1 + p); + + xMaxColor.m_c[c] = (int)((xh.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p; + xMaxColor.m_c[c] = clamp(xMaxColor.m_c[c], p, iscalep - 1 + p); + } + + lo[p] = xMinColor; + hi[p] = xMaxColor; + + for ( int c = 0; c < 4; c++) + { + lo[p].m_c[c] >>= 1; + hi[p].m_c[c] >>= 1; + } + } + + fixDegenerateEndpoints(mode, &lo[0], &hi[0], &xl, &xh, iscalep >> 1); + fixDegenerateEndpoints(mode, &lo[1], &hi[1], &xl, &xh, iscalep >> 1); + + uint32_t pbits[2]; + + pbits[0] = 0; pbits[1] = 0; + evaluate_solution(&lo[0], &hi[0], pbits, pParams, pResults, num_pixels, pPixels); + + pbits[0] = 1; pbits[1] = 1; + evaluate_solution(&lo[1], &hi[1], pbits, pParams, pResults, num_pixels, pPixels); + } + } + else + { + // compensated rounding + const int iscalep = (1 << (pParams->m_comp_bits + 1)) - 1; + const float scalep = (float)iscalep; + + const int32_t totalComps = pParams->m_has_alpha ? 4 : 3; + + uint32_t best_pbits[2]; + color_quad_i bestMinColor, bestMaxColor; + + if (!pParams->m_endpoints_share_pbit) + { + float best_err0 = 1e+9; + float best_err1 = 1e+9; + + for ( int p = 0; p < 2; p++) + { + color_quad_i xMinColor, xMaxColor; + + // Notes: The pbit controls which quantization intervals are selected. + // total_levels=2^(comp_bits+1), where comp_bits=4 for mode 0, etc. + // pbit 0: v=(b*2)/(total_levels-1), pbit 1: v=(b*2+1)/(total_levels-1) where b is the component bin from [0,total_levels/2-1] and v is the [0,1] component value + // rearranging you get for pbit 0: b=floor(v*(total_levels-1)/2+.5) + // rearranging you get for pbit 1: b=floor((v*(total_levels-1)-1)/2+.5) + for ( uint32_t c = 0; c < 4; c++) + { + xMinColor.m_c[c] = (int)((xl.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p; + xMinColor.m_c[c] = clamp(xMinColor.m_c[c], p, iscalep - 1 + p); + + xMaxColor.m_c[c] = (int)((xh.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p; + xMaxColor.m_c[c] = clamp(xMaxColor.m_c[c], p, iscalep - 1 + p); + } + + color_quad_i scaledLow = scale_color(&xMinColor, pParams); + color_quad_i scaledHigh = scale_color(&xMaxColor, pParams); + + float err0 = 0; + float err1 = 0; + for ( int i = 0; i < totalComps; i++) + { + err0 += square(scaledLow.m_c[i] - xl.m_c[i]*255.0f); + err1 += square(scaledHigh.m_c[i] - xh.m_c[i]*255.0f); + } + + if (err0 < best_err0) + { + best_err0 = err0; + best_pbits[0] = p; + + bestMinColor.m_c[0] = xMinColor.m_c[0] >> 1; + bestMinColor.m_c[1] = xMinColor.m_c[1] >> 1; + bestMinColor.m_c[2] = xMinColor.m_c[2] >> 1; + bestMinColor.m_c[3] = xMinColor.m_c[3] >> 1; + } + + if (err1 < best_err1) + { + best_err1 = err1; + best_pbits[1] = p; + + bestMaxColor.m_c[0] = xMaxColor.m_c[0] >> 1; + bestMaxColor.m_c[1] = xMaxColor.m_c[1] >> 1; + bestMaxColor.m_c[2] = xMaxColor.m_c[2] >> 1; + bestMaxColor.m_c[3] = xMaxColor.m_c[3] >> 1; + } + } + } + else + { + // Endpoints share pbits + float best_err = 1e+9; + + for ( int p = 0; p < 2; p++) + { + color_quad_i xMinColor, xMaxColor; + + for ( uint32_t c = 0; c < 4; c++) + { + xMinColor.m_c[c] = (int)((xl.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p; + xMinColor.m_c[c] = clamp(xMinColor.m_c[c], p, iscalep - 1 + p); + + xMaxColor.m_c[c] = (int)((xh.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p; + xMaxColor.m_c[c] = clamp(xMaxColor.m_c[c], p, iscalep - 1 + p); + } + + color_quad_i scaledLow = scale_color(&xMinColor, pParams); + color_quad_i scaledHigh = scale_color(&xMaxColor, pParams); + + float err = 0; + for ( int i = 0; i < totalComps; i++) + err += square((scaledLow.m_c[i]/255.0f) - xl.m_c[i]) + square((scaledHigh.m_c[i]/255.0f) - xh.m_c[i]); + + if (err < best_err) + { + best_err = err; + best_pbits[0] = p; + best_pbits[1] = p; + + bestMinColor.m_c[0] = xMinColor.m_c[0] >> 1; + bestMinColor.m_c[1] = xMinColor.m_c[1] >> 1; + bestMinColor.m_c[2] = xMinColor.m_c[2] >> 1; + bestMinColor.m_c[3] = xMinColor.m_c[3] >> 1; + + bestMaxColor.m_c[0] = xMaxColor.m_c[0] >> 1; + bestMaxColor.m_c[1] = xMaxColor.m_c[1] >> 1; + bestMaxColor.m_c[2] = xMaxColor.m_c[2] >> 1; + bestMaxColor.m_c[3] = xMaxColor.m_c[3] >> 1; + } + } + } + + fixDegenerateEndpoints(mode, &bestMinColor, &bestMaxColor, &xl, &xh, iscalep >> 1); + + if ((pResults->m_best_overall_err == UINT64_MAX) || color_quad_i_notequals(&bestMinColor, &pResults->m_low_endpoint) || color_quad_i_notequals(&bestMaxColor, &pResults->m_high_endpoint) || (best_pbits[0] != pResults->m_pbits[0]) || (best_pbits[1] != pResults->m_pbits[1])) + { + evaluate_solution(&bestMinColor, &bestMaxColor, best_pbits, pParams, pResults, num_pixels, pPixels); + } + } + } + else + { + const int iscale = (1 << pParams->m_comp_bits) - 1; + const float scale = (float)iscale; + + color_quad_i trialMinColor, trialMaxColor; + color_quad_i_set_clamped(&trialMinColor, (int)(xl.m_c[0] * scale + .5f), (int)(xl.m_c[1] * scale + .5f), (int)(xl.m_c[2] * scale + .5f), (int)(xl.m_c[3] * scale + .5f)); + color_quad_i_set_clamped(&trialMaxColor, (int)(xh.m_c[0] * scale + .5f), (int)(xh.m_c[1] * scale + .5f), (int)(xh.m_c[2] * scale + .5f), (int)(xh.m_c[3] * scale + .5f)); + + fixDegenerateEndpoints(mode, &trialMinColor, &trialMaxColor, &xl, &xh, iscale); + + if ((pResults->m_best_overall_err == UINT64_MAX) || color_quad_i_notequals(&trialMinColor, &pResults->m_low_endpoint) || color_quad_i_notequals(&trialMaxColor, &pResults->m_high_endpoint)) + { + uint32_t pbits[2]; + pbits[0] = 0; + pbits[1] = 0; + + evaluate_solution(&trialMinColor, &trialMaxColor, pbits, pParams, pResults, num_pixels, pPixels); + } + } + + return pResults->m_best_overall_err; +} + +// Note: In mode 6, m_has_alpha will only be true for transparent blocks. +static uint64_t color_cell_compression( uint32_t mode, const color_cell_compressor_params * pParams, color_cell_compressor_results * pResults, + const bc7e_compress_block_params * pComp_params, uint32_t num_pixels, const color_quad_i * pPixels, bool refinement) +{ + pResults->m_best_overall_err = UINT64_MAX; + pResults->m_used_lut = false; // set true below only if a one-color LUT result wins + + if ((mode != 6) && (mode != 7)) + { + assert(!pParams->m_has_alpha); + } + + if ((mode <= 2) || (mode == 4) || (mode >= 6)) + { + const uint32_t cr = pPixels[0].m_c[0]; + const uint32_t cg = pPixels[0].m_c[1]; + const uint32_t cb = pPixels[0].m_c[2]; + const uint32_t ca = pPixels[0].m_c[3]; + + bool allSame = true; + for ( uint32_t i = 1; i < num_pixels; i++) + { + if ((cr != (uint32_t)pPixels[i].m_c[0]) || (cg != (uint32_t)pPixels[i].m_c[1]) || (cb != (uint32_t)pPixels[i].m_c[2]) || (ca != (uint32_t)pPixels[i].m_c[3])) + { + allSame = false; + break; + } + } + + cif (allSame && pComp_params->m_use_luts) + { + pResults->m_used_lut = true; // solid/allSame subset -> one-color optimal-endpoint LUT + if (mode == 0) + return pack_mode0_to_one_color(pParams, pResults, cr, cg, cb, pResults->m_pSelectors, num_pixels, pPixels); + if (mode == 1) + return pack_mode1_to_one_color(pParams, pResults, cr, cg, cb, pResults->m_pSelectors, num_pixels, pPixels); + else if (mode == 6) + return pack_mode6_to_one_color(pParams, pResults, cr, cg, cb, ca, pResults->m_pSelectors, num_pixels, pPixels); + else if (mode == 7) + return pack_mode7_to_one_color(pParams, pResults, cr, cg, cb, ca, pResults->m_pSelectors, num_pixels, pPixels); + else + return pack_mode24_to_one_color(pParams, pResults, cr, cg, cb, pResults->m_pSelectors, num_pixels, pPixels); + } + } + + vec4F meanColor, axis; + vec4F_set_scalar(&meanColor, 0.0f); + + for ( uint32_t i = 0; i < num_pixels; i++) + { + vec4F color = vec4F_from_color(&pPixels[i]); + meanColor = vec4F_add(&meanColor, &color); + } + + vec4F meanColorScaled = vec4F_mul(&meanColor, 1.0f / (float)((int)num_pixels)); + + meanColor = vec4F_mul(&meanColor, 1.0f / (float)((int)num_pixels * 255.0f)); + vec4F_saturate_in_place(&meanColor); + + if (pParams->m_has_alpha) + { + vec4F v; + vec4F_set_scalar(&v, 0.0f); + cfor ( uint32_t i = 0; i < num_pixels; i++) + { + vec4F color = vec4F_from_color(&pPixels[i]); + color = vec4F_sub(&color, &meanColorScaled); + + vec4F a = vec4F_mul(&color, color.m_c[0]); + vec4F b = vec4F_mul(&color, color.m_c[1]); + vec4F c = vec4F_mul(&color, color.m_c[2]); + vec4F d = vec4F_mul(&color, color.m_c[3]); + + vec4F n = i ? v : color; + vec4F_normalize_in_place(&n); + + v.m_c[0] += vec4F_dot(&a, &n); + v.m_c[1] += vec4F_dot(&b, &n); + v.m_c[2] += vec4F_dot(&c, &n); + v.m_c[3] += vec4F_dot(&d, &n); + } + axis = v; + vec4F_normalize_in_place(&axis); + } + else + { + float cov[6]; + cov[0] = 0; cov[1] = 0; cov[2] = 0; + cov[3] = 0; cov[4] = 0; cov[5] = 0; + + cfor ( uint32_t i = 0; i < num_pixels; i++) + { + const color_quad_i * pV = &pPixels[i]; + + float r = pV->m_c[0] - meanColorScaled.m_c[0]; + float g = pV->m_c[1] - meanColorScaled.m_c[1]; + float b = pV->m_c[2] - meanColorScaled.m_c[2]; + + cov[0] += r*r; + cov[1] += r*g; + cov[2] += r*b; + cov[3] += g*g; + cov[4] += g*b; + cov[5] += b*b; + } + + float vfr, vfg, vfb; + //vfr = hi[0] - lo[0]; + //vfg = hi[1] - lo[1]; + //vfb = hi[2] - lo[2]; + // This is more stable. + vfr = .9f; + vfg = 1.0f; + vfb = .7f; + + for ( uint32_t iter = 0; iter < 3; iter++) + { + float r = vfr*cov[0] + vfg*cov[1] + vfb*cov[2]; + float g = vfr*cov[1] + vfg*cov[3] + vfb*cov[4]; + float b = vfr*cov[2] + vfg*cov[4] + vfb*cov[5]; + + float m = maximumf(maximumf(abs(r), abs(g)), abs(b)); + if (m > 1e-10f) + { + m = 1.0f / m; + r *= m; + g *= m; + b *= m; + } + + //float delta = square(vfr - r) + square(vfg - g) + square(vfb - b); + + vfr = r; + vfg = g; + vfb = b; + + //if ((iter > 1) && (delta < 1e-8f)) + // break; + } + + float len = vfr*vfr + vfg*vfg + vfb*vfb; + + if (len < 1e-10f) + vec4F_set_scalar(&axis, 0.0f); + else + { + len = 1.0f / sqrt(len); + vfr *= len; + vfg *= len; + vfb *= len; + vec4F_set(&axis, vfr, vfg, vfb, 0); + } + } + + cif (vec4F_dot(&axis, &axis) < .5f) + { + if (pParams->m_perceptual) + vec4F_set(&axis, .213f, .715f, .072f, pParams->m_has_alpha ? .715f : 0); + else + vec4F_set(&axis, 1.0f, 1.0f, 1.0f, pParams->m_has_alpha ? 1.0f : 0); + vec4F_normalize_in_place(&axis); + } + + float l = 1e+9f, h = -1e+9f; + + cfor ( uint32_t i = 0; i < num_pixels; i++) + { + vec4F color = vec4F_from_color(&pPixels[i]); + + vec4F q = vec4F_sub(&color, &meanColorScaled); + float d = vec4F_dot(&q, &axis); + + l = minimumf(l, d); + h = maximumf(h, d); + } + + l *= (1.0f / 255.0f); + h *= (1.0f / 255.0f); + + vec4F b0 = vec4F_mul(&axis, l); + vec4F b1 = vec4F_mul(&axis, h); + vec4F c0 = vec4F_add(&meanColor, &b0); + vec4F c1 = vec4F_add(&meanColor, &b1); + vec4F minColor = vec4F_saturate(&c0); + vec4F maxColor = vec4F_saturate(&c1); + + vec4F whiteVec; + vec4F_set_scalar(&whiteVec, 1.0f); + if (vec4F_dot(&minColor, &whiteVec) > vec4F_dot(&maxColor, &whiteVec)) + { + vec4F temp = minColor; + minColor = maxColor; + maxColor = temp; + } + + if (!find_optimal_solution(mode, &minColor, &maxColor, pParams, pResults, pComp_params->m_pbit_search, num_pixels, pPixels)) + return 0; + + if (!refinement) + return pResults->m_best_overall_err; + + for ( uint32_t i = 0; i < pComp_params->m_refinement_passes; i++) + { + vec4F xl, xh; + vec4F_set_scalar(&xl, 0.0f); + vec4F_set_scalar(&xh, 0.0f); + if (pParams->m_has_alpha) + compute_least_squares_endpoints_rgba(num_pixels, pResults->m_pSelectors, pParams->m_pSelector_weightsx, &xl, &xh, pPixels); + else + { + compute_least_squares_endpoints_rgb(num_pixels, pResults->m_pSelectors, pParams->m_pSelector_weightsx, &xl, &xh, pPixels); + + xl.m_c[3] = 255.0f; + xh.m_c[3] = 255.0f; + } + + xl = vec4F_mul(&xl, (1.0f / 255.0f)); + xh = vec4F_mul(&xh, (1.0f / 255.0f)); + + if (!find_optimal_solution(mode, &xl, &xh, pParams, pResults, pComp_params->m_pbit_search, num_pixels, pPixels)) + return 0; + } + + if (pComp_params->m_uber_level > 0) + { + int selectors_temp[16], selectors_temp1[16]; + for ( uint32_t i = 0; i < num_pixels; i++) + selectors_temp[i] = pResults->m_pSelectors[i]; + + const int max_selector = pParams->m_num_selector_weights - 1; + + uint32_t min_sel = 16; + uint32_t max_sel = 0; + for ( uint32_t i = 0; i < num_pixels; i++) + { + uint32_t sel = selectors_temp[i]; + min_sel = minimumu(min_sel, sel); + max_sel = maximumu(max_sel, sel); + } + + vec4F xl, xh; + vec4F_set_scalar(&xl, 0.0f); + vec4F_set_scalar(&xh, 0.0f); + + if (pComp_params->m_uber1_mask & 1) + { + for ( uint32_t i = 0; i < num_pixels; i++) + { + uint32_t sel = selectors_temp[i]; + if ((sel == min_sel) && (sel < (pParams->m_num_selector_weights - 1))) + sel++; + selectors_temp1[i] = sel; + } + + if (pParams->m_has_alpha) + compute_least_squares_endpoints_rgba(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels); + else + { + compute_least_squares_endpoints_rgb(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels); + xl.m_c[3] = 255.0f; + xh.m_c[3] = 255.0f; + } + + xl = vec4F_mul(&xl, (1.0f / 255.0f)); + xh = vec4F_mul(&xh, (1.0f / 255.0f)); + + if (!find_optimal_solution(mode, &xl, &xh, pParams, pResults, pComp_params->m_pbit_search, num_pixels, pPixels)) + return 0; + } + + if (pComp_params->m_uber1_mask & 2) + { + for ( uint32_t i = 0; i < num_pixels; i++) + { + uint32_t sel = selectors_temp[i]; + if ((sel == max_sel) && (sel > 0)) + sel--; + selectors_temp1[i] = sel; + } + + if (pParams->m_has_alpha) + compute_least_squares_endpoints_rgba(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels); + else + { + compute_least_squares_endpoints_rgb(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels); + xl.m_c[3] = 255.0f; + xh.m_c[3] = 255.0f; + } + + xl = vec4F_mul(&xl, (1.0f / 255.0f)); + xh = vec4F_mul(&xh, (1.0f / 255.0f)); + + if (!find_optimal_solution(mode, &xl, &xh, pParams, pResults, pComp_params->m_pbit_search, num_pixels, pPixels)) + return 0; + } + + if (pComp_params->m_uber1_mask & 4) + { + for ( uint32_t i = 0; i < num_pixels; i++) + { + uint32_t sel = selectors_temp[i]; + if ((sel == min_sel) && (sel < (pParams->m_num_selector_weights - 1))) + sel++; + else if ((sel == max_sel) && (sel > 0)) + sel--; + selectors_temp1[i] = sel; + } + + if (pParams->m_has_alpha) + compute_least_squares_endpoints_rgba(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels); + else + { + compute_least_squares_endpoints_rgb(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels); + xl.m_c[3] = 255.0f; + xh.m_c[3] = 255.0f; + } + + xl = vec4F_mul(&xl, (1.0f / 255.0f)); + xh = vec4F_mul(&xh, (1.0f / 255.0f)); + + if (!find_optimal_solution(mode, &xl, &xh, pParams, pResults, pComp_params->m_pbit_search, num_pixels, pPixels)) + return 0; + } + + const uint32_t uber_err_thresh = (num_pixels * 56) >> 4; + if ((pComp_params->m_uber_level >= 2) && (pResults->m_best_overall_err > uber_err_thresh)) + { + const int Q = (pComp_params->m_uber_level >= 4) ? (pComp_params->m_uber_level - 2) : 1; + for ( int ly = -Q; ly <= 1; ly++) + { + for ( int hy = max_selector - 1; hy <= (max_selector + Q); hy++) + { + if ((ly == 0) && (hy == max_selector)) + continue; + + for ( uint32_t i = 0; i < num_pixels; i++) + selectors_temp1[i] = (int)clampf(floor((float)max_selector * ((float)(int)selectors_temp[i] - (float)ly) / ((float)hy - (float)ly) + .5f), 0, (float)max_selector); + + vec4F_set_scalar(&xl, 0.0f); + vec4F_set_scalar(&xh, 0.0f); + if (pParams->m_has_alpha) + compute_least_squares_endpoints_rgba(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels); + else + { + compute_least_squares_endpoints_rgb(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels); + xl.m_c[3] = 255.0f; + xh.m_c[3] = 255.0f; + } + + xl = vec4F_mul(&xl, (1.0f / 255.0f)); + xh = vec4F_mul(&xh, (1.0f / 255.0f)); + + if (!find_optimal_solution(mode, &xl, &xh, pParams, pResults, pComp_params->m_pbit_search && (pComp_params->m_uber_level >= 2), num_pixels, pPixels)) + return 0; + } + } + } + } + + if (((mode <= 2) || (mode == 4) || (mode >= 6)) && pComp_params->m_use_luts) + { + color_cell_compressor_results avg_results; + memset(&avg_results, 0, sizeof(avg_results)); + + avg_results.m_best_overall_err = pResults->m_best_overall_err; + avg_results.m_pSelectors = pResults->m_pSelectors; + avg_results.m_pSelectors_temp = pResults->m_pSelectors_temp; + + const uint32_t r = (int)(.5f + meanColor.m_c[0] * 255.0f); + const uint32_t g = (int)(.5f + meanColor.m_c[1] * 255.0f); + const uint32_t b = (int)(.5f + meanColor.m_c[2] * 255.0f); + const uint32_t a = (int)(.5f + meanColor.m_c[3] * 255.0f); + + uint64_t avg_err; + if (mode == 0) + avg_err = pack_mode0_to_one_color(pParams, &avg_results, r, g, b, pResults->m_pSelectors_temp, num_pixels, pPixels); + else if (mode == 1) + avg_err = pack_mode1_to_one_color(pParams, &avg_results, r, g, b, pResults->m_pSelectors_temp, num_pixels, pPixels); + else if (mode == 6) + avg_err = pack_mode6_to_one_color(pParams, &avg_results, r, g, b, a, pResults->m_pSelectors_temp, num_pixels, pPixels); + else if (mode == 7) + avg_err = pack_mode7_to_one_color(pParams, &avg_results, r, g, b, a, pResults->m_pSelectors_temp, num_pixels, pPixels); + else + avg_err = pack_mode24_to_one_color(pParams, &avg_results, r, g, b, pResults->m_pSelectors_temp, num_pixels, pPixels); + + if (avg_err < pResults->m_best_overall_err) + { + pResults->m_best_overall_err = avg_err; + pResults->m_low_endpoint = avg_results.m_low_endpoint; + pResults->m_high_endpoint = avg_results.m_high_endpoint; + pResults->m_pbits[0] = avg_results.m_pbits[0]; + pResults->m_pbits[1] = avg_results.m_pbits[1]; + + for ( uint32_t i = 0; i < num_pixels; i++) + pResults->m_pSelectors[i] = pResults->m_pSelectors_temp[i]; + + pResults->m_used_lut = true; // average-color one-color LUT candidate won + } + } + + return pResults->m_best_overall_err; +} + +static uint64_t color_cell_compression_est( uint32_t mode, const color_cell_compressor_params * pParams, uint64_t best_err_so_far, uint32_t num_pixels, const color_quad_i * pPixels) +{ + NOTE_UNUSED(best_err_so_far); + assert((pParams->m_num_selector_weights == 4) || (pParams->m_num_selector_weights == 8)); + + float lr = 255, lg = 255, lb = 255; + float hr = 0, hg = 0, hb = 0; + for ( uint32_t i = 0; i < num_pixels; i++) + { + const color_quad_i * pC = &pPixels[i]; + + float r = (float)(pC->m_c[0]); + float g = (float)(pC->m_c[1]); + float b = (float)(pC->m_c[2]); + + lr = min(lr, r); + lg = min(lg, g); + lb = min(lb, b); + + hr = max(hr, r); + hg = max(hg, g); + hb = max(hb, b); + } + + const uint32_t N = 1 << g_bc7_color_index_bitcount[mode]; + + uint64_t total_err = 0; + + float sr = lr; + float sg = lg; + float sb = lb; + + float dir = hr - lr; + float dig = hg - lg; + float dib = hb - lb; + + float far = dir; + float fag = dig; + float fab = dib; + + float low = far * sr + fag * sg + fab * sb; + float high = far * hr + fag * hg + fab * hb; + + float scale = ((float)N - 1) / ((float)(high - low) + BC7E_DENOM_BIAS); + float inv_n = 1.0f / ((float)N - 1); + + float total_errf = 0; + + // We don't handle perceptual very well here, but the difference is very slight (<.05 dB avg Luma PSNR across a large corpus) and the perf lost was high (2x slower). + if ((pParams->m_weights[0] != 1) || (pParams->m_weights[1] != 1) || (pParams->m_weights[2] != 1)) + { + float wr = (float)(pParams->m_weights[0]); + float wg = (float)(pParams->m_weights[1]); + float wb = (float)(pParams->m_weights[2]); + + for ( uint32_t i = 0; i < num_pixels; i++) + { + const color_quad_i * pC = &pPixels[i]; + + float d = far * (float)pC->m_c[0] + fag * (float)pC->m_c[1] + fab * (float)pC->m_c[2]; + + float s = clamp(floorf((d - low) * scale + .5f) * inv_n, 0.0f, 1.0f); + + float itr = sr + dir * s; + float itg = sg + dig * s; + float itb = sb + dib * s; + + float dr = itr - (float)pC->m_c[0]; + float dg = itg - (float)pC->m_c[1]; + float db = itb - (float)pC->m_c[2]; + + total_errf += wr * dr * dr + wg * dg * dg + wb * db * db; + } + } + else + { + for ( uint32_t i = 0; i < num_pixels; i++) + { + const color_quad_i * pC = &pPixels[i]; + + float d = far * (float)pC->m_c[0] + fag * (float)pC->m_c[1] + fab * (float)pC->m_c[2]; + + float s = clamp(floorf((d - low) * scale + .5f) * inv_n, 0.0f, 1.0f); + + float itr = sr + dir * s; + float itg = sg + dig * s; + float itb = sb + dib * s; + + float dr = itr - (float)pC->m_c[0]; + float dg = itg - (float)pC->m_c[1]; + float db = itb - (float)pC->m_c[2]; + + total_errf += dr * dr + dg * dg + db * db; + } + } + + total_err = (int64_t)total_errf; + + return total_err; +} + +static uint64_t color_cell_compression_est_mode7( uint32_t mode, const color_cell_compressor_params * pParams, uint64_t best_err_so_far, uint32_t num_pixels, const color_quad_i * pPixels) +{ + NOTE_UNUSED(best_err_so_far); + NOTE_UNUSED(mode); // only referenced by the assert below (compiled out under NDEBUG) + assert((mode == 7) && (pParams->m_num_selector_weights == 4)); + + float lr = 255, lg = 255, lb = 255, la = 255; + float hr = 0, hg = 0, hb = 0, ha = 0; + for ( uint32_t i = 0; i < num_pixels; i++) + { + const color_quad_i * pC = &pPixels[i]; + + float r = (float)(pC->m_c[0]); + float g = (float)(pC->m_c[1]); + float b = (float)(pC->m_c[2]); + float a = (float)(pC->m_c[3]); + + lr = min(lr, r); + lg = min(lg, g); + lb = min(lb, b); + la = min(la, a); + + hr = max(hr, r); + hg = max(hg, g); + hb = max(hb, b); + ha = max(ha, a); + } + + const uint32_t N = 4; + + uint64_t total_err = 0; + + float sr = lr; + float sg = lg; + float sb = lb; + float sa = la; + + float dir = hr - lr; + float dig = hg - lg; + float dib = hb - lb; + float dia = ha - la; + + float far = dir; + float fag = dig; + float fab = dib; + float faa = dia; + + float low = far * sr + fag * sg + fab * sb + faa * sa; + float high = far * hr + fag * hg + fab * hb + faa * ha; + + float scale = ((float)N - 1) / ((float)(high - low) + BC7E_DENOM_BIAS); + float inv_n = 1.0f / ((float)N - 1); + + float total_errf = 0; + + // We don't handle perceptual very well here, but the difference is very slight (<.05 dB avg Luma PSNR across a large corpus) and the perf lost was high (2x slower). + if ( (!pParams->m_perceptual) && ((pParams->m_weights[0] != 1) || (pParams->m_weights[1] != 1) || (pParams->m_weights[2] != 1) || (pParams->m_weights[3] != 1)) ) + { + float wr = (float)(pParams->m_weights[0]); + float wg = (float)(pParams->m_weights[1]); + float wb = (float)(pParams->m_weights[2]); + float wa = (float)(pParams->m_weights[3]); + + for ( uint32_t i = 0; i < num_pixels; i++) + { + const color_quad_i * pC = &pPixels[i]; + + float d = far * (float)pC->m_c[0] + fag * (float)pC->m_c[1] + fab * (float)pC->m_c[2] + faa * (float)pC->m_c[3]; + + float s = clamp(floorf((d - low) * scale + .5f) * inv_n, 0.0f, 1.0f); + + float itr = sr + dir * s; + float itg = sg + dig * s; + float itb = sb + dib * s; + float ita = sa + dia * s; + + float dr = itr - (float)pC->m_c[0]; + float dg = itg - (float)pC->m_c[1]; + float db = itb - (float)pC->m_c[2]; + float da = ita - (float)pC->m_c[3]; + + total_errf += wr * dr * dr + wg * dg * dg + wb * db * db + wa * da * da; + } + } + else + { + for ( uint32_t i = 0; i < num_pixels; i++) + { + const color_quad_i * pC = &pPixels[i]; + + float d = far * (float)pC->m_c[0] + fag * (float)pC->m_c[1] + fab * (float)pC->m_c[2] + faa * (float)pC->m_c[3]; + + float s = clamp(floorf((d - low) * scale + .5f) * inv_n, 0.0f, 1.0f); + + float itr = sr + dir * s; + float itg = sg + dig * s; + float itb = sb + dib * s; + float ita = sa + dia * s; + + float dr = itr - (float)pC->m_c[0]; + float dg = itg - (float)pC->m_c[1]; + float db = itb - (float)pC->m_c[2]; + float da = ita - (float)pC->m_c[3]; + + total_errf += dr * dr + dg * dg + db * db + da * da; + } + } + + total_err = (int64_t)total_errf; + + return total_err; +} + +static uint32_t estimate_partition( uint32_t mode, const color_quad_i * pPixels, const bc7e_compress_block_params * pComp_params) +{ + const uint32_t total_subsets = g_bc7_num_subsets[mode]; + uint32_t total_partitions = minimumu(pComp_params->m_max_partitions_mode[mode], 1U << g_bc7_partition_bits[mode]); + + if (total_partitions <= 1) + return 0; + + uint64_t best_err = UINT64_MAX; + uint32_t best_partition = 0; + + color_cell_compressor_params params; + color_cell_compressor_params_clear(¶ms); + + params.m_pSelector_weights = (g_bc7_color_index_bitcount[mode] == 2) ? g_bc7_weights2 : g_bc7_weights3; + params.m_num_selector_weights = 1 << g_bc7_color_index_bitcount[mode]; + + memcpy(params.m_weights, pComp_params->m_weights, sizeof(params.m_weights)); + + if (mode >= 6) + { + params.m_weights[0] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[0]; + params.m_weights[1] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[1]; + params.m_weights[2] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[2]; + params.m_weights[3] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[3]; + } + + params.m_perceptual = pComp_params->m_perceptual; + + for ( uint32_t partition = 0; partition < total_partitions; partition++) + { + const int * pPartition = (total_subsets == 3) ? &g_bc7_partition3[partition * 16] : &g_bc7_partition2[partition * 16]; + + color_quad_i subset_colors[3][16]; + uint32_t subset_total_colors[3]; + subset_total_colors[0] = 0; + subset_total_colors[1] = 0; + subset_total_colors[2] = 0; + + for ( uint32_t index = 0; index < 16; index++) + { + const uint32_t p = pPartition[index]; + + subset_colors[p][subset_total_colors[p]] = pPixels[index]; + subset_total_colors[p]++; + } + + uint64_t total_subset_err = 0; + + for ( uint32_t subset = 0; subset < total_subsets; subset++) + { + uint64_t err; + if (mode == 7) + err = color_cell_compression_est_mode7(mode, ¶ms, best_err, subset_total_colors[subset], &subset_colors[subset][0]); + else + err = color_cell_compression_est(mode, ¶ms, best_err, subset_total_colors[subset], &subset_colors[subset][0]); + + total_subset_err += err; + + } // subset + + if (total_subset_err < best_err) + { + best_err = total_subset_err; + best_partition = partition; + if (!best_err) + break; + } + + if (total_subsets == 2) + { + if ((partition == BC7E_2SUBSET_CHECKERBOARD_PARTITION_INDEX) && (best_partition != BC7E_2SUBSET_CHECKERBOARD_PARTITION_INDEX)) + break; + } + + } // partition + + return best_partition; +} + +struct solution +{ + uint32_t m_index; + uint64_t m_err; +}; + +static uint32_t estimate_partition_list( uint32_t mode, const color_quad_i * pPixels, const bc7e_compress_block_params * pComp_params, + solution * pSolutions, int32_t max_solutions) +{ + const int32_t orig_max_solutions = max_solutions; + + const uint32_t total_subsets = g_bc7_num_subsets[mode]; + uint32_t total_partitions = minimumu(pComp_params->m_max_partitions_mode[mode], 1U << g_bc7_partition_bits[mode]); + + if (total_partitions <= 1) + { + pSolutions[0].m_index = 0; + pSolutions[0].m_err = 0; + return 1; + } + else if (max_solutions >= (int)total_partitions) + { + for ( int i = 0; i < (int)total_partitions; i++) + { + pSolutions[i].m_index = i; + pSolutions[i].m_err = i; + } + return total_partitions; + } + + const int32_t HIGH_FREQUENCY_SORTED_PARTITION_THRESHOLD = 4; + if (total_subsets == 2) + { + if (max_solutions < HIGH_FREQUENCY_SORTED_PARTITION_THRESHOLD) + max_solutions = HIGH_FREQUENCY_SORTED_PARTITION_THRESHOLD; + } + + color_cell_compressor_params params; + color_cell_compressor_params_clear(¶ms); + + params.m_pSelector_weights = (g_bc7_color_index_bitcount[mode] == 2) ? g_bc7_weights2 : g_bc7_weights3; + params.m_num_selector_weights = 1 << g_bc7_color_index_bitcount[mode]; + + memcpy(params.m_weights, pComp_params->m_weights, sizeof(params.m_weights)); + + if (mode >= 6) + { + params.m_weights[0] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[0]; + params.m_weights[1] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[1]; + params.m_weights[2] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[2]; + params.m_weights[3] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[3]; + } + + params.m_perceptual = pComp_params->m_perceptual; + + int32_t num_solutions = 0; + + for ( uint32_t partition = 0; partition < total_partitions; partition++) + { + const int * pPartition = (total_subsets == 3) ? &g_bc7_partition3[partition * 16] : &g_bc7_partition2[partition * 16]; + + color_quad_i subset_colors[3][16]; + uint32_t subset_total_colors[3]; + subset_total_colors[0] = 0; + subset_total_colors[1] = 0; + subset_total_colors[2] = 0; + + for ( uint32_t index = 0; index < 16; index++) + { + const uint32_t p = pPartition[index]; + + subset_colors[p][subset_total_colors[p]] = pPixels[index]; + subset_total_colors[p]++; + } + + uint64_t total_subset_err = 0; + + for ( uint32_t subset = 0; subset < total_subsets; subset++) + { + uint64_t err; + if (mode == 7) + err = color_cell_compression_est_mode7(mode, ¶ms, UINT64_MAX, subset_total_colors[subset], &subset_colors[subset][0]); + else + err = color_cell_compression_est(mode, ¶ms, UINT64_MAX, subset_total_colors[subset], &subset_colors[subset][0]); + + total_subset_err += err; + + } // subset + + int32_t i; + for (i = 0; i < num_solutions; i++) + { + if (total_subset_err < pSolutions[i].m_err) + break; + } + + if (i < num_solutions) + { + int32_t solutions_to_move = (max_solutions - 1) - i; + int32_t num_elements_at_i = num_solutions - i; + if (solutions_to_move > num_elements_at_i) + solutions_to_move = num_elements_at_i; + + assert(((i + 1) + solutions_to_move) <= max_solutions); + assert((i + solutions_to_move) <= num_solutions); + + for (int32_t j = solutions_to_move - 1; j >= 0; --j) + { + pSolutions[i + j + 1] = pSolutions[i + j]; + } + } + + if (num_solutions < max_solutions) + num_solutions++; + + if (i < num_solutions) + { + pSolutions[i].m_err = total_subset_err; + + pSolutions[i].m_index = partition; + } + +#if BC7E_NON_DETERMINISTIC + if ((total_subsets == 2) && (partition == BC7E_2SUBSET_CHECKERBOARD_PARTITION_INDEX)) + { + if (all(i >= HIGH_FREQUENCY_SORTED_PARTITION_THRESHOLD)) + break; + } +#endif + + } // partition + +#if 0 + for ( int i = 0; i < num_solutions; i++) + { + assert(pSolutions[i].m_index < total_partitions); + } + + for ( int i = 0; i < (num_solutions - 1); i++) + { + assert(pSolutions[i].m_err <= pSolutions[i + 1].m_err); + } +#endif + + return min(num_solutions, orig_max_solutions); +} + +static inline void set_block_bits(uint8_t *pBytes, uint32_t val, uint32_t num_bits, uint32_t * pCur_ofs) +{ + assert(num_bits < 32); + uint32_t limit = 1U << num_bits; + assert(val < limit); + NOTE_UNUSED(limit); // only referenced by the assert above (compiled out under NDEBUG) + + while (num_bits) + { + const uint32_t n = minimumu(8 - (*pCur_ofs & 7), num_bits); + + pBytes[*pCur_ofs >> 3] |= (uint8_t)(val << (*pCur_ofs & 7)); + + val >>= n; + num_bits -= n; + *pCur_ofs += n; + } + + assert(*pCur_ofs <= 128); +} + +struct bc7_optimization_results +{ + uint32_t m_mode; + uint32_t m_partition; + int m_selectors[16]; + int m_alpha_selectors[16]; + color_quad_i m_low[3]; + color_quad_i m_high[3]; + uint32_t m_pbits[3][2]; + uint32_t m_rotation; + uint32_t m_index_selector; + // True if the WINNING encoding used the one-color optimal-endpoint LUT on any + // subset (OR'd across the winning mode's subsets). Set at each winner-update. + bool m_used_lut; +}; + +static void encode_bc7_block(void *pBlock, const bc7_optimization_results * pResults) +{ + const uint32_t best_mode = pResults->m_mode; + + const uint32_t total_subsets = g_bc7_num_subsets[best_mode]; + + const uint32_t total_partitions = 1 << g_bc7_partition_bits[best_mode]; + + const int *pPartition; + if (total_subsets == 1) + pPartition = &g_bc7_partition1[0]; + else if (total_subsets == 2) + pPartition = &g_bc7_partition2[pResults->m_partition * 16]; + else + pPartition = &g_bc7_partition3[pResults->m_partition * 16]; + + int color_selectors[16]; + for ( int i = 0; i < 16; i++) + color_selectors[i] = pResults->m_selectors[i]; + + int alpha_selectors[16]; + for ( int i = 0; i < 16; i++) + alpha_selectors[i] = pResults->m_alpha_selectors[i]; + + color_quad_i low[3], high[3]; + low[0] = pResults->m_low[0]; + low[1] = pResults->m_low[1]; + low[2] = pResults->m_low[2]; + + high[0] = pResults->m_high[0]; + high[1] = pResults->m_high[1]; + high[2] = pResults->m_high[2]; + + uint32_t pbits[3][2]; + for ( int i = 0; i < 3; i++) + { + pbits[i][0] = pResults->m_pbits[i][0]; + pbits[i][1] = pResults->m_pbits[i][1]; + } + + int anchor[3]; + anchor[0] = -1; + anchor[1] = -1; + anchor[2] = -1; + + for ( uint32_t k = 0; k < total_subsets; k++) + { + uint32_t anchor_index = 0; + if (k) + { + if ((total_subsets == 3) && (k == 1)) + { + anchor_index = g_bc7_table_anchor_index_third_subset_1[pResults->m_partition]; + } + else if ((total_subsets == 3) && (k == 2)) + { + anchor_index = g_bc7_table_anchor_index_third_subset_2[pResults->m_partition]; + } + else + { + anchor_index = g_bc7_table_anchor_index_second_subset[pResults->m_partition]; + } + } + + anchor[k] = anchor_index; + + const uint32_t color_index_bits = get_bc7_color_index_size(best_mode, pResults->m_index_selector); + const uint32_t num_color_indices = 1 << color_index_bits; + + if (color_selectors[anchor_index] & (num_color_indices >> 1)) + { + for ( uint32_t i = 0; i < 16; i++) + { + if ((uint32_t)pPartition[i] == k) + color_selectors[i] = (num_color_indices - 1) - color_selectors[i]; + } + + if (get_bc7_mode_has_seperate_alpha_selectors(best_mode)) + { + for ( uint32_t q = 0; q < 3; q++) + { + int t = low[k].m_c[q]; + low[k].m_c[q] = high[k].m_c[q]; + high[k].m_c[q] = t; + } + } + else + { + color_quad_i tmp = low[k]; + low[k] = high[k]; + high[k] = tmp; + } + + if (!g_bc7_mode_has_shared_p_bits[best_mode]) + { + uint32_t t = pbits[k][0]; + pbits[k][0] = pbits[k][1]; + pbits[k][1] = t; + } + } + + if (get_bc7_mode_has_seperate_alpha_selectors(best_mode)) + { + const uint32_t alpha_index_bits = get_bc7_alpha_index_size(best_mode, pResults->m_index_selector); + const uint32_t num_alpha_indices = 1 << alpha_index_bits; + + if (alpha_selectors[anchor_index] & (num_alpha_indices >> 1)) + { + for ( uint32_t i = 0; i < 16; i++) + { + if ((uint32_t)pPartition[i] == k) + alpha_selectors[i] = (num_alpha_indices - 1) - alpha_selectors[i]; + } + + int t = low[k].m_c[3]; + low[k].m_c[3] = high[k].m_c[3]; + high[k].m_c[3] = t; + } + } + } + + uint8_t *pBlock_bytes = (uint8_t *)(pBlock); + memset(pBlock_bytes, 0, BC7E_BLOCK_SIZE); + + uint32_t cur_bit_ofs = 0; + + set_block_bits(pBlock_bytes, 1 << best_mode, best_mode + 1, &cur_bit_ofs); + + if ((best_mode == 4) || (best_mode == 5)) + set_block_bits(pBlock_bytes, pResults->m_rotation, 2, &cur_bit_ofs); + + if (best_mode == 4) + set_block_bits(pBlock_bytes, pResults->m_index_selector, 1, &cur_bit_ofs); + + if (total_partitions > 1) + set_block_bits(pBlock_bytes, pResults->m_partition, (total_partitions == 64) ? 6 : 4, &cur_bit_ofs); + + const uint32_t total_comps = (best_mode >= 4) ? 4 : 3; + for ( uint32_t comp = 0; comp < total_comps; comp++) + { + for ( uint32_t subset = 0; subset < total_subsets; subset++) + { + set_block_bits(pBlock_bytes, low[subset].m_c[comp], (comp == 3) ? g_bc7_alpha_precision_table[best_mode] : g_bc7_color_precision_table[best_mode], &cur_bit_ofs); + set_block_bits(pBlock_bytes, high[subset].m_c[comp], (comp == 3) ? g_bc7_alpha_precision_table[best_mode] : g_bc7_color_precision_table[best_mode], &cur_bit_ofs); + } + } + + if (g_bc7_mode_has_p_bits[best_mode]) + { + for ( uint32_t subset = 0; subset < total_subsets; subset++) + { + set_block_bits(pBlock_bytes, pbits[subset][0], 1, &cur_bit_ofs); + if (!g_bc7_mode_has_shared_p_bits[best_mode]) + set_block_bits(pBlock_bytes, pbits[subset][1], 1, &cur_bit_ofs); + } + } + + for ( uint32_t y = 0; y < 4; y++) + { + for ( uint32_t x = 0; x < 4; x++) + { + int idx = x + y * 4; + + uint32_t n = pResults->m_index_selector ? get_bc7_alpha_index_size(best_mode, pResults->m_index_selector) : get_bc7_color_index_size(best_mode, pResults->m_index_selector); + + if ((idx == anchor[0]) || (idx == anchor[1]) || (idx == anchor[2])) + n--; + + set_block_bits(pBlock_bytes, pResults->m_index_selector ? alpha_selectors[idx] : color_selectors[idx], n, &cur_bit_ofs); + } + } + + if (get_bc7_mode_has_seperate_alpha_selectors(best_mode)) + { + for ( uint32_t y = 0; y < 4; y++) + { + for ( uint32_t x = 0; x < 4; x++) + { + int idx = x + y * 4; + + uint32_t n = pResults->m_index_selector ? get_bc7_color_index_size(best_mode, pResults->m_index_selector) : get_bc7_alpha_index_size(best_mode, pResults->m_index_selector); + + if ((idx == anchor[0]) || (idx == anchor[1]) || (idx == anchor[2])) + n--; + + set_block_bits(pBlock_bytes, pResults->m_index_selector ? color_selectors[idx] : alpha_selectors[idx], n, &cur_bit_ofs); + } + } + } + + assert(cur_bit_ofs == 128); +} + +static inline void encode_bc7_block_mode6(void *pBlock, bc7_optimization_results * pResults) +{ + color_quad_i low, high; + uint32_t pbits[2]; + + uint32_t invert_selectors = 0; + if (pResults->m_selectors[0] & 8) + { + invert_selectors = 15; + + low = pResults->m_high[0]; + high = pResults->m_low[0]; + + pbits[0] = pResults->m_pbits[0][1]; + pbits[1] = pResults->m_pbits[0][0]; + } + else + { + low = pResults->m_low[0]; + high = pResults->m_high[0]; + + pbits[0] = pResults->m_pbits[0][0]; + pbits[1] = pResults->m_pbits[0][1]; + } + + uint64_t l = 0, h = 0; + + l = 1 << 6; + + l |= (low.m_c[0] << 7); + l |= (high.m_c[0] << 14); + + l |= (low.m_c[1] << 21); + l |= ((uint64_t)high.m_c[1] << 28); + + l |= ((uint64_t)low.m_c[2] << 35); + l |= ((uint64_t)high.m_c[2] << 42); + + l |= ((uint64_t)low.m_c[3] << 49); + l |= ((uint64_t)high.m_c[3] << 56); + + l |= ((uint64_t)pbits[0] << 63); + + h = pbits[1]; + + h |= ((invert_selectors ^ pResults->m_selectors[0]) << 1); + + // TODO: Just invert all these bits in one single operation, not as individual + h |= ((invert_selectors ^ pResults->m_selectors[1]) << 4); + h |= ((invert_selectors ^ pResults->m_selectors[2]) << 8); + h |= ((invert_selectors ^ pResults->m_selectors[3]) << 12); + h |= ((invert_selectors ^ pResults->m_selectors[4]) << 16); + + h |= ((invert_selectors ^ pResults->m_selectors[5]) << 20); + h |= ((invert_selectors ^ pResults->m_selectors[6]) << 24); + h |= ((invert_selectors ^ pResults->m_selectors[7]) << 28); + h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[8]) << 32); + + h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[9]) << 36); + h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[10]) << 40); + h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[11]) << 44); + h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[12]) << 48); + + h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[13]) << 52); + h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[14]) << 56); + h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[15]) << 60); + + ((uint64_t *)(pBlock))[0] = l; + + ((uint64_t *)(pBlock))[1] = h; +} + +static void handle_alpha_block_mode4(const color_quad_i * pPixels, const bc7e_compress_block_params * pComp_params, color_cell_compressor_params * pParams, uint32_t lo_a, uint32_t hi_a, + bc7_optimization_results * pOpt_results4, uint64_t * pMode4_err) +{ + pParams->m_has_alpha = false; + pParams->m_comp_bits = 5; + pParams->m_has_pbits = false; + pParams->m_endpoints_share_pbit = false; + pParams->m_perceptual = pComp_params->m_perceptual; + + for ( uint32_t index_selector = 0; index_selector < 2; index_selector++) + { + if ((pComp_params->m_mode4_index_mask & (1 << index_selector)) == 0) + continue; + + if (index_selector) + { + pParams->m_pSelector_weights = g_bc7_weights3; + pParams->m_pSelector_weightsx = (const vec4F * )&g_bc7_weights3x[0]; + pParams->m_num_selector_weights = 8; + } + else + { + pParams->m_pSelector_weights = g_bc7_weights2; + pParams->m_pSelector_weightsx = (const vec4F * )&g_bc7_weights2x[0]; + pParams->m_num_selector_weights = 4; + } + + color_cell_compressor_results results; + + int selectors[16]; + results.m_pSelectors = selectors; + + int selectors_temp[16]; + results.m_pSelectors_temp = selectors_temp; + + uint64_t trial_err = color_cell_compression(4, pParams, &results, pComp_params, 16, pPixels, true); + assert(trial_err == results.m_best_overall_err); + + uint32_t la = minimumi((lo_a + 2) >> 2, 63); + uint32_t ha = minimumi((hi_a + 2) >> 2, 63); + + if (la == ha) + { + if (lo_a != hi_a) + { + if (ha != 63) + ha++; + else if (la != 0) + la--; + } + } + + uint64_t best_alpha_err = UINT64_MAX; + uint32_t best_la = 0, best_ha = 0; + int best_alpha_selectors[16] = {}; + + for ( int32_t pass = 0; pass < 2; pass++) + { + int32_t vals[8]; + + if (index_selector == 0) + { + vals[0] = (la << 2) | (la >> 4); + vals[7] = (ha << 2) | (ha >> 4); + + for ( uint32_t i = 1; i < 7; i++) + vals[i] = (vals[0] * (64 - g_bc7_weights3[i]) + vals[7] * g_bc7_weights3[i] + 32) >> 6; + } + else + { + vals[0] = (la << 2) | (la >> 4); + vals[3] = (ha << 2) | (ha >> 4); + + const int32_t w_s1 = 21, w_s2 = 43; + vals[1] = (vals[0] * (64 - w_s1) + vals[3] * w_s1 + 32) >> 6; + vals[2] = (vals[0] * (64 - w_s2) + vals[3] * w_s2 + 32) >> 6; + } + + uint64_t trial_alpha_err = 0; + + int trial_alpha_selectors[16]; + for ( uint32_t i = 0; i < 16; i++) + { + const int32_t a = pPixels[i].m_c[3]; + + int s = 0; + int32_t be = iabs32(a - vals[0]); + + int e = iabs32(a - vals[1]); if (e < be) { be = e; s = 1; } + e = iabs32(a - vals[2]); if (e < be) { be = e; s = 2; } + e = iabs32(a - vals[3]); if (e < be) { be = e; s = 3; } + + if (index_selector == 0) + { + e = iabs32(a - vals[4]); if (e < be) { be = e; s = 4; } + e = iabs32(a - vals[5]); if (e < be) { be = e; s = 5; } + e = iabs32(a - vals[6]); if (e < be) { be = e; s = 6; } + e = iabs32(a - vals[7]); if (e < be) { be = e; s = 7; } + } + + trial_alpha_err += (be * be) * pParams->m_weights[3]; + + trial_alpha_selectors[i] = s; + } + + if (trial_alpha_err < best_alpha_err) + { + best_alpha_err = trial_alpha_err; + best_la = la; + best_ha = ha; + for ( uint32_t i = 0; i < 16; i++) + best_alpha_selectors[i] = trial_alpha_selectors[i]; + } + + if (pass == 0) + { + float xl, xh; + compute_least_squares_endpoints_a(16, trial_alpha_selectors, index_selector ? (const vec4F * )&g_bc7_weights2x[0] : (const vec4F * )&g_bc7_weights3x[0], &xl, &xh, pPixels); + if (xl > xh) + swapf(&xl, &xh); + la = clampi((int)floor(xl * (63.0f / 255.0f) + .5f), 0, 63); + ha = clampi((int)floor(xh * (63.0f / 255.0f) + .5f), 0, 63); + } + + } // pass + + if (pComp_params->m_uber_level > 0) + { + const int D = min((int)pComp_params->m_uber_level, 3); + for ( int ld = -D; ld <= D; ld++) + { + for ( int hd = -D; hd <= D; hd++) + { + la = clamp((int)best_la + ld, 0, 63); + ha = clamp((int)best_ha + hd, 0, 63); + + int32_t vals[8]; + + if (index_selector == 0) + { + vals[0] = (la << 2) | (la >> 4); + vals[7] = (ha << 2) | (ha >> 4); + + for ( uint32_t i = 1; i < 7; i++) + vals[i] = (vals[0] * (64 - g_bc7_weights3[i]) + vals[7] * g_bc7_weights3[i] + 32) >> 6; + } + else + { + vals[0] = (la << 2) | (la >> 4); + vals[3] = (ha << 2) | (ha >> 4); + + const int32_t w_s1 = 21, w_s2 = 43; + vals[1] = (vals[0] * (64 - w_s1) + vals[3] * w_s1 + 32) >> 6; + vals[2] = (vals[0] * (64 - w_s2) + vals[3] * w_s2 + 32) >> 6; + } + + uint64_t trial_alpha_err = 0; + + int trial_alpha_selectors[16]; + for ( uint32_t i = 0; i < 16; i++) + { + const int32_t a = pPixels[i].m_c[3]; + + int s = 0; + int32_t be = iabs32(a - vals[0]); + + int e = iabs32(a - vals[1]); if (e < be) { be = e; s = 1; } + e = iabs32(a - vals[2]); if (e < be) { be = e; s = 2; } + e = iabs32(a - vals[3]); if (e < be) { be = e; s = 3; } + + if (index_selector == 0) + { + e = iabs32(a - vals[4]); if (e < be) { be = e; s = 4; } + e = iabs32(a - vals[5]); if (e < be) { be = e; s = 5; } + e = iabs32(a - vals[6]); if (e < be) { be = e; s = 6; } + e = iabs32(a - vals[7]); if (e < be) { be = e; s = 7; } + } + + trial_alpha_err += (be * be) * pParams->m_weights[3]; + + trial_alpha_selectors[i] = s; + } + + if (trial_alpha_err < best_alpha_err) + { + best_alpha_err = trial_alpha_err; + best_la = la; + best_ha = ha; + for ( uint32_t i = 0; i < 16; i++) + best_alpha_selectors[i] = trial_alpha_selectors[i]; + } + + } // hd + + } // ld + } + + trial_err += best_alpha_err; + + if (trial_err < *pMode4_err) + { + *pMode4_err = trial_err; + + pOpt_results4->m_mode = 4; + pOpt_results4->m_index_selector = index_selector; + pOpt_results4->m_rotation = 0; + pOpt_results4->m_partition = 0; + pOpt_results4->m_used_lut = results.m_used_lut; + + pOpt_results4->m_low[0] = results.m_low_endpoint; + pOpt_results4->m_high[0] = results.m_high_endpoint; + pOpt_results4->m_low[0].m_c[3] = best_la; + pOpt_results4->m_high[0].m_c[3] = best_ha; + + for ( uint32_t i = 0; i < 16; i++) + pOpt_results4->m_selectors[i] = selectors[i]; + + for ( uint32_t i = 0; i < 16; i++) + pOpt_results4->m_alpha_selectors[i] = best_alpha_selectors[i]; + } + + } // index_selector +} + +static void handle_alpha_block_mode5(const color_quad_i * pPixels, const bc7e_compress_block_params * pComp_params, color_cell_compressor_params * pParams, uint32_t lo_a, uint32_t hi_a, + bc7_optimization_results * pOpt_results5, uint64_t * pMode5_err) +{ + pParams->m_pSelector_weights = g_bc7_weights2; + pParams->m_pSelector_weightsx = (const vec4F * )&g_bc7_weights2x[0]; + pParams->m_num_selector_weights = 4; + + pParams->m_comp_bits = 7; + pParams->m_has_alpha = false; + pParams->m_has_pbits = false; + pParams->m_endpoints_share_pbit = false; + + pParams->m_perceptual = pComp_params->m_perceptual; + + color_cell_compressor_results results5; + results5.m_pSelectors = pOpt_results5->m_selectors; + + int selectors_temp[16]; + results5.m_pSelectors_temp = selectors_temp; + + *pMode5_err = color_cell_compression(5, pParams, &results5, pComp_params, 16, pPixels, true); + assert(*pMode5_err == results5.m_best_overall_err); + + pOpt_results5->m_low[0] = results5.m_low_endpoint; + pOpt_results5->m_high[0] = results5.m_high_endpoint; + + cif (lo_a == hi_a) + { + pOpt_results5->m_low[0].m_c[3] = lo_a; + pOpt_results5->m_high[0].m_c[3] = hi_a; + for ( uint32_t i = 0; i < 16; i++) + pOpt_results5->m_alpha_selectors[i] = 0; + } + else + { + uint64_t mode5_alpha_err = UINT64_MAX; + + for ( uint32_t pass = 0; pass < 2; pass++) + { + int32_t vals[4]; + vals[0] = lo_a; + vals[3] = hi_a; + + const int32_t w_s1 = 21, w_s2 = 43; + vals[1] = (vals[0] * (64 - w_s1) + vals[3] * w_s1 + 32) >> 6; + vals[2] = (vals[0] * (64 - w_s2) + vals[3] * w_s2 + 32) >> 6; + + int trial_alpha_selectors[16]; + + uint64_t trial_alpha_err = 0; + for ( uint32_t i = 0; i < 16; i++) + { + const int32_t a = pPixels[i].m_c[3]; + + int s = 0; + int32_t be = iabs32(a - vals[0]); + int e = iabs32(a - vals[1]); if (e < be) { be = e; s = 1; } + e = iabs32(a - vals[2]); if (e < be) { be = e; s = 2; } + e = iabs32(a - vals[3]); if (e < be) { be = e; s = 3; } + + trial_alpha_selectors[i] = s; + + trial_alpha_err += (be * be) * pParams->m_weights[3]; + } + + if (trial_alpha_err < mode5_alpha_err) + { + mode5_alpha_err = trial_alpha_err; + pOpt_results5->m_low[0].m_c[3] = lo_a; + pOpt_results5->m_high[0].m_c[3] = hi_a; + for ( uint32_t i = 0; i < 16; i++) + pOpt_results5->m_alpha_selectors[i] = trial_alpha_selectors[i]; + } + + if (!pass) + { + float xl, xh; + compute_least_squares_endpoints_a(16, trial_alpha_selectors, (const vec4F * )&g_bc7_weights2x[0], &xl, &xh, pPixels); + + uint32_t new_lo_a = clampi((int)floor(xl + .5f), 0, 255); + uint32_t new_hi_a = clampi((int)floor(xh + .5f), 0, 255); + if (new_lo_a > new_hi_a) + swapu(&new_lo_a, &new_hi_a); + + if ((new_lo_a == lo_a) && (new_hi_a == hi_a)) + break; + + lo_a = new_lo_a; + hi_a = new_hi_a; + } + } + + if (pComp_params->m_uber_level > 0) + { + const int D = min((int)pComp_params->m_uber_level, 3); + for ( int ld = -D; ld <= D; ld++) + { + for ( int hd = -D; hd <= D; hd++) + { + lo_a = clamp((int)pOpt_results5->m_low[0].m_c[3] + ld, 0, 255); + hi_a = clamp((int)pOpt_results5->m_high[0].m_c[3] + hd, 0, 255); + + int32_t vals[4]; + vals[0] = lo_a; + vals[3] = hi_a; + + const int32_t w_s1 = 21, w_s2 = 43; + vals[1] = (vals[0] * (64 - w_s1) + vals[3] * w_s1 + 32) >> 6; + vals[2] = (vals[0] * (64 - w_s2) + vals[3] * w_s2 + 32) >> 6; + + int trial_alpha_selectors[16]; + + uint64_t trial_alpha_err = 0; + for ( uint32_t i = 0; i < 16; i++) + { + const int32_t a = pPixels[i].m_c[3]; + + int s = 0; + int32_t be = iabs32(a - vals[0]); + int e = iabs32(a - vals[1]); if (e < be) { be = e; s = 1; } + e = iabs32(a - vals[2]); if (e < be) { be = e; s = 2; } + e = iabs32(a - vals[3]); if (e < be) { be = e; s = 3; } + + trial_alpha_selectors[i] = s; + + trial_alpha_err += (be * be) * pParams->m_weights[3]; + } + + if (trial_alpha_err < mode5_alpha_err) + { + mode5_alpha_err = trial_alpha_err; + pOpt_results5->m_low[0].m_c[3] = lo_a; + pOpt_results5->m_high[0].m_c[3] = hi_a; + for ( uint32_t i = 0; i < 16; i++) + pOpt_results5->m_alpha_selectors[i] = trial_alpha_selectors[i]; + } + + } // hd + + } // ld + } + + *pMode5_err += mode5_alpha_err; + } + + pOpt_results5->m_mode = 5; + pOpt_results5->m_index_selector = 0; + pOpt_results5->m_rotation = 0; + pOpt_results5->m_partition = 0; + pOpt_results5->m_used_lut = results5.m_used_lut; +} + +static void handle_alpha_block(void * pBlock, const color_quad_i * pPixels, const bc7e_compress_block_params * pComp_params, color_cell_compressor_params * pParams, uint32_t lo_a, uint32_t hi_a, int forced_partition = -1, uint64_t* pBest_err = nullptr, bool* pUsed_lut = nullptr) +{ + pParams->m_perceptual = pComp_params->m_perceptual; + + bc7_optimization_results opt_results = {}; + + uint64_t best_err = UINT64_MAX; + + // Mode 4 + if (pComp_params->m_alpha_settings.m_use_mode4) + { + color_cell_compressor_params params4 = *pParams; + + const int num_rotations = (pComp_params->m_perceptual || (!pComp_params->m_alpha_settings.m_use_mode4_rotation)) ? 1 : 4; + for ( uint32_t rotation = 0; rotation < (uint32_t)num_rotations; rotation++) + { + if ((pComp_params->m_mode4_rotation_mask & (1 << rotation)) == 0) + continue; + + memcpy(params4.m_weights, pParams->m_weights, sizeof(params4.m_weights)); + if (rotation) + swapu(¶ms4.m_weights[rotation - 1], ¶ms4.m_weights[3]); + + color_quad_i rot_pixels[16]; + const color_quad_i * pTrial_pixels = pPixels; + uint32_t trial_lo_a = lo_a, trial_hi_a = hi_a; + if (rotation) + { + trial_lo_a = 255; + trial_hi_a = 0; + + for ( uint32_t i = 0; i < 16; i++) + { + color_quad_i c = pPixels[i]; + swapi(&c.m_c[3], &c.m_c[rotation - 1]); + rot_pixels[i] = c; + + trial_lo_a = minimumu(trial_lo_a, c.m_c[3]); + trial_hi_a = maximumu(trial_hi_a, c.m_c[3]); + } + + pTrial_pixels = rot_pixels; + } + + bc7_optimization_results trial_opt_results4; + + uint64_t trial_mode4_err = best_err; + + handle_alpha_block_mode4(pTrial_pixels, pComp_params, ¶ms4, trial_lo_a, trial_hi_a, &trial_opt_results4, &trial_mode4_err); + + if (trial_mode4_err < best_err) + { + best_err = trial_mode4_err; + + opt_results.m_mode = 4; + opt_results.m_index_selector = trial_opt_results4.m_index_selector; + opt_results.m_rotation = rotation; + opt_results.m_partition = 0; + opt_results.m_used_lut = trial_opt_results4.m_used_lut; + + opt_results.m_low[0] = trial_opt_results4.m_low[0]; + opt_results.m_high[0] = trial_opt_results4.m_high[0]; + + for ( uint32_t i = 0; i < 16; i++) + opt_results.m_selectors[i] = trial_opt_results4.m_selectors[i]; + + for ( uint32_t i = 0; i < 16; i++) + opt_results.m_alpha_selectors[i] = trial_opt_results4.m_alpha_selectors[i]; + } + } // rotation + } + + // Mode 6 + if (pComp_params->m_alpha_settings.m_use_mode6) + { + color_cell_compressor_params params6 = *pParams; + + params6.m_weights[0] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[0]; + params6.m_weights[1] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[1]; + params6.m_weights[2] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[2]; + params6.m_weights[3] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[3]; + + color_cell_compressor_results results6; + + params6.m_pSelector_weights = g_bc7_weights4; + params6.m_pSelector_weightsx = (const vec4F *)&g_bc7_weights4x[0]; + params6.m_num_selector_weights = 16; + + params6.m_comp_bits = 7; + params6.m_has_pbits = true; + params6.m_endpoints_share_pbit = false; + params6.m_has_alpha = true; + + int selectors[16]; + results6.m_pSelectors = selectors; + + int selectors_temp[16]; + results6.m_pSelectors_temp = selectors_temp; + + uint64_t mode6_err = color_cell_compression(6, ¶ms6, &results6, pComp_params, 16, pPixels, true); + assert(mode6_err == results6.m_best_overall_err); + + if (mode6_err < best_err) + { + best_err = mode6_err; + + opt_results.m_mode = 6; + opt_results.m_index_selector = 0; + opt_results.m_rotation = 0; + opt_results.m_partition = 0; + + opt_results.m_low[0] = results6.m_low_endpoint; + opt_results.m_high[0] = results6.m_high_endpoint; + + opt_results.m_pbits[0][0] = results6.m_pbits[0]; + opt_results.m_pbits[0][1] = results6.m_pbits[1]; + opt_results.m_used_lut = results6.m_used_lut; + + for ( int i = 0; i < 16; i++) + opt_results.m_selectors[i] = selectors[i]; + } + } + + // Mode 5 + if (pComp_params->m_alpha_settings.m_use_mode5) + { + color_cell_compressor_params params5 = *pParams; + + const int num_rotations = (pComp_params->m_perceptual || (!pComp_params->m_alpha_settings.m_use_mode5_rotation)) ? 1 : 4; + for ( uint32_t rotation = 0; rotation < (uint32_t)num_rotations; rotation++) + { + if ((pComp_params->m_mode5_rotation_mask & (1 << rotation)) == 0) + continue; + + memcpy(params5.m_weights, pParams->m_weights, sizeof(params5.m_weights)); + if (rotation) + swapu(¶ms5.m_weights[rotation - 1], ¶ms5.m_weights[3]); + + color_quad_i rot_pixels[16]; + const color_quad_i * pTrial_pixels = pPixels; + uint32_t trial_lo_a = lo_a, trial_hi_a = hi_a; + if (rotation) + { + trial_lo_a = 255; + trial_hi_a = 0; + + for ( uint32_t i = 0; i < 16; i++) + { + color_quad_i c = pPixels[i]; + swapi(&c.m_c[3], &c.m_c[rotation - 1]); + rot_pixels[i] = c; + + trial_lo_a = minimumu(trial_lo_a, c.m_c[3]); + trial_hi_a = maximumu(trial_hi_a, c.m_c[3]); + } + + pTrial_pixels = rot_pixels; + } + + bc7_optimization_results trial_opt_results5; + + uint64_t trial_mode5_err = 0; + + handle_alpha_block_mode5(pTrial_pixels, pComp_params, ¶ms5, trial_lo_a, trial_hi_a, &trial_opt_results5, &trial_mode5_err); + + if (trial_mode5_err < best_err) + { + best_err = trial_mode5_err; + + opt_results = trial_opt_results5; + opt_results.m_rotation = rotation; + } + } // rotation + } + + // Mode 7 + if (pComp_params->m_alpha_settings.m_use_mode7) + { + solution solutions[BC7E_MAX_PARTITIONS7]; + uint32_t num_solutions; + if (forced_partition >= 0) + { + solutions[0].m_index = forced_partition; + num_solutions = 1; + } + else + num_solutions = estimate_partition_list(7, pPixels, pComp_params, solutions, pComp_params->m_alpha_settings.m_max_mode7_partitions_to_try); + + color_cell_compressor_params params7 = *pParams; + + params7.m_weights[0] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[0]; + params7.m_weights[1] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[1]; + params7.m_weights[2] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[2]; + params7.m_weights[3] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[3]; + + params7.m_pSelector_weights = g_bc7_weights2; + params7.m_pSelector_weightsx = (const vec4F *)&g_bc7_weights2x[0]; + params7.m_num_selector_weights = 4; + + params7.m_comp_bits = 5; + params7.m_has_pbits = true; + params7.m_endpoints_share_pbit = false; + + params7.m_has_alpha = true; + + int selectors_temp[16]; + + const bool disable_faster_part_selection = false; + + for ( uint32_t solution_index = 0; solution_index < num_solutions; solution_index++) + { + const uint32_t trial_partition = solutions[solution_index].m_index; + assert(trial_partition < 64); + + const int *pPartition = &g_bc7_partition2[trial_partition * 16]; + + color_quad_i subset_colors[2][16]; + + uint32_t subset_total_colors7[2]; + subset_total_colors7[0] = 0; + subset_total_colors7[1] = 0; + + int subset_pixel_index7[2][16]; + int subset_selectors7[2][16]; + color_cell_compressor_results subset_results7[2]; + + for ( uint32_t idx = 0; idx < 16; idx++) + { + const uint32_t p = pPartition[idx]; + assert(p < 2); + + subset_colors[p][subset_total_colors7[p]] = pPixels[idx]; + subset_pixel_index7[p][subset_total_colors7[p]] = idx; + subset_total_colors7[p]++; + } + + uint64_t trial_err = 0; + for ( uint32_t subset = 0; subset < 2; subset++) + { + color_cell_compressor_results * pResults = &subset_results7[subset]; + + pResults->m_pSelectors = &subset_selectors7[subset][0]; + pResults->m_pSelectors_temp = selectors_temp; + + uint64_t err = color_cell_compression(7, ¶ms7, pResults, pComp_params, subset_total_colors7[subset], &subset_colors[subset][0], (num_solutions <= 2) || disable_faster_part_selection); + assert(err == pResults->m_best_overall_err); + + trial_err += err; + if (trial_err > best_err) + break; + } // subset + + if (trial_err < best_err) + { + best_err = trial_err; + + opt_results.m_mode = 7; + opt_results.m_index_selector = 0; + opt_results.m_rotation = 0; + opt_results.m_partition = trial_partition; + + for ( uint32_t subset = 0; subset < 2; subset++) + { + for ( uint32_t i = 0; i < subset_total_colors7[subset]; i++) + { + const uint32_t pixel_index = subset_pixel_index7[subset][i]; + + opt_results.m_selectors[pixel_index] = subset_selectors7[subset][i]; + } + + opt_results.m_low[subset] = subset_results7[subset].m_low_endpoint; + opt_results.m_high[subset] = subset_results7[subset].m_high_endpoint; + + opt_results.m_pbits[subset][0] = subset_results7[subset].m_pbits[0]; + opt_results.m_pbits[subset][1] = subset_results7[subset].m_pbits[1]; + } + opt_results.m_used_lut = subset_results7[0].m_used_lut || subset_results7[1].m_used_lut; + } + + } // solution_index + + if ((num_solutions > 2) && (opt_results.m_mode == 7) && (!disable_faster_part_selection)) + { + const uint32_t trial_partition = opt_results.m_partition; + assert(trial_partition < 64); + + const int *pPartition = &g_bc7_partition2[trial_partition * 16]; + + color_quad_i subset_colors[2][16]; + + uint32_t subset_total_colors7[2]; + subset_total_colors7[0] = 0; + subset_total_colors7[1] = 0; + + int subset_pixel_index7[2][16]; + int subset_selectors7[2][16]; + color_cell_compressor_results subset_results7[2]; + + for ( uint32_t idx = 0; idx < 16; idx++) + { + const uint32_t p = pPartition[idx]; + assert(p < 2); + + subset_colors[p][subset_total_colors7[p]] = pPixels[idx]; + subset_pixel_index7[p][subset_total_colors7[p]] = idx; + subset_total_colors7[p]++; + } + + uint64_t trial_err = 0; + for ( uint32_t subset = 0; subset < 2; subset++) + { + color_cell_compressor_results * pResults = &subset_results7[subset]; + + pResults->m_pSelectors = &subset_selectors7[subset][0]; + pResults->m_pSelectors_temp = selectors_temp; + + uint64_t err = color_cell_compression(7, ¶ms7, pResults, pComp_params, subset_total_colors7[subset], &subset_colors[subset][0], true); + assert(err == pResults->m_best_overall_err); + + trial_err += err; + if (trial_err > best_err) + break; + } // subset + + if (trial_err < best_err) + { + best_err = trial_err; + + for ( uint32_t subset = 0; subset < 2; subset++) + { + for ( uint32_t i = 0; i < subset_total_colors7[subset]; i++) + { + const uint32_t pixel_index = subset_pixel_index7[subset][i]; + + opt_results.m_selectors[pixel_index] = subset_selectors7[subset][i]; + } + + opt_results.m_low[subset] = subset_results7[subset].m_low_endpoint; + opt_results.m_high[subset] = subset_results7[subset].m_high_endpoint; + + opt_results.m_pbits[subset][0] = subset_results7[subset].m_pbits[0]; + opt_results.m_pbits[subset][1] = subset_results7[subset].m_pbits[1]; + } + opt_results.m_used_lut = subset_results7[0].m_used_lut || subset_results7[1].m_used_lut; + } + } + } + + if (pBest_err) *pBest_err = best_err; + if (pUsed_lut) *pUsed_lut = opt_results.m_used_lut; + encode_bc7_block(pBlock, &opt_results); +} + +static void handle_opaque_block(void * pBlock, const color_quad_i * pPixels, const bc7e_compress_block_params * pComp_params, color_cell_compressor_params * pParams, int forced_partition = -1, uint64_t* pBest_err = nullptr, bool* pUsed_lut = nullptr) +{ + int selectors_temp[16]; + + bc7_optimization_results opt_results = {}; + + uint64_t best_err = UINT64_MAX; + + // Mode 6 + if (pComp_params->m_opaque_settings.m_use_mode[6]) + { + pParams->m_pSelector_weights = g_bc7_weights4; + pParams->m_pSelector_weightsx = (const vec4F * )&g_bc7_weights4x[0]; + pParams->m_num_selector_weights = 16; + + pParams->m_comp_bits = 7; + pParams->m_has_pbits = true; + pParams->m_endpoints_share_pbit = false; + + pParams->m_perceptual = pComp_params->m_perceptual; + + color_cell_compressor_results results6; + results6.m_pSelectors = opt_results.m_selectors; + results6.m_pSelectors_temp = selectors_temp; + + best_err = color_cell_compression(6, pParams, &results6, pComp_params, 16, pPixels, true); + + opt_results.m_mode = 6; + opt_results.m_index_selector = 0; + opt_results.m_rotation = 0; + opt_results.m_partition = 0; + opt_results.m_used_lut = results6.m_used_lut; + + opt_results.m_low[0] = results6.m_low_endpoint; + opt_results.m_high[0] = results6.m_high_endpoint; + + opt_results.m_pbits[0][0] = results6.m_pbits[0]; + opt_results.m_pbits[0][1] = results6.m_pbits[1]; + } + + solution solutions2[BC7E_MAX_PARTITIONS3]; + uint32_t num_solutions2 = 0; + if (pComp_params->m_opaque_settings.m_use_mode[1] || pComp_params->m_opaque_settings.m_use_mode[3]) + { + if (forced_partition >= 0) + { + solutions2[0].m_index = forced_partition; + num_solutions2 = 1; + } + else if (pComp_params->m_opaque_settings.m_max_mode13_partitions_to_try == 1) + { + solutions2[0].m_index = estimate_partition(1, pPixels, pComp_params); + num_solutions2 = 1; + } + else + { + num_solutions2 = estimate_partition_list(1, pPixels, pComp_params, solutions2, pComp_params->m_opaque_settings.m_max_mode13_partitions_to_try); + } + } + + const bool disable_faster_part_selection = false; + + // Mode 1 + if (pComp_params->m_opaque_settings.m_use_mode[1]) + { + pParams->m_pSelector_weights = g_bc7_weights3; + pParams->m_pSelector_weightsx = (const vec4F *)&g_bc7_weights3x[0]; + pParams->m_num_selector_weights = 8; + + pParams->m_comp_bits = 6; + pParams->m_has_pbits = true; + pParams->m_endpoints_share_pbit = true; + + pParams->m_perceptual = pComp_params->m_perceptual; + + for ( uint32_t solution_index = 0; solution_index < num_solutions2; solution_index++) + { + const uint32_t trial_partition = solutions2[solution_index].m_index; + assert(trial_partition < 64); + + const int *pPartition = &g_bc7_partition2[trial_partition * 16]; + + color_quad_i subset_colors[2][16]; + + uint32_t subset_total_colors1[2]; + subset_total_colors1[0] = 0; + subset_total_colors1[1] = 0; + + int subset_pixel_index1[2][16]; + int subset_selectors1[2][16]; + color_cell_compressor_results subset_results1[2]; + + for ( uint32_t idx = 0; idx < 16; idx++) + { + const uint32_t p = pPartition[idx]; + assert(p < 2); + + subset_colors[p][subset_total_colors1[p]] = pPixels[idx]; + subset_pixel_index1[p][subset_total_colors1[p]] = idx; + subset_total_colors1[p]++; + } + + uint64_t trial_err = 0; + for ( uint32_t subset = 0; subset < 2; subset++) + { + color_cell_compressor_results * pResults = &subset_results1[subset]; + + pResults->m_pSelectors = &subset_selectors1[subset][0]; + pResults->m_pSelectors_temp = selectors_temp; + + uint64_t err = color_cell_compression(1, pParams, pResults, pComp_params, subset_total_colors1[subset], &subset_colors[subset][0], (num_solutions2 <= 2) || disable_faster_part_selection); + assert(err == pResults->m_best_overall_err); + + trial_err += err; + if (trial_err > best_err) + break; + + } // subset + + if (trial_err < best_err) + { + best_err = trial_err; + + opt_results.m_mode = 1; + opt_results.m_index_selector = 0; + opt_results.m_rotation = 0; + opt_results.m_partition = trial_partition; + opt_results.m_used_lut = subset_results1[0].m_used_lut || subset_results1[1].m_used_lut; + + for ( uint32_t subset = 0; subset < 2; subset++) + { + for ( uint32_t i = 0; i < subset_total_colors1[subset]; i++) + { + const uint32_t pixel_index = subset_pixel_index1[subset][i]; + + opt_results.m_selectors[pixel_index] = subset_selectors1[subset][i]; + } + + opt_results.m_low[subset] = subset_results1[subset].m_low_endpoint; + opt_results.m_high[subset] = subset_results1[subset].m_high_endpoint; + + opt_results.m_pbits[subset][0] = subset_results1[subset].m_pbits[0]; + } + } + } + + if ((num_solutions2 > 2) && (opt_results.m_mode == 1) && (!disable_faster_part_selection)) + { + const uint32_t trial_partition = opt_results.m_partition; + assert(trial_partition < 64); + + const int *pPartition = &g_bc7_partition2[trial_partition * 16]; + + color_quad_i subset_colors[2][16]; + + uint32_t subset_total_colors1[2]; + subset_total_colors1[0] = 0; + subset_total_colors1[1] = 0; + + int subset_pixel_index1[2][16]; + int subset_selectors1[2][16]; + color_cell_compressor_results subset_results1[2]; + + for ( uint32_t idx = 0; idx < 16; idx++) + { + const uint32_t p = pPartition[idx]; + assert(p < 2); + + subset_colors[p][subset_total_colors1[p]] = pPixels[idx]; + subset_pixel_index1[p][subset_total_colors1[p]] = idx; + subset_total_colors1[p]++; + } + + uint64_t trial_err = 0; + for ( uint32_t subset = 0; subset < 2; subset++) + { + color_cell_compressor_results * pResults = &subset_results1[subset]; + + pResults->m_pSelectors = &subset_selectors1[subset][0]; + pResults->m_pSelectors_temp = selectors_temp; + + uint64_t err = color_cell_compression(1, pParams, pResults, pComp_params, subset_total_colors1[subset], &subset_colors[subset][0], true); + assert(err == pResults->m_best_overall_err); + + trial_err += err; + if (trial_err > best_err) + break; + + } // subset + + if (trial_err < best_err) + { + best_err = trial_err; + opt_results.m_used_lut = subset_results1[0].m_used_lut || subset_results1[1].m_used_lut; + + for ( uint32_t subset = 0; subset < 2; subset++) + { + for ( uint32_t i = 0; i < subset_total_colors1[subset]; i++) + { + const uint32_t pixel_index = subset_pixel_index1[subset][i]; + opt_results.m_selectors[pixel_index] = subset_selectors1[subset][i]; + } + + opt_results.m_low[subset] = subset_results1[subset].m_low_endpoint; + opt_results.m_high[subset] = subset_results1[subset].m_high_endpoint; + + opt_results.m_pbits[subset][0] = subset_results1[subset].m_pbits[0]; + } + } + } + } + + // Mode 0 + if (pComp_params->m_opaque_settings.m_use_mode[0]) + { + solution solutions3[BC7E_MAX_PARTITIONS0]; + uint32_t num_solutions3 = 0; + if (forced_partition >= 0) + { + solutions3[0].m_index = forced_partition; + num_solutions3 = 1; + } + else if (pComp_params->m_opaque_settings.m_max_mode0_partitions_to_try == 1) + { + solutions3[0].m_index = estimate_partition(0, pPixels, pComp_params); + num_solutions3 = 1; + } + else + { + num_solutions3 = estimate_partition_list(0, pPixels, pComp_params, solutions3, pComp_params->m_opaque_settings.m_max_mode0_partitions_to_try); + } + + pParams->m_pSelector_weights = g_bc7_weights3; + pParams->m_pSelector_weightsx = (const vec4F *)&g_bc7_weights3x[0]; + pParams->m_num_selector_weights = 8; + + pParams->m_comp_bits = 4; + pParams->m_has_pbits = true; + pParams->m_endpoints_share_pbit = false; + + pParams->m_perceptual = pComp_params->m_perceptual; + + for ( uint32_t solution_index = 0; solution_index < num_solutions3; solution_index++) + { + const uint32_t best_partition0 = solutions3[solution_index].m_index; + + const int *pPartition = &g_bc7_partition3[best_partition0 * 16]; + + color_quad_i subset_colors[3][16]; + + uint32_t subset_total_colors0[3]; + subset_total_colors0[0] = 0; + subset_total_colors0[1] = 0; + subset_total_colors0[2] = 0; + + int subset_pixel_index0[3][16]; + + for ( uint32_t idx = 0; idx < 16; idx++) + { + const uint32_t p = pPartition[idx]; + + subset_colors[p][subset_total_colors0[p]] = pPixels[idx]; + subset_pixel_index0[p][subset_total_colors0[p]] = idx; + subset_total_colors0[p]++; + } + + color_cell_compressor_results subset_results0[3]; + int subset_selectors0[3][16]; + + uint64_t mode0_err = 0; + for ( uint32_t subset = 0; subset < 3; subset++) + { + color_cell_compressor_results * pResults = &subset_results0[subset]; + + pResults->m_pSelectors = &subset_selectors0[subset][0]; + pResults->m_pSelectors_temp = selectors_temp; + + uint64_t err = color_cell_compression(0, pParams, pResults, pComp_params, subset_total_colors0[subset], &subset_colors[subset][0], true); + assert(err == pResults->m_best_overall_err); + + mode0_err += err; + if (mode0_err > best_err) + break; + } // subset + + if (mode0_err < best_err) + { + best_err = mode0_err; + + opt_results.m_mode = 0; + opt_results.m_index_selector = 0; + opt_results.m_rotation = 0; + opt_results.m_partition = best_partition0; + + for ( uint32_t subset = 0; subset < 3; subset++) + { + for ( uint32_t i = 0; i < subset_total_colors0[subset]; i++) + { + const uint32_t pixel_index = subset_pixel_index0[subset][i]; + + opt_results.m_selectors[pixel_index] = subset_selectors0[subset][i]; + } + + opt_results.m_low[subset] = subset_results0[subset].m_low_endpoint; + opt_results.m_high[subset] = subset_results0[subset].m_high_endpoint; + + opt_results.m_pbits[subset][0] = subset_results0[subset].m_pbits[0]; + opt_results.m_pbits[subset][1] = subset_results0[subset].m_pbits[1]; + } + opt_results.m_used_lut = subset_results0[0].m_used_lut || subset_results0[1].m_used_lut || subset_results0[2].m_used_lut; + } + } + } + + // Mode 3 + if (pComp_params->m_opaque_settings.m_use_mode[3]) + { + pParams->m_pSelector_weights = g_bc7_weights2; + pParams->m_pSelector_weightsx = (const vec4F *)&g_bc7_weights2x[0]; + pParams->m_num_selector_weights = 4; + + pParams->m_comp_bits = 7; + pParams->m_has_pbits = true; + pParams->m_endpoints_share_pbit = false; + + pParams->m_perceptual = pComp_params->m_perceptual; + + for ( uint32_t solution_index = 0; solution_index < num_solutions2; solution_index++) + { + const uint32_t trial_partition = solutions2[solution_index].m_index; + assert(trial_partition < 64); + + const int *pPartition = &g_bc7_partition2[trial_partition * 16]; + + color_quad_i subset_colors[2][16]; + + uint32_t subset_total_colors3[2]; + subset_total_colors3[0] = 0; + subset_total_colors3[1] = 0; + + int subset_pixel_index3[2][16]; + int subset_selectors3[2][16]; + color_cell_compressor_results subset_results3[2]; + + for ( uint32_t idx = 0; idx < 16; idx++) + { + const uint32_t p = pPartition[idx]; + assert(p < 2); + + subset_colors[p][subset_total_colors3[p]] = pPixels[idx]; + + subset_pixel_index3[p][subset_total_colors3[p]] = idx; + + subset_total_colors3[p]++; + } + + uint64_t trial_err = 0; + for ( uint32_t subset = 0; subset < 2; subset++) + { + color_cell_compressor_results * pResults = &subset_results3[subset]; + + pResults->m_pSelectors = &subset_selectors3[subset][0]; + pResults->m_pSelectors_temp = selectors_temp; + + uint64_t err = color_cell_compression(3, pParams, pResults, pComp_params, subset_total_colors3[subset], &subset_colors[subset][0], (num_solutions2 <= 2) || disable_faster_part_selection); + assert(err == pResults->m_best_overall_err); + + trial_err += err; + if (trial_err > best_err) + break; + } // subset + + if (trial_err < best_err) + { + best_err = trial_err; + + opt_results.m_mode = 3; + opt_results.m_index_selector = 0; + opt_results.m_rotation = 0; + opt_results.m_partition = trial_partition; + + for ( uint32_t subset = 0; subset < 2; subset++) + { + for ( uint32_t i = 0; i < subset_total_colors3[subset]; i++) + { + const uint32_t pixel_index = subset_pixel_index3[subset][i]; + + opt_results.m_selectors[pixel_index] = subset_selectors3[subset][i]; + } + + opt_results.m_low[subset] = subset_results3[subset].m_low_endpoint; + opt_results.m_high[subset] = subset_results3[subset].m_high_endpoint; + + opt_results.m_pbits[subset][0] = subset_results3[subset].m_pbits[0]; + opt_results.m_pbits[subset][1] = subset_results3[subset].m_pbits[1]; + } + opt_results.m_used_lut = subset_results3[0].m_used_lut || subset_results3[1].m_used_lut; + } + + } // solution_index + + if ((num_solutions2 > 2) && (opt_results.m_mode == 3) && (!disable_faster_part_selection)) + { + const uint32_t trial_partition = opt_results.m_partition; + assert(trial_partition < 64); + + const int *pPartition = &g_bc7_partition2[trial_partition * 16]; + + color_quad_i subset_colors[2][16]; + + uint32_t subset_total_colors3[2]; + subset_total_colors3[0] = 0; + subset_total_colors3[1] = 0; + + int subset_pixel_index3[2][16]; + int subset_selectors3[2][16]; + color_cell_compressor_results subset_results3[2]; + + for ( uint32_t idx = 0; idx < 16; idx++) + { + const uint32_t p = pPartition[idx]; + assert(p < 2); + + subset_colors[p][subset_total_colors3[p]] = pPixels[idx]; + + subset_pixel_index3[p][subset_total_colors3[p]] = idx; + + subset_total_colors3[p]++; + } + + uint64_t trial_err = 0; + for ( uint32_t subset = 0; subset < 2; subset++) + { + color_cell_compressor_results * pResults = &subset_results3[subset]; + + pResults->m_pSelectors = &subset_selectors3[subset][0]; + pResults->m_pSelectors_temp = selectors_temp; + + uint64_t err = color_cell_compression(3, pParams, pResults, pComp_params, subset_total_colors3[subset], &subset_colors[subset][0], true); + assert(err == pResults->m_best_overall_err); + + trial_err += err; + if (trial_err > best_err) + break; + } // subset + + if (trial_err < best_err) + { + best_err = trial_err; + + for ( uint32_t subset = 0; subset < 2; subset++) + { + for ( uint32_t i = 0; i < subset_total_colors3[subset]; i++) + { + const uint32_t pixel_index = subset_pixel_index3[subset][i]; + + opt_results.m_selectors[pixel_index] = subset_selectors3[subset][i]; + } + + opt_results.m_low[subset] = subset_results3[subset].m_low_endpoint; + opt_results.m_high[subset] = subset_results3[subset].m_high_endpoint; + + opt_results.m_pbits[subset][0] = subset_results3[subset].m_pbits[0]; + opt_results.m_pbits[subset][1] = subset_results3[subset].m_pbits[1]; + } + opt_results.m_used_lut = subset_results3[0].m_used_lut || subset_results3[1].m_used_lut; + } + } + } + + // Mode 5 + if ((!pComp_params->m_perceptual) && (pComp_params->m_opaque_settings.m_use_mode[5])) + { + color_cell_compressor_params params5 = *pParams; + + for ( uint32_t rotation = 0; rotation < 4; rotation++) + { + if ((pComp_params->m_mode5_rotation_mask & (1 << rotation)) == 0) + continue; + + memcpy(params5.m_weights, pParams->m_weights, sizeof(params5.m_weights)); + if (rotation) + swapu(¶ms5.m_weights[rotation - 1], ¶ms5.m_weights[3]); + + color_quad_i rot_pixels[16]; + const color_quad_i * pTrial_pixels = pPixels; + uint32_t trial_lo_a = 255, trial_hi_a = 255; + if (rotation) + { + trial_lo_a = 255; + trial_hi_a = 0; + + for ( uint32_t i = 0; i < 16; i++) + { + color_quad_i c = pPixels[i]; + swapi(&c.m_c[3], &c.m_c[rotation - 1]); + rot_pixels[i] = c; + + trial_lo_a = minimumu(trial_lo_a, c.m_c[3]); + trial_hi_a = maximumu(trial_hi_a, c.m_c[3]); + } + + pTrial_pixels = rot_pixels; + } + + bc7_optimization_results trial_opt_results5; + + uint64_t trial_mode5_err = 0; + + handle_alpha_block_mode5(pTrial_pixels, pComp_params, ¶ms5, trial_lo_a, trial_hi_a, &trial_opt_results5, &trial_mode5_err); + + if (trial_mode5_err < best_err) + { + best_err = trial_mode5_err; + + opt_results = trial_opt_results5; + opt_results.m_rotation = rotation; + } + } // rotation + } + + // Mode 2 + if (pComp_params->m_opaque_settings.m_use_mode[2]) + { + solution solutions3[BC7E_MAX_PARTITIONS2]; + uint32_t num_solutions3 = 0; + if (forced_partition >= 0) + { + solutions3[0].m_index = forced_partition; + num_solutions3 = 1; + } + else if (pComp_params->m_opaque_settings.m_max_mode2_partitions_to_try == 1) + { + solutions3[0].m_index = estimate_partition(2, pPixels, pComp_params); + num_solutions3 = 1; + } + else + { + num_solutions3 = estimate_partition_list(2, pPixels, pComp_params, solutions3, pComp_params->m_opaque_settings.m_max_mode2_partitions_to_try); + } + + pParams->m_pSelector_weights = g_bc7_weights2; + pParams->m_pSelector_weightsx = (const vec4F *)&g_bc7_weights2x[0]; + pParams->m_num_selector_weights = 4; + + pParams->m_comp_bits = 5; + pParams->m_has_pbits = false; + pParams->m_endpoints_share_pbit = false; + + pParams->m_perceptual = pComp_params->m_perceptual; + + for ( uint32_t solution_index = 0; solution_index < num_solutions3; solution_index++) + { + const int32_t best_partition2 = solutions3[solution_index].m_index; + + uint32_t subset_total_colors2[3]; + subset_total_colors2[0] = 0; + subset_total_colors2[1] = 0; + subset_total_colors2[2] = 0; + + int subset_pixel_index2[3][16]; + + const int *pPartition = &g_bc7_partition3[best_partition2 * 16]; + + color_quad_i subset_colors[3][16]; + + for ( uint32_t idx = 0; idx < 16; idx++) + { + const uint32_t p = pPartition[idx]; + + subset_colors[p][subset_total_colors2[p]] = pPixels[idx]; + + subset_pixel_index2[p][subset_total_colors2[p]] = idx; + + subset_total_colors2[p]++; + } + + int subset_selectors2[3][16]; + color_cell_compressor_results subset_results2[3]; + + uint64_t mode2_err = 0; + for ( uint32_t subset = 0; subset < 3; subset++) + { + color_cell_compressor_results * pResults = &subset_results2[subset]; + + pResults->m_pSelectors = &subset_selectors2[subset][0]; + pResults->m_pSelectors_temp = selectors_temp; + + uint64_t err = color_cell_compression(2, pParams, pResults, pComp_params, subset_total_colors2[subset], &subset_colors[subset][0], true); + assert(err == pResults->m_best_overall_err); + + mode2_err += err; + if (mode2_err > best_err) + break; + } // subset + + if (mode2_err < best_err) + { + best_err = mode2_err; + + opt_results.m_mode = 2; + opt_results.m_index_selector = 0; + opt_results.m_rotation = 0; + opt_results.m_partition = best_partition2; + + for ( uint32_t subset = 0; subset < 3; subset++) + { + for ( uint32_t i = 0; i < subset_total_colors2[subset]; i++) + { + const uint32_t pixel_index = subset_pixel_index2[subset][i]; + + opt_results.m_selectors[pixel_index] = subset_selectors2[subset][i]; + } + + opt_results.m_low[subset] = subset_results2[subset].m_low_endpoint; + opt_results.m_high[subset] = subset_results2[subset].m_high_endpoint; + } + opt_results.m_used_lut = subset_results2[0].m_used_lut || subset_results2[1].m_used_lut || subset_results2[2].m_used_lut; + } + } + } + + // Mode 4 + if ((!pComp_params->m_perceptual) && (pComp_params->m_opaque_settings.m_use_mode[4])) + { + color_cell_compressor_params params4 = *pParams; + + for ( uint32_t rotation = 0; rotation < 4; rotation++) + { + if ((pComp_params->m_mode4_rotation_mask & (1 << rotation)) == 0) + continue; + + memcpy(params4.m_weights, pParams->m_weights, sizeof(params4.m_weights)); + if (rotation) + swapu(¶ms4.m_weights[rotation - 1], ¶ms4.m_weights[3]); + + color_quad_i rot_pixels[16]; + const color_quad_i * pTrial_pixels = pPixels; + uint32_t trial_lo_a = 255, trial_hi_a = 255; + if (rotation) + { + trial_lo_a = 255; + trial_hi_a = 0; + + for ( uint32_t i = 0; i < 16; i++) + { + color_quad_i c = pPixels[i]; + swapi(&c.m_c[3], &c.m_c[rotation - 1]); + rot_pixels[i] = c; + + trial_lo_a = minimumu(trial_lo_a, c.m_c[3]); + trial_hi_a = maximumu(trial_hi_a, c.m_c[3]); + } + + pTrial_pixels = rot_pixels; + } + + bc7_optimization_results trial_opt_results4; + + uint64_t trial_mode4_err = best_err; + + handle_alpha_block_mode4(pTrial_pixels, pComp_params, ¶ms4, trial_lo_a, trial_hi_a, &trial_opt_results4, &trial_mode4_err); + + if (trial_mode4_err < best_err) + { + best_err = trial_mode4_err; + + opt_results.m_mode = 4; + opt_results.m_index_selector = trial_opt_results4.m_index_selector; + opt_results.m_rotation = rotation; + opt_results.m_partition = 0; + opt_results.m_used_lut = trial_opt_results4.m_used_lut; + + opt_results.m_low[0] = trial_opt_results4.m_low[0]; + opt_results.m_high[0] = trial_opt_results4.m_high[0]; + + for ( uint32_t i = 0; i < 16; i++) + opt_results.m_selectors[i] = trial_opt_results4.m_selectors[i]; + + for ( uint32_t i = 0; i < 16; i++) + opt_results.m_alpha_selectors[i] = trial_opt_results4.m_alpha_selectors[i]; + } + } // rotation + } + + if (pBest_err) *pBest_err = best_err; + if (pUsed_lut) *pUsed_lut = opt_results.m_used_lut; + encode_bc7_block(pBlock, &opt_results); +} + +// all solid color blocks can be 100% perfectly encoded with just mode 5 +static void handle_block_solid(void * pBlock, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t ca, bool* pUsed_lut = nullptr) +{ + if (pUsed_lut) *pUsed_lut = true; // solid blocks always use the mode-5 optimal-endpoint LUT + uint32_t er = g_bc7_mode_5_optimal_endpoints[cr]; + uint32_t eg = g_bc7_mode_5_optimal_endpoints[cg]; + uint32_t eb = g_bc7_mode_5_optimal_endpoints[cb]; + color_quad_i lp, hp; + color_quad_i_set(&lp, er & 0xFF, eg & 0xFF, eb & 0xFF, ca); + color_quad_i_set(&hp, er >> 8, eg >> 8, eb >> 8, ca); + + bc7_optimization_results opt; + opt.m_mode = 5; + opt.m_low[0] = lp; + opt.m_high[0] = hp; + opt.m_pbits[0][0] = 0; + opt.m_pbits[0][1] = 0; + opt.m_index_selector = 0; + opt.m_rotation = 0; + opt.m_partition = 0; + for ( int i = 0; i < 16; ++i) + opt.m_selectors[i] = BC7E_MODE_5_OPTIMAL_INDEX; + for ( int i = 0; i < 16; ++i) + opt.m_alpha_selectors[i] = 0; + encode_bc7_block(pBlock, &opt); +} + +static void handle_opaque_block_mode6(void * pBlock, const color_quad_i * pPixels, const bc7e_compress_block_params * pComp_params, color_cell_compressor_params * pParams, bool* pUsed_lut = nullptr) +{ + int selectors_temp[16]; + + bc7_optimization_results opt_results = {}; + + uint64_t best_err = UINT64_MAX; + + // Mode 6 + pParams->m_pSelector_weights = g_bc7_weights4; + pParams->m_pSelector_weightsx = (const vec4F * )&g_bc7_weights4x[0]; + pParams->m_num_selector_weights = 16; + + pParams->m_comp_bits = 7; + pParams->m_has_pbits = true; + pParams->m_endpoints_share_pbit = false; + + pParams->m_perceptual = pComp_params->m_perceptual; + + color_cell_compressor_results results6; + results6.m_pSelectors = opt_results.m_selectors; + results6.m_pSelectors_temp = selectors_temp; + + best_err = color_cell_compression(6, pParams, &results6, pComp_params, 16, pPixels, true); + NOTE_UNUSED(best_err); // mode 6 here is unconditional; the returned error isn't compared - silence clang -Wunused-but-set-variable + + opt_results.m_mode = 6; + opt_results.m_index_selector = 0; + opt_results.m_rotation = 0; + opt_results.m_partition = 0; + + opt_results.m_low[0] = results6.m_low_endpoint; + opt_results.m_high[0] = results6.m_high_endpoint; + + opt_results.m_pbits[0][0] = results6.m_pbits[0]; + opt_results.m_pbits[0][1] = results6.m_pbits[1]; + + if (pUsed_lut) *pUsed_lut = results6.m_used_lut; + encode_bc7_block_mode6(pBlock, &opt_results); +} + +// Compress a single 4x4 RGBA block to ONE specified BC7 mode only. Additive API: it does +// not touch bc7e_compress_blocks(). Writes the 16-byte block to pBlock and returns its error. +// mode : 0..7 +// partition : modes 0,1,2,3,7 -> partition pattern index, or -1 to auto-select the +// optimal one (existing estimate_partition* logic). Ignored for 4,5,6. +// rotation : modes 4,5 -> dual-plane component rotation [0..3] (0 = none). +// index_selector : mode 4 -> 0 or 1 (which index set is the scalar channel). +// Forcing an opaque mode (0-3) on a block with alpha drops alpha (those modes force A=255). +uint64_t bc7e_compress_block_single_mode(uint64_t* pBlock, const uint32_t* pPixelsRGBA, const bc7e_compress_block_params* pComp_params, + uint32_t mode, int partition, uint32_t rotation, uint32_t index_selector) +{ + assert(g_codec_initialized); + assert(mode <= 7); + + // Extract the 16 pixels + alpha range (mirrors bc7e_compress_blocks). + const color_quad_u8* pSrcPixels = (const color_quad_u8*)pPixelsRGBA; + color_quad_i temp_pixels[16]; + int lo_a = 255, hi_a = 0; + for (uint32_t i = 0; i < 16; i++) + { + color_quad_u8 c = pSrcPixels[i]; + temp_pixels[i].m_c[0] = c.m_c[0]; + temp_pixels[i].m_c[1] = c.m_c[1]; + temp_pixels[i].m_c[2] = c.m_c[2]; + temp_pixels[i].m_c[3] = c.m_c[3]; + lo_a = min(lo_a, (int)c.m_c[3]); + hi_a = max(hi_a, (int)c.m_c[3]); + } + + color_cell_compressor_params ccparams; + color_cell_compressor_params_clear(&ccparams); + memcpy(ccparams.m_weights, pComp_params->m_weights, sizeof(ccparams.m_weights)); + + // Single-mode params: copy caller's settings, disable every mode, enable only this one. + bc7e_compress_block_params p = *pComp_params; + for (uint32_t i = 0; i < 7; i++) + p.m_opaque_settings.m_use_mode[i] = false; + p.m_alpha_settings.m_use_mode4 = false; + p.m_alpha_settings.m_use_mode5 = false; + p.m_alpha_settings.m_use_mode6 = false; + p.m_alpha_settings.m_use_mode7 = false; + p.m_mode6_only = false; + + // Validate/clamp a caller-supplied partition to the mode's valid range; -1 means auto-select. + // Partition field width: mode 0 is 4 bits (16 patterns); modes 1,2,3,7 are 6 bits (64 patterns); + // modes 4,5,6 have no partition field. + int forced_partition = partition; + if (forced_partition >= 0) + { + const int num_partitions = 1 << g_bc7_partition_bits[mode]; // 1 for non-partitioned modes + assert((num_partitions > 1) && "partition index supplied for a non-partitioned BC7 mode (4/5/6)"); + assert((forced_partition < num_partitions) && "partition index out of range for this BC7 mode (mode 0: 0-15, modes 1/2/3/7: 0-63)"); + if (num_partitions <= 1) + forced_partition = -1; // mode has no partition field; ignore + else if (forced_partition >= num_partitions) + forced_partition = num_partitions - 1; + } + + uint64_t best_err = UINT64_MAX; + + if (mode <= 3) + { + // Opaque-only modes (alpha dropped / forced to 255). + p.m_opaque_settings.m_use_mode[mode] = true; + handle_opaque_block(pBlock, temp_pixels, &p, &ccparams, forced_partition, &best_err); + } + else if ((mode == 4) || (mode == 5)) + { + // A non-zero rotation is only reachable under the handler's linear-metric gate, so + // force linear when one is explicitly requested (keeps the result valid + deterministic). + if (rotation != 0) + p.m_perceptual = false; + + if (mode == 4) + { + p.m_alpha_settings.m_use_mode4 = true; + p.m_alpha_settings.m_use_mode4_rotation = true; + p.m_mode4_rotation_mask = 1u << (rotation & 3); + p.m_mode4_index_mask = 1u << (index_selector & 1); + } + else + { + p.m_alpha_settings.m_use_mode5 = true; + p.m_alpha_settings.m_use_mode5_rotation = true; + p.m_mode5_rotation_mask = 1u << (rotation & 3); + } + handle_alpha_block(pBlock, temp_pixels, &p, &ccparams, (uint32_t)lo_a, (uint32_t)hi_a, -1, &best_err); + } + else if (mode == 6) + { + p.m_alpha_settings.m_use_mode6 = true; + handle_alpha_block(pBlock, temp_pixels, &p, &ccparams, (uint32_t)lo_a, (uint32_t)hi_a, -1, &best_err); + } + else // mode == 7 + { + p.m_alpha_settings.m_use_mode7 = true; + handle_alpha_block(pBlock, temp_pixels, &p, &ccparams, (uint32_t)lo_a, (uint32_t)hi_a, forced_partition, &best_err); + } + + return best_err; +} + + void bc7e_compress_blocks( uint32_t num_blocks, uint64_t * pBlocks, const uint32_t * pPixelsRGBA, const bc7e_compress_block_params * pComp_params, uint8_t * pUsed_lut) +{ + if (!g_codec_initialized) + { + // Caller has forgotten to initialize the codec, or another thread is still working on that. We can't continue. + // What do we do here? + assert(0); + + memset(pBlocks, 0, num_blocks * 16); + return; + } + + color_cell_compressor_params params; + color_cell_compressor_params_clear(¶ms); + + memcpy(params.m_weights, pComp_params->m_weights, sizeof(params.m_weights)); + + assert(pComp_params->m_mode4_rotation_mask != 0); + assert(pComp_params->m_mode4_index_mask != 0); + assert(pComp_params->m_mode5_rotation_mask != 0); + assert(pComp_params->m_uber1_mask != 0); + + for (int32_t block_index = 0; block_index < (int32_t)num_blocks; block_index++) + { + const color_quad_u8 * pSrcPixels = &((const color_quad_u8 *)(pPixelsRGBA))[block_index * 16]; + + color_quad_i temp_pixels[16]; + + int lo_r = 255, hi_r = 0; + int lo_g = 255, hi_g = 0; + int lo_b = 255, hi_b = 0; + float lo_a = 255, hi_a = 0; + + for ( uint32_t i = 0; i < 16; i++) + { + color_quad_u8 c = pSrcPixels[i]; + + int r = c.m_c[0]; + int g = c.m_c[1]; + int b = c.m_c[2]; + int a = c.m_c[3]; + + temp_pixels[i].m_c[0] = r; + temp_pixels[i].m_c[1] = g; + temp_pixels[i].m_c[2] = b; + temp_pixels[i].m_c[3] = a; + + lo_r = min(lo_r, r); hi_r = max(hi_r, r); + lo_g = min(lo_g, g); hi_g = max(hi_g, g); + lo_b = min(lo_b, b); hi_b = max(hi_b, b); + + float fa = (float)(a); + + lo_a = min(lo_a, fa); + hi_a = max(hi_a, fa); + } + + bool all_same = lo_r==hi_r && lo_g==hi_g && lo_b==hi_b && lo_a==hi_a; + + uint64_t * pBlock = &pBlocks[block_index * 2]; + + bool block_used_lut = false; + + cif (all_same) + handle_block_solid(pBlock, lo_r, lo_g, lo_b, float_to_uint8(lo_a), &block_used_lut); + else + { + const bool has_alpha = (lo_a < 255); + // TODO: alpha block mode 6 only + cif (has_alpha) + handle_alpha_block(pBlock, temp_pixels, pComp_params, ¶ms, (int)lo_a, (int)hi_a, -1, nullptr, &block_used_lut); + else + { + if (pComp_params->m_mode6_only) + handle_opaque_block_mode6(pBlock, temp_pixels, pComp_params, ¶ms, &block_used_lut); + else + handle_opaque_block(pBlock, temp_pixels, pComp_params, ¶ms, -1, nullptr, &block_used_lut); + } + } + + if (pUsed_lut) + pUsed_lut[block_index] = block_used_lut ? 1 : 0; + } +} + + void bc7e_compress_block_params_init(bc7e_compress_block_params * p, bool perceptual) +{ + p->m_max_partitions_mode[0] = BC7E_MAX_PARTITIONS0; + p->m_max_partitions_mode[1] = BC7E_MAX_PARTITIONS1; + p->m_max_partitions_mode[2] = BC7E_MAX_PARTITIONS2; + p->m_max_partitions_mode[3] = BC7E_MAX_PARTITIONS3; + p->m_max_partitions_mode[4] = 0; + p->m_max_partitions_mode[5] = 0; + p->m_max_partitions_mode[6] = 0; + p->m_max_partitions_mode[7] = BC7E_MAX_PARTITIONS7; + + p->m_use_luts = true; // default ON, matching bc7e's original behavior; callers may disable (e.g. XBC7 turns it off below lossless Q) + + p->m_perceptual = perceptual; + if (perceptual) + { + p->m_weights[0] = 128; + p->m_weights[1] = 64; + p->m_weights[2] = 16; + p->m_weights[3] = 256; + } + else + { + p->m_weights[0] = 1; + p->m_weights[1] = 1; + p->m_weights[2] = 1; + p->m_weights[3] = 1; + } + + p->m_pbit_search = false; + p->m_mode6_only = false; + p->m_refinement_passes = 1; + p->m_mode4_rotation_mask = 0xF; + p->m_mode4_index_mask = 3; + p->m_mode5_rotation_mask = 0xF; + p->m_uber1_mask = 7; + for ( uint32_t i = 0; i < 7; i++) + p->m_opaque_settings.m_use_mode[i] = true; + p->m_opaque_settings.m_max_mode13_partitions_to_try = 1; + p->m_opaque_settings.m_max_mode0_partitions_to_try = 1; + p->m_opaque_settings.m_max_mode2_partitions_to_try = 1; + p->m_alpha_settings.m_use_mode4 = true; + p->m_alpha_settings.m_use_mode5 = true; + p->m_alpha_settings.m_use_mode6 = true; + p->m_alpha_settings.m_use_mode7 = true; + p->m_alpha_settings.m_use_mode4_rotation = true; + p->m_alpha_settings.m_use_mode5_rotation = true; + p->m_alpha_settings.m_max_mode7_partitions_to_try = 1; + p->m_alpha_settings.m_mode67_error_weight_mul[0] = 1; + p->m_alpha_settings.m_mode67_error_weight_mul[1] = 1; + p->m_alpha_settings.m_mode67_error_weight_mul[2] = 1; + p->m_alpha_settings.m_mode67_error_weight_mul[3] = 1; + p->m_uber_level = 0; +} + + void bc7e_compress_block_params_init_slowest(bc7e_compress_block_params * p, bool perceptual) +{ + bc7e_compress_block_params_init(p, perceptual); + + p->m_opaque_settings.m_max_mode13_partitions_to_try = 4; + p->m_opaque_settings.m_max_mode0_partitions_to_try = 4; + p->m_opaque_settings.m_max_mode2_partitions_to_try = 4; + p->m_alpha_settings.m_max_mode7_partitions_to_try = 4; + + p->m_pbit_search = true; + p->m_uber_level = 4; +} + + void bc7e_compress_block_params_init_veryslow(bc7e_compress_block_params * p, bool perceptual) +{ + bc7e_compress_block_params_init(p, perceptual); + + p->m_opaque_settings.m_max_mode13_partitions_to_try = 2; + p->m_opaque_settings.m_max_mode0_partitions_to_try = 2; + p->m_opaque_settings.m_max_mode2_partitions_to_try = 2; + p->m_alpha_settings.m_max_mode7_partitions_to_try = 2; + + p->m_pbit_search = true; + p->m_uber_level = 2; +} + + void bc7e_compress_block_params_init_slow(bc7e_compress_block_params * p, bool perceptual) +{ + bc7e_compress_block_params_init(p, perceptual); + + p->m_alpha_settings.m_max_mode7_partitions_to_try = 2; + + p->m_pbit_search = true; + p->m_uber_level = 0; +} + + void bc7e_compress_block_params_init_basic(bc7e_compress_block_params * p, bool perceptual) +{ + bc7e_compress_block_params_init(p, perceptual); + + if (perceptual) + { + p->m_opaque_settings.m_use_mode[0] = false; + p->m_opaque_settings.m_use_mode[2] = false; + p->m_opaque_settings.m_use_mode[3] = false; + p->m_opaque_settings.m_use_mode[4] = false; + p->m_opaque_settings.m_use_mode[5] = false; + + p->m_pbit_search = false; + p->m_uber_level = 1; + } + else + { + p->m_max_partitions_mode[1] = 32; + p->m_max_partitions_mode[2] = 32; + p->m_max_partitions_mode[3] = 32; + p->m_max_partitions_mode[7] = 32; + + p->m_opaque_settings.m_use_mode[2] = false; + + p->m_pbit_search = false; + p->m_uber_level = 1; + } +} + + void bc7e_compress_block_params_init_fast(bc7e_compress_block_params * p, bool perceptual) +{ + bc7e_compress_block_params_init(p, perceptual); + + if (perceptual) + { + p->m_opaque_settings.m_use_mode[0] = false; + p->m_opaque_settings.m_use_mode[2] = false; + p->m_opaque_settings.m_use_mode[3] = false; + p->m_opaque_settings.m_use_mode[4] = false; + p->m_opaque_settings.m_use_mode[5] = false; + + p->m_alpha_settings.m_use_mode5 = false; + + p->m_opaque_settings.m_max_mode13_partitions_to_try = 1; + + p->m_pbit_search = false; + p->m_uber_level = 0; + } + else + { + p->m_opaque_settings.m_use_mode[0] = false; + p->m_opaque_settings.m_use_mode[2] = false; + p->m_opaque_settings.m_use_mode[4] = false; + p->m_opaque_settings.m_use_mode[5] = false; + + p->m_alpha_settings.m_use_mode5 = false; + + p->m_opaque_settings.m_max_mode13_partitions_to_try = 2; + + p->m_pbit_search = false; + p->m_uber_level = 0; + } +} + + void bc7e_compress_block_params_init_veryfast(bc7e_compress_block_params * p, bool perceptual) +{ + bc7e_compress_block_params_init(p, perceptual); + + if (perceptual) + { + p->m_opaque_settings.m_use_mode[0] = false; + p->m_opaque_settings.m_use_mode[2] = false; + p->m_opaque_settings.m_use_mode[3] = false; + p->m_opaque_settings.m_use_mode[4] = false; + p->m_opaque_settings.m_use_mode[5] = false; + + p->m_alpha_settings.m_use_mode5 = false; + + p->m_pbit_search = false; + p->m_uber_level = 0; + } + else + { + p->m_opaque_settings.m_use_mode[2] = false; + p->m_opaque_settings.m_use_mode[4] = false; + p->m_opaque_settings.m_use_mode[5] = false; + + p->m_alpha_settings.m_use_mode5 = false; + + p->m_pbit_search = false; + p->m_uber_level = 0; + } +} + + void bc7e_compress_block_params_init_ultrafast(bc7e_compress_block_params * p, bool perceptual) +{ + bc7e_compress_block_params_init(p, perceptual); + + p->m_mode6_only = true; + + p->m_alpha_settings.m_use_mode4 = true; + p->m_alpha_settings.m_use_mode5 = true; + p->m_alpha_settings.m_use_mode7 = false; + + p->m_mode4_rotation_mask = 1+4; + p->m_mode4_index_mask = 3; + p->m_mode5_rotation_mask = 1; + + p->m_pbit_search = false; + p->m_uber_level = 0; +} + +} // namespace bc7e_scalar diff --git a/encoder/basisu_bc7e_scalar.h b/encoder/basisu_bc7e_scalar.h new file mode 100644 index 0000000..8914bb0 --- /dev/null +++ b/encoder/basisu_bc7e_scalar.h @@ -0,0 +1,94 @@ +// bc7e_scalar.h - Pure scalar C++17 port of bc7e.ispc (no ISPC, no SIMD). +// Drop-in alternative BC7 encoder exposing the same surface as the ISPC build, +// but in the bc7e_scalar:: namespace so it can run side-by-side with ispc:: for +// quality comparison. See bc7e.ispc for the original; this is de-SIMD'd to +// encode one 4x4 block at a time. +#pragma once + +#include + +namespace bc7e_scalar +{ + // Mirrors ispc::bc7e_compress_block_params (identical layout/semantics). + struct bc7e_compress_block_params + { + uint32_t m_max_partitions_mode[8]; + + uint32_t m_weights[4]; + + uint32_t m_uber_level; + uint32_t m_refinement_passes; + + uint32_t m_mode4_rotation_mask; + uint32_t m_mode4_index_mask; + uint32_t m_mode5_rotation_mask; + uint32_t m_uber1_mask; + + bool m_perceptual; + bool m_pbit_search; + bool m_mode6_only; + // When false, color_cell_compression() is NOT allowed to use the precomputed + // "one color" optimal-endpoint lookup tables (the solid/allSame and average- + // color paths). Disabling them avoids the extreme-endpoint "trap" blocks that + // decode badly under lossy weight coding. Default TRUE (matches bc7e's original + // behavior); callers disable it when needed (e.g. XBC7 below lossless Q). + // (Repurposed from the former m_unused0 padding bool.) + bool m_use_luts; + + struct + { + uint32_t m_max_mode13_partitions_to_try; + uint32_t m_max_mode0_partitions_to_try; + uint32_t m_max_mode2_partitions_to_try; + bool m_use_mode[7]; + bool m_unused1; + } m_opaque_settings; + + struct + { + uint32_t m_max_mode7_partitions_to_try; + uint32_t m_mode67_error_weight_mul[4]; + + bool m_use_mode4; + bool m_use_mode5; + bool m_use_mode6; + bool m_use_mode7; + + bool m_use_mode4_rotation; + bool m_use_mode5_rotation; + bool m_unused2; + bool m_unused3; + } m_alpha_settings; + }; + + void bc7e_compress_block_init(); + + void bc7e_compress_block_params_init(bc7e_compress_block_params* p, bool perceptual); + void bc7e_compress_block_params_init_basic(bc7e_compress_block_params* p, bool perceptual); + void bc7e_compress_block_params_init_fast(bc7e_compress_block_params* p, bool perceptual); + void bc7e_compress_block_params_init_slow(bc7e_compress_block_params* p, bool perceptual); + void bc7e_compress_block_params_init_slowest(bc7e_compress_block_params* p, bool perceptual); + void bc7e_compress_block_params_init_ultrafast(bc7e_compress_block_params* p, bool perceptual); + void bc7e_compress_block_params_init_veryfast(bc7e_compress_block_params* p, bool perceptual); + void bc7e_compress_block_params_init_veryslow(bc7e_compress_block_params* p, bool perceptual); + + // pUsed_lut (optional, one byte per block): set to 1 if the WINNING encoding of that + // block used the precomputed "one color" optimal-endpoint lookup tables on any subset + // (solid/allSame or average-color path) -- a hint that the block placed endpoints at + // extreme positions and may be "weird"/fragile under lossy weight recoding. 0 otherwise. + void bc7e_compress_blocks(uint32_t num_blocks, uint64_t* pBlocks, const uint32_t* pPixelsRGBA, const bc7e_compress_block_params* pComp_params, uint8_t* pUsed_lut = nullptr); + + // Compress a single 4x4 RGBA block (16 pixels) to ONE specified BC7 mode only, using + // the given settings. Writes the 16-byte block to pBlock and returns its encoding error. + // Additive testing API - does not affect bc7e_compress_blocks(). + // mode : 0..7 + // partition : modes 0,1,2,3,7 -> partition pattern index; -1 = auto-select optimal + // (existing logic). Ignored for modes 4,5,6. + // rotation : modes 4,5 -> dual-plane component rotation [0..3] (0 = none). A non-zero + // rotation forces linear (non-perceptual) error internally. Ignored otherwise. + // index_selector : mode 4 -> 0 or 1 index-set selector. Ignored otherwise. + // Note: forcing an opaque mode (0-3) on a block with alpha drops alpha (A becomes 255). + uint64_t bc7e_compress_block_single_mode(uint64_t* pBlock, const uint32_t* pPixelsRGBA, const bc7e_compress_block_params* pComp_params, + uint32_t mode, int partition = -1, uint32_t rotation = 0, uint32_t index_selector = 0); + +} // namespace bc7e_scalar diff --git a/encoder/basisu_dds_export.cpp b/encoder/basisu_dds_export.cpp new file mode 100644 index 0000000..22663eb --- /dev/null +++ b/encoder/basisu_dds_export.cpp @@ -0,0 +1,569 @@ +// basisu_dds_export.cpp +// Copyright (C) 2019-2025 Binomial LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// See basisu_dds_export.h for an overview. +#include "basisu_dds_export.h" + +#include "basisu_comp.h" +#include "basisu_bc7e_scalar.h" +#include "../transcoder/basisu_transcoder_internal.h" +#include "../transcoder/basisu_transcoder_uastc.h" + +namespace basisu +{ + // --- DXGI format codes (only the ones we emit). --- + enum + { + DXGI_FORMAT_R8G8B8A8_UNORM = 28, + DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29, + DXGI_FORMAT_R8G8_UNORM = 49, + DXGI_FORMAT_R8_UNORM = 61, + DXGI_FORMAT_BC1_UNORM = 71, + DXGI_FORMAT_BC1_UNORM_SRGB = 72, + DXGI_FORMAT_BC2_UNORM = 74, + DXGI_FORMAT_BC2_UNORM_SRGB = 75, + DXGI_FORMAT_BC3_UNORM = 77, + DXGI_FORMAT_BC3_UNORM_SRGB = 78, + DXGI_FORMAT_BC4_UNORM = 80, + DXGI_FORMAT_BC5_UNORM = 83, + DXGI_FORMAT_B5G6R5_UNORM = 85, + DXGI_FORMAT_B5G5R5A1_UNORM = 86, + DXGI_FORMAT_B8G8R8A8_UNORM = 87, + DXGI_FORMAT_B8G8R8A8_UNORM_SRGB = 91, + DXGI_FORMAT_BC7_UNORM = 98, + DXGI_FORMAT_BC7_UNORM_SRGB = 99, + DXGI_FORMAT_B4G4R4A4_UNORM = 115 + }; + + // --- DDS header flag/cap constants. --- + enum + { + DDSD_CAPS = 0x1, DDSD_HEIGHT = 0x2, DDSD_WIDTH = 0x4, DDSD_PITCH = 0x8, + DDSD_PIXELFORMAT = 0x1000, DDSD_MIPMAPCOUNT = 0x20000, DDSD_LINEARSIZE = 0x80000, + + DDPF_FOURCC = 0x4, + + DDSCAPS_COMPLEX = 0x8, DDSCAPS_TEXTURE = 0x1000, DDSCAPS_MIPMAP = 0x400000, + + DDSCAPS2_CUBEMAP = 0x200, DDSCAPS2_CUBEMAP_ALLFACES = 0xFC00, + + DDS_MAGIC = 0x20534444, // "DDS " + DDS_DX10_FOURCC = 0x30315844, // "DX10" + + DDS_DIMENSION_TEXTURE2D = 3, + DDS_RESOURCE_MISC_TEXTURECUBE = 0x4 + }; + + // Per-format static info. + struct dds_format_info + { + const char* m_pToken; + bool m_block_compressed; + uint32_t m_bytes; // bytes per 4x4 block (compressed) or bytes per pixel (uncompressed) + uint32_t m_dxgi_unorm; + uint32_t m_dxgi_srgb; // 0 if no sRGB variant exists + }; + + static const dds_format_info g_dds_format_info[cDDSFmtTotal] = + { + // token block bytes unorm srgb + { "bc1", true, 8, DXGI_FORMAT_BC1_UNORM, DXGI_FORMAT_BC1_UNORM_SRGB }, + { "bc2", true, 16, DXGI_FORMAT_BC2_UNORM, DXGI_FORMAT_BC2_UNORM_SRGB }, + { "bc3", true, 16, DXGI_FORMAT_BC3_UNORM, DXGI_FORMAT_BC3_UNORM_SRGB }, + { "bc4", true, 8, DXGI_FORMAT_BC4_UNORM, 0 }, + { "bc5", true, 16, DXGI_FORMAT_BC5_UNORM, 0 }, + { "bc7", true, 16, DXGI_FORMAT_BC7_UNORM, DXGI_FORMAT_BC7_UNORM_SRGB }, + { "a8r8g8b8", false, 4, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM_SRGB }, + { "a8b8g8r8", false, 4, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB }, + { "r8", false, 1, DXGI_FORMAT_R8_UNORM, 0 }, + { "r8g8", false, 2, DXGI_FORMAT_R8G8_UNORM, 0 }, + { "r5g6b5", false, 2, DXGI_FORMAT_B5G6R5_UNORM, 0 }, + { "a1r5g5b5", false, 2, DXGI_FORMAT_B5G5R5A1_UNORM, 0 }, + { "a4r4g4b4", false, 2, DXGI_FORMAT_B4G4R4A4_UNORM, 0 } + }; + + bool parse_dds_output_format(const char* pToken, dds_output_format& fmt) + { + fmt = cDDSFmtInvalid; + if (!pToken) + return false; + + for (int i = 0; i < (int)cDDSFmtTotal; i++) + { + if (strcasecmp(pToken, g_dds_format_info[i].m_pToken) == 0) + { + fmt = (dds_output_format)i; + return true; + } + } + + // Tolerate the (geometrically impossible) "a1r5g6b5" spelling as an alias for a1r5g5b5. + if (strcasecmp(pToken, "a1r5g6b5") == 0) + { + fmt = cDDSFmtA1R5G5B5; + return true; + } + + return false; + } + + const char* get_dds_output_format_string(dds_output_format fmt) + { + if ((fmt < 0) || (fmt >= cDDSFmtTotal)) + return "?"; + return g_dds_format_info[fmt].m_pToken; + } + + bool dds_output_format_has_srgb_variant(dds_output_format fmt) + { + if ((fmt < 0) || (fmt >= cDDSFmtTotal)) + return false; + return g_dds_format_info[fmt].m_dxgi_srgb != 0; + } + + // --- Little-endian append helpers. --- + static inline void append_u16(uint8_vec& v, uint32_t x) + { + v.push_back((uint8_t)(x & 0xFF)); + v.push_back((uint8_t)((x >> 8) & 0xFF)); + } + static inline void append_u32(uint8_vec& v, uint32_t x) + { + v.push_back((uint8_t)(x & 0xFF)); + v.push_back((uint8_t)((x >> 8) & 0xFF)); + v.push_back((uint8_t)((x >> 16) & 0xFF)); + v.push_back((uint8_t)((x >> 24) & 0xFF)); + } + + // Packs a single RGBA8 texel into the uncompressed output bytes for fmt (truncating 8->N bits, which is + // the exact inverse of the reader's bit-replication expansion). Channel byte orders match the DXGI formats. + static void pack_uncompressed_pixel(uint8_vec& out, const color_rgba& c, dds_output_format fmt) + { + switch (fmt) + { + case cDDSFmtA8R8G8B8: // DXGI B8G8R8A8 -> memory order B,G,R,A + out.push_back(c.b); out.push_back(c.g); out.push_back(c.r); out.push_back(c.a); + break; + case cDDSFmtA8B8G8R8: // DXGI R8G8B8A8 -> memory order R,G,B,A + out.push_back(c.r); out.push_back(c.g); out.push_back(c.b); out.push_back(c.a); + break; + case cDDSFmtR8: // DXGI R8 -> just R + out.push_back(c.r); + break; + case cDDSFmtR8G8: // DXGI R8G8 -> source R into R, source G into G (matches BC5; swizzle input for other mappings) + out.push_back(c.r); out.push_back(c.g); + break; + case cDDSFmtR5G6B5: + append_u16(out, (uint32_t)(((c.r >> 3) << 11) | ((c.g >> 2) << 5) | (c.b >> 3))); + break; + case cDDSFmtA1R5G5B5: + append_u16(out, (uint32_t)((c.a >= 128 ? 0x8000 : 0) | ((c.r >> 3) << 10) | ((c.g >> 3) << 5) | (c.b >> 3))); + break; + case cDDSFmtA4R4G4B4: + append_u16(out, (uint32_t)(((c.a >> 4) << 12) | ((c.r >> 4) << 8) | ((c.g >> 4) << 4) | (c.b >> 4))); + break; + default: + assert(0); + break; + } + } + + // Prebuilt BC7 packing context (built once per build_dds, shared read-only across slices). + struct bc7_pack_context + { + dds_bc7_encoder m_encoder; + uint32_t m_bc7f_flags; // bc7f packer flags (when m_encoder == bc7f) + bc7e_scalar::bc7e_compress_block_params m_bc7e_params; // initialized only when m_encoder == bc7e_scalar + }; + + // Packs one prepared slice image into the bytes for fmt, using logical dims (orig_width/orig_height). + // For block formats this iterates whole 4x4 blocks (the slice image is already block-padded); for + // uncompressed it emits tight rows of exactly orig_width*orig_height pixels. + static void pack_slice(uint8_vec& out, const image& img, uint32_t orig_width, uint32_t orig_height, dds_output_format fmt, const bc7_pack_context& bc7ctx) + { + const dds_format_info& info = g_dds_format_info[fmt]; + + if (info.m_block_compressed) + { + const uint32_t blocks_x = (orig_width + 3) / 4; + const uint32_t blocks_y = (orig_height + 3) / 4; + + for (uint32_t by = 0; by < blocks_y; by++) + { + for (uint32_t bx = 0; bx < blocks_x; bx++) + { + color_rgba blk[16]; + img.extract_block_clamped(blk, bx * 4, by * 4, 4, 4); + + uint8_t dst[16]; + switch (fmt) + { + case cDDSFmtBC1: + basist::encode_bc1(dst, (const uint8_t*)blk, basist::cEncodeBC1HighQuality); + break; + case cDDSFmtBC2: + // 8 bytes explicit 4-bit alpha (one LE 16-bit word per row, texel x at nibble x), then a + // BC1 color block (encode_bc1 emits 4-color blocks, which BC2's always-4-color decode needs). + for (uint32_t ry = 0; ry < 4; ry++) + { + uint32_t row = 0; + for (uint32_t rx = 0; rx < 4; rx++) + row |= ((uint32_t)(blk[ry * 4 + rx].a >> 4)) << (rx * 4); + dst[ry * 2] = (uint8_t)(row & 0xFF); + dst[ry * 2 + 1] = (uint8_t)(row >> 8); + } + basist::encode_bc1(dst + 8, (const uint8_t*)blk, basist::cEncodeBC1HighQuality); + break; + case cDDSFmtBC3: + basist::encode_bc4(dst, &blk[0].a, sizeof(color_rgba)); + basist::encode_bc1(dst + 8, (const uint8_t*)blk, basist::cEncodeBC1HighQuality); + break; + case cDDSFmtBC4: + basist::encode_bc4(dst, &blk[0].r, sizeof(color_rgba)); + break; + case cDDSFmtBC5: + basist::encode_bc4(dst, &blk[0].r, sizeof(color_rgba)); + basist::encode_bc4(dst + 8, &blk[0].g, sizeof(color_rgba)); + break; + case cDDSFmtBC7: + // basist::color_rgba and basisu::color_rgba have identical layout (r,g,b,a uint8_t). + if (bc7ctx.m_encoder == cDDSBC7Encoder_BC7E_Scalar) + { + uint64_t blk64[2]; + bc7e_scalar::bc7e_compress_blocks(1, blk64, reinterpret_cast(blk), &bc7ctx.m_bc7e_params, nullptr); + memcpy(dst, blk64, 16); + } + else + { + basist::bc7f::fast_pack_bc7_auto_rgba(dst, reinterpret_cast(blk), bc7ctx.m_bc7f_flags); + } + break; + default: + assert(0); + break; + } + + out.append(dst, info.m_bytes); + } + } + } + else + { + for (uint32_t y = 0; y < orig_height; y++) + for (uint32_t x = 0; x < orig_width; x++) + pack_uncompressed_pixel(out, img(x, y), fmt); + } + } + + // Builds the DDS magic + DDS_HEADER + DDS_HEADER_DXT10 (148 bytes total). + static void build_dx10_header(uint8_vec& out, uint32_t width, uint32_t height, uint32_t mip_count, + uint32_t array_size, bool is_cubemap, uint32_t dxgi_format, const dds_format_info& info) + { + // dwPitchOrLinearSize (informational; readers generally recompute). + uint32_t pitch_or_linear_size; + uint32_t flags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT; + if (info.m_block_compressed) + { + pitch_or_linear_size = ((width + 3) / 4) * ((height + 3) / 4) * info.m_bytes; + flags |= DDSD_LINEARSIZE; + } + else + { + pitch_or_linear_size = width * info.m_bytes; + flags |= DDSD_PITCH; + } + if (mip_count > 1) + flags |= DDSD_MIPMAPCOUNT; + + uint32_t caps = DDSCAPS_TEXTURE; + if ((mip_count > 1) || is_cubemap || (array_size > 1)) + caps |= DDSCAPS_COMPLEX; + if (mip_count > 1) + caps |= DDSCAPS_MIPMAP; + + const uint32_t caps2 = is_cubemap ? (DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_ALLFACES) : 0; + + append_u32(out, DDS_MAGIC); + + // DDS_HEADER (124 bytes). + append_u32(out, 124); // dwSize + append_u32(out, flags); // dwFlags + append_u32(out, height); // dwHeight + append_u32(out, width); // dwWidth + append_u32(out, pitch_or_linear_size); + append_u32(out, 0); // dwDepth + append_u32(out, mip_count); // dwMipMapCount + for (uint32_t i = 0; i < 11; i++) // dwReserved1[11] + append_u32(out, 0); + + // DDS_PIXELFORMAT (32 bytes) - DX10 indirection. + append_u32(out, 32); // ddspf.dwSize + append_u32(out, DDPF_FOURCC); // ddspf.dwFlags + append_u32(out, DDS_DX10_FOURCC); // ddspf.dwFourCC = "DX10" + append_u32(out, 0); // dwRGBBitCount + append_u32(out, 0); // dwRBitMask + append_u32(out, 0); // dwGBitMask + append_u32(out, 0); // dwBBitMask + append_u32(out, 0); // dwABitMask + + append_u32(out, caps); // dwCaps + append_u32(out, caps2); // dwCaps2 + append_u32(out, 0); // dwCaps3 + append_u32(out, 0); // dwCaps4 + append_u32(out, 0); // dwReserved2 + + // DDS_HEADER_DXT10 (20 bytes). + append_u32(out, dxgi_format); // dxgiFormat + append_u32(out, DDS_DIMENSION_TEXTURE2D); + append_u32(out, is_cubemap ? DDS_RESOURCE_MISC_TEXTURECUBE : 0); // miscFlag + append_u32(out, array_size); // arraySize (number of array elements; cubes for a cubemap) + append_u32(out, 0); // miscFlags2 + } + + bool build_dds(uint8_vec& dds_data, const basis_compressor& comp, const dds_export_params& params, + std::string& error_msg, + uint32_t* pOut_width, uint32_t* pOut_height, + uint32_t* pOut_levels, uint32_t* pOut_layers, uint32_t* pOut_faces) + { + error_msg.clear(); + dds_data.resize(0); + + const dds_output_format fmt = params.m_format; + + if ((fmt < 0) || (fmt >= cDDSFmtTotal)) + { + error_msg = "invalid output format"; + return false; + } + + const dds_format_info& info = g_dds_format_info[fmt]; + + const basisu::vector& slices = comp.get_slice_images(); + const basisu_backend_slice_desc_vec& descs = comp.get_slice_descs(); + + if (!slices.size() || (slices.size() != descs.size())) + { + error_msg = "no prepared slices (was process_source_images() run successfully?)"; + return false; + } + + const bool is_cubemap = (comp.get_params().m_tex_type == basist::cBASISTexTypeCubemapArray); + + // Determine base dims, layer count, mip level count, and face count - mirrors create_ktx2_file(). + uint32_t base_width = 0, base_height = 0, total_layers = 0, total_levels = 0, total_faces = 1; + for (uint32_t i = 0; i < descs.size(); i++) + { + if ((descs[i].m_mip_index == 0) && (!base_width)) + { + base_width = descs[i].m_orig_width; + base_height = descs[i].m_orig_height; + } + + total_layers = maximum(total_layers, descs[i].m_source_file_index + 1); + + if (!descs[i].m_source_file_index) + total_levels = maximum(total_levels, descs[i].m_mip_index + 1); + } + + if (is_cubemap) + { + if ((total_layers % 6) != 0) + { + error_msg = "cubemap source image count is not a multiple of 6"; + return false; + } + total_layers /= 6; + total_faces = 6; + } + + if (!base_width || !base_height || !total_layers || !total_levels) + { + error_msg = "could not determine texture dimensions from the prepared slices"; + return false; + } + + // Build a (layer, face, level) -> slice index map using the same decomposition as create_ktx2_file(). + const uint32_t total_subresources = total_layers * total_faces * total_levels; + basisu::vector slice_map(total_subresources); + for (uint32_t i = 0; i < total_subresources; i++) + slice_map[i] = -1; + + for (uint32_t i = 0; i < descs.size(); i++) + { + // Note: descs[i].m_alpha just flags that the slice contains alpha; in the XUBC7 (m_uastc) path each + // slice holds full RGBA, so it is NOT a separate alpha-only slice (that only happens for ETC1S). + + const uint32_t level_index = descs[i].m_mip_index; + uint32_t layer_index = descs[i].m_source_file_index; + uint32_t face_index = 0; + if (is_cubemap) + { + face_index = layer_index % 6; + layer_index /= 6; + } + + if ((layer_index >= total_layers) || (face_index >= total_faces) || (level_index >= total_levels)) + { + error_msg = "slice descriptor out of range (internal error)"; + return false; + } + + const uint32_t map_index = (layer_index * total_faces + face_index) * total_levels + level_index; + + // Two slices mapping to the same subresource would indicate RGB/alpha slice splitting (ETC1S), which + // we don't expect here since we force the XUBC7 path. + if (slice_map[map_index] >= 0) + { + error_msg = "multiple slices map to the same (layer, face, level) - RGB/alpha slice splitting is not supported"; + return false; + } + + slice_map[map_index] = (int)i; + } + + for (uint32_t i = 0; i < total_subresources; i++) + { + if (slice_map[i] < 0) + { + error_msg = "missing slice for a (layer, face, level) - source images are not all the same size / mip count"; + return false; + } + } + + // Select the DXGI variant (UNORM vs sRGB). + const bool want_srgb = comp.get_params().m_ktx2_and_basis_srgb_transfer_function; + const uint32_t dxgi_format = (want_srgb && info.m_dxgi_srgb) ? info.m_dxgi_srgb : info.m_dxgi_unorm; + if (want_srgb && !info.m_dxgi_srgb && comp.get_params().m_status_output) + printf("Note: format %s has no sRGB DXGI variant; writing UNORM (texel data is unchanged regardless).\n", info.m_pToken); + + // Build the BC7 packing context once (only relevant for the bc7 format). + bc7_pack_context bc7ctx; + memset(&bc7ctx, 0, sizeof(bc7ctx)); + bc7ctx.m_encoder = params.m_bc7_encoder; + switch (clamp(params.m_bc7f_level, cDDSBC7FLevel_Analytical, cDDSBC7FLevel_NonAnalytical)) + { + case cDDSBC7FLevel_Analytical: bc7ctx.m_bc7f_flags = basist::bc7f::cPackBC7FlagDefault; break; + case cDDSBC7FLevel_NonAnalytical: bc7ctx.m_bc7f_flags = basist::bc7f::cPackBC7FlagDefaultNonAnalytical; break; + default: bc7ctx.m_bc7f_flags = basist::bc7f::cPackBC7FlagDefaultPartiallyAnalytical; break; + } + if ((fmt == cDDSFmtBC7) && (params.m_bc7_encoder == cDDSBC7Encoder_BC7E_Scalar)) + { + bc7e_scalar::bc7e_compress_block_init(); + + typedef void (*bc7e_init_func)(bc7e_scalar::bc7e_compress_block_params*, bool); + static const bc7e_init_func s_bc7e_level_init[7] = + { + &bc7e_scalar::bc7e_compress_block_params_init_ultrafast, // 0 + &bc7e_scalar::bc7e_compress_block_params_init_veryfast, // 1 + &bc7e_scalar::bc7e_compress_block_params_init_fast, // 2 + &bc7e_scalar::bc7e_compress_block_params_init_basic, // 3 + &bc7e_scalar::bc7e_compress_block_params_init_slow, // 4 + &bc7e_scalar::bc7e_compress_block_params_init_veryslow, // 5 + &bc7e_scalar::bc7e_compress_block_params_init_slowest, // 6 + }; + const int lvl = clamp(params.m_bc7e_scalar_level, 0, 6); + + // Mirrors the XUBC7 path: for perceptual (sRGB) sources run bc7e in its built-in perceptual error + // mode (it ignores m_weights then); for linear sources run linear and hand it the same RGBA channel + // weights XUASTC LDR / XUBC7 use. + const bool perceptual = comp.get_params().m_perceptual; + s_bc7e_level_init[lvl](&bc7ctx.m_bc7e_params, perceptual); + + if (!perceptual) + { + for (uint32_t i = 0; i < 4; i++) + bc7ctx.m_bc7e_params.m_weights[i] = comp.get_params().m_xuastc_ldr_channel_weights[i]; + } + } + + // Optional debug logging (gated by the compressor's m_debug param, same flag -debug/-verbose set). + const bool debug = comp.get_params().m_debug; + + if (debug) + { + const bool wrote_srgb_dbg = want_srgb && (info.m_dxgi_srgb != 0); + debug_printf("DDS export: format \"%s\" (%s), DXGI=%u, %ux%u, %u level(s), %u layer(s), %u face(s) (%s), %u subresource(s)\n", + info.m_pToken, wrote_srgb_dbg ? "sRGB" : "UNORM", dxgi_format, base_width, base_height, + total_levels, total_layers, total_faces, is_cubemap ? "cubemap" : "2D", total_subresources); + debug_printf("DDS export: %s, %u byte(s) per %s\n", + info.m_block_compressed ? "block-compressed" : "uncompressed", info.m_bytes, + info.m_block_compressed ? "block" : "pixel"); + + if (fmt == cDDSFmtBC7) + { + // Print the FULL BC7 configuration (both encoders' settings) regardless of which encoder + // is active, so the actual values used can be verified at a glance. The "encoder=" line + // states which one is in effect; the line for the other encoder is informational. + const bool using_bc7e = (params.m_bc7_encoder == cDDSBC7Encoder_BC7E_Scalar); + + const int bc7f_lvl = clamp(params.m_bc7f_level, cDDSBC7FLevel_Analytical, cDDSBC7FLevel_NonAnalytical); + const char* pBc7fLvl = (bc7f_lvl == cDDSBC7FLevel_Analytical) ? "analytical" : + (bc7f_lvl == cDDSBC7FLevel_NonAnalytical) ? "non-analytical" : "partially-analytical"; + + const int bc7e_lvl = clamp(params.m_bc7e_scalar_level, 0, 6); + const bool perceptual = comp.get_params().m_perceptual; + const uint32_t* pW = comp.get_params().m_xuastc_ldr_channel_weights; + + debug_printf("DDS export: BC7 encoder=%s\n", using_bc7e ? "bc7e_scalar" : "bc7f"); + debug_printf("DDS export: bc7f level=%d (%s), pack flags=0x%X%s\n", + bc7f_lvl, pBc7fLvl, bc7ctx.m_bc7f_flags, using_bc7e ? " (inactive)" : " (active)"); + debug_printf("DDS export: bc7e_scalar level=%d, perceptual=%u, weights=[%u %u %u %u]%s%s\n", + bc7e_lvl, (uint32_t)perceptual, pW[0], pW[1], pW[2], pW[3], + perceptual ? " (weights ignored in perceptual mode)" : "", + using_bc7e ? " (active)" : " (inactive)"); + } + } + + // Assemble the file into the caller's buffer: header then subresources in DDS order (layer, face, mip). + build_dx10_header(dds_data, base_width, base_height, total_levels, total_layers, is_cubemap, dxgi_format, info); + + if (comp.get_params().m_status_output) + printf("Writing DDS (format \"%s\", %ux%u): %u subresource(s) [%u level(s), %u layer(s), %u face(s)]\n", + info.m_pToken, base_width, base_height, total_subresources, total_levels, total_layers, total_faces); + + for (uint32_t layer = 0; layer < total_layers; layer++) + { + for (uint32_t face = 0; face < total_faces; face++) + { + for (uint32_t level = 0; level < total_levels; level++) + { + const uint32_t map_index = (layer * total_faces + face) * total_levels + level; + const int slice_index = slice_map[map_index]; + + const basisu_backend_slice_desc& desc = descs[slice_index]; + + if (comp.get_params().m_status_output) + printf("DDS: packing subresource %u/%u (layer %u, face %u, level %u): %ux%u\n", + map_index + 1, total_subresources, layer, face, level, desc.m_orig_width, desc.m_orig_height); + + const size_t before_size = dds_data.size(); + pack_slice(dds_data, slices[slice_index], desc.m_orig_width, desc.m_orig_height, fmt, bc7ctx); + + if (debug) + debug_printf(" subresource [layer %u, face %u, level %u]: %ux%u -> %u byte(s)\n", + layer, face, level, desc.m_orig_width, desc.m_orig_height, (uint32_t)(dds_data.size() - before_size)); + } + } + } + + if (pOut_width) *pOut_width = base_width; + if (pOut_height) *pOut_height = base_height; + if (pOut_levels) *pOut_levels = total_levels; + if (pOut_layers) *pOut_layers = total_layers; + if (pOut_faces) *pOut_faces = total_faces; + + return true; + } + +} // namespace basisu diff --git a/encoder/basisu_dds_export.h b/encoder/basisu_dds_export.h new file mode 100644 index 0000000..8b48fce --- /dev/null +++ b/encoder/basisu_dds_export.h @@ -0,0 +1,123 @@ +// basisu_dds_export.h +// Copyright (C) 2019-2025 Binomial LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Basic DX10-style .DDS writer for basisu_tool's -dds command. It consumes the prepared source image +// slices from a basis_compressor that has been run in process_source_images() mode (so all the image +// loading, mipmap generation, and cubemap/array layout is done by the encoder), then packs each slice +// into the requested output format and writes a modern DX10 (DDS_HEADER_DXT10) .dds file. +// +// Supported -dds_format tokens (see parse_dds_output_format): +// Block compressed: bc1 bc2 bc3 bc4 bc5 bc7 +// Uncompressed: a8r8g8b8 r8 r8g8 r5g6b5 a1r5g5b5 a4r4g4b4 +// +// The sRGB-vs-UNORM DXGI variant is selected from the compressor's transfer function flag +// (m_ktx2_and_basis_srgb_transfer_function, set by -srgb/-photo/-linear); only bc1/bc2/bc3/bc7/a8r8g8b8/a8b8g8r8 +// have an sRGB DXGI variant, the rest are always written as UNORM. No transfer-function conversion is +// ever applied to the texel data - we only pick the format tag and pack the bytes as-is. +#pragma once + +#include +#include "../transcoder/basisu.h" // for basisu::uint8_vec + +namespace basisu +{ + class basis_compressor; + + // The output formats supported by the -dds command. + enum dds_output_format + { + cDDSFmtInvalid = -1, + + // Block compressed (packed with the real-time BC encoders). + cDDSFmtBC1, + cDDSFmtBC2, + cDDSFmtBC3, + cDDSFmtBC4, + cDDSFmtBC5, + cDDSFmtBC7, + + // Uncompressed (simple pixel format conversion). + cDDSFmtA8R8G8B8, // DXGI B8G8R8A8 (32bpp, BGRA in memory), has sRGB variant + cDDSFmtA8B8G8R8, // DXGI R8G8B8A8 (32bpp, RGBA in memory), has sRGB variant + cDDSFmtR8, // DXGI R8 (8bpp) + cDDSFmtR8G8, // DXGI R8G8 (16bpp), source R->R, source G->G (like BC5) + cDDSFmtR5G6B5, // DXGI B5G6R5 (16bpp) + cDDSFmtA1R5G5B5, // DXGI B5G5R5A1 (16bpp) + cDDSFmtA4R4G4B4, // DXGI B4G4R4A4 (16bpp) + + cDDSFmtTotal + }; + + // Parses a -dds_format token (case-insensitive) into a dds_output_format. Returns false (and sets + // fmt to cDDSFmtInvalid) on an unrecognized token. + bool parse_dds_output_format(const char* pToken, dds_output_format& fmt); + + // Returns the canonical token string for a format (for messages), or "?" if invalid. + const char* get_dds_output_format_string(dds_output_format fmt); + + // True if the format has a distinct sRGB DXGI variant (bc1/bc3/bc7/a8r8g8b8); false for the formats + // that are always written UNORM. Lets a caller report the actually-emitted variant accurately. + bool dds_output_format_has_srgb_variant(dds_output_format fmt); + + // Which BC7 base packer to use for bc7 output. (Eventually a third encoder may be added.) + enum dds_bc7_encoder + { + cDDSBC7Encoder_Default = -1, // "not specified" sentinel (CLI use); resolves to the dds_export_params default + cDDSBC7Encoder_BC7F = 0, // built-in fast real-time packer (basist::bc7f), default + cDDSBC7Encoder_BC7E_Scalar = 1 // higher-quality (slower) scalar bc7e encoder + }; + + // bc7f quality level -> which analytical mode flags the bc7f packer uses (higher = slower/better). + enum dds_bc7f_level + { + cDDSBC7FLevel_Analytical = 0, // cPackBC7FlagDefault + cDDSBC7FLevel_PartiallyAnalytical = 1, // cPackBC7FlagDefaultPartiallyAnalytical (default) + cDDSBC7FLevel_NonAnalytical = 2 // cPackBC7FlagDefaultNonAnalytical (slowest/best) + }; + + // Input parameters for build_dds(). Intended to grow as more -dds options are added (BC1 quality, etc.). + struct dds_export_params + { + dds_output_format m_format; + + // BC7-only knobs (ignored for non-bc7 formats): + dds_bc7_encoder m_bc7_encoder; // which BC7 base packer + int m_bc7f_level; // dds_bc7f_level [0,2]: analytical / partially / non analytical (bc7f only) + int m_bc7e_scalar_level; // bc7e_scalar quality level [0,6], 0=ultrafast..6=slowest (bc7e_scalar only) + + dds_export_params() + : m_format(cDDSFmtInvalid), + m_bc7_encoder(cDDSBC7Encoder_BC7F), + m_bc7f_level(cDDSBC7FLevel_PartiallyAnalytical), + m_bc7e_scalar_level(2) + { } + explicit dds_export_params(dds_output_format format) + : m_format(format), + m_bc7_encoder(cDDSBC7Encoder_BC7F), + m_bc7f_level(cDDSBC7FLevel_PartiallyAnalytical), + m_bc7e_scalar_level(2) + { } + }; + + // Builds an in-memory DX10 .dds file from the prepared slices of comp (which must have had a successful + // process_source_images() call) into dds_data (which is resize(0)'d first, then appended to). The caller + // is responsible for writing dds_data out (or using it however it likes). Picks the UNORM vs sRGB DXGI + // variant from comp's transfer function flag. On failure returns false and sets error_msg. On success, + // the optional out_* values report the texture's dimensions/structure (for a caller summary). + bool build_dds(uint8_vec& dds_data, const basis_compressor& comp, const dds_export_params& params, + std::string& error_msg, + uint32_t* pOut_width = nullptr, uint32_t* pOut_height = nullptr, + uint32_t* pOut_levels = nullptr, uint32_t* pOut_layers = nullptr, uint32_t* pOut_faces = nullptr); + +} // namespace basisu diff --git a/encoder/basisu_xbc7_encode.cpp b/encoder/basisu_xbc7_encode.cpp new file mode 100644 index 0000000..035e722 --- /dev/null +++ b/encoder/basisu_xbc7_encode.cpp @@ -0,0 +1,3643 @@ +// File: basisu_xbc7_encode.cpp +#include "basisu_xbc7_encode.h" +#include "../transcoder/basisu_transcoder.h" +#include "../zstd/zstd.h" +#include "../transcoder/basisu_xbc7_decoder.h" +#include "basisu_bc7e_scalar.h" + +using basist::fixed16_16; + +namespace basisu { + +namespace xbc7 +{ + using namespace basist::xbc7; // shared XBC7 defs (DCT/syms/enums/eval) now live in basist::xbc7 + + + + class blob_stream_writer + { + public: + blob_stream_writer() {} + + void clear() { m_blobs.clear(); } + + inline uint8_vec& get_blob_vec(uint32_t id) + { + assert(id < BLOB_STREAM_MAX_IDS); + if (id >= m_blobs.size()) + m_blobs.resize(id + 1); + return m_blobs[id]; + } + + // Hot path: append one byte to blob `id`. id must be < 128. + inline void put_byte(uint32_t id, uint8_t b) + { + assert(id < BLOB_STREAM_MAX_IDS); + if (id >= m_blobs.size()) + m_blobs.resize(id + 1); + m_blobs[id].push_back(b); + } + + inline void put_bytes(uint32_t id, const void* p, size_t n) + { + assert(id < BLOB_STREAM_MAX_IDS); + if (id >= m_blobs.size()) + m_blobs.resize(id + 1); + m_blobs[id].append(static_cast(p), n); + } + + void append_bytes(uint32_t id, const uint8_vec& blob) + { + assert(id < BLOB_STREAM_MAX_IDS); + if (id >= m_blobs.size()) + m_blobs.resize(id + 1); + m_blobs[id].append(blob); + } + + // for the per-stream byte accounting / stats dashboard + inline size_t get_blob_size(uint32_t id) const { return (id < m_blobs.size()) ? m_blobs[id].size() : 0; } + inline uint32_t get_num_ids() const { return m_blobs.size_u32(); } + inline const uint8_vec* get_blob_data(uint32_t id) const { return (id < m_blobs.size()) ? &m_blobs[id] : nullptr; } + + // Compress (where it helps) and serialize everything to `out`. + // Returns false only on internal error (blob > 4GB etc). + bool serialize(uint8_vec& out, int zstd_level = 19, uint32_t* pStored_sizes = nullptr) const // pStored_sizes: optional [BLOB_STREAM_MAX_IDS] array, receives stored (post-compression) payload bytes per id + { + out.resize(0); + + uint32_t num_stored = 0; + for (uint32_t id = 0; id < m_blobs.size(); id++) + if (m_blobs[id].size()) + num_stored++; + + if (num_stored > 255) + return false; + + out.push_back(BLOB_STREAM_MAGIC_BEGIN); + out.push_back((uint8_t)num_stored); + + uint8_vec comp_buf; + + for (uint32_t id = 0; id < m_blobs.size(); id++) + { + const uint8_vec& blob = m_blobs[id]; + if (!blob.size()) + continue; + + if (blob.size() > UINT32_MAX) + return false; + + const uint32_t uncomp_size = (uint32_t)blob.size(); + + // Try Zstd; keep it only if it's worth it. Small blobs keep any + // win, but a LARGE blob must shrink by at least 1/400 (0.25%): + // raw blobs are zero-copy at decode (no Zstd frame to walk, no + // arena bytes), so a marginal win costs more than it saves. + const uint8_t* pStored = blob.data(); + uint32_t stored_size = 0; // 0 == raw + + const size_t bound = ZSTD_compressBound(blob.size()); + comp_buf.resize(bound); + + const size_t comp_res = ZSTD_compress(comp_buf.data(), bound, blob.data(), blob.size(), zstd_level); + if ((!ZSTD_isError(comp_res)) && (comp_res < blob.size())) + { + const uint32_t BLOB_RATIO_CHECK_MIN_SIZE = 128; // blobs <= this keep any win + const uint32_t BLOB_MIN_SAVINGS_DENOM = 400; // larger blobs must save >= size/400 + + const uint64_t saved_bytes = (uint64_t)blob.size() - comp_res; + + if ((blob.size() <= BLOB_RATIO_CHECK_MIN_SIZE) || ((saved_bytes * BLOB_MIN_SAVINGS_DENOM) >= (uint64_t)blob.size())) + { + pStored = comp_buf.data(); + stored_size = (uint32_t)comp_res; + } + } + + if (pStored_sizes) + pStored_sizes[id] = stored_size ? stored_size : uncomp_size; + + // directory entry: id+flag byte, then varint size(s) + out.push_back((uint8_t)(id | (stored_size ? 0x80u : 0u))); + if (stored_size) + { + push_varint(out, uncomp_size); + push_varint(out, stored_size); + } + else + push_varint(out, uncomp_size); + + out.append(pStored, stored_size ? stored_size : uncomp_size); + } + + out.push_back(BLOB_STREAM_MAGIC_END); + + return true; + } + + private: + basisu::vector m_blobs; + + static inline void push_varint(uint8_vec& v, uint32_t x) + { + while (x >= 0x80u) + { + v.push_back((uint8_t)(x | 0x80u)); + x >>= 7; + } + v.push_back((uint8_t)x); + } + }; + + + +static const char* g_cand_names[cCandFirstXYDelta] = +{ + "absolute", "left edge", "upper edge", "L+U blend", "reflect left", "reflect upper", + "L+U avg", "L+U strong blend", "gradient", "damped gradient", "diag avg", + "diag edge blend", "upper+diag edge blend", "MED", "GAB", "plane fit", + "diag down-left", "diag down-right" +}; + + +// Post-quantization DCT coefficient statistics, collected from the WINNING +// candidate of each non-solid block, per coded plane. Gives the picture the +// run/coeff/sign stream design needs: where surviving ACs sit in zigzag +// order, their amplitudes and signs, run lengths, and DC behavior. +struct xbc7_dct_stats +{ + uint64_t m_total_planes = 0; + + // DC (range is [-256, 256] by construction) + uint64_t m_dc_zero = 0, m_dc_pos = 0, m_dc_neg = 0; + uint64_t m_dc_sum_abs = 0; + int m_dc_min = 257, m_dc_max = -257; + uint64_t m_dc_mag_hist[10] = {}; + + // AC + uint64_t m_total_acs = 0; + uint64_t m_ac_pos = 0, m_ac_neg = 0; + uint64_t m_ac_sum_abs = 0; + int m_ac_min = 257, m_ac_max = -257; + uint64_t m_ac_mag_hist[10] = {}; + uint64_t m_ac_count_hist[16] = {}; // nonzero ACs per plane, 0..15 + uint64_t m_zig_pos_hist[16] = {}; // zigzag position (1..15) of each nonzero AC + uint64_t m_run_hist[16] = {}; // zero-run length coded before each nonzero AC + uint64_t m_eob_markers = 0; // trailing-zeros records + + // |v| -> bucket: 1, 2, 3, 4, 5-8, 9-16, 17-32, 33-64, 65-128, 129+ + static uint32_t mag_bucket(uint32_t m) + { + if (m <= 4) return m - 1; + if (m <= 8) return 4; + if (m <= 16) return 5; + if (m <= 32) return 6; + if (m <= 64) return 7; + if (m <= 128) return 8; + return 9; + } + + void record_plane(const dct_syms& syms) + { + m_total_planes++; + + const int dc = syms.m_dc; + if (!dc) m_dc_zero++; else if (dc > 0) m_dc_pos++; else m_dc_neg++; + m_dc_sum_abs += (uint64_t)basisu::iabs(dc); + m_dc_min = basisu::minimum(m_dc_min, dc); + m_dc_max = basisu::maximum(m_dc_max, dc); + if (dc) + m_dc_mag_hist[mag_bucket((uint32_t)basisu::iabs(dc))]++; + + uint32_t num_acs = 0; + uint32_t zig_idx = 1; + + for (uint32_t i = 0; i < syms.m_ac_vals.size_u32(); i++) + { + const uint32_t run_len = (uint32_t)syms.m_ac_vals[i].m_num_zeros; + const int c = syms.m_ac_vals[i].m_coeff; + + if (c == INT16_MAX) + { + m_eob_markers++; + break; + } + + zig_idx += run_len; + assert(zig_idx < 16); + if (zig_idx >= 16) + break; + + m_run_hist[basisu::minimum(run_len, 15)]++; + m_zig_pos_hist[zig_idx]++; + + if (c > 0) m_ac_pos++; else m_ac_neg++; + m_ac_sum_abs += (uint64_t)basisu::iabs(c); + m_ac_min = basisu::minimum(m_ac_min, c); + m_ac_max = basisu::maximum(m_ac_max, c); + m_ac_mag_hist[mag_bucket((uint32_t)basisu::iabs(c))]++; + + num_acs++; + zig_idx++; + } + + m_total_acs += num_acs; + m_ac_count_hist[basisu::minimum(num_acs, 15)]++; + } + + // accumulate another collector (per-job stats -> totals); counters add, + // extrema take min/max + void merge(const xbc7_dct_stats& o) + { + m_total_planes += o.m_total_planes; + + m_dc_zero += o.m_dc_zero; m_dc_pos += o.m_dc_pos; m_dc_neg += o.m_dc_neg; + m_dc_sum_abs += o.m_dc_sum_abs; + m_dc_min = basisu::minimum(m_dc_min, o.m_dc_min); + m_dc_max = basisu::maximum(m_dc_max, o.m_dc_max); + for (uint32_t i = 0; i < 10; i++) + m_dc_mag_hist[i] += o.m_dc_mag_hist[i]; + + m_total_acs += o.m_total_acs; + m_ac_pos += o.m_ac_pos; m_ac_neg += o.m_ac_neg; + m_ac_sum_abs += o.m_ac_sum_abs; + m_ac_min = basisu::minimum(m_ac_min, o.m_ac_min); + m_ac_max = basisu::maximum(m_ac_max, o.m_ac_max); + for (uint32_t i = 0; i < 10; i++) + m_ac_mag_hist[i] += o.m_ac_mag_hist[i]; + for (uint32_t i = 0; i < 16; i++) + { + m_ac_count_hist[i] += o.m_ac_count_hist[i]; + m_zig_pos_hist[i] += o.m_zig_pos_hist[i]; + m_run_hist[i] += o.m_run_hist[i]; + } + + m_eob_markers += o.m_eob_markers; + } + + void print() const + { + static const char* s_mag_labels[10] = { "1", "2", "3", "4", "5-8", "9-16", "17-32", "33-64", "65-128", "129+" }; + + fmt_debug_printf("---- WEIGHTS: DCT coefficient statistics (winning candidates, per coded plane) ----\n"); + + const float inv_p = m_total_planes ? (100.0f / (float)m_total_planes) : 0.0f; + const float inv_a = m_total_acs ? (100.0f / (float)m_total_acs) : 0.0f; + + fmt_debug_printf("Coded planes: {}, total nonzero ACs: {}, avg ACs/plane: {}\n", + m_total_planes, m_total_acs, m_total_planes ? ((float)m_total_acs / (float)m_total_planes) : 0.0f); + + fmt_debug_printf("DC: zero: {} ({}%), pos: {} ({}%), neg: {} ({}%), range [{}, {}], mean |DC| (all planes): {}\n", + m_dc_zero, (float)m_dc_zero * inv_p, m_dc_pos, (float)m_dc_pos * inv_p, m_dc_neg, (float)m_dc_neg * inv_p, + m_dc_min, m_dc_max, m_total_planes ? ((float)m_dc_sum_abs / (float)m_total_planes) : 0.0f); + + fmt_debug_printf("DC magnitude histogram (nonzero DCs): "); + for (uint32_t i = 0; i < 10; i++) + if (m_dc_mag_hist[i]) + fmt_debug_printf("{}:{} ", s_mag_labels[i], m_dc_mag_hist[i]); + fmt_debug_printf("\n"); + + fmt_debug_printf("AC: pos: {} ({}%), neg: {} ({}%), range [{}, {}], mean |AC| (per nonzero AC): {}\n", + m_ac_pos, (float)m_ac_pos * inv_a, m_ac_neg, (float)m_ac_neg * inv_a, + m_ac_min, m_ac_max, m_total_acs ? ((float)m_ac_sum_abs / (float)m_total_acs) : 0.0f); + + fmt_debug_printf("AC magnitude histogram: "); + for (uint32_t i = 0; i < 10; i++) + if (m_ac_mag_hist[i]) + fmt_debug_printf("{}:{} ({}%) ", s_mag_labels[i], m_ac_mag_hist[i], (float)m_ac_mag_hist[i] * inv_a); + fmt_debug_printf("\n"); + + fmt_debug_printf("Nonzero ACs per plane: "); + for (uint32_t i = 0; i < 16; i++) + if (m_ac_count_hist[i]) + fmt_debug_printf("{}:{} ({}%) ", i, m_ac_count_hist[i], (float)m_ac_count_hist[i] * inv_p); + fmt_debug_printf("\n"); + + fmt_debug_printf("Zigzag position of nonzero ACs (1..15): "); + for (uint32_t i = 1; i < 16; i++) + fmt_debug_printf("{}:{} ", i, m_zig_pos_hist[i]); + fmt_debug_printf("\n"); + + fmt_debug_printf("Zero-run length before each AC: "); + for (uint32_t i = 0; i < 16; i++) + if (m_run_hist[i]) + fmt_debug_printf("{}:{} ({}%) ", i, m_run_hist[i], (float)m_run_hist[i] * inv_a); + fmt_debug_printf("\n"); + + fmt_debug_printf("Trailing-zeros (EOB) markers: {} ({}% of planes)\n", m_eob_markers, (float)m_eob_markers * inv_p); + } +}; + + + // ---- debug-image color palettes (encode-time visualization only) ---- + // The command byte is split into three field-specific color images rather + // than one hard-to-read grayscale; each image carries a drawn legend. + + // CORE command id (xbc7_command_id, command bits 0-2). + static const color_rgba g_xbc7_command_vis_colors[8] = + { + color_rgba( 0, 0, 255, 255), // 0 RepeatLast - blue + color_rgba( 0, 255, 255, 255), // 1 RepeatUpper - cyan + color_rgba(128, 128, 128, 255), // 2 SolidDPCM - gray + color_rgba(255, 0, 0, 255), // 3 NewConfig - red + color_rgba( 0, 255, 0, 255), // 4 ReuseConfigLeft - green + color_rgba(255, 255, 0, 255), // 5 ReuseConfigUpper - yellow + color_rgba(255, 0, 255, 255), // 6 ReuseConfigLeftDiagonal - magenta + color_rgba(255, 128, 0, 255), // 7 ReuseConfigRightDiagonal - orange + }; + + // ENDPOINT prediction mode (xbc7_command_endpoint_mode, command bits 3-5). + static const color_rgba g_xbc7_endpoint_mode_vis_colors[8] = + { + color_rgba(255, 0, 0, 255), // 0 Raw - red + color_rgba( 0, 200, 0, 255), // 1 DPCM-Left - green + color_rgba( 0, 128, 255, 255), // 2 DPCM-Up - blue + color_rgba(255, 255, 0, 255), // 3 DPCM-LeftDiag - yellow + color_rgba(255, 0, 255, 255), // 4 DPCM-RightDiag - magenta + color_rgba( 0, 255, 255, 255), // 5 DPCM-Index - cyan + color_rgba(255, 128, 0, 255), // 6 DPCM-Left-Sub1 - orange + color_rgba(160, 0, 255, 255), // 7 DPCM-Up-Sub1 - purple + }; + + // WEIGHT mode (xbc7_command_weight_mode, command bit 6). + static const color_rgba g_xbc7_weight_mode_vis_colors[2] = + { + color_rgba( 0, 200, 0, 255), // 0 Raw/DPCM (lossless) - green + color_rgba(255, 0, 0, 255), // 1 DCT (lossy) - red + }; + + // Blocks with no endpoint/weight fields (fully predicted: RepeatLast/ + // RepeatUpper/SolidDPCM) in the endpoint/weight-mode images. + static const color_rgba g_xbc7_vis_na_color(40, 40, 40, 255); + + // AC-COUNT heatmap: number of nonzero DCT weight ACs in a block (DCT-coded + // blocks only). Bucketed via xbc7_ac_count_bucket(). + static const color_rgba g_xbc7_ac_count_vis_colors[6] = + { + color_rgba( 0, 0, 128, 255), // 0 ACs - dark blue (flat / DC only) + color_rgba( 0, 128, 255, 255), // 1-2 ACs - blue + color_rgba( 0, 200, 0, 255), // 3-5 ACs - green + color_rgba(255, 255, 0, 255), // 6-10 ACs - yellow + color_rgba(255, 128, 0, 255), // 11-20 ACs - orange + color_rgba(255, 0, 0, 255), // 21+ ACs - red + }; + + static inline uint32_t xbc7_ac_count_bucket(uint32_t num_acs) + { + if (num_acs == 0) return 0; + if (num_acs <= 2) return 1; + if (num_acs <= 5) return 2; + if (num_acs <= 10) return 3; + if (num_acs <= 20) return 4; + return 5; + } + + // PREDICTOR categories: the ~50 weight predictors (absolute, 17 synthetic, + // 32 block references) collapsed into a handful of buckets; the 32 block + // refs are split by their causal direction. + enum xbc7_pred_category + { + cPredCatAbsolute = 0, // no prediction + cPredCatSynthetic, // edge/gradient synthetic predictors + cPredCatRefLeft, // block ref, same row (dy==0) + cPredCatRefUp, // block ref, straight up (dx==0) + cPredCatRefUpLeft, // block ref, up-and-left + cPredCatRefUpRight, // block ref, up-and-right + cNumPredCategories + }; + + static const color_rgba g_xbc7_predictor_vis_colors[cNumPredCategories] = + { + color_rgba(255, 0, 0, 255), // Absolute - red + color_rgba(255, 128, 0, 255), // Synthetic - orange + color_rgba( 0, 200, 0, 255), // BlockRef Left - green + color_rgba( 0, 128, 255, 255), // BlockRef Up - blue + color_rgba( 0, 255, 255, 255), // BlockRef Up-Left - cyan + color_rgba(255, 0, 255, 255), // BlockRef Up-Right - magenta + }; + + // Map a (cand + amp*cTotalCandidates) predictor index to its category. + static inline uint32_t xbc7_predictor_category(uint32_t pred_index) + { + const uint32_t cand = pred_index % cTotalCandidates; + if (cand == cCandAbsolute) + return cPredCatAbsolute; + if (cand < cCandFirstXYDelta) + return cPredCatSynthetic; + const xbc7_xy_delta d = g_xbc7_xy_deltas[cand - cCandFirstXYDelta]; + if (d.m_dy == 0) return cPredCatRefLeft; // dx<0, same row + if (d.m_dx == 0) return cPredCatRefUp; // straight up + return (d.m_dx < 0) ? cPredCatRefUpLeft : cPredCatRefUpRight; + } + + // Weighted RGBA PSNR of a 16-pixel (4x4) block vs the source, using the + // caller's per-channel weights (pack_options::m_weights), exactly mirroring + // basisu_astc_ldr_encode.cpp's WSSE->PSNR: per-pixel WSSE via + // color_rgba::get_weighted_dist2(), wmse = wsse / (sum_weights * num_pixels), + // then 20*log10(255/sqrt(wmse)), capped for ~zero error. Used by the BC7 + // alt-pack and endpoint-DPCM RDO passes. + static inline float xbc7_block_wsse_psnr(const color_rgba* pOrig, const color_rgba* pDec, const uint32_t comp_weights[4]) + { + uint64_t wsse = 0; + for (uint32_t i = 0; i < 16; i++) + wsse += pDec[i].get_weighted_dist2(pOrig[i], comp_weights); + + const float total_comp_weights = (float)(comp_weights[0] + comp_weights[1] + comp_weights[2] + comp_weights[3]); + const float wmse = (float)wsse / (total_comp_weights * 16.0f); + return (wmse > 1e-5f) ? (20.0f * log10f(255.0f / sqrtf(wmse))) : 10000.0f; + } + + // Standalone: recompute the optimal per-texel weights of a BC7 logical block + // for its FIXED config + endpoints, ignoring its existing (possibly stale) + // weights. out_blk receives in_blk's config + endpoints with freshly optimized + // weights. comp_weights[4] are the per-channel RGBA error weights. No + // dependency on pack_options, so this can be lifted into shared code later. + // + // Per plane: each texel's weight independently selects its interpolation + // between the fixed endpoints, so setting all texels to the same value w and + // decoding reveals every texel's value at w in one decode; sweeping w over all + // 1<fill_box(bx * 4, by * 4, 4, 4, c); + } + + // Color-fill a block across all debug images from its final command byte. + // has_ep_wt is false for fully-predicted blocks (RepeatLast/RepeatUpper/ + // SolidDPCM), which carry no endpoint/weight fields. pred_index is the weight + // predictor (UINT32_MAX == none); ac_count is the nonzero DCT-weight AC count + // (UINT32_MAX == not DCT-coded). UINT32_MAX maps to the n/a color. + static inline void xbc7_vis_fill_command(const xbc7_debug_image_set& dbg, uint32_t bx, uint32_t by, + uint8_t command_byte, bool has_ep_wt, uint32_t pred_index, uint32_t ac_count) + { + xbc7_vis_fill_block(dbg.m_pCommand, bx, by, g_xbc7_command_vis_colors[command_byte & 7]); + + if (has_ep_wt) + { + xbc7_vis_fill_block(dbg.m_pEndpoint_mode, bx, by, g_xbc7_endpoint_mode_vis_colors[(command_byte >> XBC7_COMMAND_ENDPOINT_MODE_SHIFT) & 7]); + xbc7_vis_fill_block(dbg.m_pWeight_mode, bx, by, g_xbc7_weight_mode_vis_colors[(command_byte >> XBC7_COMMAND_WEIGHT_MODE_SHIFT) & 1]); + } + else + { + xbc7_vis_fill_block(dbg.m_pEndpoint_mode, bx, by, g_xbc7_vis_na_color); + xbc7_vis_fill_block(dbg.m_pWeight_mode, bx, by, g_xbc7_vis_na_color); + } + + xbc7_vis_fill_block(dbg.m_pPredictor, bx, by, + (pred_index == UINT32_MAX) ? g_xbc7_vis_na_color : g_xbc7_predictor_vis_colors[xbc7_predictor_category(pred_index)]); + + xbc7_vis_fill_block(dbg.m_pAC_count, bx, by, + (ac_count == UINT32_MAX) ? g_xbc7_vis_na_color : g_xbc7_ac_count_vis_colors[xbc7_ac_count_bucket(ac_count)]); + } + + // ---- legend drawn into a strip at the bottom of each debug image ---- + const uint32_t XBC7_VIS_LEGEND_ROW_H = 10; // px per entry (8px glyph + 2px gap) + + static inline uint32_t xbc7_vis_legend_height(uint32_t num_entries) { return num_entries * XBC7_VIS_LEGEND_ROW_H + 4; } + + struct xbc7_vis_legend_entry { color_rgba m_color; const char* m_pLabel; }; + + // Draws a swatch+label list into the BOTTOM strip of img, overwriting the + // block pixels there (the image stays the source resolution). Everything + // clips, so it's safe even if the strip is taller than the whole image. + static void xbc7_vis_draw_legend(image& img, const xbc7_vis_legend_entry* pEntries, uint32_t num_entries) + { + const uint32_t legend_h = xbc7_vis_legend_height(num_entries); + const uint32_t top_y = (img.get_height() >= legend_h) ? (img.get_height() - legend_h) : 0; + + img.fill_box(0, top_y, img.get_width(), img.get_height() - top_y, color_rgba(16, 16, 16, 255)); + + for (uint32_t i = 0; i < num_entries; i++) + { + const uint32_t y = top_y + 2 + i * XBC7_VIS_LEGEND_ROW_H; + img.fill_box(2, y, 8, 8, pEntries[i].m_color); + img.debug_text(14, y, 1, 1, color_rgba(255, 255, 255, 255), nullptr, false, "%s", pEntries[i].m_pLabel); + } + } + + // ---------------- encoder striping ---------------- + // The main coding pass splits into 1..XBC7_MAX_ENCODER_STRIPES horizontal + // stripes of whole block rows, one job per stripe. Each stripe is coded + // causally from scratch (the encoder never emits references above a + // stripe's first row), so thin stripes cost compression: small images stay + // serial, and once striping kicks in every stripe gets at least + // XBC7_MIN_STRIPE_BLOCK_ROWS rows. + + // (XBC7_MAX_ENCODER_STRIPES is shared with the decoder -- defined in + // basisu_xbc7_decode.h.) + + // images shorter than this many TEXEL rows always encode as one stripe + const uint32_t XBC7_MIN_IMAGE_TEXEL_ROWS_TO_STRIPE = 128; + + // minimum BLOCK rows per stripe once striping kicks in (bounds the + // per-seam compression loss); 16 block rows == 64 texel rows + const uint32_t XBC7_MIN_STRIPE_BLOCK_ROWS = 16; + + + // Computes the encoder's stripe count (1..XBC7_MAX_ENCODER_STRIPES) and + // each stripe's contiguous block-row range. Deliberately a function of + // the image dimensions ONLY -- not of thread availability -- so the + // encoded output is reproducible across machines; with fewer pool threads + // than stripes the extra jobs simply queue. + static uint32_t compute_encoder_stripes( + uint32_t height_in_texels, uint32_t num_blocks_y, + basisu::vector& stripes) + { + uint32_t num_stripes = 1; + + if (height_in_texels >= XBC7_MIN_IMAGE_TEXEL_ROWS_TO_STRIPE) + { + num_stripes = num_blocks_y / XBC7_MIN_STRIPE_BLOCK_ROWS; + num_stripes = basisu::clamp(num_stripes, 1, XBC7_MAX_ENCODER_STRIPES); + } + + num_stripes = basisu::minimum(num_stripes, basisu::maximum(1u, num_blocks_y)); + + compute_stripe_ranges(num_blocks_y, num_stripes, stripes); + + return num_stripes; + } + + // Highest stripe count this image can carry without violating the striping + // rules: at least XBC7_MIN_STRIPE_BLOCK_ROWS per stripe (so none is tiny or + // empty), no more than XBC7_MAX_ENCODER_STRIPES, and 1 for images too short + // to stripe usefully. Mirrors the bound compute_encoder_stripes uses. + static uint32_t max_stripes_for_image(uint32_t height_in_texels, uint32_t num_blocks_y) + { + if (height_in_texels < XBC7_MIN_IMAGE_TEXEL_ROWS_TO_STRIPE) + return 1; + + const uint32_t by_min_size = num_blocks_y / XBC7_MIN_STRIPE_BLOCK_ROWS; // each stripe >= min block rows + return basisu::clamp(by_min_size, 1, XBC7_MAX_ENCODER_STRIPES); + } + + uint32_t pack_options::set_num_stripes_for_image(const image& img, uint32_t desired_num_stripes) + { + const uint32_t height = img.get_height(); + const uint32_t num_blocks_y = (height + 3) / 4; + + const uint32_t max_stripes = max_stripes_for_image(height, num_blocks_y); + + m_num_stripes = basisu::clamp(desired_num_stripes, 1, max_stripes); + return m_num_stripes; + } + + void pack_options::set_rdo_level(uint32_t rdo_level) + { + rdo_level = basisu::minimum(rdo_level, 100); + + if (!rdo_level) + { + // RDO fully disabled: flags off, all drops zero. + m_bc7_alt_pack_enabled = false; + m_endpoint_rdo_enabled = false; + m_repeat_rdo_enabled = false; + m_solid_rdo_enabled = false; + + m_bc7_alt_max_psnr_drop = 0.0f; + m_endpoint_rdo_max_psnr_drop = 0.0f; + m_repeat_rdo_max_psnr_drop = 0.0f; + m_solid_rdo_max_psnr_drop = 0.0f; + m_ac_trunc_rdo_max_psnr_drop = 0.0f; + return; + } + + const float frac = (float)rdo_level / 100.0f; // (0, 1] + const float general_drop = 10.0f * frac; // BC7 alt-pack / endpoint-DPCM / AC-trunc -> up to 10 dB + const float reuse_drop = 4.0f * frac; // repeat / solid block reuse -> up to 4 dB + + m_bc7_alt_pack_enabled = true; + m_bc7_alt_max_psnr_drop = general_drop; + + m_endpoint_rdo_enabled = true; + m_endpoint_rdo_max_psnr_drop = general_drop; + + m_ac_trunc_rdo_max_psnr_drop = general_drop; // no enable flag; >0 enables + + m_repeat_rdo_enabled = true; + m_repeat_rdo_max_psnr_drop = reuse_drop; + + m_solid_rdo_enabled = true; + m_solid_rdo_max_psnr_drop = reuse_drop; + } + + // ------------------------- XBC7 encoding statistics ------------------------- + // Populated inline during pack_image (negligible cost), printed when + // opts.m_print_stats is set. Three layers: where the bytes go (per-blob + // accounting), what the encoder chose (command/mode/predictor histograms), + // and why (residual and coefficient distributions). + struct xbc7_pack_stats + { + // context + uint32_t m_width = 0, m_height = 0, m_total_blocks = 0, m_dct_q = 0; + bool m_has_alpha = false; + basisu::vector m_stripes; + + // choices + uint64_t m_cmd_hist[8] = {}; + uint64_t m_mode_hist[8] = {}; // coded (non-repeat, non-solid) blocks + uint64_t m_pred_hist[cTotalCandidates * 4] = {}; + uint64_t m_ep_raw_blocks = 0, m_ep_dpcm_blocks = 0, m_ep_dpcm_subsets = 0; + + // AC-truncation RDO + uint64_t m_ac_trunc_pruned = 0; // weight-DCT AC coeffs zeroed + uint64_t m_ac_trunc_blocks = 0; // blocks where >=1 coeff was pruned + uint64_t m_ep_mode_hist[8] = {}; // xbc7_command_endpoint_mode of every coded block + uint64_t m_ep_index_hist[NUM_XY_DELTAS] = {}; // XY-delta table index of every EP block reference + uint64_t m_pbit_delta_bits = 0, m_pbit_delta_nonzero = 0; + + // distributions: EP residuals by [width class][stream channel slot], + // wrapped bytes interpreted as int8 (the zero-peaked reading) + uint64_t m_ep_resid_count[2][4] = {}; // [0]=fine, [1]=coarse + uint64_t m_ep_resid_sum_abs[2][4] = {}; + uint64_t m_ep_resid_zero[2][4] = {}; + + uint64_t m_solid_delta_count[4] = {}; + uint64_t m_solid_delta_sum_abs[4] = {}; + uint64_t m_solid_delta_zero[4] = {}; + + // weights: residual DCT vs lossless DPCM decision + tuning data + uint32_t m_wt_alpha_pct = 0; + uint64_t m_wt_dct_blocks = 0, m_wt_dpcm_blocks = 0; + uint64_t m_wt_dct_est_bits_total = 0, m_wt_dpcm_est_bits_total = 0, m_wt_chosen_est_bits = 0; + + uint64_t m_wt_block_acs_hist_dct[33] = {}; // total ACs per block (clamped), split by which mode won + uint64_t m_wt_block_acs_hist_dpcm[33] = {}; + uint64_t m_wt_dpcm_pred_hist[cTotalCandidates * 4] = {}; + uint64_t m_wt_resid_count[3] = {}, m_wt_resid_zero[3] = {}, m_wt_resid_sum_abs[3] = {}; // [weight_bits - 2] + uint64_t m_wt_resid_mag_hist[3][9] = {}; // [weight_bits - 2][min(|centered resid|, 8)] + + xbc7_dct_stats m_dct_stats; // winning-candidate coefficient stats + + // where the bytes go (filled at serialize time) + uint32_t m_blob_raw_size[BLOB_STREAM_MAX_IDS] = {}; + uint32_t m_blob_stored_size[BLOB_STREAM_MAX_IDS] = {}; + uint32_t m_total_file_size = 0; + + // order-0 byte histograms of the pre-Zstd blob contents + uint64_t m_blob_hist[cBlobFirstUnused][256] = {}; + + void record_blob_bytes(uint32_t id, const uint8_vec* pBlob) + { + if ((id >= (uint32_t)cBlobFirstUnused) || (!pBlob)) + return; + for (size_t i = 0; i < pBlob->size(); i++) + m_blob_hist[id][(*pBlob)[i]]++; + } + + void record_ep_residuals(bool fine_class, const uint8_t* pResiduals, uint32_t num_residuals) + { + const uint32_t cls = fine_class ? 0 : 1; + for (uint32_t i = 0; i < num_residuals; i++) + { + const uint32_t chan = i >> 1; + const int v = (int8_t)pResiduals[i]; + m_ep_resid_count[cls][chan]++; + m_ep_resid_sum_abs[cls][chan] += (uint64_t)basisu::iabs(v); + if (!v) + m_ep_resid_zero[cls][chan]++; + } + } + + // Accumulate a per-stripe (per-job) collector into this one. Pure + // counters only: the context fields (dims/Q/stripes/alpha) and the + // blob size/histogram arrays are owned by the final stats object and + // filled outside the jobs. + void merge(const xbc7_pack_stats& o) + { + for (uint32_t i = 0; i < 8; i++) + { + m_cmd_hist[i] += o.m_cmd_hist[i]; + m_mode_hist[i] += o.m_mode_hist[i]; + m_ep_mode_hist[i] += o.m_ep_mode_hist[i]; + } + + for (uint32_t i = 0; i < cTotalCandidates * 4; i++) + { + m_pred_hist[i] += o.m_pred_hist[i]; + m_wt_dpcm_pred_hist[i] += o.m_wt_dpcm_pred_hist[i]; + } + + m_ep_raw_blocks += o.m_ep_raw_blocks; + m_ep_dpcm_blocks += o.m_ep_dpcm_blocks; + m_ep_dpcm_subsets += o.m_ep_dpcm_subsets; + + m_ac_trunc_pruned += o.m_ac_trunc_pruned; + m_ac_trunc_blocks += o.m_ac_trunc_blocks; + + for (uint32_t i = 0; i < NUM_XY_DELTAS; i++) + m_ep_index_hist[i] += o.m_ep_index_hist[i]; + + m_pbit_delta_bits += o.m_pbit_delta_bits; + m_pbit_delta_nonzero += o.m_pbit_delta_nonzero; + + for (uint32_t cls = 0; cls < 2; cls++) + { + for (uint32_t c = 0; c < 4; c++) + { + m_ep_resid_count[cls][c] += o.m_ep_resid_count[cls][c]; + m_ep_resid_sum_abs[cls][c] += o.m_ep_resid_sum_abs[cls][c]; + m_ep_resid_zero[cls][c] += o.m_ep_resid_zero[cls][c]; + } + } + + for (uint32_t c = 0; c < 4; c++) + { + m_solid_delta_count[c] += o.m_solid_delta_count[c]; + m_solid_delta_sum_abs[c] += o.m_solid_delta_sum_abs[c]; + m_solid_delta_zero[c] += o.m_solid_delta_zero[c]; + } + + m_wt_dct_blocks += o.m_wt_dct_blocks; + m_wt_dpcm_blocks += o.m_wt_dpcm_blocks; + m_wt_dct_est_bits_total += o.m_wt_dct_est_bits_total; + m_wt_dpcm_est_bits_total += o.m_wt_dpcm_est_bits_total; + m_wt_chosen_est_bits += o.m_wt_chosen_est_bits; + + for (uint32_t i = 0; i <= 32; i++) + { + m_wt_block_acs_hist_dct[i] += o.m_wt_block_acs_hist_dct[i]; + m_wt_block_acs_hist_dpcm[i] += o.m_wt_block_acs_hist_dpcm[i]; + } + + for (uint32_t cls = 0; cls < 3; cls++) + { + m_wt_resid_count[cls] += o.m_wt_resid_count[cls]; + m_wt_resid_zero[cls] += o.m_wt_resid_zero[cls]; + m_wt_resid_sum_abs[cls] += o.m_wt_resid_sum_abs[cls]; + + for (uint32_t m = 0; m < 9; m++) + m_wt_resid_mag_hist[cls][m] += o.m_wt_resid_mag_hist[cls][m]; + } + + m_dct_stats.merge(o.m_dct_stats); + } + + // fmt_variants rejects format specs on strings, so pad manually + static std::string pad(const char* pStr, size_t n) + { + std::string s(pStr); + if (s.size() < n) + s.resize(n, ' '); + return s; + } + + void print() const + { + static const char* s_blob_names[cBlobFirstUnused] = + { + "header", "commands", "cfg: block_config", "cfg: partition2", "cfg: partition3", + "wt: predictors", "wt-dct: dc_small", "wt-dct: dc_large", "wt-dct: ac_coeffs", + "wt-dct: signs", "ep: pbits", + "ep: fine_r", "ep: fine_g", "ep: fine_b", "ep: fine_a", + "ep: coarse_r", "ep: coarse_g", "ep: coarse_b", "ep: coarse_a", + "ep: raw", "ep: blk_index", "wt-dpcm: absolute", "solid: rgba_deltas", + "wt-dpcm: resid_2", "wt-dpcm: resid_3", "wt-dpcm: resid_4", "seek_table" + }; + static const char* s_cmd_names[8] = + { + "repeat_last", "repeat_upper", "solid_dpcm", "new_config", + "reuse_left", "reuse_upper", "reuse_ldiag", "reuse_rdiag" + }; + + const float inv_blocks = m_total_blocks ? (100.0f / (float)m_total_blocks) : 0.0f; + + fmt_debug_printf("\n========== XBC7 pack stats ==========\n"); + fmt_debug_printf("{}x{}, {} blocks, Q={}, has_alpha={}\n", + m_width, m_height, m_total_blocks, m_dct_q, m_has_alpha); + + { + std::string s(string_format("encoder stripes: %u", m_stripes.size_u32())); + for (uint32_t i = 0; i < m_stripes.size_u32(); i++) + s += string_format("%s block rows %u-%u", i ? "," : " --", m_stripes[i].m_first_block_row, m_stripes[i].m_first_block_row + m_stripes[i].m_num_block_rows - 1); + fmt_debug_printf("{}\n", s); + } + + // ---- per-blob byte accounting ---- + fmt_debug_printf("\n---- blob accounting (raw -> stored after Zstd) ----\n"); + fmt_debug_printf("{}{}{}{}{}{}\n", + pad("blob", 20), pad("raw", 10), pad("stored", 10), pad("ratio", 8), pad("%file", 8), "bits/blk"); + + uint64_t total_raw = 0, total_stored = 0; + for (uint32_t id = 0; id < (uint32_t)cBlobFirstUnused; id++) + { + if (!m_blob_raw_size[id]) + continue; + + total_raw += m_blob_raw_size[id]; + total_stored += m_blob_stored_size[id]; + + fmt_debug_printf("{}{}{}{}{}{.3}\n", + pad(s_blob_names[id], 20), + pad(string_format("%u", m_blob_raw_size[id]).c_str(), 10), + pad(string_format("%u", m_blob_stored_size[id]).c_str(), 10), + pad(string_format("%.3f", m_blob_stored_size[id] ? ((float)m_blob_raw_size[id] / (float)m_blob_stored_size[id]) : 0.0f).c_str(), 8), + pad(string_format("%.2f", m_total_file_size ? ((float)m_blob_stored_size[id] * 100.0f / (float)m_total_file_size) : 0.0f).c_str(), 8), + m_total_blocks ? ((float)m_blob_stored_size[id] * 8.0f / (float)m_total_blocks) : 0.0f); + } + + const uint64_t overhead = (uint64_t)m_total_file_size - total_stored; + fmt_debug_printf("{}{}{}\n", pad("TOTAL", 20), + pad(string_format("%llu", (unsigned long long)total_raw).c_str(), 10), + pad(string_format("%llu", (unsigned long long)total_stored).c_str(), 10)); + fmt_debug_printf("container+dir overhead: {} bytes\n", overhead); + fmt_debug_printf("file: {} bytes, {.3} bits/block, {.4} bpp\n", + m_total_file_size, + m_total_blocks ? ((float)m_total_file_size * 8.0f / (float)m_total_blocks) : 0.0f, + (m_width && m_height) ? ((float)m_total_file_size * 8.0f / (float)(m_width * m_height)) : 0.0f); + + // ---- group rollup: where the bytes go, raw vs stored ---- + { + // m_super buckets the fine groups into the coarse summary: + // 0 = commands+config, 1 = endpoints/pbits, 2 = weights, 3 = other + struct group_def { const char* m_pName; uint8_t m_super; uint8_t m_ids[12]; uint32_t m_num_ids; }; + static const group_def s_groups[] = + { + { "commands", 0, { cBlobCommands }, 1 }, + { "config/partition", 0, { cBlobBC7BlockConfig, cBlobPartition2, cBlobPartition3 }, 3 }, + { "endpoints/pbits", 1, { cBlobEPDeltaFineR, cBlobEPDeltaFineG, cBlobEPDeltaFineB, cBlobEPDeltaFineA, + cBlobEPDeltaCoarseR, cBlobEPDeltaCoarseG, cBlobEPDeltaCoarseB, cBlobEPDeltaCoarseA, + cBlobEPRaw, cBlobEPBlockIndex, cBlobPBits }, 11 }, + { "wt predictors", 2, { cBlobWeightPredictors }, 1 }, + { "wt dct", 2, { cBlobDCCoeffsSmall, cBlobDCCoeffsLarge, cBlobACCoeffs, cBlobCoeffSigns }, 4 }, + { "wt dpcm", 2, { cBlobRawWeightBits, cBlobDPCMWeightResid2, cBlobDPCMWeightResid3, cBlobDPCMWeightResid4 }, 4 }, + { "solid", 3, { cBlobSolidRGBADeltas }, 1 }, + { "header", 3, { cBlobHeader }, 1 }, + { "seek_table", 3, { cBlobStripeSeekTable }, 1 }, + }; + const uint32_t num_groups = (uint32_t)(sizeof(s_groups) / sizeof(s_groups[0])); + + const float inv_file = m_total_file_size ? (100.0f / (float)m_total_file_size) : 0.0f; + + fmt_debug_printf("\n---- group rollup ----\n"); + fmt_debug_printf("{}{}{}{}\n", pad("group", 19), pad("raw", 10), pad("stored", 10), "%file"); + + uint64_t group_stored_total = 0; + uint64_t super_raw[4] = {}, super_stored[4] = {}; + + for (uint32_t g = 0; g < num_groups; g++) + { + uint64_t raw = 0, stored = 0; + for (uint32_t i = 0; i < s_groups[g].m_num_ids; i++) + { + raw += m_blob_raw_size[s_groups[g].m_ids[i]]; + stored += m_blob_stored_size[s_groups[g].m_ids[i]]; + } + group_stored_total += stored; + + super_raw[s_groups[g].m_super] += raw; + super_stored[s_groups[g].m_super] += stored; + + if (!raw) + continue; + + fmt_debug_printf("{}{}{}{.2}\n", + pad(s_groups[g].m_pName, 19), + pad(string_format("%llu", (unsigned long long)raw).c_str(), 10), + pad(string_format("%llu", (unsigned long long)stored).c_str(), 10), + (float)stored * inv_file); + } + + const uint64_t container_ovh = (uint64_t)m_total_file_size - group_stored_total; + fmt_debug_printf("{}{}{}{.2}\n", pad("container", 19), pad("", 10), + pad(string_format("%llu", (unsigned long long)container_ovh).c_str(), 10), (float)container_ovh * inv_file); + fmt_debug_printf("{}{}{}{}\n", pad("TOTAL", 19), pad("", 10), + pad(string_format("%u", m_total_file_size).c_str(), 10), "100.00"); + + // coarse three-bucket summary + static const char* s_super_names[4] = { "= commands+config", "= endpoints/pbits", "= weights (all)", "= other" }; + + fmt_debug_printf("\n"); + for (uint32_t s = 0; s < 4; s++) + { + if (!super_raw[s]) + continue; + + fmt_debug_printf("{}{}{}{.2}\n", + pad(s_super_names[s], 19), + pad(string_format("%llu", (unsigned long long)super_raw[s]).c_str(), 10), + pad(string_format("%llu", (unsigned long long)super_stored[s]).c_str(), 10), + (float)super_stored[s] * inv_file); + } + } + + // ---- per-blob order-0 symbol stats ---- + // H = Shannon entropy of the raw byte stream (bits/byte); ideal = the + // order-0 entropy-coded size. stored/ideal < 1 means Zstd's matches + + // higher-order context beat a memoryless coder; > 1 means an entropy + // coder (FSE/rANS over this alphabet) would beat what Zstd achieves. + fmt_debug_printf("\n---- blob symbol stats (order-0, pre-Zstd) ----\n"); + fmt_debug_printf("{}{}{}{}{}{}{}\n", + pad("blob", 20), pad("bytes", 10), pad("syms", 6), pad("H b/byte", 10), pad("ideal", 10), pad("stored", 10), "st/id"); + + for (uint32_t id = 0; id < (uint32_t)cBlobFirstUnused; id++) + { + uint64_t n = 0; + uint32_t distinct = 0; + for (uint32_t s = 0; s < 256; s++) + { + n += m_blob_hist[id][s]; + if (m_blob_hist[id][s]) + distinct++; + } + if (!n) + continue; + + double entropy = 0.0; + for (uint32_t s = 0; s < 256; s++) + { + if (!m_blob_hist[id][s]) + continue; + const double p = (double)m_blob_hist[id][s] / (double)n; + entropy -= p * std::log2(p); + } + + const uint64_t ideal_bytes = (uint64_t)std::ceil(entropy * (double)n / 8.0); + + fmt_debug_printf("{}{}{}{}{}{}{.3}\n", + pad(s_blob_names[id], 20), + pad(string_format("%llu", (unsigned long long)n).c_str(), 10), + pad(string_format("%u", distinct).c_str(), 6), + pad(string_format("%.3f", entropy).c_str(), 10), + pad(string_format("%llu", (unsigned long long)ideal_bytes).c_str(), 10), + pad(string_format("%u", m_blob_stored_size[id]).c_str(), 10), + ideal_bytes ? ((float)m_blob_stored_size[id] / (float)ideal_bytes) : 0.0f); + + // condensed top symbols: up to 6, plus a count of the rest + uint64_t hist[256]; + memcpy(hist, m_blob_hist[id], sizeof(hist)); + + std::string tops(" top:"); + uint32_t shown = 0; + uint64_t shown_count = 0; + for (; shown < 6; shown++) + { + uint32_t best_s = 0; + uint64_t best_c = 0; + for (uint32_t s = 0; s < 256; s++) + { + if (hist[s] > best_c) + { + best_c = hist[s]; + best_s = s; + } + } + if (!best_c) + break; + + hist[best_s] = 0; + shown_count += best_c; + tops += string_format(" %u:%llu(%.1f%%)", best_s, (unsigned long long)best_c, (double)best_c * 100.0 / (double)n); + } + if (distinct > shown) + tops += string_format(" +%u more (%.1f%%)", distinct - shown, (double)(n - shown_count) * 100.0 / (double)n); + + fmt_debug_printf("{}\n", tops); + } + + // ---- commands / modes ---- + fmt_debug_printf("\n---- COMMANDS: per-block command histogram ----\n"); + for (uint32_t i = 0; i < 8; i++) + if (m_cmd_hist[i]) + fmt_debug_printf("{}: {} ({.2}%)\n", pad(s_cmd_names[i], 13), m_cmd_hist[i], (float)m_cmd_hist[i] * inv_blocks); + + fmt_debug_printf("\n---- CONFIG: BC7 modes of coded blocks ----\n"); + for (uint32_t i = 0; i < 8; i++) + if (m_mode_hist[i]) + fmt_debug_printf("mode {}: {} ({.2}%)\n", i, m_mode_hist[i], (float)m_mode_hist[i] * inv_blocks); + + // ---- endpoints ---- + fmt_debug_printf("\n---- ENDPOINTS: predictor modes + residuals ----\n"); + fmt_debug_printf("raw blocks: {}, dpcm blocks: {} ({} subsets)\n", m_ep_raw_blocks, m_ep_dpcm_blocks, m_ep_dpcm_subsets); + fmt_debug_printf("ep modes: raw {}, left {}, up {}, ldiag {}, rdiag {}, index {}, left_s1 {}, up_s1 {}\n", + m_ep_mode_hist[0], m_ep_mode_hist[1], m_ep_mode_hist[2], m_ep_mode_hist[3], m_ep_mode_hist[4], m_ep_mode_hist[5], + m_ep_mode_hist[6], m_ep_mode_hist[7]); + + if (m_ep_mode_hist[(uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMBlockIndex]) + { + std::string idx_line("ep index deltas:"); + for (uint32_t i = 0; i < NUM_XY_DELTAS; i++) + if (m_ep_index_hist[i]) + idx_line += string_format(" (%i,%i):%llu", (int)g_xbc7_xy_deltas[i].m_dx, (int)g_xbc7_xy_deltas[i].m_dy, (unsigned long long)m_ep_index_hist[i]); + fmt_debug_printf("{}\n", idx_line); + } + if (m_pbit_delta_bits) + fmt_debug_printf("pbit deltas: {} bits, nonzero: {} ({.2}%)\n", + m_pbit_delta_bits, m_pbit_delta_nonzero, (float)m_pbit_delta_nonzero * 100.0f / (float)m_pbit_delta_bits); + + static const char* s_class_names[2] = { "fine", "coarse" }; + static const char* s_chan_names[4] = { "R", "G", "B", "A" }; + for (uint32_t cls = 0; cls < 2; cls++) + { + for (uint32_t c = 0; c < 4; c++) + { + if (!m_ep_resid_count[cls][c]) + continue; + fmt_debug_printf("ep resid {} {}: n={}, mean|d|={.3}, zero={.2}%\n", + pad(s_class_names[cls], 6), s_chan_names[c], + m_ep_resid_count[cls][c], + (float)m_ep_resid_sum_abs[cls][c] / (float)m_ep_resid_count[cls][c], + (float)m_ep_resid_zero[cls][c] * 100.0f / (float)m_ep_resid_count[cls][c]); + } + } + + // ---- weight predictors (DCT-mode blocks; DPCM-mode predictors are in the next section) ---- + fmt_debug_printf("\n---- WEIGHTS: DCT-mode predictors ----\n"); + { + uint64_t class_totals[3] = { 0, 0, 0 }; // absolute / synthetic / dictionary + uint64_t amp_totals[4] = { 0, 0, 0, 0 }; + uint64_t coded_blocks = 0; + + for (uint32_t c = 0; c < cTotalCandidates; c++) + { + for (uint32_t a = 0; a < 4; a++) + { + const uint64_t t = m_pred_hist[c + a * cTotalCandidates]; + amp_totals[a] += t; + coded_blocks += t; + + if (c == cCandAbsolute) + class_totals[0] += t; + else if (c >= cCandFirstXYDelta) + class_totals[2] += t; + else + class_totals[1] += t; + } + } + + const float inv_coded = coded_blocks ? (100.0f / (float)coded_blocks) : 0.0f; + + fmt_debug_printf("classes: absolute {} ({.2}%), synthetic {} ({.2}%), dictionary {} ({.2}%)\n", + class_totals[0], (float)class_totals[0] * inv_coded, + class_totals[1], (float)class_totals[1] * inv_coded, + class_totals[2], (float)class_totals[2] * inv_coded); + fmt_debug_printf("amp codes: identity {}, flip {}, half {}, half-flip {}\n", + amp_totals[0], amp_totals[1], amp_totals[2], amp_totals[3]); + + // top 12 joint (cand, amp) symbols + fmt_debug_printf("top predictors:\n"); + + uint32_t idx[cTotalCandidates * 4]; + for (uint32_t i = 0; i < cTotalCandidates * 4; i++) + idx[i] = i; + std::sort(idx, idx + cTotalCandidates * 4, + [&](uint32_t a, uint32_t b) { return m_pred_hist[a] > m_pred_hist[b]; }); + + for (uint32_t r = 0; r < 12; r++) + { + const uint32_t i = idx[r]; + if (!m_pred_hist[i]) + break; + + const uint32_t cand = i % cTotalCandidates, amp = i / cTotalCandidates; + static const char* s_amp_names[4] = { "", " (flip)", " (half)", " (half-flip)" }; + + std::string name; + if (cand < cCandFirstXYDelta) + name = g_cand_names[cand]; + else + { + const xbc7_xy_delta& dlt = g_xbc7_xy_deltas[cand - cCandFirstXYDelta]; + name = string_format("copy (%i,%i)", (int)dlt.m_dx, (int)dlt.m_dy); + } + name += s_amp_names[amp]; + + fmt_debug_printf(" {}: {} ({.2}%)\n", pad(name.c_str(), 28), m_pred_hist[i], (float)m_pred_hist[i] * inv_coded); + } + } + + // ---- weight coding mode ---- + fmt_debug_printf("\n---- WEIGHTS: DCT vs lossless DPCM decision ----\n"); + { + const uint64_t wt_coded = m_wt_dct_blocks + m_wt_dpcm_blocks; + const float inv_wt = wt_coded ? (100.0f / (float)wt_coded) : 0.0f; + + fmt_debug_printf("dct: {} ({.2}%), dpcm: {} ({.2}%) [decision: dpcm_bits*100 <= dct_bits*{}]\n", + m_wt_dct_blocks, (float)m_wt_dct_blocks * inv_wt, m_wt_dpcm_blocks, (float)m_wt_dpcm_blocks * inv_wt, m_wt_alpha_pct); + fmt_debug_printf("est bits -- all DCT: {}, all DPCM: {}, chosen: {}\n", + m_wt_dct_est_bits_total / 8, m_wt_dpcm_est_bits_total / 8, m_wt_chosen_est_bits / 8); + + std::string l1("ACs/block when DCT won: "); + std::string l2("ACs/block when DPCM won: "); + for (uint32_t i = 0; i <= 32; i++) + { + if (m_wt_block_acs_hist_dct[i]) + l1 += string_format("%u:%llu ", i, (unsigned long long)m_wt_block_acs_hist_dct[i]); + if (m_wt_block_acs_hist_dpcm[i]) + l2 += string_format("%u:%llu ", i, (unsigned long long)m_wt_block_acs_hist_dpcm[i]); + } + fmt_debug_printf("{}\n", l1); + if (m_wt_dpcm_blocks) + fmt_debug_printf("{}\n", l2); + + for (uint32_t cls = 0; cls < 3; cls++) + { + if (!m_wt_resid_count[cls]) + continue; + fmt_debug_printf("dpcm resid {}b: n={}, mean|d|={.3}, zero={.2}%\n", + cls + 2, m_wt_resid_count[cls], + (float)m_wt_resid_sum_abs[cls] / (float)m_wt_resid_count[cls], + (float)m_wt_resid_zero[cls] * 100.0f / (float)m_wt_resid_count[cls]); + + std::string mh(string_format("dpcm resid %ub |mag| hist:", cls + 2)); + for (uint32_t m = 0; m < 9; m++) + if (m_wt_resid_mag_hist[cls][m]) + mh += string_format(" %u:%.3f%%", m, (double)m_wt_resid_mag_hist[cls][m] * 100.0 / (double)m_wt_resid_count[cls]); + fmt_debug_printf("{}\n", mh); + } + + if (m_wt_dpcm_blocks) + { + uint32_t idx[cTotalCandidates * 4]; + for (uint32_t i = 0; i < cTotalCandidates * 4; i++) + idx[i] = i; + std::sort(idx, idx + cTotalCandidates * 4, + [&](uint32_t a, uint32_t b) { return m_wt_dpcm_pred_hist[a] > m_wt_dpcm_pred_hist[b]; }); + + std::string tops("top dpcm predictors:"); + for (uint32_t r = 0; r < 8; r++) + { + const uint32_t i = idx[r]; + if (!m_wt_dpcm_pred_hist[i]) + break; + + const uint32_t cand = i % cTotalCandidates, amp = i / cTotalCandidates; + static const char* s_amp_suffix[4] = { "", "/flip", "/half", "/half-flip" }; + + std::string name; + if (cand < cCandFirstXYDelta) + name = g_cand_names[cand]; + else + { + const xbc7_xy_delta& dlt = g_xbc7_xy_deltas[cand - cCandFirstXYDelta]; + name = string_format("copy(%i,%i)", (int)dlt.m_dx, (int)dlt.m_dy); + } + + tops += string_format(" %s%s:%llu", name.c_str(), s_amp_suffix[amp], (unsigned long long)m_wt_dpcm_pred_hist[i]); + } + fmt_debug_printf("{}\n", tops); + } + } + + // ---- solid ---- + if (m_cmd_hist[(uint32_t)xbc7_command_id::cCmdSolidDPCM]) + { + fmt_debug_printf("\n---- SOLID: color deltas vs neighbor edge prediction ----\n"); + for (uint32_t c = 0; c < 4; c++) + { + if (!m_solid_delta_count[c]) + continue; + fmt_debug_printf("{}: n={}, mean|d|={.3}, zero={.2}%\n", + s_chan_names[c], m_solid_delta_count[c], + (float)m_solid_delta_sum_abs[c] / (float)m_solid_delta_count[c], + (float)m_solid_delta_zero[c] * 100.0f / (float)m_solid_delta_count[c]); + } + } + + // ---- DCT coefficient stats ---- + fmt_debug_printf("\n"); + m_dct_stats.print(); + + fmt_debug_printf("=====================================\n\n"); + } + }; + + + + // NOTE: prev-emission coherence biasing of the predictor/mode sweeps was + // tried here (prefer the previously-emitted symbol within a cost margin, + // to cluster the predictor/command/index streams into Zstd-friendly runs) + // and MEASURED WORSE at every margin including pure tie-breaking: the + // existing earliest-candidate tie-break already concentrates ties onto a + // few globally-frequent symbols, and that global skew compresses better + // than local runs. Don't re-try without a new mechanism. + + // The mechanism that DOES work, generalized: a later sweep candidate must + // beat the incumbent by more than this margin to displace it, so ties and + // near-ties collapse onto the lowest-index candidates GLOBALLY (global + // symbol skew is what Zstd prices). For the endpoint sweep the low-index + // candidates are also the dedicated neighbor modes, which don't spend an + // index byte. The weight-DCT sweep has its own long-standing equivalent + // (ERR_SWITCH_MARGIN_PERMILLE). + const uint32_t XBC7_SWEEP_SWITCH_MARGIN_PERMILLE = 30; + + // ---- effort-driven weight-predictor pruning ---------------------------- + // The per-block weight-grid sweeps (pack_weights / pack_weights_dpcm) try + // every (cand_index, amp_code) predictor; the DCT sweep re-runs a forward+ + // inverse DCT per candidate, so it dominates encode time at Q < 100. The + // effort knob prunes WHICH candidates are tried (not the iteration ORDER -- + // the sweeps still walk cand_index 0..cTotalCandidates and merely SKIP the + // disabled ones, so the earliest-candidate tie-break is preserved and effort + // 10 reproduces the pre-knob output exactly). + // + // g_xbc7_pred_priority lists candidates most-valuable first, derived from + // winning-predictor histograms across many images at Q=1/10/80 (opaque + + // alpha). The dictionary "copy(dx,dy)" predictors collectively win ~2/3 of + // blocks, so the high-value core deliberately mixes the strongest synthetic + // predictors (left/upper edge, diag avg) with the nearest causal copies. + // cCandAbsolute is first -- it's the no-prediction fallback and must always + // be available. Any candidate not listed here is appended in natural order + // by xbc7_build_pred_mask (so the table need only spell out the head). + static const uint8_t g_xbc7_pred_priority[] = + { + cCandAbsolute, cCandLeftEdge, cCandUpperEdge, + cCandFirstXYDelta + 0, cCandFirstXYDelta + 1, cCandDiagAvg, // <-- core 6 (effort 0) + cCandFirstXYDelta + 6, cCandFirstXYDelta + 2, cCandFirstXYDelta + 7, + cCandFirstXYDelta + 8, cCandReflectLeft, cCandFirstXYDelta + 4, + cCandFirstXYDelta + 5, cCandReflectUpper, cCandPlaneFit, + cCandFirstXYDelta + 3, cCandLUBlendStrong, cCandDDR, cCandDDL, + cCandGradient, cCandGradientDamped, cCandLUBlend, cCandLUAvg, + cCandUpperDiagEdgeBlend, cCandDiagEdgeBlend, cCandMED, cCandGAB, + }; + + // Build the enabled-candidate bitmask for an effort level [0,10]. effort 0 + // enables the core 6; the count ramps linearly to the full set at effort 10 + // (where the mask is all-ones -> the sweeps are byte-for-byte unchanged). + // cTotalCandidates < 64, so a single uint64_t holds the whole set. + static uint64_t xbc7_build_pred_mask(uint32_t effort) + { + static_assert(cTotalCandidates <= 64, "predictor mask must fit in uint64_t"); + + effort = basisu::minimum(effort, 10); + + const uint32_t cCoreCands = 6; + uint32_t count = cCoreCands + ((cTotalCandidates - cCoreCands) * effort + 5) / 10; + count = basisu::minimum(count, cTotalCandidates); + + // priority head, then any remaining candidate in natural order -> a valid + // permutation of [0, cTotalCandidates) regardless of what the head omits + uint8_t order[cTotalCandidates]; + bool used[cTotalCandidates] = { false }; + uint32_t n = 0; + for (uint32_t i = 0; i < sizeof(g_xbc7_pred_priority); i++) + { + const uint8_t c = g_xbc7_pred_priority[i]; + assert((c < cTotalCandidates) && !used[c]); // head must be unique & in-range + used[c] = true; + order[n++] = c; + } + for (uint32_t c = 0; c < cTotalCandidates; c++) + if (!used[c]) + order[n++] = (uint8_t)c; + assert(n == cTotalCandidates); + + uint64_t mask = 0; + for (uint32_t i = 0; i < count; i++) + mask |= (1ull << order[i]); + + assert(mask & (1ull << cCandAbsolute)); // fallback always present + return mask; + } + + // Amp codes the per-block sweeps try, by effort [0,10]. The amp loop runs in + // descending win-share order (0=identity, 1=flip, 2=half-contrast, 3=half- + // flip -- confirmed by histograms), so trying the first N keeps the most + // valuable. effort 10 -> 4 (the full set -> output unchanged vs pre-knob). + static uint32_t xbc7_amp_codes_for_effort(uint32_t effort) + { + static const uint8_t tbl[11] = { 1, 1, 1, 1, 2, 2, 3, 3, 4, 4, 4 }; + return tbl[basisu::minimum(effort, 10)]; + } + + bool pack_weights( + uint32_t bx, uint32_t by, uint32_t num_blocks_x, + const tile_bounds& tile, + const color_rgba orig_block[16], + const vector2D &log_blks, + dct_syms best_syms[2], basist::bc7u::log_bc7_block &best_cand_log_blk, uint32_t& best_predictor_index, + xbc7_weight_grid_dct_fixed &weight_grid_dct_fixed, fxvec &dct_work_fixed, const pack_options& opts, + uint64_t enabled_pred_mask, uint32_t num_amp_codes) + { + best_predictor_index = 0; + + const uint32_t global_q = opts.m_dct_q; + + const uint64_t ERR_SWITCH_MARGIN_PERMILLE = 20; + const uint32_t TOTAL_AMP_CODES = num_amp_codes; // 0=identity, 1=flip, 2=half contrast, 3=half of flip (effort-pruned) + + const basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by); + + best_syms[0].clear(); + best_syms[1].clear(); + best_cand_log_blk = log_blk; + + uint64_t best_err = UINT64_MAX; + uint32_t best_num_ac_syms = UINT32_MAX; + + for (uint32_t amp_code = 0; amp_code < TOTAL_AMP_CODES; amp_code++) + { + for (uint32_t cand_index = 0; cand_index < cTotalCandidates; cand_index++) + { + if ((amp_code) && (cand_index == cCandAbsolute)) + continue; + + // effort pruning: skip predictors not in this effort's enabled set + // (mask is all-ones at effort 10, so this never fires there) + if (!((enabled_pred_mask >> cand_index) & 1u)) + continue; + + basist::bc7u::log_bc7_block cand_log_blk(log_blk); + + uint32_t cand_total_ac_syms = 0; + + dct_syms cand_syms[2]; + cand_syms[0].clear(); + cand_syms[1].clear(); + + for (uint32_t p = 0; p < cand_log_blk.m_num_planes; p++) + { + int weight_preds[16]; + int* pWeight_predictions = nullptr; + + if (cand_index != cCandAbsolute) + { + bool eval_status = eval_weight_predictor( + cand_index, amp_code, + bx, by, num_blocks_x, + tile, + log_blks, + p, weight_preds); + + if (!eval_status) + goto skip_cand; + + pWeight_predictions = weight_preds; + } + + dct_syms syms; + + weight_grid_dct_fixed.forward(basist::fixed16_16::from_int(global_q), p, pWeight_predictions, cand_log_blk, syms, dct_work_fixed); + + memset(cand_log_blk.m_weights[p], 0, 16); + + bool status = weight_grid_dct_fixed.inverse(basist::fixed16_16::from_int(global_q), p, pWeight_predictions, syms, cand_log_blk, dct_work_fixed); + + if (!status) + { + assert(0); + return false; + } + + uint32_t total_acs = syms.m_ac_vals.size_u32(); + if (total_acs && (syms.m_ac_vals[total_acs - 1].m_coeff == INT16_MAX)) + total_acs--; + cand_total_ac_syms += total_acs; + + cand_syms[p] = syms; + + } // p + + // scoped so 'goto skip_cand' leaps over (not into) these inits -- required under /permissive- + { + color_rgba cand_block_pixels[16]; + if (!basist::bc7u::unpack_bc7(cand_log_blk, (basist::color_rgba*)cand_block_pixels)) + { + assert(0); + return false; + } + + uint64_t cand_err = 0; + for (uint32_t i = 0; i < 16; i++) + cand_err += cand_block_pixels[i].get_weighted_dist2(orig_block[i], opts.m_weights); + + if ((cand_total_ac_syms < best_num_ac_syms) || + ((cand_total_ac_syms == best_num_ac_syms) && ((cand_err * 1000) < (best_err * (1000 - ERR_SWITCH_MARGIN_PERMILLE))))) + { + best_cand_log_blk = cand_log_blk; + best_predictor_index = cand_index + amp_code * cTotalCandidates; + best_num_ac_syms = cand_total_ac_syms; + best_err = cand_err; + best_syms[0] = cand_syms[0]; + best_syms[1] = cand_syms[1]; + } + } // end candidate-eval scope + + skip_cand: + ; + + } // cand_index + + } // amp_code + + assert(best_syms[0].m_ac_vals.size()); + + return true; + } + + // Estimated per-symbol coding costs in 1/8-bit units ("octobits"), measured + // on the kodim corpus via the stats dashboard. + // + // DPCM weight residual cost, [weight_bits - 2][min(|centered resid|, 8)]: + // -log2 of each symbol's probability (the magnitude's mass split across + // +/-), measured at Q=100 where EVERY block codes DPCM, so the + // distributions carry no mode-decision selection bias. Order-0 model; + // Zstd lands within ~13% of it on these streams. + static const uint32_t g_dpcm_resid_cost_obits[3][9] = + { + { 8, 17, 47, 47, 47, 47, 47, 47, 47 }, // 2-bit (centered range [-2,1]) + { 12, 19, 27, 38, 63, 63, 63, 63, 63 }, // 3-bit (centered range [-4,3]) + { 12, 25, 29, 33, 38, 44, 52, 61, 81 }, // 4-bit (centered range [-8,7]) + }; + + // DCT stream bytes priced at their measured post-Zstd cost (Q 60..95): + // DC bytes ~4.7 bits with the 6-bit DC quantization below (was ~6.4 at + // 8-bit DC), AC run/magnitude stream bytes 3.1..3.8 bits (3.5 used). + // Sign bits are incompressible noise: exactly 1 bit each. + const uint32_t DCT_DC_BYTE_COST_OBITS = 38; + const uint32_t DCT_AC_BYTE_COST_OBITS = 28; + + // Lossless residual DPCM weight coding: predict the dequantized [0,64] grid + // with the shared predictor bank, quantize the prediction to the plane's bit + // depth (quant_weight), then wrap-difference the quantized indices mod 2^n. + // Reconstruction is exact, so the predictor choice affects ONLY compressed + // size: pick the candidate minimizing the estimated emitted cost (octobit + // table above) -- a content-adaptive estimate, since zero-heavy residual + // grids are far cheaper than the stream average. cand 0 (absolute, + // predictor index 0) is always available, emits the raw indices to a + // separate blob, and is priced at its raw stored size. + struct dpcm_weights + { + uint32_t m_pred_index = 0; // joint (cand + amp * cTotalCandidates) byte + uint8_t m_syms[2][16] = {}; // wrapped n-bit symbols (raw indices if absolute), [plane][texel] + uint64_t m_est_cost_obits = 0; // estimated emitted cost of the winning candidate, 1/8-bit units + }; + + static void pack_weights_dpcm( + uint32_t bx, uint32_t by, uint32_t num_blocks_x, + const tile_bounds& tile, + const vector2D& log_blks, + dpcm_weights& result, + uint64_t enabled_pred_mask, uint32_t num_amp_codes) + { + const uint32_t TOTAL_AMP_CODES = num_amp_codes; + + const basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by); + + uint64_t best_cost = UINT64_MAX; + + for (uint32_t amp_code = 0; amp_code < TOTAL_AMP_CODES; amp_code++) + { + for (uint32_t cand_index = 0; cand_index < cTotalCandidates; cand_index++) + { + if ((amp_code) && (cand_index == cCandAbsolute)) + continue; + + // effort pruning (see pack_weights): all-ones at effort 10 + if (!((enabled_pred_mask >> cand_index) & 1u)) + continue; + + uint64_t cost = 0; + uint8_t syms[2][16] = {}; // zeroed so single-plane blocks never copy indeterminate plane-1 bytes + bool valid = true; + + for (uint32_t p = 0; p < log_blk.m_num_planes; p++) + { + const uint32_t num_bits = log_blk.m_weight_bits[p]; + const uint32_t mask = (1u << num_bits) - 1; + const int half = 1 << (num_bits - 1); + + int weight_preds[16]; + int* pWeight_predictions = nullptr; + + if (cand_index != cCandAbsolute) + { + if (!eval_weight_predictor(cand_index, amp_code, bx, by, num_blocks_x, tile, log_blks, p, weight_preds)) + { + valid = false; + break; + } + pWeight_predictions = weight_preds; + } + + for (uint32_t i = 0; i < 16; i++) + { + const uint32_t pred_index = pWeight_predictions ? basist::bc7u::quant_weight(pWeight_predictions[i], num_bits) : 0; + const uint32_t sym = (log_blk.m_weights[p][i] - pred_index) & mask; + syms[p][i] = (uint8_t)sym; + + if (pWeight_predictions) + { + const int v = (sym >= (uint32_t)half) ? ((int)sym - (int)(mask + 1)) : (int)sym; + cost += g_dpcm_resid_cost_obits[num_bits - 2][basisu::minimum(basisu::iabs(v), 8)]; + } + else + { + // absolute: raw indices at their stored size (2 bits, + // or 4 for the nibble-expanded 3-bit class, or 4) + cost += 8u * ((num_bits == 2) ? 2u : 4u); + } + } + } // p + + if (!valid) + continue; + + // later candidates must beat the incumbent by the switch margin + // (UINT64_MAX guard: the multiply would wrap before any winner) + if ((best_cost == UINT64_MAX) || ((cost * 1000) < (best_cost * (1000 - (uint64_t)XBC7_SWEEP_SWITCH_MARGIN_PERMILLE)))) + { + best_cost = cost; + result.m_pred_index = cand_index + amp_code * cTotalCandidates; + memcpy(result.m_syms, syms, sizeof(syms)); + } + + } // cand_index + } // amp_code + + result.m_est_cost_obits = best_cost; + } + + // per-stripe (per-job) output state; merged in stripe order after the jobs join + struct stripe_output + { + blob_stream_writer m_blob_writer; + basisu::bitwise_coder m_coeff_signs; + basisu::bitwise_coder m_pbits; + basisu::bitwise_coder m_raw_endpoint_bits; + xbc7_pack_stats m_stats; + }; + + // Optional block-reuse RDO pre-pass: runs after the BC7 base pack, BEFORE the + // endpoint RDO (once a whole block is reused there's no point trying cheaper + // endpoints). In raster order per stripe (honoring boundaries), for each + // non-solid block it tries the cheapest whole-block representations and adopts + // the best acceptable one: a REPEAT (exact copy of the left/upper causal + // neighbor -> 1-byte command; preferred) or SOLID (the block's mean color -> + // cheap SolidDPCM). "Acceptable" = weighted RGBA PSNR >= the shared floor AND + // within the per-mode tolerated drop vs the block's current PSNR. Modifies + // log_blks in place; serial (causal). Returns false on a decode failure. + static bool block_reuse_rdo_pass( + const image& orig_img, + basisu::vector2D& log_blks, + uint32_t num_blocks_x, + const basisu::vector& stripes, + const pack_options& opts) + { + const uint32_t* comp_weights = opts.m_weights; + const float min_block_psnr = opts.m_rdo_min_block_psnr; + const bool has_alpha = orig_img.has_alpha(); + + uint32_t changed_repeat = 0, changed_solid = 0; + + for (uint32_t st = 0; st < stripes.size(); st++) + { + const uint32_t first_row = stripes[st].m_first_block_row; + const uint32_t end_row = first_row + stripes[st].m_num_block_rows; + + for (uint32_t by = first_row; by < end_row; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by); + + // NOTE: solid blocks are NOT skipped -- a RepeatLast/RepeatUpper + // command (1 byte) is cheaper than SolidDPCM, and pack_stripe checks + // Repeat before Solid, so we aggressively reuse a neighbor even for a + // solid block when it passes the PSNR test (VQ-style). + color_rgba src_pixels[16]; + orig_img.extract_block_clamped(src_pixels, bx * 4, by * 4, 4, 4); + + color_rgba dec[16]; + if (!basist::bc7u::unpack_bc7(log_blk, (basist::color_rgba*)dec)) + { + assert(0); + return false; + } + const float orig_psnr = xbc7_block_wsse_psnr(src_pixels, dec, comp_weights); + + // ---- Repeat: copy a causal neighbor wholesale (cheapest command) ---- + if (opts.m_repeat_rdo_enabled) + { + const basist::bc7u::log_bc7_block* preds[2] = { nullptr, nullptr }; + if (bx >= 1) preds[0] = &log_blks(bx - 1, by); // left -> RepeatLast + if (by > first_row) preds[1] = &log_blks(bx, by - 1); // upper -> RepeatUpper + + const basist::bc7u::log_bc7_block* best = nullptr; + float best_psnr = 0.0f; + for (uint32_t i = 0; i < 2; i++) + { + if (!preds[i]) + continue; + if (!basist::bc7u::unpack_bc7(*preds[i], (basist::color_rgba*)dec)) + { + assert(0); + return false; + } + const float p = xbc7_block_wsse_psnr(src_pixels, dec, comp_weights); + if ((p >= min_block_psnr) && (p >= orig_psnr - opts.m_repeat_rdo_max_psnr_drop) && ((!best) || (p > best_psnr))) + { + best = preds[i]; + best_psnr = p; + } + } + + if (best) + { + log_blk = *best; // bit-identical -> pack_stripe emits Repeat + changed_repeat++; + continue; // Repeat is the cheapest -- done with this block + } + } + + // ---- Solid: replace with the block's mean color (next cheapest) ---- + // (skip if already solid -- it already codes cheaply as SolidDPCM) + if (opts.m_solid_rdo_enabled && !basist::bc7u::is_solid_blk(log_blk)) + { + uint32_t sum[4] = { 0, 0, 0, 0 }; + for (uint32_t i = 0; i < 16; i++) + for (uint32_t c = 0; c < 4; c++) + sum[c] += src_pixels[i][c]; + + color_rgba mean((sum[0] + 8) >> 4, (sum[1] + 8) >> 4, (sum[2] + 8) >> 4, (sum[3] + 8) >> 4); + if (!has_alpha) + mean.a = 255; + + basist::bc7u::log_bc7_block cand; + cand.clear(); + basist::bc7u::create_solid_blk(cand, (const basist::color_rgba&)mean); + + if (!basist::bc7u::unpack_bc7(cand, (basist::color_rgba*)dec)) + { + assert(0); + return false; + } + const float p = xbc7_block_wsse_psnr(src_pixels, dec, comp_weights); + if ((p >= min_block_psnr) && (p >= orig_psnr - opts.m_solid_rdo_max_psnr_drop)) + { + log_blk = cand; + changed_solid++; + } + } + } + } + } + + if (opts.m_debug_output) + { + uint32_t total_blocks = 0; + for (uint32_t st = 0; st < stripes.size(); st++) + total_blocks += num_blocks_x * stripes[st].m_num_block_rows; + if (opts.m_repeat_rdo_enabled) + fmt_debug_printf("repeat RDO: {} blocks changed ({.2}%)\n", changed_repeat, total_blocks ? (changed_repeat * 100.0f / (float)total_blocks) : 0.0f); + if (opts.m_solid_rdo_enabled) + fmt_debug_printf("solid RDO: {} blocks changed ({.2}%)\n", changed_solid, total_blocks ? (changed_solid * 100.0f / (float)total_blocks) : 0.0f); + } + + return true; + } + + // Optional endpoint-prediction RDO pre-pass: runs after the BC7 base pack but + // BEFORE stripe coding. In raster order (per stripe, honoring its boundaries) + // it tries forcing each block's endpoints to a causal neighbor's prediction + // (left/upper/left-diag/right-diag) via a zero-residual endpoint DPCM, and + // re-optimizes that candidate's weights for the new endpoints, and adopts the + // best candidate when its weighted RGBA PSNR drops by no more than + // max_psnr_drop dB. The matching zero residuals then cost almost nothing in + // the coding pass (often collapsing to a Repeat). Solid blocks are skipped -- + // they have their own cheaper SolidDPCM path. Modifies log_blks in place; + // serial, since each block predicts from already-finalized causal neighbors. + // Returns false on an (unexpected) decode failure. + static bool endpoint_dpcm_rdo_pass( + const image& orig_img, + basisu::vector2D& log_blks, + uint32_t num_blocks_x, + const basisu::vector& stripes, + const pack_options& opts) + { + const uint32_t* comp_weights = opts.m_weights; + const float max_psnr_drop = opts.m_endpoint_rdo_max_psnr_drop; + const float min_block_psnr = opts.m_rdo_min_block_psnr; + + uint32_t changed_blocks = 0; // blocks whose endpoints+weights were rewritten + +#if defined(DEBUG) || defined(_DEBUG) + // sanity: re-optimizing a candidate's weights can never RAISE its WSSE + // (the original weights are in the swept set); count any case where it does + uint64_t dbg_weight_opt_cands = 0, dbg_weight_opt_worse = 0; +#endif + + for (uint32_t st = 0; st < stripes.size(); st++) + { + const uint32_t first_row = stripes[st].m_first_block_row; + const uint32_t end_row = first_row + stripes[st].m_num_block_rows; + + for (uint32_t by = first_row; by < end_row; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by); + + // solid blocks have a dedicated, cheaper command -- leave them alone + if (basist::bc7u::is_solid_blk(log_blk)) + continue; + + // already identical to a causal neighbor (e.g. from the block-reuse + // RDO, or naturally) -- it'll code as a cheap Repeat, so don't + // disturb its endpoints + if (((bx >= 1) && basist::bc7u::compare_block_full(log_blk, log_blks(bx - 1, by))) || + ((by > first_row) && basist::bc7u::compare_block_full(log_blk, log_blks(bx, by - 1)))) + continue; + + color_rgba src_pixels[16]; + orig_img.extract_block_clamped(src_pixels, bx * 4, by * 4, 4, 4); + + color_rgba orig_pixels[16]; + if (!basist::bc7u::unpack_bc7(log_blk, (basist::color_rgba*)orig_pixels)) + { + assert(0); + return false; + } + const float orig_psnr = xbc7_block_wsse_psnr(src_pixels, orig_pixels, comp_weights); + + // causal neighbor predictors valid within this stripe, in order: + // left, upper, left-diag, right-diag (upper rows must stay in-stripe) + const basist::bc7u::log_bc7_block* preds[4] = { nullptr, nullptr, nullptr, nullptr }; + if (bx >= 1) preds[0] = &log_blks(bx - 1, by); + if (by > first_row) preds[1] = &log_blks(bx, by - 1); + if ((bx >= 1) && (by > first_row)) preds[2] = &log_blks(bx - 1, by - 1); + if ((bx + 1 < num_blocks_x) && (by > first_row)) preds[3] = &log_blks(bx + 1, by - 1); + + basist::bc7u::log_bc7_block best_cand{}; // value-init silences a C4701 false positive (only read when have_cand) + float best_cand_psnr = 0.0f; + bool have_cand = false; + + for (uint32_t i = 0; i < 4; i++) + { + if (!preds[i]) + continue; + + // copy keeps mode/config/weights; only endpoints+pbits change + basist::bc7u::log_bc7_block cand(log_blk); + + for (uint32_t s = 0; s < cand.m_num_partitions; s++) + { + uint8_t residuals[8] = { 0 }; // zero residuals -> endpoints become the prediction + uint8_t pbits[2] = { 0 }; + uint32_t num_residuals = 0, num_pbits = 0; + basist::bc7u::endpoint_dpcm(true, *preds[i], 0, cand, s, residuals, num_residuals, pbits, num_pbits); + } + +#if defined(DEBUG) || defined(_DEBUG) + // WSSE with the now-stale weights, before re-optimization + uint64_t dbg_wsse_before = 0; + { + color_rgba pre[16]; + if (!basist::bc7u::unpack_bc7(cand, (basist::color_rgba*)pre)) + { + assert(0); + return false; + } + for (uint32_t k = 0; k < 16; k++) + dbg_wsse_before += pre[k].get_weighted_dist2(src_pixels[k], comp_weights); + } +#endif + + // the slammed endpoints leave the copied weights stale -- recompute + // the optimal weights for the new endpoints (can only help vs. stale) + basist::bc7u::log_bc7_block cand_opt; + if (!optimize_block_weights(cand, src_pixels, comp_weights, cand_opt)) + return false; // already asserted inside + cand = cand_opt; + + color_rgba cand_pixels[16]; + if (!basist::bc7u::unpack_bc7(cand, (basist::color_rgba*)cand_pixels)) + { + assert(0); + return false; + } + +#if defined(DEBUG) || defined(_DEBUG) + { + uint64_t dbg_wsse_after = 0; + for (uint32_t k = 0; k < 16; k++) + dbg_wsse_after += cand_pixels[k].get_weighted_dist2(src_pixels[k], comp_weights); + + dbg_weight_opt_cands++; + if (dbg_wsse_after > dbg_wsse_before) + dbg_weight_opt_worse++; // weight re-optimization must never make WSSE worse + } +#endif + + const float cand_psnr = xbc7_block_wsse_psnr(src_pixels, cand_pixels, comp_weights); + if ((!have_cand) || (cand_psnr > best_cand_psnr)) + { + have_cand = true; + best_cand_psnr = cand_psnr; + best_cand = cand; + } + } + + // adopt the best candidate if quality drops by no more than allowed + // (a gain is of course also accepted) + if (have_cand && (best_cand_psnr >= min_block_psnr) && (best_cand_psnr >= orig_psnr - max_psnr_drop)) + { + log_blk = best_cand; + changed_blocks++; + } + } + } + } + + if (opts.m_debug_output) + { + uint32_t total_blocks = 0; + for (uint32_t st = 0; st < stripes.size(); st++) + total_blocks += num_blocks_x * stripes[st].m_num_block_rows; + fmt_debug_printf("endpoint RDO: {} blocks changed ({.2}%)\n", changed_blocks, total_blocks ? (changed_blocks * 100.0f / (float)total_blocks) : 0.0f); + } + +#if defined(DEBUG) || defined(_DEBUG) + fmt_debug_printf("endpoint RDO weight re-opt: {} candidates, {} got WORSE (expected 0)\n", dbg_weight_opt_cands, dbg_weight_opt_worse); + // assert(!dbg_weight_opt_worse); // intentionally NOT asserted -- probably too strong; observe via the stat above instead +#endif + + return true; + } + + // Optional weight-DCT AC-truncation RDO. Run ONCE on the winning DCT symbols + // (after pack_weights, NOT inside its candidate sweep). Zeros the highest- + // frequency weight-DCT AC coefficients one at a time in reverse zigzag order, + // protecting the 2x2 low-freq corner (DC,(1,0),(0,1),(1,1) -> zigzag {0,1,2,4}), + // while the decoded weighted RGBA PSNR stays within m_ac_trunc_rdo_max_psnr_drop + // dB of the un-truncated block AND >= the shared floor. Updates best_syms + // (pruned) and best_cand_log_blk (weights rebuilt via the same inverse() the + // decoder uses, so it round-trips). Returns false on an unexpected decode failure. + static bool ac_truncate_rdo( + dct_syms best_syms[2], + basist::bc7u::log_bc7_block& best_cand_log_blk, + uint32_t best_predictor_index, + const color_rgba orig_block[16], + uint32_t bx, uint32_t by, uint32_t num_blocks_x, + const tile_bounds& tile, + const vector2D& log_blks, + xbc7_weight_grid_dct_fixed& weight_grid_dct_fixed, + fxvec& dct_work, + const pack_options& opts, + xbc7_pack_stats& stats) + { + const uint32_t num_planes = best_cand_log_blk.m_num_planes; + + // winning predictor -> per-plane predictions (pack_weights left untouched; + // eval_weight_predictor asserts non-absolute, so absolute -> null preds) + const uint32_t cand_index = best_predictor_index % cTotalCandidates; + const uint32_t amp_code = best_predictor_index / cTotalCandidates; + + int preds[2][16]; + const int* pPreds[2] = { nullptr, nullptr }; + if (cand_index != cCandAbsolute) + { + for (uint32_t p = 0; p < num_planes; p++) + { + if (!eval_weight_predictor(cand_index, amp_code, bx, by, num_blocks_x, tile, log_blks, p, preds[p])) + { + assert(0); + return false; + } + pPreds[p] = preds[p]; + } + } + + // baseline PSNR of the un-truncated DCT-coded block + color_rgba dec[16]; + if (!basist::bc7u::unpack_bc7(best_cand_log_blk, (basist::color_rgba*)dec)) + { + assert(0); + return false; + } + const float orig_psnr = xbc7_block_wsse_psnr(orig_block, dec, opts.m_weights); + + int flat[2][16]; + for (uint32_t p = 0; p < num_planes; p++) + xbc7_syms_to_flat(best_syms[p], flat[p]); + + const float drop = opts.m_ac_trunc_rdo_max_psnr_drop; + const float floor_psnr = opts.m_rdo_min_block_psnr; + + uint64_t pruned_here = 0; + + // reverse zigzag, skipping protected low-freq slots (zigzag 1,2,4; 0 = DC) + for (uint32_t zz = 15; zz >= 1; zz--) + { + if ((zz == 1) || (zz == 2) || (zz == 4)) + continue; + + const uint32_t nat = g_zigzag4x4_xy[zz][0] + g_zigzag4x4_xy[zz][1] * 4; + + // skip if this coefficient is already zero in every plane + bool any_nonzero = false; + for (uint32_t p = 0; p < num_planes; p++) + if (flat[p][nat] != 0) + any_nonzero = true; + if (!any_nonzero) + continue; + + int saved[2] = { 0, 0 }; + for (uint32_t p = 0; p < num_planes; p++) + { + saved[p] = flat[p][nat]; + flat[p][nat] = 0; + } + + // rebuild the candidate from the tentatively-truncated grids + basist::bc7u::log_bc7_block trial_blk = best_cand_log_blk; // config + endpoints kept + dct_syms trial_syms[2]; + for (uint32_t p = 0; p < num_planes; p++) + { + xbc7_flat_to_syms(flat[p], trial_syms[p]); + memset(trial_blk.m_weights[p], 0, 16); + if (!weight_grid_dct_fixed.inverse(basist::fixed16_16::from_int(opts.m_dct_q), p, pPreds[p], trial_syms[p], trial_blk, dct_work)) + { + assert(0); + return false; + } + } + + if (!basist::bc7u::unpack_bc7(trial_blk, (basist::color_rgba*)dec)) + { + assert(0); + return false; + } + const float psnr = xbc7_block_wsse_psnr(orig_block, dec, opts.m_weights); + + if ((psnr >= floor_psnr) && (psnr >= orig_psnr - drop)) + { + best_cand_log_blk = trial_blk; + for (uint32_t p = 0; p < num_planes; p++) + { + best_syms[p] = trial_syms[p]; + if (saved[p]) + pruned_here++; + } + } + else + { + for (uint32_t p = 0; p < num_planes; p++) + flat[p][nat] = saved[p]; // revert this coefficient + break; // stop at the first unacceptable truncation + } + } + + stats.m_ac_trunc_pruned += pruned_here; + if (pruned_here) + stats.m_ac_trunc_blocks++; + + return true; + } + + // with every causal reference clamped to the stripe (the decoder mirrors + // the one IMPLICIT cross-row prediction -- solid blocks -- via the header + // stripe count; explicit references are simply never emitted across a + // stripe boundary). All scratch state is local or lives in `out`. + static bool pack_stripe( + const image& orig_img, + const pack_options& opts, + bool has_alpha, + uint32_t num_blocks_x, + const stripe_range& stripe, + vector2D& log_blks, + vector2D* pCoded_log_blocks, + const vector2D& base_used_lut, // per-block: 1 if the bc7e_scalar base pack used its endpoint LUTs (force DPCM) + stripe_output& out, + const xbc7_debug_image_set& dbg_imgs) // debug only; members null unless m_debug_images. Stripes write disjoint rows, so filling is thread-safe. + { + const uint32_t first_row = stripe.m_first_block_row; + const uint32_t end_row = stripe.m_first_block_row + stripe.m_num_block_rows; + + // this stripe as an inclusive block AABB (full width, for now); ALL + // causal references in this pass are gated through tile.contains() + const tile_bounds tile = { 0, (int)first_row, (int)num_blocks_x - 1, (int)end_row - 1 }; + + blob_stream_writer& blob_writer = out.m_blob_writer; + basisu::bitwise_coder& coeff_signs = out.m_coeff_signs; + basisu::bitwise_coder& pbits = out.m_pbits; + basisu::bitwise_coder& raw_endpoint_bits = out.m_raw_endpoint_bits; + xbc7_pack_stats& stats = out.m_stats; + + xbc7_weight_grid_dct_fixed weight_grid_dct_fixed; + weight_grid_dct_fixed.init(); + + fxvec dct_work_fixed; + + // hoisted so the per-block visualization calls compile out entirely when disabled + const bool debug_images = opts.m_debug_images; + + // effort -> enabled weight-predictor set + amp-code count (constant for the + // whole stripe; at effort 10 both are full so the sweeps are unchanged) + const uint64_t enabled_pred_mask = xbc7_build_pred_mask(opts.m_effort_level); + const uint32_t num_amp_codes = xbc7_amp_codes_for_effort(opts.m_effort_level); + + for (uint32_t by = first_row; by < end_row; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + // causal neighbors, clamped to the tile AABB: blocks outside it + // belong to another coding unit and are off limits + const basist::bc7u::log_bc7_block* pLeft_log_blk = tile.contains((int)bx - 1, (int)by) ? &log_blks(bx - 1, by) : nullptr; + const basist::bc7u::log_bc7_block* pUp_log_blk = tile.contains((int)bx, (int)by - 1) ? &log_blks(bx, by - 1) : nullptr; + + const basist::bc7u::log_bc7_block* pLeft_diag_log_blk = tile.contains((int)bx - 1, (int)by - 1) ? &log_blks(bx - 1, by - 1) : nullptr; + const basist::bc7u::log_bc7_block* pRight_diag_log_blk = tile.contains((int)bx + 1, (int)by - 1) ? &log_blks(bx + 1, by - 1) : nullptr; + + basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by); + + if (pUp_log_blk && basist::bc7u::compare_block_full(log_blk, *pUp_log_blk)) + { + blob_writer.put_byte(cBlobCommands, (uint8_t)xbc7_command_id::cCmdRepeatUpper); + stats.m_cmd_hist[(uint32_t)xbc7_command_id::cCmdRepeatUpper]++; + + if (debug_images) + xbc7_vis_fill_command(dbg_imgs, bx, by, (uint8_t)xbc7_command_id::cCmdRepeatUpper, false, UINT32_MAX, UINT32_MAX); + + if (pCoded_log_blocks) + { + (*pCoded_log_blocks)(bx, by) = log_blk; + } + + continue; + } + + if (pLeft_log_blk && basist::bc7u::compare_block_full(log_blk, *pLeft_log_blk)) + { + blob_writer.put_byte(cBlobCommands, (uint8_t)xbc7_command_id::cCmdRepeatLast); + stats.m_cmd_hist[(uint32_t)xbc7_command_id::cCmdRepeatLast]++; + + if (debug_images) + xbc7_vis_fill_command(dbg_imgs, bx, by, (uint8_t)xbc7_command_id::cCmdRepeatLast, false, UINT32_MAX, UINT32_MAX); + + if (pCoded_log_blocks) + { + (*pCoded_log_blocks)(bx, by) = log_blk; + } + + continue; + } + + if (basist::bc7u::is_solid_blk(log_blk)) + { + int preds[4] = {}; + int num_preds = 0; + + if (pLeft_log_blk) + { + for (uint32_t y = 0; y < 4; y++) + { + basist::color_rgba p; + if (!basist::bc7u::unpack_bc7_texel(*pLeft_log_blk, p, 3, y)) + { + assert(0); + return false; + } + preds[0] += p.r; + preds[1] += p.g; + preds[2] += p.b; + preds[3] += p.a; + } + num_preds += 4; + } + + if (pUp_log_blk) + { + for (uint32_t x = 0; x < 4; x++) + { + basist::color_rgba p; + if (!basist::bc7u::unpack_bc7_texel(*pUp_log_blk, p, x, 3)) + { + assert(0); + return false; + } + preds[0] += p.r; + preds[1] += p.g; + preds[2] += p.b; + preds[3] += p.a; + } + num_preds += 4; + } + + if (num_preds) + { + for (uint32_t c = 0; c < 4; c++) + preds[c] = (preds[c] + (num_preds / 2)) / num_preds; + } + + blob_writer.put_byte(cBlobCommands, (uint8_t)xbc7_command_id::cCmdSolidDPCM); + stats.m_cmd_hist[(uint32_t)xbc7_command_id::cCmdSolidDPCM]++; + + if (debug_images) + xbc7_vis_fill_command(dbg_imgs, bx, by, (uint8_t)xbc7_command_id::cCmdSolidDPCM, false, UINT32_MAX, UINT32_MAX); + + basist::color_rgba solid_color; + if (!basist::bc7u::unpack_bc7_texel(log_blk, solid_color, 0, 0)) + { + assert(0); + return false; + } + + if (!has_alpha) + solid_color.a = 255; // important in case a p-bit set a=254 + + for (uint32_t c = 0; c < (has_alpha ? 4u : 3u); c++) + { + const uint8_t delta = (uint8_t)(solid_color[c] - preds[c]); + blob_writer.put_byte(cBlobSolidRGBADeltas, delta); + + stats.m_solid_delta_count[c]++; + stats.m_solid_delta_sum_abs[c] += (uint64_t)basisu::iabs((int8_t)delta); + if (!delta) + stats.m_solid_delta_zero[c]++; + } + + basist::bc7u::create_solid_blk(log_blk, solid_color); + + if (pCoded_log_blocks) + { + (*pCoded_log_blocks)(bx, by) = log_blk; + } + + continue; + } + + color_rgba orig_block[16]; + orig_img.extract_block_clamped(orig_block, bx * 4, by * 4, 4, 4); + + uint8_t command_byte = 0; + + // Code config + if ((pLeft_log_blk) && (basist::bc7u::compare_block_configs(log_blk, *pLeft_log_blk, false))) + { + command_byte = (uint8_t)xbc7_command_id::cCmdReuseConfigLeft; + } + else if ((pUp_log_blk) && (basist::bc7u::compare_block_configs(log_blk, *pUp_log_blk, false))) + { + command_byte = (uint8_t)xbc7_command_id::cCmdReuseConfigUpper; + } + else if ((pLeft_diag_log_blk) && (basist::bc7u::compare_block_configs(log_blk, *pLeft_diag_log_blk, false))) + { + command_byte = (uint8_t)xbc7_command_id::cCmdReuseConfigLeftDiagonal; + } + else if ((pRight_diag_log_blk) && (basist::bc7u::compare_block_configs(log_blk, *pRight_diag_log_blk, false))) + { + command_byte = (uint8_t)xbc7_command_id::cCmdReuseConfigRightDiagonal; + } + else + { + command_byte = (uint8_t)xbc7_command_id::cCmdNewConfig; + + uint32_t config_byte = log_blk.m_mode | (log_blk.m_dp_rotation_index << 3) | (log_blk.m_mode4_index_selector << 5); + + blob_writer.put_byte(cBlobBC7BlockConfig, (uint8_t)config_byte); + } + + stats.m_cmd_hist[command_byte & 7]++; + stats.m_mode_hist[log_blk.m_mode]++; + + + // Code partition pattern index + if (log_blk.m_num_partitions == 2) + blob_writer.put_byte(cBlobPartition2, log_blk.m_pattern_index); + else if (log_blk.m_num_partitions == 3) + blob_writer.put_byte(cBlobPartition3, log_blk.m_pattern_index); + + // Endpoint coding: sweep every causal DPCM predictor -- the four + // dedicated neighbor modes, then the 32 XY-delta block references -- + // and keep the one minimizing the SSE of the residual bytes that + // will actually be emitted (int8 reading, post G-decorrelation, + // alpha bytes that the opaque mode 6 rule suppresses excluded). + // Dedicated modes are evaluated first and replacement is strict + // less-than, so an XY delta aliasing a dedicated neighbor can never + // win a tie and waste an index byte. + const basist::bc7u::log_bc7_block* pEP_pred_blk = nullptr; + uint32_t ep_pred_mode = (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointRaw; + uint32_t ep_pred_delta_index = 0; + uint32_t ep_pred_subset = 0; + + { + uint64_t best_ep_cost = UINT64_MAX; + + auto eval_ep_predictor = [&](const basist::bc7u::log_bc7_block& pred_blk, uint32_t pred_subset) -> uint64_t + { + uint64_t cost = 0; + + for (uint32_t s = 0; s < log_blk.m_num_partitions; s++) + { + uint8_t residuals[8]; + uint32_t num_residuals; + uint8_t residual_pbits[2]; + uint32_t num_residual_pbits; + + basist::bc7u::endpoint_dpcm(false, + pred_blk, pred_subset, // mirrors the emission below + log_blk, s, + residuals, num_residuals, residual_pbits, num_residual_pbits); + + uint32_t num_costed = num_residuals; + if ((log_blk.m_mode == 6) && (!has_alpha)) + num_costed = 6; // alpha residuals are never sent + + for (uint32_t i = 0; i < num_costed; i++) + { + const int v = (int8_t)residuals[i]; + cost += (uint64_t)((int64_t)v * v); + } + + // NOTE: residual_pbits are deliberately NOT costed. + // Penalizing pbit mismatches was tried and measured + // a wash at best (penalty 1: ~0%, penalty 2: +0.4% + // WORSE): the delta stream is raw 1 bit/pbit either + // way, so the only possible win is making its packed + // bytes Zstd-compressible -- but at 8 deltas/byte + // even a 30% nonzero rate leaves ~7 bits/byte of + // entropy, and the residual bytes traded away to + // get there cost more. + } + + return cost; + }; + + const basist::bc7u::log_bc7_block* neighbor_preds[4] = { pLeft_log_blk, pUp_log_blk, pLeft_diag_log_blk, pRight_diag_log_blk }; + const xbc7_command_endpoint_mode neighbor_modes[4] = + { + xbc7_command_endpoint_mode::cCmdEndpointDPCMLeft, xbc7_command_endpoint_mode::cCmdEndpointDPCMUp, + xbc7_command_endpoint_mode::cCmdEndpointDPCMLeftDiagonal, xbc7_command_endpoint_mode::cCmdEndpointDPCMRightDiagonal + }; + + for (uint32_t i = 0; i < 4; i++) + { + if (!neighbor_preds[i]) + continue; + + const uint64_t cost = eval_ep_predictor(*neighbor_preds[i], 0); + if ((!pEP_pred_blk) || ((cost * 1000) < (best_ep_cost * (1000 - (uint64_t)XBC7_SWEEP_SWITCH_MARGIN_PERMILLE)))) + { + best_ep_cost = cost; + pEP_pred_blk = neighbor_preds[i]; + ep_pred_mode = (uint32_t)neighbor_modes[i]; + ep_pred_subset = 0; + } + } + + // Subset-1 variants of left/up (EP modes 6/7), legal only when + // the neighbor is partitioned; swept AFTER the subset-0 modes + // so the switch margin keeps them losing ties. NOTE: a much + // stricter dedicated margin was tried and measured WORSE -- + // these modes' SSE wins are nearly always decisive, so the + // filter barely fires; the (small) net cost on rare content + // with hyper-skewed command streams is an SSE-vs-bytes + // mismatch that only rate-based EP selection would fix. + const basist::bc7u::log_bc7_block* s1_preds[2] = { pLeft_log_blk, pUp_log_blk }; + const xbc7_command_endpoint_mode s1_modes[2] = + { + xbc7_command_endpoint_mode::cCmdEndpointDPCMLeftSubset1, xbc7_command_endpoint_mode::cCmdEndpointDPCMUpSubset1 + }; + + for (uint32_t i = 0; i < 2; i++) + { + if ((!s1_preds[i]) || (s1_preds[i]->m_num_partitions < 2)) + continue; + + const uint64_t cost = eval_ep_predictor(*s1_preds[i], 1); + if ((!pEP_pred_blk) || ((cost * 1000) < (best_ep_cost * (1000 - (uint64_t)XBC7_SWEEP_SWITCH_MARGIN_PERMILLE)))) + { + best_ep_cost = cost; + pEP_pred_blk = s1_preds[i]; + ep_pred_mode = (uint32_t)s1_modes[i]; + ep_pred_subset = 1; + } + } + + for (uint32_t i = 0; i < NUM_XY_DELTAS; i++) + { + const xbc7_xy_delta& delta = g_xbc7_xy_deltas[i]; + const int nx = (int)bx + delta.m_dx; + const int ny = (int)by + delta.m_dy; + + if (!tile.contains(nx, ny)) + continue; + + const basist::bc7u::log_bc7_block& pred_blk = log_blks(nx, ny); + + const uint64_t cost = eval_ep_predictor(pred_blk, 0); + if ((!pEP_pred_blk) || ((cost * 1000) < (best_ep_cost * (1000 - (uint64_t)XBC7_SWEEP_SWITCH_MARGIN_PERMILLE)))) + { + best_ep_cost = cost; + pEP_pred_blk = &pred_blk; + ep_pred_mode = (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMBlockIndex; + ep_pred_delta_index = i; + ep_pred_subset = 0; + } + } + } + + if (pEP_pred_blk) + { + command_byte |= (uint8_t)(ep_pred_mode << XBC7_COMMAND_ENDPOINT_MODE_SHIFT); + stats.m_ep_dpcm_blocks++; + stats.m_ep_mode_hist[ep_pred_mode]++; + + if (ep_pred_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMBlockIndex) + { + blob_writer.put_byte(cBlobEPBlockIndex, (uint8_t)ep_pred_delta_index); + stats.m_ep_index_hist[ep_pred_delta_index]++; + } + + + for (uint32_t s = 0; s < log_blk.m_num_partitions; s++) + { + uint8_t residuals[8]; + uint32_t num_residuals; + uint8_t residual_pbits[2]; + uint32_t num_residual_pbits; + + basist::bc7u::endpoint_dpcm(false, + *pEP_pred_blk, ep_pred_subset, // subset 1 for EP modes 6/7 + log_blk, s, + residuals, num_residuals, residual_pbits, num_residual_pbits); + + { + basist::bc7u::log_bc7_block temp_log_blk(log_blk); + memset(temp_log_blk.m_endpoints, 0, sizeof(temp_log_blk.m_endpoints)); + + basist::bc7u::endpoint_dpcm(true, + *pEP_pred_blk, ep_pred_subset, // subset 1 for EP modes 6/7 + temp_log_blk, s, + residuals, num_residuals, residual_pbits, num_residual_pbits); + +#if defined(DEBUG) || defined(_DEBUG) + for (uint32_t e = 0; e < 2; e++) + { + for (uint32_t c = 0; c < 4; c++) + { + if (log_blk.m_endpoints[s][e][c] != temp_log_blk.m_endpoints[s][e][c]) + { + fmt_error_printf("DPCM endpoint decomp failed"); + assert(0); + return false; + } + } + } +#endif + } + + assert((num_residuals % 2) == 0); + + if ((log_blk.m_mode == 6) && (!has_alpha)) + { + assert(num_residuals == 8); + + if ((log_blk.m_endpoints[0][0][3] != 127) || (log_blk.m_endpoints[0][1][3] != 127)) + { + assert(0); + return false; + } + +#if 0 + if ((residuals[6] != 0) || (residuals[7] != 0)) + { + assert(0); + return false; + } +#endif + + num_residuals = 6; + } + + stats.m_ep_dpcm_subsets++; + stats.record_ep_residuals(log_blk.m_endpoint_bits[0] >= 6, residuals, num_residuals); + + if (log_blk.m_endpoint_bits[0] >= 6) + { + for (uint32_t i = 0; i < num_residuals; i += 2) + { + const uint32_t chan = i >> 1; + blob_writer.put_byte(cBlobEPDeltaFineR + chan, residuals[i + 0]); + blob_writer.put_byte(cBlobEPDeltaFineR + chan, residuals[i + 1]); + } + } + else + { + for (uint32_t i = 0; i < num_residuals; i += 2) + { + const uint32_t chan = i >> 1; + blob_writer.put_byte(cBlobEPDeltaCoarseR + chan, residuals[i + 0]); + blob_writer.put_byte(cBlobEPDeltaCoarseR + chan, residuals[i + 1]); + } + } + + for (uint32_t p = 0; p < num_residual_pbits; p++) + { + pbits.put_bits(residual_pbits[p], 1); + + stats.m_pbit_delta_bits++; + stats.m_pbit_delta_nonzero += residual_pbits[p]; + } + } + } + else + { + command_byte |= ((uint8_t)xbc7::xbc7_command_endpoint_mode::cCmdEndpointRaw << XBC7_COMMAND_ENDPOINT_MODE_SHIFT); + stats.m_ep_raw_blocks++; + stats.m_ep_mode_hist[(uint32_t)xbc7_command_endpoint_mode::cCmdEndpointRaw]++; + + for (uint32_t s = 0; s < log_blk.m_num_partitions; s++) + { + for (uint32_t c = 0; c < log_blk.get_num_comps(); c++) + { + for (uint32_t e = 0; e < 2; e++) + { + raw_endpoint_bits.put_bits(log_blk.m_endpoints[s][e][c], log_blk.m_endpoint_bits[c == 3]); + } + } + } + + for (uint32_t p = 0; p < log_blk.m_num_pbits; p++) + raw_endpoint_bits.put_bits(log_blk.m_pbits[p], 1); + } + + // Weights: residual DCT vs lossless residual DPCM, chosen per + // block by estimated emitted size (see decision below). Q >= 100 + // is the lossless mode: weights are ALWAYS residual DPCM -- the + // whole file is then exact relative to the input BC7 logical + // blocks -- and the expensive DCT candidate search is skipped. + // + // We ALSO force lossless DPCM (and skip the DCT search) for any block + // the bc7e_scalar base pack flagged as using its one-color endpoint + // LUTs: those extreme endpoints only "land" with exact weights, so the + // lossy weight DCT would decode them to "trap" artifacts. (LUTs are + // already disabled at Q < 100; this also covers the always-LUT solid + // path. At Q == 100 DPCM is forced anyway, so LUTs are harmless there.) + const bool force_dpcm = (opts.m_dct_q >= 100) || (base_used_lut(bx, by) != 0); + + dct_syms best_syms[2]; + // value-init silences a C4701 false positive: best_cand_log_blk is only + // read on the DCT path (!force_dpcm), where pack_weights() assigns it + // first; the DPCM path 'continue's before the read. + basist::bc7u::log_bc7_block best_cand_log_blk{}; + uint32_t best_predictor_index = 0; + + uint64_t dct_est_obits = 0; + uint32_t block_total_acs = 0; + + if (!force_dpcm) + { + bool pack_status = pack_weights(bx, by, num_blocks_x, + tile, + orig_block, + log_blks, + best_syms, best_cand_log_blk, best_predictor_index, + weight_grid_dct_fixed, dct_work_fixed, opts, enabled_pred_mask, num_amp_codes); + + if (!pack_status) + return false; + + // Estimated DCT emission cost, mirroring the writes below: + // each stream byte at its measured post-Zstd cost, plus the + // exact (incompressible) sign bits. + for (uint32_t p = 0; p < log_blk.m_num_planes; p++) + { + uint32_t num_acs = 0, num_eobs = 0; + for (uint32_t c = 0; c < best_syms[p].m_ac_vals.size(); c++) + { + if (best_syms[p].m_ac_vals[c].m_coeff == INT16_MAX) + num_eobs++; + else + num_acs++; + } + dct_est_obits += DCT_DC_BYTE_COST_OBITS + (2 * (uint64_t)DCT_AC_BYTE_COST_OBITS) * num_acs + (uint64_t)DCT_AC_BYTE_COST_OBITS * num_eobs + + 8 * ((uint64_t)num_acs + ((best_predictor_index != cCandAbsolute) ? 1 : 0)); + block_total_acs += num_acs; + } + + stats.m_wt_dct_est_bits_total += dct_est_obits; + } + + dpcm_weights dpcm; + pack_weights_dpcm(bx, by, num_blocks_x, tile, log_blks, dpcm, enabled_pred_mask, num_amp_codes); + + stats.m_wt_dpcm_est_bits_total += dpcm.m_est_cost_obits; + + // Both estimates are in measured post-Zstd octobits, so 100 is + // the neutral operating point. Ties go to DPCM: it's lossless, + // so a tie is a free quality win. + bool use_dpcm = force_dpcm || ((dpcm.m_est_cost_obits * 100) <= (dct_est_obits * opts.m_wt_dpcm_alpha_pct)); + + if (use_dpcm) + { + // the weight-mode command bit is cCmdWeightRaw == 0: nothing to OR in + stats.m_wt_dpcm_blocks++; + stats.m_wt_block_acs_hist_dpcm[basisu::minimum(block_total_acs, 32)]++; + stats.m_wt_chosen_est_bits += dpcm.m_est_cost_obits; + stats.m_wt_dpcm_pred_hist[dpcm.m_pred_index]++; + + blob_writer.put_byte(cBlobWeightPredictors, (uint8_t)dpcm.m_pred_index); + + const bool is_absolute = (dpcm.m_pred_index == (uint32_t)cCandAbsolute); + + for (uint32_t p = 0; p < log_blk.m_num_planes; p++) + { + const uint32_t num_bits = log_blk.m_weight_bits[p]; + const uint32_t blob_id = is_absolute ? (uint32_t)cBlobRawWeightBits : + ((num_bits == 2) ? (uint32_t)cBlobDPCMWeightResid2 : + (num_bits == 3) ? (uint32_t)cBlobDPCMWeightResid3 : (uint32_t)cBlobDPCMWeightResid4); + + if (!is_absolute) + { + const uint32_t cls = num_bits - 2; + const int half = 1 << (num_bits - 1); + for (uint32_t i = 0; i < 16; i++) + { + const int sym = dpcm.m_syms[p][i]; + const int v = (sym >= half) ? (sym - (1 << num_bits)) : sym; + stats.m_wt_resid_count[cls]++; + stats.m_wt_resid_sum_abs[cls] += (uint64_t)iabs(v); + if (!v) + stats.m_wt_resid_zero[cls]++; + stats.m_wt_resid_mag_hist[cls][basisu::minimum(iabs(v), 8)]++; + } + } + + if (num_bits == 2) + { + // 4 per byte, LSB first + for (uint32_t i = 0; i < 16; i += 4) + { + blob_writer.put_byte(blob_id, (uint8_t)(dpcm.m_syms[p][i] | + (dpcm.m_syms[p][i + 1] << 2) | (dpcm.m_syms[p][i + 2] << 4) | (dpcm.m_syms[p][i + 3] << 6))); + } + } + else + { + // 3-bit expanded to nibbles / 4-bit: 2 per byte, LSB first + for (uint32_t i = 0; i < 16; i += 2) + { + blob_writer.put_byte(blob_id, (uint8_t)(dpcm.m_syms[p][i] | (dpcm.m_syms[p][i + 1] << 4))); + } + } + } // p + + // lossless: reconstruction == input, no weight write-back needed + if (pCoded_log_blocks) + { + (*pCoded_log_blocks)(bx, by) = log_blk; + } + + if (debug_images) + xbc7_vis_fill_command(dbg_imgs, bx, by, command_byte, true, dpcm.m_pred_index, UINT32_MAX); // lossless DPCM weights: no DCT ACs + + blob_writer.put_byte(cBlobCommands, command_byte); + + continue; + } + + command_byte |= ((uint8_t)xbc7::xbc7_command_weight_mode::cCmdWeightDCT << XBC7_COMMAND_WEIGHT_MODE_SHIFT); + + // optional AC-truncation RDO: prune high-freq weight-DCT ACs from the + // winning symbols (once per block), within the configured PSNR drop + if (opts.m_ac_trunc_rdo_max_psnr_drop > 0.0f) + { + if (!ac_truncate_rdo(best_syms, best_cand_log_blk, best_predictor_index, orig_block, + bx, by, num_blocks_x, tile, log_blks, weight_grid_dct_fixed, dct_work_fixed, opts, stats)) + return false; + + // refresh the AC count for the histogram below (pruning changed it) + block_total_acs = 0; + for (uint32_t p = 0; p < log_blk.m_num_planes; p++) + { + uint32_t n = best_syms[p].m_ac_vals.size_u32(); + if (n && (best_syms[p].m_ac_vals[n - 1].m_coeff == INT16_MAX)) + n--; + block_total_acs += n; + } + } + + stats.m_wt_dct_blocks++; + stats.m_wt_block_acs_hist_dct[basisu::minimum(block_total_acs, 32)]++; + stats.m_wt_chosen_est_bits += dct_est_obits; + + blob_writer.put_byte(cBlobWeightPredictors, (uint8_t)best_predictor_index); + stats.m_pred_hist[best_predictor_index]++; + + for (uint32_t p = 0; p < log_blk.m_num_planes; p++) + { + stats.m_dct_stats.record_plane(best_syms[p]); + + assert(iabs(best_syms[p].m_dc) <= 255); + + // TODO: Always writing full precision DC + blob_writer.put_byte(cBlobDCCoeffsSmall, (uint8_t)iabs(best_syms[p].m_dc)); + + if (best_predictor_index == cCandAbsolute) + { + assert(best_syms[p].m_dc >= 0); + } + else + { + coeff_signs.put_bits((best_syms[p].m_dc < 0) ? 1 : 0, 1); + } + + assert(best_syms[p].m_ac_vals.size()); + + for (uint32_t c = 0; c < best_syms[p].m_ac_vals.size(); c++) + { + uint32_t num_zeros = best_syms[p].m_ac_vals[c].m_num_zeros; + + int ac_coeff = best_syms[p].m_ac_vals[c].m_coeff; + + if (ac_coeff == INT16_MAX) + { + blob_writer.put_byte(cBlobACCoeffs, 0xFF); + } + else + { + assert(iabs(ac_coeff) <= 255); + + blob_writer.put_byte(cBlobACCoeffs, (uint8_t)num_zeros); + blob_writer.put_byte(cBlobACCoeffs, (uint8_t)iabs(ac_coeff)); + coeff_signs.put_bits((ac_coeff < 0) ? 1 : 0, 1); + } + } // c + } // p + + log_blk = best_cand_log_blk; + + if (pCoded_log_blocks) + { + (*pCoded_log_blocks)(bx, by) = best_cand_log_blk; + } + + if (debug_images) + xbc7_vis_fill_command(dbg_imgs, bx, by, command_byte, true, best_predictor_index, block_total_acs); // DCT weights + + blob_writer.put_byte(cBlobCommands, command_byte); + + } // bx + + } // by + + return true; + } + + // Packs a single 4x4 RGBA block into a physical BC7 block using whichever BC7 + // base encoder is selected in opts: cBC7F is the built-in fast real-time + // packer; cBC7E_Scalar is the higher-quality scalar bc7e encoder (whose + // per-level params must be prebuilt once into bc7e_params before threading). + // Either way it returns a standard physical BC7 block, identical in form to + // what bc7f produces, so the rest of the pipeline is unaffected. + static inline void xbc7_pack_bc7_base_block( + basist::bc7u::phys_bc7_block& phys_blk, + const color_rgba* pPixels, + const pack_options& opts, + const bc7e_scalar::bc7e_compress_block_params& bc7e_params, + bool& used_lut) + { + used_lut = false; + if (opts.m_bc7_encoder == bc7_encoder_type::cBC7E_Scalar) + { + // bc7e writes via a uint64_t*; phys_bc7_block::m_bytes has no alignment + // guarantee, so encode into an aligned local and copy the 16 bytes back. + // pUsed_lut reports whether the winning encoding used bc7e's one-color + // endpoint LUTs on any subset (a "this block may be weird" hint). + uint64_t blk64[2]; + uint8_t lut_flag = 0; + bc7e_scalar::bc7e_compress_blocks(1, blk64, (const uint32_t*)pPixels, &bc7e_params, &lut_flag); + memcpy(phys_blk.m_bytes, blk64, sizeof(phys_blk.m_bytes)); + used_lut = (lut_flag != 0); + } + else + { + // bc7f does not use the one-color endpoint LUTs. + basist::bc7f::fast_pack_bc7_auto_rgba(phys_blk.m_bytes, (basist::color_rgba*)pPixels, opts.m_bc7_pack_flags); + } + } + + bool pack_image( + const image& orig_img, + const pack_options& opts, + uint8_vec &comp_bytes, + vector2D &coded_log_blocks) + { +#if !BASISD_SUPPORT_KTX2_ZSTD + // XBC7 is entirely zstd-based: the stream is zstd-compressed (serialize) and + // decoding/validation requires zstd too. We do not support uncompressed XBC7 + // streams, so without BASISD_SUPPORT_KTX2_ZSTD there is nothing we can do -- + // fail the encode cleanly. (The zstd-using body below isn't compiled here, so + // the encoder still links: serialize() ends up unreferenced.) + BASISU_NOTE_UNUSED(orig_img); BASISU_NOTE_UNUSED(opts); + BASISU_NOTE_UNUSED(comp_bytes); BASISU_NOTE_UNUSED(coded_log_blocks); + fmt_error_printf("XBC7 pack_image: XBC7 requires zstd (BASISD_SUPPORT_KTX2_ZSTD=0) -- cannot encode\n"); + return false; +#else + // Internal pointer alias for the caller-provided reference (guaranteed + // non-null): pack_stripe() and the threaded lambdas below thread the coded + // logical blocks through as a pointer. The encoder always populates these + // and then validates the encoded stream decodes back to them exactly. + vector2D* pCoded_log_blocks = &coded_log_blocks; + + const uint32_t width = orig_img.get_width(); + const uint32_t height = orig_img.get_height(); + + if ((!width) || (width > UINT16_MAX) || (!height) || (height > UINT16_MAX)) + { + assert(0); + return false; + } + + if ((opts.m_dct_q < 1) || (opts.m_dct_q > 100)) + { + assert(0); + return false; + } + + if (!opts.m_pJob_pool) + { + assert(0); + return false; + } + + const bool use_threading = (opts.m_pJob_pool->get_total_threads() > 1); + + const uint32_t block_width = 4; + const uint32_t block_height = 4; + + const uint32_t num_blocks_x = (width + block_width - 1) / block_width; + const uint32_t num_blocks_y = (height + block_height - 1) / block_height; + const uint32_t total_blocks = num_blocks_x * num_blocks_y; + + const bool has_alpha = orig_img.has_alpha(); + + if (opts.m_debug_output) + fmt_debug_printf("XBC7 pack: {}x{} texels, alpha: {}, block: {}x{}, blocks: {}x{} = {} total\n", + width, height, has_alpha, block_width, block_height, num_blocks_x, num_blocks_y, total_blocks); + + // Dump the full set of pack_options that were passed in. + if (opts.m_debug_output) + { + fmt_debug_printf("---- XBC7 pack_options ----\n"); + fmt_debug_printf(" dct_q: {}\n", opts.m_dct_q); + { + const uint64_t dbg_mask = xbc7_build_pred_mask(opts.m_effort_level); + uint32_t dbg_num_preds = 0; + for (uint32_t c = 0; c < cTotalCandidates; c++) + dbg_num_preds += (uint32_t)((dbg_mask >> c) & 1u); + fmt_debug_printf(" effort_level: {} (weight predictors: {}/{}, amp codes: {}/4)\n", + opts.m_effort_level, dbg_num_preds, (uint32_t)cTotalCandidates, xbc7_amp_codes_for_effort(opts.m_effort_level)); + } + fmt_debug_printf(" weights[RGBA]: {} {} {} {}\n", opts.m_weights[0], opts.m_weights[1], opts.m_weights[2], opts.m_weights[3]); + fmt_debug_printf(" optimize_weights_after_bc7f: {}\n", opts.m_optimize_weights_after_bc7f); + fmt_debug_printf(" bc7_encoder: {} (bc7e_scalar level: {}, perceptual: {})\n", + (opts.m_bc7_encoder == bc7_encoder_type::cBC7E_Scalar) ? "bc7e_scalar" : "bc7f", opts.m_bc7e_scalar_level, opts.m_perceptual); + fmt_debug_printf(" self_validate: {}\n", opts.m_self_validate); + fmt_debug_printf(" bc7_pack_flags: 0x{X}\n", opts.m_bc7_pack_flags); + fmt_debug_printf(" bc7_alt_pack_enabled: {}, bc7_pack_flags_alt: 0x{X}, bc7_alt_max_psnr_drop: {.3}\n", + opts.m_bc7_alt_pack_enabled, opts.m_bc7_pack_flags_alt, opts.m_bc7_alt_max_psnr_drop); + fmt_debug_printf(" repeat_rdo_enabled: {}, repeat_rdo_max_psnr_drop: {.3}\n", opts.m_repeat_rdo_enabled, opts.m_repeat_rdo_max_psnr_drop); + fmt_debug_printf(" solid_rdo_enabled: {}, solid_rdo_max_psnr_drop: {.3}\n", opts.m_solid_rdo_enabled, opts.m_solid_rdo_max_psnr_drop); + fmt_debug_printf(" endpoint_rdo_enabled: {}, endpoint_rdo_max_psnr_drop: {.3}\n", opts.m_endpoint_rdo_enabled, opts.m_endpoint_rdo_max_psnr_drop); + fmt_debug_printf(" ac_trunc_rdo_max_psnr_drop: {.3}\n", opts.m_ac_trunc_rdo_max_psnr_drop); + fmt_debug_printf(" rdo_min_block_psnr: {.3}\n", opts.m_rdo_min_block_psnr); + fmt_debug_printf(" wt_dpcm_alpha_pct: {}\n", opts.m_wt_dpcm_alpha_pct); + fmt_debug_printf(" num_stripes (0=auto): {}\n", opts.m_num_stripes); + fmt_debug_printf(" job_pool total threads: {}\n", opts.m_pJob_pool ? (uint32_t)opts.m_pJob_pool->get_total_threads() : 0); + fmt_debug_printf(" print_stats: {}, debug_images: {}, debug_file_prefix: '{}'\n", + opts.m_print_stats, opts.m_debug_images, opts.m_debug_file_prefix.c_str()); + } + + vector2D log_blks(num_blocks_x, num_blocks_y); + + // Per-block flag: 1 if the bc7e_scalar base pack used its one-color endpoint + // LUTs on the block (pack_stripe forces lossless DPCM for these). Function + // scope so it survives from the base pack into the stripe coding pass. + vector2D base_used_lut(num_blocks_x, num_blocks_y); + + if (opts.m_debug_output) + fmt_debug_printf("XBC7 progress: BC7 base pack ({} blocks{})...\n", total_blocks, opts.m_bc7_alt_pack_enabled ? ", alt-pack RDO" : ""); + + // BC7 base pack + canonicalize. Every block is independent and writes + // only its own pre-sized log_blks slot, so this pass parallelizes with + // simple atomic row stealing; all per-block state lives on each job's + // stack. The packer/unpack/canonicalize helpers are pure functions of + // their inputs (shared state is const tables initialized at startup). + { + // Prebuild the bc7e_scalar params once (shared, read-only across the + // pack jobs); only used when that encoder is selected. The level [0,6] + // picks one of its speed/quality presets (0=ultrafast .. 6=slowest). + // A one-time global table init is required before the first encode. + bc7e_scalar::bc7e_compress_block_params bc7e_params; + memset(&bc7e_params, 0, sizeof(bc7e_params)); + if (opts.m_bc7_encoder == bc7_encoder_type::cBC7E_Scalar) + { + bc7e_scalar::bc7e_compress_block_init(); + + typedef void (*bc7e_params_init_func)(bc7e_scalar::bc7e_compress_block_params*, bool); + static const bc7e_params_init_func s_bc7e_level_init[BC7E_SCALAR_MAX_LEVEL + 1] = + { + &bc7e_scalar::bc7e_compress_block_params_init_ultrafast, // 0 + &bc7e_scalar::bc7e_compress_block_params_init_veryfast, // 1 + &bc7e_scalar::bc7e_compress_block_params_init_fast, // 2 + &bc7e_scalar::bc7e_compress_block_params_init_basic, // 3 + &bc7e_scalar::bc7e_compress_block_params_init_slow, // 4 + &bc7e_scalar::bc7e_compress_block_params_init_veryslow, // 5 + &bc7e_scalar::bc7e_compress_block_params_init_slowest, // 6 + }; + + const uint32_t lvl = clamp(opts.m_bc7e_scalar_level, BC7E_SCALAR_MIN_LEVEL, BC7E_SCALAR_MAX_LEVEL); + + // For perceptual (sRGB) sources, run bc7e in its built-in perceptual + // error mode and leave its channel weights at the preset defaults. For + // linear sources, run bc7e in linear mode and honor the explicit XBC7 + // channel weights. (bc7f supports neither, so this only affects bc7e.) + s_bc7e_level_init[lvl](&bc7e_params, opts.m_perceptual); + + if (!opts.m_perceptual) + { + for (uint32_t i = 0; i < 4; i++) + bc7e_params.m_weights[i] = opts.m_weights[i]; + } + + // bc7e's aggressive one-color endpoint LUTs place endpoints at extreme + // positions that only "land" with exact weights. Only allow them at + // lossless Q (m_dct_q == 100, weights coded losslessly); at lower Q the + // lossy weight DCT can't reproduce them and the block decodes to a + // "trap" artifact, so disable the LUTs there. + bc7e_params.m_use_luts = (opts.m_dct_q >= 100); + } + + image raw_bc7_debug_image; + if (opts.m_debug_images || (opts.m_debug_output && opts.m_print_stats)) + { + raw_bc7_debug_image.resize(orig_img.get_width(), orig_img.get_height()); + } + + std::atomic cur_row; + cur_row.store(0); + + std::atomic pack_failed_flag; + pack_failed_flag.store(false); + + std::atomic alt_changed_blocks; // BC7 alt-pack RDO: blocks where the alternate was kept + alt_changed_blocks.store(0); + + std::atomic lut_block_count; // bc7e_scalar: blocks whose winning encoding used the endpoint LUTs + lut_block_count.store(0); + + std::atomic lut_block_count_nonsolid; // same, but excluding solid blocks (all 16 source pixels equal) + lut_block_count_nonsolid.store(0); + + auto pack_rows_func = [num_blocks_x, num_blocks_y, &opts, + &cur_row, &pack_failed_flag, &alt_changed_blocks, &lut_block_count, &lut_block_count_nonsolid, + &orig_img, &log_blks, &raw_bc7_debug_image, &bc7e_params, &base_used_lut]() + { + for ( ; ; ) + { + if (pack_failed_flag) + return; + + const uint32_t by = cur_row.fetch_add(1); + if (by >= num_blocks_y) + break; + + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + color_rgba orig_block[16]; + orig_img.extract_block_clamped(orig_block, bx * 4, by * 4, 4, 4); + + basist::bc7u::phys_bc7_block phys_blk; + bool used_lut = false; + xbc7_pack_bc7_base_block(phys_blk, orig_block, opts, bc7e_params, used_lut); + base_used_lut(bx, by) = used_lut ? (uint8_t)1 : (uint8_t)0; + if (used_lut) + { + lut_block_count.fetch_add(1, std::memory_order_relaxed); + + // A block is "solid" if all 16 source pixels are equal. + bool is_solid = true; + for (uint32_t i = 1; i < 16; i++) + { + if ((orig_block[i].r != orig_block[0].r) || (orig_block[i].g != orig_block[0].g) || + (orig_block[i].b != orig_block[0].b) || (orig_block[i].a != orig_block[0].a)) + { + is_solid = false; + break; + } + } + if (!is_solid) + lut_block_count_nonsolid.fetch_add(1, std::memory_order_relaxed); + } + + if (raw_bc7_debug_image.get_width()) + { + basist::color_rgba unpacked_block[16]; + bool as = basist::bc7u::unpack_bc7(&phys_blk, unpacked_block); + assert(as); + BASISU_NOTE_UNUSED(as); + + raw_bc7_debug_image.set_block_clipped((color_rgba *)unpacked_block, bx * 4, by * 4, 4, 4); + } + + // Optional poor-man's RDO: also pack with the alternate + // (typically cheaper) flags and keep IT unless the primary's + // RGBA PSNR is at least the threshold dB higher. + if (opts.m_bc7_alt_pack_enabled) + { + basist::bc7u::phys_bc7_block phys_alt; + basist::bc7f::fast_pack_bc7_auto_rgba(phys_alt.m_bytes, (basist::color_rgba*)orig_block, opts.m_bc7_pack_flags_alt); + + basist::color_rgba dec_primary[16], dec_alt[16]; + if ((!basist::bc7u::unpack_bc7(phys_blk.m_bytes, dec_primary)) || + (!basist::bc7u::unpack_bc7(phys_alt.m_bytes, dec_alt))) + { + assert(0); + pack_failed_flag.store(true); + return; + } + + const float psnr_primary = xbc7_block_wsse_psnr(orig_block, (const color_rgba*)dec_primary, opts.m_weights); + const float psnr_alt = xbc7_block_wsse_psnr(orig_block, (const color_rgba*)dec_alt, opts.m_weights); + + if ((psnr_alt >= opts.m_rdo_min_block_psnr) && ((psnr_primary - psnr_alt) < opts.m_bc7_alt_max_psnr_drop)) + { + phys_blk = phys_alt; // above the quality floor and drop within tolerance -- keep the cheaper block + alt_changed_blocks.fetch_add(1, std::memory_order_relaxed); + } + } + + basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by); + + bool unpack_status = basist::bc7u::unpack_bc7(&phys_blk, log_blk); + if (!unpack_status) + { + assert(0); + pack_failed_flag.store(true); + return; + } + +#if defined(DEBUG) || defined(_DEBUG) + { + // sanity check canonicalization code + basist::bc7u::log_bc7_block temp_log_blk(log_blk); + basist::bc7u::canonicalize_endpoints(temp_log_blk); + + color_rgba ap[16], bp[16]; + bool as = basist::bc7u::unpack_bc7(log_blk, (basist::color_rgba*)ap); + assert(as); + + bool bs = basist::bc7u::unpack_bc7(temp_log_blk, (basist::color_rgba*)bp); + assert(bs); + + if (memcmp(ap, bp, sizeof(color_rgba) * 16) != 0) + { + assert(0); + pack_failed_flag.store(true); + return; + } + } +#endif + basist::bc7u::canonicalize_endpoints(log_blk); + + // optional: re-optimize the weights for bc7f's endpoints. + // bc7f is a fast real-time packer, so its weights aren't + // per-texel optimal -- this exhaustively re-derives them + // (slower, but threaded with the base pack). Endpoints/config + // unchanged, so it can only hold or improve quality. + if (opts.m_optimize_weights_after_bc7f) + { + basist::bc7u::log_bc7_block opt_blk; + if (!optimize_block_weights(log_blk, orig_block, opts.m_weights, opt_blk)) + { + assert(0); + pack_failed_flag.store(true); + return; + } + log_blk = opt_blk; + } + + } // bx + } + }; + + if ((use_threading) && (num_blocks_y > 1)) + { + const uint32_t num_threads = (uint32_t)opts.m_pJob_pool->get_total_threads(); + + // one job per worker; each drains rows via the atomic counter + for (uint32_t job_index = 0; job_index < num_threads; job_index++) + opts.m_pJob_pool->add_job(pack_rows_func); + + opts.m_pJob_pool->wait_for_all(); + } + else + { + pack_rows_func(); + } + + if (pack_failed_flag) + return false; + + if (opts.m_debug_output && opts.m_bc7_alt_pack_enabled) + { + const uint32_t n = alt_changed_blocks.load(); + fmt_debug_printf("BC7 alt-pack RDO: {} blocks changed ({.2}%)\n", n, total_blocks ? (n * 100.0f / (float)total_blocks) : 0.0f); + } + + if (opts.m_debug_output && (opts.m_bc7_encoder == bc7_encoder_type::cBC7E_Scalar)) + { + const uint32_t n = lut_block_count.load(); + const uint32_t n_nonsolid = lut_block_count_nonsolid.load(); + fmt_debug_printf("bc7e_scalar: {} of {} blocks used endpoint LUTs ({.2}%); {} excluding solid blocks ({.2}%) [m_use_luts={}]\n", + n, total_blocks, total_blocks ? (n * 100.0f / (float)total_blocks) : 0.0f, + n_nonsolid, total_blocks ? (n_nonsolid * 100.0f / (float)total_blocks) : 0.0f, bc7e_params.m_use_luts); + } + + if (raw_bc7_debug_image.get_width()) + { + if (opts.m_debug_images) + save_png(opts.m_debug_file_prefix + "vis_raw_bc7.png", raw_bc7_debug_image); + + if ((opts.m_debug_output) && (opts.m_print_stats)) + { + fmt_debug_printf("\nRaw packed BC7 image stats:\n"); + print_image_metrics(orig_img, raw_bc7_debug_image); + } + } + } + + // Stripe geometry for the main coding pass. The count is a pure function + // of m_num_stripes (or the image dimensions when it's 0), NOT of the pool + // size, so the output is reproducible regardless of thread count. Pass + // m_num_stripes = 1 for the single-stripe (no seam cost, no seek table) + // format. More stripes than pool threads simply queue. + basisu::vector stripes; + uint32_t num_stripes; + + if (opts.m_num_stripes) + { + // caller forced a specific count -- clamp to what the format and the + // image can hold (can't exceed the block-row count or the max) + num_stripes = basisu::clamp(opts.m_num_stripes, 1, basisu::minimum(num_blocks_y, XBC7_MAX_ENCODER_STRIPES)); + compute_stripe_ranges(num_blocks_y, num_stripes, stripes); + } + else + { + num_stripes = compute_encoder_stripes(height, num_blocks_y, stripes); + } + + // optional block-reuse RDO: replace whole blocks with a Repeat of a neighbor + // or a solid color where quality allows -- runs FIRST (cheapest commands) + if (opts.m_repeat_rdo_enabled || opts.m_solid_rdo_enabled) + { + if (opts.m_debug_output) + fmt_debug_printf("XBC7 progress: block-reuse RDO pass (repeat/solid)...\n"); + + if (!block_reuse_rdo_pass(orig_img, log_blks, num_blocks_x, stripes, opts)) + return false; + } + + // optional endpoint-prediction RDO: rewrite block endpoints toward their + // causal neighbors (zero-residual DPCM) + re-optimize weights where quality + // allows, BEFORE coding + if (opts.m_endpoint_rdo_enabled) + { + if (opts.m_debug_output) + fmt_debug_printf("XBC7 progress: endpoint-DPCM RDO pass...\n"); + + if (!endpoint_dpcm_rdo_pass(orig_img, log_blks, num_blocks_x, stripes, opts)) + return false; + } + + if (pCoded_log_blocks) + { + pCoded_log_blocks->resize(num_blocks_x, num_blocks_y); + for (uint32_t y = 0; y < num_blocks_y; y++) + { + for (uint32_t x = 0; x < num_blocks_x; x++) + { + (*pCoded_log_blocks)(x, y).clear(); + (*pCoded_log_blocks)(x, y).m_mode = -1; + } + } + } + + blob_stream_writer blob_writer; + + basisu::bitwise_coder coeff_signs; + coeff_signs.reserve(1024); + + basisu::bitwise_coder pbits; + pbits.reserve(1024); + + basisu::bitwise_coder raw_endpoint_bits; + raw_endpoint_bits.reserve(1024); + + xbc7_pack_stats stats; + stats.m_width = width; + stats.m_height = height; + stats.m_total_blocks = total_blocks; + stats.m_dct_q = opts.m_dct_q; + stats.m_has_alpha = has_alpha; + stats.m_wt_alpha_pct = opts.m_wt_dpcm_alpha_pct; + stats.m_stripes = stripes; + + // ---- main coding pass: one job per stripe ---- + // Each stripe codes into its own stripe_output (disjoint vector + // elements, pre-sized here, so the jobs never touch shared mutable + // state outside their own log_blks/pCoded rows). + basisu::vector stripe_outputs(num_stripes); + + // optional per-block debug visualizations (one 4x4 pixel block per BC7 + // block), each with a drawn legend strip below the block grid. Shared + // across stripe jobs, but each stripe fills disjoint rows. + image vis_command_img, vis_ep_mode_img, vis_wt_mode_img, vis_ac_count_img, vis_predictor_img; + xbc7_debug_image_set dbg_imgs; + if (opts.m_debug_images) + { + // same resolution as the source image; the legend is drawn into the + // bottom strip afterward (overwriting a few block rows -- all clipped) + vis_command_img.resize(width, height); + vis_ep_mode_img.resize(width, height); + vis_wt_mode_img.resize(width, height); + vis_ac_count_img.resize(width, height); + vis_predictor_img.resize(width, height); + dbg_imgs.m_pAC_count = &vis_ac_count_img; + dbg_imgs.m_pPredictor = &vis_predictor_img; + dbg_imgs.m_pCommand = &vis_command_img; + dbg_imgs.m_pEndpoint_mode = &vis_ep_mode_img; + dbg_imgs.m_pWeight_mode = &vis_wt_mode_img; + } + + std::atomic stripe_failed_flag; + stripe_failed_flag.store(false); + + if (opts.m_debug_output) + fmt_debug_printf("XBC7 progress: main coding pass ({} stripe{}, {})...\n", + num_stripes, (num_stripes == 1) ? "" : "s", ((use_threading) && (num_stripes > 1)) ? "threaded" : "serial"); + + if ((use_threading) && (num_stripes > 1)) + { + for (uint32_t stripe_index = 0; stripe_index < num_stripes; stripe_index++) + { + opts.m_pJob_pool->add_job([stripe_index, has_alpha, num_blocks_x, + &stripes, &stripe_outputs, &stripe_failed_flag, + &orig_img, &opts, &log_blks, pCoded_log_blocks, &base_used_lut, dbg_imgs] + { + if (!pack_stripe(orig_img, opts, has_alpha, num_blocks_x, + stripes[stripe_index], log_blks, pCoded_log_blocks, base_used_lut, stripe_outputs[stripe_index], dbg_imgs)) + { + stripe_failed_flag.store(true); + } + }); + } + + opts.m_pJob_pool->wait_for_all(); + } + else + { + for (uint32_t stripe_index = 0; stripe_index < num_stripes; stripe_index++) + { + if (!pack_stripe(orig_img, opts, has_alpha, num_blocks_x, + stripes[stripe_index], log_blks, pCoded_log_blocks, base_used_lut, stripe_outputs[stripe_index], dbg_imgs)) + { + stripe_failed_flag.store(true); + break; + } + } + } + + if (stripe_failed_flag) + return false; + + if (opts.m_debug_images) + { + const xbc7_vis_legend_entry cmd_legend[8] = + { + { g_xbc7_command_vis_colors[0], "0 RepeatLast" }, + { g_xbc7_command_vis_colors[1], "1 RepeatUpper" }, + { g_xbc7_command_vis_colors[2], "2 SolidDPCM" }, + { g_xbc7_command_vis_colors[3], "3 NewConfig" }, + { g_xbc7_command_vis_colors[4], "4 ReuseConfig Left" }, + { g_xbc7_command_vis_colors[5], "5 ReuseConfig Up" }, + { g_xbc7_command_vis_colors[6], "6 ReuseConfig LeftDiag" }, + { g_xbc7_command_vis_colors[7], "7 ReuseConfig RightDiag" }, + }; + + const xbc7_vis_legend_entry ep_legend[9] = + { + { g_xbc7_endpoint_mode_vis_colors[0], "0 Raw" }, + { g_xbc7_endpoint_mode_vis_colors[1], "1 DPCM Left" }, + { g_xbc7_endpoint_mode_vis_colors[2], "2 DPCM Up" }, + { g_xbc7_endpoint_mode_vis_colors[3], "3 DPCM LeftDiag" }, + { g_xbc7_endpoint_mode_vis_colors[4], "4 DPCM RightDiag" }, + { g_xbc7_endpoint_mode_vis_colors[5], "5 DPCM BlockIndex" }, + { g_xbc7_endpoint_mode_vis_colors[6], "6 DPCM Left Subset1" }, + { g_xbc7_endpoint_mode_vis_colors[7], "7 DPCM Up Subset1" }, + { g_xbc7_vis_na_color, "n/a (fully predicted block)" }, + }; + + const xbc7_vis_legend_entry wt_legend[3] = + { + { g_xbc7_weight_mode_vis_colors[0], "0 Raw/DPCM weights (lossless)" }, + { g_xbc7_weight_mode_vis_colors[1], "1 DCT weights (lossy)" }, + { g_xbc7_vis_na_color, "n/a (fully predicted block)" }, + }; + + const xbc7_vis_legend_entry ac_legend[7] = + { + { g_xbc7_ac_count_vis_colors[0], "0 ACs (flat / DC only)" }, + { g_xbc7_ac_count_vis_colors[1], "1-2 ACs" }, + { g_xbc7_ac_count_vis_colors[2], "3-5 ACs" }, + { g_xbc7_ac_count_vis_colors[3], "6-10 ACs" }, + { g_xbc7_ac_count_vis_colors[4], "11-20 ACs" }, + { g_xbc7_ac_count_vis_colors[5], "21+ ACs" }, + { g_xbc7_vis_na_color, "n/a (not DCT-coded)" }, + }; + + const xbc7_vis_legend_entry pred_legend[7] = + { + { g_xbc7_predictor_vis_colors[cPredCatAbsolute], "Absolute (no prediction)" }, + { g_xbc7_predictor_vis_colors[cPredCatSynthetic], "Synthetic edge/gradient" }, + { g_xbc7_predictor_vis_colors[cPredCatRefLeft], "BlockRef Left" }, + { g_xbc7_predictor_vis_colors[cPredCatRefUp], "BlockRef Up" }, + { g_xbc7_predictor_vis_colors[cPredCatRefUpLeft], "BlockRef Up-Left" }, + { g_xbc7_predictor_vis_colors[cPredCatRefUpRight], "BlockRef Up-Right" }, + { g_xbc7_vis_na_color, "n/a (fully predicted block)" }, + }; + + xbc7_vis_draw_legend(vis_command_img, cmd_legend, 8); + xbc7_vis_draw_legend(vis_ep_mode_img, ep_legend, 9); + xbc7_vis_draw_legend(vis_wt_mode_img, wt_legend, 3); + xbc7_vis_draw_legend(vis_ac_count_img, ac_legend, 7); + xbc7_vis_draw_legend(vis_predictor_img, pred_legend, 7); + + save_png(opts.m_debug_file_prefix + "vis_xbc7_command.png", vis_command_img); + save_png(opts.m_debug_file_prefix + "vis_xbc7_endpoint_mode.png", vis_ep_mode_img); + save_png(opts.m_debug_file_prefix + "vis_xbc7_weight_mode.png", vis_wt_mode_img); + save_png(opts.m_debug_file_prefix + "vis_xbc7_weight_ac_count.png", vis_ac_count_img); + save_png(opts.m_debug_file_prefix + "vis_xbc7_weight_predictor.png", vis_predictor_img); + } + + if (opts.m_debug_output) + fmt_debug_printf("XBC7 progress: merging stripe streams{}...\n", (num_stripes > 1) ? " + building seek table" : ""); + + // Per-stripe seek table: for each stripe, the start offset of its data + // in every per-stripe stream id 1..25 -- a BYTE offset for byte blobs, + // a BIT offset for the three bit blobs (signs/pbits/ep_raw, which stay + // bit-merged with no padding). Recorded BEFORE each stripe is appended. + // Built only when there's more than one stripe. + const uint32_t SEEK_NUM_STREAMS = (uint32_t)cBlobStripeSeekTable - 1; // ids 1..25 + basisu::vector> seek_table; // [stripe * SEEK_NUM_STREAMS + (id-1)], little-endian DELTAS from prev stripe + if (num_stripes > 1) + seek_table.resize(num_stripes * SEEK_NUM_STREAMS); + + // running start offset of the PREVIOUS stripe in each stream (indexed by + // blob id), so each table entry can be stored as a small delta + uint64_t prev_seek_ofs[(uint32_t)cBlobStripeSeekTable] = {}; + + // ---- merge the per-stripe streams, strictly in stripe (raster) order ---- + for (uint32_t stripe_index = 0; stripe_index < num_stripes; stripe_index++) + { + const stripe_output& so = stripe_outputs[stripe_index]; + + if (num_stripes > 1) + { + for (uint32_t id = 1; id < (uint32_t)cBlobStripeSeekTable; id++) + { + uint64_t ofs; + if (id == cBlobCoeffSigns) + ofs = coeff_signs.get_total_bits(); // bit offset + else if (id == cBlobPBits) + ofs = pbits.get_total_bits(); // bit offset + else if (id == cBlobEPRaw) + ofs = raw_endpoint_bits.get_total_bits(); // bit offset + else + ofs = blob_writer.get_blob_size(id); // byte offset + + // store the delta from the previous stripe's start (stripe 0 -> 0). + // offsets are monotonic, so the delta is non-negative and small; + // packed_uint<4> keeps the low 32 bits, little-endian. + seek_table[stripe_index * SEEK_NUM_STREAMS + (id - 1)] = ofs - prev_seek_ofs[id]; + prev_seek_ofs[id] = ofs; + } + } + + for (uint32_t id = 0; id < BLOB_STREAM_MAX_IDS; id++) + { + const uint8_vec* pBlob = so.m_blob_writer.get_blob_data(id); + if ((pBlob) && (pBlob->size())) + blob_writer.append_bytes(id, *pBlob); + } + + // bit streams: append() carries each stripe's unflushed partial bit + // buffer, so the merged streams contain NO padding at the seams -- + // the decoder still reads single continuous LSB-first streams, and + // the seek table addresses stripe starts at BIT granularity + coeff_signs.append(so.m_coeff_signs); + pbits.append(so.m_pbits); + raw_endpoint_bits.append(so.m_raw_endpoint_bits); + + stats.merge(so.m_stats); + } + + if (raw_endpoint_bits.get_total_bits()) + { + raw_endpoint_bits.flush(); + blob_writer.get_blob_vec(cBlobEPRaw) = raw_endpoint_bits.get_bytes(); + } + + if (pbits.get_total_bits()) + { + pbits.flush(); + blob_writer.get_blob_vec(cBlobPBits) = pbits.get_bytes(); + } + + if (coeff_signs.get_total_bits()) + { + coeff_signs.flush(); + blob_writer.get_blob_vec(cBlobCoeffSigns) = coeff_signs.get_bytes(); + } + + // emit the seek table, byte-plane (SoA) transposed: all entries' byte 0 + // first, then every byte 1, then byte 2, then byte 3. The deltas are + // small, so the upper planes are near-all-zero and Zstd crushes them -- + // much better than the interleaved little-endian layout. Lossless + // reordering; the decoder reverses it before reading the deltas. + if (num_stripes > 1) + { + const uint32_t num_entries = (uint32_t)seek_table.size(); // num_stripes * SEEK_NUM_STREAMS + const uint8_t* pSrc = (const uint8_t*)seek_table.data(); // little-endian, 4 bytes/entry + uint8_vec planed(num_entries * 4); + for (uint32_t plane = 0; plane < 4; plane++) + for (uint32_t e = 0; e < num_entries; e++) + planed[plane * num_entries + e] = pSrc[e * 4 + plane]; + blob_writer.put_bytes(cBlobStripeSeekTable, planed.data(), planed.size()); + } + + xbc7_header hdr; + clear_obj(hdr); + hdr.m_dct_q = (uint8_t)opts.m_dct_q; + + hdr.m_flags = 0; + if (has_alpha) + hdr.m_flags = hdr.m_flags | (uint8_t)XBC7_FLAG_HAS_ALPHA; + + hdr.m_width_in_texels = orig_img.get_width(); + hdr.m_height_in_texels = orig_img.get_height(); + hdr.m_num_stripes = (uint8_t)num_stripes; + + blob_writer.put_bytes(cBlobHeader, &hdr, sizeof(hdr)); + + for (uint32_t id = 0; id < BLOB_STREAM_MAX_IDS; id++) + { + stats.m_blob_raw_size[id] = (uint32_t)blob_writer.get_blob_size(id); + stats.record_blob_bytes(id, blob_writer.get_blob_data(id)); + } + + if (opts.m_debug_output) + fmt_debug_printf("XBC7 progress: serializing + Zstd compressing blobs...\n"); + + if (!blob_writer.serialize(comp_bytes, 19, stats.m_blob_stored_size)) + return false; + + stats.m_total_file_size = comp_bytes.size_u32(); + + if (opts.m_debug_output && (opts.m_ac_trunc_rdo_max_psnr_drop > 0.0f)) + fmt_debug_printf("AC-trunc RDO: {} coeffs pruned across {} blocks ({.2}% of blocks)\n", + stats.m_ac_trunc_pruned, stats.m_ac_trunc_blocks, total_blocks ? (stats.m_ac_trunc_blocks * 100.0f / (float)total_blocks) : 0.0f); + + // m_debug_output is the master gate; m_print_stats selects the dashboard + if (opts.m_debug_output && opts.m_print_stats) + stats.print(); + + if (pCoded_log_blocks) + { + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + if (!basist::bc7u::compare_block_full((*pCoded_log_blocks)(bx, by), log_blks(bx, by))) + { + assert(0); + return false; + } + } + } + } + + if (opts.m_debug_output) + fmt_debug_printf("XBC7 progress: tiny-mip size check...\n"); + + // Tiny-mip fallback: for the smallest levels the blob container's fixed + // overhead can exceed a raw packed-BC7 encoding. If a tiny-mip stream -- + // [marker][num_blocks_x:u8][num_blocks_y:u8] followed by 16 bytes per BC7 + // block -- is smaller (and the block dims fit a byte), emit that instead. + // The decoder distinguishes streams by the leading byte: 0xB7 = blob, + // 0xB8 = tiny-mip without alpha, 0xB9 = tiny-mip with alpha (so the + // has_alpha bit rides in the marker). The packed blocks are the FINAL + // logical blocks (identical to pCoded_log_blocks), so the decode round- + // trips exactly (pack<->unpack of a logical block is lossless). + if ((num_blocks_x <= 255) && (num_blocks_y <= 255)) + { + const size_t tiny_size = 3 + (size_t)total_blocks * 16; + const uint32_t blob_size = comp_bytes.size_u32(); + + if (tiny_size < (size_t)blob_size) + { + comp_bytes.resize(0); + comp_bytes.reserve((uint32_t)tiny_size); + comp_bytes.push_back(has_alpha ? (uint8_t)0xB9 : (uint8_t)0xB8); // marker carries the alpha bit + comp_bytes.push_back((uint8_t)num_blocks_x); + comp_bytes.push_back((uint8_t)num_blocks_y); + + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + basist::bc7u::phys_bc7_block phys; + if (!basist::bc7u::pack_bc7(log_blks(bx, by), &phys)) + { + assert(0); + return false; + } + comp_bytes.append(phys.m_bytes, sizeof(phys.m_bytes)); + + // The tiny-mip decoder reconstructs each block by UNPACKING + // the stored physical block. BC7 pack<->unpack is pixel- + // lossless but can re-canonicalize the LOGICAL fields + // (endpoint swap + weight complement -> identical pixels, + // different struct). So the coded reference must be the + // unpacked form -- exactly what the decoder will yield -- + // not the original log_blks, or a per-block logical compare + // would spuriously fail. + if (pCoded_log_blocks) + { + if (!basist::bc7u::unpack_bc7(&phys, (*pCoded_log_blocks)(bx, by))) + { + assert(0); + return false; + } + } + } + } + + assert(comp_bytes.size() == tiny_size); + + if (opts.m_debug_output) + fmt_debug_printf("XBC7 tiny-mip selected: {} blocks, {} bytes (blob path was {} bytes)\n", + total_blocks, comp_bytes.size_u32(), blob_size); + } + } + + if (opts.m_debug_output) + fmt_debug_printf("XBC7 output: {} bytes, {} bits/pixel\n", + comp_bytes.size_u32(), ((float)comp_bytes.size() * 8.0f) / (float)orig_img.get_total_pixels()); + + // Self-validation (opts.m_self_validate, ON by default): decode the stream we + // just produced through the transcoder and verify EVERY logical BC7 block + // round-trips exactly to the coded reference (coded_log_blocks). This + // guarantees that every emitted XBC7 stream is valid and unpacks correctly + // during encoding. It exercises whichever stream format was written above -- + // the blob format or the tiny-mip format (coded_log_blocks already holds the + // form the decoder yields in each case, including the tiny-mip's + // re-canonicalized blocks). + if (opts.m_self_validate) + { + if (opts.m_debug_output) + fmt_debug_printf("XBC7 progress: decode self-validation...\n"); + + struct verify_ctx + { + const vector2D* m_pRef; + uint32_t m_expected_nbx, m_expected_nby; + bool m_dims_ok; + bool m_blocks_ok; + } vctx; + vctx.m_pRef = &coded_log_blocks; + vctx.m_expected_nbx = num_blocks_x; + vctx.m_expected_nby = num_blocks_y; + vctx.m_dims_ok = true; + vctx.m_blocks_ok = true; + + auto verify_init_cb = [](uint32_t nbx, uint32_t nby, uint32_t, uint32_t, uint32_t, bool, void* pData) -> bool + { + verify_ctx& c = *(verify_ctx*)pData; + if ((nbx != c.m_expected_nbx) || (nby != c.m_expected_nby)) + { + c.m_dims_ok = false; + return false; // abort decode: dimensions disagree with what we encoded + } + return true; + }; + + auto verify_block_cb = [](uint32_t bx, uint32_t by, const basist::bc7u::log_bc7_block& decoded_blk, void* pData) -> bool + { + verify_ctx& c = *(verify_ctx*)pData; + if (!basist::bc7u::compare_block_full((*c.m_pRef)(bx, by), decoded_blk)) + { + c.m_blocks_ok = false; + return false; // abort decode: a block did not round-trip + } + return true; + }; + + const bool decode_ok = basist::xbc7::unpack_image( + basist::xbc7::byte_span(comp_bytes), + verify_init_cb, &vctx, + verify_block_cb, &vctx); + + if ((!decode_ok) || (!vctx.m_dims_ok) || (!vctx.m_blocks_ok)) + { + assert(0 && "XBC7 pack_image: encoded stream failed decode self-validation"); + fmt_error_printf("XBC7 pack_image: SELF-VALIDATION FAILED -- the encoded stream did not decode back to the expected BC7 logical blocks (decode_ok={}, dims_ok={}, blocks_ok={})\n", + decode_ok, vctx.m_dims_ok, vctx.m_blocks_ok); + return false; + } + + if (opts.m_debug_output) + fmt_debug_printf("XBC7 self-validation: decode round-trip OK ({} blocks)\n", total_blocks); + } + + return true; +#endif // BASISD_SUPPORT_KTX2_ZSTD + } + + + +} // namespace xbc7 + +} // namespace basisu diff --git a/encoder/basisu_xbc7_encode.h b/encoder/basisu_xbc7_encode.h new file mode 100644 index 0000000..5db7575 --- /dev/null +++ b/encoder/basisu_xbc7_encode.h @@ -0,0 +1,200 @@ +// File: basisu_xbc7_encode.h +// XBC7: experimental supercompressed-BC7 codec (prototype moved out of basisu_tool.cpp). +#pragma once +#include "basisu_enc.h" +#include "../transcoder/basisu_transcoder_internal.h" + +namespace basisu { +namespace xbc7 { + + const uint32_t DEFAULT_EFFORT_LEVEL = 3; + + // Default quality "level" for the optional bc7e_scalar BC7 base encoder. + const uint32_t DEFAULT_BC7E_SCALAR_LEVEL = 2; + + // Supported bc7e_scalar quality level range (higher = slower/better). Maps to the + // encoder's ultrafast..slowest presets where it's invoked; clamp to this range. + const uint32_t BC7E_SCALAR_MIN_LEVEL = 0; + const uint32_t BC7E_SCALAR_MAX_LEVEL = 6; + + // Which BC7 base encoder XBC7 uses to pack the initial BC7 blocks before + // supercompression. cBC7F is the built-in fast real-time packer (the default); + // cBC7E_Scalar is the higher-quality (slower) scalar bc7e encoder. + enum class bc7_encoder_type + { + cBC7F = 0, + cBC7E_Scalar = 1 + }; + + struct pack_options + { + uint32_t m_dct_q = 100; // [1,100]; 100 == lossless mode (weights always residual DPCM) + uint32_t m_weights[4] = { 1, 1, 1, 1 }; + + // Encoder effort/speed knob [0,10]: 0 == fastest, 10 == slowest/best + // (9 is the usual practical max; 10 exists for very high core counts). + // Currently controls how many weight-grid predictors the per-block DCT and + // DPCM sweeps evaluate: at 10 the full candidate set is searched (identical + // output to before this knob existed); as effort drops the sweep is pruned + // down to a small core of empirically high-value predictors (~6 at effort + // 0), trading a little ratio for a large drop in encode time -- the DCT + // predictor search (Q < 100) is the dominant cost. Default + // DEFAULT_EFFORT_LEVEL (a balanced speed/quality point, NOT the full + // search); pass 10 to reproduce the pre-knob output exactly. + uint32_t m_effort_level = DEFAULT_EFFORT_LEVEL; + + // Optimize weights after encoding with bc7f. bc7f is a fast real-time BC7 + // packer whose per-texel weights aren't optimal; when true, every block's + // weights are exhaustively re-derived for its (unchanged) config+endpoints + // right after the base pack. Slower (but threaded with the base pack); + // endpoints/config are untouched, so quality only holds or improves. + // Default off. + bool m_optimize_weights_after_bc7f = false; + + // Flags for the underlying real-time BC7 base packer + // (basist::bc7f::fast_pack_bc7_auto_rgba). See bc7f::cPackBC7Flag* / + // cPackBC7FlagDefault* in basisu_transcoder_internal.h -- controls the + // quality/speed of the BC7 blocks XBC7 then supercompresses. + uint32_t m_bc7_pack_flags = basist::bc7f::cPackBC7FlagDefaultPartiallyAnalytical; + + // Selects which BC7 base encoder packs the initial blocks. Default cBC7F + // (the existing fast built-in packer); cBC7E_Scalar selects the higher-quality + // scalar bc7e encoder at m_bc7e_scalar_level. Affects only the primary base + // pack -- the optional alt-pack RDO path still uses bc7f for now. + bc7_encoder_type m_bc7_encoder = bc7_encoder_type::cBC7F; + + // Quality level for the bc7e_scalar encoder (only used when m_bc7_encoder == + // cBC7E_Scalar). Higher = slower/better. Default DEFAULT_BC7E_SCALAR_LEVEL (2). + uint32_t m_bc7e_scalar_level = DEFAULT_BC7E_SCALAR_LEVEL; + + // True if the source is perceptual (sRGB) data. Currently only consulted by + // the bc7e_scalar base encoder: when true it runs in its built-in perceptual + // error mode (and ignores m_weights); when false it runs linear and honors + // m_weights. Mirror basis_compressor_params::m_perceptual here. + bool m_perceptual = false; + + // When true (the default), the encoder decodes the stream it just produced + // (via the transcoder) and verifies every logical BC7 block round-trips + // exactly to the coded blocks; a failure fails the encode. Guarantees every + // emitted stream is valid/decodable. Disable only to skip the extra decode. + bool m_self_validate = true; + + // Optional "poor man's RDO" on the BC7 base pack. When enabled, every + // block is ALSO packed with m_bc7_pack_flags_alt (typically a cheaper, + // e.g. mode-6-only set). Both candidates are unpacked and their RGBA PSNR + // vs the source measured; the cheaper alternate is KEPT as long as it's no + // more than m_bc7_alt_max_psnr_drop dB worse than the primary. Trades a + // little quality for fewer command/config bits downstream. Default off. + bool m_bc7_alt_pack_enabled = false; + uint32_t m_bc7_pack_flags_alt = basist::bc7f::cPackBC7FlagPBitOpt | basist::bc7f::cPackBC7FlagUseTrivialMode6; + float m_bc7_alt_max_psnr_drop = 0.5f; // dB the alternate may be worse than the primary and still be kept + + // Optional block-reuse RDO pre-pass (runs after the BC7 base pack, BEFORE + // the endpoint RDO -- once a whole block is reused there's no point trying + // cheaper endpoints). For each non-solid block it tries replacing it with: + // - REPEAT: an exact copy of its left/upper causal neighbor (the cheapest + // command -- one byte, no config/endpoints/weights), kept if within + // m_repeat_rdo_max_psnr_drop dB; and/or + // - SOLID: its mean color (cheap SolidDPCM command), kept if within + // m_solid_rdo_max_psnr_drop dB. + // Repeat is preferred over Solid (cheaper). Each independently enabled; off + // by default. + bool m_repeat_rdo_enabled = false; + float m_repeat_rdo_max_psnr_drop = 0.5f; // dB a block may drop to become a Repeat of a neighbor + bool m_solid_rdo_enabled = false; + float m_solid_rdo_max_psnr_drop = 0.5f; // dB a block may drop to become a solid-color block + + // Optional endpoint-prediction RDO pre-pass (runs after the BC7 base pack, + // before stripe coding). For each block it tries forcing the endpoints to + // each valid causal neighbor's prediction (left/upper/left-diag/right-diag) + // via a zero-residual endpoint DPCM and keeps the best whose weighted RGBA + // PSNR drops by no more than m_endpoint_rdo_max_psnr_drop dB -- those zero + // residuals then cost almost nothing downstream. Default off. + bool m_endpoint_rdo_enabled = false; + float m_endpoint_rdo_max_psnr_drop = 0.5f; // dB the block PSNR may drop to adopt a neighbor's endpoints + + // Optional weight-DCT AC-truncation RDO (DCT-coded blocks only). After a + // block is DCT-coded, its highest-frequency weight-DCT AC coefficients are + // zeroed one at a time in reverse zigzag order (the 2x2 low-freq corner DC, + // (1,0),(0,1),(1,1) is protected) while the decoded PSNR stays within this + // dB drop -- fewer coded ACs. 0 == disabled. + float m_ac_trunc_rdo_max_psnr_drop = 0.0f; + + // Quality floor shared by ALL RDO passes (block-reuse + endpoint + alt-pack): + // a candidate whose absolute weighted RGBA PSNR is below this is rejected + // outright, even if its drop is within tolerance -- prevents accepting + // excessive distortion in already-degraded blocks. + float m_rdo_min_block_psnr = 33.0f; // dB + + // Master switch for ALL development/debug console output (printed via + // fmt_debug_printf, so it also respects the global enable_debug_printf() + // and stays silent in normal builds). Default false. Nothing prints + // unless this is true. + bool m_debug_output = false; + + // Print the detailed pack statistics dashboard. Requires m_debug_output + // to also be set (m_debug_output gates everything). + bool m_print_stats = false; + + // When true, the encoder writes visualization PNG(s) -- one pixel block + // per BC7 block -- to (m_debug_file_prefix + ".png"). Encode-time + // only; never affects the compressed output. Default off. + bool m_debug_images = false; + std::string m_debug_file_prefix; + + // Weight DCT-vs-DPCM decision: choose lossless DPCM when + // dpcm_cost * 100 <= dct_cost * alpha. Both costs are measured-rate + // estimates (see g_dpcm_resid_cost_obits + the DCT byte cost + // constants), so 100 is the neutral operating point; raising alpha + // flips more blocks to lossless DPCM (quality up, size up). + uint32_t m_wt_dpcm_alpha_pct = 100; + + // REQUIRED, never null: the caller's job pool (total threads INCLUDES + // the caller, basisu convention). The encoder always uses it; a pool of 1 + // just runs serially. Pool size affects scheduling only, never the emitted + // bytes (the stripe count is independent of it). + job_pool* m_pJob_pool = nullptr; + + // Stripe count for the main coding pass: 0 == auto (derived from image + // dimensions); else force exactly this many, clamped to + // [1, min(block_rows, XBC7_MAX_ENCODER_STRIPES)]. 1 == single-stripe (max + // ratio: no seam cost, no seek table). More stripes == more decode + // parallelism but a slightly larger file (per-seam prediction reset + + // seek-table cost); more stripes than pool threads simply queue. Prefer + // set_num_stripes_for_image() over assigning this directly. + uint32_t m_num_stripes = 0; + + // Validate a desired stripe count against an image and store it in + // m_num_stripes. Clamps so every stripe holds at least the minimum + // efficient number of block rows (never tiny or empty stripes) and never + // exceeds the format max; images too short to stripe usefully collapse to + // a single stripe. Returns the actual count stored (>= 1). + uint32_t set_num_stripes_for_image(const image& img, uint32_t desired_num_stripes); + + // Configure ALL "poor man's RDO" knobs from a single level [0,100]. + // 0 -> RDO fully OFF: every enable flag false, every PSNR-drop 0. + // [1,100] -> enables the RDO passes and linearly ramps their tolerated + // per-block PSNR drop with the level: the general passes + // (BC7 alt-pack, endpoint-DPCM, weight-DCT AC-truncation) up to + // 10 dB at 100, and the cheaper block-reuse passes (repeat, + // solid) up to 4 dB at 100. + // The absolute quality floor (m_rdo_min_block_psnr) is left untouched -- it + // still gates every candidate regardless of level. + void set_rdo_level(uint32_t rdo_level); + }; + + // coded_log_blocks (REQUIRED, by reference): receives the final coded logical + // BC7 blocks (resized + filled by the encoder). It's mandatory because the + // encoder always decodes the stream it just produced and validates every block + // round-trips to these -- see the self-validation at the end of pack_image(). + bool pack_image( + const image& orig_img, + const pack_options& opts, + uint8_vec& comp_bytes, + vector2D& coded_log_blocks); + + // NOTE: the decoder API (unpack_image / unpack_image_threaded) now lives in + // basisu_xbc7_decode.h. + +} // namespace xbc7 +} // namespace basisu