// basisu_astc_hdr_enc.cpp
#include "basisu_astc_hdr_enc.h"
#include "../transcoder/basisu_transcoder.h"

using namespace basist;

namespace basisu
{

const float DEF_R_ERROR_SCALE = 2.0f;
const float DEF_G_ERROR_SCALE = 3.0f;

static inline uint32_t get_max_qlog(uint32_t bits)
{
	switch (bits)
	{
	case 7: return MAX_QLOG7;
	case 8: return MAX_QLOG8;
	case 9: return MAX_QLOG9;
	case 10: return MAX_QLOG10;
	case 11: return MAX_QLOG11;
	case 12: return MAX_QLOG12;
	case 16: return MAX_QLOG16;
	default: assert(0); break;
	}
	return 0;
}

#if 0
static inline float get_max_qlog_val(uint32_t bits)
{
	switch (bits)
	{
	case 7: return MAX_QLOG7_VAL;
	case 8: return MAX_QLOG8_VAL;
	case 9: return MAX_QLOG9_VAL;
	case 10: return MAX_QLOG10_VAL;
	case 11: return MAX_QLOG11_VAL;
	case 12: return MAX_QLOG12_VAL;
	case 16: return MAX_QLOG16_VAL;
	default: assert(0); break;
	}
	return 0;
}
#endif

static inline int get_bit(
	int src_val, int src_bit)
{
	assert(src_bit >= 0 && src_bit <= 31);
	int bit = (src_val >> src_bit) & 1;
	return bit;
}

static inline void pack_bit(
	int& dst, int dst_bit,
	int src_val, int src_bit = 0)
{
	assert(dst_bit >= 0 && dst_bit <= 31);
	int bit = get_bit(src_val, src_bit);
	dst |= (bit << dst_bit);
}

//--------------------------------------------------------------------------------------------------------------------------

astc_hdr_codec_options::astc_hdr_codec_options()
{
	init();
}

void astc_hdr_codec_options::init()
{
	m_bc6h_err_weight = .85f;
	m_r_err_scale = DEF_R_ERROR_SCALE;
	m_g_err_scale = DEF_G_ERROR_SCALE;

	// Disabling by default to avoid transcoding outliers (try kodim26). The quality lost is very low. TODO: Could include the uber result in the output.
	m_allow_uber_mode = false;

	// Must set best quality level first to set defaults.
	set_quality_best();

	set_quality_level(cDefaultLevel);
}

void astc_hdr_codec_options::set_quality_best()
{
	m_mode11_direct_only = false;
		
	// highest achievable quality
	m_use_solid = true;

	m_use_mode11 = true;
	m_mode11_uber_mode = true;
	m_first_mode11_weight_ise_range = MODE11_FIRST_ISE_RANGE;
	m_last_mode11_weight_ise_range = MODE11_LAST_ISE_RANGE;
	m_first_mode11_submode = -1;
	m_last_mode11_submode = 7;

	m_use_mode7_part1 = true;
	m_first_mode7_part1_weight_ise_range = MODE7_PART1_FIRST_ISE_RANGE;
	m_last_mode7_part1_weight_ise_range = MODE7_PART1_LAST_ISE_RANGE;

	m_use_mode7_part2 = true;
	m_mode7_part2_part_masks = UINT32_MAX;
	m_first_mode7_part2_weight_ise_range = MODE7_PART2_FIRST_ISE_RANGE;
	m_last_mode7_part2_weight_ise_range = MODE7_PART2_LAST_ISE_RANGE;

	m_use_mode11_part2 = true;
	m_mode11_part2_part_masks = UINT32_MAX;
	m_first_mode11_part2_weight_ise_range = MODE11_PART2_FIRST_ISE_RANGE;
	m_last_mode11_part2_weight_ise_range = MODE11_PART2_LAST_ISE_RANGE;

	m_refine_weights = true;

	m_use_estimated_partitions = false;
	m_max_estimated_partitions = 0;
}

void astc_hdr_codec_options::set_quality_normal()
{
	m_use_solid = true;

	// We'll allow uber mode in normal if the user allows it.
	m_use_mode11 = true;
	m_mode11_uber_mode = true;
	m_first_mode11_weight_ise_range = 6;
	m_last_mode11_weight_ise_range = MODE11_LAST_ISE_RANGE;

	m_use_mode7_part1 = true;
	m_first_mode7_part1_weight_ise_range = MODE7_PART1_LAST_ISE_RANGE;
	m_last_mode7_part1_weight_ise_range = MODE7_PART1_LAST_ISE_RANGE;

	m_use_mode7_part2 = true;
	m_mode7_part2_part_masks = UINT32_MAX;
	m_first_mode7_part2_weight_ise_range = MODE7_PART2_LAST_ISE_RANGE;
	m_last_mode7_part2_weight_ise_range = MODE7_PART2_LAST_ISE_RANGE;

	m_use_mode11_part2 = true;
	m_mode11_part2_part_masks = UINT32_MAX;
	m_first_mode11_part2_weight_ise_range = MODE11_PART2_LAST_ISE_RANGE;
	m_last_mode11_part2_weight_ise_range = MODE11_PART2_LAST_ISE_RANGE;

	m_refine_weights = true;
}

void astc_hdr_codec_options::set_quality_fastest()
{
	m_use_solid = true;

	m_use_mode11 = true;
	m_mode11_uber_mode = false;
	m_first_mode11_weight_ise_range = MODE11_LAST_ISE_RANGE;
	m_last_mode11_weight_ise_range = MODE11_LAST_ISE_RANGE;

	m_use_mode7_part1 = false;
	m_use_mode7_part2 = false;
	m_use_mode11_part2 = false;

	m_refine_weights = false;
}

//--------------------------------------------------------------------------------------------------------------------------

void astc_hdr_codec_options::set_quality_level(int level)
{
	level = clamp(level, cMinLevel, cMaxLevel);
	
	m_level = level;

	switch (level)
	{
	case 0:
	{
		set_quality_fastest();
		break;
	}
	case 1:
	{
		set_quality_normal();

		m_first_mode11_weight_ise_range = MODE11_LAST_ISE_RANGE - 1;
		m_last_mode11_weight_ise_range = MODE11_LAST_ISE_RANGE;

		m_use_mode7_part1 = false;
		m_use_mode7_part2 = false;

		m_use_estimated_partitions = true;
		m_max_estimated_partitions = 1;

		m_mode11_part2_part_masks = 1 | 2;
		m_mode7_part2_part_masks = 1 | 2;
		break;
	}
	case 2:
	{
		set_quality_normal();

		m_use_estimated_partitions = true;
		m_max_estimated_partitions = 2;

		m_mode11_part2_part_masks = 1 | 2;
		m_mode7_part2_part_masks = 1 | 2;

		break;
	}
	case 3:
	{
		set_quality_best();

		m_use_estimated_partitions = true;
		m_max_estimated_partitions = 2;

		m_mode11_part2_part_masks = 1 | 2 | 4 | 8;
		m_mode7_part2_part_masks = 1 | 2 | 4 | 8;

		break;
	}
	case 4:
	{
		set_quality_best();

		break;
	}
	}
}

//--------------------------------------------------------------------------------------------------------------------------

#if 0
static inline half_float qlog12_to_half_slow(uint32_t qlog12)
{
	return qlog_to_half_slow(qlog12, 12);
}
#endif

// max usable qlog8 value is 247, 248=inf, >=249 is nan
// max usable qlog7 value is 123, 124=inf, >=125 is nan

// To go from a smaller qlog to an larger one, shift left by X bits.

//const uint32_t TOTAL_USABLE_QLOG8 = 248; // 0-247 are usable, 0=0, 247=60416.0, 246=55296.0

// for qlog7's shift left by 1
//half_float g_qlog8_to_half[256];
//float g_qlog8_to_float[256];

//half_float g_qlog12_to_half[4096];
//float g_qlog12_to_float[4096];

static half_float g_qlog16_to_half[65536];

inline half_float qlog_to_half(uint32_t val, uint32_t bits)
{
	assert((bits >= 5) && (bits <= 16));
	assert(val < (1U << bits));
	return g_qlog16_to_half[val << (16 - bits)];
}

// nearest values given a positive half float value (only)
static uint16_t g_half_to_qlog7[32768], g_half_to_qlog8[32768], g_half_to_qlog9[32768], g_half_to_qlog10[32768], g_half_to_qlog11[32768], g_half_to_qlog12[32768];

const uint32_t HALF_TO_QLOG_TABS_BASE = 7;
static uint16_t* g_pHalf_to_qlog_tabs[8] =
{
	g_half_to_qlog7,
	g_half_to_qlog8,

	g_half_to_qlog9,
	g_half_to_qlog10,

	g_half_to_qlog11,
	g_half_to_qlog12
};

static inline uint32_t half_to_qlog7_12(half_float h, uint32_t bits)
{
	assert((bits >= HALF_TO_QLOG_TABS_BASE) && (bits <= 12));
	assert(h < 32768);

	return g_pHalf_to_qlog_tabs[bits - HALF_TO_QLOG_TABS_BASE][h];
}

#if 0
// Input is the low 11 bits of the qlog
// Returns the 10-bit mantissa of the half float value
static int qlog11_to_half_float_mantissa(int M)
{
	assert(M <= 0x7FF);
	int Mt;
	if (M < 512)
		Mt = 3 * M;
	else if (M >= 1536)
		Mt = 5 * M - 2048;
	else
		Mt = 4 * M - 512;
	return (Mt >> 3);
}
#endif

// Input is the 10-bit mantissa of the half float value
// Output is the 11-bit qlog value
// Inverse of qlog11_to_half_float_mantissa()
static inline int half_float_mantissa_to_qlog11(int hf)
{
	int q0 = (hf * 8 + 2) / 3;
	int q1 = (hf * 8 + 2048 + 4) / 5;

	if (q0 < 512)
		return q0;
	else if (q1 >= 1536)
		return q1;

	int q2 = (hf * 8 + 512 + 2) / 4;
	return q2;
}

static inline int half_to_qlog16(int hf)
{
	// extract 5 bits exponent, which is carried through to qlog16 unchanged
	const int exp = (hf >> 10) & 0x1F;

	// extract and invert the 10 bit mantissa to nearest qlog11 (should be lossless)
	const int mantissa = half_float_mantissa_to_qlog11(hf & 0x3FF);
	assert(mantissa <= 0x7FF);

	// Now combine to qlog16, which is what ASTC HDR interpolates using the [0-64] weights.
	uint32_t qlog16 = (exp << 11) | mantissa;

	// should be a lossless operation
	assert(qlog16_to_half_slow(qlog16) == hf);

	return qlog16;
}

static inline uint32_t quant_qlog16(uint32_t q16, uint32_t desired_bits)
{
	assert((desired_bits >= 7) && (desired_bits <= 12));
	assert(q16 <= 65535);

	const uint32_t shift = 16 - desired_bits;
	uint32_t e = (q16 + (1U << (shift - 1U)) - 1U) >> shift;

	uint32_t max_val = (1U << desired_bits) - 1U;
	e = minimum<uint32_t>(e, max_val);

	return e;
}

static void compute_half_to_qlog_table(uint32_t bits, uint16_t* pTable, const basisu::vector<float> &qlog16_to_float)
{
	assert(bits >= 5 && bits <= 12);
	const uint32_t max_val = (1 << bits) - 1;

	// For all positive half-floats
	for (uint32_t h = 0; h < 32768; h++)
	{
		// Skip invalid values
		if (is_half_inf_or_nan((half_float)h))
			continue;
		const float desired_val = half_to_float((half_float)h);

		float best_err = 1e+30f;
		uint32_t best_qlog = 0;

		// For all possible qlog's
		for (uint32_t i = 0; i <= max_val; i++)
		{
			// Skip invalid values
			float v = qlog16_to_float[i << (16 - bits)];
			if (std::isnan(v))
				continue;

			// Compute error
			float err = fabs(v - desired_val);

			// Find best
			if (err < best_err)
			{
				best_err = err;
				best_qlog = i;
			}
		}

		pTable[h] = (uint16_t)best_qlog;
	}

#if 0
	uint32_t t = 0;

	const uint32_t nb = 12;
	int nb_shift = 16 - nb;

	for (uint32_t q16 = 0; q16 < 65536; q16++)
	{
		half_float h = qlog16_to_half_slow(q16);
		if (is_half_inf_or_nan(h))
			continue;

		int q7 = half_to_qlog7_12(h, nb);

		uint32_t best_err = UINT32_MAX, best_l = 0;
		for (int l = 0; l < (1 << nb); l++)
		{
			int dec_q16 = l << nb_shift;
			int err = iabs(dec_q16 - q16);
			if (err < best_err)
			{
				best_err = err;
				best_l = l;
			}
		}

		//int e = (q16 + 253) >> 9; // 345

		int e = (q16 + (1 << (nb_shift - 1)) - 1) >> nb_shift; // 285
		if (best_l != e)
			//if (q7 != best_l)
		{
			printf("q16=%u, h=%u, q7=%u, e=%u, best_l=%u\n", q16, h, q7, e, best_l);
			t++;
		}
	}

	printf("Mismatches: %u\n", t);
	exit(0);
#endif
}

static void init_qlog_tables()
{
	basisu::vector<float> qlog16_to_float(65536);

	// for all possible qlog16, compute the corresponding half float
	for (uint32_t i = 0; i <= 65535; i++)
	{
		half_float h = qlog16_to_half_slow(i);
		g_qlog16_to_half[i] = h;

		qlog16_to_float[i] = half_to_float(h);
	}

	// for all possible half floats, find the nearest qlog5-12 float
	for (uint32_t bits = HALF_TO_QLOG_TABS_BASE; bits <= 12; bits++)
	{
		compute_half_to_qlog_table(bits, g_pHalf_to_qlog_tabs[bits - HALF_TO_QLOG_TABS_BASE], qlog16_to_float);
	}
}

// [ise_range][0] = # levels
// [ise_range][1...] = lerp value [0,64]
// in ASTC order
// Supported ISE weight ranges: 0 to 10, 11 total
const uint32_t MIN_SUPPORTED_ISE_WEIGHT_INDEX = 1; // ISE 1=3 levels
const uint32_t MAX_SUPPORTED_ISE_WEIGHT_INDEX = 10; // ISE 10=24 levels

static const uint8_t g_ise_weight_lerps[MAX_SUPPORTED_ISE_WEIGHT_INDEX + 1][32] =
{
	{ 0 }, // ise range=0 is invalid for 4x4 block sizes (<24 weight bits in the block)
	{ 3, 0, 32, 64 }, // 1
	{ 4, 0, 21, 43, 64 }, // 2
	{ 5, 0, 16, 32, 48, 64 }, // 3
	{ 6, 0, 64, 12, 52, 25, 39 }, // 4
	{ 8, 0, 9, 18, 27, 37, 46, 55, 64 }, // 5
	{ 10, 0, 64, 7, 57, 14, 50, 21, 43, 28, 36 }, // 6
	{ 12, 0, 64, 17, 47, 5, 59, 23, 41, 11, 53, 28, 36 }, // 7
	{ 16, 0, 4, 8, 12, 17, 21, 25, 29, 35, 39, 43, 47, 52, 56, 60, 64 }, // 8
	{ 20, 0, 64, 16, 48, 3, 61, 19, 45, 6, 58, 23, 41, 9, 55, 26, 38, 13, 51, 29, 35 }, // 9
	{ 24, 0, 64, 8, 56, 16, 48, 24, 40, 2, 62, 11, 53, 19, 45, 27, 37, 5, 59, 13, 51, 22, 42, 30, 34 } // 10
};

//{ 12, 0, 64, 17, 47, 5, 59, 23, 41, 11, 53, 28, 36 }, // 7
//static const uint8_t g_weight_order_7[12] = { 0, 4, 8, 2, 6, 10, 11, 7, 3, 9, 5, 1 };

static vec3F calc_mean(uint32_t num_pixels, const vec4F* pPixels)
{
	vec3F mean(0.0f);

	for (uint32_t i = 0; i < num_pixels; i++)
	{
		const vec4F& p = pPixels[i];

		mean[0] += p[0];
		mean[1] += p[1];
		mean[2] += p[2];
	}

	return mean / static_cast<float>(num_pixels);
}

static vec3F calc_rgb_pca(uint32_t num_pixels, const vec4F* pPixels, const vec3F& mean_color)
{
	float cov[6] = { 0, 0, 0, 0, 0, 0 };

	for (uint32_t i = 0; i < num_pixels; i++)
	{
		const vec4F& v = pPixels[i];

		float r = v[0] - mean_color[0];
		float g = v[1] - mean_color[1];
		float b = v[2] - mean_color[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 xr = .9f, xg = 1.0f, xb = .7f;
	for (uint32_t iter = 0; iter < 3; iter++)
	{
		float r = xr * cov[0] + xg * cov[1] + xb * cov[2];
		float g = xr * cov[1] + xg * cov[3] + xb * cov[4];
		float b = xr * cov[2] + xg * cov[4] + xb * cov[5];

		float m = maximumf(maximumf(fabsf(r), fabsf(g)), fabsf(b));

		if (m > 1e-10f)
		{
			m = 1.0f / m;

			r *= m;
			g *= m;
			b *= m;
		}

		xr = r;
		xg = g;
		xb = b;
	}

	float len = xr * xr + xg * xg + xb * xb;

	vec3F axis;
	if (len < 1e-10f)
		axis.set(0.0f);
	else
	{
		len = 1.0f / sqrtf(len);

		xr *= len;
		xg *= len;
		xb *= len;

		axis.set(xr, xg, xb, 0);
	}

	if (axis.dot(axis) < .5f)
	{
		axis.set(1.0f, 1.0f, 1.0f, 0.0f);
		axis.normalize_in_place();
	}

	return axis;
}

static vec3F interp_color(const vec3F& mean, const vec3F& dir, float df, const aabb3F& colorspace_box, const aabb3F& input_box, bool* pInside = nullptr)
{
#if 0
	assert(mean[0] >= input_box[0][0]);
	assert(mean[1] >= input_box[0][1]);
	assert(mean[2] >= input_box[0][2]);
	assert(mean[0] <= input_box[1][0]);
	assert(mean[1] <= input_box[1][1]);
	assert(mean[2] <= input_box[1][2]);
#endif

	if (pInside)
		*pInside = false;

	vec3F k(mean + dir * df);
	if (colorspace_box.contains(k))
	{
		if (pInside)
			*pInside = true;

		return k;
	}

	// starts inside
	vec3F s(mean);

	// ends outside
	vec3F e(mean + dir * df);

	// a ray guaranteed to go from the outside to inside
	ray3F r(e, (s - e).normalize_in_place());
	vec3F c;
	float t = 0.0f;

	intersection::result res = intersection::ray_aabb(c, t, r, input_box);
	if (res != intersection::cSuccess)
		c = k;

	return c;
}

// all in Q16 space, 0-65535
static bool compute_least_squares_endpoints_rgb(
	uint32_t N, const uint8_t* pSelectors, const vec4F* pSelector_weights,
	vec3F* pXl, vec3F* pXh, const vec4F* pColors, const aabb3F& input_box)
{
	// Least squares using normal equations: http://www.cs.cornell.edu/~bindel/class/cs3220-s12/notes/lec10.pdf 
	// https://web.archive.org/web/20150319232457/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][0];
		z10 += pSelector_weights[sel][1];
		z11 += pSelector_weights[sel][2];

		float w = pSelector_weights[sel][3];
		q00_r += w * pColors[i][0];
		t_r += pColors[i][0];

		q00_g += w * pColors[i][1];
		t_g += pColors[i][1];

		q00_b += w * pColors[i][2];
		t_b += pColors[i][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)
		return false;

	det = 1.0f / det;

	float iz00, iz01, iz10, iz11;
	iz00 = z11 * det;
	iz01 = -z01 * det;
	iz10 = -z10 * det;
	iz11 = z00 * det;

	(*pXl)[0] = (float)(iz00 * q00_r + iz01 * q10_r);
	(*pXh)[0] = (float)(iz10 * q00_r + iz11 * q10_r);

	(*pXl)[1] = (float)(iz00 * q00_g + iz01 * q10_g);
	(*pXh)[1] = (float)(iz10 * q00_g + iz11 * q10_g);

	(*pXl)[2] = (float)(iz00 * q00_b + iz01 * q10_b);
	(*pXh)[2] = (float)(iz10 * q00_b + iz11 * q10_b);

	for (uint32_t c = 0; c < 3; c++)
	{
		float l = (*pXl)[c], h = (*pXh)[c];

		if (input_box.get_dim(c) < .0000125f)
		{
			l = input_box[0][c];
			h = input_box[1][c];
		}

		(*pXl)[c] = l;
		(*pXh)[c] = h;
	}

	vec3F mean((*pXl + *pXh) * .5f);
	vec3F dir(*pXh - *pXl);

	float ln = dir.length();
	if (ln)
	{
		dir /= ln;

		float ld = (*pXl - mean).dot(dir);
		float hd = (*pXh - mean).dot(dir);

		aabb3F colorspace_box(vec3F(0.0f), vec3F(MAX_QLOG16_VAL));

		bool was_inside1 = false;

		vec3F l = interp_color(mean, dir, ld, colorspace_box, input_box, &was_inside1);
		if (!was_inside1)
			*pXl = l;

		bool was_inside2 = false;
		vec3F h = interp_color(mean, dir, hd, colorspace_box, input_box, &was_inside2);
		if (!was_inside2)
			*pXh = h;
	}

	pXl->clamp(0.0f, MAX_QLOG16_VAL);
	pXh->clamp(0.0f, MAX_QLOG16_VAL);

	return true;
}

static vec4F g_astc_ls_weights_ise[MAX_SUPPORTED_ISE_WEIGHT_INDEX + 1][24];

static uint8_t g_map_astc_to_linear_order[MAX_SUPPORTED_ISE_WEIGHT_INDEX + 1][24]; // [ise_range][astc_index] -> linear index
static uint8_t g_map_linear_to_astc_order[MAX_SUPPORTED_ISE_WEIGHT_INDEX + 1][24]; // [ise_range][linear_index] -> astc_index

static void encode_astc_hdr_init()
{
	// Precomputed weight constants used during least fit determination. For each entry: w * w, (1.0f - w) * w, (1.0f - w) * (1.0f - w), w
	for (uint32_t range = MIN_SUPPORTED_ISE_WEIGHT_INDEX; range <= MAX_SUPPORTED_ISE_WEIGHT_INDEX; range++)
	{
		const uint32_t num_levels = g_ise_weight_lerps[range][0];
		assert((num_levels >= 3) && (num_levels <= 24));

		for (uint32_t i = 0; i < num_levels; i++)
		{
			float w = g_ise_weight_lerps[range][1 + i] * (1.0f / 64.0f);

			g_astc_ls_weights_ise[range][i].set(w * w, (1.0f - w) * w, (1.0f - w) * (1.0f - w), w);
		}
	}

	for (uint32_t ise_range = MIN_SUPPORTED_ISE_WEIGHT_INDEX; ise_range <= MAX_SUPPORTED_ISE_WEIGHT_INDEX; ise_range++)
	{
		const uint32_t num_levels = g_ise_weight_lerps[ise_range][0];
		assert((num_levels >= 3) && (num_levels <= 24));

		uint32_t s[32];
		for (uint32_t i = 0; i < num_levels; i++)
			s[i] = (g_ise_weight_lerps[ise_range][1 + i] << 8) + i;

		std::sort(s, s + num_levels);

		for (uint32_t i = 0; i < num_levels; i++)
			g_map_linear_to_astc_order[ise_range][i] = (uint8_t)(s[i] & 0xFF);

		for (uint32_t i = 0; i < num_levels; i++)
			g_map_astc_to_linear_order[ise_range][g_map_linear_to_astc_order[ise_range][i]] = (uint8_t)i;
	}
}

void interpolate_qlog12_colors(
	const int e[2][3],
	half_float* pDecoded_half,
	vec3F* pDecoded_float,
	uint32_t n, uint32_t ise_weight_range)
{
	assert((ise_weight_range >= MIN_SUPPORTED_ISE_WEIGHT_INDEX) && (ise_weight_range <= MAX_SUPPORTED_ISE_WEIGHT_INDEX));

	for (uint32_t i = 0; i < 2; i++)
	{
		for (uint32_t j = 0; j < 3; j++)
		{
			assert(in_range(e[i][j], 0, 0xFFF));
		}
	}

	for (uint32_t i = 0; i < n; i++)
	{
		const int c = g_ise_weight_lerps[ise_weight_range][1 + i];
		assert(c == (int)astc_helpers::dequant_bise_weight(i, ise_weight_range));

		half_float rf, gf, bf;

		{
			uint32_t r0 = e[0][0] << 4;
			uint32_t r1 = e[1][0] << 4;
			int ri = (r0 * (64 - c) + r1 * c + 32) / 64;
			rf = qlog16_to_half_slow(ri);
		}

		{
			uint32_t g0 = e[0][1] << 4;
			uint32_t g1 = e[1][1] << 4;
			int gi = (g0 * (64 - c) + g1 * c + 32) / 64;
			gf = qlog16_to_half_slow(gi);
		}

		{
			uint32_t b0 = e[0][2] << 4;
			uint32_t b1 = e[1][2] << 4;
			int bi = (b0 * (64 - c) + b1 * c + 32) / 64;
			bf = qlog16_to_half_slow(bi);
		}

		if (pDecoded_half)
		{
			pDecoded_half[i * 3 + 0] = rf;
			pDecoded_half[i * 3 + 1] = gf;
			pDecoded_half[i * 3 + 2] = bf;
		}

		if (pDecoded_float)
		{
			pDecoded_float[i][0] = half_to_float(rf);
			pDecoded_float[i][1] = half_to_float(gf);
			pDecoded_float[i][2] = half_to_float(bf);
		}
	}
}

// decoded in ASTC order, not linear order
// return false if the ISE endpoint quantization leads to non-valid endpoints being decoded
bool get_astc_hdr_mode_11_block_colors(
	const uint8_t* pEndpoints,
	half_float* pDecoded_half,
	vec3F* pDecoded_float,
	uint32_t n, uint32_t ise_weight_range, uint32_t ise_endpoint_range)
{
	assert((ise_weight_range >= 1) && (ise_weight_range <= MAX_SUPPORTED_ISE_WEIGHT_INDEX));

	int e[2][3];
	if (!decode_mode11_to_qlog12(pEndpoints, e, ise_endpoint_range))
		return false;

	interpolate_qlog12_colors(e, pDecoded_half, pDecoded_float, n, ise_weight_range);

	return true;
}

// decoded in ASTC order, not linear order
// return false if the ISE endpoint quantization leads to non-valid endpoints being decoded
bool get_astc_hdr_mode_7_block_colors(
	const uint8_t* pEndpoints,
	half_float* pDecoded_half,
	vec3F* pDecoded_float,
	uint32_t n, uint32_t ise_weight_range, uint32_t ise_endpoint_range)
{
	assert((ise_weight_range >= 1) && (ise_weight_range <= MAX_SUPPORTED_ISE_WEIGHT_INDEX));

	int e[2][3];
	if (!decode_mode7_to_qlog12(pEndpoints, e, nullptr, ise_endpoint_range))
		return false;

	interpolate_qlog12_colors(e, pDecoded_half, pDecoded_float, n, ise_weight_range);

	return true;
}

// Fast high precision piecewise linear approximation of log2(bias+x).
// Half may be zero, positive or denormal. No NaN/Inf/negative.
static inline double q(half_float x)
{
	union { float f; int32_t i; uint32_t u; } fi;

	fi.f = fast_half_to_float_pos_not_inf_or_nan(x);

	assert(fi.f >= 0.0f);

	fi.f += .125f;

	return (double)fi.u; // approx log2f(fi.f), need to return double for the precision
}

double eval_selectors(
	uint32_t num_pixels,
	uint8_t* pWeights,
	const half_float* pBlock_pixels_half,
	uint32_t num_weight_levels,
	const half_float* pDecoded_half,
	const astc_hdr_codec_options& coptions,
	uint32_t usable_selector_bitmask)
{
	assert((num_pixels >= 1) && (num_pixels <= 16));
	assert(usable_selector_bitmask);

	const float R_WEIGHT = coptions.m_r_err_scale;
	const float G_WEIGHT = coptions.m_g_err_scale;

	double total_error = 0;

#ifdef _DEBUG
	for (uint32_t i = 0; i < num_weight_levels; i++)
	{
		assert(!is_half_inf_or_nan(pDecoded_half[i * 3 + 0]));
		assert(!is_half_inf_or_nan(pDecoded_half[i * 3 + 1]));
		assert(!is_half_inf_or_nan(pDecoded_half[i * 3 + 2]));
	}
#endif

	for (uint32_t p = 0; p < num_pixels; p++)
	{
		const half_float* pDesired_half = &pBlock_pixels_half[p * 3];

		double lowest_e = 1e+30f;

		// this is an approximation of MSLE
		for (uint32_t i = 0; i < num_weight_levels; i++)
		{
			if (((1 << i) & usable_selector_bitmask) == 0)
				continue;

			// compute piecewise linear approximation of log2(a+eps)-log2(b+eps), for each component, then MSLE
			double rd = q(pDecoded_half[i * 3 + 0]) - q(pDesired_half[0]);
			double gd = q(pDecoded_half[i * 3 + 1]) - q(pDesired_half[1]);
			double bd = q(pDecoded_half[i * 3 + 2]) - q(pDesired_half[2]);

			double e = R_WEIGHT * (rd * rd) + G_WEIGHT * (gd * gd) + bd * bd;

			if (e < lowest_e)
			{
				lowest_e = e;
				pWeights[p] = (uint8_t)i;
			}
		}

		total_error += lowest_e;

	} // p

	return total_error;
}

//--------------------------------------------------------------------------------------------------------------------------

double compute_block_error(const half_float* pOrig_block, const half_float* pPacked_block, const astc_hdr_codec_options& coptions)
{
	const float R_WEIGHT = coptions.m_r_err_scale;
	const float G_WEIGHT = coptions.m_g_err_scale;

	double total_error = 0;
		
	for (uint32_t p = 0; p < 16; p++)
	{
		double rd = q(pOrig_block[p * 3 + 0]) - q(pPacked_block[p * 3 + 0]);
		double gd = q(pOrig_block[p * 3 + 1]) - q(pPacked_block[p * 3 + 1]);
		double bd = q(pOrig_block[p * 3 + 2]) - q(pPacked_block[p * 3 + 2]);

		double e = R_WEIGHT * (rd * rd) + G_WEIGHT * (gd * gd) + bd * bd;

		total_error += e;
	}

	return total_error;
}

//--------------------------------------------------------------------------------------------------------------------------

static inline int compute_clamped_val(int v, int l, int h, bool& did_clamp, int& max_clamp_mag)
{
	assert(l < h);

	if (v < l)
	{
		max_clamp_mag = basisu::maximum<int>(max_clamp_mag, l - v);

		v = l;
		did_clamp = true;
	}
	else if (v > h)
	{
		max_clamp_mag = basisu::maximum<int>(max_clamp_mag, v - h);

		v = h;
		did_clamp = true;
	}

	return v;
}

static bool pack_astc_mode11_submode(uint32_t submode, uint8_t* pEndpoints, const vec3F& low_q16, const vec3F& high_q16, int& max_clamp_mag)
{
	assert(submode <= 7);

	const uint8_t s_b_bits[8] = { 7, 8, 6, 7,  8, 6, 7, 6 };
	const uint8_t s_c_bits[8] = { 6, 6, 7, 7,  6, 7, 7, 7 };
	const uint8_t s_d_bits[8] = { 7, 6, 7, 6,  5, 6, 5, 6 };

	const uint32_t a_bits = 9 + (submode >> 1);
	const uint32_t b_bits = s_b_bits[submode];
	const uint32_t c_bits = s_c_bits[submode];
	const uint32_t d_bits = s_d_bits[submode];

	const int max_a_val = (1 << a_bits) - 1;
	const int max_b_val = (1 << b_bits) - 1;
	const int max_c_val = (1 << c_bits) - 1;

	// The maximum usable value before it turns to NaN/Inf
	const int max_a_qlog = get_max_qlog(a_bits);

	const int min_d_val = -(1 << (d_bits - 1));
	const int max_d_val = -min_d_val - 1;
	assert((max_d_val - min_d_val + 1) == (1 << d_bits));

	int val_q[2][3];

	for (uint32_t c = 0; c < 3; c++)
	{
#if 1
		// this is better
		const half_float l = qlog16_to_half_slow((uint32_t)std::round(low_q16[c]));
		val_q[0][c] = half_to_qlog7_12(l, a_bits);
		
		const half_float h = qlog16_to_half_slow((uint32_t)std::round(high_q16[c]));
		val_q[1][c] = half_to_qlog7_12(h, a_bits);
#else
		val_q[0][c] = quant_qlog16((uint32_t)std::round(low_q16[c]), a_bits);
		val_q[1][c] = quant_qlog16((uint32_t)std::round(high_q16[c]), a_bits);
#endif
				
#if 1
		if (val_q[0][c] == val_q[1][c])
		{
#if 0
			if (l <= h)
#else
			if (low_q16[c] < high_q16[c])
#endif
			{
				if (val_q[0][c])
					val_q[0][c]--;

				if (val_q[1][c] != max_a_val)
					val_q[1][c]++;
			}
			else
			{
				if (val_q[0][c] != max_a_val)
					val_q[0][c]++;

				if (val_q[1][c])
					val_q[1][c]--;
			}
		}
#endif

		val_q[0][c] = minimum<uint32_t>(val_q[0][c], max_a_qlog);
		val_q[1][c] = minimum<uint32_t>(val_q[1][c], max_a_qlog);
	}

	int highest_q = -1, highest_val = 0, highest_comp = 0;

	for (uint32_t v = 0; v < 2; v++)
	{
		for (uint32_t c = 0; c < 3; c++)
		{
			assert(val_q[v][c] >= 0 && val_q[v][c] <= max_a_val);

			if (val_q[v][c] > highest_q)
			{
				highest_q = val_q[v][c];
				highest_val = v;
				highest_comp = c;
			}
		}
	}

	const bool had_tie = (val_q[highest_val ^ 1][highest_comp] == highest_q);

	if (highest_val != 1)
	{
		for (uint32_t c = 0; c < 3; c++)
		{
			std::swap(val_q[0][c], val_q[1][c]);
		}
	}

	if (highest_comp)
	{
		std::swap(val_q[0][0], val_q[0][highest_comp]);
		std::swap(val_q[1][0], val_q[1][highest_comp]);
	}

	int orig_q[2][3];
	memcpy(orig_q, val_q, sizeof(val_q));

	// val[1][0] is now guaranteed to be highest
	int best_va = 0, best_vb0 = 0, best_vb1 = 0, best_vc = 0, best_vd0 = 0, best_vd1 = 0;
	int best_max_clamp_mag = 0;
	bool best_did_clamp = false;
	int best_q[2][3] = { { 0, 0, 0}, { 0, 0, 0 }  };
	BASISU_NOTE_UNUSED(best_q);
	uint32_t best_dist = UINT_MAX;

	for (uint32_t pass = 0; pass < 2; pass++)
	{
		int trial_va = val_q[1][0];

		assert(trial_va <= max_a_val);
		assert(trial_va >= val_q[1][1]);
		assert(trial_va >= val_q[1][2]);

		assert(trial_va >= val_q[0][0]);
		assert(trial_va >= val_q[0][1]);
		assert(trial_va >= val_q[0][2]);

		bool did_clamp = false;
		int trial_max_clamp_mag = 0;

		int trial_vb0 = compute_clamped_val(trial_va - val_q[1][1], 0, max_b_val, did_clamp, trial_max_clamp_mag);
		int trial_vb1 = compute_clamped_val(trial_va - val_q[1][2], 0, max_b_val, did_clamp, trial_max_clamp_mag);
		int trial_vc = compute_clamped_val(trial_va - val_q[0][0], 0, max_c_val, did_clamp, trial_max_clamp_mag);
		int trial_vd0 = compute_clamped_val((trial_va - trial_vb0 - trial_vc) - val_q[0][1], min_d_val, max_d_val, did_clamp, trial_max_clamp_mag);
		int trial_vd1 = compute_clamped_val((trial_va - trial_vb1 - trial_vc) - val_q[0][2], min_d_val, max_d_val, did_clamp, trial_max_clamp_mag);

		if (!did_clamp)
		{
			// Make sure decoder gets the expected values
			assert(trial_va == val_q[1][0]);
			assert(trial_va - trial_vb0 == val_q[1][1]);
			assert(trial_va - trial_vb1 == val_q[1][2]);

			assert((trial_va - trial_vc) == val_q[0][0]);
			assert((trial_va - trial_vb0 - trial_vc - trial_vd0) == val_q[0][1]);
			assert((trial_va - trial_vb1 - trial_vc - trial_vd1) == val_q[0][2]);
		}

		const int r_e0 = clamp<int>(trial_va, 0, max_a_val);
		const int r_e1 = clamp<int>(trial_va - trial_vb0, 0, max_a_val);
		const int r_e2 = clamp<int>(trial_va - trial_vb1, 0, max_a_val);

		const int r_f0 = clamp<int>(trial_va - trial_vc, 0, max_a_val);
		const int r_f1 = clamp<int>(trial_va - trial_vb0 - trial_vc - trial_vd0, 0, max_a_val);
		const int r_f2 = clamp<int>(trial_va - trial_vb1 - trial_vc - trial_vd1, 0, max_a_val);

		assert(r_e0 <= max_a_qlog);
		assert(r_e1 <= max_a_qlog);
		assert(r_e2 <= max_a_qlog);

		assert(r_f0 <= max_a_qlog);
		assert(r_f1 <= max_a_qlog);
		assert(r_f2 <= max_a_qlog);

		if ((!did_clamp) || (!had_tie))
		{
			best_va = trial_va;
			best_vb0 = trial_vb0;
			best_vb1 = trial_vb1;
			best_vc = trial_vc;
			best_vd0 = trial_vd0;
			best_vd1 = trial_vd1;
			best_max_clamp_mag = trial_max_clamp_mag;
			best_did_clamp = did_clamp;

			best_q[1][0] = r_e0;
			best_q[1][1] = r_e1;
			best_q[1][2] = r_e2;
			best_q[0][0] = r_f0;
			best_q[0][1] = r_f1;
			best_q[0][2] = r_f2;
			break;
		}

		// we had a tie and it did clamp, try swapping L/H for a potential slight gain

		const uint32_t r_dist1 = basisu::square<int>(r_e0 - val_q[1][0]) + basisu::square<int>(r_e1 - val_q[1][1]) + basisu::square<int>(r_e2 - val_q[1][2]);
		const uint32_t r_dist0 = basisu::square<int>(r_f0 - val_q[0][0]) + basisu::square<int>(r_f1 - val_q[0][1]) + basisu::square<int>(r_f2 - val_q[0][2]);

		const uint32_t total_dist = r_dist1 + r_dist0;

		if (total_dist < best_dist)
		{
			best_dist = total_dist;

			best_va = trial_va;
			best_vb0 = trial_vb0;
			best_vb1 = trial_vb1;
			best_vc = trial_vc;
			best_vd0 = trial_vd0;
			best_vd1 = trial_vd1;
			best_did_clamp = did_clamp;

			best_q[1][0] = r_e0;
			best_q[1][1] = r_e1;
			best_q[1][2] = r_e2;
			best_q[0][0] = r_f0;
			best_q[0][1] = r_f1;
			best_q[0][2] = r_f2;
		}

		for (uint32_t c = 0; c < 3; c++)
			std::swap(val_q[0][c], val_q[1][c]);
	}

	// pack bits now
	int v0 = 0, v1 = 0, v2 = 0, v3 = 0, v4 = 0, v5 = 0;

	int x0 = 0, x1 = 0, x2 = 0, x3 = 0, x4 = 0, x5 = 0;
	switch (submode)
	{
	case 0:
		x0 = get_bit(best_vb0, 6); x1 = get_bit(best_vb1, 6); x2 = get_bit(best_vd0, 6); x3 = get_bit(best_vd1, 6); x4 = get_bit(best_vd0, 5); x5 = get_bit(best_vd1, 5);
		break;
	case 1:
		x0 = get_bit(best_vb0, 6); x1 = get_bit(best_vb1, 6); x2 = get_bit(best_vb0, 7); x3 = get_bit(best_vb1, 7); x4 = get_bit(best_vd0, 5); x5 = get_bit(best_vd1, 5);
		break;
	case 2:
		x0 = get_bit(best_va, 9); x1 = get_bit(best_vc, 6); x2 = get_bit(best_vd0, 6); x3 = get_bit(best_vd1, 6); x4 = get_bit(best_vd0, 5); x5 = get_bit(best_vd1, 5);
		break;
	case 3:
		x0 = get_bit(best_vb0, 6); x1 = get_bit(best_vb1, 6); x2 = get_bit(best_va, 9); x3 = get_bit(best_vc, 6); x4 = get_bit(best_vd0, 5); x5 = get_bit(best_vd1, 5);
		break;
	case 4:
		x0 = get_bit(best_vb0, 6); x1 = get_bit(best_vb1, 6); x2 = get_bit(best_vb0, 7); x3 = get_bit(best_vb1, 7); x4 = get_bit(best_va, 9); x5 = get_bit(best_va, 10);
		break;
	case 5:
		x0 = get_bit(best_va, 9); x1 = get_bit(best_va, 10); x2 = get_bit(best_vc, 7); x3 = get_bit(best_vc, 6); x4 = get_bit(best_vd0, 5); x5 = get_bit(best_vd1, 5);
		break;
	case 6:
		x0 = get_bit(best_vb0, 6); x1 = get_bit(best_vb1, 6); x2 = get_bit(best_va, 11); x3 = get_bit(best_vc, 6); x4 = get_bit(best_va, 9); x5 = get_bit(best_va, 10);
		break;
	case 7:
		x0 = get_bit(best_va, 9); x1 = get_bit(best_va, 10); x2 = get_bit(best_va, 11); x3 = get_bit(best_vc, 6); x4 = get_bit(best_vd0, 5); x5 = get_bit(best_vd1, 5);
		break;
	default:
		break;
	}

	// write mode
	pack_bit(v1, 7, submode, 0);
	pack_bit(v2, 7, submode, 1);
	pack_bit(v3, 7, submode, 2);

	// highest component
	pack_bit(v4, 7, highest_comp, 0);
	pack_bit(v5, 7, highest_comp, 1);

	// write bit 8 of va
	pack_bit(v1, 6, best_va, 8);

	// extra bits
	pack_bit(v2, 6, x0);
	pack_bit(v3, 6, x1);
	pack_bit(v4, 6, x2);
	pack_bit(v5, 6, x3);
	pack_bit(v4, 5, x4);
	pack_bit(v5, 5, x5);

	v0 = best_va & 0xFF;
	v1 |= (best_vc & 63);
	v2 |= (best_vb0 & 63);
	v3 |= (best_vb1 & 63);
	v4 |= (best_vd0 & 31);
	v5 |= (best_vd1 & 31);

	assert(in_range(v0, 0, 255) && in_range(v1, 0, 255) && in_range(v2, 0, 255) && in_range(v3, 0, 255) && in_range(v4, 0, 255) && in_range(v5, 0, 255));

	pEndpoints[0] = (uint8_t)v0;
	pEndpoints[1] = (uint8_t)v1;
	pEndpoints[2] = (uint8_t)v2;
	pEndpoints[3] = (uint8_t)v3;
	pEndpoints[4] = (uint8_t)v4;
	pEndpoints[5] = (uint8_t)v5;

#ifdef _DEBUG
	// Test for valid pack by unpacking
	{
		if (highest_comp)
		{
			std::swap(best_q[0][0], best_q[0][highest_comp]);
			std::swap(best_q[1][0], best_q[1][highest_comp]);

			std::swap(orig_q[0][0], orig_q[0][highest_comp]);
			std::swap(orig_q[1][0], orig_q[1][highest_comp]);
		}

		int test_e[2][3];
		decode_mode11_to_qlog12(pEndpoints, test_e, astc_helpers::BISE_256_LEVELS);
		for (uint32_t i = 0; i < 2; i++)
		{
			for (uint32_t j = 0; j < 3; j++)
			{
				assert(best_q[i][j] == test_e[i][j] >> (12 - a_bits));

				if (!best_did_clamp)
				{
					assert((orig_q[i][j] == test_e[i][j] >> (12 - a_bits)) ||
						(orig_q[1 - i][j] == test_e[i][j] >> (12 - a_bits)));
				}
			}
		}
	}
#endif

	max_clamp_mag = best_max_clamp_mag;

	return best_did_clamp;
}

//--------------------------------------------------------------------------------------------------------------------------

static void pack_astc_mode11_direct(uint8_t* pEndpoints, const vec3F& l_q16, const vec3F& h_q16)
{
	for (uint32_t i = 0; i < 3; i++)
	{
		// TODO: This goes from QLOG16->HALF->QLOG8/7
		half_float l_half = qlog16_to_half_slow(clamp((int)std::round(l_q16[i]), 0, 65535));
		half_float h_half = qlog16_to_half_slow(clamp((int)std::round(h_q16[i]), 0, 65535));

		int l_q, h_q;

		if (i == 2)
		{
			l_q = g_half_to_qlog7[bounds_check((uint32_t)l_half, 0U, 32768U)];
			h_q = g_half_to_qlog7[bounds_check((uint32_t)h_half, 0U, 32768U)];

			l_q = minimum<uint32_t>(l_q, MAX_QLOG7);
			h_q = minimum<uint32_t>(h_q, MAX_QLOG7);
		}
		else
		{
			l_q = g_half_to_qlog8[bounds_check((uint32_t)l_half, 0U, 32768U)];
			h_q = g_half_to_qlog8[bounds_check((uint32_t)h_half, 0U, 32768U)];

			l_q = minimum<uint32_t>(l_q, MAX_QLOG8);
			h_q = minimum<uint32_t>(h_q, MAX_QLOG8);
		}

#if 1
		if (l_q == h_q)
		{
			const int m = (i == 2) ? MAX_QLOG7 : MAX_QLOG8;

			if (l_q16[i] <= h_q16[i])
			{
				if (l_q)
					l_q--;

				if (h_q != m)
					h_q++;
			}
			else
			{
				if (h_q)
					h_q--;

				if (l_q != m)
					l_q++;
			}
		}
#endif
				
		if (i == 2)
		{
			assert(l_q <= (int)MAX_QLOG7 && h_q <= (int)MAX_QLOG7);
			l_q |= 128;
			h_q |= 128;
		}
		else
		{
			assert(l_q <= (int)MAX_QLOG8 && h_q <= (int)MAX_QLOG8);
		}

		pEndpoints[2 * i + 0] = (uint8_t)l_q;
		pEndpoints[2 * i + 1] = (uint8_t)h_q;
	}
}

//--------------------------------------------------------------------------------------------------------------------------

static bool pack_astc_mode7_submode(uint32_t submode, uint8_t* pEndpoints, const vec3F& rgb_q16, float s_q16, int& max_clamp_mag, uint32_t ise_weight_range)
{
	assert((ise_weight_range >= 1) && (ise_weight_range <= MAX_SUPPORTED_ISE_WEIGHT_INDEX));

	assert(submode <= 5);
	max_clamp_mag = 0;

	static const uint8_t s_r_bits[6] = { 11, 11, 10, 9, 8, 7 };
	static const uint8_t s_g_b_bits[6] = { 5, 6, 5, 6, 7, 7 };
	static const uint8_t s_s_bits[6] = { 7, 5, 8, 7, 6, 7 };

	// The precision of the components
	const uint32_t prec_bits = s_r_bits[submode];

	int qlog[4], pack_bits[4];

	for (uint32_t i = 0; i < 4; i++)
	{
		const float f = (i == 3) ? s_q16 : rgb_q16[i];

		// The # of bits the component is packed into
		if (i == 0)
			pack_bits[i] = s_r_bits[submode];
		else if (i == 3)
			pack_bits[i] = s_s_bits[submode];
		else
			pack_bits[i] = s_g_b_bits[submode];

#if 0
		// this is slightly worse
		// TODO: going from qlog16 to half loses some precision. Then going from half to qlog 7-12 will have extra error.
		half_float h = qlog_to_half(clamp((int)std::round(f), 0, MAX_QLOG16), 16);
		qlog[i] = half_to_qlog7_12((half_float)bounds_check((uint32_t)h, 0U, 32768U), prec_bits);
#else
		qlog[i] = quant_qlog16(clamp<int>((int)std::round(f), 0, MAX_QLOG16), prec_bits);

		// Only bias if there are enough texel weights, 4=6 weights
		if (ise_weight_range >= 4)
		{
			// Explictly bias the high color, and the scale up, to better exploit the weights.
			// The quantized range also then encompases the complete input range.
			const uint32_t max_val = (1 << prec_bits) - 1;
			const uint32_t K = 3;
			if (i == 3)
			{
				qlog[i] = minimum<uint32_t>(qlog[i] + K * 2, max_val);
			}
			else
			{
				qlog[i] = minimum<uint32_t>(qlog[i] + K, max_val);
			}
		}
#endif

		if (i != 3)
			qlog[i] = minimum<uint32_t>(qlog[i], get_max_qlog(prec_bits));

		// If S=0, we lose freedom for the texel weights to add any value.
		if ((i == 3) && (qlog[i] == 0))
			qlog[i] = 1;
	}

	uint32_t maj_index = 0;

	bool did_clamp = false;

	if (submode != 5)
	{
		int largest_qlog = 0;
		for (uint32_t i = 0; i < 3; i++)
		{
			if (qlog[i] > largest_qlog)
			{
				largest_qlog = qlog[i];
				maj_index = i;
			}
		}

		if (maj_index)
		{
			std::swap(qlog[0], qlog[maj_index]);
		}

		assert(qlog[0] >= qlog[1]);
		assert(qlog[0] >= qlog[2]);

		qlog[1] = qlog[0] - qlog[1];
		qlog[2] = qlog[0] - qlog[2];

		for (uint32_t i = 1; i < 4; i++)
		{
			const int max_val = (1 << pack_bits[i]) - 1;

			if (qlog[i] > max_val)
			{
				max_clamp_mag = maximum<int>(max_clamp_mag, qlog[i] - max_val);
				qlog[i] = max_val;
				did_clamp = true;
			}
		}
	}

	for (uint32_t i = 0; i < 4; i++)
	{
		const int max_val = (1 << pack_bits[i]) - 1; (void)max_val;

		assert(qlog[i] <= max_val);
	}

	int mode = 0;

	int r = qlog[0] & 63; // 6-bits
	int g = qlog[1] & 31; // 5-bits
	int b = qlog[2] & 31; // 5-bits
	int s = qlog[3] & 31; // 5-bits

	int x0 = 0, x1 = 0, x2 = 0, x3 = 0, x4 = 0, x5 = 0, x6 = 0;

	switch (submode)
	{
	case 0:
	{
		mode = (maj_index << 2) | 0;
		assert((mode & 0xC) != 0xC);

		x0 = get_bit(qlog[0], 9); // R9
		x1 = get_bit(qlog[0], 8); // R8
		x2 = get_bit(qlog[0], 7); // R7
		x3 = get_bit(qlog[0], 10); // R10
		x4 = get_bit(qlog[0], 6); // R6 
		x5 = get_bit(qlog[3], 6); // S6
		x6 = get_bit(qlog[3], 5); // S5
		break;
	}
	case 1:
	{
		mode = (maj_index << 2) | 1;
		assert((mode & 0xC) != 0xC);

		x0 = get_bit(qlog[0], 8); // R8
		x1 = get_bit(qlog[1], 5); // G5
		x2 = get_bit(qlog[0], 7); // R7
		x3 = get_bit(qlog[2], 5); // B5
		x4 = get_bit(qlog[0], 6); // R6 
		x5 = get_bit(qlog[0], 10); // R10
		x6 = get_bit(qlog[0], 9); // R9
		break;
	}
	case 2:
	{
		mode = (maj_index << 2) | 2;
		assert((mode & 0xC) != 0xC);

		x0 = get_bit(qlog[0], 9); // R9
		x1 = get_bit(qlog[0], 8); // R8
		x2 = get_bit(qlog[0], 7); // R7
		x3 = get_bit(qlog[0], 6); // R6
		x4 = get_bit(qlog[3], 7); // S7 
		x5 = get_bit(qlog[3], 6); // S6
		x6 = get_bit(qlog[3], 5); // S5
		break;
	}
	case 3:
	{
		mode = (maj_index << 2) | 3;
		assert((mode & 0xC) != 0xC);

		x0 = get_bit(qlog[0], 8); // R8
		x1 = get_bit(qlog[1], 5); // G5
		x2 = get_bit(qlog[0], 7); // R7
		x3 = get_bit(qlog[2], 5); // B5
		x4 = get_bit(qlog[0], 6); // R6 
		x5 = get_bit(qlog[3], 6); // S6
		x6 = get_bit(qlog[3], 5); // S5
		break;
	}
	case 4:
	{
		mode = maj_index | 0xC; // 0b1100
		assert((mode & 0xC) == 0xC);
		assert(mode != 0xF);

		x0 = get_bit(qlog[1], 6); // G6
		x1 = get_bit(qlog[1], 5); // G5
		x2 = get_bit(qlog[2], 6); // B6
		x3 = get_bit(qlog[2], 5); // B5
		x4 = get_bit(qlog[0], 6); // R6 
		x5 = get_bit(qlog[0], 7); // R7
		x6 = get_bit(qlog[3], 5); // S5
		break;
	}
	case 5:
	{
		mode = 0xF;

		x0 = get_bit(qlog[1], 6); // G6
		x1 = get_bit(qlog[1], 5); // G5
		x2 = get_bit(qlog[2], 6); // B6
		x3 = get_bit(qlog[2], 5); // B5
		x4 = get_bit(qlog[0], 6); // R6 
		x5 = get_bit(qlog[3], 6); // S6
		x6 = get_bit(qlog[3], 5); // S5
		break;
	}
	default:
	{
		assert(0);
		break;
	}
	}

	pEndpoints[0] = (uint8_t)((get_bit(mode, 1) << 7) | (get_bit(mode, 0) << 6) | r);
	pEndpoints[1] = (uint8_t)((get_bit(mode, 2) << 7) | (x0 << 6) | (x1 << 5) | g);
	pEndpoints[2] = (uint8_t)((get_bit(mode, 3) << 7) | (x2 << 6) | (x3 << 5) | b);
	pEndpoints[3] = (uint8_t)((x4 << 7) | (x5 << 6) | (x6 << 5) | s);

#ifdef _DEBUG
	// Test for valid pack by unpacking
	{
		const int inv_shift = 12 - prec_bits;

		int unpacked_e[2][3];
		if (submode != 5)
		{
			unpacked_e[1][0] = left_shift32(qlog[0], inv_shift);
			unpacked_e[1][1] = clamp(left_shift32((qlog[0] - qlog[1]), inv_shift), 0, 0xFFF);
			unpacked_e[1][2] = clamp(left_shift32((qlog[0] - qlog[2]), inv_shift), 0, 0xFFF);

			unpacked_e[0][0] = clamp(left_shift32((qlog[0] - qlog[3]), inv_shift), 0, 0xFFF);
			unpacked_e[0][1] = clamp(left_shift32(((qlog[0] - qlog[1]) - qlog[3]), inv_shift), 0, 0xFFF);
			unpacked_e[0][2] = clamp(left_shift32(((qlog[0] - qlog[2]) - qlog[3]), inv_shift), 0, 0xFFF);
		}
		else
		{
			unpacked_e[1][0] = left_shift32(qlog[0], inv_shift);
			unpacked_e[1][1] = left_shift32(qlog[1], inv_shift);
			unpacked_e[1][2] = left_shift32(qlog[2], inv_shift);

			unpacked_e[0][0] = clamp(left_shift32((qlog[0] - qlog[3]), inv_shift), 0, 0xFFF);
			unpacked_e[0][1] = clamp(left_shift32((qlog[1] - qlog[3]), inv_shift), 0, 0xFFF);
			unpacked_e[0][2] = clamp(left_shift32((qlog[2] - qlog[3]), inv_shift), 0, 0xFFF);
		}

		if (maj_index)
		{
			std::swap(unpacked_e[0][0], unpacked_e[0][maj_index]);
			std::swap(unpacked_e[1][0], unpacked_e[1][maj_index]);
		}

		int e[2][3];
		decode_mode7_to_qlog12_ise20(pEndpoints, e, nullptr);

		for (uint32_t i = 0; i < 3; i++)
		{
			assert(unpacked_e[0][i] == e[0][i]);
			assert(unpacked_e[1][i] == e[1][i]);
		}
	}
#endif

	return did_clamp;
}

//--------------------------------------------------------------------------------------------------------------------------

static void quantize_ise_endpoints(uint32_t ise_endpoint_range, const uint8_t* pSrc_endpoints, uint8_t *pDst_endpoints, uint32_t n)
{
	assert((ise_endpoint_range >= astc_helpers::FIRST_VALID_ENDPOINT_ISE_RANGE) && (ise_endpoint_range <= astc_helpers::LAST_VALID_ENDPOINT_ISE_RANGE));

	if (ise_endpoint_range == astc_helpers::BISE_256_LEVELS)
	{
		memcpy(pDst_endpoints, pSrc_endpoints, n);
	}
	else
	{
		for (uint32_t i = 0; i < n; i++)
		{
			uint32_t v = pSrc_endpoints[i];
			assert(v <= 255);

			pDst_endpoints[i] = astc_helpers::g_dequant_tables.get_endpoint_tab(ise_endpoint_range).m_val_to_ise[v];
		}
	}
}

//--------------------------------------------------------------------------------------------------------------------------

// Note this could fail to find any valid solution if use_endpoint_range!=20.
// Returns true if improved.
static bool try_mode11(uint32_t num_pixels,
	uint8_t* pEndpoints, uint8_t* pWeights, double& cur_block_error, uint32_t& submode_used,
	vec3F& low_color_q16, const vec3F& high_color_q16,
	half_float block_pixels_half[16][3],
	uint32_t num_weight_levels, uint32_t ise_weight_range, const astc_hdr_codec_options& coptions, bool direct_only, uint32_t ise_endpoint_range, 
	bool constrain_ise_weight8_selectors, 
	int32_t first_submode, int32_t last_submode) // -1, 7
{
	assert((ise_weight_range >= 1) && (ise_weight_range <= MAX_SUPPORTED_ISE_WEIGHT_INDEX));
	assert((num_weight_levels >= 3) && (num_weight_levels <= 32));
	assert((num_pixels >= 1) && (num_pixels <= 16));

	bool improved_flag = false;

	half_float decoded_half[32][3];
	vec3F decoded_float[32];
	uint8_t orig_trial_endpoints[NUM_MODE11_ENDPOINTS], trial_endpoints[NUM_MODE11_ENDPOINTS], trial_weights[16];

	if (direct_only)
	{
		first_submode = -1;
		last_submode = -1;
	}

	assert(first_submode <= last_submode);
	assert((first_submode >= -1) && (first_submode <= 7));
	assert((last_submode >= -1) && (last_submode <= 7));

	// TODO: First determine if a submode doesn't clamp first. If one is found, encode to that and we're done.
	for (int submode = last_submode; submode >= first_submode; submode--)
	{
		bool did_clamp = false;
		int max_clamp_mag = 0;
		if (submode == -1)
		{
			// If it had to clamp with one of the submodes, try direct which can't clamp, but has low precision.
			pack_astc_mode11_direct(orig_trial_endpoints, low_color_q16, high_color_q16);
		}
		else
		{
			did_clamp = pack_astc_mode11_submode(submode, orig_trial_endpoints, low_color_q16, high_color_q16, max_clamp_mag);

			// If it had to clamp and the clamp was too high, it'll distort the endpoint colors too much, which could lead to noticeable artifacts.
			const int MAX_CLAMP_MAG_ACCEPT_THRESH = 4;
			if ((did_clamp) && (max_clamp_mag > MAX_CLAMP_MAG_ACCEPT_THRESH))
				continue;
		}
				
		// This will distort the endpoints if the ISE endpoint range isn't 256 levels (20).
		// It could massively distort the endpoints, but still result in a valid encoding.
		quantize_ise_endpoints(ise_endpoint_range, orig_trial_endpoints, trial_endpoints, NUM_MODE11_ENDPOINTS);
		
		if (!get_astc_hdr_mode_11_block_colors(trial_endpoints, &decoded_half[0][0], decoded_float, num_weight_levels, ise_weight_range, ise_endpoint_range))
			continue;

		uint32_t usable_selector_bitmask = UINT32_MAX;
		if ((constrain_ise_weight8_selectors) && (ise_weight_range == astc_helpers::BISE_16_LEVELS))
			usable_selector_bitmask = (1 << 0) | (1 << 1) | (1 << 4) | (1 << 5) | (1 << 10) | (1 << 11) | (1 << 14) | (1 << 15);

		double trial_blk_error = eval_selectors(num_pixels, trial_weights, &block_pixels_half[0][0], num_weight_levels, &decoded_half[0][0], coptions, usable_selector_bitmask);
		if (trial_blk_error < cur_block_error)
		{
			cur_block_error = trial_blk_error;
			memcpy(pEndpoints, trial_endpoints, NUM_MODE11_ENDPOINTS);
			memcpy(pWeights, trial_weights, num_pixels);
			submode_used = submode + 1;
			improved_flag = true;
		}

		// If it didn't clamp it was a lossless encode at this precision, so we can stop early as there's probably no use trying lower precision submodes.
		// (Although it may be, because a lower precision pack could try nearby voxel coords.)
		// However, at lower levels quantization may cause the decoded endpoints to be very distorted, so we need to evaluate up to direct.
		if (ise_endpoint_range == astc_helpers::BISE_256_LEVELS) 
		{
			if (!did_clamp)
				break;
		}
	}

	return improved_flag;
}

//--------------------------------------------------------------------------------------------------------------------------

static bool try_mode7(
	uint32_t num_pixels,
	uint8_t* pEndpoints, uint8_t* pWeights, double& cur_block_error, uint32_t& submode_used,
	vec3F& high_color_q16, const float s_q16,
	half_float block_pixels_half[16][3],
	uint32_t num_weight_levels, uint32_t ise_weight_range, const astc_hdr_codec_options& coptions, 
	uint32_t ise_endpoint_range)
{
	assert((ise_weight_range >= 1) && (ise_weight_range <= MAX_SUPPORTED_ISE_WEIGHT_INDEX));
	assert((num_pixels >= 1) && (num_pixels <= 16));

	bool improved_flag = false;

	half_float decoded_half[24][3];
	vec3F decoded_float[24];

	uint8_t orig_trial_endpoints[NUM_MODE7_ENDPOINTS], trial_endpoints[NUM_MODE7_ENDPOINTS], trial_weights[16];

	// TODO: First determine if a submode doesn't clamp first. If one is found, encode to that and we're done.
	for (int submode = 0; submode <= 5; submode++)
	{
		int max_clamp_mag = 0;
		const bool did_clamp = pack_astc_mode7_submode(submode, orig_trial_endpoints, high_color_q16, s_q16, max_clamp_mag, ise_weight_range);

		if (submode < 5)
		{
			const int MAX_CLAMP_MAG_ACCEPT_THRESH = 4;
			if ((did_clamp) && (max_clamp_mag > MAX_CLAMP_MAG_ACCEPT_THRESH))
				continue;
		}

		// This will distort the endpoints if the ISE endpoint range isn't 256 levels (20).
		// It could massively distort the endpoints, but still result in a valid encoding.
		quantize_ise_endpoints(ise_endpoint_range, orig_trial_endpoints, trial_endpoints, NUM_MODE7_ENDPOINTS);

		if (!get_astc_hdr_mode_7_block_colors(trial_endpoints, &decoded_half[0][0], decoded_float, num_weight_levels, ise_weight_range, ise_endpoint_range))
			continue;

		double trial_blk_error = eval_selectors(num_pixels, trial_weights, &block_pixels_half[0][0], num_weight_levels, &decoded_half[0][0], coptions);
		if (trial_blk_error < cur_block_error)
		{
			cur_block_error = trial_blk_error;
			memcpy(pEndpoints, trial_endpoints, NUM_MODE7_ENDPOINTS);
			memcpy(pWeights, trial_weights, num_pixels);
			submode_used = submode;
			improved_flag = true;
		}

		if (ise_endpoint_range == astc_helpers::BISE_256_LEVELS)
		{
			if (!did_clamp)
				break;
		}
	}

	return improved_flag;
}

//--------------------------------------------------------------------------------------------------------------------------

static double encode_astc_hdr_block_mode_11(
	uint32_t num_pixels,
	const vec4F* pBlock_pixels,
	uint32_t ise_weight_range,
	uint32_t& best_submode,
	double cur_block_error,
	uint8_t* blk_endpoints, uint8_t* blk_weights,
	const astc_hdr_codec_options& coptions,
	bool direct_only,
	uint32_t ise_endpoint_range,
	bool uber_mode,
	bool constrain_ise_weight8_selectors,
	int32_t first_submode, int32_t last_submode)
{
	assert((ise_weight_range >= 1) && (ise_weight_range <= MAX_SUPPORTED_ISE_WEIGHT_INDEX));
	assert((ise_endpoint_range >= astc_helpers::FIRST_VALID_ENDPOINT_ISE_RANGE) && (ise_endpoint_range <= astc_helpers::LAST_VALID_ENDPOINT_ISE_RANGE));
	assert((num_pixels >= 1) && (num_pixels <= 16));

	best_submode = 0;

	half_float block_pixels_half[16][3];
	vec4F block_pixels_q16[16];
		
	// TODO: This is done redundantly.
	for (uint32_t i = 0; i < num_pixels; i++)
	{
		block_pixels_half[i][0] = float_to_half_non_neg_no_nan_inf(pBlock_pixels[i][0]);
		block_pixels_q16[i][0] = (float)half_to_qlog16(block_pixels_half[i][0]);

		block_pixels_half[i][1] = float_to_half_non_neg_no_nan_inf(pBlock_pixels[i][1]);
		block_pixels_q16[i][1] = (float)half_to_qlog16(block_pixels_half[i][1]);

		block_pixels_half[i][2] = float_to_half_non_neg_no_nan_inf(pBlock_pixels[i][2]);
		block_pixels_q16[i][2] = (float)half_to_qlog16(block_pixels_half[i][2]);

		block_pixels_q16[i][3] = 0.0f;
	}

	const uint32_t num_weight_levels = astc_helpers::get_ise_levels(ise_weight_range);
	
	// TODO: should match MAX_SUPPORTED_ISE_WEIGHT_INDEX
	const uint32_t MAX_WEIGHT_LEVELS = 32;
	(void)MAX_WEIGHT_LEVELS;
	assert(num_weight_levels <= MAX_WEIGHT_LEVELS);

	vec3F block_mean_color_q16(calc_mean(num_pixels, block_pixels_q16));
	vec3F block_axis_q16(calc_rgb_pca(num_pixels, block_pixels_q16, block_mean_color_q16));

	aabb3F color_box_q16(cInitExpand);

	float l = 1e+30f, h = -1e+30f;
	vec3F low_color_q16, high_color_q16;

	for (uint32_t i = 0; i < num_pixels; i++)
	{
		color_box_q16.expand(block_pixels_q16[i]);

		vec3F k(vec3F(block_pixels_q16[i]) - block_mean_color_q16);
		float kd = k.dot(block_axis_q16);

		if (kd < l)
		{
			l = kd;
			low_color_q16 = block_pixels_q16[i];
		}

		if (kd > h)
		{
			h = kd;
			high_color_q16 = block_pixels_q16[i];
		}
	}

	vec3F old_low_color_q16(low_color_q16), old_high_color_q16(high_color_q16);
	for (uint32_t i = 0; i < 3; i++)
	{
		low_color_q16[i] = lerp<float>(old_low_color_q16[i], old_high_color_q16[i], 1.0f / 64.0f);
		high_color_q16[i] = lerp<float>(old_low_color_q16[i], old_high_color_q16[i], 63.0f / 64.0f);
	}
		
	uint8_t trial_blk_endpoints[NUM_MODE11_ENDPOINTS];
	uint8_t trial_blk_weights[16];
	uint32_t trial_best_submode = 0;
	
	clear_obj(trial_blk_endpoints);
	clear_obj(trial_blk_weights);
	
	double trial_blk_error = 1e+30f;

	bool did_improve = try_mode11(num_pixels, trial_blk_endpoints, trial_blk_weights, trial_blk_error, trial_best_submode,
		low_color_q16, high_color_q16,
		block_pixels_half, num_weight_levels, ise_weight_range, coptions, direct_only, ise_endpoint_range, constrain_ise_weight8_selectors,
		first_submode, last_submode);
	
	// If we couldn't find ANY usable solution due to endpoint quantization, just return. There's nothing we can do.
	if (!did_improve)
		return cur_block_error;

	// Did the solution improve?
	if (trial_blk_error < cur_block_error)
	{
		cur_block_error = trial_blk_error;
		memcpy(blk_endpoints, trial_blk_endpoints, NUM_MODE11_ENDPOINTS);
		memcpy(blk_weights, trial_blk_weights, num_pixels);
		best_submode = trial_best_submode;
	}
		
#define USE_LEAST_SQUARES (1)
#if USE_LEAST_SQUARES
	// least squares on the most promising trial weight indices found
	const uint32_t NUM_LS_PASSES = 3;

	for (uint32_t pass = 0; pass < NUM_LS_PASSES; pass++)
	{
		vec3F l_q16, h_q16;
		if (!compute_least_squares_endpoints_rgb(num_pixels, trial_blk_weights, &g_astc_ls_weights_ise[ise_weight_range][0], &l_q16, &h_q16, block_pixels_q16, color_box_q16))
			break;

		bool was_improved = try_mode11(num_pixels, blk_endpoints, blk_weights, cur_block_error, best_submode,
			l_q16, h_q16,
			block_pixels_half, num_weight_levels, ise_weight_range, coptions, direct_only, ise_endpoint_range, constrain_ise_weight8_selectors,
			first_submode, last_submode);

		if (!was_improved)
			break;

		// It's improved, so let's take the new weight indices.
		memcpy(trial_blk_weights, blk_weights, num_pixels);

	} // pass
#endif
		
	if (uber_mode)
	{
		// Try varying the current best weight indices. This can be expanded/improved, but at potentially great cost.

		uint8_t temp_astc_weights[16];
		memcpy(temp_astc_weights, trial_blk_weights, num_pixels);

		uint32_t min_lin_sel = 256, max_lin_sel = 0;
		for (uint32_t i = 0; i < num_pixels; i++)
		{
			const uint32_t astc_sel = temp_astc_weights[i];

			const uint32_t lin_sel = g_map_astc_to_linear_order[ise_weight_range][astc_sel];
			assert(lin_sel < num_weight_levels);

			min_lin_sel = minimumu(min_lin_sel, lin_sel);
			max_lin_sel = maximumu(max_lin_sel, lin_sel);
		}

		bool was_improved = false;
		(void)was_improved;

		{
			bool weights_changed = false;
			uint8_t trial_weights[16];
			for (uint32_t i = 0; i < num_pixels; i++)
			{
				uint32_t astc_sel = temp_astc_weights[i];
				uint32_t lin_sel = g_map_astc_to_linear_order[ise_weight_range][astc_sel];

				if ((lin_sel == min_lin_sel) && (lin_sel < (num_weight_levels - 1)))
				{
					lin_sel++;
					weights_changed = true;
				}

				trial_weights[i] = g_map_linear_to_astc_order[ise_weight_range][lin_sel];
			}

			if (weights_changed)
			{
				vec3F l_q16, h_q16;
				if (compute_least_squares_endpoints_rgb(num_pixels, trial_weights, &g_astc_ls_weights_ise[ise_weight_range][0], &l_q16, &h_q16, block_pixels_q16, color_box_q16))
				{
					if (try_mode11(num_pixels, blk_endpoints, blk_weights, cur_block_error, best_submode,
						l_q16, h_q16,
						block_pixels_half, num_weight_levels, ise_weight_range, coptions, direct_only, ise_endpoint_range, constrain_ise_weight8_selectors, 
						first_submode, last_submode))
					{
						was_improved = true;
					}
				}
			}
		}

		{
			bool weights_changed = false;
			uint8_t trial_weights[16];
			for (uint32_t i = 0; i < num_pixels; i++)
			{
				uint32_t astc_sel = temp_astc_weights[i];
				uint32_t lin_sel = g_map_astc_to_linear_order[ise_weight_range][astc_sel];

				if ((lin_sel == max_lin_sel) && (lin_sel > 0))
				{
					lin_sel--;
					weights_changed = true;
				}

				trial_weights[i] = g_map_linear_to_astc_order[ise_weight_range][lin_sel];
			}

			if (weights_changed)
			{
				vec3F l_q16, h_q16;
				if (compute_least_squares_endpoints_rgb(num_pixels, trial_weights, &g_astc_ls_weights_ise[ise_weight_range][0], &l_q16, &h_q16, block_pixels_q16, color_box_q16))
				{
					if (try_mode11(num_pixels, blk_endpoints, blk_weights, cur_block_error, best_submode,
						l_q16, h_q16,
						block_pixels_half, num_weight_levels, ise_weight_range, coptions, direct_only, ise_endpoint_range, constrain_ise_weight8_selectors,
						first_submode, last_submode))
					{
						was_improved = true;
					}
				}
			}
		}

		{
			bool weights_changed = false;
			uint8_t trial_weights[16];
			for (uint32_t i = 0; i < num_pixels; i++)
			{
				uint32_t astc_sel = temp_astc_weights[i];
				uint32_t lin_sel = g_map_astc_to_linear_order[ise_weight_range][astc_sel];

				if ((lin_sel == max_lin_sel) && (lin_sel > 0))
				{
					lin_sel--;
					weights_changed = true;
				}
				else if ((lin_sel == min_lin_sel) && (lin_sel < (num_weight_levels - 1)))
				{
					lin_sel++;
					weights_changed = true;
				}

				trial_weights[i] = g_map_linear_to_astc_order[ise_weight_range][lin_sel];
			}

			if (weights_changed)
			{
				vec3F l_q16, h_q16;
				if (compute_least_squares_endpoints_rgb(num_pixels, trial_weights, &g_astc_ls_weights_ise[ise_weight_range][0], &l_q16, &h_q16, block_pixels_q16, color_box_q16))
				{
					if (try_mode11(num_pixels, blk_endpoints, blk_weights, cur_block_error, best_submode,
						l_q16, h_q16,
						block_pixels_half, num_weight_levels, ise_weight_range, coptions, direct_only, ise_endpoint_range, constrain_ise_weight8_selectors,
						first_submode, last_submode))
					{
						was_improved = true;
					}
				}
			}
		}
	} // uber_mode

	return cur_block_error;
}

//--------------------------------------------------------------------------------------------------------------------------

static double encode_astc_hdr_block_mode_7(
	uint32_t num_pixels, const vec4F* pBlock_pixels,
	uint32_t ise_weight_range,
	uint32_t& best_submode,
	double cur_block_error,
	uint8_t* blk_endpoints,  //[4]
	uint8_t* blk_weights, // [num_pixels]
	const astc_hdr_codec_options& coptions,
	uint32_t ise_endpoint_range)
{
	assert((num_pixels >= 1) && (num_pixels <= 16));
	assert((ise_weight_range >= 1) && (ise_weight_range <= 10));
	assert((ise_endpoint_range >= astc_helpers::FIRST_VALID_ENDPOINT_ISE_RANGE) && (ise_endpoint_range <= astc_helpers::LAST_VALID_ENDPOINT_ISE_RANGE));
	const uint32_t num_weight_levels = astc_helpers::get_ise_levels(ise_weight_range);

	const uint32_t MAX_WEIGHT_LEVELS = 24;
	assert(num_weight_levels <= MAX_WEIGHT_LEVELS);
	BASISU_NOTE_UNUSED(MAX_WEIGHT_LEVELS);

	best_submode = 0;

	half_float block_pixels_half[16][3];

	vec4F block_pixels_q16[16];
	for (uint32_t i = 0; i < num_pixels; i++)
	{
		block_pixels_half[i][0] = float_to_half_non_neg_no_nan_inf(pBlock_pixels[i][0]);
		block_pixels_q16[i][0] = (float)half_to_qlog16(block_pixels_half[i][0]);

		block_pixels_half[i][1] = float_to_half_non_neg_no_nan_inf(pBlock_pixels[i][1]);
		block_pixels_q16[i][1] = (float)half_to_qlog16(block_pixels_half[i][1]);

		block_pixels_half[i][2] = float_to_half_non_neg_no_nan_inf(pBlock_pixels[i][2]);
		block_pixels_q16[i][2] = (float)half_to_qlog16(block_pixels_half[i][2]);

		block_pixels_q16[i][3] = 0.0f;
	}

	vec3F block_mean_color_q16(calc_mean(num_pixels, block_pixels_q16));

	vec3F block_axis_q16(0.577350259f);

	aabb3F color_box_q16(cInitExpand);

	float l = 1e+30f, h = -1e+30f;
	for (uint32_t i = 0; i < num_pixels; i++)
	{
		color_box_q16.expand(block_pixels_q16[i]);

		vec3F k(vec3F(block_pixels_q16[i]) - block_mean_color_q16);
		float kd = k.dot(block_axis_q16);

		l = basisu::minimum<float>(l, kd);
		h = basisu::maximum<float>(h, kd);
	}

	vec3F low_color_q16(interp_color(block_mean_color_q16, block_axis_q16, l, color_box_q16, color_box_q16));
	vec3F high_color_q16(interp_color(block_mean_color_q16, block_axis_q16, h, color_box_q16, color_box_q16));

	low_color_q16.clamp(0.0f, MAX_QLOG16_VAL);
	high_color_q16.clamp(0.0f, MAX_QLOG16_VAL);

	vec3F diff(high_color_q16 - low_color_q16);
	float s_q16 = diff.dot(block_axis_q16) * block_axis_q16[0];

	uint8_t trial_blk_endpoints[NUM_MODE7_ENDPOINTS];
	uint8_t trial_blk_weights[16];
	uint32_t trial_best_submode = 0;

	clear_obj(trial_blk_endpoints);
	clear_obj(trial_blk_weights);

	double trial_blk_error = 1e+30f;

	bool did_improve = try_mode7(num_pixels, trial_blk_endpoints, trial_blk_weights, trial_blk_error, trial_best_submode,
		high_color_q16, ceilf(s_q16),
		block_pixels_half, num_weight_levels, ise_weight_range, coptions, ise_endpoint_range);

	// If we couldn't find ANY usable solution due to endpoint quantization, just return. There's nothing we can do.
	if (!did_improve)
	{
		return cur_block_error;
	}

	// Did the solution improve?
	if (trial_blk_error < cur_block_error)
	{
		cur_block_error = trial_blk_error;
		memcpy(blk_endpoints, trial_blk_endpoints, NUM_MODE7_ENDPOINTS);
		memcpy(blk_weights, trial_blk_weights, num_pixels);
		best_submode = trial_best_submode;
	}

	const float one_over_num_pixels = 1.0f / (float)num_pixels;

	const uint32_t NUM_TRIALS = 2;
	for (uint32_t trial = 0; trial < NUM_TRIALS; trial++)
	{
		// Given a set of selectors and S, try to compute a better high color
		vec3F new_high_color_q16(block_mean_color_q16);

		int e[2][3];
		int cur_s = 0;
		if (!decode_mode7_to_qlog12(trial_blk_endpoints, e, &cur_s, ise_endpoint_range))
			break;

		cur_s <<= 4;

		for (uint32_t i = 0; i < num_pixels; i++)
		{
			uint32_t astc_sel = trial_blk_weights[i];
			float lerp = g_ise_weight_lerps[ise_weight_range][astc_sel + 1] * (1.0f / 64.0f);

			float k = (float)cur_s * (1.0f - lerp) * one_over_num_pixels;
			new_high_color_q16[0] += k;
			new_high_color_q16[1] += k;
			new_high_color_q16[2] += k;
		}

		bool improved = try_mode7(num_pixels, blk_endpoints, blk_weights, cur_block_error, best_submode,
			new_high_color_q16, (float)cur_s,
			block_pixels_half, num_weight_levels, ise_weight_range, coptions, ise_endpoint_range);

		if (improved)
		{
			memcpy(trial_blk_endpoints, blk_endpoints, NUM_MODE7_ENDPOINTS);
			memcpy(trial_blk_weights, blk_weights, num_pixels);
		}

		// Given a set of selectors and a high color, try to compute a better S.
		float t = 0.0f;

		for (uint32_t i = 0; i < num_pixels; i++)
		{
			uint32_t astc_sel = trial_blk_weights[i];
			float lerp = g_ise_weight_lerps[ise_weight_range][astc_sel + 1] * (1.0f / 64.0f);

			t += (1.0f) - lerp;
		}

		t *= one_over_num_pixels;

		//int e[2][3];
		if (!decode_mode7_to_qlog12(trial_blk_endpoints, e, nullptr, ise_endpoint_range))
			break;

		vec3F cur_h_q16((float)(e[1][0] << 4), (float)(e[1][1] << 4), (float)(e[1][2] << 4));

		if (fabs(t) > .0000125f)
		{
			float s_r = (cur_h_q16[0] - block_mean_color_q16[0]) / t;
			float s_g = (cur_h_q16[1] - block_mean_color_q16[1]) / t;
			float s_b = (cur_h_q16[2] - block_mean_color_q16[2]) / t;

			// TODO: gather statistics on these
			if (try_mode7(num_pixels, blk_endpoints, blk_weights, cur_block_error, best_submode,
				cur_h_q16, ceilf(s_r),
				block_pixels_half, num_weight_levels, ise_weight_range, coptions, ise_endpoint_range))
			{
				improved = true;
			}

			if (try_mode7(num_pixels, blk_endpoints, blk_weights, cur_block_error, best_submode,
				cur_h_q16, ceilf(s_g),
				block_pixels_half, num_weight_levels, ise_weight_range, coptions, ise_endpoint_range))
			{
				improved = true;
			}

			if (try_mode7(num_pixels, blk_endpoints, blk_weights, cur_block_error, best_submode,
				cur_h_q16, ceilf(s_b),
				block_pixels_half, num_weight_levels, ise_weight_range, coptions, ise_endpoint_range))
			{
				improved = true;
			}

			if (try_mode7(num_pixels, blk_endpoints, blk_weights, cur_block_error, best_submode,
				cur_h_q16, ceilf((s_r + s_g + s_b) / 3.0f),
				block_pixels_half, num_weight_levels, ise_weight_range, coptions, ise_endpoint_range))
			{
				improved = true;
			}
		}

		if (!improved)
			break;

		memcpy(trial_blk_endpoints, blk_endpoints, NUM_MODE7_ENDPOINTS);
		memcpy(trial_blk_weights, blk_weights, num_pixels);

	} // trial

	return cur_block_error;
}

//--------------------------------------------------------------------------------------------------------------------------

static bool pack_solid(const vec4F* pBlock_linear_colors, basisu::vector<astc_hdr_pack_results>& all_results, const astc_hdr_codec_options& coptions)
{
	float r = 0.0f, g = 0.0f, b = 0.0f;

	const float LOG_BIAS = .125f;

	bool solid_block = true;
	for (uint32_t i = 0; i < 16; i++)
	{
		if ((pBlock_linear_colors[0][0] != pBlock_linear_colors[i][0]) ||
			(pBlock_linear_colors[0][1] != pBlock_linear_colors[i][1]) ||
			(pBlock_linear_colors[0][2] != pBlock_linear_colors[i][2]))
		{
			solid_block = false;
		}

		r += log2f(pBlock_linear_colors[i][0] + LOG_BIAS);
		g += log2f(pBlock_linear_colors[i][1] + LOG_BIAS);
		b += log2f(pBlock_linear_colors[i][2] + LOG_BIAS);
	}

	if (solid_block)
	{
		r = pBlock_linear_colors[0][0];
		g = pBlock_linear_colors[0][1];
		b = pBlock_linear_colors[0][2];
	}
	else
	{
		r = maximum<float>(0.0f, powf(2.0f, r * (1.0f / 16.0f)) - LOG_BIAS);
		g = maximum<float>(0.0f, powf(2.0f, g * (1.0f / 16.0f)) - LOG_BIAS);
		b = maximum<float>(0.0f, powf(2.0f, b * (1.0f / 16.0f)) - LOG_BIAS);

		// for safety
		r = minimum<float>(r, MAX_HALF_FLOAT);
		g = minimum<float>(g, MAX_HALF_FLOAT);
		b = minimum<float>(b, MAX_HALF_FLOAT);
	}

	half_float rh = float_to_half_non_neg_no_nan_inf(r), gh = float_to_half_non_neg_no_nan_inf(g), bh = float_to_half_non_neg_no_nan_inf(b), ah = float_to_half_non_neg_no_nan_inf(1.0f);

	astc_hdr_pack_results results;
	results.clear();

	uint8_t* packed_blk = (uint8_t*)&results.m_solid_blk;
	results.m_is_solid = true;

	packed_blk[0] = 0b11111100;
	packed_blk[1] = 255;
	packed_blk[2] = 255;
	packed_blk[3] = 255;
	packed_blk[4] = 255;
	packed_blk[5] = 255;
	packed_blk[6] = 255;
	packed_blk[7] = 255;

	packed_blk[8] = (uint8_t)rh;
	packed_blk[9] = (uint8_t)(rh >> 8);
	packed_blk[10] = (uint8_t)gh;
	packed_blk[11] = (uint8_t)(gh >> 8);
	packed_blk[12] = (uint8_t)bh;
	packed_blk[13] = (uint8_t)(bh >> 8);
	packed_blk[14] = (uint8_t)ah;
	packed_blk[15] = (uint8_t)(ah >> 8);

	results.m_best_block_error = 0;

	if (!solid_block)
	{
		const float R_WEIGHT = coptions.m_r_err_scale;
		const float G_WEIGHT = coptions.m_g_err_scale;

		// This MUST match how errors are computed in eval_selectors().
		for (uint32_t i = 0; i < 16; i++)
		{
			half_float dr = float_to_half_non_neg_no_nan_inf(pBlock_linear_colors[i][0]), dg = float_to_half_non_neg_no_nan_inf(pBlock_linear_colors[i][1]), db = float_to_half_non_neg_no_nan_inf(pBlock_linear_colors[i][2]);
			double rd = q(rh) - q(dr);
			double gd = q(gh) - q(dg);
			double bd = q(bh) - q(db);

			double e = R_WEIGHT * (rd * rd) + G_WEIGHT * (gd * gd) + bd * bd;

			results.m_best_block_error += e;
		}
	}

	const half_float hc[3] = { rh, gh, bh };

	bc6h_enc_block_solid_color(&results.m_bc6h_block, hc);

	all_results.push_back(results);

	return solid_block;
}

//--------------------------------------------------------------------------------------------------------------------------

static void pack_mode11(
	const vec4F* pBlock_linear_colors, 
	basisu::vector<astc_hdr_pack_results>& all_results, 
	const astc_hdr_codec_options& coptions, 
	uint32_t first_weight_ise_range, uint32_t last_weight_ise_range, bool constrain_ise_weight8_selectors)
{
	uint8_t trial_endpoints[NUM_MODE11_ENDPOINTS], trial_weights[16];
	uint32_t trial_submode11 = 0;

	clear_obj(trial_endpoints);
	clear_obj(trial_weights);
		
	for (uint32_t weight_ise_range = first_weight_ise_range; weight_ise_range <= last_weight_ise_range; weight_ise_range++)
	{
		const bool direct_only = coptions.m_mode11_direct_only;
		
		uint32_t endpoint_ise_range = astc_helpers::BISE_256_LEVELS;
		if (weight_ise_range == astc_helpers::BISE_16_LEVELS)
			endpoint_ise_range = astc_helpers::BISE_192_LEVELS;
		else
		{
			assert(weight_ise_range < astc_helpers::BISE_16_LEVELS);
		}
				
		double trial_error = encode_astc_hdr_block_mode_11(16, pBlock_linear_colors, weight_ise_range, trial_submode11, 1e+30f, trial_endpoints, trial_weights, coptions, direct_only, 
			endpoint_ise_range, coptions.m_mode11_uber_mode && (weight_ise_range >= astc_helpers::BISE_4_LEVELS) && coptions.m_allow_uber_mode, constrain_ise_weight8_selectors, coptions.m_first_mode11_submode, coptions.m_last_mode11_submode);

		if (trial_error < 1e+30f)
		{
			astc_hdr_pack_results results;
			results.clear();

			results.m_best_block_error = trial_error;

			results.m_best_submodes[0] = trial_submode11;
			results.m_constrained_weights = constrain_ise_weight8_selectors;
						
			results.m_best_blk.m_num_partitions = 1;
			results.m_best_blk.m_color_endpoint_modes[0] = 11;
			results.m_best_blk.m_weight_ise_range = weight_ise_range;
			results.m_best_blk.m_endpoint_ise_range = endpoint_ise_range;
			
			memcpy(results.m_best_blk.m_endpoints, trial_endpoints, NUM_MODE11_ENDPOINTS);
			memcpy(results.m_best_blk.m_weights, trial_weights, 16);

#ifdef _DEBUG
			{
				half_float block_pixels_half[16][3];

				vec4F block_pixels_q16[16];
				for (uint32_t i = 0; i < 16; i++)
				{
					block_pixels_half[i][0] = float_to_half_non_neg_no_nan_inf(pBlock_linear_colors[i][0]);
					block_pixels_half[i][1] = float_to_half_non_neg_no_nan_inf(pBlock_linear_colors[i][1]);
					block_pixels_half[i][2] = float_to_half_non_neg_no_nan_inf(pBlock_linear_colors[i][2]);
				}
				
				half_float unpacked_astc_blk_rgba[4][4][4];
				bool res = astc_helpers::decode_block(results.m_best_blk, unpacked_astc_blk_rgba, 4, 4, astc_helpers::cDecodeModeHDR16);
				assert(res);

				half_float unpacked_astc_blk_rgb[4][4][3];
				for (uint32_t y = 0; y < 4; y++)
					for (uint32_t x = 0; x < 4; x++)
						for (uint32_t c = 0; c < 3; c++)
							unpacked_astc_blk_rgb[y][x][c] = unpacked_astc_blk_rgba[y][x][c];

				double cmp_err = compute_block_error(&block_pixels_half[0][0], &unpacked_astc_blk_rgb[0][0][0], coptions);
				assert(results.m_best_block_error == cmp_err);
			}
#endif

			// transcode to BC6H
			assert(results.m_best_blk.m_color_endpoint_modes[0] == 11);
			
			// Get qlog12 endpoints
			int e[2][3];
			bool success = decode_mode11_to_qlog12(results.m_best_blk.m_endpoints, e, results.m_best_blk.m_endpoint_ise_range);
			assert(success);
			BASISU_NOTE_UNUSED(success);

			// Transform endpoints to half float
			half_float h_e[3][2] =
			{
				{ qlog_to_half(e[0][0], 12), qlog_to_half(e[1][0], 12) },
				{ qlog_to_half(e[0][1], 12), qlog_to_half(e[1][1], 12) },
				{ qlog_to_half(e[0][2], 12), qlog_to_half(e[1][2], 12) }
			};

			// Transcode to bc6h
			success = transcode_bc6h_1subset(h_e, results.m_best_blk, results.m_bc6h_block);
			assert(success);

			all_results.push_back(results);
		}
	}
}

//--------------------------------------------------------------------------------------------------------------------------

static void pack_mode7_single_part(const vec4F* pBlock_linear_colors, basisu::vector<astc_hdr_pack_results>& all_results, const astc_hdr_codec_options& coptions)
{
	uint8_t trial_endpoints[NUM_MODE7_ENDPOINTS], trial_weights[16];
	uint32_t trial_submode7 = 0;

	clear_obj(trial_endpoints);
	clear_obj(trial_weights);

	for (uint32_t weight_ise_range = coptions.m_first_mode7_part1_weight_ise_range; weight_ise_range <= coptions.m_last_mode7_part1_weight_ise_range; weight_ise_range++)
	{
		const uint32_t ise_endpoint_range = astc_helpers::BISE_256_LEVELS;

		double trial_error = encode_astc_hdr_block_mode_7(16, pBlock_linear_colors, weight_ise_range, trial_submode7, 1e+30f, trial_endpoints, trial_weights, coptions, ise_endpoint_range);

		if (trial_error < 1e+30f)
		{
			astc_hdr_pack_results results;
			results.clear();

			results.m_best_block_error = trial_error;

			results.m_best_submodes[0] = trial_submode7;
			
			results.m_best_blk.m_num_partitions = 1;
			results.m_best_blk.m_color_endpoint_modes[0] = 7;
			results.m_best_blk.m_weight_ise_range = weight_ise_range;
			results.m_best_blk.m_endpoint_ise_range = ise_endpoint_range;
			
			memcpy(results.m_best_blk.m_endpoints, trial_endpoints, NUM_MODE7_ENDPOINTS);
			memcpy(results.m_best_blk.m_weights, trial_weights, 16);

			// transcode to BC6H
			assert(results.m_best_blk.m_color_endpoint_modes[0] == 7);
			
			// Get qlog12 endpoints
			int e[2][3];
			if (!decode_mode7_to_qlog12(results.m_best_blk.m_endpoints, e, nullptr, results.m_best_blk.m_endpoint_ise_range))
				continue;

			// Transform endpoints to half float
			half_float h_e[3][2] =
			{
				{ qlog_to_half(e[0][0], 12), qlog_to_half(e[1][0], 12) },
				{ qlog_to_half(e[0][1], 12), qlog_to_half(e[1][1], 12) },
				{ qlog_to_half(e[0][2], 12), qlog_to_half(e[1][2], 12) }
			};

			// Transcode to bc6h
			bool status = transcode_bc6h_1subset(h_e, results.m_best_blk, results.m_bc6h_block);
			assert(status);
			(void)status;

			all_results.push_back(results);
		}
	}
}

//--------------------------------------------------------------------------------------------------------------------------

static bool estimate_partition2(const vec4F* pBlock_pixels, int* pBest_parts, uint32_t num_best_parts)
{
	assert(num_best_parts <= basist::TOTAL_ASTC_BC6H_COMMON_PARTITIONS2);

	vec3F training_vecs[16], mean(0.0f);

	for (uint32_t i = 0; i < 16; i++)
	{
		vec3F& v = training_vecs[i];

		v[0] = (float)float_to_half_non_neg_no_nan_inf(pBlock_pixels[i][0]);
		v[1] = (float)float_to_half_non_neg_no_nan_inf(pBlock_pixels[i][1]);
		v[2] = (float)float_to_half_non_neg_no_nan_inf(pBlock_pixels[i][2]);

		mean += v;
	}
	mean *= (1.0f / 16.0f);

	vec3F cluster_centroids[2] = { mean - vec3F(.1f), mean + vec3F(.1f) };

	uint32_t cluster_pixels[2][16];
	uint32_t num_cluster_pixels[2];
	vec3F new_cluster_means[2];

	for (uint32_t s = 0; s < 4; s++)
	{
		num_cluster_pixels[0] = 0;
		num_cluster_pixels[1] = 0;

		new_cluster_means[0].clear();
		new_cluster_means[1].clear();

		for (uint32_t i = 0; i < 16; i++)
		{
			float d0 = training_vecs[i].squared_distance(cluster_centroids[0]);
			float d1 = training_vecs[i].squared_distance(cluster_centroids[1]);

			if (d0 < d1)
			{
				cluster_pixels[0][num_cluster_pixels[0]] = i;
				new_cluster_means[0] += training_vecs[i];
				num_cluster_pixels[0]++;
			}
			else
			{
				cluster_pixels[1][num_cluster_pixels[1]] = i;
				new_cluster_means[1] += training_vecs[i];
				num_cluster_pixels[1]++;
			}
		}

		if (!num_cluster_pixels[0] || !num_cluster_pixels[1])
			return false;

		cluster_centroids[0] = new_cluster_means[0] / (float)num_cluster_pixels[0];
		cluster_centroids[1] = new_cluster_means[1] / (float)num_cluster_pixels[1];
	}

	int desired_parts[4][4]; // [y][x]
	for (uint32_t p = 0; p < 2; p++)
	{
		for (uint32_t i = 0; i < num_cluster_pixels[p]; i++)
		{
			const uint32_t pix_index = cluster_pixels[p][i];

			desired_parts[pix_index >> 2][pix_index & 3] = p;
		}
	}

	uint32_t part_similarity[basist::TOTAL_ASTC_BC6H_COMMON_PARTITIONS2];

	for (uint32_t part_index = 0; part_index < basist::TOTAL_ASTC_BC6H_COMMON_PARTITIONS2; part_index++)
	{
		const uint32_t bc7_pattern = basist::g_astc_bc7_common_partitions2[part_index].m_bc7;

		int total_sim_non_inv = 0;
		int total_sim_inv = 0;

		for (uint32_t y = 0; y < 4; y++)
		{
			for (uint32_t x = 0; x < 4; x++)
			{
				int part = basist::g_bc7_partition2[16 * bc7_pattern + x + y * 4];

				if (part == desired_parts[y][x])
					total_sim_non_inv++;

				if ((part ^ 1) == desired_parts[y][x])
					total_sim_inv++;
			}
		}

		int total_sim = maximum(total_sim_non_inv, total_sim_inv);

		part_similarity[part_index] = (total_sim << 8) | part_index;

	} // part_index;

	std::sort(part_similarity, part_similarity + basist::TOTAL_ASTC_BC6H_COMMON_PARTITIONS2);

	for (uint32_t i = 0; i < num_best_parts; i++)
		pBest_parts[i] = part_similarity[(basist::TOTAL_ASTC_BC6H_COMMON_PARTITIONS2 - 1) - i] & 0xFF;

	return true;
}

//--------------------------------------------------------------------------------------------------------------------------

static void pack_mode7_2part(const vec4F* pBlock_linear_colors, basisu::vector<astc_hdr_pack_results>& all_results, const astc_hdr_codec_options& coptions,
	int num_estimated_partitions, const int *pEstimated_partitions,
	uint32_t first_weight_ise_range, uint32_t last_weight_ise_range)
{
	assert(coptions.m_mode7_part2_part_masks);

	astc_helpers::log_astc_block trial_blk;
	clear_obj(trial_blk);
	trial_blk.m_grid_width = 4;
	trial_blk.m_grid_height = 4;

	trial_blk.m_num_partitions = 2;
	trial_blk.m_color_endpoint_modes[0] = 7;
	trial_blk.m_color_endpoint_modes[1] = 7;

	uint32_t first_part_index = 0, last_part_index = basist::TOTAL_ASTC_BC6H_COMMON_PARTITIONS2;
		
	if (num_estimated_partitions)
	{
		first_part_index = 0;
		last_part_index = num_estimated_partitions;
	}
	
	for (uint32_t part_index_iter = first_part_index; part_index_iter < last_part_index; ++part_index_iter)
	{
		uint32_t part_index;
		if (num_estimated_partitions)
		{
			part_index = pEstimated_partitions[part_index_iter];
			assert(part_index < basist::TOTAL_ASTC_BC6H_COMMON_PARTITIONS2);
		}
		else
		{
			part_index = part_index_iter;
			if (((1U << part_index) & coptions.m_mode7_part2_part_masks) == 0)
				continue;
		}
								
		const uint32_t astc_pattern = basist::g_astc_bc7_common_partitions2[part_index].m_astc;
		const uint32_t bc7_pattern = basist::g_astc_bc7_common_partitions2[part_index].m_bc7;
		const bool invert_flag = basist::g_astc_bc7_common_partitions2[part_index].m_invert;

		vec4F part_pixels[2][16];
		uint32_t pixel_part_index[4][4]; // [y][x]
		uint32_t num_part_pixels[2] = { 0, 0 };

		// Extract each subset's texels for this partition pattern
		for (uint32_t y = 0; y < 4; y++)
		{
			for (uint32_t x = 0; x < 4; x++)
			{
				uint32_t part = basist::g_bc7_partition2[16 * bc7_pattern + x + y * 4];
				if (invert_flag)
					part = 1 - part;

				pixel_part_index[y][x] = part;
				part_pixels[part][num_part_pixels[part]] = pBlock_linear_colors[x + y * 4];

				num_part_pixels[part]++;
			}
		}

		trial_blk.m_partition_id = astc_pattern;
				
		for (uint32_t weight_ise_range = first_weight_ise_range; weight_ise_range <= last_weight_ise_range; weight_ise_range++)
		{
			assert(weight_ise_range <= astc_helpers::BISE_8_LEVELS);

			uint32_t ise_endpoint_range = astc_helpers::BISE_256_LEVELS;
			if (weight_ise_range == astc_helpers::BISE_5_LEVELS)
				ise_endpoint_range = astc_helpers::BISE_192_LEVELS;
			else if (weight_ise_range == astc_helpers::BISE_6_LEVELS)
				ise_endpoint_range = astc_helpers::BISE_128_LEVELS;
			else if (weight_ise_range == astc_helpers::BISE_8_LEVELS)
				ise_endpoint_range = astc_helpers::BISE_80_LEVELS;

			uint8_t trial_endpoints[2][NUM_MODE7_ENDPOINTS], trial_weights[2][16];
			uint32_t trial_submode7[2];

			clear_obj(trial_endpoints);
			clear_obj(trial_weights);
			clear_obj(trial_submode7);

			double total_trial_err = 0;
			for (uint32_t pack_part_index = 0; pack_part_index < 2; pack_part_index++)
			{
				total_trial_err += encode_astc_hdr_block_mode_7(
					num_part_pixels[pack_part_index], &part_pixels[pack_part_index][0],
					weight_ise_range, trial_submode7[pack_part_index], 1e+30f,
					&trial_endpoints[pack_part_index][0], &trial_weights[pack_part_index][0], coptions, ise_endpoint_range);

			} // pack_part_index

			if (total_trial_err < 1e+30f)
			{
				trial_blk.m_weight_ise_range = weight_ise_range;
				trial_blk.m_endpoint_ise_range = ise_endpoint_range;

				for (uint32_t pack_part_index = 0; pack_part_index < 2; pack_part_index++)
					memcpy(&trial_blk.m_endpoints[pack_part_index * NUM_MODE7_ENDPOINTS], &trial_endpoints[pack_part_index][0], NUM_MODE7_ENDPOINTS);

				uint32_t src_pixel_index[2] = { 0, 0 };
				for (uint32_t y = 0; y < 4; y++)
				{
					for (uint32_t x = 0; x < 4; x++)
					{
						uint32_t p = pixel_part_index[y][x];
						trial_blk.m_weights[x + y * 4] = trial_weights[p][src_pixel_index[p]++];
					}
				}
								
				astc_hdr_pack_results results;
				results.clear();

				results.m_best_block_error = total_trial_err;
				results.m_best_submodes[0] = trial_submode7[0];
				results.m_best_submodes[1] = trial_submode7[1];
				results.m_best_pat_index = part_index;

				results.m_best_blk = trial_blk;

				bool status = transcode_bc6h_2subsets(part_index, results.m_best_blk, results.m_bc6h_block);
				assert(status);
				BASISU_NOTE_UNUSED(status);

				all_results.push_back(results);
			}

		} // weight_ise_range

	} // part_index
}

//--------------------------------------------------------------------------------------------------------------------------

static void pack_mode11_2part(const vec4F* pBlock_linear_colors, basisu::vector<astc_hdr_pack_results>& all_results, const astc_hdr_codec_options& coptions,
	int num_estimated_partitions, const int* pEstimated_partitions)
{
	assert(coptions.m_mode11_part2_part_masks);

	astc_helpers::log_astc_block trial_blk;
	clear_obj(trial_blk);
	trial_blk.m_grid_width = 4;
	trial_blk.m_grid_height = 4;

	trial_blk.m_num_partitions = 2;
	trial_blk.m_color_endpoint_modes[0] = 11;
	trial_blk.m_color_endpoint_modes[1] = 11;
			
	uint32_t first_part_index = 0, last_part_index = basist::TOTAL_ASTC_BC6H_COMMON_PARTITIONS2;

	if (num_estimated_partitions)
	{
		first_part_index = 0;
		last_part_index = num_estimated_partitions;
	}

	for (uint32_t part_index_iter = first_part_index; part_index_iter < last_part_index; ++part_index_iter)
	{
		uint32_t part_index;
		if (num_estimated_partitions)
		{
			part_index = pEstimated_partitions[part_index_iter];
			assert(part_index < basist::TOTAL_ASTC_BC6H_COMMON_PARTITIONS2);
		}
		else
		{
			part_index = part_index_iter;
			if (((1U << part_index) & coptions.m_mode11_part2_part_masks) == 0)
				continue;
		}

		const uint32_t astc_pattern = basist::g_astc_bc7_common_partitions2[part_index].m_astc;
		const uint32_t bc7_pattern = basist::g_astc_bc7_common_partitions2[part_index].m_bc7;
		const bool invert_flag = basist::g_astc_bc7_common_partitions2[part_index].m_invert;

		vec4F part_pixels[2][16];
		uint32_t pixel_part_index[4][4]; // [y][x]
		uint32_t num_part_pixels[2] = { 0, 0 };

		// Extract each subset's texels for this partition pattern
		for (uint32_t y = 0; y < 4; y++)
		{
			for (uint32_t x = 0; x < 4; x++)
			{
				uint32_t part = basist::g_bc7_partition2[16 * bc7_pattern + x + y * 4];
				if (invert_flag)
					part = 1 - part;

				pixel_part_index[y][x] = part;
				part_pixels[part][num_part_pixels[part]] = pBlock_linear_colors[x + y * 4];

				num_part_pixels[part]++;
			}
		}
				
		trial_blk.m_partition_id = astc_pattern;
						
		for (uint32_t weight_ise_range = coptions.m_first_mode11_part2_weight_ise_range; weight_ise_range <= coptions.m_last_mode11_part2_weight_ise_range; weight_ise_range++)
		{
			bool direct_only = false;
			uint32_t ise_endpoint_range = astc_helpers::BISE_64_LEVELS;
			if (weight_ise_range == astc_helpers::BISE_4_LEVELS)
				ise_endpoint_range = astc_helpers::BISE_40_LEVELS;

			uint8_t trial_endpoints[2][NUM_MODE11_ENDPOINTS], trial_weights[2][16];
			uint32_t trial_submode11[2];

			clear_obj(trial_endpoints); 
			clear_obj(trial_weights);
			clear_obj(trial_submode11);

			double total_trial_err = 0;
			for (uint32_t pack_part_index = 0; pack_part_index < 2; pack_part_index++)
			{
				total_trial_err += encode_astc_hdr_block_mode_11(
					num_part_pixels[pack_part_index], &part_pixels[pack_part_index][0],
					weight_ise_range, trial_submode11[pack_part_index], 1e+30f,
					&trial_endpoints[pack_part_index][0], &trial_weights[pack_part_index][0], coptions,
					direct_only, ise_endpoint_range, coptions.m_mode11_uber_mode && (weight_ise_range >= astc_helpers::BISE_4_LEVELS) && coptions.m_allow_uber_mode, false,
					coptions.m_first_mode11_submode, coptions.m_last_mode11_submode);

			} // pack_part_index

			if (total_trial_err < 1e+30f)
			{
				trial_blk.m_weight_ise_range = weight_ise_range;
				trial_blk.m_endpoint_ise_range = ise_endpoint_range;

				for (uint32_t pack_part_index = 0; pack_part_index < 2; pack_part_index++)
					memcpy(&trial_blk.m_endpoints[pack_part_index * NUM_MODE11_ENDPOINTS], &trial_endpoints[pack_part_index][0], NUM_MODE11_ENDPOINTS);

				uint32_t src_pixel_index[2] = { 0, 0 };
				for (uint32_t y = 0; y < 4; y++)
				{
					for (uint32_t x = 0; x < 4; x++)
					{
						uint32_t p = pixel_part_index[y][x];
						trial_blk.m_weights[x + y * 4] = trial_weights[p][src_pixel_index[p]++];
					}
				}
								
				astc_hdr_pack_results results;
				results.clear();

				results.m_best_block_error = total_trial_err;
				results.m_best_submodes[0] = trial_submode11[0];
				results.m_best_submodes[1] = trial_submode11[1];
				results.m_best_pat_index = part_index;

				results.m_best_blk = trial_blk;

				bool status = transcode_bc6h_2subsets(part_index, results.m_best_blk, results.m_bc6h_block);
				assert(status);
				BASISU_NOTE_UNUSED(status);

				all_results.push_back(results);
			}

		} // weight_ise_range

	} // part_index
}

//--------------------------------------------------------------------------------------------------------------------------

bool g_astc_hdr_enc_initialized;

void astc_hdr_enc_init()
{
	if (g_astc_hdr_enc_initialized)
		return;

	astc_hdr_core_init();

	astc_helpers::init_tables(true);
			
	init_qlog_tables();

	encode_astc_hdr_init();
								
	g_astc_hdr_enc_initialized = true;
}

bool astc_hdr_enc_block(
	const float* pRGBPixels, 
	const astc_hdr_codec_options& coptions,
	basisu::vector<astc_hdr_pack_results>& all_results)
{
	assert(g_astc_hdr_enc_initialized);
	if (!g_astc_hdr_enc_initialized)
	{
		// astc_hdr_enc_init() MUST be called first.
		assert(0);
		return false;
	}

	all_results.resize(0);
				
	vec4F block_linear_colors[16];

	// Sanity check the input block.
	for (uint32_t i = 0; i < 16; i++)
	{
		for (uint32_t j = 0; j < 3; j++)
		{
			float v = pRGBPixels[i * 3 + j];

			if (std::isinf(v) || std::isnan(v))
			{
				// Input pixels cannot be NaN or +-Inf.
				assert(0);
				return false;
			}

			if (v < 0.0f)
			{
				// Input pixels cannot be signed.
				assert(0);
				return false;
			}

			if (v > MAX_HALF_FLOAT)
			{
				// Too large for half float.
				assert(0);
				return false;
			}
			
			block_linear_colors[i][j] = v;
		}
		
		block_linear_colors[i][3] = 1.0f;
	}

	assert(coptions.m_use_solid || coptions.m_use_mode11 || coptions.m_use_mode7_part2 || coptions.m_use_mode7_part1 || coptions.m_use_mode11_part2);
					
	bool is_solid = false;
	if (coptions.m_use_solid)
		is_solid = pack_solid(block_linear_colors, all_results, coptions);

	if (!is_solid)
	{
		if (coptions.m_use_mode11)
		{
			const size_t cur_num_results = all_results.size();

			pack_mode11(block_linear_colors, all_results, coptions, coptions.m_first_mode11_weight_ise_range, coptions.m_last_mode11_weight_ise_range, false);

			if (coptions.m_last_mode11_weight_ise_range == astc_helpers::BISE_16_LEVELS)
			{
				pack_mode11(block_linear_colors, all_results, coptions, astc_helpers::BISE_16_LEVELS, astc_helpers::BISE_16_LEVELS, true);
			}

			// If we couldn't get any mode 11 results at all, and we were restricted to just trying weight ISE range 8 (which required endpoint quantization) then 
			// fall back to weight ISE range 7 (which doesn't need any endpoint quantization).
			// This is to guarantee we always get at least 1 non-solid result.
			if (all_results.size() == cur_num_results)
			{
				if (coptions.m_first_mode11_weight_ise_range == astc_helpers::BISE_16_LEVELS)
				{
					pack_mode11(block_linear_colors, all_results, coptions, astc_helpers::BISE_12_LEVELS, astc_helpers::BISE_12_LEVELS, false);
				}
			}
		}
				
		if (coptions.m_use_mode7_part1)
		{
			// Mode 7 1-subset never requires endpoint quantization, so it cannot fail to find at least one usable solution.
			pack_mode7_single_part(block_linear_colors, all_results, coptions);
		}
				
		bool have_est = false;
		int best_parts[basist::TOTAL_ASTC_BC6H_COMMON_PARTITIONS2];

		if ((coptions.m_use_mode7_part2) || (coptions.m_use_mode11_part2))
		{
			if (coptions.m_use_estimated_partitions)
				have_est = estimate_partition2(block_linear_colors, best_parts, coptions.m_max_estimated_partitions);
		}

		if (coptions.m_use_mode7_part2)
		{
			const size_t cur_num_results = all_results.size();

			pack_mode7_2part(block_linear_colors, all_results, coptions, have_est ? coptions.m_max_estimated_partitions : 0, best_parts, 
				coptions.m_first_mode7_part2_weight_ise_range, coptions.m_last_mode7_part2_weight_ise_range);

			// If we couldn't find any packable 2-subset mode 7 results at weight levels >= 5 levels (which always requires endpoint quant), then try falling back to 
			// 5 levels which doesn't require endpoint quantization.
			if (all_results.size() == cur_num_results)
			{
				if (coptions.m_first_mode7_part2_weight_ise_range >= astc_helpers::BISE_5_LEVELS)
				{
					pack_mode7_2part(block_linear_colors, all_results, coptions, have_est ? coptions.m_max_estimated_partitions : 0, best_parts, 
						astc_helpers::BISE_4_LEVELS, astc_helpers::BISE_4_LEVELS);
				}
			}
		}
		
		if (coptions.m_use_mode11_part2)
		{
			// This always requires endpoint quant, so it could fail to find any usable solutions.
			pack_mode11_2part(block_linear_colors, all_results, coptions, have_est ? coptions.m_max_estimated_partitions : 0, best_parts);
		}
	}

	if (coptions.m_refine_weights)
	{
		// TODO: Move this above, do it once only.
		basist::half_float rgb_pixels_half[16 * 3];
		for (uint32_t i = 0; i < 16; i++)
		{
			rgb_pixels_half[i * 3 + 0] = float_to_half_non_neg_no_nan_inf(pRGBPixels[i * 3 + 0]);
			rgb_pixels_half[i * 3 + 1] = float_to_half_non_neg_no_nan_inf(pRGBPixels[i * 3 + 1]);
			rgb_pixels_half[i * 3 + 2] = float_to_half_non_neg_no_nan_inf(pRGBPixels[i * 3 + 2]);
		}

		for (uint32_t i = 0; i < all_results.size(); i++)
		{
			bool status = astc_hdr_refine_weights(rgb_pixels_half, all_results[i], coptions, coptions.m_bc6h_err_weight, &all_results[i].m_improved_via_refinement_flag);
			assert(status);
			BASISU_NOTE_UNUSED(status);
		}
	}

	return true;
}

bool astc_hdr_pack_results_to_block(astc_blk& dst_blk, const astc_hdr_pack_results& results)
{
	assert(g_astc_hdr_enc_initialized);
	if (!g_astc_hdr_enc_initialized)
		return false;

	if (results.m_is_solid)
	{
		memcpy(&dst_blk, &results.m_solid_blk, sizeof(results.m_solid_blk));
	}
	else
	{
		bool status = astc_helpers::pack_astc_block((astc_helpers::astc_block&)dst_blk, results.m_best_blk);
		if (!status)
		{
			assert(0);
			return false;
		}
	}

	return true;
}

// Refines a block's chosen weight indices, balancing BC6H and ASTC HDR error.
bool astc_hdr_refine_weights(const half_float *pSource_block, astc_hdr_pack_results& cur_results, const astc_hdr_codec_options& coptions, float bc6h_weight, bool *pImproved_flag)
{
	if (pImproved_flag)
		*pImproved_flag = false;

	if (cur_results.m_is_solid)
		return true;

	const uint32_t total_weights = astc_helpers::get_ise_levels(cur_results.m_best_blk.m_weight_ise_range);

	assert((total_weights >= 3) && (total_weights <= 16));

	double best_err[4][4];
	uint8_t best_weight[4][4];
	for (uint32_t y = 0; y < 4; y++)
	{
		for (uint32_t x = 0; x < 4; x++)
		{
			best_err[y][x] = 1e+30f;
			best_weight[y][x] = 0;
		}
	}

	astc_hdr_pack_results temp_results;

	const float c_weights[3] = { coptions.m_r_err_scale, coptions.m_g_err_scale, 1.0f };

	for (uint32_t weight_index = 0; weight_index < total_weights; weight_index++)
	{
		temp_results = cur_results;
		for (uint32_t i = 0; i < 16; i++)
			temp_results.m_best_blk.m_weights[i] = (uint8_t)weight_index;
		
		half_float unpacked_astc_blk_rgba[4][4][4];
		bool res = astc_helpers::decode_block(temp_results.m_best_blk, unpacked_astc_blk_rgba, 4, 4, astc_helpers::cDecodeModeHDR16);
		assert(res);

		basist::bc6h_block trial_bc6h_blk;
		res = basist::astc_hdr_transcode_to_bc6h(temp_results.m_best_blk, trial_bc6h_blk);
		assert(res);
				
		half_float unpacked_bc6h_blk[4][4][3];
		res = unpack_bc6h(&trial_bc6h_blk, unpacked_bc6h_blk, false);
		assert(res);
		BASISU_NOTE_UNUSED(res);

		for (uint32_t y = 0; y < 4; y++)
		{
			for (uint32_t x = 0; x < 4; x++)
			{
				double total_err = 0.0f;

				for (uint32_t c = 0; c < 3; c++)
				{
					const half_float orig_c = pSource_block[(x + y * 4) * 3 + c];
					const double orig_c_q = q(orig_c);
					
					const half_float astc_c = unpacked_astc_blk_rgba[y][x][c];
					const double astc_c_q = q(astc_c);
					const double astc_e = square(astc_c_q - orig_c_q) * c_weights[c];
					
					const half_float bc6h_c = unpacked_bc6h_blk[y][x][c];
					const double bc6h_c_q = q(bc6h_c);
					const double bc6h_e = square(bc6h_c_q - orig_c_q) * c_weights[c];

					const double overall_err = astc_e * (1.0f - bc6h_weight) + bc6h_e * bc6h_weight;

					total_err += overall_err;

				} //  c

				if (total_err < best_err[y][x])
				{
					best_err[y][x] = total_err;
					best_weight[y][x] = (uint8_t)weight_index;
				}

			} // x
		} // y

	} // weight_index

	bool any_changed = false;
	for (uint32_t i = 0; i < 16; i++)
	{
		if (cur_results.m_best_blk.m_weights[i] != best_weight[i >> 2][i & 3])
		{
			any_changed = true;
			break;
		}
	}

	if (any_changed)
	{
		memcpy(cur_results.m_best_blk.m_weights, best_weight, 16);

		{
			bool res = basist::astc_hdr_transcode_to_bc6h(cur_results.m_best_blk, cur_results.m_bc6h_block);
			assert(res);
			BASISU_NOTE_UNUSED(res);

			half_float unpacked_astc_blk_rgba[4][4][4];
			res = astc_helpers::decode_block(cur_results.m_best_blk, unpacked_astc_blk_rgba, 4, 4, astc_helpers::cDecodeModeHDR16);
			assert(res);

			half_float unpacked_astc_blk_rgb[4][4][3];
			for (uint32_t y = 0; y < 4; y++)
				for (uint32_t x = 0; x < 4; x++)
					for (uint32_t c = 0; c < 3; c++)
						unpacked_astc_blk_rgb[y][x][c] = unpacked_astc_blk_rgba[y][x][c];

			cur_results.m_best_block_error = compute_block_error(pSource_block, &unpacked_astc_blk_rgb[0][0][0], coptions);
		}

		if (pImproved_flag)
			*pImproved_flag = true;
	}

	return true;
}

void astc_hdr_block_stats::update(const astc_hdr_pack_results& log_blk)
{
	std::lock_guard<std::mutex> lck(m_mutex);

	m_total_blocks++;

	if (log_blk.m_improved_via_refinement_flag)
		m_total_refined++;

	if (log_blk.m_is_solid)
	{
		m_total_solid++;
	}
	else
	{
		int best_weight_range = log_blk.m_best_blk.m_weight_ise_range;

		if (log_blk.m_best_blk.m_color_endpoint_modes[0] == 7)
		{
			m_mode7_submode_hist[bounds_check(log_blk.m_best_submodes[0], 0U, 6U)]++;

			if (log_blk.m_best_blk.m_num_partitions == 2)
			{
				m_total_mode7_2part++;

				m_mode7_submode_hist[bounds_check(log_blk.m_best_submodes[1], 0U, 6U)]++;
				m_total_2part++;

				m_weight_range_hist_7_2part[bounds_check(best_weight_range, 0, 11)]++;

				m_part_hist[bounds_check(log_blk.m_best_pat_index, 0U, 32U)]++;
			}
			else
			{
				m_total_mode7_1part++;

				m_weight_range_hist_7[bounds_check(best_weight_range, 0, 11)]++;
			}
		}
		else
		{
			m_mode11_submode_hist[bounds_check(log_blk.m_best_submodes[0], 0U, 9U)]++;
			if (log_blk.m_constrained_weights)
				m_total_mode11_1part_constrained_weights++;

			if (log_blk.m_best_blk.m_num_partitions == 2)
			{
				m_total_mode11_2part++;

				m_mode11_submode_hist[bounds_check(log_blk.m_best_submodes[1], 0U, 9U)]++;
				m_total_2part++;

				m_weight_range_hist_11_2part[bounds_check(best_weight_range, 0, 11)]++;

				m_part_hist[bounds_check(log_blk.m_best_pat_index, 0U, 32U)]++;
			}
			else
			{
				m_total_mode11_1part++;

				m_weight_range_hist_11[bounds_check(best_weight_range, 0, 11)]++;
			}
		}
	}
}

void astc_hdr_block_stats::print()
{
	std::lock_guard<std::mutex> lck(m_mutex);

	assert(m_total_blocks);
	if (!m_total_blocks)
		return;

	printf("\nLow-level ASTC Encoder Statistics:\n");
	printf("Total blocks: %u\n", m_total_blocks);
	printf("Total solid: %u %3.2f%%\n", m_total_solid, (m_total_solid * 100.0f) / m_total_blocks);
	printf("Total refined: %u %3.2f%%\n", m_total_refined, (m_total_refined * 100.0f) / m_total_blocks);

	printf("Total mode 11, 1 partition: %u %3.2f%%\n", m_total_mode11_1part, (m_total_mode11_1part * 100.0f) / m_total_blocks);
	printf("Total mode 11, 1 partition, constrained weights: %u %3.2f%%\n", m_total_mode11_1part_constrained_weights, (m_total_mode11_1part_constrained_weights * 100.0f) / m_total_blocks);
	printf("Total mode 11, 2 partition: %u %3.2f%%\n", m_total_mode11_2part, (m_total_mode11_2part * 100.0f) / m_total_blocks);

	printf("Total mode 7, 1 partition: %u %3.2f%%\n", m_total_mode7_1part, (m_total_mode7_1part * 100.0f) / m_total_blocks);
	printf("Total mode 7, 2 partition: %u %3.2f%%\n", m_total_mode7_2part, (m_total_mode7_2part * 100.0f) / m_total_blocks);

	printf("Total 2 partitions: %u %3.2f%%\n", m_total_2part, (m_total_2part * 100.0f) / m_total_blocks);
	printf("\n");

	printf("ISE texel weight range histogram mode 11:\n");
	for (uint32_t i = 1; i <= MODE11_LAST_ISE_RANGE; i++)
		printf("%u %u\n", i, m_weight_range_hist_11[i]);
	printf("\n");

	printf("ISE texel weight range histogram mode 11, 2 partition:\n");
	for (uint32_t i = 1; i <= MODE11_PART2_LAST_ISE_RANGE; i++)
		printf("%u %u\n", i, m_weight_range_hist_11_2part[i]);
	printf("\n");

	printf("ISE texel weight range histogram mode 7:\n");
	for (uint32_t i = 1; i <= MODE7_PART1_LAST_ISE_RANGE; i++)
		printf("%u %u\n", i, m_weight_range_hist_7[i]);
	printf("\n");

	printf("ISE texel weight range histogram mode 7, 2 partition:\n");
	for (uint32_t i = 1; i <= MODE7_PART2_LAST_ISE_RANGE; i++)
		printf("%u %u\n", i, m_weight_range_hist_7_2part[i]);
	printf("\n");

	printf("Mode 11 submode histogram:\n");
	for (uint32_t i = 0; i <= MODE11_TOTAL_SUBMODES; i++) // +1 because of the extra direct encoding
		printf("%u %u\n", i, m_mode11_submode_hist[i]);
	printf("\n");

	printf("Mode 7 submode histogram:\n");
	for (uint32_t i = 0; i < MODE7_TOTAL_SUBMODES; i++)
		printf("%u %u\n", i, m_mode7_submode_hist[i]);
	printf("\n");

	printf("Partition pattern table usage histogram:\n");
	for (uint32_t i = 0; i < basist::TOTAL_ASTC_BC7_COMMON_PARTITIONS2; i++)
		printf("%u:%u ", i, m_part_hist[i]);
	printf("\n\n");
}

} // namespace basisu