new files for v2.5
diff --git a/encoder/basisu_astc_ldr_fencode.cpp b/encoder/basisu_astc_ldr_fencode.cpp
new file mode 100644
index 0000000..0ff4054
--- /dev/null
+++ b/encoder/basisu_astc_ldr_fencode.cpp
@@ -0,0 +1,5423 @@
+// basisu_astc_ldr_fencode.cpp
+#include "basisu_astc_ldr_fencode.h"
+#include "../transcoder/basisu_astc_helpers.h"
+#include "basisu_astc_ldr_encode.h"
+#include "basisu_enc.h"
+#include <algorithm>
+
+namespace basisu
+{
+namespace astc_ldrf
+{
+
+// cem: 4 bits, ofs 0
+// w: 4 bits, ofs 4
+// h: 4 bits, ofs 8
+// e: 5 bits, ofs 12
+// w: 4 bits, ofs 17
+// dp: 1 bit, ofs 21
+
+// cems 6,8,10,12 only, gw>=gh (opposite case of gw<gh handled in code), dp ccs index handled in code
+const uint32_t TOTAL_SINGLE_SUBSET_CONFIGS_RGBA = 1589;
+static const uint32_t g_single_subset_configs_rgba[TOTAL_SINGLE_SUBSET_CONFIGS_RGBA] =
+{
+	0x14556, 0x14466, 0x14566, 0x14666, 0x14476, 0x14576, 0x14676, 0x14776, 0x14386, 0x14486, 0x14586, 0x14686, 0x14786, 0x14886, 0x14396, 0x14496, 0x14596, 0x14696, 0x14796, 0x143a6, 0x144a6, 0x145a6, 0x146a6, 0x143b6, 0x144b6, 0x145b6, 0x142c6, 0x143c6, 0x144c6, 0x145c6, 0x14558, 0x14468,
+	0x14568, 0x14668, 0x14478, 0x14578, 0x14678, 0x14778, 0x14388, 0x14488, 0x14588, 0x14688, 0x14788, 0x13888, 0x14398, 0x14498, 0x14598, 0x14698, 0x14798, 0x143a8, 0x144a8, 0x145a8, 0x146a8, 0x143b8, 0x144b8, 0x145b8, 0x142c8, 0x143c8, 0x144c8, 0x145c8, 0x1455a, 0x1446a, 0x1456a, 0x1466a,
+	0x1447a, 0x1457a, 0x1467a, 0x1477a, 0x1438a, 0x1448a, 0x1458a, 0x1468a, 0x1478a, 0x1388a, 0x1439a, 0x1449a, 0x1459a, 0x1469a, 0x1479a, 0x143aa, 0x144aa, 0x145aa, 0x146aa, 0x143ba, 0x144ba, 0x145ba, 0x142ca, 0x143ca, 0x144ca, 0x145ca, 0x1455c, 0x1446c, 0x1456c, 0x1466c, 0x1447c, 0x1457c,
+	0x1467c, 0x1377c, 0x1438c, 0x1448c, 0x1458c, 0x1368c, 0x1078c, 0xd88c, 0x1439c, 0x1449c, 0x1459c, 0x1169c, 0xe79c, 0x143ac, 0x144ac, 0x135ac, 0xf6ac, 0x143bc, 0x144bc, 0x115bc, 0x142cc, 0x143cc, 0x134cc, 0xf5cc, 0x34446, 0x34356, 0x34456, 0x34556, 0x34366, 0x34466, 0x34566, 0x34666,
+	0x34376, 0x34476, 0x34576, 0x34676, 0x34776, 0x34286, 0x34386, 0x34486, 0x34586, 0x34686, 0x2b786, 0x34296, 0x34396, 0x34496, 0x34596, 0x2e696, 0x342a6, 0x343a6, 0x344a6, 0x335a6, 0x276a6, 0x342b6, 0x343b6, 0x344b6, 0x2d5b6, 0x342c6, 0x343c6, 0x344c6, 0x275c6, 0x34448, 0x34358, 0x34458,
+	0x34558, 0x34368, 0x34468, 0x34568, 0x34668, 0x34378, 0x34478, 0x34578, 0x31678, 0x2c778, 0x34288, 0x34388, 0x34488, 0x33588, 0x2d688, 0x26788, 0x34298, 0x34398, 0x34498, 0x2f598, 0x28698, 0x342a8, 0x343a8, 0x334a8, 0x2b5a8, 0x342b8, 0x343b8, 0x304b8, 0x275b8, 0x342c8, 0x343c8, 0x2d4c8,
+	0x3444a, 0x3435a, 0x3445a, 0x3455a, 0x3436a, 0x3446a, 0x3456a, 0x3466a, 0x3437a, 0x3447a, 0x3457a, 0x3167a, 0x2c77a, 0x3428a, 0x3438a, 0x3448a, 0x3358a, 0x2d68a, 0x2678a, 0x3429a, 0x3439a, 0x3449a, 0x2f59a, 0x2869a, 0x342aa, 0x343aa, 0x334aa, 0x2b5aa, 0x342ba, 0x343ba, 0x304ba, 0x275ba,
+	0x342ca, 0x343ca, 0x2d4ca, 0x3444c, 0x3435c, 0x3445c, 0x3455c, 0x3436c, 0x3446c, 0x3356c, 0x3066c, 0x3437c, 0x3447c, 0x3057c, 0x2c67c, 0x2877c, 0x3428c, 0x3438c, 0x3248c, 0x2d58c, 0x2868c, 0x2478c, 0x3429c, 0x3439c, 0x3049c, 0x2a59c, 0x2569c, 0x342ac, 0x333ac, 0x2d4ac, 0x275ac, 0x342bc,
+	0x313bc, 0x2b4bc, 0x245bc, 0x342cc, 0x303cc, 0x284cc, 0x54346, 0x54446, 0x54356, 0x54456, 0x54556, 0x54266, 0x54366, 0x54466, 0x54566, 0x54666, 0x54276, 0x54376, 0x54476, 0x54576, 0x50676, 0x54286, 0x54386, 0x54486, 0x53586, 0x47686, 0x54296, 0x54396, 0x54496, 0x4b596, 0x542a6, 0x543a6,
+	0x534a6, 0x542b6, 0x543b6, 0x4d4b6, 0x542c6, 0x543c6, 0x474c6, 0x54348, 0x54448, 0x54358, 0x54458, 0x54558, 0x54268, 0x54368, 0x54468, 0x54568, 0x4f668, 0x54278, 0x54378, 0x54478, 0x50578, 0x49678, 0x54288, 0x54388, 0x53488, 0x4b588, 0x54298, 0x54398, 0x4f498, 0x46598, 0x542a8, 0x543a8,
+	0x4b4a8, 0x542b8, 0x523b8, 0x474b8, 0x542c8, 0x4f3c8, 0x5434a, 0x5444a, 0x5435a, 0x5445a, 0x5455a, 0x5426a, 0x5436a, 0x5446a, 0x5456a, 0x4f66a, 0x5427a, 0x5437a, 0x5447a, 0x5057a, 0x4967a, 0x5428a, 0x5438a, 0x5348a, 0x4b58a, 0x5429a, 0x5439a, 0x4f49a, 0x4659a, 0x542aa, 0x543aa, 0x4b4aa,
+	0x542ba, 0x523ba, 0x474ba, 0x542ca, 0x4f3ca, 0x5434c, 0x5444c, 0x5435c, 0x5445c, 0x5355c, 0x5426c, 0x5436c, 0x5346c, 0x4f56c, 0x4a66c, 0x5427c, 0x5437c, 0x5047c, 0x4b57c, 0x4667c, 0x5428c, 0x5338c, 0x4d48c, 0x4758c, 0x5429c, 0x5139c, 0x4a49c, 0x4459c, 0x542ac, 0x4f3ac, 0x474ac, 0x542bc,
+	0x4d3bc, 0x444bc, 0x532cc, 0x4a3cc, 0x74346, 0x74446, 0x74256, 0x74356, 0x74456, 0x74556, 0x74266, 0x74366, 0x74466, 0x74566, 0x70666, 0x74276, 0x74376, 0x74476, 0x71576, 0x74286, 0x74386, 0x74486, 0x68586, 0x74296, 0x74396, 0x70496, 0x742a6, 0x743a6, 0x684a6, 0x742b6, 0x743b6, 0x742c6,
+	0x703c6, 0x74348, 0x74448, 0x74258, 0x74358, 0x74458, 0x74558, 0x74268, 0x74368, 0x74468, 0x70568, 0x69668, 0x74278, 0x74378, 0x72478, 0x6a578, 0x74288, 0x74388, 0x6e488, 0x64588, 0x74298, 0x74398, 0x69498, 0x742a8, 0x703a8, 0x644a8, 0x742b8, 0x6d3b8, 0x742c8, 0x693c8, 0x7434a, 0x7444a,
+	0x7425a, 0x7435a, 0x7445a, 0x7455a, 0x7426a, 0x7436a, 0x7446a, 0x7056a, 0x6966a, 0x7427a, 0x7437a, 0x7247a, 0x6a57a, 0x7428a, 0x7438a, 0x6e48a, 0x6458a, 0x7429a, 0x7439a, 0x6949a, 0x742aa, 0x703aa, 0x644aa, 0x742ba, 0x6d3ba, 0x742ca, 0x693ca, 0x7434c, 0x7444c, 0x7425c, 0x7435c, 0x7445c,
+	0x6f55c, 0x7426c, 0x7436c, 0x7046c, 0x6b56c, 0x6666c, 0x7427c, 0x7337c, 0x6d47c, 0x6757c, 0x7428c, 0x7038c, 0x6948c, 0x7429c, 0x6e39c, 0x6649c, 0x742ac, 0x6b3ac, 0x722bc, 0x683bc, 0x702cc, 0x663cc, 0x94336, 0x94346, 0x94446, 0x94256, 0x94356, 0x94456, 0x94556, 0x94266, 0x94366, 0x94466,
+	0x94566, 0x88666, 0x94276, 0x94376, 0x94476, 0x8b576, 0x94286, 0x94386, 0x90486, 0x94296, 0x94396, 0x88496, 0x942a6, 0x943a6, 0x942b6, 0x8e3b6, 0x942c6, 0x883c6, 0x94338, 0x94348, 0x94448, 0x94258, 0x94358, 0x94458, 0x93558, 0x94268, 0x94368, 0x94468, 0x8c568, 0x84668, 0x94278, 0x94378,
+	0x8f478, 0x86578, 0x94288, 0x94388, 0x89488, 0x94298, 0x90398, 0x84498, 0x942a8, 0x8c3a8, 0x942b8, 0x883b8, 0x942c8, 0x843c8, 0x9433a, 0x9434a, 0x9444a, 0x9425a, 0x9435a, 0x9445a, 0x9355a, 0x9426a, 0x9436a, 0x9446a, 0x8c56a, 0x8466a, 0x9427a, 0x9437a, 0x8f47a, 0x8657a, 0x9428a, 0x9438a,
+	0x8948a, 0x9429a, 0x9039a, 0x8449a, 0x942aa, 0x8c3aa, 0x942ba, 0x883ba, 0x942ca, 0x843ca, 0x9433c, 0x9434c, 0x9444c, 0x9425c, 0x9435c, 0x9245c, 0x8d55c, 0x9426c, 0x9436c, 0x8e46c, 0x8856c, 0x9427c, 0x9137c, 0x8a47c, 0x9428c, 0x8e38c, 0x8648c, 0x9429c, 0x8b39c, 0x922ac, 0x883ac, 0x902bc,
+	0x853bc, 0x8e2cc, 0xb4336, 0xb4246, 0xb4346, 0xb4446, 0xb4256, 0xb4356, 0xb4456, 0xb4556, 0xb4266, 0xb4366, 0xb4466, 0xab566, 0xb4276, 0xb4376, 0xb0476, 0xb4286, 0xb4386, 0xa7486, 0xb4296, 0xb2396, 0xb42a6, 0xab3a6, 0xb42b6, 0xb42c6, 0xb4338, 0xb4248, 0xb4348, 0xb4448, 0xb4258, 0xb4358,
+	0xb4458, 0xae558, 0xb4268, 0xb4368, 0xaf468, 0xa6568, 0xb4278, 0xb4378, 0xa9478, 0xb4288, 0xaf388, 0xb4298, 0xab398, 0xb42a8, 0xa63a8, 0xb22b8, 0xaf2c8, 0xb433a, 0xb424a, 0xb434a, 0xb444a, 0xb425a, 0xb435a, 0xb445a, 0xae55a, 0xb426a, 0xb436a, 0xaf46a, 0xa656a, 0xb427a, 0xb437a, 0xa947a,
+	0xb428a, 0xaf38a, 0xb429a, 0xab39a, 0xb42aa, 0xa63aa, 0xb22ba, 0xaf2ca, 0xb433c, 0xb424c, 0xb434c, 0xb344c, 0xb425c, 0xb435c, 0xaf45c, 0xa955c, 0xb426c, 0xb136c, 0xaa46c, 0xa456c, 0xb427c, 0xae37c, 0xa647c, 0xb328c, 0xaa38c, 0xb129c, 0xa739c, 0xaf2ac, 0xa43ac, 0xad2bc, 0xaa2cc, 0xd4336,
+	0xd4246, 0xd4346, 0xd4446, 0xd4256, 0xd4356, 0xd4456, 0xd0556, 0xd4266, 0xd4366, 0xd3466, 0xd4276, 0xd4376, 0xc8476, 0xd4286, 0xd3386, 0xd4296, 0xcb396, 0xd42a6, 0xd42b6, 0xd32c6, 0xd4338, 0xd4248, 0xd4348, 0xd4448, 0xd4258, 0xd4358, 0xd2458, 0xc9558, 0xd4268, 0xd4368, 0xcb468, 0xd4278,
+	0xd0378, 0xc4478, 0xd4288, 0xcb388, 0xd4298, 0xc6398, 0xd22a8, 0xce2b8, 0xcb2c8, 0xd433a, 0xd424a, 0xd434a, 0xd444a, 0xd425a, 0xd435a, 0xd245a, 0xc955a, 0xd426a, 0xd436a, 0xcb46a, 0xd427a, 0xd037a, 0xc447a, 0xd428a, 0xcb38a, 0xd429a, 0xc639a, 0xd22aa, 0xce2ba, 0xcb2ca, 0xd433c, 0xd424c,
+	0xd434c, 0xd144c, 0xd425c, 0xd335c, 0xcc45c, 0xc655c, 0xd426c, 0xcf36c, 0xc746c, 0xd427c, 0xcb37c, 0xd128c, 0xc738c, 0xcf29c, 0xc439c, 0xcc2ac, 0xca2bc, 0xc72cc, 0xf4336, 0xf4246, 0xf4346, 0xf4446, 0xf4256, 0xf4356, 0xf4456, 0xeb556, 0xf4266, 0xf4366, 0xee466, 0xf4276, 0xf4376, 0xf4286,
+	0xee386, 0xf4296, 0xf42a6, 0xf32b6, 0xee2c6, 0xf4338, 0xf4248, 0xf4348, 0xf4448, 0xf4258, 0xf4358, 0xef458, 0xe6558, 0xf4268, 0xf3368, 0xe8468, 0xf4278, 0xed378, 0xf4288, 0xe8388, 0xf3298, 0xef2a8, 0xeb2b8, 0xe82c8, 0xf433a, 0xf424a, 0xf434a, 0xf444a, 0xf425a, 0xf435a, 0xef45a, 0xe655a,
+	0xf426a, 0xf336a, 0xe846a, 0xf427a, 0xed37a, 0xf428a, 0xe838a, 0xf329a, 0xef2aa, 0xeb2ba, 0xe82ca, 0xf433c, 0xf424c, 0xf434c, 0xf044c, 0xf425c, 0xf135c, 0xea45c, 0xe455c, 0xf426c, 0xed36c, 0xe546c, 0xf227c, 0xe937c, 0xf028c, 0xe538c, 0xed29c, 0xea2ac, 0xe72bc, 0xe52cc, 0x114236, 0x114336,
+	0x114246, 0x114346, 0x114446, 0x114256, 0x114356, 0x113456, 0x114266, 0x114366, 0x107466, 0x114276, 0x110376, 0x114286, 0x107386, 0x114296, 0x1132a6, 0x10d2b6, 0x1072c6, 0x114238, 0x114338, 0x114248, 0x114348, 0x113448, 0x114258, 0x114358, 0x10b458, 0x114268, 0x10f368, 0x114278, 0x109378, 0x113288, 0x10f298, 0x10b2a8,
+	0x1072b8, 0x11423a, 0x11433a, 0x11424a, 0x11434a, 0x11344a, 0x11425a, 0x11435a, 0x10b45a, 0x11426a, 0x10f36a, 0x11427a, 0x10937a, 0x11328a, 0x10f29a, 0x10b2aa, 0x1072ba, 0x11423c, 0x11433c, 0x11424c, 0x11334c, 0x10d44c, 0x11425c, 0x10f35c, 0x10745c, 0x11326c, 0x10a36c, 0x11027c, 0x10637c, 0x10d28c, 0x10a29c, 0x1072ac,
+	0x1042bc, 0x134236, 0x134336, 0x134246, 0x134346, 0x134446, 0x134256, 0x134356, 0x12e456, 0x134266, 0x134366, 0x134276, 0x12b376, 0x134286, 0x134296, 0x12e2a6, 0x1272b6, 0x134238, 0x134338, 0x134248, 0x134348, 0x130448, 0x134258, 0x133358, 0x128458, 0x134268, 0x12c368, 0x134278, 0x126378, 0x130288, 0x12c298, 0x1282a8,
+	0x13423a, 0x13433a, 0x13424a, 0x13434a, 0x13044a, 0x13425a, 0x13335a, 0x12845a, 0x13426a, 0x12c36a, 0x13427a, 0x12637a, 0x13028a, 0x12c29a, 0x1282aa, 0x13423c, 0x13433c, 0x13424c, 0x13234c, 0x12b44c, 0x13425c, 0x12d35c, 0x12545c, 0x13226c, 0x12836c, 0x12e27c, 0x12b28c, 0x12829c, 0x1252ac, 0x154236, 0x154336, 0x154246,
+	0x154346, 0x154446, 0x154256, 0x154356, 0x14a456, 0x154266, 0x151366, 0x154276, 0x154286, 0x151296, 0x14a2a6, 0x154238, 0x154338, 0x154248, 0x154348, 0x14e448, 0x154258, 0x151358, 0x145458, 0x154268, 0x14a368, 0x153278, 0x14e288, 0x14a298, 0x1452a8, 0x15423a, 0x15433a, 0x15424a, 0x15434a, 0x14e44a, 0x15425a, 0x15135a,
+	0x14545a, 0x15426a, 0x14a36a, 0x15327a, 0x14e28a, 0x14a29a, 0x1452aa, 0x15423c, 0x15433c, 0x15424c, 0x15034c, 0x14a44c, 0x15425c, 0x14b35c, 0x15026c, 0x14636c, 0x14d27c, 0x14a28c, 0x14629c, 0x174236, 0x174336, 0x174246, 0x174346, 0x173446, 0x174256, 0x174356, 0x174266, 0x16b366, 0x174276, 0x173286, 0x16b296, 0x174238,
+	0x174338, 0x174248, 0x174348, 0x16b448, 0x174258, 0x16e358, 0x174268, 0x166368, 0x170278, 0x16b288, 0x166298, 0x17423a, 0x17433a, 0x17424a, 0x17434a, 0x16b44a, 0x17425a, 0x16e35a, 0x17426a, 0x16636a, 0x17027a, 0x16b28a, 0x16629a, 0x17423c, 0x17433c, 0x17424c, 0x16f34c, 0x16744c, 0x17325c, 0x16935c, 0x16f26c, 0x16436c,
+	0x16b27c, 0x16728c, 0x16429c, 0x214346, 0x214446, 0x214356, 0x214456, 0x214556, 0x214266, 0x214366, 0x214466, 0x214566, 0x214276, 0x214376, 0x214476, 0x214286, 0x214386, 0x214486, 0x214296, 0x214396, 0x2142a6, 0x2143a6, 0x2142b6, 0x2142c6, 0x214348, 0x214448, 0x214358, 0x214458, 0x214558, 0x214268, 0x214368, 0x214468,
+	0x214568, 0x214278, 0x214378, 0x214478, 0x214288, 0x214388, 0x212488, 0x214298, 0x214398, 0x2142a8, 0x2143a8, 0x2142b8, 0x2142c8, 0x21434a, 0x21444a, 0x21435a, 0x21445a, 0x21455a, 0x21426a, 0x21436a, 0x21446a, 0x21456a, 0x21427a, 0x21437a, 0x21447a, 0x21428a, 0x21438a, 0x21248a, 0x21429a, 0x21439a, 0x2142aa, 0x2143aa,
+	0x2142ba, 0x2142ca, 0x21434c, 0x21444c, 0x21435c, 0x21445c, 0x21255c, 0x21426c, 0x21436c, 0x21346c, 0x20e56c, 0x21427c, 0x21437c, 0x21047c, 0x21428c, 0x21338c, 0x20d48c, 0x21429c, 0x21039c, 0x2142ac, 0x20e3ac, 0x2142bc, 0x2132cc, 0x234336, 0x234246, 0x234346, 0x234446, 0x234256, 0x234356, 0x234456, 0x231556, 0x234266,
+	0x234366, 0x234466, 0x225566, 0x234276, 0x234376, 0x22a476, 0x234286, 0x234386, 0x234296, 0x22c396, 0x2342a6, 0x2253a6, 0x2342b6, 0x2342c6, 0x234338, 0x234248, 0x234348, 0x234448, 0x234258, 0x234358, 0x232458, 0x22a558, 0x234268, 0x234368, 0x22c468, 0x234278, 0x230378, 0x225478, 0x234288, 0x22c388, 0x234298, 0x227398,
+	0x2322a8, 0x22f2b8, 0x22c2c8, 0x23433a, 0x23424a, 0x23434a, 0x23444a, 0x23425a, 0x23435a, 0x23245a, 0x22a55a, 0x23426a, 0x23436a, 0x22c46a, 0x23427a, 0x23037a, 0x22547a, 0x23428a, 0x22c38a, 0x23429a, 0x22739a, 0x2322aa, 0x22f2ba, 0x22c2ca, 0x23433c, 0x23424c, 0x23434c, 0x23144c, 0x23425c, 0x23335c, 0x22d45c, 0x22755c,
+	0x23426c, 0x22f36c, 0x22846c, 0x23427c, 0x22b37c, 0x23128c, 0x22838c, 0x22f29c, 0x22439c, 0x22d2ac, 0x22a2bc, 0x2282cc, 0x254236, 0x254336, 0x254246, 0x254346, 0x254446, 0x254256, 0x254356, 0x251456, 0x254266, 0x254366, 0x245466, 0x254276, 0x24e376, 0x254286, 0x245386, 0x254296, 0x2512a6, 0x24b2b6, 0x2452c6, 0x254238,
+	0x254338, 0x254248, 0x254348, 0x252448, 0x254258, 0x254358, 0x24a458, 0x254268, 0x24e368, 0x254278, 0x248378, 0x252288, 0x24e298, 0x24a2a8, 0x2462b8, 0x25423a, 0x25433a, 0x25424a, 0x25434a, 0x25244a, 0x25425a, 0x25435a, 0x24a45a, 0x25426a, 0x24e36a, 0x25427a, 0x24837a, 0x25228a, 0x24e29a, 0x24a2aa, 0x2462ba, 0x25423c,
+	0x25433c, 0x25424c, 0x25334c, 0x24d44c, 0x25425c, 0x24e35c, 0x24745c, 0x25326c, 0x24a36c, 0x25027c, 0x24537c, 0x24d28c, 0x24a29c, 0x2472ac, 0x2442bc, 0x274236, 0x274336, 0x274246, 0x274346, 0x274446, 0x274256, 0x274356, 0x267456, 0x274266, 0x26e366, 0x274276, 0x274286, 0x26e296, 0x2672a6, 0x274238, 0x274338, 0x274248,
+	0x274348, 0x26d448, 0x274258, 0x26f358, 0x274268, 0x268368, 0x271278, 0x26d288, 0x268298, 0x27423a, 0x27433a, 0x27424a, 0x27434a, 0x26d44a, 0x27425a, 0x26f35a, 0x27426a, 0x26836a, 0x27127a, 0x26d28a, 0x26829a, 0x27423c, 0x27433c, 0x27424c, 0x27034c, 0x26844c, 0x27325c, 0x26a35c, 0x27026c, 0x26536c, 0x26c27c, 0x26828c,
+	0x26529c, 0x294236, 0x294336, 0x294246, 0x294346, 0x28e446, 0x294256, 0x293356, 0x294266, 0x287366, 0x294276, 0x28e286, 0x287296, 0x294238, 0x294338, 0x294248, 0x293348, 0x288448, 0x294258, 0x28b358, 0x293268, 0x28e278, 0x288288, 0x29423a, 0x29433a, 0x29424a, 0x29334a, 0x28844a, 0x29425a, 0x28b35a, 0x29326a, 0x28e27a,
+	0x28828a, 0x29423c, 0x29333c, 0x29424c, 0x28d34c, 0x28544c, 0x29125c, 0x28735c, 0x28d26c, 0x28927c, 0x28528c, 0x2b4226, 0x2b4236, 0x2b4336, 0x2b4246, 0x2b4346, 0x2a5446, 0x2b4256, 0x2aa356, 0x2b4266, 0x2ae276, 0x2a5286, 0x2b4228, 0x2b4238, 0x2b4338, 0x2b4248, 0x2ae348, 0x2b4258, 0x2a5358, 0x2ae268, 0x2a8278, 0x2b422a,
+	0x2b423a, 0x2b433a, 0x2b424a, 0x2ae34a, 0x2b425a, 0x2a535a, 0x2ae26a, 0x2a827a, 0x2b422c, 0x2b423c, 0x2b033c, 0x2b324c, 0x2aa34c, 0x2ae25c, 0x2aa26c, 0x2a527c, 0x2d4226, 0x2d4236, 0x2d4336, 0x2d4246, 0x2d1346, 0x2d4256, 0x2d1266, 0x2c7276, 0x2d4228, 0x2d4238, 0x2d4338, 0x2d4248, 0x2ca348, 0x2d1258, 0x2ca268, 0x2d422a,
+	0x2d423a, 0x2d433a, 0x2d424a, 0x2ca34a, 0x2d125a, 0x2ca26a, 0x2d422c, 0x2d423c, 0x2ce33c, 0x2d024c, 0x2c734c, 0x2cb25c, 0x2c726c, 0x2f4226, 0x2f4236, 0x2f4336, 0x2f4246, 0x2ec346, 0x2f4256, 0x2ec266, 0x2f4228, 0x2f4238, 0x2f2338, 0x2f4248, 0x2e7348, 0x2ee258, 0x2e7268, 0x2f422a, 0x2f423a, 0x2f233a, 0x2f424a, 0x2e734a,
+	0x2ee25a, 0x2e726a, 0x2f422c, 0x2f423c, 0x2ec33c, 0x2ef24c, 0x2e434c, 0x2ea25c, 0x2e426c, 0x314226, 0x314236, 0x314336, 0x314246, 0x305346, 0x311256, 0x305266, 0x314228, 0x314238, 0x30e338, 0x312248, 0x30a258, 0x31422a, 0x31423a, 0x30e33a, 0x31224a, 0x30a25a, 0x31422c, 0x31323c, 0x30a33c, 0x30d24c, 0x30725c, 0x334226,
+	0x334236, 0x333336, 0x334246, 0x32c256, 0x334228, 0x334238, 0x32b338, 0x32f248, 0x327258, 0x33422a, 0x33423a, 0x32b33a, 0x32f24a, 0x32725a, 0x33422c, 0x33123c, 0x32733c, 0x32a24c, 0x32425c, 0x354226, 0x354236, 0x34f336, 0x354246, 0x348256, 0x354228, 0x354238, 0x349338, 0x34d248, 0x344258, 0x35422a, 0x35423a, 0x34933a,
+	0x34d24a, 0x34425a, 0x35422c, 0x35023c, 0x34533c, 0x34924c, 0x374226, 0x374236, 0x36a336, 0x371246, 0x374228, 0x374238, 0x365338, 0x36a248, 0x37422a, 0x37423a, 0x36533a, 0x36a24a, 0x37422c, 0x36e23c, 0x36724c
+};
+
+// cems 0,4 only, gw>=gh (opposite case of gw<gh handled in code)
+const uint32_t TOTAL_SINGLE_SUBSET_CONFIGS_LA = 700;
+static const uint32_t g_single_subset_configs_la[TOTAL_SINGLE_SUBSET_CONFIGS_LA] =
+{
+	0x14550, 0x14460, 0x14560, 0x14660, 0x14470, 0x14570, 0x14670, 0x14770, 0x14380, 0x14480, 0x14580, 0x14680, 0x14780, 0x14880, 0x14390, 0x14490, 0x14590, 0x14690, 0x14790, 0x143a0, 0x144a0, 0x145a0, 0x146a0, 0x143b0, 0x144b0, 0x145b0, 0x142c0, 0x143c0, 0x144c0, 0x145c0, 0x14554, 0x14464,
+	0x14564, 0x14664, 0x14474, 0x14574, 0x14674, 0x14774, 0x14384, 0x14484, 0x14584, 0x14684, 0x14784, 0x14884, 0x14394, 0x14494, 0x14594, 0x14694, 0x14794, 0x143a4, 0x144a4, 0x145a4, 0x146a4, 0x143b4, 0x144b4, 0x145b4, 0x142c4, 0x143c4, 0x144c4, 0x145c4, 0x34440, 0x34350, 0x34450, 0x34550,
+	0x34360, 0x34460, 0x34560, 0x34660, 0x34370, 0x34470, 0x34570, 0x34670, 0x34770, 0x34280, 0x34380, 0x34480, 0x34580, 0x34680, 0x34780, 0x34290, 0x34390, 0x34490, 0x34590, 0x34690, 0x342a0, 0x343a0, 0x344a0, 0x345a0, 0x326a0, 0x342b0, 0x343b0, 0x344b0, 0x345b0, 0x342c0, 0x343c0, 0x344c0,
+	0x325c0, 0x34444, 0x34354, 0x34454, 0x34554, 0x34364, 0x34464, 0x34564, 0x34664, 0x34374, 0x34474, 0x34574, 0x34674, 0x34774, 0x34284, 0x34384, 0x34484, 0x34584, 0x34684, 0x2b784, 0x34294, 0x34394, 0x34494, 0x34594, 0x2e694, 0x342a4, 0x343a4, 0x344a4, 0x335a4, 0x276a4, 0x342b4, 0x343b4,
+	0x344b4, 0x2d5b4, 0x342c4, 0x343c4, 0x344c4, 0x275c4, 0x54340, 0x54440, 0x54350, 0x54450, 0x54550, 0x54260, 0x54360, 0x54460, 0x54560, 0x54660, 0x54270, 0x54370, 0x54470, 0x54570, 0x54670, 0x54280, 0x54380, 0x54480, 0x54580, 0x52680, 0x54290, 0x54390, 0x54490, 0x54590, 0x542a0, 0x543a0,
+	0x544a0, 0x542b0, 0x543b0, 0x544b0, 0x542c0, 0x543c0, 0x524c0, 0x54344, 0x54444, 0x54354, 0x54454, 0x54554, 0x54264, 0x54364, 0x54464, 0x54564, 0x54664, 0x54274, 0x54374, 0x54474, 0x54574, 0x50674, 0x54284, 0x54384, 0x54484, 0x53584, 0x47684, 0x54294, 0x54394, 0x54494, 0x4b594, 0x542a4,
+	0x543a4, 0x534a4, 0x542b4, 0x543b4, 0x4d4b4, 0x542c4, 0x543c4, 0x474c4, 0x74340, 0x74440, 0x74250, 0x74350, 0x74450, 0x74550, 0x74260, 0x74360, 0x74460, 0x74560, 0x74660, 0x74270, 0x74370, 0x74470, 0x74570, 0x74280, 0x74380, 0x74480, 0x74580, 0x74290, 0x74390, 0x74490, 0x742a0, 0x743a0,
+	0x744a0, 0x742b0, 0x743b0, 0x742c0, 0x743c0, 0x74344, 0x74444, 0x74254, 0x74354, 0x74454, 0x74554, 0x74264, 0x74364, 0x74464, 0x74564, 0x70664, 0x74274, 0x74374, 0x74474, 0x71574, 0x74284, 0x74384, 0x74484, 0x68584, 0x74294, 0x74394, 0x70494, 0x742a4, 0x743a4, 0x684a4, 0x742b4, 0x743b4,
+	0x742c4, 0x703c4, 0x94330, 0x94340, 0x94440, 0x94250, 0x94350, 0x94450, 0x94550, 0x94260, 0x94360, 0x94460, 0x94560, 0x94660, 0x94270, 0x94370, 0x94470, 0x94570, 0x94280, 0x94380, 0x94480, 0x94290, 0x94390, 0x94490, 0x942a0, 0x943a0, 0x942b0, 0x943b0, 0x942c0, 0x943c0, 0x94334, 0x94344,
+	0x94444, 0x94254, 0x94354, 0x94454, 0x94554, 0x94264, 0x94364, 0x94464, 0x94564, 0x88664, 0x94274, 0x94374, 0x94474, 0x8b574, 0x94284, 0x94384, 0x90484, 0x94294, 0x94394, 0x88494, 0x942a4, 0x943a4, 0x942b4, 0x8e3b4, 0x942c4, 0x883c4, 0xb4330, 0xb4240, 0xb4340, 0xb4440, 0xb4250, 0xb4350,
+	0xb4450, 0xb4550, 0xb4260, 0xb4360, 0xb4460, 0xb4560, 0xb4270, 0xb4370, 0xb4470, 0xb4280, 0xb4380, 0xb2480, 0xb4290, 0xb4390, 0xb42a0, 0xb43a0, 0xb42b0, 0xb42c0, 0xb4334, 0xb4244, 0xb4344, 0xb4444, 0xb4254, 0xb4354, 0xb4454, 0xb4554, 0xb4264, 0xb4364, 0xb4464, 0xab564, 0xb4274, 0xb4374,
+	0xb0474, 0xb4284, 0xb4384, 0xa7484, 0xb4294, 0xb2394, 0xb42a4, 0xab3a4, 0xb42b4, 0xb42c4, 0xd4330, 0xd4240, 0xd4340, 0xd4440, 0xd4250, 0xd4350, 0xd4450, 0xd4550, 0xd4260, 0xd4360, 0xd4460, 0xd4270, 0xd4370, 0xd4470, 0xd4280, 0xd4380, 0xd4290, 0xd4390, 0xd42a0, 0xd42b0, 0xd42c0, 0xd4334,
+	0xd4244, 0xd4344, 0xd4444, 0xd4254, 0xd4354, 0xd4454, 0xd0554, 0xd4264, 0xd4364, 0xd3464, 0xd4274, 0xd4374, 0xc8474, 0xd4284, 0xd3384, 0xd4294, 0xcb394, 0xd42a4, 0xd42b4, 0xd32c4, 0xf4330, 0xf4240, 0xf4340, 0xf4440, 0xf4250, 0xf4350, 0xf4450, 0xf4550, 0xf4260, 0xf4360, 0xf4460, 0xf4270,
+	0xf4370, 0xf4280, 0xf4380, 0xf4290, 0xf42a0, 0xf42b0, 0xf42c0, 0xf4334, 0xf4244, 0xf4344, 0xf4444, 0xf4254, 0xf4354, 0xf4454, 0xeb554, 0xf4264, 0xf4364, 0xee464, 0xf4274, 0xf4374, 0xf4284, 0xee384, 0xf4294, 0xf42a4, 0xf32b4, 0xee2c4, 0x114230, 0x114330, 0x114240, 0x114340, 0x114440, 0x114250,
+	0x114350, 0x114450, 0x114260, 0x114360, 0x112460, 0x114270, 0x114370, 0x114280, 0x112380, 0x114290, 0x1142a0, 0x1142b0, 0x1122c0, 0x114234, 0x114334, 0x114244, 0x114344, 0x114444, 0x114254, 0x114354, 0x113454, 0x114264, 0x114364, 0x107464, 0x114274, 0x110374, 0x114284, 0x107384, 0x114294, 0x1132a4, 0x10d2b4, 0x1072c4,
+	0x134230, 0x134330, 0x134240, 0x134340, 0x134440, 0x134250, 0x134350, 0x134450, 0x134260, 0x134360, 0x134270, 0x134370, 0x134280, 0x134290, 0x1342a0, 0x1322b0, 0x134234, 0x134334, 0x134244, 0x134344, 0x134444, 0x134254, 0x134354, 0x12e454, 0x134264, 0x134364, 0x134274, 0x12b374, 0x134284, 0x134294, 0x12e2a4, 0x1272b4,
+	0x154230, 0x154330, 0x154240, 0x154340, 0x154440, 0x154250, 0x154350, 0x154450, 0x154260, 0x154360, 0x154270, 0x154280, 0x154290, 0x1542a0, 0x154234, 0x154334, 0x154244, 0x154344, 0x154444, 0x154254, 0x154354, 0x14a454, 0x154264, 0x151364, 0x154274, 0x154284, 0x151294, 0x14a2a4, 0x174230, 0x174330, 0x174240, 0x174340,
+	0x174440, 0x174250, 0x174350, 0x174260, 0x174360, 0x174270, 0x174280, 0x174290, 0x174234, 0x174334, 0x174244, 0x174344, 0x173444, 0x174254, 0x174354, 0x174264, 0x16b364, 0x174274, 0x173284, 0x16b294, 0x214344, 0x214444, 0x214354, 0x214454, 0x214554, 0x214264, 0x214364, 0x214464, 0x214564, 0x214274, 0x214374, 0x214474,
+	0x214284, 0x214384, 0x214484, 0x214294, 0x214394, 0x2142a4, 0x2143a4, 0x2142b4, 0x2142c4, 0x234334, 0x234244, 0x234344, 0x234444, 0x234254, 0x234354, 0x234454, 0x231554, 0x234264, 0x234364, 0x234464, 0x225564, 0x234274, 0x234374, 0x22a474, 0x234284, 0x234384, 0x234294, 0x22c394, 0x2342a4, 0x2253a4, 0x2342b4, 0x2342c4,
+	0x254234, 0x254334, 0x254244, 0x254344, 0x254444, 0x254254, 0x254354, 0x251454, 0x254264, 0x254364, 0x245464, 0x254274, 0x24e374, 0x254284, 0x245384, 0x254294, 0x2512a4, 0x24b2b4, 0x2452c4, 0x274234, 0x274334, 0x274244, 0x274344, 0x274444, 0x274254, 0x274354, 0x267454, 0x274264, 0x26e364, 0x274274, 0x274284, 0x26e294,
+	0x2672a4, 0x294234, 0x294334, 0x294244, 0x294344, 0x28e444, 0x294254, 0x293354, 0x294264, 0x287364, 0x294274, 0x28e284, 0x287294, 0x2b4224, 0x2b4234, 0x2b4334, 0x2b4244, 0x2b4344, 0x2a5444, 0x2b4254, 0x2aa354, 0x2b4264, 0x2ae274, 0x2a5284, 0x2d4224, 0x2d4234, 0x2d4334, 0x2d4244, 0x2d1344, 0x2d4254, 0x2d1264, 0x2c7274,
+	0x2f4224, 0x2f4234, 0x2f4334, 0x2f4244, 0x2ec344, 0x2f4254, 0x2ec264, 0x314224, 0x314234, 0x314334, 0x314244, 0x305344, 0x311254, 0x305264, 0x334224, 0x334234, 0x333334, 0x334244, 0x32c254, 0x354224, 0x354234, 0x34f334, 0x354244, 0x348254, 0x374224, 0x374234, 0x36a334, 0x371244
+};
+
+enum
+{
+	cR = 0,
+	cG = 1,
+	cB = 2,
+	cA = 3
+};
+
+enum cem_index
+{
+	cCEM6 = 0,   // RGB Base+Scale
+	cCEM8 = 1,   // RGB Direct (also CEM 0 for ranking purposes)
+	cCEM10 = 2,  // RGB Base Scale+Two A
+	cCEM12 = 3,  // RGBA Direct (also CEM 4 for ranking purposes)
+
+	cCEMTotalIndices
+};
+
+static const uint32_t PIXELBUF_ROW_PITCH = 16, PIXELBUF_COMP_PITCH = 16 * 16, PIXELBUF_SIZE_IN_FLOATS = PIXELBUF_COMP_PITCH * 4;
+
+struct pixelbuf
+{
+	float* m_pBuf;
+	uint32_t m_width, m_height;
+
+	inline pixelbuf() {}
+	inline pixelbuf(uint32_t width, uint32_t height, float* pBuf) : m_pBuf(pBuf), m_width(width), m_height(height) {}
+};
+
+static inline float pixelbuf_get_comp(const float* pPixel_buf, uint32_t x, uint32_t y, uint32_t c)
+{
+	assert((x + y * PIXELBUF_ROW_PITCH + c * PIXELBUF_COMP_PITCH) < PIXELBUF_SIZE_IN_FLOATS);
+	return pPixel_buf[x + y * PIXELBUF_ROW_PITCH + c * PIXELBUF_COMP_PITCH];
+}
+
+static inline double pixelbuf_get_comp(const double* pPixel_buf, uint32_t x, uint32_t y, uint32_t c)
+{
+	assert((x + y * PIXELBUF_ROW_PITCH + c * PIXELBUF_COMP_PITCH) < PIXELBUF_SIZE_IN_FLOATS);
+	return pPixel_buf[x + y * PIXELBUF_ROW_PITCH + c * PIXELBUF_COMP_PITCH];
+}
+
+static inline float pixelbuf_get_comp(const pixelbuf &pbuf, uint32_t x, uint32_t y, uint32_t c)
+{
+	assert((x + y * PIXELBUF_ROW_PITCH + c * PIXELBUF_COMP_PITCH) < PIXELBUF_SIZE_IN_FLOATS);
+	return pbuf.m_pBuf[x + y * PIXELBUF_ROW_PITCH + c * PIXELBUF_COMP_PITCH];
+}
+
+static inline void pixelbuf_set_comp(float* pPixel_buf, uint32_t x, uint32_t y, uint32_t c, float value)
+{
+	assert((x + y * PIXELBUF_ROW_PITCH + c * PIXELBUF_COMP_PITCH) < PIXELBUF_SIZE_IN_FLOATS);
+	pPixel_buf[x + y * PIXELBUF_ROW_PITCH + c * PIXELBUF_COMP_PITCH] = value;
+}
+
+static inline void pixelbuf_set_comp(double* pPixel_buf, uint32_t x, uint32_t y, uint32_t c, double value)
+{
+	assert((x + y * PIXELBUF_ROW_PITCH + c * PIXELBUF_COMP_PITCH) < PIXELBUF_SIZE_IN_FLOATS);
+	pPixel_buf[x + y * PIXELBUF_ROW_PITCH + c * PIXELBUF_COMP_PITCH] = value;
+}
+
+static inline void pixelbuf_set_comp(const pixelbuf &pbuf, uint32_t x, uint32_t y, uint32_t c, float value)
+{
+	assert((x + y * PIXELBUF_ROW_PITCH + c * PIXELBUF_COMP_PITCH) < PIXELBUF_SIZE_IN_FLOATS);
+	pbuf.m_pBuf[x + y * PIXELBUF_ROW_PITCH + c * PIXELBUF_COMP_PITCH] = value;
+}
+
+static inline void pixelbuf_get_pixel(const float* pPixel_buf, uint32_t x, uint32_t y, float* pDst_pixel, uint32_t num_comps)
+{
+	assert((num_comps == 3) || (num_comps == 4));
+
+	pDst_pixel[0] = pixelbuf_get_comp(pPixel_buf, x, y, cR);
+	pDst_pixel[1] = pixelbuf_get_comp(pPixel_buf, x, y, cG);
+	pDst_pixel[2] = pixelbuf_get_comp(pPixel_buf, x, y, cB);
+
+	if (num_comps == 4)
+		pDst_pixel[3] = pixelbuf_get_comp(pPixel_buf, x, y, cA);
+}
+
+static inline void pixelbuf_get_pixel3(const float* pPixel_buf, uint32_t x, uint32_t y, float* pDst_pixel)
+{
+	pDst_pixel[0] = pixelbuf_get_comp(pPixel_buf, x, y, cR);
+	pDst_pixel[1] = pixelbuf_get_comp(pPixel_buf, x, y, cG);
+	pDst_pixel[2] = pixelbuf_get_comp(pPixel_buf, x, y, cB);
+}
+
+static inline void pixelbuf_get_pixel4(const float* pPixel_buf, uint32_t x, uint32_t y, float* pDst_pixel)
+{
+	pDst_pixel[0] = pixelbuf_get_comp(pPixel_buf, x, y, cR);
+	pDst_pixel[1] = pixelbuf_get_comp(pPixel_buf, x, y, cG);
+	pDst_pixel[2] = pixelbuf_get_comp(pPixel_buf, x, y, cB);
+	pDst_pixel[3] = pixelbuf_get_comp(pPixel_buf, x, y, cA);
+}
+
+static inline void pixelbuf_load_block(pixelbuf &pbuf, const rgba32_image &src_img, uint32_t x_ofs, uint32_t y_ofs, uint32_t num_comps)
+{
+	const uint32_t width = pbuf.m_width, height = pbuf.m_height;
+	float* pDst_pixel_buf = pbuf.m_pBuf;
+
+	assert((num_comps == 3) || (num_comps == 4));
+	assert((x_ofs + width) <= src_img.m_width);
+	assert((y_ofs + height) <= src_img.m_height);
+	assert(src_img.m_width && src_img.m_height && (src_img.m_row_pitch_in_texels >= src_img.m_width));
+		
+	const uint32_t row_byte_pitch = src_img.m_row_pitch_in_texels * sizeof(uint32_t);
+
+	const uint8_t* pSrc_row = src_img.m_pPixels + ((y_ofs * height) * src_img.m_row_pitch_in_texels + (x_ofs * width)) * sizeof(uint32_t);
+
+	if (num_comps == 3)
+	{
+		for (uint32_t y = 0; y < height; y++, pSrc_row += row_byte_pitch)
+		{
+			const uint8_t* pSrc = pSrc_row;
+
+			for (uint32_t x = 0; x < width; x++, pSrc += 4)
+			{
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cR, (float)pSrc[0]);
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cG, (float)pSrc[1]);
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cB, (float)pSrc[2]);
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cA, 255.0f);
+			}
+		}
+	}
+	else
+	{
+		for (uint32_t y = 0; y < height; y++, pSrc_row += row_byte_pitch)
+		{
+			const uint8_t* pSrc = pSrc_row;
+
+			for (uint32_t x = 0; x < width; x++, pSrc += 4)
+			{
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cR, (float)pSrc[0]);
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cG, (float)pSrc[1]);
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cB, (float)pSrc[2]);
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cA, (float)pSrc[3]);
+			}
+		}
+	}
+}
+
+static void pixelbuf_set_comp_to_val(pixelbuf &pbuf, uint32_t comp, float val)
+{
+	const uint32_t width = pbuf.m_width, height = pbuf.m_height;
+	float* pDst_pixel_buf = pbuf.m_pBuf;
+
+	for (uint32_t y = 0; y < height; y++)
+		for (uint32_t x = 0; x < width; x++)
+			pixelbuf_set_comp(pDst_pixel_buf, x, y, comp, val);
+}
+
+static void pixelbuf_swap_comp_with_alpha(pixelbuf &pbuf, uint32_t comp)
+{
+	const uint32_t width = pbuf.m_width, height = pbuf.m_height;
+	float* pDst_pixel_buf = pbuf.m_pBuf;
+
+	assert((comp >= 0) && (comp <= 3));
+
+	if (comp == cR)
+	{
+		for (uint32_t y = 0; y < height; y++)
+		{
+			for (uint32_t x = 0; x < width; x++)
+			{
+				const float r = pixelbuf_get_comp(pDst_pixel_buf, x, y, cR);
+				const float a = pixelbuf_get_comp(pDst_pixel_buf, x, y, cA);
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cR, a);
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cA, r);
+			}
+		}
+	}
+	else if (comp == cG)
+	{
+		for (uint32_t y = 0; y < height; y++)
+		{
+			for (uint32_t x = 0; x < width; x++)
+			{
+				const float g = pixelbuf_get_comp(pDst_pixel_buf, x, y, cG);
+				const float a = pixelbuf_get_comp(pDst_pixel_buf, x, y, cA);
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cG, a);
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cA, g);
+			}
+		}
+	}
+	else if (comp == cB)
+	{
+		for (uint32_t y = 0; y < height; y++)
+		{
+			for (uint32_t x = 0; x < width; x++)
+			{
+				const float b = pixelbuf_get_comp(pDst_pixel_buf, x, y, cB);
+				const float a = pixelbuf_get_comp(pDst_pixel_buf, x, y, cA);
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cB, a);
+				pixelbuf_set_comp(pDst_pixel_buf, x, y, cA, b);
+			}
+		}
+	}
+}
+
+static inline bool does_cem_have_alpha(uint32_t cem)
+{
+	return (cem == 4) || (cem >= 10);
+}
+
+static inline uint32_t get_num_cem_chans(uint32_t cem)
+{
+	return ((cem == 4) || (cem >= 10)) ? 4 : 3;
+}
+
+// l and la
+[[maybe_unused]] static inline bool is_cem_0_or_4(uint32_t cem)
+{
+	return (cem == 0) || (cem == 4);
+}
+
+// base+scale and base+scale plus 2 a
+static inline bool is_cem_6_or_10(uint32_t cem)
+{
+	return (cem == 6) || (cem == 10);
+}
+
+// rgb(a) direct
+static inline bool is_cem_8_or_12(uint32_t cem)
+{
+	return (cem == 8) || (cem == 12);
+}
+
+// rgb(a) base+ofs
+static inline bool is_cem_9_or_13(uint32_t cem)
+{
+	return (cem == 9) || (cem == 13);
+}
+
+void convert_rank_lblock_to_ise(astc_helpers::log_astc_block& log_blk)
+{
+	if ((log_blk.m_solid_color_flag_ldr) || (is_lblock_ise(log_blk)))
+		return;
+
+	const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(log_blk.m_endpoint_ise_range).m_rank_to_ISE;
+
+	const uint32_t num_endpoint_vals = astc_helpers::get_total_endpoint_vals(log_blk);
+	for (uint32_t i = 0; i < num_endpoint_vals; i++)
+		log_blk.m_endpoints[i] = (uint8_t)endpoint_tab[log_blk.m_endpoints[i]];
+
+	const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(log_blk.m_weight_ise_range).m_rank_to_ISE;
+
+	const uint32_t num_weight_vals = astc_helpers::get_total_weights(log_blk);
+	for (uint32_t i = 0; i < num_weight_vals; i++)
+		log_blk.m_weights[i] = (uint8_t)weight_tab[log_blk.m_weights[i]];
+
+	log_blk.m_user_mode = (uint8_t)cUserModeISEValues;
+}
+
+void convert_ise_lblock_to_rank(astc_helpers::log_astc_block& log_blk)
+{
+	if ((log_blk.m_solid_color_flag_ldr) || (!is_lblock_ise(log_blk)))
+		return;
+
+	const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(log_blk.m_endpoint_ise_range).m_ISE_to_rank;
+
+	const uint32_t num_endpoint_vals = astc_helpers::get_total_endpoint_vals(log_blk);
+	for (uint32_t i = 0; i < num_endpoint_vals; i++)
+		log_blk.m_endpoints[i] = (uint8_t)endpoint_tab[log_blk.m_endpoints[i]];
+
+	const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(log_blk.m_weight_ise_range).m_ISE_to_rank;
+
+	const uint32_t num_weight_vals = astc_helpers::get_total_weights(log_blk);
+	for (uint32_t i = 0; i < num_weight_vals; i++)
+		log_blk.m_weights[i] = (uint8_t)weight_tab[log_blk.m_weights[i]];
+
+	log_blk.m_user_mode = (uint8_t)cUserModeRankValues;
+}
+
+static double compute_block_error(const astc_helpers::log_astc_block& lblk, const pixelbuf& src_block, const single_subset_enc_context& ctx)
+{
+	const astc_helpers::log_astc_block* pBlock = &lblk;
+
+	astc_helpers::log_astc_block lblk_temp;
+
+	if (!is_lblock_ise(lblk))
+	{
+		lblk_temp = lblk;
+		convert_rank_lblock_to_ise(lblk_temp);
+		pBlock = &lblk_temp;
+	}
+
+	astc_helpers::color_rgba block_pixels[astc_helpers::MAX_BLOCK_PIXELS];
+	bool status = astc_helpers::decode_block_xuastc_ldr(*pBlock, block_pixels, ctx.m_block_width, ctx.m_block_height, ctx.m_astc_decode_mode);
+	assert(status);
+	if (!status)
+		return DBL_MAX;
+
+	const float wr = (float)ctx.m_chan_weights[0], wg = (float)ctx.m_chan_weights[1], wb = (float)ctx.m_chan_weights[2], wa = (float)ctx.m_chan_weights[3];
+
+	double wsse = 0;
+
+	const astc_helpers::color_rgba* pBlock_pixel = block_pixels;
+
+	for (uint32_t y = 0; y < ctx.m_block_height; y++)
+	{
+		for (uint32_t x = 0; x < ctx.m_block_width; x++)
+		{
+			wsse += squaref((float)pBlock_pixel->m_r - pixelbuf_get_comp(src_block.m_pBuf, x, y, 0)) * wr;
+			wsse += squaref((float)pBlock_pixel->m_g - pixelbuf_get_comp(src_block.m_pBuf, x, y, 1)) * wg;
+			wsse += squaref((float)pBlock_pixel->m_b - pixelbuf_get_comp(src_block.m_pBuf, x, y, 2)) * wb;
+			wsse += squaref((float)pBlock_pixel->m_a - pixelbuf_get_comp(src_block.m_pBuf, x, y, 3)) * wa;
+
+			++pBlock_pixel;
+		}
+	}
+
+	return wsse;
+}
+
+// Scatter/covariance matrix values
+// Symmetric matrix form:
+// 0 1 2 3
+// 1 4 5 6
+// 2 5 7 8
+// 3 6 8 9
+enum
+{
+	cCovarRR = 0, cCovarRG = 1, cCovarRB = 2, cCovarRA = 3,
+	              cCovarGG = 4, cCovarGB = 5, cCovarGA = 6,
+	                            cCovarBB = 7, cCovarBA = 8,
+	                                          cCovarAA = 9, 
+	cTotalCovar = 10,
+};
+
+// Columns of the symmetric 4x4 covariance matrix
+static const uint8_t s_covar_col_indices[4][4] =
+{
+	{ cCovarRR, cCovarRG, cCovarRB, cCovarRA },
+	{ cCovarRG, cCovarGG, cCovarGB, cCovarGA },
+	{ cCovarRB, cCovarGB, cCovarBB, cCovarBA },
+	{ cCovarRA, cCovarGA, cCovarBA, cCovarAA }
+};
+
+// Channel pair Pearson correlation coefficients
+enum
+{
+	cCorrRG = 0, cCorrRB = 1, cCorrRA = 2,
+	             cCorrGB = 3, cCorrGA = 4,
+	                          cCorrBA = 5,
+	cTotalCorr = 6
+};
+
+static void compute_block_moments(
+	float pMoments[cTotalCovar], float pSums[4],
+	const pixelbuf& pixel_buf, 
+	uint32_t num_comps, 
+	bool zero_centered)
+{
+	assert((num_comps == 3) || (num_comps == 4));
+
+	if (zero_centered)
+	{
+		if (num_comps == 3)
+		{
+			for (uint32_t y = 0; y < pixel_buf.m_height; y++)
+			{
+				for (uint32_t x = 0; x < pixel_buf.m_width; x++)
+				{
+					const float r = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cR);
+					const float g = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cG);
+					const float b = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cB);
+
+					pMoments[cCovarRR] += r * r; pMoments[cCovarRG] += r * g; pMoments[cCovarRB] += r * b;
+					pMoments[cCovarGG] += g * g; pMoments[cCovarGB] += g * b;
+					pMoments[cCovarBB] += b * b;
+				} // x
+			} // y
+		}
+		else
+		{
+			assert(num_comps == 4);
+
+			for (uint32_t y = 0; y < pixel_buf.m_height; y++)
+			{
+				for (uint32_t x = 0; x < pixel_buf.m_width; x++)
+				{
+					const float r = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cR);
+					const float g = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cG);
+					const float b = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cB);
+					const float a = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cA);
+
+					pMoments[cCovarRR] += r * r; pMoments[cCovarRG] += r * g; pMoments[cCovarRB] += r * b;
+					pMoments[cCovarGG] += g * g; pMoments[cCovarGB] += g * b;
+					pMoments[cCovarBB] += b * b;
+
+					pMoments[cCovarRA] += r * a; pMoments[cCovarGA] += g * a; pMoments[cCovarBA] += b * a; pMoments[cCovarAA] += a * a;
+				} // x
+			} // y
+		}
+	}
+	else
+	{
+		if (num_comps == 3)
+		{
+			for (uint32_t y = 0; y < pixel_buf.m_height; y++)
+			{
+				for (uint32_t x = 0; x < pixel_buf.m_width; x++)
+				{
+					const float r = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cR);
+					const float g = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cG);
+					const float b = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cB);
+
+					pSums[cR] += r; pSums[cG] += g; pSums[cB] += b;
+
+					pMoments[cCovarRR] += r * r; pMoments[cCovarRG] += r * g; pMoments[cCovarRB] += r * b;
+					pMoments[cCovarGG] += g * g; pMoments[cCovarGB] += g * b;
+					pMoments[cCovarBB] += b * b;
+				} // x
+			} // y
+		}
+		else
+		{
+			assert(num_comps == 4);
+
+			for (uint32_t y = 0; y < pixel_buf.m_height; y++)
+			{
+				for (uint32_t x = 0; x < pixel_buf.m_width; x++)
+				{
+					const float r = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cR);
+					const float g = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cG);
+					const float b = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cB);
+					const float a = pixelbuf_get_comp(pixel_buf.m_pBuf, x, y, cA);
+
+					pSums[cR] += r; pSums[cG] += g; pSums[cB] += b; pSums[cA] += a;
+
+					pMoments[cCovarRR] += r * r; pMoments[cCovarRG] += r * g; pMoments[cCovarRB] += r * b;
+					pMoments[cCovarGG] += g * g; pMoments[cCovarGB] += g * b;
+					pMoments[cCovarBB] += b * b;
+
+					pMoments[cCovarRA] += r * a; pMoments[cCovarGA] += g * a; pMoments[cCovarBA] += b * a; pMoments[cCovarAA] += a * a;
+				} // x
+			} // y
+		}
+	}
+}
+
+static inline void calc_unnormalized_covariance(
+	const float pMoments[cTotalCovar], const float pSums[4], 
+	float pDst_covar[cTotalCovar], 
+	uint32_t num_comps, 
+	uint32_t total_pixels, 
+	bool zero_centered)
+{
+	assert((num_comps == 3) || (num_comps == 4));
+	assert(total_pixels > 0);
+
+	if (zero_centered)
+	{
+		memcpy(pDst_covar, pMoments, sizeof(float) * cTotalCovar);
+		return;
+	}
+	
+	const float oo_total_texels = 1.0f / (float)total_pixels;
+
+	const float sum_r = pSums[cR], sum_g = pSums[cG], sum_b = pSums[cB];
+
+	pDst_covar[cCovarRR] = pMoments[cCovarRR] - (sum_r * sum_r * oo_total_texels);
+	pDst_covar[cCovarRG] = pMoments[cCovarRG] - (sum_r * sum_g * oo_total_texels);
+	pDst_covar[cCovarRB] = pMoments[cCovarRB] - (sum_r * sum_b * oo_total_texels);
+	pDst_covar[cCovarGG] = pMoments[cCovarGG] - (sum_g * sum_g * oo_total_texels);
+	pDst_covar[cCovarGB] = pMoments[cCovarGB] - (sum_g * sum_b * oo_total_texels);
+	pDst_covar[cCovarBB] = pMoments[cCovarBB] - (sum_b * sum_b * oo_total_texels);
+	
+	pDst_covar[cCovarRA] = 0; pDst_covar[cCovarGA] = 0; pDst_covar[cCovarBA] = 0; pDst_covar[cCovarAA] = 0;
+	if (num_comps == 4)
+	{
+		const float sum_a = pSums[cA];
+		pDst_covar[cCovarRA] = pMoments[cCovarRA] - (sum_r * sum_a * oo_total_texels);
+		pDst_covar[cCovarGA] = pMoments[cCovarGA] - (sum_g * sum_a * oo_total_texels);
+		pDst_covar[cCovarBA] = pMoments[cCovarBA] - (sum_b * sum_a * oo_total_texels);
+		pDst_covar[cCovarAA] = pMoments[cCovarAA] - (sum_a * sum_a * oo_total_texels);
+	}
+}
+
+// corr [-1,1]: 0=RG, 1=RB, 2=RA, 3=GB, 4=GA, 5=BA
+// note not clamped
+// xy = cross term
+// xx = first chan
+// xy = second chan
+static inline float corr_pair(float xy, float xx, float yy)
+{
+	float d = xx * yy;
+	if (d <= TINY_EPS)
+		return 1.0f; // one or both channels aren't active; returning 1.0f, not 0.0f, to simplify channel correlation checks later
+
+	float r = xy / sqrtf(d); // note not clamped
+	return r;
+}
+
+// Pearson correlation coefficients (assumed mean removed covar)
+static inline void corr_from_covar(float corr[6], const float covar[10], uint32_t num_comps)
+{
+	assert((num_comps == 3) || (num_comps == 4));
+
+	corr[cCorrRG] = corr_pair(covar[cCovarRG], covar[cCovarRR], covar[cCovarGG]); // RG
+	corr[cCorrRB] = corr_pair(covar[cCovarRB], covar[cCovarRR], covar[cCovarBB]); // RB
+	corr[cCorrGB] = corr_pair(covar[cCovarGB], covar[cCovarGG], covar[cCovarBB]); // GB
+
+	corr[cCorrRA] = corr[cCorrGA] = corr[cCorrBA] = 1.0f; // chan pairs with A default to 1.0f, not 0
+
+	if (num_comps == 4)
+	{
+		corr[cCorrRA] = corr_pair(covar[cCovarRA], covar[cCovarRR], covar[cCovarAA]); // RA
+		corr[cCorrGA] = corr_pair(covar[cCovarGA], covar[cCovarGG], covar[cCovarAA]); // GA
+		corr[cCorrBA] = corr_pair(covar[cCovarBA], covar[cCovarBB], covar[cCovarAA]); // BA
+	}
+}
+
+// computes unnormalized mean or zero centered 4D covar (really scatter) and block mean (covar matrix elements are NOT divided by the total # of texels in the block)
+// mean will be 0 if zero_centered is true
+static inline void compute_covariance(float pDst_covar[cTotalCovar], float mean[4], const pixelbuf& block, uint32_t num_comps, bool zero_centered)
+{
+	assert((num_comps == 3) || (num_comps == 4));
+
+	float moments[cTotalCovar];
+	
+	memset(moments, 0, sizeof(moments));
+	memset(mean, 0, sizeof(float) * 4);
+
+	compute_block_moments(moments, mean, block, num_comps, zero_centered);
+	
+	const uint32_t total_pixels = block.m_width * block.m_height;
+	calc_unnormalized_covariance(moments, mean, pDst_covar, num_comps, total_pixels, zero_centered);
+
+	if (!zero_centered)
+	{
+		const float one_over_total = 1.0f / (float)total_pixels;
+		for (uint32_t c = 0; c < num_comps; c++)
+			mean[c] *= one_over_total;
+	}
+}
+
+// 4x4 * 4x1 = 4x1
+// Computes covar_matrix * vec4.
+static inline void covar_mul_vec4(float pDst[4], const float pCovar[cTotalCovar], const float pSrc[4])
+{
+	const float x = pSrc[0], y = pSrc[1], z = pSrc[2], w = pSrc[3];
+
+	pDst[0] = pCovar[cCovarRR] * x + pCovar[cCovarRG] * y + pCovar[cCovarRB] * z + pCovar[cCovarRA] * w;
+	pDst[1] = pCovar[cCovarRG] * x + pCovar[cCovarGG] * y + pCovar[cCovarGB] * z + pCovar[cCovarGA] * w;
+	pDst[2] = pCovar[cCovarRB] * x + pCovar[cCovarGB] * y + pCovar[cCovarBB] * z + pCovar[cCovarBA] * w;
+	pDst[3] = pCovar[cCovarRA] * x + pCovar[cCovarGA] * y + pCovar[cCovarBA] * z + pCovar[cCovarAA] * w;
+}
+
+// 3x3 * 3x1 = 3x1
+// Computes covar_matrix * vec3.
+// pDst[3] is explicitly cleared so callers can safely treat pDst as vec4.
+static inline void covar_mul_vec3(float pDst[4], const float pCovar[cTotalCovar], const float pSrc[4])
+{
+	const float x = pSrc[0], y = pSrc[1], z = pSrc[2];
+
+	pDst[0] = pCovar[cCovarRR] * x + pCovar[cCovarRG] * y + pCovar[cCovarRB] * z;
+	pDst[1] = pCovar[cCovarRG] * x + pCovar[cCovarGG] * y + pCovar[cCovarGB] * z;
+	pDst[2] = pCovar[cCovarRB] * x + pCovar[cCovarGB] * y + pCovar[cCovarBB] * z;
+	pDst[3] = 0.0f;
+}
+
+static inline void get_initial_axis_from_largest_diag(float v[4], const float covar[cTotalCovar], uint32_t num_comps)
+{
+	assert((num_comps == 3) || (num_comps == 4));
+		
+	[[maybe_unused]] static const uint8_t s_diag_indices[4] = { cCovarRR, cCovarGG, cCovarBB, cCovarAA };
+
+	uint32_t best_col = 0;
+	float best_diag = covar[cCovarRR];
+
+	if (covar[cCovarGG] > best_diag)
+	{
+		best_diag = covar[cCovarGG];
+		best_col = 1;
+	}
+
+	if (covar[cCovarBB] > best_diag)
+	{
+		best_diag = covar[cCovarBB];
+		best_col = 2;
+	}
+
+	if ((num_comps == 4) && (covar[cCovarAA] > best_diag))
+	{
+		best_diag = covar[cCovarAA];
+		best_col = 3;
+	}
+
+	const uint8_t* pCol = s_covar_col_indices[best_col];
+
+	v[0] = covar[pCol[0]];
+	v[1] = covar[pCol[1]];
+	v[2] = covar[pCol[2]];
+	v[3] = (num_comps == 4) ? covar[pCol[3]] : 0.0f;
+}
+
+// all 4 elements of pAxis set
+static void compute_principle_axis(float pAxis[4], const float pCovar[cTotalCovar], uint32_t max_pair_iters, uint32_t num_comps)
+{
+	assert(max_pair_iters);
+	assert((num_comps == 3) || (num_comps == 4));
+
+	//vec4_set(pAxis, 1.0f, 1.0f, 1.0f, (num_comps == 4) ? 1.0f : 0.0f);
+	get_initial_axis_from_largest_diag(pAxis, pCovar, num_comps);
+
+	//vec_normalize(pAxis, num_comps);
+	float s = maximum(fabs(pAxis[0]), fabs(pAxis[1]), fabs(pAxis[2]), fabs(pAxis[3]));
+	if (s > TINY_EPS)
+		vec4_scale(pAxis, 1.0f / s);
+
+	float temp_vec[4], prev_axis[4];
+
+	for (uint32_t i = 0; i < max_pair_iters; ++i)
+	{
+		vec_copy(prev_axis, pAxis, 4);
+
+		if (num_comps == 3)
+		{
+			covar_mul_vec3(temp_vec, pCovar, pAxis);
+			covar_mul_vec3(pAxis, pCovar, temp_vec);
+		}
+		else
+		{
+			covar_mul_vec4(temp_vec, pCovar, pAxis);
+			covar_mul_vec4(pAxis, pCovar, temp_vec);
+		}
+				
+		const float total_sq = vec_normalize(pAxis, num_comps);
+
+		assert(!std::isinf(total_sq));
+		
+		if (total_sq < TINY_EPS)
+		{
+			// should be very rare
+			vec4_set(pAxis, 1.0f, 1.0f, 1.0f, (num_comps == 4) ? 1.0f : 0.0f);
+			vec_normalize(pAxis, num_comps);
+			break;
+		}
+
+		if (i)
+		{
+			float d = vec4_dot(pAxis, prev_axis);
+			const float DOT_THRESH = 0.9997f;
+			if (d >= DOT_THRESH)
+				break;
+		}
+	}
+}
+
+struct block_stats
+{
+	float m_covar[cTotalCovar]; // unnormalized covar
+	float m_corr[cTotalCorr]; // Pearson's but unclamped, defaults to 1.0 (see corr_pair())
+	float m_mean[4];
+	float m_axis[4]; // 3D or 4D normalized
+};
+
+// endpoint values returned in direct ASTC endpoint order: lr hr lg hg lb hb la ha
+static void calc_initial_cem_endpoints(const pixelbuf& block, float pCEM_values[8], uint32_t num_comps, block_stats *pOut_stats, bool zero_centered)
+{
+	//static inline void compute_covariance(float pDst_covar[cTotalCovar], float* pMean, const pixelbuf & block, uint32_t num_comps, bool zero_centered)
+	float cov[cTotalCovar];
+	float block_mean[4];
+
+	compute_covariance(cov, block_mean, block, num_comps, zero_centered);
+
+	if (pOut_stats)
+	{
+		memcpy(pOut_stats->m_covar, cov, sizeof(float) * cTotalCovar);
+
+		corr_from_covar(pOut_stats->m_corr, cov, num_comps);
+
+		// will be 0 if zero_centered
+		memcpy(pOut_stats->m_mean, block_mean, sizeof(float) * 4);
+	}
+	
+	cov[cCovarRR] += SMALL_EPS;
+	cov[cCovarGG] += SMALL_EPS;
+	cov[cCovarBB] += SMALL_EPS;
+	if (num_comps == 4)
+		cov[cCovarAA] += SMALL_EPS;
+
+	float axis[4];
+	
+	const uint32_t MAX_POWER_ITER_PAIRS = 5;
+	compute_principle_axis(axis, cov, MAX_POWER_ITER_PAIRS, num_comps);
+
+	if (pOut_stats)
+		memcpy(pOut_stats->m_axis, axis, sizeof(float) * 4);
+
+	float span[2] = { 1e+30f, -1e+30f };
+	for (uint32_t y = 0; y < block.m_height; y++)
+	{
+		for (uint32_t x = 0; x < block.m_width; x++)
+		{
+			float d = 0;
+			for (uint32_t c = 0; c < num_comps; c++)
+				d += (pixelbuf_get_comp(block.m_pBuf, x, y, c) - block_mean[c]) * axis[c];
+
+			span[0] = basisu::minimum(span[0], d);
+			span[1] = basisu::maximum(span[1], d);
+		}
+	}
+
+	// degenerate span check
+	if ((span[0] + 1.0f) > span[1])
+	{
+		span[0] -= 0.5f;
+		span[1] += 0.5f;
+	}
+
+	for (uint32_t e = 0; e < 2; e++)
+	{
+		for (uint32_t c = 0; c < num_comps; c++)
+			pCEM_values[c * 2 + e] = clamp<float>(axis[c] * span[e] + block_mean[c], 0.0f, 255.0f);
+
+		if (num_comps == 3)
+		{
+			pCEM_values[3 * 2 + 0] = 0;
+			pCEM_values[3 * 2 + 1] = 255;
+		}
+	}
+}
+
+// 8 bytes - used for shortlist generation
+struct astc_unpacked_config
+{
+	uint8_t m_cem;
+	uint8_t m_grid_width;
+	uint8_t m_grid_height;
+	uint8_t m_endpoint_range;
+	
+	uint8_t m_weight_range;
+	uint8_t m_dual_plane;
+	uint8_t m_ccs_index;
+	uint8_t m_unused;
+};
+
+struct single_subset_shortlist_state
+{
+	uint32_t m_block_width, m_block_height;
+	uint32_t m_max_candidates;
+
+	uint32_t m_num_src_block_comps; // whether the original src block has 3 or 4 active components
+	bool m_src_is_luma_only;
+	bool m_should_include_dual_plane;
+
+	pixelbuf m_pbuf;
+
+	float m_block_pixels[PIXELBUF_SIZE_IN_FLOATS];
+
+	// 2x2 to 12x12, [h - 2][w - 2], lost high frequency pixel energy (SSE) for RGBA
+	float m_downsample_sse[11][11];
+
+	// 0=CEM  6: RGB Base+Scale       (3D PCA, zero RGB mean, A=255), CCS can be [0,2]
+	// 1=CEM  8: RGB Direct           (3D PCA, A=255), CCS can be [0,2]
+	// 2=CEM 10: RGB Base+Scale+Two A (3D PCA, zero RGB mean, A=direct), CCS can be [0,3]
+	// 3=CEM 12: RGBA Direct          (4D PCA, A=direct), CCS can be [0,3]
+
+	// Single Plane
+	// [CEM index][chan]
+	float m_sp_spans[cCEMTotalIndices][4];
+	float m_sp_slam_to_line_error[cCEMTotalIndices];
+	bool m_sp_valid[cCEMTotalIndices];
+
+	// Dual Plane
+	// [CEM index][CCS Index][chan]
+	float m_dp_spans[cCEMTotalIndices][4][4];
+	float m_dp_slam_to_line_error[cCEMTotalIndices][4];
+	bool m_dp_valid[cCEMTotalIndices][4];
+
+	// mean centered block stats
+	// unclamped Pearson, order RG, RB, GB, RA, GA, BA; beware if a channel is invalid it's 1 not 0
+	// normalized 3D or 4D (depending on if the block had alpha or not)
+	block_stats m_stats;
+
+	float m_best_sse[MAX_CANDIDATES];
+	astc_unpacked_config m_best_configs[MAX_CANDIDATES];
+};
+
+static void fit_and_measure(
+	const pixelbuf &block,
+	uint32_t num_comps,				// 3 (RGB only) or 4 (RGBA)
+	bool zero_centered,				// true => line through origin
+	float pOut_span[4],				// written for [0..num_chans-1]; rest untouched
+	float* pOut_ortho_error,		// slam to line error
+	block_stats *pOut_stats,
+	float pOut_ortho_error_rgba[4]) // per-channel slam to line error (only num_comps channels valid)
+{
+	float endpoints[8];
+	calc_initial_cem_endpoints(block, endpoints, num_comps, pOut_stats, zero_centered);
+
+	pOut_span[3] = 0.0f;
+
+	float endpoint_org[4] = { 0, 0, 0, 0 };
+	float endpoint_dir[4] = { 0, 0, 0, 0 };
+	for (uint32_t c = 0; c < num_comps; c++)
+	{
+		endpoint_org[c] = endpoints[c * 2 + 0];
+		endpoint_dir[c] = endpoints[c * 2 + 1] - endpoints[c * 2 + 0];
+		pOut_span[c] = endpoint_dir[c];  // signed; squared in quant formula
+	}
+
+	const float inv_dir_sq_len = 1.0f / (vec_get_squared_len(endpoint_dir, num_comps) + TINY_EPS);
+
+	float total_ortho_error_c[4] = { };
+
+	for (uint32_t y = 0; y < block.m_height; y++)
+	{
+		for (uint32_t x = 0; x < block.m_width; x++)
+		{
+			float d = 0.0f;
+			for (uint32_t c = 0; c < num_comps; c++)
+			{
+				float v = pixelbuf_get_comp(block.m_pBuf, x, y, c);
+				d += (v - endpoint_org[c]) * endpoint_dir[c];
+			}
+
+			const float dist_along = d * inv_dir_sq_len;
+						
+			for (uint32_t c = 0; c < num_comps; c++)
+			{
+				const float block_val = pixelbuf_get_comp(block.m_pBuf, x, y, c);
+				const float nearest_val = endpoint_dir[c] * dist_along + endpoint_org[c];
+				total_ortho_error_c[c] += square(block_val - nearest_val);
+			}
+		}
+	}
+
+	float total_ortho_error = 0;
+
+	for (uint32_t c = 0; c < num_comps; c++)
+		total_ortho_error += total_ortho_error_c[c];
+
+	*pOut_ortho_error = total_ortho_error;
+
+	if (pOut_ortho_error_rgba)
+		vec4_copy(pOut_ortho_error_rgba, total_ortho_error_c);
+}
+
+static void fit_and_measure_2D(
+	const pixelbuf& block, uint32_t chan_to_zero,
+	bool zero_centered,				// true => line through origin
+	float pOut_span[4],				// written for [0..num_chans-1]; rest untouched
+	float* pOut_ortho_error,
+	block_stats* pOut_stats,
+	float *pOut_ortho_error_rgba)
+{
+	float temp_pixels[PIXELBUF_SIZE_IN_FLOATS];
+	memcpy(temp_pixels, block.m_pBuf, PIXELBUF_SIZE_IN_FLOATS * sizeof(float));
+
+	pixelbuf temp_pbuf(block);
+	temp_pbuf.m_pBuf = temp_pixels;
+	
+	pixelbuf_set_comp_to_val(temp_pbuf, chan_to_zero, 0.0f);
+
+	const int num_chans = 3;
+	return fit_and_measure(temp_pbuf, num_chans, zero_centered, pOut_span, pOut_ortho_error, pOut_stats, pOut_ortho_error_rgba);
+}
+
+static void fit_and_measure_1D(const pixelbuf &block, uint32_t comp_index, float* pOut_span, float* pOut_slam_to_255_sse)
+{
+	float lo = 1e+30f, hi = -1e+30f, a_err = 0.0f;
+
+	for (uint32_t y = 0; y < block.m_height; y++)
+	{
+		for (uint32_t x = 0; x < block.m_width; x++)
+		{
+			const float v = pixelbuf_get_comp(block.m_pBuf, x, y, comp_index);
+
+			lo = basisu::minimum(lo, v);
+			hi = basisu::maximum(hi, v);
+
+			a_err += square(v - 255.0f);
+		}
+	}
+
+	if (pOut_span)
+		*pOut_span = hi - lo;
+
+	if (pOut_slam_to_255_sse)
+		*pOut_slam_to_255_sse = a_err;
+}
+
+#if 0
+static float compute_ac_energy_from_dct(uint32_t block_width, uint32_t block_height, float* pDCT)
+{
+	pixel
+	float total_energy = 0.0f;
+	for (uint32_t i = 1; i < num_texels; i++)
+	{
+		const float v = square(pDCT[i]);
+		pDCT[i] = v;
+		total_energy += v;
+	}
+
+	pDCT[0] = 0.0f;
+	return total_energy;
+}
+#endif
+
+#define BASISU_USE_ENERGY_PREFIX_SUM (1)
+typedef double prefix_sum_t; // probably not really necessary, but due to the amount of summation involved double is useful here (and still way faster than not using a prefix sum table)
+
+// Build 2D prefix sums over a row-major block of energies.
+// prefix[x + y * block_width] contains the sum over [0..x] x [0..y], inclusive.
+// Slams DC energy to 0, returns total_ac_energy.
+static inline prefix_sum_t prepare_dct_energy_prefix_table(uint32_t block_width, uint32_t block_height, float* pEnergy, prefix_sum_t* pPrefix)
+{
+	pixelbuf_set_comp(pEnergy, 0, 0, 0, 0.0f); // ignore DC
+
+	prefix_sum_t total_ac_energy = 0.0f;
+
+	for (uint32_t y = 0; y < block_height; y++)
+	{
+		prefix_sum_t row_sum = 0.0f;
+
+		for (uint32_t x = 0; x < block_width; x++)
+		{
+			const float ac_energy = pixelbuf_get_comp(pEnergy, x, y, 0);
+			
+			total_ac_energy += ac_energy;
+
+			row_sum += ac_energy;
+
+			const prefix_sum_t val_above = y ? pixelbuf_get_comp(pPrefix, x, y - 1, 0) : 0.0f;
+
+			pixelbuf_set_comp(pPrefix, x, y, 0, row_sum + val_above);
+		} // x
+	} // y
+
+	return total_ac_energy;
+}
+
+// Sum of the origin-anchored rectangle [0, grid_w) x [0, grid_h).
+static inline prefix_sum_t query_dct_energy_prefix_sum(uint32_t block_width, uint32_t block_height, const prefix_sum_t* pPrefix, uint32_t grid_w, uint32_t grid_h)
+{
+	BASISU_NOTE_UNUSED(block_width);
+	BASISU_NOTE_UNUSED(block_height);
+
+	assert((grid_w >= 1) && (grid_w <= block_width));
+	assert((grid_h >= 1) && (grid_h <= block_height));
+
+	return pixelbuf_get_comp(pPrefix, grid_w - 1, grid_h - 1, 0);
+}
+
+static inline prefix_sum_t compute_lost_dct_energy_prefix_sum(uint32_t block_width, uint32_t block_height, const prefix_sum_t* pEnergy_prefix_sum, uint32_t grid_w, uint32_t grid_h, prefix_sum_t total_ac_energy)
+{
+	const prefix_sum_t kept_energy = query_dct_energy_prefix_sum(block_width, block_height, pEnergy_prefix_sum, grid_w, grid_h);
+
+	return maximum<prefix_sum_t>(total_ac_energy - kept_energy, 0.0f);
+}
+
+static bool should_include_dual_plane(const single_subset_shortlist_state& shortlist_state, bool has_a)
+{
+	const block_stats& stats = shortlist_state.m_stats;
+
+	const float var_r = stats.m_covar[cCovarRR], var_g = stats.m_covar[cCovarGG], var_b = stats.m_covar[cCovarBB];
+	const bool has_r = (var_r != 0.0f), has_g = (var_g != 0.0f), has_b = (var_b != 0.0f);
+
+	const uint32_t total_active_chans = has_r + has_g + has_b + has_a;
+
+	if (total_active_chans <= 1)
+		return false;
+
+	//const float MIN_A_DP_CORR = .995f;
+	//const float MIN_RGB_DP_CORR = .985f;
+
+	const float MIN_A_DP_CORR = .9999f;
+	const float MIN_RGB_DP_CORR = .9999f;
+
+	if (has_a)
+	{
+		float min_corr_vs_a = basisu::minimum(basisu::minimum(fabsf(stats.m_corr[cCorrRA]), fabsf(stats.m_corr[cCorrGA])), fabsf(stats.m_corr[cCorrBA]));
+		if (min_corr_vs_a < MIN_A_DP_CORR)
+			return true;
+	}
+
+	const float rg_corr = fabs(stats.m_corr[cCorrRG]);
+	const float rb_corr = fabs(stats.m_corr[cCorrRB]);
+	const float gb_corr = fabs(stats.m_corr[cCorrGB]);
+
+	float min_p = basisu::minimum(rg_corr, rb_corr, gb_corr);
+
+	if (min_p < MIN_RGB_DP_CORR)
+		return true;
+
+	return false;
+}
+
+// num_comps==4 if block has alpha and cems 4/10/12 are valid
+// if num_comps=3 only stats for cem 0/6/8 are valid (if the block doesn't have alpha, it makes no sense to use cem 4/10/12)
+static void compute_block_metrics(single_subset_shortlist_state&state, uint32_t num_comps, const basist::astc_ldr_t::dct2f &dct, const single_subset_enc_context &ctx)
+{
+	float temp_buf[PIXELBUF_SIZE_IN_FLOATS];
+	memcpy(temp_buf, state.m_block_pixels, PIXELBUF_SIZE_IN_FLOATS * sizeof(float));
+	
+	pixelbuf temp_pbuf(state.m_block_width, state.m_block_height, temp_buf);
+
+	memset(state.m_sp_spans, 0, sizeof(state.m_sp_spans));
+	memset(state.m_sp_slam_to_line_error, 0, sizeof(state.m_sp_slam_to_line_error));
+	memset(state.m_sp_valid, 0, sizeof(state.m_sp_valid));
+
+	memset(state.m_dp_spans, 0, sizeof(state.m_dp_spans));
+	memset(state.m_dp_slam_to_line_error, 0, sizeof(state.m_dp_slam_to_line_error));
+	memset(state.m_dp_valid, 0, sizeof(state.m_dp_valid));
+
+	float chan_ranges[4], a_slam_to_255_sse = 0;
+
+	for (uint32_t c = 0; c < 4; c++)
+		fit_and_measure_1D(temp_pbuf, c, &chan_ranges[c], (c == 3) ? &a_slam_to_255_sse : nullptr);
+
+	// Single Plane
+	float cem6_ortho_error_rgba[4] = { };
+	float cem10_ortho_error_rgba[4] = { };
+
+	// CEM 6: RGB Base+Scale, A=always 255
+	fit_and_measure(temp_pbuf, 3, true, state.m_sp_spans[cCEM6], &state.m_sp_slam_to_line_error[cCEM6], nullptr, cem6_ortho_error_rgba);
+	state.m_sp_valid[cCEM6] = true;
+		
+	if (num_comps == 4)
+	{
+		// CEM 8: RGB Direct: A=always 255
+		fit_and_measure(temp_pbuf, 3, false, state.m_sp_spans[cCEM8], &state.m_sp_slam_to_line_error[cCEM8], nullptr, nullptr);
+		state.m_sp_valid[cCEM8] = true;
+
+		// CEM 10: RGB Base+Scale Plus Two A
+		fit_and_measure(temp_pbuf, 4, true, state.m_sp_spans[cCEM10], &state.m_sp_slam_to_line_error[cCEM10], nullptr, cem10_ortho_error_rgba);
+		state.m_sp_valid[cCEM10] = true;
+
+		// CEM 12: RGBA Direct - also general block stats
+		fit_and_measure(temp_pbuf, 4, false, state.m_sp_spans[cCEM12], &state.m_sp_slam_to_line_error[cCEM12], &state.m_stats, nullptr);
+		state.m_sp_valid[cCEM12] = true;
+	}
+	else
+	{
+		// CEM 8: RGB Direct: A=always 255
+		fit_and_measure(temp_pbuf, 3, false, state.m_sp_spans[cCEM8], &state.m_sp_slam_to_line_error[cCEM8], &state.m_stats, nullptr);
+		state.m_sp_valid[cCEM8] = true;
+	}
+
+	// Dual Plane
+	const float CEM6_10_DP_RGB_CHAN_ORTHO_WEIGHT = .25f;
+	
+	for (uint32_t comp = 0; comp < num_comps; comp++)
+	{
+		pixelbuf_swap_comp_with_alpha(temp_pbuf, comp);
+
+		// CCS must be [0,2] for CEM 6/8 dual plane (they can't encode alpha, it's set to 255)
+		if (comp < 3)
+		{
+			// CEM 6: Although R,G, or B are on a separate plane, they still share the same Scale factor. A=always 255.
+			state.m_dp_spans[cCEM6][comp][0] = state.m_sp_spans[cCEM6][0];
+			state.m_dp_spans[cCEM6][comp][1] = state.m_sp_spans[cCEM6][1];
+			state.m_dp_spans[cCEM6][comp][2] = state.m_sp_spans[cCEM6][2];
+			state.m_dp_slam_to_line_error[cCEM6][comp] = a_slam_to_255_sse;
+			for (uint32_t c = 0; c < 3; c++)
+			{
+				const float chan_weight = (c == comp) ? CEM6_10_DP_RGB_CHAN_ORTHO_WEIGHT : 1.0f;
+				state.m_dp_slam_to_line_error[cCEM6][comp] += cem6_ortho_error_rgba[c] * chan_weight;
+			}
+			state.m_dp_valid[cCEM6][comp] = true;
+
+			// CEM 8: RGB Direct, R,G,B on a separate plane, A=always 255.
+			// Source A has been rotated somewhere into RGB, so we zero that out, so it's only 2D PCA on the remaining channels on weight plane 0.
+			fit_and_measure_2D(temp_pbuf, comp, false, state.m_dp_spans[cCEM8][comp], &state.m_dp_slam_to_line_error[cCEM8][comp], nullptr, nullptr);
+			state.m_dp_spans[cCEM8][comp][comp] = 0; // force it to 0 in case forcing channel comp to 0 made the block entire solid, causing PCA to return the gray axis
+			
+			state.m_dp_spans[cCEM8][comp][3] = chan_ranges[comp];
+			std::swap(state.m_dp_spans[cCEM8][comp][3], state.m_dp_spans[cCEM8][comp][comp]);
+			state.m_dp_slam_to_line_error[cCEM8][comp] += a_slam_to_255_sse;
+			state.m_dp_valid[cCEM8][comp] = true;
+		}
+
+		if (num_comps == 4)
+		{
+			// CEM 10: RGB Base+Scale + Two A, R,G,B or A on a separate plane
+			if (comp == 3)
+			{
+				// This is just sp CEM6 + entirely separate alpha, so slam to line error is just RGB
+				state.m_dp_spans[cCEM10][comp][0] = state.m_sp_spans[cCEM6][0];
+				state.m_dp_spans[cCEM10][comp][1] = state.m_sp_spans[cCEM6][1];
+				state.m_dp_spans[cCEM10][comp][2] = state.m_sp_spans[cCEM6][2];
+				state.m_dp_spans[cCEM10][comp][3] = chan_ranges[3];
+				state.m_dp_slam_to_line_error[cCEM10][comp] = state.m_sp_slam_to_line_error[cCEM6];
+			}
+			else
+			{
+				// this is sp CEM10 but one RGB channel is given its own weight plane after encoding, so slam to line error is reduced a bit on the ccs channel
+				state.m_dp_spans[cCEM10][comp][0] = state.m_sp_spans[cCEM10][0];
+				state.m_dp_spans[cCEM10][comp][1] = state.m_sp_spans[cCEM10][1];
+				state.m_dp_spans[cCEM10][comp][2] = state.m_sp_spans[cCEM10][2];
+				state.m_dp_spans[cCEM10][comp][3] = state.m_sp_spans[cCEM10][3];
+				
+				state.m_dp_slam_to_line_error[cCEM10][comp] = 0.0f;
+				for (uint32_t c = 0; c < 4; c++)
+				{
+					const float chan_weight = (c == comp) ? CEM6_10_DP_RGB_CHAN_ORTHO_WEIGHT : 1.0f;
+					state.m_dp_slam_to_line_error[cCEM10][comp] += cem10_ortho_error_rgba[c] * chan_weight;
+				}
+			}
+			state.m_dp_valid[cCEM10][comp] = true;
+
+			// CEM 12: RGBA Direct, R,G,B, or A on a separate plane
+			fit_and_measure(temp_pbuf, 3, false, state.m_dp_spans[cCEM12][comp], &state.m_dp_slam_to_line_error[cCEM12][comp], nullptr, nullptr);
+			state.m_dp_spans[cCEM12][comp][3] = chan_ranges[comp];
+			std::swap(state.m_dp_spans[cCEM12][comp][3], state.m_dp_spans[cCEM12][comp][comp]);
+			state.m_dp_valid[cCEM12][comp] = true;
+		}
+
+		pixelbuf_swap_comp_with_alpha(temp_pbuf, comp);
+
+	} // comp
+
+	// Add slam alpha to 255 penalties for CEM6/CEM8.
+	state.m_sp_slam_to_line_error[cCEM6] += a_slam_to_255_sse;
+	state.m_sp_slam_to_line_error[cCEM8] += a_slam_to_255_sse;
+
+	assert(dct.cols() == (int)state.m_block_width);
+	assert(dct.rows() == (int)state.m_block_height);
+
+	// Apply Parseval's theorem to rapidly estimate SSE due to weight grid downsampling.
+	float dct_temp[12 * 12];
+		
+	for (uint32_t c = 0; c < num_comps; c++)
+		dct.forward(temp_buf + PIXELBUF_COMP_PITCH * c, PIXELBUF_ROW_PITCH, temp_buf + PIXELBUF_COMP_PITCH * c, PIXELBUF_ROW_PITCH, dct_temp);
+
+	memset(state.m_downsample_sse, 0, sizeof(state.m_downsample_sse));
+
+	for (uint32_t y = 0; y < state.m_block_height; y++)
+	{
+		for (uint32_t x = 0; x < state.m_block_width; x++)
+		{
+			float total_comp_energy = 0;
+			for (uint32_t c = 0; c < num_comps; c++)
+				total_comp_energy += square(pixelbuf_get_comp(temp_buf, x, y, c));
+			
+			pixelbuf_set_comp(temp_buf, x, y, 0, total_comp_energy);
+		} // x
+	} // y
+	
+#if	BASISU_USE_ENERGY_PREFIX_SUM
+	prefix_sum_t prefix_sum_buf[PIXELBUF_COMP_PITCH]; // only 1 component is used
+	const prefix_sum_t total_ac_energy = prepare_dct_energy_prefix_table(state.m_block_width, state.m_block_height, temp_buf, prefix_sum_buf);
+#endif
+				
+	for (uint32_t grid_h = 2; grid_h <= state.m_block_height; grid_h++)
+	{
+		for (uint32_t grid_w = 2; grid_w <= state.m_block_width; grid_w++)
+		{
+			// check for valid grid size
+			if ((grid_w * grid_h) > 64)
+				continue;
+						
+#if	BASISU_USE_ENERGY_PREFIX_SUM
+			const prefix_sum_t sse = compute_lost_dct_energy_prefix_sum(state.m_block_width, state.m_block_height, prefix_sum_buf, grid_w, grid_h, total_ac_energy);
+			state.m_downsample_sse[grid_h - 2][grid_w - 2] = (float)sse;
+#else
+			float sse = 0;
+			for (uint32_t y = 0; y < state.m_block_height; y++)
+			{
+				for (uint32_t x = 0; x < state.m_block_width; x++)
+				{
+					if ((y < grid_h) && (x < grid_w))
+						continue;
+					sse += pixelbuf_get_comp(temp_buf, x, y, 0);
+				} // x
+			} // y
+			state.m_downsample_sse[grid_h - 2][grid_w - 2] = sse;
+#endif // BASISU_USE_ENERGY_PREFIX_SUM
+
+		} // grid_w
+	} // grid_h
+
+	//----
+
+	assert(state.m_num_src_block_comps == num_comps);
+
+	if (state.m_num_src_block_comps == 3)
+	{
+		assert(state.m_stats.m_mean[3] == 0.0f);
+		assert(state.m_stats.m_covar[3] == 0.0f);
+	}
+	else
+	{
+		assert(state.m_stats.m_mean[3] < 255.0f);
+	}
+
+	state.m_should_include_dual_plane = false;
+	if (!ctx.m_disable_dual_plane)
+		state.m_should_include_dual_plane = should_include_dual_plane(state, num_comps == 4);
+}
+
+// ab_sum = (2.0f * float(w_levels) - 1.0f) / (3.0f * (float(w_levels) - 1.0f)), for [2,32] (entries 0,1 invalid)
+static const float g_ab_sum_tab[33] = // [0,32]
+{
+	0.0f, 0.0f, // 0-1 invalid
+	1.000000f, 0.833333f, 0.777778f, 0.750000f, // 2-32
+	0.733333f, 0.722222f, 0.714286f, 0.708333f,
+	0.703704f, 0.700000f, 0.696970f, 0.694444f,
+	0.692308f, 0.690476f, 0.688889f, 0.687500f,
+	0.686275f, 0.685185f, 0.684211f, 0.683333f,
+	0.682540f, 0.681818f, 0.681159f, 0.680556f,
+	0.680000f, 0.679487f, 0.679012f, 0.678571f,
+	0.678161f, 0.677778f, 0.677419f
+};
+
+// 1/(levels-1)
+static const float g_intervals_recip[257] = // [0,256]
+{
+	0.0f,      0.0f,      1.000000f, 0.500000f, 0.333333f, 0.250000f, 0.200000f, 0.166667f, // levels 0-1 invalid
+	0.142857f, 0.125000f, 0.111111f, 0.100000f, 0.090909f, 0.083333f, 0.076923f, 0.071429f,
+	0.066667f, 0.062500f, 0.058824f, 0.055556f, 0.052632f, 0.050000f, 0.047619f, 0.045455f,
+	0.043478f, 0.041667f, 0.040000f, 0.038462f, 0.037037f, 0.035714f, 0.034483f, 0.033333f,
+	0.032258f, 0.031250f, 0.030303f, 0.029412f, 0.028571f, 0.027778f, 0.027027f, 0.026316f,
+	0.025641f, 0.025000f, 0.024390f, 0.023810f, 0.023256f, 0.022727f, 0.022222f, 0.021739f,
+	0.021277f, 0.020833f, 0.020408f, 0.020000f, 0.019608f, 0.019231f, 0.018868f, 0.018519f,
+	0.018182f, 0.017857f, 0.017544f, 0.017241f, 0.016949f, 0.016667f, 0.016393f, 0.016129f,
+	0.015873f, 0.015625f, 0.015385f, 0.015152f, 0.014925f, 0.014706f, 0.014493f, 0.014286f,
+	0.014085f, 0.013889f, 0.013699f, 0.013514f, 0.013333f, 0.013158f, 0.012987f, 0.012821f,
+	0.012658f, 0.012500f, 0.012346f, 0.012195f, 0.012048f, 0.011905f, 0.011765f, 0.011628f,
+	0.011494f, 0.011364f, 0.011236f, 0.011111f, 0.010989f, 0.010870f, 0.010753f, 0.010638f,
+	0.010526f, 0.010417f, 0.010309f, 0.010204f, 0.010101f, 0.010000f, 0.009901f, 0.009804f,
+	0.009709f, 0.009615f, 0.009524f, 0.009434f, 0.009346f, 0.009259f, 0.009174f, 0.009091f,
+	0.009009f, 0.008929f, 0.008850f, 0.008772f, 0.008696f, 0.008621f, 0.008547f, 0.008475f,
+	0.008403f, 0.008333f, 0.008264f, 0.008197f, 0.008130f, 0.008065f, 0.008000f, 0.007937f,
+	0.007874f, 0.007812f, 0.007752f, 0.007692f, 0.007634f, 0.007576f, 0.007519f, 0.007463f,
+	0.007407f, 0.007353f, 0.007299f, 0.007246f, 0.007194f, 0.007143f, 0.007092f, 0.007042f,
+	0.006993f, 0.006944f, 0.006897f, 0.006849f, 0.006803f, 0.006757f, 0.006711f, 0.006667f,
+	0.006623f, 0.006579f, 0.006536f, 0.006494f, 0.006452f, 0.006410f, 0.006369f, 0.006329f,
+	0.006289f, 0.006250f, 0.006211f, 0.006173f, 0.006135f, 0.006098f, 0.006061f, 0.006024f,
+	0.005988f, 0.005952f, 0.005917f, 0.005882f, 0.005848f, 0.005814f, 0.005780f, 0.005747f,
+	0.005714f, 0.005682f, 0.005650f, 0.005618f, 0.005587f, 0.005556f, 0.005525f, 0.005495f,
+	0.005464f, 0.005435f, 0.005405f, 0.005376f, 0.005348f, 0.005319f, 0.005291f, 0.005263f,
+	0.005236f, 0.005208f, 0.005181f, 0.005155f, 0.005128f, 0.005102f, 0.005076f, 0.005051f,
+	0.005025f, 0.005000f, 0.004975f, 0.004950f, 0.004926f, 0.004902f, 0.004878f, 0.004854f,
+	0.004831f, 0.004808f, 0.004785f, 0.004762f, 0.004739f, 0.004717f, 0.004695f, 0.004673f,
+	0.004651f, 0.004630f, 0.004608f, 0.004587f, 0.004566f, 0.004545f, 0.004525f, 0.004505f,
+	0.004484f, 0.004464f, 0.004444f, 0.004425f, 0.004405f, 0.004386f, 0.004367f, 0.004348f,
+	0.004329f, 0.004310f, 0.004292f, 0.004274f, 0.004255f, 0.004237f, 0.004219f, 0.004202f,
+	0.004184f, 0.004167f, 0.004149f, 0.004132f, 0.004115f, 0.004098f, 0.004082f, 0.004065f,
+	0.004049f, 0.004032f, 0.004016f, 0.004000f, 0.003984f, 0.003968f, 0.003953f, 0.003937f,
+	0.003922f
+};
+
+// Note: This uses 1/w_levels, not 1/(w_levels - 1), for the effective weight quantization step.
+// Although the representable weights are spaced by 1/(w_levels - 1), the encoder
+// refits endpoints after weight selection, so low-level weight modes behave more
+// like scalar clustering with w_levels reconstruction bins. This is especially
+// important for 2-level weights, where the fixed-endpoint model overestimates
+// the weight error term by 4x.
+
+#if 1
+static inline float analytical_quant_est_sse(uint32_t astc_cem, uint32_t e_levels, uint32_t w_levels, const float spans[4], uint32_t num_pixels)
+{
+	assert((e_levels >= 2) && (e_levels <= 256) && (w_levels >= 2) && (w_levels <= 32));
+	assert(spans);
+	assert((astc_cem == 0) || (astc_cem == 4) || (astc_cem == 6) || (astc_cem == 8) || (astc_cem == 10) || (astc_cem == 12));
+
+	const float Dep = g_intervals_recip[e_levels]; // endpoint quant step
+	//const float Dw = g_intervals_recip[w_levels]; // weight quant step
+	const float Dw = g_intervals_recip[w_levels + 1]; // weight quant step, adjusted to approximately factor in endpoint LS fit (especially less pessimestic for 2-3 level configs)
+	const float ab_sum = g_ab_sum_tab[w_levels];
+
+	// sanity checks
+	assert(fabs(Dep - (1.0f / (float)(e_levels - 1))) < .000125f);
+	//assert(fabs(Dw - (1.0f / (float)(w_levels - 1))) < .000125f);
+	assert(fabs(Dw - (1.0f / (float)(w_levels))) < .000125f);
+	assert(fabs(ab_sum - (2.0f * float(w_levels) - 1.0f) / (3.0f * (float(w_levels) - 1.0f))) < .000125f);
+
+	//const int num_chans = ((astc_cem == 6) || (astc_cem == 8)) ? 3 : 4;
+	//const int num_chans = (astc_cem <= 8) ? 3 : 4;
+	const int num_chans = get_num_cem_chans(astc_cem);
+
+	// For num_chans = 3 we know the decoder always outputs 255 for Alpha, so there isn't any endpoint quant error there, and spans[3] should always be 0.
+	if (num_chans == 3)
+	{
+		assert(spans[3] == 0.0f);
+	}
+
+	// 5418.75f = ((255.0f * 255.0f) / 12.0f)
+	float pixel_sse = (e_levels == 256) ? 0.0f : ((Dep * Dep) * ab_sum * 5418.75f * (float)num_chans);
+
+	const float k = (Dw * Dw) * (1.0f / 12.0f);
+
+	float t = (spans[0] * spans[0] + spans[1] * spans[1] + spans[2] * spans[2]);
+	if (num_chans == 4) //(astc_cem > 8)
+		t += spans[3] * spans[3];
+
+	pixel_sse += k * t;
+
+	return pixel_sse * float(num_pixels);
+}
+#else
+static inline float analytical_quant_est_sse(uint32_t astc_cem, uint32_t e_levels, uint32_t w_levels, const float spans[4], uint32_t num_pixels)
+{
+	assert((e_levels >= 2) && (e_levels <= 256) && (w_levels >= 2) && (w_levels <= 32));
+	assert(spans);
+	assert((astc_cem == 0) || (astc_cem == 4) || (astc_cem == 6) || (astc_cem == 8) || (astc_cem == 10) || (astc_cem == 12));
+
+	const float Dep = 1.0f / (float)(e_levels - 1); // endpoint quant step
+	//const float Dw = 1.0f / (float)(w_levels - 1); // weight quant step
+	const float Dw = 1.0f / (float)(w_levels); // weight quant step, adjusted to approximately factor in endpoint LS fit (especially less pessimestic for 2-3 level configs)
+
+	assert(fabs(g_intervals_recip[w_levels + 1] - Dw) < TINY_EPS);
+
+	// TODO: precompute
+	const float N = float(w_levels);
+	const float ab_sum = (2.0f * N - 1.0f) / (3.0f * (N - 1.0f)); // simple model, assumes uniform weights, estimates how much endpoint quantization error survives after interpolation, averaged over all weight levels.
+
+	//const int num_chans = ((astc_cem == 6) || (astc_cem == 8)) ? 3 : 4;
+	const int num_chans = get_num_cem_chans(astc_cem);
+
+	// For num_chans = 3 we know the decoder always outputs 255 for Alpha, so there isn't any endpoint quant error there, and spans[3] should always be 0.
+	if (num_chans == 3)
+	{
+		assert(spans[3] == 0.0f);
+	}
+
+	float pixel_sse = (e_levels == 256) ? 0.0f : ((Dep * Dep) * ((1.0f / 12.0f) * ab_sum * (255.0f * 255.0f)) * (float)num_chans);
+
+	const float k = (Dw * Dw) * (1.0f / 12.0f);
+	for (int i = 0; i < num_chans; i++)
+		pixel_sse += k * (float)(spans[i] * spans[i]);
+
+	return pixel_sse * float(num_pixels);
+}
+#endif
+
+const float DEF_SCALE_WEIGHT = 1.0f; // 1.0-3.0 seems reasonable
+const float DEF_QUANT_WEIGHT = 1.0f;
+
+// Does NOT include downsample SSE.
+static inline float estimate_base_config_sse(single_subset_shortlist_state&state, const astc_unpacked_config& cfg)
+{
+	const uint32_t astc_cem = cfg.m_cem;
+	//assert((astc_cem >= 6) && (astc_cem <= 12));
+	assert(is_cem_0_or_4(astc_cem) || is_cem_6_or_10(astc_cem) || is_cem_8_or_12(astc_cem));
+
+	//float error = 0.0f;
+
+	uint32_t cem_index = 0; // = (astc_cem - 6) >> 1;
+	if (astc_cem >= 6)
+		cem_index = (astc_cem - 6) >> 1;
+	else if (astc_cem == 0)
+		cem_index = cCEM8;
+	else
+	{
+		assert(astc_cem == 4);
+		cem_index = cCEM12;
+	}
+
+	const int num_endpoint_levels = astc_helpers::get_ise_levels(cfg.m_endpoint_range);
+	const int num_weight_levels = astc_helpers::get_ise_levels(cfg.m_weight_range);
+
+	const bool dual_plane = cfg.m_dual_plane;
+	const uint32_t ccs_index = cfg.m_ccs_index;
+	
+	// sanity check, decoded A will always be 255 here, A span should be 0
+	if ((dual_plane) && (ccs_index == 3))
+	{
+		// forbidden combo, useless
+		assert((astc_cem != 0) && (astc_cem != 6) && (astc_cem != 8));
+	}
+
+	const int num_block_pixels = state.m_block_height * state.m_block_width;
+
+	// sanity check
+	assert(dual_plane ? state.m_dp_valid[cem_index][ccs_index] : state.m_sp_valid[cem_index]);
+
+	const float quant_error = analytical_quant_est_sse(astc_cem, num_endpoint_levels, num_weight_levels, 
+		dual_plane ? state.m_dp_spans[cem_index][ccs_index] : state.m_sp_spans[cem_index], num_block_pixels);
+
+	const float ortho_error = dual_plane ? state.m_dp_slam_to_line_error[cem_index][ccs_index] : state.m_sp_slam_to_line_error[cem_index];
+
+	return quant_error * DEF_QUANT_WEIGHT + ortho_error;
+}
+
+// also adds in downsample SSE
+static inline float estimate_full_config_sse(single_subset_shortlist_state& state, const astc_unpacked_config& cfg, float scale_weight)
+{
+	return state.m_downsample_sse[cfg.m_grid_height - 2][cfg.m_grid_width - 2] * scale_weight + estimate_base_config_sse(state, cfg);
+}
+
+static inline float estimate_full_config_sse(single_subset_shortlist_state& state, const astc_unpacked_config& cfg, float base_sse, float scale_weight)
+{
+	return state.m_downsample_sse[cfg.m_grid_height - 2][cfg.m_grid_width - 2] * scale_weight + base_sse;
+}
+
+[[maybe_unused]] static inline void unpack_config(astc_unpacked_config& cfg, uint32_t packed_config)
+{
+	cfg.m_cem = (uint8_t)extract_bits(packed_config, 0, 4);
+	cfg.m_grid_width = (uint8_t)extract_bits(packed_config, 4, 4);
+	cfg.m_grid_height = (uint8_t)extract_bits(packed_config, 8, 4);
+	cfg.m_endpoint_range = (uint8_t)extract_bits(packed_config, 12, 5);
+	cfg.m_weight_range = (uint8_t)extract_bits(packed_config, 17, 4);
+	cfg.m_dual_plane = (uint8_t)extract_bits(packed_config, 21, 1) != 0;
+	cfg.m_ccs_index = 0;
+	cfg.m_unused = 0;
+}
+
+static inline void estimate_and_add_config(
+	single_subset_shortlist_state& shortlist_state,
+	float sse, const astc_unpacked_config&cfg,
+	float &max_candidate_sse, uint32_t &num_candidates, uint32_t max_candidates)
+{
+	if (num_candidates < max_candidates)
+	{
+		assert((num_candidates >= 0) && (num_candidates < std::size(shortlist_state.m_best_configs)));
+
+		memcpy(&shortlist_state.m_best_configs[num_candidates], &cfg, sizeof(cfg));
+		shortlist_state.m_best_sse[num_candidates] = sse;
+
+		max_candidate_sse = maximum(max_candidate_sse, sse);
+
+		num_candidates++;
+		
+		return;
+	}
+
+	if (sse >= max_candidate_sse)
+		return;
+
+	assert(num_candidates == max_candidates);
+		
+	astc_unpacked_config worst_cfg(cfg);
+	float worst_sse = sse;
+
+	float prev_max_candidate_sse = max_candidate_sse;
+	max_candidate_sse = 0;
+
+	for (uint32_t i = 0; i < num_candidates; i++)
+	{
+		if (shortlist_state.m_best_sse[i] > worst_sse)
+		{
+			std::swap(shortlist_state.m_best_sse[i], worst_sse);
+			std::swap(shortlist_state.m_best_configs[i], worst_cfg);
+		}
+
+		max_candidate_sse = basisu::maximum<float>(max_candidate_sse, shortlist_state.m_best_sse[i]);
+	}
+
+	assert(worst_sse == prev_max_candidate_sse);
+	BASISU_NOTE_UNUSED(prev_max_candidate_sse);
+}
+
+static void init_single_subset_shortlist_state(
+	const rgba32_image& src_block_rgba32,
+	const single_subset_enc_context& context,
+	single_subset_shortlist_state& shortlist_state,
+	bool src_is_luma_only,
+	uint32_t num_src_block_comps)
+{
+	shortlist_state.m_block_width = context.m_block_width;
+	shortlist_state.m_block_height = context.m_block_height;
+	shortlist_state.m_max_candidates = context.m_max_candidates;
+	shortlist_state.m_num_src_block_comps = num_src_block_comps;
+	shortlist_state.m_src_is_luma_only = src_is_luma_only;
+
+	// TODO
+	pixelbuf& pbuf = shortlist_state.m_pbuf;
+	pbuf.m_width = context.m_block_width;
+	pbuf.m_height = context.m_block_height;
+	pbuf.m_pBuf = shortlist_state.m_block_pixels;
+
+	pixelbuf_load_block(pbuf, src_block_rgba32, 0, 0, num_src_block_comps);
+		
+	compute_block_metrics(shortlist_state, num_src_block_comps, context.m_dct, context);
+
+#if defined(DEBUG) || defined(_DEBUG)
+	{
+		float min_a = 255.0f;
+		for (uint32_t y = 0; y < pbuf.m_height; y++)
+			for (uint32_t x = 0; x < pbuf.m_width; x++)
+				min_a = minimum(min_a, pixelbuf_get_comp(pbuf.m_pBuf, x, y, 3));
+
+		if (min_a == 255.0f)
+		{
+			assert(num_src_block_comps == 3);
+		}
+		else
+		{
+			assert(num_src_block_comps == 4);
+		}
+	}
+#endif
+}
+
+static uint32_t generate_single_subset_shortlist(
+	uint32_t total_configs, const uint32_t *pPacked_configs,
+	const single_subset_enc_context &context, 
+	[[maybe_unused]] const rgba32_image &src_block_rgba32,
+	[[maybe_unused]] bool src_is_luma_only,
+	uint32_t num_src_block_comps, // 3 or 4, determined by caller
+	single_subset_shortlist_state& shortlist_state, 
+	float scale_weight, uint32_t max_candidates)
+{
+	static_assert(sizeof(astc_unpacked_config) == sizeof(uint64_t), "sizeof(astc_unpacked_config) != sizeof(uint64_t)");
+	assert((num_src_block_comps == 3) || (num_src_block_comps == 4));
+	assert(max_candidates <= MAX_CANDIDATES);
+
+	const bool has_alpha = (num_src_block_comps == 4);
+	const uint32_t num_actual_block_chans = has_alpha ? 4 : 3;
+					
+	uint32_t num_candidates = 0;
+	float max_candidate_sse = 0;
+
+	astc_unpacked_config cfg;
+	clear_obj(cfg);
+		
+	for (uint32_t id = 0; id < total_configs; id++)
+	{
+		const uint32_t packed_config = pPacked_configs[id];
+
+		cfg.m_cem = (uint8_t)extract_bits(packed_config, 0, 4);
+
+		if ((num_actual_block_chans < 4) && (does_cem_have_alpha(cfg.m_cem)))
+			continue;
+
+		cfg.m_grid_width = (uint8_t)extract_bits(packed_config, 4, 4);
+		cfg.m_grid_height = (uint8_t)extract_bits(packed_config, 8, 4);
+		
+		assert(cfg.m_grid_width >= cfg.m_grid_height);
+
+		const bool wh_flag = (cfg.m_grid_width <= context.m_block_width) && (cfg.m_grid_height <= context.m_block_height);
+		if (!wh_flag)
+		{
+			// hw_flag cannot be true, grid_width is >= grid_height, and block_width >= block_height, so swapping GW/GH isn't going to help.
+			assert(!((cfg.m_grid_height <= context.m_block_width) && (cfg.m_grid_width <= context.m_block_height)));
+			continue;
+		}
+		
+		const bool hw_flag = (cfg.m_grid_width != cfg.m_grid_height) && (cfg.m_grid_height <= context.m_block_width) && (cfg.m_grid_width <= context.m_block_height);
+
+		cfg.m_endpoint_range = (uint8_t)extract_bits(packed_config, 12, 5);
+		cfg.m_weight_range = (uint8_t)extract_bits(packed_config, 17, 4);
+		cfg.m_dual_plane = (uint8_t)extract_bits(packed_config, 21, 1) != 0;
+										
+#if 0
+		// HACK HACK
+		//if (!cfg.m_dual_plane)
+		//	continue;
+		if ((cfg.m_cem != 0) && (cfg.m_cem != 8) && (cfg.m_cem != 12))
+			continue;
+		//if (cfg.m_cem == 6)
+		//	continue;
+		//if (!cfg.m_dual_plane)
+		//	continue;
+#endif
+
+		if (cfg.m_dual_plane)
+		{
+			assert(cfg.m_cem != 0);
+
+			if (!shortlist_state.m_should_include_dual_plane)
+				continue;
+
+			uint32_t ccs_first = 0, ccs_last = num_actual_block_chans - 1;
+				
+			if (!does_cem_have_alpha(cfg.m_cem))
+			{
+				ccs_last = 2;
+			}
+			else if (cfg.m_cem == 4)
+			{
+				assert(num_actual_block_chans == 4);
+
+				ccs_first = 3;
+			}
+
+			if (hw_flag)
+			{
+				astc_unpacked_config alt_cfg(cfg);
+				std::swap(alt_cfg.m_grid_width, alt_cfg.m_grid_height);
+
+				for (uint32_t ccs_index = ccs_first; ccs_index <= ccs_last; ccs_index++)
+				{
+					cfg.m_ccs_index = (uint8_t)ccs_index;
+					
+					const float base_sse = estimate_base_config_sse(shortlist_state, cfg);
+
+					const float wh_sse = estimate_full_config_sse(shortlist_state, cfg, base_sse, scale_weight);
+					estimate_and_add_config(shortlist_state, wh_sse, cfg, max_candidate_sse, num_candidates, max_candidates);
+
+					alt_cfg.m_ccs_index = (uint8_t)ccs_index;
+					const float hw_sse = estimate_full_config_sse(shortlist_state, alt_cfg, base_sse, scale_weight);
+					estimate_and_add_config(shortlist_state, hw_sse, alt_cfg, max_candidate_sse, num_candidates, max_candidates);
+
+					assert(equal_abs_tol(wh_sse, estimate_full_config_sse(shortlist_state, cfg, scale_weight), TINY_EPS));
+					assert(equal_abs_tol(hw_sse, estimate_full_config_sse(shortlist_state, alt_cfg, scale_weight), TINY_EPS));
+				}
+			}
+			else
+			{
+				for (uint32_t ccs_index = ccs_first; ccs_index <= ccs_last; ccs_index++)
+				{
+					cfg.m_ccs_index = (uint8_t)ccs_index;
+					estimate_and_add_config(shortlist_state, estimate_full_config_sse(shortlist_state, cfg, scale_weight), cfg, max_candidate_sse, num_candidates, max_candidates);
+				}
+			}
+		}
+		else
+		{
+			cfg.m_ccs_index = 0;
+
+			if (hw_flag)
+			{
+				const float base_sse = estimate_base_config_sse(shortlist_state, cfg);
+				
+				const float wh_sse = estimate_full_config_sse(shortlist_state, cfg, base_sse, scale_weight);
+				assert(equal_abs_tol(wh_sse, estimate_full_config_sse(shortlist_state, cfg, scale_weight), TINY_EPS));
+				estimate_and_add_config(shortlist_state, wh_sse, cfg, max_candidate_sse, num_candidates, max_candidates);
+
+				std::swap(cfg.m_grid_width, cfg.m_grid_height);
+				const float hw_sse = estimate_full_config_sse(shortlist_state, cfg, base_sse, scale_weight);
+				assert(equal_abs_tol(hw_sse, estimate_full_config_sse(shortlist_state, cfg, scale_weight), TINY_EPS));
+				estimate_and_add_config(shortlist_state, hw_sse, cfg, max_candidate_sse, num_candidates, max_candidates);
+			}
+			else
+			{
+				estimate_and_add_config(shortlist_state, estimate_full_config_sse(shortlist_state, cfg, scale_weight), cfg, max_candidate_sse, num_candidates, max_candidates);
+			}
+		}
+
+	} // id
+
+	assert((num_candidates > 0) && (num_candidates <= max_candidates));
+
+	return num_candidates;
+}
+
+// Each subtable is the pseudoinverse of the corresponding 1D ASTC bilinear
+// upsample matrix T, computed as Tplus = inverse(transpose(T) * T) * transpose(T).
+// For a block size B and grid size G, the subtable contains B*G coefficients.
+// The natural matrix shape is Tplus[G][B] ([row][col]) (destination major), but the downsample coefficients are stored
+// transposed/source-major as filter[b * G + g] = Tplus[g][b].
+// T=1D ASTC bilinear upsample matrix
+// Tplus = (T^T * T) ^ -1 * T^T (T^T=T transposed)
+// Input:
+//   T: block_size x grid_size
+// Output:
+//   Tplus: grid_size x block_size
+static const float s_pseudoinverse_coeffs[1585] =
+{
+	#include "basisu_astc_ldr_pseudoinv_tab.inl"
+};
+
+// offsets into s_pseudoinverse_coeffs[] for each possible 1D downsampling scenario
+static int16_t g_pseudoinverse_coeff_offsets[9][11] = // [block_size-4][grid_size-2]
+{
+	// dest grid width range: 2-12												source block texel size
+	{ 0,   8,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1, -1 },    					// 4
+	{ 20,  30,  45,  -1,  -1,  -1,  -1,  -1,  -1,  -1, -1 },   					// 5
+	{ 65,  77,  95,  119,  -1,  -1,  -1,  -1,  -1,  -1, -1 },   				// 6
+	{ -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1, -1 },   					// 7
+	{ 149,  165,  189,  221,  261,  309,  -1,  -1,  -1,  -1, -1 },     			// 8
+	{ -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1, -1 },  					// 9
+	{ 365,  385,  415,  455,  505,  565,  635,  715,  -1,  -1, -1 },  			// 10
+	{ -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1, -1 },  					// 11
+	{ 805,  829,  865,  913,  973,  1045,  1129,  1225,  1333,  1453, -1 }  	// 12
+};
+
+static void pseudoinverse_block_to_grid(const pixelbuf& src, pixelbuf& dst, uint32_t num_comps)
+{
+	assert((num_comps == 1) || (num_comps == 3) || (num_comps == 4));
+
+	const uint32_t src_width = src.m_width, src_height = src.m_height;
+	const uint32_t dst_width = dst.m_width, dst_height = dst.m_height;
+
+	// sanity checks
+	assert(&src != &dst);
+
+	assert((src_width >= 4) && (src_width <= 12));
+	assert((src_height >= 4) && (src_height <= 12));
+
+	assert((src_width != 7) && (src_width != 9) && (src_width != 11));
+	assert((src_height != 7) && (src_height != 9) && (src_height != 11));
+
+	assert((dst_width >= 2) && (dst_width <= src_width));
+	assert((dst_height >= 2) && (dst_height <= src_height));
+	assert((dst_width * dst_height) <= 64); // ASTC max grid size limitation
+
+	if ((src_width == dst_width) && (src_height == dst_height))
+	{
+		memcpy(dst.m_pBuf, src.m_pBuf, PIXELBUF_SIZE_IN_FLOATS * sizeof(float));
+		return;
+	}
+
+	const int h_ofs = g_pseudoinverse_coeff_offsets[src_width - 4][dst_width - 2]; // across X (left/right)
+	assert((h_ofs < 0) || (h_ofs + src_width * dst_width) <= std::size(s_pseudoinverse_coeffs));
+	const float* pH_coeffs = (h_ofs < 0) ? nullptr : &s_pseudoinverse_coeffs[h_ofs];
+
+	const int v_ofs = g_pseudoinverse_coeff_offsets[src_height - 4][dst_height - 2]; // across Y (up/down)
+	assert((v_ofs < 0) || (v_ofs + src_height * dst_height) <= std::size(s_pseudoinverse_coeffs));
+	const float* pV_coeffs = (v_ofs < 0) ? nullptr : &s_pseudoinverse_coeffs[v_ofs];
+
+	float temp_buf[12][4]; // src_width x 1
+
+	// TODO: compute # of ops to do vertical vs. horizontal first, use minimum
+	for (uint32_t dst_y = 0; dst_y < dst_height; dst_y++)
+	{
+		// first filter vertically, outputs src_width x 1 samples
+		if (src_height == dst_height)
+		{
+			assert(v_ofs == -1);
+
+			for (uint32_t src_x = 0; src_x < src_width; src_x++)
+			{
+				for (uint32_t c = 0; c < num_comps; c++)
+					temp_buf[src_x][c] = pixelbuf_get_comp(src.m_pBuf, src_x, dst_y, c);
+			} // src_x
+		}
+		else
+		{
+			assert(v_ofs != -1);
+
+			if (num_comps == 1)
+			{
+				for (uint32_t src_x = 0; src_x < src_width; src_x++)
+				{
+					float r = 0;
+
+					for (uint32_t src_y = 0; src_y < src_height; src_y++)
+					{
+						const float w = pV_coeffs[src_y * dst_height + dst_y];
+
+						r += pixelbuf_get_comp(src.m_pBuf, src_x, src_y, 0) * w;
+					}
+
+					temp_buf[src_x][0] = r;
+				}
+			}
+			else
+			{
+				for (uint32_t src_x = 0; src_x < src_width; src_x++)
+				{
+					float r = 0, g = 0, b = 0, a = 0;
+
+					for (uint32_t src_y = 0; src_y < src_height; src_y++)
+					{
+						const float w = pV_coeffs[src_y * dst_height + dst_y];
+
+						r += pixelbuf_get_comp(src.m_pBuf, src_x, src_y, 0) * w;
+						g += pixelbuf_get_comp(src.m_pBuf, src_x, src_y, 1) * w;
+						b += pixelbuf_get_comp(src.m_pBuf, src_x, src_y, 2) * w;
+						if (num_comps == 4)
+							a += pixelbuf_get_comp(src.m_pBuf, src_x, src_y, 3) * w;
+					}
+
+					temp_buf[src_x][0] = r;
+					temp_buf[src_x][1] = g;
+					temp_buf[src_x][2] = b;
+					if (num_comps == 4)
+						temp_buf[src_x][3] = a;
+				}
+			}
+		}
+
+		// input is now src_width x 1 in temp_buf
+		// filter horizontally, outputs dst_width x 1
+		if (src_width == dst_width)
+		{
+			assert(h_ofs == -1);
+
+			for (uint32_t dst_x = 0; dst_x < dst_width; dst_x++)
+			{
+				for (uint32_t c = 0; c < num_comps; c++)
+					pixelbuf_set_comp(dst.m_pBuf, dst_x, dst_y, c, temp_buf[dst_x][c]);
+			} // dst_x
+		}
+		else
+		{
+			assert(h_ofs != -1);
+
+			if (num_comps == 1)
+			{
+				for (uint32_t dst_x = 0; dst_x < dst_width; dst_x++)
+				{
+					float r = 0;
+
+					for (uint32_t src_x = 0; src_x < src_width; src_x++)
+					{
+						const float w = pH_coeffs[src_x * dst_width + dst_x];
+
+						r += temp_buf[src_x][0] * w;
+
+					} // src_x
+
+					pixelbuf_set_comp(dst.m_pBuf, dst_x, dst_y, 0, r);
+
+				} // dst_x
+			}
+			else
+			{
+				for (uint32_t dst_x = 0; dst_x < dst_width; dst_x++)
+				{
+					float r = 0, g = 0, b = 0, a = 0;
+
+					for (uint32_t src_x = 0; src_x < src_width; src_x++)
+					{
+						const float w = pH_coeffs[src_x * dst_width + dst_x];
+
+						r += temp_buf[src_x][0] * w;
+						g += temp_buf[src_x][1] * w;
+						b += temp_buf[src_x][2] * w;
+						if (num_comps == 4)
+							a += temp_buf[src_x][3] * w;
+
+					} // src_x
+
+					pixelbuf_set_comp(dst.m_pBuf, dst_x, dst_y, 0, r);
+					pixelbuf_set_comp(dst.m_pBuf, dst_x, dst_y, 1, g);
+					pixelbuf_set_comp(dst.m_pBuf, dst_x, dst_y, 2, b);
+					if (num_comps == 4)
+						pixelbuf_set_comp(dst.m_pBuf, dst_x, dst_y, 3, a);
+
+				} // dst_x
+			}
+		}
+	} // dst_y
+}
+
+// pEndpoints[8] decode order two vec4F's = LR LG LB LA, HR HG HB HA
+// pCEM_vals_orig[] in rank space (not ISE)
+static void cem_decode(uint32_t cem, const uint8_t pCEM_vals_quantized[8], uint32_t endpoint_range, float pEndpoints[8], float *pActual_scale)
+{
+	assert(astc_helpers::is_cem_ldr(cem));
+
+	const uint8_t* pCEM_vals = pCEM_vals_quantized;
+
+	uint8_t dequantized_cem_vals[8]; // ISE 20
+
+	if (endpoint_range < astc_helpers::BISE_256_LEVELS)
+	{
+		pCEM_vals = dequantized_cem_vals;
+		
+		const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(endpoint_range);
+		
+		const uint32_t num_vals = astc_helpers::get_num_cem_values(cem);
+		for (uint32_t i = 0; i < num_vals; i++)
+			dequantized_cem_vals[i] = (uint8_t)endpoint_tab.get_rank_to_val(pCEM_vals_quantized[i]);
+	}
+
+	int v0 = pCEM_vals[0], v1 = pCEM_vals[1], v2 = pCEM_vals[2], v3 = pCEM_vals[3];
+
+	switch (cem)
+	{
+	case 0:
+	case 4:
+	{
+		// 0 or 4
+		pEndpoints[0] = (float)v0;
+		pEndpoints[1] = (float)v0;
+		pEndpoints[2] = (float)v0;
+		pEndpoints[3] = 0xFF;
+
+		pEndpoints[4] = (float)v1;
+		pEndpoints[5] = (float)v1;
+		pEndpoints[6] = (float)v1;
+		pEndpoints[7] = 0xFF;
+
+		if (cem == 4)
+		{
+			pEndpoints[3] = (float)v2;
+			pEndpoints[7] = (float)v3;
+		}
+
+		break;
+	}
+	case 6:
+	case 10:
+	{
+		// 6 or 10
+		pEndpoints[0] = (float)((v0 * v3) >> 8);
+		pEndpoints[1] = (float)((v1 * v3) >> 8);
+		pEndpoints[2] = (float)((v2 * v3) >> 8);
+		pEndpoints[3] = 0xFF;
+
+		pEndpoints[4] = (float)v0;
+		pEndpoints[5] = (float)v1;
+		pEndpoints[6] = (float)v2;
+		pEndpoints[7] = 0xFF;
+
+		if (cem == 10)
+		{
+			pEndpoints[3] = pCEM_vals[4];
+			pEndpoints[7] = pCEM_vals[5];
+		}
+
+		if (pActual_scale)
+			*pActual_scale = (float)v3 * (1.0f / 256.0f);
+
+		break;
+	}
+	case 8:
+	case 12:
+	{
+		// 8 or 12
+		int v4 = pCEM_vals[4], v5 = pCEM_vals[5], v6 = 255, v7 = 255;
+
+		if (cem == 12)
+		{
+			v6 = pCEM_vals[6];
+			v7 = pCEM_vals[7];
+		}
+
+		if ((v1 + v3 + v5) >= (v0 + v2 + v4))
+		{
+			pEndpoints[0] = (float)v0;
+			pEndpoints[1] = (float)v2;
+			pEndpoints[2] = (float)v4;
+			pEndpoints[3] = (float)v6;
+
+			pEndpoints[4] = (float)v1;
+			pEndpoints[5] = (float)v3;
+			pEndpoints[6] = (float)v5;
+			pEndpoints[7] = (float)v7;
+		}
+		else
+		{
+			astc_helpers::blue_contract(v0, v2, v4);
+			astc_helpers::blue_contract(v1, v3, v5);
+
+			pEndpoints[0] = (float)v1;
+			pEndpoints[1] = (float)v3;
+			pEndpoints[2] = (float)v5;
+			pEndpoints[3] = (float)v7;
+
+			pEndpoints[4] = (float)v0;
+			pEndpoints[5] = (float)v2;
+			pEndpoints[6] = (float)v4;
+			pEndpoints[7] = (float)v6;
+		}
+
+		if (pActual_scale)
+			*pActual_scale = 0;
+
+		break;
+	}
+	default:
+	{
+		assert(cem <= 13); // LDR check
+
+		int dec_endpoints[4][2]; // [c][l/h]
+		astc_helpers::decode_endpoint(cem, dec_endpoints, pCEM_vals);
+
+		pEndpoints[0] = (float)dec_endpoints[0][0];
+		pEndpoints[1] = (float)dec_endpoints[1][0];
+		pEndpoints[2] = (float)dec_endpoints[2][0];
+		pEndpoints[3] = (float)dec_endpoints[3][0];
+
+		pEndpoints[4] = (float)dec_endpoints[0][1];
+		pEndpoints[5] = (float)dec_endpoints[1][1];
+		pEndpoints[6] = (float)dec_endpoints[2][1];
+		pEndpoints[7] = (float)dec_endpoints[3][1];
+		
+		if (pActual_scale)
+			*pActual_scale = 0;
+
+		break;
+	}
+	}
+
+#if defined(DEBUG) || defined(_DEBUG)
+	{
+		// sanity check
+		int dec_endpoints[4][2]; // [c][l/h]
+		astc_helpers::decode_endpoint(cem, dec_endpoints, pCEM_vals);
+		for (uint32_t i = 0; i < 4; i++)
+		{
+			assert((float)dec_endpoints[i][0] == pEndpoints[i]);
+			assert((float)dec_endpoints[i][1] == pEndpoints[4 + i]);
+		}
+	}
+#endif
+}
+
+// values in decoded L RGBA H RGBA order (not cem encode order)
+static float calc_shortest_endpoint_dist(const float a[8], const float b[8], uint32_t num_comps)
+{
+	assert((num_comps == 3) || (num_comps == 4));
+	float dist0, dist1;
+
+	if (num_comps == 3)
+	{
+		dist0 = vec3_squared_dist(a + 0, b + 0) + vec3_squared_dist(a + 4, b + 4);
+		dist1 = vec3_squared_dist(a + 0, b + 4) + vec3_squared_dist(a + 4, b + 0);
+	}
+	else
+	{
+		dist0 = vec4_squared_dist(a + 0, b + 0) + vec4_squared_dist(a + 4, b + 4);
+		dist1 = vec4_squared_dist(a + 0, b + 4) + vec4_squared_dist(a + 4, b + 0);
+	}
+
+	return minimum(dist0, dist1);
+}
+
+static inline int quant_endpoint_val_to_rank(float value, uint32_t range, uint32_t num_levels)
+{
+	(void)range;
+	
+	if (basisu::is_pow2(num_levels))
+	{
+		return clamp<int>((int)(value * (1.0f / 255.0f) * (num_levels - 1) + 0.5f), 0, num_levels - 1);
+	}
+	else
+	{
+		// TODO: Compute optimal rounding tables
+		value = clamp(value, 0.0f, 255.0f);
+		int v = (int)value;
+		
+		const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(range);
+		
+		int r0 = endpoint_tab.get_val_to_rank(v);
+		float v0 = fabs((float)endpoint_tab.get_rank_to_val(r0) - value);
+
+		int rp = minimum<int>(r0 + 1, num_levels - 1);
+		float vp = fabs((float)endpoint_tab.get_rank_to_val(rp) - value);
+
+		return (v0 < vp) ? r0 : rp;
+	}
+}
+
+// returns [0,255]
+static inline int dequant_endpoint_rank_to_val(uint32_t rank, uint32_t range)
+{
+	return astc_helpers::g_dequant_tables.get_endpoint_tab(range).get_rank_to_val(rank);
+}
+
+// returns [0,64]
+[[maybe_unused]] static inline int dequant_weight_rank_to_val(uint32_t rank, uint32_t range)
+{
+	assert((range <= astc_helpers::BISE_32_LEVELS) || (range == astc_helpers::BISE_64_LEVELS));
+
+	if (range == astc_helpers::BISE_64_LEVELS)
+		return rank;
+	else
+		return astc_helpers::g_dequant_tables.get_weight_tab(range).get_rank_to_val(rank);
+}
+
+[[maybe_unused]] static inline int apply_delta_to_rank_value(int rank_val, int delta, uint32_t num_levels)
+{
+	return clamp<int>(rank_val + delta, 0, num_levels - 1);
+}
+
+static inline int compute_endpoint_sum(const uint8_t pCEM_values[6], uint32_t endpoint_ise_range)
+{
+	const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(endpoint_ise_range);
+
+	int sum = 0;
+
+	for (uint32_t i = 0; i < 6; i++)
+	{
+		int v = endpoint_tab.get_rank_to_val(pCEM_values[i]);
+		
+		if ((i & 1) == 0) // low endpoints subtract
+			v = -v;
+
+		sum += v;
+	}
+
+	return sum;
+}
+
+static inline void cem_bc_encode(uint32_t cem_index, uint32_t endpoint_ise_index, uint8_t pCEM_values[8], uint32_t num_levels, bool use_bc)
+{
+	assert((cem_index == 8) || (cem_index == 9) || (cem_index == 12) || (cem_index == 13));
+
+	int cur_sum = compute_endpoint_sum(pCEM_values, endpoint_ise_index);
+	if (cur_sum == 0)
+	{
+		for (uint32_t i = 0; i < 6; i++)
+		{
+			int dir = (i & 1) ? 1 : -1;
+
+			int cur_r = pCEM_values[i];
+			int new_r = clamp<int>(cur_r + dir, 0, num_levels - 1);
+			if (new_r == cur_r)
+				continue;
+
+			pCEM_values[i] = (uint8_t)new_r;
+
+			cur_sum = compute_endpoint_sum(pCEM_values, endpoint_ise_index);
+			if (cur_sum != 0)
+				break;
+		}
+	}
+
+	assert(cur_sum != 0);
+
+	bool cur_bc = cur_sum < 0;
+
+	if (cur_bc != use_bc)
+	{
+		const uint32_t num_comps = (cem_index >= 12) ? 4 : 3;
+
+		for (uint32_t i = 0; i < num_comps; i++)
+			std::swap(pCEM_values[2 * i + 0], pCEM_values[2 * i + 1]);
+	}
+
+#if defined(DEBUG) || defined(_DEBUG)
+	{
+		uint8_t cem_ise_vals[8];
+		for (uint32_t i = 0; i < astc_helpers::get_num_cem_values(cem_index); i++)
+			cem_ise_vals[i] = astc_helpers::g_dequant_tables.get_endpoint_tab(endpoint_ise_index).m_rank_to_ISE[pCEM_values[i]];
+
+		const bool check_bc = astc_helpers::used_blue_contraction(cem_index, cem_ise_vals, endpoint_ise_index);
+		// should never happen, but if it does, it's not fatal (hurts quality especially in single shot mode)
+		assert(check_bc == use_bc);
+	}
+#endif
+}
+
+// pEndpoints[] = ASTC direct order: LR HR LG HG LB HB LA HA
+// true if BC was used
+static bool encode_cem_8_12(uint32_t cem_index, uint8_t* pCEM_values, uint32_t endpoint_ise_range, const float pEndpoints[8], bool allow_bc)
+{
+	const uint32_t num_endpoint_levels = astc_helpers::get_ise_levels(endpoint_ise_range);
+	const uint32_t num_endpoint_vals = (cem_index >= 12) ? 8 : 6;
+
+	// don't bother with BC if 256 levels, pointless
+	bool use_bc = allow_bc && (num_endpoint_levels < 256);
+	float enc_endpoints[8];
+		
+	if (use_bc)
+	{
+		for (uint32_t i = 0; i < 2; i++)
+		{
+			float r = pEndpoints[0 + i], g = pEndpoints[2 + i], b = pEndpoints[4 + i];
+
+			r = r * 2 - b;
+			g = g * 2 - b;
+
+			float clamped_r = clamp<float>(r, 0, 255);
+			float clamped_g = clamp<float>(g, 0, 255);
+
+			if ((r != clamped_r) || (g != clamped_g))
+			{
+				use_bc = false;
+				break;
+			}
+
+			enc_endpoints[0 + i] = clamped_r;
+			enc_endpoints[2 + i] = clamped_g;
+			enc_endpoints[4 + i] = b;
+		}
+	}
+
+	if (!use_bc)
+		vec_copy(enc_endpoints, pEndpoints, 6);
+
+	if (cem_index == 12)
+	{
+		enc_endpoints[6] = pEndpoints[6];
+		enc_endpoints[7] = pEndpoints[7];
+	}
+
+	for (uint32_t i = 0; i < num_endpoint_vals; i++)
+		pCEM_values[i] = (uint8_t)quant_endpoint_val_to_rank(enc_endpoints[i], endpoint_ise_range, num_endpoint_levels);
+
+	cem_bc_encode(cem_index, endpoint_ise_range, pCEM_values, num_endpoint_levels, use_bc);
+	
+	return use_bc;
+}
+
+// pEndpoints[] = ASTC direct order: LR HR LG HG LB HB LA HA
+// scale derived from L endpoint
+static void encode_cem_6_10(uint32_t cem_index, uint8_t* pCEM_values, uint32_t endpoint_ise_range, const float pEndpoints[8])
+{
+	assert((cem_index == 6) || (cem_index == 10));
+
+	const uint32_t num_endpoint_levels = astc_helpers::get_ise_levels(endpoint_ise_range);
+		
+	float h[3];
+
+	for (uint32_t c = 0; c < 3; c++)
+	{
+		pCEM_values[c] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[c * 2 + 1], endpoint_ise_range, num_endpoint_levels);
+
+		h[c] = (float)dequant_endpoint_rank_to_val(pCEM_values[c], endpoint_ise_range);
+	}
+
+	float l[3] = { pEndpoints[0], pEndpoints[2], pEndpoints[4] };
+
+	float hh_dot = vec3_dot(h, h);
+	float lh_dot = vec3_dot(l, h);
+	
+	float scale = (256.0f * lh_dot) / (hh_dot + TINY_EPS);
+
+	pCEM_values[3] = (uint8_t)quant_endpoint_val_to_rank(scale, endpoint_ise_range, num_endpoint_levels);
+	
+	if (cem_index == 10)
+	{
+		pCEM_values[4] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[6], endpoint_ise_range, num_endpoint_levels);
+		pCEM_values[5] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[7], endpoint_ise_range, num_endpoint_levels);
+	}
+}
+
+// pEndpoints[] = ASTC direct order: LR HR LG HG LB HB LA HA
+static void encode_cem_0_4(uint32_t cem_index, uint8_t* pCEM_values, uint32_t endpoint_ise_range, const float pEndpoints[8])
+{
+	assert((cem_index == 0) || (cem_index == 4));
+
+	const uint32_t num_endpoint_levels = astc_helpers::get_ise_levels(endpoint_ise_range);
+			
+	pCEM_values[0] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[0], endpoint_ise_range, num_endpoint_levels);
+	pCEM_values[1] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[1], endpoint_ise_range, num_endpoint_levels);
+
+	if (cem_index == 4)
+	{
+		pCEM_values[2] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[6], endpoint_ise_range, num_endpoint_levels);
+		pCEM_values[3] = (uint8_t)quant_endpoint_val_to_rank(pEndpoints[7], endpoint_ise_range, num_endpoint_levels);
+	}
+}
+
+// pEndpoints[] = ASTC direct order: LR HR LG HG LB HB LA HA
+// returns false if base+ofs encode clamped
+static bool encode_cem_9_13(uint32_t cem_index, uint8_t* pCEM_values, uint32_t endpoint_ise_range, const float pEndpoints[8], bool allow_bc)
+{
+	assert(is_cem_9_or_13(cem_index));
+
+	const uint32_t num_chans = get_num_cem_chans(cem_index);
+
+	basist::color_rgba e[2];
+	
+	e[0].a = 255;
+	e[1].a = 255;
+
+	for (uint32_t c = 0; c < num_chans; c++)
+	{
+		e[0][c] = (uint8_t)clamp<int>(fast_roundf_int(pEndpoints[c * 2 + 0]), 0, 255);
+		e[1][c] = (uint8_t)clamp<int>(fast_roundf_int(pEndpoints[c * 2 + 1]), 0, 255);
+	} // c
+
+	bool bc_clamped_flag, base_ofs_clamped_flag, endpoints_swapped_flag;
+			
+	bool status = basist::astc_ldr_t::pack_base_offset(cem_index, endpoint_ise_range, pCEM_values, e[0], e[1], allow_bc, true, bc_clamped_flag, base_ofs_clamped_flag, endpoints_swapped_flag);
+	BASISU_NOTE_UNUSED(status);
+	assert(status);
+	
+	return !base_ofs_clamped_flag;
+}
+
+// source pEndpoints[] = ASTC direct order: LR HR LG HG LB HB LA HA
+// output are quantized ASTC CEM values (rank space)
+// true is CEM specific, for 9/13 it means the base+ofs pack didn't clamp
+bool cem_encode(uint32_t cem_index, const float pEndpoints[8], uint32_t endpoint_ise_range, uint8_t* pCEM_values, bool allow_bc, bool high_effort)
+{
+	bool status = true;
+
+	switch (cem_index)
+	{
+	case 0:
+	case 4:
+	{
+		encode_cem_0_4(cem_index, pCEM_values, endpoint_ise_range, pEndpoints);
+		break;
+	}
+	case 6:
+	case 10:
+	{
+		encode_cem_6_10(cem_index, pCEM_values, endpoint_ise_range, pEndpoints);
+		break;
+	}
+	case 8:
+	case 12:
+	{
+		const bool used_bc = encode_cem_8_12(cem_index, pCEM_values, endpoint_ise_range, pEndpoints, allow_bc);
+
+		if ((high_effort) && (used_bc))
+		{
+			// not necessary - small to tiny gain
+			float ep_bc[8];
+			cem_decode(cem_index, pCEM_values, endpoint_ise_range, ep_bc, nullptr);
+
+			uint8_t CEM_values_plain[8];
+
+			const bool used_bc2 = encode_cem_8_12(cem_index, CEM_values_plain, endpoint_ise_range, pEndpoints, false);
+			BASISU_NOTE_UNUSED(used_bc2);
+			assert(!used_bc2);
+
+			float ep_plain[8];
+			cem_decode(cem_index, CEM_values_plain, endpoint_ise_range, ep_plain, nullptr);
+
+			const uint32_t num_comps = get_num_cem_chans(cem_index);
+
+			float desired_endpoints[8];
+			
+			for (uint32_t c = 0; c < num_comps; c++)
+			{
+				desired_endpoints[c + 0] = pEndpoints[c * 2 + 0];
+				desired_endpoints[c + 4] = pEndpoints[c * 2 + 1];
+			}
+
+			const float dist_bc = calc_shortest_endpoint_dist(desired_endpoints, ep_bc, num_comps);
+			const float dist_plain = calc_shortest_endpoint_dist(desired_endpoints, ep_plain, num_comps);
+
+			if (dist_plain < dist_bc)
+			{
+				memcpy(pCEM_values, CEM_values_plain, astc_helpers::get_num_cem_values(cem_index));
+			}
+		}
+
+		break;
+	}
+	case 9:
+	case 13:
+	{
+		status = encode_cem_9_13(cem_index, pCEM_values, endpoint_ise_range, pEndpoints, allow_bc);
+		break;
+	}
+	default:
+	{
+		assert(0);
+		memset(pCEM_values, 0, astc_helpers::get_num_cem_values(cem_index));
+		status = false;
+		break;
+	}
+	}
+
+	return status;
+}
+
+static void eval_weights_first_plane(
+	const pixelbuf& pixels, 
+	uint8_t* pWeights, uint32_t weight_ise_range, 
+	uint32_t cem, const uint8_t *pCEM_values, uint32_t endpoint_ise_range,
+	uint32_t num_comps,
+	int chan_to_swap = -1)
+{
+	assert((num_comps == 3) || (num_comps == 4));
+	
+	float dec_endpoints[8];
+	cem_decode(cem, pCEM_values, endpoint_ise_range, dec_endpoints, nullptr);
+
+	if (chan_to_swap >= 0)
+	{
+		assert(chan_to_swap <= 3);
+		std::swap(dec_endpoints[0 + chan_to_swap], dec_endpoints[0 + 3]);
+		std::swap(dec_endpoints[4 + chan_to_swap], dec_endpoints[4 + 3]);
+	}
+
+	float dir[4] = {
+		dec_endpoints[4] - dec_endpoints[0],
+		dec_endpoints[5] - dec_endpoints[1],
+		dec_endpoints[6] - dec_endpoints[2],
+		(num_comps == 4)  ? (dec_endpoints[7] - dec_endpoints[3]) : 0.0f };
+
+	uint32_t num_weight_levels = astc_helpers::get_ise_levels(weight_ise_range);
+	if (num_weight_levels == 64)
+		num_weight_levels = 65; // [0,64] (special case for raw ASTC weight mode, used for intermediate calcs)
+	
+	const uint32_t num_weight_levels_minus_one = num_weight_levels - 1;
+	
+	float dir_len2 = vec4_dot(dir, dir);
+	
+	if (dir_len2 < TINY_EPS)
+	{
+		memset(pWeights, 0, pixels.m_width * pixels.m_height);
+		return;
+	}
+
+	vec_scale(dir, (float)num_weight_levels_minus_one / dir_len2, num_comps);
+
+	const float w_bias = 0.5f - vec4_dot(dec_endpoints, dir); // 0.5f=rounding
+					
+	uint8_t* pDst_weights = pWeights;
+
+	if (num_comps == 4)
+	{
+		for (uint32_t y = 0; y < pixels.m_height; y++)
+		{
+			for (uint32_t x = 0; x < pixels.m_width; x++)
+			{
+				float v[4];
+				pixelbuf_get_pixel4(pixels.m_pBuf, x, y, v);
+
+				float w = (v[0] * dir[0]) + (v[1] * dir[1]) + (v[2] * dir[2]) + (v[3] * dir[3]) + w_bias;
+
+				int qw = (int)(w);
+
+				if ((uint32_t)qw > num_weight_levels_minus_one)
+					qw = ((~qw) >> 31) & num_weight_levels_minus_one;
+
+				*pDst_weights++ = (uint8_t)qw;
+
+			} // x
+		} // y
+	}
+	else
+	{
+		for (uint32_t y = 0; y < pixels.m_height; y++)
+		{
+			for (uint32_t x = 0; x < pixels.m_width; x++)
+			{
+				float v[3];
+				pixelbuf_get_pixel3(pixels.m_pBuf, x, y, v);
+
+				float w = (v[0] * dir[0]) + (v[1] * dir[1]) + (v[2] * dir[2]) + w_bias;
+
+				int qw = (int)(w);
+
+				if ((uint32_t)qw > num_weight_levels_minus_one)
+					qw = ((~qw) >> 31) & num_weight_levels_minus_one;
+
+				*pDst_weights++ = (uint8_t)qw;
+
+			} // x
+		} // y
+	}
+}
+
+static void eval_weights_for_plane(
+	const pixelbuf& pixels,
+	uint32_t weight_stride, // 1 or 2
+	uint8_t* pWeights, uint32_t weight_ise_range,
+	uint32_t cem, const uint8_t* pCEM_values, uint32_t endpoint_ise_range,
+	uint32_t active_chans_mask, 
+	int block_chan_to_swap_with_alpha) // only impacts reads from pixels
+{
+	float dec_endpoints[8];
+	cem_decode(cem, pCEM_values, endpoint_ise_range, dec_endpoints, nullptr);
+
+	float dir[4];
+	vec4_sub(dir, dec_endpoints + 4, dec_endpoints);
+
+	const uint32_t num_comps = 4;
+		
+	for (uint32_t i = 0; i < 4; i++)
+		if ((active_chans_mask & (1 << i)) == 0)
+			dir[i] = 0;
+
+	uint32_t num_weight_levels = astc_helpers::get_ise_levels(weight_ise_range);
+	if (num_weight_levels == 64)
+		num_weight_levels = 65; // [0,64] (special case for raw ASTC weight mode, used for intermediate calcs)
+
+	const uint32_t num_weight_levels_minus_one = num_weight_levels - 1;
+
+	float dir_len2 = vec4_dot(dir, dir);
+
+	if (dir_len2 < TINY_EPS)
+	{
+		const uint32_t total_pixels = pixels.m_width * pixels.m_height;
+		for (uint32_t i = 0; i < total_pixels; i++)
+			pWeights[i * weight_stride] = 0;
+		return;
+	}
+
+	vec_scale(dir, (float)num_weight_levels_minus_one / dir_len2, num_comps);
+
+	const float w_bias = 0.5f - vec4_dot(dec_endpoints, dir); // 0.5f=rounding
+
+	uint8_t* pDst_weights = pWeights;
+
+	for (uint32_t y = 0; y < pixels.m_height; y++)
+	{
+		for (uint32_t x = 0; x < pixels.m_width; x++)
+		{
+			float v[4];
+			pixelbuf_get_pixel4(pixels.m_pBuf, x, y, v);
+
+			if (block_chan_to_swap_with_alpha >= 0)
+				std::swap(v[3], v[block_chan_to_swap_with_alpha]);
+
+			float w = (v[0] * dir[0]) + (v[1] * dir[1]) + (v[2] * dir[2]) + (v[3] * dir[3]) + w_bias;
+
+			int qw = (int)(w);
+
+			if ((uint32_t)qw > num_weight_levels_minus_one)
+				qw = ((~qw) >> 31) & num_weight_levels_minus_one;
+
+			*pDst_weights = (uint8_t)qw;
+			pDst_weights += weight_stride;
+
+		} // x
+	} // y
+}
+
+[[maybe_unused]] static bool refine_endpoints_given_weights_cem_6_or_10_method2(
+	const pixelbuf& pixels, uint32_t cem,
+	const uint8_t *pWeights, uint32_t weight_ise_range,
+	const uint8_t* pCEM_values, uint32_t endpoint_ise_range,
+	float pNew_CEM_vals[6], // always writes 6 values
+	uint32_t num_comps)
+{
+	assert(is_cem_6_or_10(cem));
+	assert((num_comps == 3) || (num_comps == 4));
+
+	float dec_endpoints[8], actual_scale = 0.0f;
+	cem_decode(cem, pCEM_values, endpoint_ise_range, dec_endpoints, &actual_scale);
+
+	float actual_high[3] = { dec_endpoints[4], dec_endpoints[5], dec_endpoints[6] };
+
+	// not valid if weight_ise_range is 64 levels (which is a special case for [0,64] or 65 actual levels)
+	const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(minimum<uint32_t>(astc_helpers::BISE_32_LEVELS, weight_ise_range));
+
+	float Pa[3], Pb[3];
+	vec3_zero(Pa);
+	vec3_zero(Pb);
+	
+	float A = 0.0f, B = 0.0f, C = 0.0f;
+
+	const uint8_t* pSrc_weights = pWeights;
+	for (uint32_t y = 0; y < pixels.m_height; y++)
+	{
+		for (uint32_t x = 0; x < pixels.m_width; x++)
+		{
+			float px[3];
+			pixelbuf_get_pixel3(pixels.m_pBuf, x, y, px);
+
+			const int qw = *pSrc_weights++;
+			const int iw = (weight_ise_range == astc_helpers::BISE_64_LEVELS) ? qw : weight_tab.get_rank_to_val(qw);
+			assert(iw <= 64);
+
+			float t = (float)iw * (1.0f / 64.0f);
+			float bi = t, ai = 1.0f - t;
+
+			vec3_scale_add(Pa, px, ai, Pa);
+			vec3_scale_add(Pb, px, bi, Pb);
+			
+			A += ai * ai;
+			B += ai * bi;
+			C += bi * bi;
+		} // x
+	} // y
+
+	const float MAX_S = 255.0f / 256.0f;
+
+	bool did_clamp = false;
+
+	float new_high[3];
+	vec3_copy(new_high, actual_high);
+	float new_scale = actual_scale;
+
+	float h2 = vec3_dot(actual_high, actual_high);
+	if ((h2 > TINY_EPS) && (A > TINY_EPS))
+	{
+		new_scale = (vec3_dot(Pa, actual_high) / h2 - B) / A;
+		new_scale = clamp(new_scale, 0.0f, MAX_S); // not setting did_clamp on intermediate new_scale
+	}
+
+	const float den = A * new_scale * new_scale + 2.0f * B * new_scale + C;
+	if (den > TINY_EPS)
+	{
+		vec3_scale_add(new_high, Pa, new_scale, Pb);
+		vec3_div(new_high, den);
+		for (uint32_t i = 0; i < 3; i++)
+			new_high[i] = clamp_flag(new_high[i], 0.0f, 255.0f, did_clamp);
+	}
+
+	h2 = vec3_dot(new_high, new_high);
+	if ((h2 > TINY_EPS) && (A > TINY_EPS))
+	{
+		new_scale = (vec3_dot(Pa, new_high) / h2 - B) / A;
+		new_scale = clamp_flag(new_scale, 0.0f, MAX_S, did_clamp);
+	}
+
+	for (uint32_t c = 0; c < 3; c++)
+	{
+		pNew_CEM_vals[c * 2 + 0] = new_scale * new_high[c];
+		pNew_CEM_vals[c * 2 + 1] = new_high[c];
+	}
+
+	pNew_CEM_vals[6] = 255.0f;
+	pNew_CEM_vals[7] = 255.0f;
+
+	if (num_comps == 4)
+	{
+		float z00 = 0, z01 = 0, z10 = 0, z11 = 0;
+		float q00_a = 0, q10_a = 0, t_a = 0;
+
+		pSrc_weights = pWeights;
+		for (uint32_t y = 0; y < pixels.m_height; y++)
+		{
+			for (uint32_t x = 0; x < pixels.m_width; x++)
+			{
+				const float a = pixelbuf_get_comp(pixels.m_pBuf, x, y, 3);
+
+				const uint32_t qw = *pSrc_weights++;
+				const int dw = (weight_ise_range <= astc_helpers::BISE_32_LEVELS) ? weight_tab.get_rank_to_val(qw) : qw;
+				const float w = dw * (1.0f / 64.0f);
+
+				z00 += w * w;
+				z10 += (1.0f - w) * w;
+				z11 += (1.0f - w) * (1.0f - w);
+
+				q00_a += w * a;
+				t_a += a;
+			} // x
+		} // y
+
+		q10_a = t_a - q00_a;
+		z01 = z10;
+						
+		float xl, xh;
+		
+		float det = z00 * z11 - z01 * z10;
+		//if (fabs(det) >= TINY_EPS)
+		if (det >= TINY_EPS)
+		{
+			det = 1.0f / det;
+
+			float iz00 = z11 * det;
+			float iz01 = -z01 * det;
+			float iz10 = -z10 * det;
+			float iz11 = z00 * det;
+						
+			xl = clamp_flag(iz10 * q00_a + iz11 * q10_a, 0.0f, 255.0f, did_clamp);
+			xh = clamp_flag(iz00 * q00_a + iz01 * q10_a, 0.0f, 255.0f, did_clamp);
+		}
+		else
+		{
+			xl = t_a / (float)(pixels.m_width * pixels.m_height);
+			xh = xl;
+		}
+
+		pNew_CEM_vals[6] = xl;
+		pNew_CEM_vals[7] = xh;
+	}
+
+	return did_clamp;
+}
+
+static bool refine_endpoints_given_weights_cem_6_or_10_method3(
+	const pixelbuf& pixels,
+	uint32_t cem,
+	const uint8_t* pWeights, uint32_t weight_ise_range,
+	float pNew_CEM_vals[6], // always writes 6 values
+	uint32_t num_comps)
+{
+	BASISU_NOTE_UNUSED(cem);
+	assert(is_cem_6_or_10(cem));
+
+	assert((num_comps == 3) || (num_comps == 4));
+	const uint32_t pixel_count = pixels.m_width * pixels.m_height;
+	const float pixel_count_f = (float)pixel_count;
+
+	bool did_clamp = false;
+
+	const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(minimum<uint32_t>(astc_helpers::BISE_32_LEVELS, weight_ise_range));
+
+	float sum_w = 0.0f, sum_w2 = 0.0f;
+	float rgb_sum[3] = { };
+	float weighted_rgb_sum[3] = { };
+	
+	int min_dw = 256, max_dw = 0;
+
+	const uint8_t* pSrc_weights = pWeights;
+	for (uint32_t y = 0; y < pixels.m_height; y++)
+	{
+		for (uint32_t x = 0; x < pixels.m_width; x++)
+		{
+			float p[3];
+			pixelbuf_get_pixel(pixels.m_pBuf, x, y, p, 3);
+
+			const uint32_t qw = *pSrc_weights++;
+			
+			const int dw = (weight_ise_range <= astc_helpers::BISE_32_LEVELS) ? weight_tab.get_rank_to_val(qw) : qw;
+			assert(dw <= 64);
+			
+			min_dw = minimum(min_dw, dw);
+			max_dw = maximum(max_dw, dw);
+
+			const float w = dw * (1.0f / 64.0f);
+
+			sum_w += w;
+			sum_w2 += w * w;
+
+			vec3_add(rgb_sum, p);
+			vec3_scale_add(weighted_rgb_sum, p, w, weighted_rgb_sum);
+
+		} // x
+	} // y
+
+	float rgb_mean[3];
+	vec3_div(rgb_mean, rgb_sum, pixel_count_f);
+
+	const float A = pixel_count_f - 2.0f * sum_w + sum_w2;
+	const float B = sum_w - sum_w2;
+	const float C = sum_w2;
+
+	float Pb[3];
+	vec3_copy(Pb, weighted_rgb_sum);
+
+	float Pa[3]; 
+	vec3_sub(Pa, rgb_sum, weighted_rgb_sum);
+	
+	const float kMaxScale01 = 255.0f / 256.0f;
+
+	float best_scale01 = kMaxScale01;
+	float best_obj = -1.0f; // objective is always >= 0 when valid
+
+	auto try_scale = [&](float s)
+	{
+		// Reject out-of-range scales. Do not clamp roots into endpoint candidates.
+		if ((s < 0.0f) || (s > kMaxScale01))
+			return;
+
+		const float den = A * s * s + 2.0f * B * s + C;
+		if (den < TINY_EPS)
+			return;
+
+		float v[3];
+		vec3_scale_add(v, Pa, s, Pb);
+
+		const float obj = vec3_dot(v, v) / den;
+
+		if (obj > best_obj)
+		{
+			best_obj = obj;
+			best_scale01 = s;
+		}
+	};
+
+	float out_high_rgb[3];
+	vec3_copy(out_high_rgb, rgb_mean);
+	float out_scale = kMaxScale01;
+
+	if (max_dw == min_dw)
+	{
+		// weights all equal
+		const float w = (float)min_dw * (1.0f / 64.0f);
+
+		const float scale01 = kMaxScale01;
+
+		// Decoder effective multiplier:
+		// recon = lerp(scale01 * high, high, w)
+		//       = high * (scale01 * (1 - w) + w)
+		//       = high * (scale01 + w * (1 - scale01))
+		const float f = scale01 + w * (1.0f - scale01);
+
+		const float max_mean = fmaxf(rgb_mean[0], fmaxf(rgb_mean[1], rgb_mean[2]));
+
+		if ((f > TINY_EPS) && (max_mean <= 255.0f * f))
+		{
+			// Exact constant-color reconstruction without high endpoint clamp.
+			vec3_div(out_high_rgb, f);
+		}
+		// else: keep out_high_rgb = rgb_mean to avoid high endpoint clamp.
+
+		vec3_clamp_flag(out_high_rgb, 0.0f, 255.0f, did_clamp);
+
+		out_scale = scale01;
+	}
+	else
+	{
+		const float n2 = vec3_dot(Pa, Pa);
+		const float n1 = vec3_dot(Pa, Pb);
+		const float n0 = vec3_dot(Pb, Pb);
+				
+		const float q1 = n2 * C - n0 * A;
+		const float q0 = n1 * C - n0 * B;
+		const float q2 = n2 * B - n1 * A;
+
+#if 0
+		if ((q2 > TINY_EPS) || (q2 < -TINY_EPS))
+		{
+			//const double disc = (double)q1 * (double)q1 - 4.0f * (double)q2 * (double)q0;
+			const float disc = q1 * q1 - 4.0f * q2 * q0;
+
+			if (disc >= 0.0f)
+			{
+				//const float sqrt_disc = (float)sqrt(disc);
+				const float sqrt_disc = sqrtf(disc);
+
+				const float inv_2q2 = 0.5f / q2;
+
+				try_scale((-q1 - sqrt_disc) * inv_2q2);
+				try_scale((-q1 + sqrt_disc) * inv_2q2);
+			}
+		}
+		else if ((q1 > TINY_EPS) || (q1 < -TINY_EPS))
+		{
+			try_scale(-q0 / q1);
+		}
+#else
+		const float aq0 = fabsf(q0);
+		const float aq1 = fabsf(q1);
+		const float aq2 = fabsf(q2);
+
+		const float m = fmaxf(aq0, fmaxf(aq1, aq2));
+
+		if (m > 0.0f)
+		{
+			const float s = 1.0f / m;
+
+			const float a = q2 * s;
+			const float b = q1 * s;
+			const float c = q0 * s;
+
+			if (fabsf(a) > TINY_EPS)
+			{
+				const float disc = b * b - 4.0f * a * c;
+
+				if (disc >= 0.0f)
+				{
+					const float sqrt_disc = sqrtf(disc);
+
+					const float t = -0.5f * (b + copysignf(sqrt_disc, b));
+
+					if (t != 0.0f)
+					{
+						try_scale(t / a);
+						try_scale(c / t);
+					}
+					else
+					{
+						try_scale(-b * (0.5f / a));
+					}
+				}
+			}
+			else if (fabsf(b) > TINY_EPS)
+			{
+				try_scale(-c / b);
+			}
+		}
+#endif
+
+		// If no valid stationary root exists, or weights are flat,
+		// use conservative fallback scales.
+		if (best_obj < 0.0f)
+		{
+			try_scale(kMaxScale01);
+			try_scale(0.875f);
+			try_scale(0.75f);
+			try_scale(0.5f);
+			try_scale(0.25f);
+		}
+				
+		if (best_obj >= 0.0f)
+		{
+			const float scale01 = best_scale01;
+
+			const float high_den =
+				A * scale01 * scale01 +
+				2.0f * B * scale01 +
+				C;
+
+			if (high_den > TINY_EPS)
+			{
+				vec3_scale_add(out_high_rgb, Pa, scale01, Pb);
+				vec3_div(out_high_rgb, high_den);
+			}
+
+			vec3_clamp_flag(out_high_rgb, 0.0f, 255.0f, did_clamp);
+
+			out_scale = clamp_flag(scale01, 0.0f, kMaxScale01, did_clamp);
+		}
+
+	} // if (max_dw == min_dw)
+	
+	for (uint32_t c = 0; c < 3; c++)
+	{
+		pNew_CEM_vals[c * 2 + 0] = out_scale * out_high_rgb[c];
+		pNew_CEM_vals[c * 2 + 1] = out_high_rgb[c];
+	}
+
+	pNew_CEM_vals[6] = 255.0f;
+	pNew_CEM_vals[7] = 255.0f;
+
+	if (num_comps == 4)
+	{
+		float z00 = 0, z01 = 0, z10 = 0, z11 = 0;
+		float q00_a = 0, q10_a = 0, t_a = 0;
+
+#if 0
+		// TODO
+		z00 = sum_w2;
+		z10 = B;
+		z11 = A;
+		...
+		q00_a = sum w * a
+		t_a = sum a
+		q10_a = t_a - q00_a
+		...
+		const float z00 = C;
+		const float z01 = B;
+		const float z10 = B;
+		const float z11 = A;
+#endif
+
+		pSrc_weights = pWeights;
+		for (uint32_t y = 0; y < pixels.m_height; y++)
+		{
+			for (uint32_t x = 0; x < pixels.m_width; x++)
+			{
+				const float a = pixelbuf_get_comp(pixels.m_pBuf, x, y, 3);
+
+				const uint32_t qw = *pSrc_weights++;
+				const int dw = (weight_ise_range <= astc_helpers::BISE_32_LEVELS) ? weight_tab.get_rank_to_val(qw) : qw;
+				const float w = dw * (1.0f / 64.0f);
+
+				// TODO: some of these values we've already computed above
+				z00 += w * w;
+				z10 += (1.0f - w) * w;
+				z11 += (1.0f - w) * (1.0f - w);
+
+				q00_a += w * a;
+				t_a += a;
+			} // x
+		} // y
+
+		q10_a = t_a - q00_a;
+		z01 = z10;
+
+		float xl, xh;
+
+		float det = z00 * z11 - z01 * z10;
+		//if (fabs(det) >= TINY_EPS) // todo should always be pos?
+		if (det >= TINY_EPS) // todo should always be pos?
+		{
+			det = 1.0f / det;
+
+			float iz00 = z11 * det;
+			float iz01 = -z01 * det;
+			float iz10 = -z10 * det;
+			float iz11 = z00 * det;
+
+			xl = clamp_flag(iz10 * q00_a + iz11 * q10_a, 0.0f, 255.0f, did_clamp);
+			xh = clamp_flag(iz00 * q00_a + iz01 * q10_a, 0.0f, 255.0f, did_clamp);
+		}
+		else
+		{
+			xl = t_a / (float)(pixels.m_width * pixels.m_height);
+			xh = xl;
+		}
+
+		pNew_CEM_vals[6] = xl;
+		pNew_CEM_vals[7] = xh;
+	}
+	
+	return did_clamp;
+}
+
+static bool refine_endpoints_given_weights_cem_8_9_12_or_13(
+	const pixelbuf& pixels,
+	uint32_t cem,
+	const uint8_t* pWeights, uint32_t weight_ise_range,
+	float pNew_CEM_vals[8], // always writes 8 values
+	uint32_t num_comps)
+{
+	BASISU_NOTE_UNUSED(cem);
+	assert(is_cem_8_or_12(cem) || is_cem_9_or_13(cem));
+	assert((num_comps == 3) || (num_comps == 4));
+
+	float z00 = 0, z01 = 0, z10 = 0, z11 = 0;
+	float q00_a[4] = { }, t_a[4] = { };
+
+	bool did_clamp = false;
+
+	const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(minimum<uint32_t>(astc_helpers::BISE_32_LEVELS, weight_ise_range));
+
+	const uint8_t *pSrc_weights = pWeights;
+	for (uint32_t y = 0; y < pixels.m_height; y++)
+	{
+		for (uint32_t x = 0; x < pixels.m_width; x++)
+		{
+			const uint32_t qw = *pSrc_weights++;
+			const int dw = (weight_ise_range <= astc_helpers::BISE_32_LEVELS) ? weight_tab.get_rank_to_val(qw) : qw;
+			assert(dw <= 64);
+
+			const float w = dw * (1.0f / 64.0f);
+
+			z00 += w * w;
+			z10 += (1.0f - w) * w;
+			z11 += (1.0f - w) * (1.0f - w);
+
+			for (uint32_t c = 0; c < num_comps; c++)
+			{
+				const float a = pixelbuf_get_comp(pixels.m_pBuf, x, y, c);
+
+				q00_a[c] += w * a;
+				t_a[c] += a;
+			}
+		} // x
+	} // y
+
+	float q10_a[4];
+	for (uint32_t c = 0; c < num_comps; c++)
+		q10_a[c] = t_a[c] - q00_a[c];
+
+	z01 = z10;
+			
+	float det = z00 * z11 - z01 * z10; // should be non-negative mathematically
+	
+	//if (fabs(det) >= TINY_EPS)
+	if (det >= TINY_EPS)
+	{
+		det = 1.0f / det;
+
+		float iz00 = z11 * det;
+		float iz01 = -z01 * det;
+		float iz10 = -z10 * det;
+		float iz11 = z00 * det;
+
+		for (uint32_t c = 0; c < num_comps; c++)
+		{
+			pNew_CEM_vals[c * 2 + 0] = clamp_flag(iz10 * q00_a[c] + iz11 * q10_a[c], 0.0f, 255.0f, did_clamp);
+			pNew_CEM_vals[c * 2 + 1] = clamp_flag(iz00 * q00_a[c] + iz01 * q10_a[c], 0.0f, 255.0f, did_clamp);
+		}
+	}
+	else
+	{
+		const float one_over_total_pixels = 1.0f / (float)(pixels.m_width * pixels.m_height);
+		for (uint32_t c = 0; c < num_comps; c++)
+		{
+			pNew_CEM_vals[c * 2 + 0] = t_a[c] * one_over_total_pixels;
+			pNew_CEM_vals[c * 2 + 1] = pNew_CEM_vals[c * 2 + 0];
+		}
+	}
+
+	if (num_comps == 3)
+	{
+		pNew_CEM_vals[3 * 2 + 0] = 255.0f;
+		pNew_CEM_vals[3 * 2 + 1] = 255.0f;
+	}
+
+	return did_clamp;
+}
+
+static bool refine_endpoints_given_weights_cem_0_or_4(
+	const pixelbuf& pixels,
+	uint32_t cem,
+	const uint8_t* pWeights, uint32_t weight_ise_range,
+	float pNew_CEM_vals[8], // always writes 8 values
+	uint32_t num_comps)
+{
+	BASISU_NOTE_UNUSED(cem);
+	BASISU_NOTE_UNUSED(num_comps);
+	assert(is_cem_0_or_4(cem));
+	assert((num_comps == 3) || (num_comps == 4));
+
+	float z00 = 0, z01 = 0, z10 = 0, z11 = 0;
+	float q00_a[2] = { }, t_a[2] = { };
+
+	bool did_clamp = false;
+
+	const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(minimum<uint32_t>(astc_helpers::BISE_32_LEVELS, weight_ise_range));
+
+	const uint32_t num_actual_comps = (cem == 4) ? 2 : 1;
+
+	const uint8_t* pSrc_weights = pWeights;
+	for (uint32_t y = 0; y < pixels.m_height; y++)
+	{
+		for (uint32_t x = 0; x < pixels.m_width; x++)
+		{
+			const uint32_t qw = *pSrc_weights++;
+			const int dw = (weight_ise_range <= astc_helpers::BISE_32_LEVELS) ? weight_tab.get_rank_to_val(qw) : qw;
+			const float w = dw * (1.0f / 64.0f);
+
+			z00 += w * w;
+			z10 += (1.0f - w) * w;
+			z11 += (1.0f - w) * (1.0f - w);
+
+			for (uint32_t c = 0; c < num_actual_comps; c++)
+			{
+				const float a = pixelbuf_get_comp(pixels.m_pBuf, x, y, c ? 3 : 0);
+
+				q00_a[c] += w * a;
+				t_a[c] += a;
+			}
+		} // x
+	} // y
+
+	float q10_a[2];
+	for (uint32_t c = 0; c < num_actual_comps; c++)
+		q10_a[c] = t_a[c] - q00_a[c];
+
+	z01 = z10;
+
+	// default A bounds for cem 0
+	pNew_CEM_vals[6] = 255.0f;
+	pNew_CEM_vals[7] = 255.0f;
+
+	float det = z00 * z11 - z01 * z10; // should be non-negative mathematically
+	//if (fabs(det) >= TINY_EPS)
+	if (det >= TINY_EPS)
+	{
+		det = 1.0f / det;
+
+		float iz00 = z11 * det;
+		float iz01 = -z01 * det;
+		float iz10 = -z10 * det;
+		float iz11 = z00 * det;
+
+		for (uint32_t c = 0; c < num_actual_comps; c++)
+		{
+			float l = clamp_flag(iz10 * q00_a[c] + iz11 * q10_a[c], 0.0f, 255.0f, did_clamp);
+			float h = clamp_flag(iz00 * q00_a[c] + iz01 * q10_a[c], 0.0f, 255.0f, did_clamp);
+
+			if (c == 0)
+			{
+				pNew_CEM_vals[0] = l;
+				pNew_CEM_vals[1] = h;
+			}
+			else
+			{
+				pNew_CEM_vals[6] = l;
+				pNew_CEM_vals[7] = h;
+			}
+		}
+	}
+	else
+	{
+		const float one_over_total_pixels = 1.0f / (float)(pixels.m_width * pixels.m_height);
+
+		for (uint32_t c = 0; c < num_actual_comps; c++)
+		{
+			float l = t_a[c] * one_over_total_pixels;
+			float h = l;
+
+			if (c == 0)
+			{
+				pNew_CEM_vals[0] = l;
+				pNew_CEM_vals[1] = h;
+			}
+			else
+			{
+				pNew_CEM_vals[6] = l;
+				pNew_CEM_vals[7] = h;
+			}
+		}
+	}
+
+	// set G and B to R bounds
+	pNew_CEM_vals[2] = pNew_CEM_vals[0];
+	pNew_CEM_vals[3] = pNew_CEM_vals[1];
+
+	pNew_CEM_vals[4] = pNew_CEM_vals[0];
+	pNew_CEM_vals[5] = pNew_CEM_vals[1];
+
+	return did_clamp;
+}
+
+// true if clamping occured on outputs
+static bool refine_endpoints_given_weights(
+	const pixelbuf& pixels,
+	uint32_t cem, const uint8_t* pWeights, uint32_t weight_ise_range,
+	[[maybe_unused]] const uint8_t* pCEM_values, [[maybe_unused]] uint32_t endpoint_ise_range,
+	float pNew_CEM_vals[8],
+	uint32_t num_comps)
+{
+	assert(is_cem_0_or_4(cem) || is_cem_6_or_10(cem) || is_cem_8_or_12(cem));
+
+	switch (cem)
+	{
+	case 0:
+	case 4:
+	{
+		assert(is_cem_0_or_4(cem));
+
+		return refine_endpoints_given_weights_cem_0_or_4(
+			pixels, cem,
+			pWeights, weight_ise_range,
+			pNew_CEM_vals,
+			num_comps);
+	}
+	case 6:
+	case 10:
+	{
+#if 1
+		// root finding, creates new endpoints
+		return refine_endpoints_given_weights_cem_6_or_10_method3(
+			pixels, cem,
+			pWeights, weight_ise_range,
+			pNew_CEM_vals,
+			num_comps);
+#else
+		// coordinate descent, needs current endpoints
+		return refine_endpoints_given_weights_cem_6_or_10_method2(
+			pixels, cem,
+			pWeights, weight_ise_range, pCEM_values, endpoint_ise_range,
+			pNew_CEM_vals,
+			num_comps);
+#endif
+	}
+	case 8:
+	case 9:
+	case 12:
+	case 13:
+	{
+		return refine_endpoints_given_weights_cem_8_9_12_or_13(
+			pixels, cem,
+			pWeights, weight_ise_range,
+			pNew_CEM_vals,
+			num_comps);
+	}
+	default:
+		assert(0);
+		break;
+	}
+
+	return false;
+}
+
+// ccs component was swizzled into A
+static void eval_weights_second_plane(
+	const pixelbuf& pixels,
+	uint32_t cem, uint32_t ccs_index,
+	uint8_t* pWeights0, uint8_t* pWeights1, uint32_t weight_ise_range,
+	float desired_cem_endpoints[8], // in CEM order
+	const uint8_t* pCEM_values, uint32_t endpoint_ise_range,
+	uint8_t* pNew_CEM_values,
+	uint32_t num_pca_comps, bool ccs_chan_rotated_flag, 
+	const single_subset_enc_context& ctx) // true if channel ccs_index was rotated vs. alpha, ccs_index should be <= 2
+{
+	assert(!is_cem_9_or_13(cem));
+	BASISU_NOTE_UNUSED(num_pca_comps);
+
+	float lo_v = 1e+30f, hi_v = -1e+30f;
+
+	// ccs_chan_rotated_flag ccs chan was rotated into alpha, otherwise there was no ccs channel swapping
+	const uint32_t actual_src_ccs_chan = ccs_chan_rotated_flag ? 3 : ccs_index;
+
+	for (uint32_t y = 0; y < pixels.m_height; y++)
+	{
+		for (uint32_t x = 0; x < pixels.m_width; x++)
+		{
+			float v = pixelbuf_get_comp(pixels.m_pBuf, x, y, actual_src_ccs_chan);
+			lo_v = minimum(lo_v, v);
+			hi_v = maximum(hi_v, v);
+		} // x
+	} // y
+
+	// be aware lo_v/hi_v are not clamped 
+
+	if (ccs_index == 3)
+	{
+		// second plane controls A: insert alpha range directly into the encoded CEM values and we're done
+		int lo_v_q = quant_endpoint_val_to_rank(lo_v, endpoint_ise_range, astc_helpers::get_ise_levels(endpoint_ise_range));
+		int hi_v_q = quant_endpoint_val_to_rank(hi_v, endpoint_ise_range, astc_helpers::get_ise_levels(endpoint_ise_range));
+
+		if (cem == 10)
+		{
+			if (pNew_CEM_values != pCEM_values)
+				memcpy(pNew_CEM_values, pCEM_values, 4);
+
+			pNew_CEM_values[4] = (uint8_t)lo_v_q;
+			pNew_CEM_values[5] = (uint8_t)hi_v_q;
+
+			desired_cem_endpoints[4] = lo_v;
+			desired_cem_endpoints[5] = hi_v;
+		}
+		else if (cem == 12)
+		{
+			if (pNew_CEM_values != pCEM_values)
+				memcpy(pNew_CEM_values, pCEM_values, 6);
+
+			pNew_CEM_values[6] = (uint8_t)lo_v_q;
+			pNew_CEM_values[7] = (uint8_t)hi_v_q;
+
+			desired_cem_endpoints[6] = lo_v;
+			desired_cem_endpoints[7] = hi_v;
+		}
+		else if (cem == 4)
+		{
+			if (pNew_CEM_values != pCEM_values)
+				memcpy(pNew_CEM_values, pCEM_values, 2);
+
+			pNew_CEM_values[2] = (uint8_t)lo_v_q;
+			pNew_CEM_values[3] = (uint8_t)hi_v_q;
+
+			desired_cem_endpoints[2] = lo_v;
+			desired_cem_endpoints[3] = hi_v;
+		}
+		else
+		{
+			assert(0);
+		}
+	}
+	else if (is_cem_8_or_12(cem)) //((cem == 8) || (cem == 12))
+	{
+		assert(num_pca_comps == 3);
+
+		// ccs_index is [0,2], cem is 8/12
+		// ccs was swizzled with A, undo that, insert pixel bounds of ccs channel into correct endpoint channel
+		assert(ccs_chan_rotated_flag);
+
+		desired_cem_endpoints[3 * 2 + 0] = lo_v;
+		desired_cem_endpoints[3 * 2 + 1] = hi_v;
+
+		std::swap(desired_cem_endpoints[3 * 2 + 0], desired_cem_endpoints[ccs_index * 2 + 0]);
+		std::swap(desired_cem_endpoints[3 * 2 + 1], desired_cem_endpoints[ccs_index * 2 + 1]);
+
+		cem_encode(cem, desired_cem_endpoints, endpoint_ise_range, pNew_CEM_values, true, ctx.m_higher_effort_bc);
+	}
+	else
+	{
+		assert(is_cem_6_or_10(cem));
+
+		// ccs_index is [0,2], cem is 6/10 - no change to endpoints, we're just going to split out one RGB channel into a sep weight plane
+		if (pNew_CEM_values != pCEM_values)
+			memcpy(pNew_CEM_values, pCEM_values, astc_helpers::get_num_cem_values(cem));
+
+		// no change to desired_cem_endpoints
+	}
+
+	float final_endpoints[8];
+	cem_decode(cem, pNew_CEM_values, endpoint_ise_range, final_endpoints, nullptr);
+
+	const uint32_t num_weight_levels = astc_helpers::get_ise_levels(weight_ise_range);
+	assert(num_weight_levels <= 32);
+
+	const float l = final_endpoints[0 + ccs_index];
+	const float h = final_endpoints[4 + ccs_index];
+	const float scale = (float)(num_weight_levels - 1) / ((h - l) + TINY_EPS);
+
+	uint8_t* pDst_weights = pWeights1;
+	for (uint32_t y = 0; y < pixels.m_height; y++)
+	{
+		for (uint32_t x = 0; x < pixels.m_width; x++)
+		{
+			const float v = pixelbuf_get_comp(pixels.m_pBuf, x, y, actual_src_ccs_chan);
+
+			const int iw = clamp<int>((int)((v - l) * scale + .5f), 0, num_weight_levels - 1);
+
+			*pDst_weights++ = (uint8_t)iw;
+		} // x
+	} // y
+
+	if ((ccs_index < 3) && (is_cem_8_or_12(cem)))
+	{
+		assert(ccs_chan_rotated_flag);
+
+		// CEM 8 or 12.
+		// We re-encoded the endpoints, so update the weights in case they changed.
+		// Final endpoints are not CCS swapped (rotated), so we'll need eval_weights_first_plane() to swap them on 
+		// endpoint decode to match the input pixels here which are swapped with A.
+		eval_weights_first_plane(pixels,
+			pWeights0, weight_ise_range, cem,
+			pNew_CEM_values, endpoint_ise_range,
+			3, ccs_index);
+	}
+}
+
+static void try_base_ofs_cem(
+	uint32_t cem_index,
+	const pixelbuf& block,
+	astc_helpers::log_astc_block& log_blk,
+	[[maybe_unused]] const single_subset_enc_context& ctx,
+	[[maybe_unused]] uint32_t num_block_comps, // 3 or 4, for 3 alpha=255
+	float desired_cem_endpoints[8], 
+	bool ccs_chan_rotated_flag) // if true, a and the ccs chan are swapped in block's pixels
+{
+	assert(!is_lblock_ise(log_blk));
+
+	if (is_cem_9_or_13(log_blk.m_color_endpoint_modes[0]))
+		return;
+
+	assert(is_cem_8_or_12(cem_index));
+
+	if (log_blk.m_endpoint_ise_range == astc_helpers::BISE_256_LEVELS)
+		return;
+
+	// try base+ofs, very rare win (~1-3% vs. direct)
+	uint8_t CEM_values_base_ofs[8];
+
+	const uint32_t cem_index_base_ofs = (cem_index == 8) ? 9 : 13;
+	const uint32_t num_comps = get_num_cem_chans(cem_index);
+	const uint32_t num_base_ofs_cem_vals = astc_helpers::get_num_cem_values(cem_index_base_ofs);
+
+	const bool base_ofs_fit = encode_cem_9_13(cem_index_base_ofs, CEM_values_base_ofs, log_blk.m_endpoint_ise_range, desired_cem_endpoints, true);
+	if (!base_ofs_fit)
+		return;
+
+	const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(log_blk.m_endpoint_ise_range);
+	for (uint32_t i = 0; i < num_base_ofs_cem_vals; i++)
+		CEM_values_base_ofs[i] = endpoint_tab.m_ISE_to_rank[CEM_values_base_ofs[i]];
+
+	float ep_base_ofs[8];
+	cem_decode(cem_index_base_ofs, CEM_values_base_ofs, log_blk.m_endpoint_ise_range, ep_base_ofs, nullptr);
+
+	float ep_direct[8];
+	cem_decode(cem_index, log_blk.m_endpoints, log_blk.m_endpoint_ise_range, ep_direct, nullptr);
+		
+	float desired_endpoints_lh_rgba[8];
+
+	for (uint32_t c = 0; c < num_comps; c++)
+	{
+		desired_endpoints_lh_rgba[c + 0] = desired_cem_endpoints[c * 2 + 0];
+		desired_endpoints_lh_rgba[c + 4] = desired_cem_endpoints[c * 2 + 1];
+	}
+
+	const float dist_base_ofs = calc_shortest_endpoint_dist(desired_endpoints_lh_rgba, ep_base_ofs, num_comps);
+	const float dist_direct = calc_shortest_endpoint_dist(desired_endpoints_lh_rgba, ep_direct, num_comps);
+
+	if (dist_base_ofs < dist_direct)
+	{
+		log_blk.m_color_endpoint_modes[0] = (uint8_t)cem_index_base_ofs;
+		memcpy(log_blk.m_endpoints, CEM_values_base_ofs, num_base_ofs_cem_vals);
+				
+		const uint32_t weight1_chan_mask = log_blk.m_dual_plane ? (1u << log_blk.m_color_component_selector) : 0;
+		const uint32_t weight0_chan_mask = ((1 << num_comps) - 1) ^ weight1_chan_mask;
+		
+		eval_weights_for_plane(
+			block,
+			log_blk.m_dual_plane ? 2 : 1,
+			log_blk.m_weights, log_blk.m_weight_ise_range,
+			log_blk.m_color_endpoint_modes[0], log_blk.m_endpoints, log_blk.m_endpoint_ise_range,
+			weight0_chan_mask, ccs_chan_rotated_flag ? log_blk.m_color_component_selector : -1);
+
+		if (log_blk.m_dual_plane)
+		{
+			eval_weights_for_plane(
+				block,
+				2,
+				log_blk.m_weights + 1, log_blk.m_weight_ise_range,
+				log_blk.m_color_endpoint_modes[0], log_blk.m_endpoints, log_blk.m_endpoint_ise_range,
+				weight1_chan_mask, ccs_chan_rotated_flag ? log_blk.m_color_component_selector : -1);
+		}
+	}
+}
+
+// PCA comps
+// single plane:
+//   cem 0: 3D (R=G=B, really 1D)
+//   cem 4: 4D (R=G=B + A, really 2D)
+//   cem 6: 3D, zero centered
+//   cem 8: 3D
+//   cem 10: 4D, zero centered
+//   cem 12: 4D 
+// dual plane, CCS=3:
+//   cem 0: invalid
+//   cem 4: 3D (R=G=B + A, really 1D)
+//   cem 6: invalid
+//   cem 8: invalid
+//   cem 10: 3D, zero centered
+//   cem 12: 3D
+// dual plane, CCS=[0,2] - for CEM 6/10, solve as SP, then introduce 2nd weight plane but don't change the encoded RGBA endpoints
+//   cem 0: invalid
+//   cem 4: invalid
+//   cem 6: 3D zero centered, unrotated pixels (i.e. ccs channel NOT swapped with A)
+//   cem 8: 3D, rotated pixels (essentially 2D PCA as all A=255)
+//   cem 10: 4D, zero centered, unrotated pixels (exceptional case)
+//   cem 12: 3D, rotated pixels
+static inline void get_pca_config(const astc_unpacked_config& cfg, uint32_t& num_pca_comps, bool& ccs_chan_rotated_flag)
+{
+	num_pca_comps = 3;
+	ccs_chan_rotated_flag = false;
+
+	if (cfg.m_dual_plane)
+	{
+		assert(cfg.m_cem != 0);
+
+		if (cfg.m_cem == 4)
+		{
+			assert(cfg.m_ccs_index == 3);
+			//num_pca_comps = 3;
+		}
+		else if (cfg.m_ccs_index < 3)
+		{
+			num_pca_comps = (cfg.m_cem == 10) ? 4 : 3;
+			ccs_chan_rotated_flag = is_cem_8_or_12(cfg.m_cem); // ((cfg.m_cem & 3) == 0); // cem 8 or 12, otherwise not ccs channel rotated with A
+		}
+	}
+	else
+	{
+		num_pca_comps = ((cfg.m_cem == 4) || (cfg.m_cem >= 10)) ? 4 : 3;
+	}
+}
+
+static void create_single_subset_block(
+	const pixelbuf& block,
+	astc_helpers::log_astc_block& log_blk,
+	const single_subset_enc_context& ctx,
+	uint32_t num_block_comps, // 3 or 4, for 3 alpha=255
+	uint32_t num_pca_comps, // 3D or 4D
+	bool ccs_chan_rotated_flag,
+	bool try_base_ofs) // ccs_chan_rotated_flag is true for dual plane if the CCS channel has been swapped/rotated with alpha
+{
+	const uint32_t cem_index = log_blk.m_color_endpoint_modes[0];
+	
+	assert(is_cem_0_or_4(cem_index) || is_cem_6_or_10(cem_index) || is_cem_8_or_12(cem_index));
+
+	const bool cem_6_or_10 = is_cem_6_or_10(cem_index); // (cem_index & 3) == 2;
+	//const bool cem_has_alpha = does_cem_have_alpha(cem_index); // only true when the block has alpha, too
+	const bool dual_plane = log_blk.m_dual_plane;
+
+	if (num_block_comps == 3)
+	{
+		assert((cem_index == 0) || (cem_index == 6) || (cem_index == 8));
+	}
+
+	if (dual_plane)
+	{
+		if (log_blk.m_color_component_selector == 3)
+		{
+			assert((cem_index == 4) || (cem_index >= 10));
+		}
+		else 
+		{
+			assert(!is_cem_0_or_4(cem_index));
+		}
+	}
+
+	float desired_cem_endpoints[8];
+	calc_initial_cem_endpoints(block, desired_cem_endpoints, num_pca_comps, nullptr, cem_6_or_10);
+
+	cem_encode(cem_index, desired_cem_endpoints, log_blk.m_endpoint_ise_range, log_blk.m_endpoints, true, ctx.m_higher_effort_bc);
+
+	eval_weights_first_plane(block, log_blk.m_weights, log_blk.m_weight_ise_range,
+		cem_index, log_blk.m_endpoints, log_blk.m_endpoint_ise_range,
+		num_pca_comps);
+
+	for (uint32_t i = 0; i < ctx.m_num_ls_iterations; i++)
+	{
+		refine_endpoints_given_weights(block, cem_index, log_blk.m_weights, log_blk.m_weight_ise_range, log_blk.m_endpoints, log_blk.m_endpoint_ise_range, desired_cem_endpoints, num_pca_comps);
+				
+		cem_encode(cem_index, desired_cem_endpoints, log_blk.m_endpoint_ise_range, log_blk.m_endpoints, true, ctx.m_higher_effort_bc);
+
+		eval_weights_first_plane(block, log_blk.m_weights, log_blk.m_weight_ise_range,
+			cem_index, log_blk.m_endpoints, log_blk.m_endpoint_ise_range,
+			num_pca_comps);
+	} // i
+
+	if (dual_plane)
+	{
+		uint8_t weights[2][astc_helpers::MAX_GRID_WEIGHTS];
+
+		eval_weights_second_plane(
+			block,
+			cem_index, log_blk.m_color_component_selector,
+			log_blk.m_weights, weights[1], log_blk.m_weight_ise_range,
+			desired_cem_endpoints,
+			log_blk.m_endpoints, log_blk.m_endpoint_ise_range,
+			log_blk.m_endpoints,
+			num_pca_comps, ccs_chan_rotated_flag, ctx);
+
+		const uint32_t num_grid_samples = log_blk.m_grid_width * log_blk.m_grid_height;
+		memcpy(weights[0], log_blk.m_weights, num_grid_samples);
+
+		for (uint32_t i = 0; i < num_grid_samples; i++)
+		{
+			log_blk.m_weights[i * 2 + 0] = weights[0][i];
+			log_blk.m_weights[i * 2 + 1] = weights[1][i];
+		} // i
+	}
+
+	if ((try_base_ofs) && ((cem_index == 8) || (cem_index == 12)))
+	{
+		try_base_ofs_cem(cem_index,
+			block,
+			log_blk,
+			ctx,
+			num_block_comps,
+			desired_cem_endpoints,
+			ccs_chan_rotated_flag);
+	}
+}
+
+static void encode_single_subset_block(
+	const single_subset_enc_context& ctx,
+	const pixelbuf& src_block, uint32_t num_src_block_comps,
+	const astc_unpacked_config& cfg,
+	astc_helpers::log_astc_block& log_blk,
+	bool try_base_ofs)
+{
+	assert(cfg.m_grid_width <= ctx.m_block_width);
+	assert(cfg.m_grid_height <= ctx.m_block_height);
+
+	log_blk.clear();
+	log_blk.m_grid_width = cfg.m_grid_width;
+	log_blk.m_grid_height = cfg.m_grid_height;
+	log_blk.m_endpoint_ise_range = cfg.m_endpoint_range;
+	log_blk.m_weight_ise_range = cfg.m_weight_range;
+	log_blk.m_dual_plane = cfg.m_dual_plane;
+	log_blk.m_color_component_selector = cfg.m_ccs_index;
+	log_blk.m_num_partitions = 1;
+	log_blk.m_color_endpoint_modes[0] = cfg.m_cem;
+	log_blk.m_user_mode = cUserModeRankValues;
+
+	if (num_src_block_comps == 3)
+	{
+		//assert(cfg.m_cem <= 8);
+		assert((cfg.m_cem == 0) || (cfg.m_cem == 6) || (cfg.m_cem == 8));
+	}
+	
+	uint32_t num_pca_comps;
+	bool ccs_chan_rotated_flag;
+	get_pca_config(cfg, num_pca_comps, ccs_chan_rotated_flag);
+
+	float grid_pixels[PIXELBUF_SIZE_IN_FLOATS];
+	pixelbuf grid_pbuf(cfg.m_grid_width, cfg.m_grid_height, grid_pixels);
+
+	const uint32_t num_block_chans_to_encode = minimum<uint32_t>(get_num_cem_chans(cfg.m_cem), num_src_block_comps); // always 3 or 4
+
+	pseudoinverse_block_to_grid(src_block, grid_pbuf, num_block_chans_to_encode);
+
+	if (num_block_chans_to_encode == 3)
+		pixelbuf_set_comp_to_val(grid_pbuf, 3, 255.0f);
+
+	if (ccs_chan_rotated_flag)
+		pixelbuf_swap_comp_with_alpha(grid_pbuf, cfg.m_ccs_index);
+
+	create_single_subset_block(grid_pbuf, log_blk, ctx, num_block_chans_to_encode, num_pca_comps, ccs_chan_rotated_flag, try_base_ofs);
+}
+
+bool init_single_subset_context(
+	single_subset_enc_context& ctx,
+	uint32_t block_width, uint32_t block_height,
+	astc_helpers::decode_mode astc_decode_mode,
+	const uint32_t chan_weights[4],
+	uint32_t max_candidates, uint32_t num_ls_iterations, bool disable_dual_plane, bool has_alpha, bool weight_polishing)
+{
+	const int idx = astc_helpers::find_astc_block_size_index(block_width, block_height);
+	assert(idx != -1);
+	if (idx == -1)
+	{
+		assert(0);
+		return false;
+	}
+
+	if (max_candidates > MAX_CANDIDATES)
+	{
+		assert(0);
+		return false;
+	}
+
+	ctx.m_block_width = block_width;
+	ctx.m_block_height = block_height;
+		
+	ctx.m_block_size_index = idx;
+	ctx.m_total_block_pixels = block_width * block_height;
+	
+	ctx.m_astc_decode_mode = astc_decode_mode;
+
+	memcpy(ctx.m_chan_weights, chan_weights, sizeof(ctx.m_chan_weights));
+		
+	ctx.m_max_candidates = max_candidates;
+	ctx.m_num_ls_iterations = num_ls_iterations;
+	ctx.m_weight_polishing = weight_polishing;
+
+	ctx.m_disable_dual_plane = disable_dual_plane;
+	ctx.m_has_alpha = has_alpha;
+
+	ctx.m_try_base_ofs = true;
+	ctx.m_higher_effort_bc = true;
+		
+	return ctx.m_dct.init(block_height, block_width); // rows=block height, cols=block width
+}
+
+static inline float calc_weighted_error(const astc_helpers::color_rgba& c, float r, float g, float b, float a, const single_subset_enc_context& state)
+{
+	return (square((float)c.m_r - r) * (float)state.m_chan_weights[0]) +
+		(square((float)c.m_g - g) * (float)state.m_chan_weights[1]) +
+		(square((float)c.m_b - b) * (float)state.m_chan_weights[2]) +
+		(square((float)c.m_a - a) * (float)state.m_chan_weights[3]);
+}
+
+// 1-3 subsets
+// returns true if lblock_to_refine was changed
+static bool weight_polish(
+	const pixelbuf& src_block,
+	astc_helpers::log_astc_block& lblock_to_refine,
+	const single_subset_enc_context& enc_state,
+	const basist::astc_ldr_t::astc_block_grid_data* pGrid_data,
+	double& best_lblock_error, astc_helpers::log_astc_block& best_lblock,
+	astc_lblock_vec* pAll_candidates)
+{
+	assert((lblock_to_refine.m_num_partitions >= 1) && (lblock_to_refine.m_num_partitions <= 3));
+		
+	const uint32_t block_width = enc_state.m_block_width, block_height = enc_state.m_block_height;
+	[[maybe_unused]] const uint32_t total_block_pixels = block_width * block_height;
+
+	[[maybe_unused]] const uint32_t num_cem_endpoint_vals = astc_helpers::get_num_cem_values(lblock_to_refine.m_color_endpoint_modes[0]);
+	[[maybe_unused]] const uint32_t total_grid_weights = lblock_to_refine.m_grid_width * lblock_to_refine.m_grid_height;
+
+	[[maybe_unused]] const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(lblock_to_refine.m_endpoint_ise_range);
+	const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(lblock_to_refine.m_weight_ise_range);
+
+	const uint32_t num_weight_levels = astc_helpers::get_ise_levels(lblock_to_refine.m_weight_ise_range);
+
+	[[maybe_unused]] const uint32_t num_partitions = lblock_to_refine.m_num_partitions;
+
+	astc_helpers::log_astc_block refined_lblock_ise(lblock_to_refine);
+	convert_rank_lblock_to_ise(refined_lblock_ise); // xuastc_ldr_block_decoder expects ISE blocks
+
+	bool changed_flag = false;
+
+	const uint32_t grid_width = refined_lblock_ise.m_grid_width, grid_height = refined_lblock_ise.m_grid_height;
+
+	astc_helpers::xuastc_ldr_block_decoder block_decoder;
+	block_decoder.init(refined_lblock_ise, block_width, block_height, enc_state.m_astc_decode_mode, pGrid_data->m_upsample_weights.get_ptr());
+
+#if defined(DEBUG) || defined(_DEBUG)
+	const double error1 = compute_block_error(refined_lblock_ise, src_block, enc_state);
+#endif
+
+	for (uint32_t y = 0; y < grid_height; y++)
+	{
+		for (uint32_t x = 0; x < grid_width; x++)
+		{
+			const uint32_t idx = x + y * grid_width;
+
+			const basisu::uint16_vec& influenced_texels = pGrid_data->m_grid_to_texel_influence_list[idx];
+
+			int cur_rank = weight_tab.m_ISE_to_rank[refined_lblock_ise.m_weights[idx]];
+
+			const uint8_t orig_weight_ise = astc_helpers::get_weight(refined_lblock_ise, 0, idx);
+
+			double best_err = 0.0f;
+
+			for (uint32_t j = 0; j < influenced_texels.size(); j++)
+			{
+				const uint32_t packed_texel_index = influenced_texels[j];
+				const uint32_t texel_x = packed_texel_index & 0xFF;
+				const uint32_t texel_y = packed_texel_index >> 8;
+				
+				astc_helpers::color_rgba dec_c;
+				block_decoder.decode_texel(texel_x, texel_y, dec_c);
+
+				best_err += calc_weighted_error(dec_c, pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 0), pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 1), pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 2), pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 3), enc_state);
+			}
+
+			if (best_err == 0.0f)
+				continue;
+						
+			uint8_t best_weight_ise = orig_weight_ise;
+
+			for (int idir = -1; idir <= 1; idir += 2)
+			{
+				const int new_rank = clamp(cur_rank + idir, 0, (int)num_weight_levels - 1);
+				const uint32_t new_ise = weight_tab.m_rank_to_ISE[new_rank];
+								
+				// change current weight
+				astc_helpers::get_weight(refined_lblock_ise, 0, idx) = (uint8_t)new_ise;
+
+				double trial_err = 0.0f;
+				for (uint32_t j = 0; j < influenced_texels.size(); j++)
+				{
+					const uint32_t packed_texel_index = influenced_texels[j];
+					const uint32_t texel_x = packed_texel_index & 0xFF;
+					const uint32_t texel_y = packed_texel_index >> 8;
+					//const uint32_t texel_index = texel_x + texel_y * block_width;
+
+					astc_helpers::color_rgba dec_c;
+					block_decoder.decode_texel(texel_x, texel_y, dec_c);
+
+					trial_err += calc_weighted_error(dec_c, pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 0), pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 1), pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 2), pixelbuf_get_comp(src_block.m_pBuf, texel_x, texel_y, 3), enc_state);
+				}
+
+				astc_helpers::get_weight(refined_lblock_ise, 0, idx) = orig_weight_ise;
+
+				if (trial_err < best_err)
+				{
+					// accept
+					best_err = trial_err;
+					best_weight_ise = (uint8_t)new_ise;
+					changed_flag = true;
+					if (best_err == 0.0f)
+						break;
+				}
+
+			} // idir
+
+			astc_helpers::get_weight(refined_lblock_ise, 0, idx) = best_weight_ise;
+
+		} // x
+	} //y
+
+#if defined(DEBUG) || defined(_DEBUG)
+	const double error2 = compute_block_error(refined_lblock_ise, src_block, enc_state);
+	assert(error2 <= error1);
+#endif
+
+	convert_ise_lblock_to_rank(refined_lblock_ise);
+	// refined_lblock_ise is now in rank space
+
+	if (changed_flag)
+	{
+		const double refined_error = compute_block_error(refined_lblock_ise, src_block, enc_state);
+		if (refined_error < best_lblock_error)
+		{
+			best_lblock_error = refined_error;
+			memcpy(&best_lblock, &refined_lblock_ise, sizeof(best_lblock));
+
+		}
+
+		if (pAll_candidates)
+			pAll_candidates->push_back(refined_lblock_ise);
+
+		lblock_to_refine = refined_lblock_ise;
+	}
+
+	return changed_flag;
+}
+
+static double compress_single_subset_internal(
+	const single_subset_enc_context& ctx,
+	const uint8_t* pBlock_pixels, 
+	astc_helpers::log_astc_block& best_lblock, 
+	astc_lblock_vec* pAll_candidates,
+	single_subset_shortlist_state& shortlist_state,
+	bool always_compute_error, float scale_weight)
+{
+#if 0
+	{
+		basisu::rand rnd;
+		rnd.seed(1115);
+		for (; ; )
+		{
+			float endpoints[6];
+			for (uint32_t i = 0; i < 6; i++)
+				endpoints[i] = rnd.frand(-4.0f, 255.0f + 4);
+
+			int range = rnd.irand(astc_helpers::FIRST_VALID_ENDPOINT_ISE_RANGE, astc_helpers::LAST_VALID_ENDPOINT_ISE_RANGE);
+			
+			uint8_t cem_vals[6];
+			encode_cem_8_12(8, cem_vals, range, endpoints);
+		}
+	}
+#endif
+
+	assert(ctx.m_block_height <= 12);
+	assert(ctx.m_block_width <= 12);
+	assert(ctx.m_total_block_pixels == (ctx.m_block_width * ctx.m_block_height));
+	assert(ctx.m_max_candidates && (ctx.m_max_candidates <= MAX_CANDIDATES));
+		
+	const uint32_t* pBlock_pixels_u32 = reinterpret_cast<const uint32_t*>(pBlock_pixels);
+
+	const uint32_t first_pixel = pBlock_pixels_u32[0];
+
+	const int last_pixel_index = ctx.m_total_block_pixels - 1;
+
+	if (pBlock_pixels_u32[last_pixel_index] == first_pixel)
+	{
+		int i;
+		for (i = 1; i < last_pixel_index; i++)
+			if (pBlock_pixels_u32[i] != first_pixel)
+				break;
+
+		if (i == last_pixel_index)
+		{
+			astc_helpers::set_ldr_solid_block(best_lblock, pBlock_pixels[0], pBlock_pixels[1], pBlock_pixels[2], pBlock_pixels[3]);
+
+			if (pAll_candidates)
+				pAll_candidates->push_back(best_lblock);
+
+			return 0.0f;
+		}
+	}
+		
+	rgba32_image block_img;
+	block_img.m_pPixels = pBlock_pixels;
+	block_img.m_width = ctx.m_block_width;
+	block_img.m_height = ctx.m_block_height;
+	block_img.m_row_pitch_in_texels = ctx.m_block_width;
+			
+	uint32_t num_src_block_comps = 3;
+	bool src_is_luma_only = true;
+
+	if (ctx.m_has_alpha)
+	{
+		for (uint32_t i = 0; i < ctx.m_total_block_pixels; i++)
+		{
+			const uint8_t r = pBlock_pixels[i * 4 + 0];
+			const uint8_t g = pBlock_pixels[i * 4 + 1];
+			const uint8_t b = pBlock_pixels[i * 4 + 2];
+			const uint8_t a = pBlock_pixels[i * 4 + 3];
+
+			if ((r != g) || (r != b))
+				src_is_luma_only = false;
+
+			if (a != 255)
+				num_src_block_comps = 4;
+		}
+	}
+	else
+	{
+		for (uint32_t i = 0; i < ctx.m_total_block_pixels; i++)
+		{
+			const uint8_t r = pBlock_pixels[i * 4 + 0];
+			const uint8_t g = pBlock_pixels[i * 4 + 1];
+			const uint8_t b = pBlock_pixels[i * 4 + 2];
+
+			if ((r != g) || (r != b))
+			{
+				src_is_luma_only = false;
+				break;
+			}
+		}
+	}
+
+	uint32_t total_packed_configs = TOTAL_SINGLE_SUBSET_CONFIGS_RGBA;
+	const uint32_t* pPacked_configs = g_single_subset_configs_rgba;
+
+	if (src_is_luma_only)
+	{
+		total_packed_configs = TOTAL_SINGLE_SUBSET_CONFIGS_LA;
+		pPacked_configs = g_single_subset_configs_la;
+	}
+
+	init_single_subset_shortlist_state(block_img, ctx, shortlist_state, src_is_luma_only, num_src_block_comps);
+					
+	const uint32_t total_candidates = generate_single_subset_shortlist(total_packed_configs, pPacked_configs, ctx, block_img, src_is_luma_only, 
+		num_src_block_comps, shortlist_state, scale_weight, ctx.m_max_candidates);
+
+	if (!total_candidates)
+	{
+		astc_helpers::set_ldr_solid_block(best_lblock, 0xFF, 0, 0xFF, 0xFF);
+
+		if (pAll_candidates)
+			pAll_candidates->push_back(best_lblock);
+
+		assert(0);
+		return DBL_MAX;
+	}
+
+	double best_err = DBL_MAX;
+		
+	for (uint32_t cand_index = 0; cand_index < total_candidates; cand_index++)
+	{
+		astc_helpers::log_astc_block trial_lblock;
+
+		encode_single_subset_block(ctx, shortlist_state.m_pbuf, num_src_block_comps, shortlist_state.m_best_configs[cand_index], trial_lblock, ctx.m_try_base_ofs);
+
+		if (pAll_candidates)
+			pAll_candidates->push_back(trial_lblock);
+
+		if (always_compute_error || (total_candidates > 1))
+		{
+			double err = compute_block_error(trial_lblock, shortlist_state.m_pbuf, ctx);
+			if (err < best_err)
+			{
+				best_err = err;
+				best_lblock = trial_lblock;
+			}
+		}
+		else
+		{
+			best_err = -1.0f;
+			best_lblock = trial_lblock;
+		}
+	}
+
+	if (ctx.m_weight_polishing)
+	{
+		const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(
+			ctx.m_block_width, ctx.m_block_height, best_lblock.m_grid_width, best_lblock.m_grid_height);
+
+		weight_polish(shortlist_state.m_pbuf, best_lblock, ctx, pGrid_data, best_err, best_lblock, pAll_candidates);
+	}
+
+	convert_rank_lblock_to_ise(best_lblock);
+
+	return best_err;
+}
+
+double compress_single_subset(
+	single_subset_enc_context& ctx,
+	const uint8_t* pBlock_pixels,
+	astc_helpers::log_astc_block& best_lblock,
+	astc_lblock_vec* pAll_candidates,
+	bool always_compute_error)
+{
+	single_subset_shortlist_state shortlist_state;
+
+	return compress_single_subset_internal(ctx, pBlock_pixels, best_lblock, pAll_candidates, shortlist_state, always_compute_error, DEF_SCALE_WEIGHT);
+}
+
+// unpack_config() compatible
+// cems 6,8,10,12 only, gw>=gh (opposite case of gw<gh handled in code), dp ccs index handled in code
+const uint32_t TOTAL_TWO_SUBSET_CONFIGS_RGBA = 542;
+static const uint32_t g_two_subset_configs_rgba[TOTAL_TWO_SUBSET_CONFIGS_RGBA] =
+{
+	0x14556, 0x14466, 0x14566, 0x13666, 0x14476, 0x14576, 0x11676, 0xe776, 0x14386, 0x14486, 0x12586, 0xf686, 0xc786, 0x9886, 0xe558, 0xe468, 0xd568, 0xb668, 0xd478, 0xc578, 0xa678, 0x8778, 0xe388, 0xc488, 0xa588, 0x8688, 0x6788, 0x4888, 0xe55a, 0xe46a, 0xd56a, 0xb66a,
+	0xd47a, 0xc57a, 0xa67a, 0x877a, 0xe38a, 0xc48a, 0xa58a, 0x868a, 0x678a, 0x488a, 0xa55c, 0xa46c, 0x856c, 0x766c, 0x947c, 0x857c, 0x667c, 0x577c, 0xa38c, 0x848c, 0x758c, 0x568c, 0x478c, 0x34446, 0x34356, 0x34456, 0x32556, 0x34366, 0x32466, 0x2f566, 0x2b666, 0x34376,
+	0x30476, 0x2c576, 0x27676, 0x34286, 0x32386, 0x2d486, 0x29586, 0x24686, 0x2e448, 0x2e358, 0x2c458, 0x2a558, 0x2d368, 0x2b468, 0x28568, 0x26668, 0x2c378, 0x29478, 0x26578, 0x2e288, 0x2b388, 0x27488, 0x24588, 0x2e44a, 0x2e35a, 0x2c45a, 0x2a55a, 0x2d36a, 0x2b46a, 0x2856a, 0x2666a, 0x2c37a,
+	0x2947a, 0x2657a, 0x2e28a, 0x2b38a, 0x2748a, 0x2458a, 0x2944c, 0x2a35c, 0x2845c, 0x2755c, 0x2936c, 0x2746c, 0x2556c, 0x2837c, 0x2647c, 0x2457c, 0x2928c, 0x2738c, 0x2448c, 0x54346, 0x54446, 0x54356, 0x52456, 0x4e556, 0x54266, 0x53366, 0x4f466, 0x4a566, 0x46666, 0x54276, 0x51376, 0x4c476,
+	0x47576, 0x54286, 0x4f386, 0x49486, 0x4e348, 0x4c448, 0x4d358, 0x4a458, 0x48558, 0x4e268, 0x4b368, 0x48468, 0x45568, 0x4d278, 0x4a378, 0x46478, 0x4c288, 0x48388, 0x44488, 0x4e34a, 0x4c44a, 0x4d35a, 0x4a45a, 0x4855a, 0x4e26a, 0x4b36a, 0x4846a, 0x4556a, 0x4d27a, 0x4a37a, 0x4647a, 0x4c28a,
+	0x4838a, 0x4448a, 0x4a34c, 0x4844c, 0x4835c, 0x4745c, 0x4555c, 0x4a26c, 0x4736c, 0x4546c, 0x4927c, 0x4637c, 0x4447c, 0x4828c, 0x4538c, 0x74346, 0x73446, 0x74256, 0x74356, 0x6f456, 0x6b556, 0x74266, 0x71366, 0x6c466, 0x67566, 0x74276, 0x6e376, 0x68476, 0x73286, 0x6c386, 0x65486, 0x6d348,
+	0x6b448, 0x6e258, 0x6c358, 0x69458, 0x66558, 0x6d268, 0x6a368, 0x66468, 0x6c278, 0x68378, 0x64478, 0x6b288, 0x66388, 0x6d34a, 0x6b44a, 0x6e25a, 0x6c35a, 0x6945a, 0x6655a, 0x6d26a, 0x6a36a, 0x6646a, 0x6c27a, 0x6837a, 0x6447a, 0x6b28a, 0x6638a, 0x6934c, 0x6744c, 0x6a25c, 0x6835c, 0x6545c,
+	0x6926c, 0x6636c, 0x6446c, 0x6827c, 0x6537c, 0x6728c, 0x6438c, 0x94336, 0x94346, 0x91446, 0x94256, 0x92356, 0x8d456, 0x88556, 0x94266, 0x8f366, 0x89466, 0x84566, 0x93276, 0x8c376, 0x85476, 0x91286, 0x89386, 0x8e338, 0x8c348, 0x8a448, 0x8e258, 0x8b358, 0x87458, 0x84558, 0x8c268, 0x89368,
+	0x85468, 0x8b278, 0x87378, 0x8a288, 0x85388, 0x8e33a, 0x8c34a, 0x8a44a, 0x8e25a, 0x8b35a, 0x8745a, 0x8455a, 0x8c26a, 0x8936a, 0x8546a, 0x8b27a, 0x8737a, 0x8a28a, 0x8538a, 0x8a33c, 0x8834c, 0x8644c, 0x8925c, 0x8735c, 0x8445c, 0x8826c, 0x8536c, 0x8727c, 0x8437c, 0x8628c, 0xb4336, 0xb4246,
+	0xb3346, 0xaf446, 0xb4256, 0xb0356, 0xaa456, 0xa5556, 0xb3266, 0xad366, 0xa6466, 0xb1276, 0xa9376, 0xaf286, 0xa6386, 0xae338, 0xae248, 0xab348, 0xa8448, 0xad258, 0xa9358, 0xa5458, 0xab268, 0xa7368, 0xaa278, 0xa5378, 0xa8288, 0xae33a, 0xae24a, 0xab34a, 0xa844a, 0xad25a, 0xa935a, 0xa545a,
+	0xab26a, 0xa736a, 0xaa27a, 0xa537a, 0xa828a, 0xa933c, 0xaa24c, 0xa734c, 0xa544c, 0xa825c, 0xa635c, 0xa726c, 0xa436c, 0xa627c, 0xa528c, 0xd4336, 0xd4246, 0xd2346, 0xcd446, 0xd4256, 0xce356, 0xc8456, 0xd2266, 0xca366, 0xcf276, 0xc7376, 0xcd286, 0xcd338, 0xce248, 0xca348, 0xc7448, 0xcc258,
+	0xc8358, 0xc4458, 0xca268, 0xc5368, 0xc9278, 0xc7288, 0xcd33a, 0xce24a, 0xca34a, 0xc744a, 0xcc25a, 0xc835a, 0xc445a, 0xca26a, 0xc536a, 0xc927a, 0xc728a, 0xc833c, 0xc924c, 0xc734c, 0xc444c, 0xc825c, 0xc535c, 0xc726c, 0xc527c, 0xc428c, 0xf4336, 0xf4246, 0xf0346, 0xeb446, 0xf3256, 0xed356,
+	0xe6456, 0xf0266, 0xe8366, 0xee276, 0xe4376, 0xeb286, 0xec338, 0xed248, 0xe9348, 0xe6448, 0xeb258, 0xe7358, 0xe9268, 0xe4368, 0xe8278, 0xe6288, 0xec33a, 0xed24a, 0xe934a, 0xe644a, 0xeb25a, 0xe735a, 0xe926a, 0xe436a, 0xe827a, 0xe628a, 0xe833c, 0xe924c, 0xe634c, 0xe725c, 0xe435c, 0xe626c,
+	0xe527c, 0x114236, 0x113336, 0x114246, 0x10f346, 0x109446, 0x112256, 0x10a356, 0x10f266, 0x106366, 0x10c276, 0x109286, 0x10e238, 0x10b338, 0x10c248, 0x108348, 0x104448, 0x10a258, 0x105358, 0x108268, 0x106278, 0x104288, 0x10e23a, 0x10b33a, 0x10c24a, 0x10834a, 0x10444a, 0x10a25a, 0x10535a, 0x10826a, 0x10627a, 0x10428a,
+	0x10a23c, 0x10733c, 0x10824c, 0x10534c, 0x10725c, 0x10526c, 0x10427c, 0x134236, 0x132336, 0x134246, 0x12d346, 0x127446, 0x130256, 0x128356, 0x12d266, 0x124366, 0x12a276, 0x127286, 0x12e238, 0x12b338, 0x12c248, 0x127348, 0x129258, 0x124358, 0x127268, 0x125278, 0x12e23a, 0x12b33a, 0x12c24a, 0x12734a, 0x12925a, 0x12435a,
+	0x12726a, 0x12527a, 0x12923c, 0x12733c, 0x12824c, 0x12434c, 0x12625c, 0x12426c, 0x154236, 0x151336, 0x153246, 0x14c346, 0x145446, 0x150256, 0x147356, 0x14c266, 0x148276, 0x145286, 0x14d238, 0x14a338, 0x14b248, 0x146348, 0x149258, 0x146268, 0x144278, 0x14d23a, 0x14a33a, 0x14b24a, 0x14634a, 0x14925a, 0x14626a, 0x14427a,
+	0x14923c, 0x14633c, 0x14724c, 0x14434c, 0x14525c, 0x14426c, 0x174236, 0x170336, 0x172246, 0x16a346, 0x16e256, 0x165356, 0x16a266, 0x167276, 0x16d238, 0x169338, 0x16a248, 0x165348, 0x168258, 0x165268, 0x16d23a, 0x16933a, 0x16a24a, 0x16534a, 0x16825a, 0x16526a, 0x16823c, 0x16633c, 0x16724c, 0x16525c
+};
+
+const uint32_t TOTAL_THREE_SUBSET_CONFIGS_RGBA = 332;
+static const uint32_t g_three_subset_configs_rgba[TOTAL_THREE_SUBSET_CONFIGS_RGBA] =
+{
+	0xe556, 0xe466, 0xd566, 0xb666, 0xd476, 0xc576, 0xa676, 0x8776, 0xe386, 0xc486, 0xa586, 0x8686, 0x6786, 0x4886, 0x8558, 0x8468, 0x7568, 0x6668, 0x7478, 0x6578, 0x5678, 0x4778, 0x8388, 0x7488, 0x5588, 0x4688, 0x855a, 0x846a, 0x756a, 0x666a, 0x747a, 0x657a,
+	0x567a, 0x477a, 0x838a, 0x748a, 0x558a, 0x468a, 0x2e446, 0x2e356, 0x2c456, 0x2a556, 0x2d366, 0x2b466, 0x28566, 0x26666, 0x2c376, 0x29476, 0x26576, 0x2e286, 0x2b386, 0x27486, 0x24586, 0x28448, 0x28358, 0x27458, 0x25558, 0x27368, 0x26468, 0x24568, 0x27378, 0x25478, 0x28288, 0x26388,
+	0x24488, 0x2844a, 0x2835a, 0x2745a, 0x2555a, 0x2736a, 0x2646a, 0x2456a, 0x2737a, 0x2547a, 0x2828a, 0x2638a, 0x2448a, 0x4e346, 0x4c446, 0x4d356, 0x4a456, 0x48556, 0x4e266, 0x4b366, 0x48466, 0x45566, 0x4d276, 0x4a376, 0x46476, 0x4c286, 0x48386, 0x44486, 0x48348, 0x47448, 0x47358, 0x45458,
+	0x44558, 0x48268, 0x46368, 0x44468, 0x47278, 0x45378, 0x47288, 0x44388, 0x4834a, 0x4744a, 0x4735a, 0x4545a, 0x4455a, 0x4826a, 0x4636a, 0x4446a, 0x4727a, 0x4537a, 0x4728a, 0x4438a, 0x6d346, 0x6b446, 0x6e256, 0x6c356, 0x69456, 0x66556, 0x6d266, 0x6a366, 0x66466, 0x6c276, 0x68376, 0x64476,
+	0x6b286, 0x66386, 0x67348, 0x66448, 0x68258, 0x66358, 0x64458, 0x67268, 0x65368, 0x67278, 0x64378, 0x66288, 0x6734a, 0x6644a, 0x6825a, 0x6635a, 0x6445a, 0x6726a, 0x6536a, 0x6727a, 0x6437a, 0x6628a, 0x8e336, 0x8c346, 0x8a446, 0x8e256, 0x8b356, 0x87456, 0x84556, 0x8c266, 0x89366, 0x85466,
+	0x8b276, 0x87376, 0x8a286, 0x85386, 0x88338, 0x87348, 0x85448, 0x88258, 0x86358, 0x84458, 0x87268, 0x84368, 0x86278, 0x85288, 0x8833a, 0x8734a, 0x8544a, 0x8825a, 0x8635a, 0x8445a, 0x8726a, 0x8436a, 0x8627a, 0x8528a, 0xae336, 0xae246, 0xab346, 0xa8446, 0xad256, 0xa9356, 0xa5456, 0xab266,
+	0xa7366, 0xaa276, 0xa5376, 0xa8286, 0xa8338, 0xa8248, 0xa6348, 0xa4448, 0xa7258, 0xa5358, 0xa6268, 0xa5278, 0xa4288, 0xa833a, 0xa824a, 0xa634a, 0xa444a, 0xa725a, 0xa535a, 0xa626a, 0xa527a, 0xa428a, 0xcd336, 0xce246, 0xca346, 0xc7446, 0xcc256, 0xc8356, 0xc4456, 0xca266, 0xc5366, 0xc9276,
+	0xc7286, 0xc7338, 0xc8248, 0xc5348, 0xc7258, 0xc4358, 0xc5268, 0xc4278, 0xc733a, 0xc824a, 0xc534a, 0xc725a, 0xc435a, 0xc526a, 0xc427a, 0xec336, 0xed246, 0xe9346, 0xe6446, 0xeb256, 0xe7356, 0xe9266, 0xe4366, 0xe8276, 0xe6286, 0xe7338, 0xe7248, 0xe5348, 0xe6258, 0xe5268, 0xe4278, 0xe733a,
+	0xe724a, 0xe534a, 0xe625a, 0xe526a, 0xe427a, 0x10e236, 0x10b336, 0x10c246, 0x108346, 0x104446, 0x10a256, 0x105356, 0x108266, 0x106276, 0x104286, 0x108238, 0x106338, 0x107248, 0x104348, 0x105258, 0x104268, 0x10823a, 0x10633a, 0x10724a, 0x10434a, 0x10525a, 0x10426a, 0x12e236, 0x12b336, 0x12c246, 0x127346, 0x129256,
+	0x124356, 0x127266, 0x125276, 0x128238, 0x126338, 0x126248, 0x124348, 0x125258, 0x124268, 0x12823a, 0x12633a, 0x12624a, 0x12434a, 0x12525a, 0x12426a, 0x14d236, 0x14a336, 0x14b246, 0x146346, 0x149256, 0x146266, 0x144276, 0x147238, 0x145338, 0x146248, 0x144258, 0x14723a, 0x14533a, 0x14624a, 0x14425a, 0x16d236, 0x169336,
+	0x16a246, 0x165346, 0x168256, 0x165266, 0x167238, 0x165338, 0x165248, 0x164258, 0x16723a, 0x16533a, 0x16524a, 0x16425a
+};
+
+bool init_multi_subset_context(
+	subset_enc_context& ctx,
+	uint32_t max_subsets,
+	uint32_t num_carrier_candidates, uint32_t num_pattern_candidates, 
+	float two_subset_var_thresh, uint32_t two_subset_dot_thresh_fract_index, 
+	float three_subset_var_thresh,
+	astc_ldr::partitions_data* pPart_data_p2, astc_ldr::partitions_data* pPart_data_p3)
+{
+	assert((max_subsets >= 1) && (max_subsets <= 3));
+
+	assert(pPart_data_p2 && pPart_data_p2->m_part_lhs_map.is_valid());
+	assert(pPart_data_p3 && pPart_data_p3->m_part_lhs_map.is_valid());
+
+	assert(astc_helpers::is_valid_block_size(ctx.m_block_width, ctx.m_block_height) && 
+		(astc_helpers::find_astc_block_size_index(ctx.m_block_width, ctx.m_block_height) == (int)ctx.m_block_size_index));
+	
+	assert(ctx.m_total_block_pixels == (ctx.m_block_width * ctx.m_block_height));
+		
+	ctx.m_max_subsets = max_subsets;
+	ctx.m_num_carrier_candidates = num_carrier_candidates;
+	ctx.m_num_pattern_candidates = num_pattern_candidates;
+	ctx.m_two_subset_var_thresh = two_subset_var_thresh;
+	ctx.m_three_subset_var_thresh = three_subset_var_thresh;
+	ctx.m_two_subset_dot_thresh_fract_index = two_subset_dot_thresh_fract_index;
+
+	const uint32_t num_two_subset_unique_pats = basist::astc_ldr_t::get_total_unique_patterns(ctx.m_block_size_index, 2);
+
+	ctx.m_num_unique_two_subset_pats = num_two_subset_unique_pats;
+	ctx.m_pUnique_two_subset_pats = basist::astc_ldr_t::g_unique_index_to_astc_part_seed[0][ctx.m_block_size_index];
+
+	ctx.m_use_method1 = true;
+	ctx.m_use_method2 = true;
+
+	ctx.m_pPart_data_p2 = pPart_data_p2;
+	ctx.m_pPart_data_p3 = pPart_data_p3;
+	
+	for (uint32_t unique_index = 0; unique_index < num_two_subset_unique_pats; unique_index++)
+	{
+		const uint32_t seed_id = ctx.m_pUnique_two_subset_pats[unique_index];
+		assert(seed_id == basist::astc_ldr_t::unique_pat_index_to_part_seed(ctx.m_block_size_index, 2, unique_index));
+		
+		bitmask192 pat_bitmask(0, 0, 0);
+		uint32_t bit_ofs = 0;
+
+		for (uint32_t y = 0; y < ctx.m_block_height; y++)
+		{
+			for (uint32_t x = 0; x < ctx.m_block_width; x++)
+			{
+				uint64_t s = astc_helpers::get_precomputed_texel_partition(ctx.m_block_width, ctx.m_block_height, seed_id, x, y, 2);
+				
+				if (s)
+				{
+					bitmask192 b(0, 0, 0);
+					b.set_bit(bit_ofs);
+
+					pat_bitmask |= b;
+				}
+
+				++bit_ofs;
+			} // x
+		} // y
+
+		ctx.m_two_subset_pat_bitmask[unique_index] = pat_bitmask;
+
+	} // unique_index
+
+	return true;
+}
+
+// gradient descent: refines weights given current 2 subset endpoints
+// returns true if weight grid changed
+static bool weight_gradient_descent(
+	const pixelbuf& src_block,
+	astc_helpers::log_astc_block& lblock_to_refine,
+	const pixelbuf subset_pbuf[3], uint32_t subset_pixel_indices[3][256],
+	const astc_unpacked_config& best_config,
+	const subset_enc_context& enc_state,
+	const basist::astc_ldr_t::astc_block_grid_data* pGrid_data,
+	double& best_lblock_error, astc_helpers::log_astc_block& best_lblock,
+	astc_lblock_vec* pAll_candidates)
+{
+	assert(!is_lblock_ise(lblock_to_refine));
+	assert((lblock_to_refine.m_num_partitions >= 2) && (lblock_to_refine.m_num_partitions <= 3));
+		
+	const uint32_t block_width = enc_state.m_block_width, block_height = enc_state.m_block_height;
+	const uint32_t total_block_pixels = block_width * block_height;
+
+	const uint32_t num_cem_endpoint_vals = astc_helpers::get_num_cem_values(best_config.m_cem);
+	const uint32_t total_grid_weights = best_config.m_grid_width * best_config.m_grid_height;
+
+	[[maybe_unused]] const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(best_config.m_endpoint_range);
+	const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(best_config.m_weight_range);
+
+	[[maybe_unused]] const uint32_t num_weight_levels = astc_helpers::get_ise_levels(best_config.m_weight_range);
+
+	const uint32_t num_partitions = lblock_to_refine.m_num_partitions;
+
+	// compute the weights we want (the "ideal" weights) minus the upsampled weights weights we have, then project that residual back to weight grid res
+
+	// compute ideal [0,64] block weights given the current endpoints
+	uint8_t ideal_block_weight_vals[astc_helpers::MAX_BLOCK_PIXELS]; // at block res, ranks, [0,64] dequantized values
+
+#if defined(DEBUG) || defined(_DEBUG)            
+	memset(ideal_block_weight_vals, 0xFF, astc_helpers::MAX_BLOCK_PIXELS);
+#endif
+
+	for (uint32_t s = 0; s < num_partitions; s++)
+	{
+		uint8_t weights[astc_helpers::MAX_BLOCK_PIXELS];
+
+		eval_weights_first_plane(subset_pbuf[s], 
+			weights, astc_helpers::BISE_64_LEVELS, 
+			best_config.m_cem, lblock_to_refine.m_endpoints + s * num_cem_endpoint_vals, best_config.m_endpoint_range, 
+			get_num_cem_chans(best_config.m_cem));
+
+		for (uint32_t j = 0; j < subset_pbuf[s].m_width; j++)
+		{
+			const uint32_t pixel_index = subset_pixel_indices[s][j];
+			assert(pixel_index < total_block_pixels);
+
+			ideal_block_weight_vals[pixel_index] = weights[j]; // [0,64] weight value space, or in rank space
+		} // j
+
+	} // s
+
+#if defined(DEBUG) || defined(_DEBUG)
+	for (uint32_t i = 0; i < total_block_pixels; i++)
+	{
+		assert(ideal_block_weight_vals[i] != 0xFF);
+	}
+#endif
+
+	// upsample the current weight grid
+
+	uint8_t dequant_grid_weights[astc_helpers::MAX_GRID_WEIGHTS]; // grid res, [0,64]
+
+	for (uint32_t i = 0; i < total_grid_weights; i++)
+		dequant_grid_weights[i] = (uint8_t)weight_tab.get_rank_to_val(lblock_to_refine.m_weights[i]);
+
+	uint8_t dequant_block_weights[astc_helpers::MAX_BLOCK_PIXELS]; // block res, [0,64]
+	if ((lblock_to_refine.m_grid_width == (int)block_width) && (lblock_to_refine.m_grid_height == (int)block_height))
+		memcpy(dequant_block_weights, dequant_grid_weights, total_block_pixels);
+	else
+		astc_helpers::upsample_weight_grid_xuastc_ldr(block_width, block_height, lblock_to_refine.m_grid_width, lblock_to_refine.m_grid_height, dequant_grid_weights, dequant_block_weights, nullptr, nullptr);
+
+	// now compute the residual at block res
+
+	int weight_block_raw_residuals[astc_helpers::MAX_BLOCK_PIXELS]; // block res, [0,64]
+
+	for (uint32_t i = 0; i < total_block_pixels; i++)
+		weight_block_raw_residuals[i] = ideal_block_weight_vals[i] - dequant_block_weights[i];
+
+	// downsample the residuals to grid res
+
+	const basisu::vector<float>& unweighted_downsample_matrix = pGrid_data->m_unweighted_downsample_matrix;
+	const basisu::vector<float>& one_over_diag_AtA = pGrid_data->m_one_over_diag_AtA;
+
+	float weight_grid_residuals_downsampled[astc_helpers::MAX_GRID_WEIGHTS]; // grid res, [0,64]
+
+	basisu::astc_ldr::downsample_weight_residual_grid(
+		unweighted_downsample_matrix.get_ptr(),
+		block_width, block_height,		// source/from dimension (block size)
+		best_config.m_grid_width, best_config.m_grid_height,		// dest/to dimension (grid size)
+		weight_block_raw_residuals,	// these are dequantized weights, NOT ISE symbols, [by][bx]
+		weight_grid_residuals_downsampled);			// [wy][wx]
+
+	for (uint32_t i = 0; i < total_grid_weights; i++)
+		weight_grid_residuals_downsampled[i] *= one_over_diag_AtA[i];
+
+	// Apply the residuals at grid res and quantize
+	const float Q = 1.0f;
+
+	astc_helpers::log_astc_block refined_lblock(lblock_to_refine);
+
+	bool changed_flag = false;
+	
+	for (uint32_t i = 0; i < total_grid_weights; i++)
+	{
+		float v = (float)weight_tab.get_rank_to_val(lblock_to_refine.m_weights[i]) + weight_grid_residuals_downsampled[i] * Q;
+
+		const int iv = clamp((int)std::roundf(v), 0, 64);
+
+		uint8_t new_weight = (uint8_t)weight_tab.get_val_to_rank(iv);
+
+		if (refined_lblock.m_weights[i] != new_weight)
+		{
+			refined_lblock.m_weights[i] = new_weight;
+			changed_flag = true;
+		}
+	}
+
+	if (changed_flag)
+	{
+		const double refined_error = compute_block_error(refined_lblock, src_block, enc_state);
+
+		if (refined_error < best_lblock_error)
+		{
+			best_lblock_error = refined_error;
+			memcpy(&best_lblock, &refined_lblock, sizeof(best_lblock));
+		}
+		
+		if (pAll_candidates)
+			pAll_candidates->push_back(refined_lblock);
+
+		lblock_to_refine = refined_lblock;
+	}
+
+	return changed_flag;
+}
+
+// given current weight grid, refine subset endpoints
+// returns true if improved
+static bool refine_endpoints_with_current_weights(
+	const pixelbuf &src_block,
+	astc_helpers::log_astc_block& lblock_to_refine,
+	const pixelbuf subset_pbuf[3], uint32_t subset_pixel_indices[3][256],
+	const astc_unpacked_config& best_config,
+	const subset_enc_context& enc_state,
+	double& best_lblock_error, astc_helpers::log_astc_block& best_lblock,
+	astc_lblock_vec* pAll_candidates)
+{
+	assert(!is_lblock_ise(lblock_to_refine));
+	assert((lblock_to_refine.m_num_partitions >= 2) && (lblock_to_refine.m_num_partitions <= 3));
+
+	const uint32_t block_width = enc_state.m_block_width, block_height = enc_state.m_block_height;
+	const uint32_t total_block_pixels = block_width * block_height;
+
+	const uint32_t num_cem_endpoint_vals = astc_helpers::get_num_cem_values(best_config.m_cem);
+	//const uint32_t num_weight_levels = get_levels(best_config.m_weight_range);
+
+	const uint32_t total_grid_weights = best_config.m_grid_width * best_config.m_grid_height;
+
+	const uint32_t num_partitions = lblock_to_refine.m_num_partitions;
+
+	[[maybe_unused]] const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(best_config.m_endpoint_range);
+	const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(best_config.m_weight_range);
+
+	astc_helpers::log_astc_block trial_block(lblock_to_refine);
+
+	uint8_t dequant_grid_weights[astc_helpers::MAX_GRID_WEIGHTS]; // grid res, [0,64]
+
+	for (uint32_t i = 0; i < total_grid_weights; i++)
+	{
+		dequant_grid_weights[i] = (uint8_t)weight_tab.get_rank_to_val(trial_block.m_weights[i]);
+	}
+
+	uint8_t dequant_block_weights[astc_helpers::MAX_BLOCK_PIXELS]; // block res, [0,64]
+
+	if ((trial_block.m_grid_width == (int)block_width) && (trial_block.m_grid_height == (int)block_height))
+		memcpy(dequant_block_weights, dequant_grid_weights, total_block_pixels);
+	else
+		astc_helpers::upsample_weight_grid_xuastc_ldr(block_width, block_height, trial_block.m_grid_width, trial_block.m_grid_height, dequant_grid_weights, dequant_block_weights, nullptr, nullptr);
+
+	const uint32_t num_cem_chans = get_num_cem_chans(best_config.m_cem);
+
+	for (uint32_t subset_index = 0; subset_index < num_partitions; subset_index++)
+	{
+		const uint32_t num_subset_pixels = subset_pbuf[subset_index].m_width;
+		assert(num_subset_pixels);
+
+		uint8_t subset_weights[astc_helpers::MAX_BLOCK_PIXELS];
+		for (uint32_t i = 0; i < num_subset_pixels; i++)
+		{
+			const uint32_t subset_pixel_index = subset_pixel_indices[subset_index][i];
+
+			subset_weights[i] = dequant_block_weights[subset_pixel_index];
+		} // i
+
+		float new_cem_valsf[8];
+		
+		refine_endpoints_given_weights(subset_pbuf[subset_index], best_config.m_cem, subset_weights, astc_helpers::BISE_64_LEVELS,
+			lblock_to_refine.m_endpoints + num_cem_endpoint_vals * subset_index, best_config.m_endpoint_range, new_cem_valsf, num_cem_chans);
+
+		uint8_t new_cem_vals[8];
+		cem_encode(best_config.m_cem, new_cem_valsf, best_config.m_endpoint_range, new_cem_vals, true, enc_state.m_higher_effort_bc);
+
+		for (uint32_t i = 0; i < num_cem_endpoint_vals; i++)
+			trial_block.m_endpoints[num_cem_endpoint_vals * subset_index + i] = new_cem_vals[i];
+
+	} // subset_index
+
+	const double trial_error = compute_block_error(trial_block, src_block, enc_state);
+	bool status = false;
+
+	if (trial_error < best_lblock_error)
+	{
+		best_lblock_error = trial_error;
+		memcpy(&best_lblock, &trial_block, sizeof(best_lblock));
+		status = true;
+	}
+
+	if (pAll_candidates)
+		pAll_candidates->push_back(trial_block);
+
+	lblock_to_refine = trial_block;
+
+	return status;
+}
+
+static void compress_block_2subsets_internal(
+	const subset_enc_context& enc_context,
+	const uint8_t* pBlock_pixels, 
+	double& best_lblock_error, astc_helpers::log_astc_block& best_lblock,
+	astc_lblock_vec* pAll_candidates,
+	single_subset_shortlist_state &shortlist_state)
+{
+	assert(enc_context.m_use_method1 || enc_context.m_use_method2);
+		
+	const uint32_t block_width = enc_context.m_block_width, block_height = enc_context.m_block_height;
+	const uint32_t total_block_pixels = enc_context.m_total_block_pixels;
+
+	const uint32_t num_src_block_chans = shortlist_state.m_num_src_block_comps;
+	[[maybe_unused]] const bool has_a = (num_src_block_chans == 4);
+
+	rgba32_image block_img;
+	block_img.m_pPixels = pBlock_pixels;
+	block_img.m_width = block_width;
+	block_img.m_height = block_height;
+	block_img.m_row_pitch_in_texels = block_width;
+
+	const float SUBSET_SCALE_WEIGHT = 2.0f;
+
+	const uint32_t total_candidates = generate_single_subset_shortlist(
+		TOTAL_TWO_SUBSET_CONFIGS_RGBA, g_two_subset_configs_rgba,
+		enc_context,
+		block_img,
+		shortlist_state.m_src_is_luma_only,
+		shortlist_state.m_num_src_block_comps,
+		shortlist_state, SUBSET_SCALE_WEIGHT, enc_context.m_num_carrier_candidates);
+
+	if (!total_candidates)
+	{
+		assert(0);
+		return;
+	}
+		
+#if 0
+	encode_single_subset_block(enc_context, shortlist_state.m_pbuf, shortlist_state.m_num_src_block_comps, shortlist_state.m_best_configs[0], best_lblock, false);
+
+	best_lblock.m_color_endpoint_modes[1] = best_lblock.m_color_endpoint_modes[0];
+	
+	uint32_t num_endpoint_vals = astc_helpers::get_num_cem_values(best_lblock.m_color_endpoint_modes[0]);
+	memcpy(best_lblock.m_endpoints + num_endpoint_vals, best_lblock.m_endpoints, num_endpoint_vals);
+
+	best_lblock.m_num_partitions = 2;
+	best_lblock.m_partition_id = 1;
+
+	convert_rank_lblock_to_ise(best_lblock);
+	return;
+#endif
+		
+	static const uint8_t s_num_dot_thresh_fracs[NUM_DOT_THRESH_FRACTS] = { 1, 3, 5, 9, 11, 15 };
+
+	static const float s_dot_thresh_fracs15[15] = {	-.55f, -0.45f, -0.35f, -0.25f, -0.15f, -0.10f, -0.075f, 0.0f, 0.075f, 0.10f, 0.15f, 0.25f, 0.35f, 0.45f, 0.55f };
+	static const float s_dot_thresh_fracs11[11] = { -0.45f, -0.35f, -0.25f, -0.15f, -0.075f, 0.0f, 0.075f, 0.15f, 0.25f, 0.35f, 0.45f };
+	static const float s_dot_thresh_fracs9[9] = { -0.35f, -0.25f, -0.15f, -0.075f, 0.0f, 0.075f, 0.15f, 0.25f, 0.35f };
+	static const float s_dot_thresh_fracs5[5] = { -.2f, -0.1f, 0.0f, .1f, .2f };
+	static const float s_dot_thresh_fracs3[3] = { -0.1f, 0.0f, .1f };
+	static const float s_dot_thresh_fracs1[1] = { 0.0f };
+	static const float* s_pDot_thresh_fracts[NUM_DOT_THRESH_FRACTS] = { s_dot_thresh_fracs1, s_dot_thresh_fracs3, s_dot_thresh_fracs5, s_dot_thresh_fracs9, s_dot_thresh_fracs11, s_dot_thresh_fracs15 };
+		
+	const uint32_t dt_index = minimum<uint32_t>(NUM_DOT_THRESH_FRACTS - 1, enc_context.m_two_subset_dot_thresh_fract_index);
+	const uint32_t num_dot_thresh_fracts = s_num_dot_thresh_fracs[dt_index];
+	const float* const pDot_thresh_fracts = s_pDot_thresh_fracts[dt_index];
+		
+	float dot_range = 0;
+
+	if (num_dot_thresh_fracts > 1)
+	{
+		const block_stats& stats = shortlist_state.m_stats;
+
+		const float mean_r = stats.m_mean[0], mean_g = stats.m_mean[1], mean_b = stats.m_mean[2], mean_a = stats.m_mean[3];
+		const float axis_r = stats.m_axis[0], axis_g = stats.m_axis[1], axis_b = stats.m_axis[2], axis_a = stats.m_axis[3];
+
+		float min_dot = FLT_MAX;
+		float max_dot = -FLT_MAX;
+
+		const uint8_t* pSrc_pixels = pBlock_pixels;
+
+		for (uint32_t y = 0; y < block_height; y++)
+		{
+			for (uint32_t x = 0; x < block_width; x++)
+			{
+				const float r = (float)pSrc_pixels[0] - mean_r;
+				const float g = (float)pSrc_pixels[1] - mean_g;
+				const float b = (float)pSrc_pixels[2] - mean_b;
+				const float a = (float)pSrc_pixels[3] - mean_a;
+				pSrc_pixels += 4;
+
+				const float dot = r * axis_r + g * axis_g + b * axis_b + a * axis_a;
+
+				min_dot = minimum(min_dot, dot);
+				max_dot = maximum(max_dot, dot);
+			}
+		}
+		
+		dot_range = max_dot - min_dot;
+	}
+
+	for (uint32_t config_cand_index = 0; config_cand_index < total_candidates; config_cand_index++)
+	{
+		const astc_unpacked_config& best_config = shortlist_state.m_best_configs[config_cand_index];
+
+		const bool cem_has_a = does_cem_have_alpha(best_config.m_cem);
+		const uint32_t num_cem_comps = get_num_cem_chans(best_config.m_cem);
+
+		const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, best_config.m_grid_width, best_config.m_grid_height);
+
+		astc_helpers::log_astc_block carrier_lblock; // carrier block in rank space
+
+		encode_single_subset_block(enc_context, shortlist_state.m_pbuf, shortlist_state.m_num_src_block_comps, best_config, carrier_lblock, false);
+
+		// base+ofs is so marginal that it's not worth the effort/extra complexity here
+		assert(!is_cem_9_or_13(carrier_lblock.m_color_endpoint_modes[0]));
+
+		convert_ise_lblock_to_rank(carrier_lblock);
+
+		for (uint32_t dot_thresh_fract_iter = 0; dot_thresh_fract_iter < num_dot_thresh_fracts; dot_thresh_fract_iter++)
+		{
+			const float dot_thresh_fract = pDot_thresh_fracts[dot_thresh_fract_iter];
+
+			bitmask192 desired_bitmask(0, 0, 0);
+
+			{
+				const block_stats& stats = shortlist_state.m_stats;
+
+				const float mean_r = stats.m_mean[0], mean_g = stats.m_mean[1], mean_b = stats.m_mean[2], mean_a = stats.m_mean[3];
+				const float axis_r = stats.m_axis[0], axis_g = stats.m_axis[1], axis_b = stats.m_axis[2], axis_a = stats.m_axis[3];
+
+				uint32_t bit_ofs = 0;
+				const uint8_t* pSrc_pixels = pBlock_pixels;
+
+				const float subset_dot_thresh = dot_thresh_fract * dot_range;
+
+				for (uint32_t y = 0; y < block_height; y++)
+				{
+					for (uint32_t x = 0; x < block_width; x++)
+					{
+						const float r = ((float)pSrc_pixels[0] - mean_r);
+						const float g = ((float)pSrc_pixels[1] - mean_g);
+						const float b = ((float)pSrc_pixels[2] - mean_b);
+						const float a = ((float)pSrc_pixels[3] - mean_a);
+						pSrc_pixels += 4;
+
+						const float dot = (r * axis_r) + (g * axis_g) + (b * axis_b) + (a * axis_a);
+
+						const uint32_t s = (dot > subset_dot_thresh) ? 1 : 0;
+						desired_bitmask.set_bit(bit_ofs, s);
+
+						bit_ofs++;
+					} // x
+				} // y
+			}
+
+			const bitmask192 active_bitmask(bitmask192::lsb_mask(total_block_pixels));
+			const bitmask192 desired_bitmask_inverted = (~desired_bitmask) & active_bitmask;
+
+			assert(desired_bitmask <= active_bitmask);
+			assert(desired_bitmask_inverted <= active_bitmask);
+
+			uint32_t best_diffs[MAX_UNIQUE_2SUBSET_PATS];
+			assert(enc_context.m_num_unique_two_subset_pats <= MAX_UNIQUE_2SUBSET_PATS);
+
+			if (enc_context.m_num_pattern_candidates == 1)
+			{
+				uint32_t best_diff = UINT32_MAX, best_diff_inverted = UINT32_MAX;
+
+				for (uint32_t i = 0; i < enc_context.m_num_unique_two_subset_pats; i++)
+				{
+					const bitmask192 pat_bitmask = enc_context.m_two_subset_pat_bitmask[i];
+			
+					const uint32_t diff = (popcount192(pat_bitmask ^ desired_bitmask) << 16) | i;
+					const uint32_t diff_inverted = (popcount192(pat_bitmask ^ desired_bitmask_inverted) << 16) | i;
+
+					best_diff = minimum(diff, best_diff);
+					best_diff_inverted = minimum(diff_inverted, best_diff_inverted);
+				}
+
+				best_diffs[0] = minimum(best_diff, best_diff_inverted);
+			}
+			else
+			{
+				for (uint32_t i = 0; i < enc_context.m_num_unique_two_subset_pats; i++)
+				{
+					const bitmask192 pat_bitmask = enc_context.m_two_subset_pat_bitmask[i];
+					assert(pat_bitmask <= active_bitmask);
+
+					const uint32_t diff = popcount192(pat_bitmask ^ desired_bitmask);
+					const uint32_t diff_inverted = popcount192(pat_bitmask ^ desired_bitmask_inverted);
+
+					best_diffs[i] = (minimum(diff, diff_inverted) << 16) + i;
+				}
+
+				std::sort(best_diffs, best_diffs + enc_context.m_num_unique_two_subset_pats);
+			}
+
+			for (uint32_t pat_cand_iter = 0; pat_cand_iter < enc_context.m_num_pattern_candidates; pat_cand_iter++)
+			{
+				const uint32_t best_unique_pat_index = best_diffs[pat_cand_iter] & 1023;
+				assert(best_unique_pat_index < enc_context.m_num_unique_two_subset_pats);
+
+				const bitmask192 best_pat_bitmask = enc_context.m_two_subset_pat_bitmask[best_unique_pat_index];
+
+				const uint32_t best_seed_id = enc_context.m_pUnique_two_subset_pats[best_unique_pat_index];
+
+				// [3] but we only use [2] here
+
+				float subset_pixels[3][PIXELBUF_SIZE_IN_FLOATS];
+
+				pixelbuf subset_pbuf[3];
+				subset_pbuf[0].m_pBuf = subset_pixels[0];
+				subset_pbuf[0].m_width = 0;
+				subset_pbuf[0].m_height = 1;
+
+				subset_pbuf[1].m_pBuf = subset_pixels[1];
+				subset_pbuf[1].m_width = 0;
+				subset_pbuf[1].m_height = 1;
+
+				uint32_t subset_pixel_indices[3][256];
+
+				const uint8_t* pSrc_pixels = pBlock_pixels;
+				uint32_t bit_ofs = 0;
+						
+				for (uint32_t y = 0; y < block_height; y++)
+				{
+					for (uint32_t x = 0; x < block_width; x++)
+					{
+						uint32_t s = (uint32_t)(best_pat_bitmask.is_bit_set(bit_ofs));
+						bit_ofs++;
+
+						const float r = (float)pSrc_pixels[0];
+						const float g = (float)pSrc_pixels[1];
+						const float b = (float)pSrc_pixels[2];
+						const float a = cem_has_a ? (float)pSrc_pixels[3] : 255;
+						pSrc_pixels += 4;
+
+						const uint32_t cur_width = subset_pbuf[s].m_width;
+
+						subset_pixel_indices[s][cur_width] = x + y * block_width;
+
+						pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 0, r);
+						pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 1, g);
+						pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 2, b);
+						pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 3, a);
+
+						subset_pbuf[s].m_width = cur_width + 1;
+
+					} // x
+
+				} // y
+
+				assert(subset_pbuf[0].m_width && subset_pbuf[1].m_width);
+
+				// -------
+
+				astc_helpers::log_astc_block final_lblock;
+				final_lblock.clear();
+				final_lblock.m_user_mode = cUserModeRankValues;
+
+				final_lblock.m_grid_width = carrier_lblock.m_grid_width;
+				final_lblock.m_grid_height = carrier_lblock.m_grid_height;
+				final_lblock.m_endpoint_ise_range = best_config.m_endpoint_range;
+				final_lblock.m_weight_ise_range = best_config.m_weight_range;
+				final_lblock.m_num_partitions = 2;
+				final_lblock.m_partition_id = safe_cast_uint16(best_seed_id);
+				final_lblock.m_color_endpoint_modes[0] = best_config.m_cem;
+				final_lblock.m_color_endpoint_modes[1] = best_config.m_cem;
+
+				// -------
+
+				const uint32_t num_cem_endpoint_vals = astc_helpers::get_num_cem_values(best_config.m_cem);
+				const uint32_t total_grid_weights = carrier_lblock.m_grid_width * carrier_lblock.m_grid_height;
+
+				//const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(best_config.m_endpoint_range);
+				const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(best_config.m_weight_range);
+
+				[[maybe_unused]] const uint32_t num_weight_levels = astc_helpers::get_ise_levels(best_config.m_weight_range);
+
+				// ------- method 1
+				if (enc_context.m_use_method1)
+				{
+					memcpy(final_lblock.m_weights, carrier_lblock.m_weights, total_grid_weights);
+					memcpy(final_lblock.m_endpoints, carrier_lblock.m_endpoints, num_cem_endpoint_vals);
+					memcpy(final_lblock.m_endpoints + num_cem_endpoint_vals, carrier_lblock.m_endpoints, num_cem_endpoint_vals);
+
+					const uint32_t NUM_M1_PASSES = 2;
+
+					for (uint32_t pass = 0; pass < NUM_M1_PASSES; pass++)
+					{
+						refine_endpoints_with_current_weights(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context,
+							best_lblock_error, best_lblock, nullptr);
+
+						bool changed_flag;
+
+						if (pass == (NUM_M1_PASSES - 1))
+						{
+							changed_flag = weight_polish(shortlist_state.m_pbuf, final_lblock, enc_context, pGrid_data,
+								best_lblock_error, best_lblock, nullptr);
+						}
+						else
+						{
+							changed_flag = weight_gradient_descent(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context, pGrid_data,
+								best_lblock_error, best_lblock, nullptr);
+						}
+
+						if (!changed_flag)
+						{
+							break;
+						}
+					}
+
+					if (pAll_candidates)
+					{
+						pAll_candidates->push_back(final_lblock);
+					}
+				}
+
+				double best_pat_lblock_error = DBL_MAX;
+				astc_helpers::log_astc_block best_pat_lblock;
+
+				// ------- method 2
+				if (enc_context.m_use_method2)
+				{
+					uint8_t desired_block_weights[astc_helpers::MAX_BLOCK_PIXELS]; // at block res, ranks
+
+#if defined(DEBUG) || defined(_DEBUG)            
+					memset(desired_block_weights, 0xFF, astc_helpers::MAX_BLOCK_PIXELS);
+#endif					
+					for (uint32_t s = 0; s < 2; s++)
+					{
+						float initial_endpoints[8];
+						calc_initial_cem_endpoints(subset_pbuf[s], initial_endpoints, num_cem_comps, nullptr, is_cem_6_or_10(best_config.m_cem));
+
+						uint8_t* pSubset_CEM_vals = final_lblock.m_endpoints + s * num_cem_endpoint_vals;
+						cem_encode(best_config.m_cem, initial_endpoints, best_config.m_endpoint_range, pSubset_CEM_vals, true, enc_context.m_higher_effort_bc);
+
+						uint8_t subset_weights[astc_helpers::MAX_BLOCK_PIXELS];
+						eval_weights_first_plane(subset_pbuf[s], subset_weights, best_config.m_weight_range,
+							best_config.m_cem, pSubset_CEM_vals, best_config.m_endpoint_range,
+							num_cem_comps);
+
+						for (uint32_t i = 0; i < enc_context.m_num_ls_iterations; i++)
+						{
+							float refined_endpoints[8];
+							refine_endpoints_given_weights(subset_pbuf[s], best_config.m_cem, 
+								subset_weights, best_config.m_weight_range, pSubset_CEM_vals, best_config.m_endpoint_range, refined_endpoints, num_cem_comps);
+														
+							cem_encode(best_config.m_cem, refined_endpoints, best_config.m_endpoint_range, pSubset_CEM_vals, true, enc_context.m_higher_effort_bc);
+
+							eval_weights_first_plane(subset_pbuf[s], subset_weights, best_config.m_weight_range,
+								best_config.m_cem, pSubset_CEM_vals, best_config.m_endpoint_range,
+								num_cem_comps);
+						} // i
+
+						// base+ofs is so rarely a win (< ~1%) that it's not worth the trouble
+
+						for (uint32_t j = 0; j < subset_pbuf[s].m_width; j++)
+						{
+							const uint32_t pixel_index = subset_pixel_indices[s][j];
+							assert(pixel_index < total_block_pixels);
+
+							desired_block_weights[pixel_index] = subset_weights[j];
+						}
+
+					} // s
+
+#if defined(DEBUG) || defined(_DEBUG)
+					for (uint32_t i = 0; i < total_block_pixels; i++)
+					{
+						assert(desired_block_weights[i] != 0xFF);
+					}
+#endif
+
+					// now downsample the ideal weights to grid res, quantize
+					// desired_block_weights[] is either [0,64] (dequantized weight values) or in the carrier's weight rank space depending on solve_weights_dequantized
+
+					if ((block_width == best_config.m_grid_width) && (block_height == best_config.m_grid_height))
+					{
+						assert(total_block_pixels == total_grid_weights);
+
+memcpy(final_lblock.m_weights, desired_block_weights, total_block_pixels);
+					}
+					else
+					{
+						// dequantize ranks to [0,64] values
+						for (uint32_t i = 0; i < total_block_pixels; i++)
+							desired_block_weights[i] = (uint8_t)weight_tab.get_rank_to_val(desired_block_weights[i]);
+
+						// downsample from block to grid res, [0,64] values
+						uint8_t downsampled_weights[astc_helpers::MAX_GRID_WEIGHTS];
+
+#if 0
+						basisu::downsample_weight_grid(pGrid_data->m_downsample_matrix.get_ptr(),
+							block_width, block_height,
+							best_config.m_grid_width, best_config.m_grid_height,
+							desired_block_weights,
+							downsampled_weights);
+#else
+						float src_temp[PIXELBUF_COMP_PITCH];
+						pixelbuf src_pbuf(block_width, block_height, src_temp);
+						for (uint32_t y = 0; y < block_height; y++)
+							for (uint32_t x = 0; x < block_width; x++)
+								pixelbuf_set_comp(src_pbuf, x, y, 0, desired_block_weights[x + y * block_width]);
+
+						float dst_temp[PIXELBUF_COMP_PITCH];
+						pixelbuf dst_pbuf(best_config.m_grid_width, best_config.m_grid_height, dst_temp);
+
+						pseudoinverse_block_to_grid(src_pbuf, dst_pbuf, 1);
+						for (uint32_t y = 0; y < best_config.m_grid_height; y++)
+							for (uint32_t x = 0; x < best_config.m_grid_width; x++)
+								downsampled_weights[x + y * best_config.m_grid_width] = (uint8_t)clamp((int)std::round(pixelbuf_get_comp(dst_pbuf, x, y, 0)), 0, 64);
+#endif
+
+						// quantize downsampled weights
+						for (uint32_t i = 0; i < total_grid_weights; i++)
+							final_lblock.m_weights[i] = (uint8_t)weight_tab.get_val_to_rank(downsampled_weights[i]);
+					}
+
+					{
+						// convert from rank to ISE space
+						const double subset_error = compute_block_error(final_lblock, shortlist_state.m_pbuf, enc_context);
+
+						if (subset_error < best_pat_lblock_error)
+						{
+							best_pat_lblock_error = subset_error;
+							best_pat_lblock = final_lblock;
+						}
+					}
+
+					const uint32_t NUM_M2_PASSES = 2;
+
+					for (uint32_t pass = 0; pass < NUM_M2_PASSES; pass++)
+					{
+						refine_endpoints_with_current_weights(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context,
+							best_pat_lblock_error, best_pat_lblock, nullptr);
+
+						bool changed_flag;
+
+						if (pass == (NUM_M2_PASSES - 1))
+						{
+							changed_flag = weight_polish(shortlist_state.m_pbuf, final_lblock, enc_context, pGrid_data,
+								best_pat_lblock_error, best_pat_lblock, nullptr);
+						}
+						else
+						{
+							changed_flag = weight_gradient_descent(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context, pGrid_data,
+								best_pat_lblock_error, best_pat_lblock, nullptr);
+						}
+
+						if (!changed_flag)
+						{
+							break;
+						}
+					}
+
+					assert(best_pat_lblock_error != DBL_MAX);
+					if (best_pat_lblock_error < best_lblock_error)
+					{
+						best_lblock_error = best_pat_lblock_error;
+						best_lblock = best_pat_lblock;
+					}
+
+					if (pAll_candidates)
+					{
+						pAll_candidates->push_back(best_pat_lblock);
+					}
+				}
+
+			} // pat_cand_iter
+
+		} // ds
+
+	} // config_cand_index
+
+	convert_rank_lblock_to_ise(best_lblock);
+}
+
+static bool create_desired_partitions_3subsets(
+	const block_stats& stats,
+	uint32_t total_block_pixels,
+	const uint8_t *pBlock_pixels,
+	uint8_t *pDesired_part)
+{
+	memset(pDesired_part, 0, total_block_pixels);
+
+	const uint32_t NUM_SUBSETS = 3;
+
+	const float mean_r = stats.m_mean[0], mean_g = stats.m_mean[1], mean_b = stats.m_mean[2], mean_a = stats.m_mean[3];
+	const float axis_r = stats.m_axis[0], axis_g = stats.m_axis[1], axis_b = stats.m_axis[2], axis_a = stats.m_axis[3];
+
+	float cluster_centroids[NUM_SUBSETS][4];
+	clear_obj(cluster_centroids);
+
+	float brightest_inten = -BIG_FLOAT_VAL, darkest_inten = BIG_FLOAT_VAL;
+
+	const uint8_t* pSrc_pixels = pBlock_pixels;
+	for (uint32_t i = 0; i < total_block_pixels; i++, pSrc_pixels += 4)
+	{
+		float v[4];
+		vec4_load_u8(v, pSrc_pixels);
+				
+		const float inten = 
+			((v[0] - mean_r) * axis_r) + ((v[1] - mean_g) * axis_g) + 
+			((v[2] - mean_b) * axis_b) + ((v[3] - mean_a) * axis_a);
+
+		if (inten < darkest_inten)
+		{
+			darkest_inten = inten;
+			vec4_copy(cluster_centroids[0], v);
+		}
+
+		if (inten > brightest_inten)
+		{
+			brightest_inten = inten;
+			vec4_copy(cluster_centroids[1], v);
+		}
+
+	} // i
+
+	float furthest_dist2 = 0.0f;
+
+	vec4_copy(cluster_centroids[2], cluster_centroids[0]);
+
+	pSrc_pixels = pBlock_pixels;
+	for (uint32_t i = 0; i < total_block_pixels; i++, pSrc_pixels += 4)
+	{
+		float v[4];
+		vec4_set(v, (float)pSrc_pixels[0], (float)pSrc_pixels[1], (float)pSrc_pixels[2], (float)pSrc_pixels[3]);
+
+		float dist_a = vec4_squared_dist(v, cluster_centroids[0]);
+		if (dist_a == 0.0f)
+			continue;
+
+		float dist_b = vec4_squared_dist(v, cluster_centroids[1]);
+		if (dist_b == 0.0f)
+			continue;
+
+		float dist2 = dist_a + dist_b;
+		if (dist2 > furthest_dist2)
+		{
+			furthest_dist2 = dist2;
+			vec4_copy(cluster_centroids[2], v);
+		}
+	}
+
+	if (vec4_compare(cluster_centroids[0], cluster_centroids[1]) ||
+		vec4_compare(cluster_centroids[0], cluster_centroids[2]) ||
+		vec4_compare(cluster_centroids[1], cluster_centroids[2]))
+	{
+		return false;
+	}
+		
+	uint32_t num_cluster_pixels[NUM_SUBSETS];
+
+	const uint32_t NUM_ITERS = 4;
+		
+	for (uint32_t s = 0; s < NUM_ITERS; s++)
+	{
+		float new_cluster_means[NUM_SUBSETS][4];
+
+		clear_obj(num_cluster_pixels);
+		clear_obj(new_cluster_means);
+
+		pSrc_pixels = pBlock_pixels;
+
+		bool changed_flag = false;
+
+		for (uint32_t i = 0; i < total_block_pixels; i++, pSrc_pixels += 4)
+		{
+			float v[4];
+			vec4_set(v, (float)pSrc_pixels[0], (float)pSrc_pixels[1], (float)pSrc_pixels[2], (float)pSrc_pixels[3]);
+
+			const float d0 = vec4_squared_dist(v, cluster_centroids[0]);
+			const float d1 = vec4_squared_dist(v, cluster_centroids[1]);
+			const float d2 = vec4_squared_dist(v, cluster_centroids[2]);
+
+			uint32_t idx = 0; float md = d0;
+			if (d1 < md) { md = d1; idx = 1; }
+			if (d2 < md) { idx = 2; }
+
+			if (idx != pDesired_part[i])
+				changed_flag = true;
+
+			pDesired_part[i] = (uint8_t)idx;
+
+			vec4_add(new_cluster_means[idx], v);
+
+			num_cluster_pixels[idx]++;
+		} // i
+
+		if (!num_cluster_pixels[0] || !num_cluster_pixels[1] || !num_cluster_pixels[2])
+			return false;
+
+		if (!changed_flag)
+			break;
+
+		if (s < (NUM_ITERS - 1))
+		{
+			for (uint32_t j = 0; j < NUM_SUBSETS; j++)
+				vec4_scale(cluster_centroids[j], new_cluster_means[j], 1.0f / (float)num_cluster_pixels[j]);
+		}
+
+	} // s
+
+	return true;
+}
+
+static void compress_block_3subsets_internal(
+	const subset_enc_context& enc_context,
+	subset_enc_thread_context& enc_thread_context,
+	const uint8_t* pBlock_pixels,
+	double& best_lblock_error, astc_helpers::log_astc_block& best_lblock,
+	astc_lblock_vec* pAll_candidates,
+	single_subset_shortlist_state& shortlist_state)
+{
+	const uint32_t block_width = enc_context.m_block_width, block_height = enc_context.m_block_height;
+	const uint32_t total_block_pixels = enc_context.m_total_block_pixels;
+
+	const uint32_t num_src_block_chans = shortlist_state.m_num_src_block_comps;
+	[[maybe_unused]] const bool has_a = (num_src_block_chans == 4);
+
+	rgba32_image block_img;
+	block_img.m_pPixels = pBlock_pixels;
+	block_img.m_width = block_width;
+	block_img.m_height = block_height;
+	block_img.m_row_pitch_in_texels = block_width;
+
+	const float SUBSET_SCALE_WEIGHT = 2.0f;
+
+	const uint32_t total_candidates = generate_single_subset_shortlist(
+		TOTAL_THREE_SUBSET_CONFIGS_RGBA, g_three_subset_configs_rgba,
+		enc_context,
+		block_img,
+		shortlist_state.m_src_is_luma_only,
+		shortlist_state.m_num_src_block_comps,
+		shortlist_state, SUBSET_SCALE_WEIGHT, enc_context.m_num_carrier_candidates);
+
+	if (!total_candidates)
+	{
+		assert(0);
+		return;
+	}
+
+#if 0
+	encode_single_subset_block(enc_context, shortlist_state.m_pbuf, shortlist_state.m_num_src_block_comps, shortlist_state.m_best_configs[0], best_lblock, false);
+
+	best_lblock.m_color_endpoint_modes[1] = best_lblock.m_color_endpoint_modes[0];
+	best_lblock.m_color_endpoint_modes[2] = best_lblock.m_color_endpoint_modes[0];
+
+	const uint32_t num_endpoint_vals = astc_helpers::get_num_cem_values(best_lblock.m_color_endpoint_modes[0]);
+	memcpy(best_lblock.m_endpoints + num_endpoint_vals, best_lblock.m_endpoints, num_endpoint_vals);
+	memcpy(best_lblock.m_endpoints + num_endpoint_vals * 2, best_lblock.m_endpoints, num_endpoint_vals);
+
+	best_lblock.m_num_partitions = 3;
+	best_lblock.m_partition_id = 1;
+
+	convert_rank_lblock_to_ise(best_lblock);
+	return;
+#endif
+		
+	enc_thread_context.m_pat_vec.init(enc_context.m_block_width, enc_context.m_block_height);
+
+	if (!create_desired_partitions_3subsets(shortlist_state.m_stats, total_block_pixels, pBlock_pixels, enc_thread_context.m_pat_vec.m_parts))
+		return;
+
+	astc_ldr::partitions_data* pPart_data = enc_context.m_pPart_data_p3;
+
+	uint32_t cand_patterns[MAX_UNIQUE_3SUBSET_PATS];
+	assert(enc_context.m_num_pattern_candidates <= MAX_UNIQUE_3SUBSET_PATS);
+	const uint32_t num_pat_candidates = pPart_data->m_part_lhs_map.find(enc_thread_context.m_pat_vec, cand_patterns, enc_context.m_num_pattern_candidates, false);
+
+	if (!num_pat_candidates)
+		return;
+
+	for (uint32_t config_cand_index = 0; config_cand_index < total_candidates; config_cand_index++)
+	{
+		const astc_unpacked_config& best_config = shortlist_state.m_best_configs[config_cand_index];
+
+		const bool cem_has_a = does_cem_have_alpha(best_config.m_cem);
+		const uint32_t num_cem_comps = get_num_cem_chans(best_config.m_cem);
+
+		const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, best_config.m_grid_width, best_config.m_grid_height);
+
+		astc_helpers::log_astc_block carrier_lblock; // carrier block in rank space
+
+		encode_single_subset_block(enc_context, shortlist_state.m_pbuf, shortlist_state.m_num_src_block_comps, best_config, carrier_lblock, false);
+
+		// base+ofs is so marginal that it's not worth the effort/extra complexity here
+		assert(!is_cem_9_or_13(carrier_lblock.m_color_endpoint_modes[0]));
+
+		convert_ise_lblock_to_rank(carrier_lblock);
+
+		for (uint32_t pat_cand_iter = 0; pat_cand_iter < num_pat_candidates; pat_cand_iter++)
+		{
+			const uint32_t best_unique_pat_index = cand_patterns[pat_cand_iter];
+			assert(best_unique_pat_index < pPart_data->m_total_unique_patterns);
+						
+			const uint32_t best_seed_id = pPart_data->m_unique_index_to_part_seed[best_unique_pat_index];
+			assert((int)best_unique_pat_index == pPart_data->m_part_seed_to_unique_index[best_seed_id]);
+				
+			float subset_pixels[3][PIXELBUF_SIZE_IN_FLOATS];
+
+			pixelbuf subset_pbuf[3];
+			subset_pbuf[0].m_pBuf = subset_pixels[0];
+			subset_pbuf[0].m_width = 0;
+			subset_pbuf[0].m_height = 1;
+
+			subset_pbuf[1].m_pBuf = subset_pixels[1];
+			subset_pbuf[1].m_width = 0;
+			subset_pbuf[1].m_height = 1;
+
+			subset_pbuf[2].m_pBuf = subset_pixels[2];
+			subset_pbuf[2].m_width = 0;
+			subset_pbuf[2].m_height = 1;
+
+			uint32_t subset_pixel_indices[3][256];
+
+			const uint8_t* pSrc_pixels = pBlock_pixels;
+			[[maybe_unused]] uint32_t bit_ofs = 0;
+
+			for (uint32_t y = 0; y < block_height; y++)
+			{
+				for (uint32_t x = 0; x < block_width; x++)
+				{
+					const uint32_t s = pPart_data->m_partition_pats[best_unique_pat_index](x, y);
+					assert(s < 3);
+
+					const float r = (float)pSrc_pixels[0];
+					const float g = (float)pSrc_pixels[1];
+					const float b = (float)pSrc_pixels[2];
+					const float a = cem_has_a ? (float)pSrc_pixels[3] : 255;
+					pSrc_pixels += 4;
+
+					const uint32_t cur_width = subset_pbuf[s].m_width;
+
+					subset_pixel_indices[s][cur_width] = x + y * block_width;
+
+					pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 0, r);
+					pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 1, g);
+					pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 2, b);
+					pixelbuf_set_comp(subset_pbuf[s], cur_width, 0, 3, a);
+
+					subset_pbuf[s].m_width = cur_width + 1;
+
+				} // x
+
+			} // y
+
+			assert(subset_pbuf[0].m_width && subset_pbuf[1].m_width && subset_pbuf[2].m_width);
+
+			// -------
+
+			astc_helpers::log_astc_block final_lblock;
+			final_lblock.clear();
+			final_lblock.m_user_mode = cUserModeRankValues;
+
+			final_lblock.m_grid_width = carrier_lblock.m_grid_width;
+			final_lblock.m_grid_height = carrier_lblock.m_grid_height;
+			final_lblock.m_endpoint_ise_range = best_config.m_endpoint_range;
+			final_lblock.m_weight_ise_range = best_config.m_weight_range;
+			final_lblock.m_num_partitions = 3;
+			final_lblock.m_partition_id = safe_cast_uint16(best_seed_id);
+			final_lblock.m_color_endpoint_modes[0] = best_config.m_cem;
+			final_lblock.m_color_endpoint_modes[1] = best_config.m_cem;
+			final_lblock.m_color_endpoint_modes[2] = best_config.m_cem;
+
+			// -------
+
+			const uint32_t num_cem_endpoint_vals = astc_helpers::get_num_cem_values(best_config.m_cem);
+			const uint32_t total_grid_weights = carrier_lblock.m_grid_width * carrier_lblock.m_grid_height;
+
+			//const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(best_config.m_endpoint_range);
+			const auto& weight_tab = astc_helpers::g_dequant_tables.get_weight_tab(best_config.m_weight_range);
+
+			[[maybe_unused]] const uint32_t num_weight_levels = astc_helpers::get_ise_levels(best_config.m_weight_range);
+
+			// ------- method 1
+			if (enc_context.m_use_method1)
+			{
+				memcpy(final_lblock.m_weights, carrier_lblock.m_weights, total_grid_weights);
+				memcpy(final_lblock.m_endpoints, carrier_lblock.m_endpoints, num_cem_endpoint_vals);
+				memcpy(final_lblock.m_endpoints + num_cem_endpoint_vals, carrier_lblock.m_endpoints, num_cem_endpoint_vals);
+				memcpy(final_lblock.m_endpoints + 2 * num_cem_endpoint_vals, carrier_lblock.m_endpoints, num_cem_endpoint_vals);	
+
+				const uint32_t NUM_M1_PASSES = 2;
+
+				for (uint32_t pass = 0; pass < NUM_M1_PASSES; pass++)
+				{
+					refine_endpoints_with_current_weights(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context,
+						best_lblock_error, best_lblock, nullptr);
+
+					bool changed_flag;
+
+					if (pass == (NUM_M1_PASSES - 1))
+					{
+						changed_flag = weight_polish(shortlist_state.m_pbuf, final_lblock, enc_context, pGrid_data,
+							best_lblock_error, best_lblock, nullptr);
+					}
+					else
+					{
+						changed_flag = weight_gradient_descent(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context, pGrid_data,
+							best_lblock_error, best_lblock, nullptr);
+					}
+
+					if (!changed_flag)
+					{
+						break;
+					}
+				}
+
+				if (pAll_candidates)
+				{
+					pAll_candidates->push_back(final_lblock);
+				}
+			}
+
+			double best_pat_lblock_error = DBL_MAX;
+			astc_helpers::log_astc_block best_pat_lblock;
+
+			// ------- method 2
+			if (enc_context.m_use_method2)
+			{
+				uint8_t desired_block_weights[astc_helpers::MAX_BLOCK_PIXELS]; // at block res, ranks
+
+#if defined(DEBUG) || defined(_DEBUG)            
+				memset(desired_block_weights, 0xFF, astc_helpers::MAX_BLOCK_PIXELS);
+#endif					
+				for (uint32_t s = 0; s < 3; s++)
+				{
+					float initial_endpoints[8];
+					calc_initial_cem_endpoints(subset_pbuf[s], initial_endpoints, num_cem_comps, nullptr, is_cem_6_or_10(best_config.m_cem));
+
+					uint8_t* pSubset_CEM_vals = final_lblock.m_endpoints + s * num_cem_endpoint_vals;
+					cem_encode(best_config.m_cem, initial_endpoints, best_config.m_endpoint_range, pSubset_CEM_vals, true, enc_context.m_higher_effort_bc);
+
+					uint8_t subset_weights[astc_helpers::MAX_BLOCK_PIXELS];
+					eval_weights_first_plane(subset_pbuf[s], subset_weights, best_config.m_weight_range,
+						best_config.m_cem, pSubset_CEM_vals, best_config.m_endpoint_range,
+						num_cem_comps);
+
+					for (uint32_t i = 0; i < enc_context.m_num_ls_iterations; i++)
+					{
+						float refined_endpoints[8];
+						refine_endpoints_given_weights(subset_pbuf[s], best_config.m_cem,
+							subset_weights, best_config.m_weight_range, pSubset_CEM_vals, best_config.m_endpoint_range, refined_endpoints, num_cem_comps);
+
+						cem_encode(best_config.m_cem, refined_endpoints, best_config.m_endpoint_range, pSubset_CEM_vals, true, enc_context.m_higher_effort_bc);
+
+						eval_weights_first_plane(subset_pbuf[s], subset_weights, best_config.m_weight_range,
+							best_config.m_cem, pSubset_CEM_vals, best_config.m_endpoint_range,
+							num_cem_comps);
+					} // i
+
+					// base+ofs is so rarely a win (< ~1%) that it's not worth the trouble
+
+					for (uint32_t j = 0; j < subset_pbuf[s].m_width; j++)
+					{
+						const uint32_t pixel_index = subset_pixel_indices[s][j];
+						assert(pixel_index < total_block_pixels);
+
+						desired_block_weights[pixel_index] = subset_weights[j];
+					}
+
+				} // s
+
+#if defined(DEBUG) || defined(_DEBUG)
+				for (uint32_t i = 0; i < total_block_pixels; i++)
+				{
+					assert(desired_block_weights[i] != 0xFF);
+				}
+#endif
+
+				// now downsample the ideal weights to grid res, quantize
+				// desired_block_weights[] is either [0,64] (dequantized weight values) or in the carrier's weight rank space depending on solve_weights_dequantized
+
+				if ((block_width == best_config.m_grid_width) && (block_height == best_config.m_grid_height))
+				{
+					assert(total_block_pixels == total_grid_weights);
+
+					memcpy(final_lblock.m_weights, desired_block_weights, total_block_pixels);
+				}
+				else
+				{
+					// dequantize ranks to [0,64] values
+					for (uint32_t i = 0; i < total_block_pixels; i++)
+						desired_block_weights[i] = (uint8_t)weight_tab.get_rank_to_val(desired_block_weights[i]);
+
+					// downsample from block to grid res, [0,64] values
+					uint8_t downsampled_weights[astc_helpers::MAX_GRID_WEIGHTS];
+
+#if 0
+					basisu::downsample_weight_grid(pGrid_data->m_downsample_matrix.get_ptr(),
+						block_width, block_height,
+						best_config.m_grid_width, best_config.m_grid_height,
+						desired_block_weights,
+						downsampled_weights);
+#else
+					float src_temp[PIXELBUF_COMP_PITCH];
+					pixelbuf src_pbuf(block_width, block_height, src_temp);
+					for (uint32_t y = 0; y < block_height; y++)
+						for (uint32_t x = 0; x < block_width; x++)
+							pixelbuf_set_comp(src_pbuf, x, y, 0, desired_block_weights[x + y * block_width]);
+
+					float dst_temp[PIXELBUF_COMP_PITCH];
+					pixelbuf dst_pbuf(best_config.m_grid_width, best_config.m_grid_height, dst_temp);
+
+					pseudoinverse_block_to_grid(src_pbuf, dst_pbuf, 1);
+					for (uint32_t y = 0; y < best_config.m_grid_height; y++)
+						for (uint32_t x = 0; x < best_config.m_grid_width; x++)
+							downsampled_weights[x + y * best_config.m_grid_width] = (uint8_t)clamp((int)std::round(pixelbuf_get_comp(dst_pbuf, x, y, 0)), 0, 64);
+#endif
+
+					// quantize downsampled weights
+					for (uint32_t i = 0; i < total_grid_weights; i++)
+						final_lblock.m_weights[i] = (uint8_t)weight_tab.get_val_to_rank(downsampled_weights[i]);
+				}
+
+				{
+					// convert from rank to ISE space
+					const double subset_error = compute_block_error(final_lblock, shortlist_state.m_pbuf, enc_context);
+
+					if (subset_error < best_pat_lblock_error)
+					{
+						best_pat_lblock_error = subset_error;
+						best_pat_lblock = final_lblock;
+					}
+				}
+
+				const uint32_t NUM_M2_PASSES = 2;
+
+				for (uint32_t pass = 0; pass < NUM_M2_PASSES; pass++)
+				{
+					refine_endpoints_with_current_weights(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context,
+						best_pat_lblock_error, best_pat_lblock, nullptr);
+
+					bool changed_flag;
+
+					if (pass == (NUM_M2_PASSES - 1))
+					{
+						changed_flag = weight_polish(shortlist_state.m_pbuf, final_lblock, enc_context, pGrid_data,
+							best_pat_lblock_error, best_pat_lblock, nullptr);
+					}
+					else
+					{
+						changed_flag = weight_gradient_descent(shortlist_state.m_pbuf, final_lblock, subset_pbuf, subset_pixel_indices, best_config, enc_context, pGrid_data,
+							best_pat_lblock_error, best_pat_lblock, nullptr);
+					}
+
+					if (!changed_flag)
+					{
+						break;
+					}
+				}
+
+				assert(best_pat_lblock_error != DBL_MAX);
+				if (best_pat_lblock_error < best_lblock_error)
+				{
+					best_lblock_error = best_pat_lblock_error;
+					best_lblock = best_pat_lblock;
+				}
+
+				if (pAll_candidates)
+				{
+					pAll_candidates->push_back(best_pat_lblock);
+				}
+			}
+
+		} // pat_cand_iter
+
+	} // config_cand_index
+
+	convert_rank_lblock_to_ise(best_lblock);
+}
+
+double compress_block_subsets(
+	const subset_enc_context& enc_context,
+	subset_enc_thread_context& enc_thread_context,
+	const uint8_t* pBlock_pixels,
+	astc_helpers::log_astc_block& best_lblock, astc_lblock_vec* pAll_candidates)
+{
+	assert(enc_context.m_use_method1 || enc_context.m_use_method2);
+
+	const bool FORCE_SUBSETS = false;
+		
+	single_subset_shortlist_state shortlist_state;
+	double best_lblock_error = compress_single_subset_internal(enc_context, pBlock_pixels, best_lblock, pAll_candidates, shortlist_state, true, DEF_SCALE_WEIGHT);
+
+	if ((best_lblock.m_solid_color_flag_ldr) || (enc_context.m_max_subsets == 1))
+		return best_lblock_error;
+
+	assert(is_lblock_ise(best_lblock));
+
+	if (FORCE_SUBSETS)
+		best_lblock_error = DBL_MAX;
+
+	const float max_chan_var = FORCE_SUBSETS ? FLT_MAX : (basisu::maximum<float>(
+		shortlist_state.m_stats.m_covar[cCovarRR], shortlist_state.m_stats.m_covar[cCovarGG],
+		shortlist_state.m_stats.m_covar[cCovarBB], shortlist_state.m_stats.m_covar[cCovarAA]) / (float)enc_context.m_total_block_pixels); // TODO: divide
+
+	if (max_chan_var >= enc_context.m_two_subset_var_thresh)
+	{
+		compress_block_2subsets_internal(
+			enc_context,
+			pBlock_pixels,
+			best_lblock_error, best_lblock,
+			pAll_candidates, shortlist_state);
+	}
+
+	if ((enc_context.m_max_subsets == 3) && (max_chan_var >= enc_context.m_three_subset_var_thresh))
+	{
+		compress_block_3subsets_internal(
+			enc_context, enc_thread_context,
+			pBlock_pixels,
+			best_lblock_error, best_lblock,
+			pAll_candidates, shortlist_state);
+	}
+
+	return best_lblock_error;
+}
+
+} // namespace astc_ldrf
+} // namespace basisu
diff --git a/encoder/basisu_astc_ldr_fencode.h b/encoder/basisu_astc_ldr_fencode.h
new file mode 100644
index 0000000..035c9fa
--- /dev/null
+++ b/encoder/basisu_astc_ldr_fencode.h
@@ -0,0 +1,440 @@
+// basisu_astc_ldr_fencode.h
+#pragma once
+#include "../transcoder/basisu.h"
+#include "../transcoder/basisu_transcoder_internal.h"
+#include "basisu_astc_ldr_common.h"
+
+namespace basisu
+{
+
+namespace astc_ldrf
+{
+
+BASISU_FORCE_INLINE int popcount64(uint64_t x)
+{
+#if defined(__cplusplus) && (__cplusplus >= 202002L) && defined(__cpp_lib_bitops)
+	return static_cast<int>(std::popcount(x));
+
+#elif defined(__EMSCRIPTEN__) || defined(__clang__) || defined(__GNUC__)
+	return __builtin_popcountll(static_cast<unsigned long long>(x));
+
+#elif defined(_MSC_VER) && defined(_M_X64)
+	return static_cast<int>(__popcnt64(x));
+
+#elif defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_ARM64) || defined(_M_ARM))
+	return static_cast<int>(
+		__popcnt(static_cast<uint32_t>(x)) +
+		__popcnt(static_cast<uint32_t>(x >> 32))
+		);
+
+#else
+	int count = 0;
+	while (x)
+	{
+		x &= (x - 1);
+		++count;
+	}
+	return count;
+#endif
+}
+
+struct bitmask192
+{
+	uint64_t m_a, m_b, m_c; // a=lowest qword (bits 0-63), b=middle (64-127), c=highest (128-191)
+
+	inline bitmask192() {}
+
+	constexpr inline bitmask192(const bitmask192& o) noexcept
+		: m_a(o.m_a), m_b(o.m_b), m_c(o.m_c)
+	{ 
+	}
+
+	inline bitmask192& operator=(const bitmask192& o) noexcept
+	{
+		m_a = o.m_a;
+		m_b = o.m_b;
+		m_c = o.m_c;
+		return *this;
+	}
+
+	constexpr inline bitmask192(uint64_t a, uint64_t b, uint64_t c) noexcept
+		: m_a(a), m_b(b), m_c(c)
+	{
+	}
+
+	static constexpr inline bitmask192 zero() noexcept
+	{
+		return bitmask192(0, 0, 0);
+	}
+
+	static constexpr inline bitmask192 all() noexcept
+	{
+		return bitmask192(UINT64_MAX, UINT64_MAX, UINT64_MAX);
+	}
+
+	inline void clear() noexcept
+	{
+		m_a = 0;
+		m_b = 0;
+		m_c = 0;
+	}
+
+	inline void set_all() noexcept
+	{
+		m_a = UINT64_MAX;
+		m_b = UINT64_MAX;
+		m_c = UINT64_MAX;
+	}
+
+	inline bool is_zero() const noexcept
+	{
+		return (m_a | m_b | m_c) == 0;
+	}
+
+	inline bool any() const noexcept
+	{
+		return (m_a | m_b | m_c) != 0;
+	}
+
+	inline bool none() const noexcept
+	{
+		return !any();
+	}
+
+	inline bool all_set() const noexcept
+	{
+		return (m_a == UINT64_MAX) && (m_b == UINT64_MAX) && (m_c == UINT64_MAX);
+	}
+
+	inline uint32_t popcount() const noexcept
+	{
+		return popcount64(m_a) + popcount64(m_b) + popcount64(m_c);
+	}
+
+	constexpr inline bitmask192 operator~() const noexcept
+	{
+		return bitmask192(~m_a, ~m_b, ~m_c);
+	}
+
+	constexpr inline bitmask192 operator&(const bitmask192& rhs) const noexcept
+	{
+		return bitmask192(m_a & rhs.m_a, m_b & rhs.m_b, m_c & rhs.m_c);
+	}
+
+	constexpr inline bitmask192 operator|(const bitmask192& rhs) const noexcept
+	{
+		return bitmask192(m_a | rhs.m_a, m_b | rhs.m_b, m_c | rhs.m_c);
+	}
+
+	constexpr inline bitmask192 operator^(const bitmask192& rhs) const noexcept
+	{
+		return bitmask192(m_a ^ rhs.m_a, m_b ^ rhs.m_b, m_c ^ rhs.m_c);
+	}
+
+	inline bitmask192& operator&=(const bitmask192& rhs) noexcept
+	{
+		m_a &= rhs.m_a;
+		m_b &= rhs.m_b;
+		m_c &= rhs.m_c;
+		return *this;
+	}
+
+	inline bitmask192& operator|=(const bitmask192& rhs) noexcept
+	{
+		m_a |= rhs.m_a;
+		m_b |= rhs.m_b;
+		m_c |= rhs.m_c;
+		return *this;
+	}
+
+	inline bitmask192& operator^=(const bitmask192& rhs) noexcept
+	{
+		m_a ^= rhs.m_a;
+		m_b ^= rhs.m_b;
+		m_c ^= rhs.m_c;
+		return *this;
+	}
+
+	constexpr inline bool operator==(const bitmask192& rhs) const noexcept
+	{
+		return (m_a == rhs.m_a) && (m_b == rhs.m_b) && (m_c == rhs.m_c);
+	}
+
+	constexpr inline bool operator!=(const bitmask192& rhs) const noexcept
+	{
+		return !(*this == rhs);
+	}
+
+	// m_a is the lowest qword, m_b highest
+	constexpr inline bool operator<(const bitmask192& rhs) const noexcept
+	{
+		if (m_c != rhs.m_c) return m_c < rhs.m_c;
+		if (m_b != rhs.m_b) return m_b < rhs.m_b;
+		return m_a < rhs.m_a;
+	}
+
+	constexpr inline bool operator>(const bitmask192& rhs) const noexcept
+	{
+		return rhs < *this;
+	}
+
+	constexpr inline bool operator<=(const bitmask192& rhs) const noexcept
+	{
+		return !(rhs < *this);
+	}
+
+	constexpr inline bool operator>=(const bitmask192& rhs) const noexcept
+	{
+		return !(*this < rhs);
+	}
+
+	explicit constexpr inline operator bool() const noexcept
+	{
+		return (m_a | m_b | m_c) != 0;
+	}
+
+	inline bool intersects(const bitmask192& rhs) const noexcept
+	{
+		return ((m_a & rhs.m_a) | (m_b & rhs.m_b) | (m_c & rhs.m_c)) != 0;
+	}
+
+	inline bool disjoint(const bitmask192& rhs) const noexcept
+	{
+		return !intersects(rhs);
+	}
+
+	constexpr inline bool contains_all(const bitmask192& rhs) const noexcept
+	{
+		return ((m_a & rhs.m_a) == rhs.m_a) &&
+			((m_b & rhs.m_b) == rhs.m_b) &&
+			((m_c & rhs.m_c) == rhs.m_c);
+	}
+
+	constexpr inline bool is_subset_of(const bitmask192& rhs) const noexcept
+	{
+		return rhs.contains_all(*this);
+	}
+
+	inline bool contains_any(const bitmask192& rhs) const noexcept
+	{
+		return intersects(rhs);
+	}
+
+	constexpr inline uint64_t low64() const noexcept
+	{
+		return m_a;
+	}
+
+	constexpr inline uint64_t mid64() const noexcept
+	{
+		return m_b;
+	}
+
+	constexpr inline uint64_t high64() const noexcept
+	{
+		return m_c;
+	}
+
+	inline void set_bit(uint32_t index) noexcept
+	{
+		assert(index < 192);
+
+		if (index < 64)
+			m_a |= uint64_t(1) << index;
+		else if (index < 128)
+			m_b |= uint64_t(1) << (index - 64);
+		else
+			m_c |= uint64_t(1) << (index - 128);
+	}
+
+	inline void set_bit(uint32_t index, uint32_t bit_val) noexcept
+	{
+		assert(index < 192);
+		assert(bit_val <= 1);
+				
+		if (index < 64)
+		{
+			m_a &= ~(uint64_t(1) << index);
+			m_a |= uint64_t(bit_val) << index;
+		}
+		else if (index < 128)
+		{
+			m_b &= ~(uint64_t(1) << (index - 64));
+			m_b |= uint64_t(bit_val) << (index - 64);
+		}
+		else
+		{
+			m_c &= ~(uint64_t(1) << (index - 128));
+			m_c |= uint64_t(bit_val) << (index - 128);
+		}
+	}
+
+	inline bool is_bit_set(uint32_t index) const noexcept
+	{
+		assert(index < 192);
+
+		if (index < 64)
+			return ((m_a >> index) & 1) != 0;
+		else if (index < 128)
+			return ((m_b >> (index - 64)) & 1) != 0;
+		else
+			return ((m_c >> (index - 128)) & 1) != 0;
+	}
+
+	static inline bitmask192 lsb_mask(uint32_t num_bits) noexcept
+	{
+		assert(num_bits <= 192);
+
+		if (num_bits == 0)
+			return bitmask192(0, 0, 0);
+
+		if (num_bits < 64)
+			return bitmask192((uint64_t(1) << num_bits) - 1, 0, 0);
+
+		if (num_bits == 64)
+			return bitmask192(UINT64_MAX, 0, 0);
+
+		if (num_bits < 128)
+		{
+			return bitmask192(
+				UINT64_MAX,
+				(uint64_t(1) << (num_bits - 64)) - 1,
+				0);
+		}
+
+		if (num_bits == 128)
+			return bitmask192(UINT64_MAX, UINT64_MAX, 0);
+
+		if (num_bits < 192)
+		{
+			return bitmask192(
+				UINT64_MAX,
+				UINT64_MAX,
+				(uint64_t(1) << (num_bits - 128)) - 1);
+		}
+
+		return bitmask192(UINT64_MAX, UINT64_MAX, UINT64_MAX);
+	}
+};
+
+inline uint32_t popcount192(const bitmask192& a) { return a.popcount(); }
+
+const uint32_t MAX_CANDIDATES = 512;
+
+struct rgba32_image
+{
+    const uint8_t* m_pPixels;
+    uint32_t m_width;
+    uint32_t m_height;
+    uint32_t m_row_pitch_in_texels; // pitch in pixels/texels, not bytes
+};
+
+struct single_subset_enc_context
+{
+    uint32_t m_block_width, m_block_height;
+    uint32_t m_block_size_index; 
+    uint32_t m_total_block_pixels;
+    
+    uint32_t m_max_candidates;
+    uint32_t m_num_ls_iterations;
+	
+    uint32_t m_chan_weights[4];
+
+    basist::astc_ldr_t::dct2f m_dct;
+
+    astc_helpers::decode_mode m_astc_decode_mode;
+    bool m_disable_dual_plane;
+	bool m_weight_polishing;
+    
+    bool m_has_alpha;
+
+	bool m_try_base_ofs;
+	bool m_higher_effort_bc;
+};
+
+const uint32_t MAX_UNIQUE_2SUBSET_PATS = 838;
+const uint32_t MAX_UNIQUE_3SUBSET_PATS = 626;
+
+typedef basisu::vector<astc_helpers::log_astc_block> astc_lblock_vec;
+
+// source pEndpoints[] = ASTC direct order: LR HR LG HG LB HB LA HA
+bool cem_encode(uint32_t cem_index, const float pEndpoints[8], uint32_t endpoint_ise_range, uint8_t* pCEM_values, bool allow_bc = true, bool high_effort = true);
+
+bool init_single_subset_context(
+    single_subset_enc_context& ctx,
+    uint32_t block_width, uint32_t block_height,
+    astc_helpers::decode_mode astc_decode_mode,
+    const uint32_t chan_weights[4],
+    uint32_t max_candidates, uint32_t num_ls_iterations, bool disable_dual_plane, bool has_alpha, bool weight_polishing);
+
+// best_lblock will be in ise space (see is_lblock_ise() below), elements in array pointed to by pAll_candidates (which may be null) will be in rank space
+double compress_single_subset(
+	single_subset_enc_context& ctx,
+	const uint8_t* pBlock_pixels,
+	astc_helpers::log_astc_block& best_lblock,
+	astc_lblock_vec* pAll_candidates,
+	bool always_compute_error);
+
+static const uint32_t NUM_DOT_THRESH_FRACTS = 6;
+
+struct subset_enc_context : single_subset_enc_context
+{
+	subset_enc_context() {}
+
+	uint32_t m_max_subsets;
+
+	uint32_t m_num_carrier_candidates;
+	uint32_t m_num_pattern_candidates;
+	float m_two_subset_var_thresh;
+	float m_three_subset_var_thresh;
+	uint32_t m_two_subset_dot_thresh_fract_index;
+
+	bitmask192 m_two_subset_pat_bitmask[MAX_UNIQUE_2SUBSET_PATS];
+
+	uint32_t m_num_unique_two_subset_pats;
+	const uint16_t* m_pUnique_two_subset_pats;
+
+	bool m_use_method1;
+	bool m_use_method2;
+
+	astc_ldr::partitions_data* m_pPart_data_p2;
+	astc_ldr::partitions_data* m_pPart_data_p3;
+};
+
+// must have first called init_single_subset_context() on ctx
+bool init_multi_subset_context(
+	subset_enc_context& ctx,
+	uint32_t max_subsets,
+	uint32_t num_carrier_candidates, uint32_t num_pattern_candidates,
+	float two_subset_var_thresh, uint32_t two_subset_dot_thresh_fract_index,
+	float three_subset_var_thresh,
+	astc_ldr::partitions_data* pPart_data_p2, astc_ldr::partitions_data* pPart_data_p3);
+
+struct subset_enc_thread_context
+{
+	astc_ldr::partition_pattern_vec m_pat_vec;
+};
+
+double compress_block_subsets(
+	const subset_enc_context& enc_context,
+	subset_enc_thread_context& enc_thread_context,
+	const uint8_t* pBlock_pixels,
+	astc_helpers::log_astc_block& best_lblock, astc_lblock_vec* pAll_candidates);
+
+enum
+{
+    cUserModeISEValues = 0,
+    cUserModeRankValues = 1
+};
+
+static inline bool is_lblock_ise(const astc_helpers::log_astc_block& log_blk)
+{
+    // the default user mode value == ISE, 1 = ranks (which is the exceptional case for astc_helpers)
+    return log_blk.m_user_mode == cUserModeISEValues;
+}
+
+void convert_rank_lblock_to_ise(astc_helpers::log_astc_block& log_blk);
+void convert_ise_lblock_to_rank(astc_helpers::log_astc_block& log_blk);
+
+} // namespace astc_ldr_f
+} // namespace basisu
diff --git a/encoder/basisu_astc_ldr_pseudoinv_tab.inl b/encoder/basisu_astc_ldr_pseudoinv_tab.inl
new file mode 100644
index 0000000..d106198
--- /dev/null
+++ b/encoder/basisu_astc_ldr_pseudoinv_tab.inl
@@ -0,0 +1,298 @@
+0.6883562f, -0.1883562f, 0.4143836f, 0.0856164f, 0.0856164f, 0.4143836f, -0.1883562f, 0.6883562f,
+0.9555160f, -0.2272727f, 0.0444840f, 0.1423488f, 0.7272727f, -0.1423488f, -0.1423488f, 0.7272727f,
+
+0.1423488f, 0.0444840f, -0.2272727f, 0.9555160f, 0.6000000f, -0.2000000f, 0.4000000f, 0.0000000f,
+0.2000000f, 0.2000000f, 0.0000000f, 0.4000000f, -0.2000000f, 0.6000000f, 0.8285714f, -0.1428571f,
+
+0.0285714f, 0.3428572f, 0.2857143f, -0.0571429f, -0.1428571f, 0.7142857f, -0.1428571f, -0.0571429f,
+0.2857143f, 0.3428572f, 0.0285714f, -0.1428571f, 0.8285714f, 0.9857143f, -0.2523810f, 0.0809524f,
+
+-0.0142857f, 0.0571429f, 1.0095239f, -0.3238095f, 0.0571429f, -0.0857143f, 0.4857143f, 0.4857143f,
+-0.0857143f, 0.0571429f, -0.3238095f, 1.0095239f, 0.0571429f, -0.0142857f, 0.0809524f, -0.2523810f,
+
+0.9857143f, 0.5107527f, -0.1774193f, 0.3817204f, -0.0483871f, 0.2526882f, 0.0806452f, 0.0806452f,
+0.2526882f, -0.0483871f, 0.3817204f, -0.1774193f, 0.5107527f, 0.7542282f, -0.1948819f, 0.0528584f,
+
+0.3983119f, 0.1476378f, -0.0400442f, -0.0169237f, 0.5472441f, -0.1484306f, -0.1484306f, 0.5472441f,
+-0.0169237f, -0.0400442f, 0.1476378f, 0.3983119f, 0.0528584f, -0.1948819f, 0.7542282f, 0.9212349f,
+
+-0.2166766f, 0.0636154f, -0.0130717f, 0.2100402f, 0.5778043f, -0.1696410f, 0.0348577f, -0.1641219f,
+0.7987264f, -0.0538284f, 0.0110606f, 0.0110606f, -0.0538284f, 0.7987264f, -0.1641219f, 0.0348577f,
+
+-0.1696410f, 0.5778043f, 0.2100402f, -0.0130717f, 0.0636154f, -0.2166766f, 0.9212349f, 0.9969320f,
+-0.2099229f, 0.0692308f, -0.0208463f, 0.0030679f, 0.0163624f, 1.1195889f, -0.3692308f, 0.1111804f,
+
+-0.0163624f, -0.0354519f, 0.2408908f, 0.8000000f, -0.2408908f, 0.0354519f, 0.0354519f, -0.2408908f,
+0.8000000f, 0.2408908f, -0.0354519f, -0.0163624f, 0.1111804f, -0.3692308f, 1.1195889f, 0.0163624f,
+
+0.0030679f, -0.0208463f, 0.0692308f, -0.2099229f, 0.9969320f, 0.4159091f, -0.1659091f, 0.3431818f,
+-0.0931818f, 0.2340909f, 0.0159091f, 0.1613636f, 0.0886364f, 0.0886364f, 0.1613636f, 0.0159091f,
+
+0.2340909f, -0.0931818f, 0.3431818f, -0.1659091f, 0.4159091f, 0.6538065f, -0.1721698f, 0.0584577f,
+0.3956889f, 0.0400943f, -0.0136134f, 0.1891948f, 0.2099057f, -0.0712703f, -0.0689228f, 0.4221698f,
+
+-0.1433414f, -0.1433414f, 0.4221698f, -0.0689228f, -0.0712703f, 0.2099057f, 0.1891948f, -0.0136134f,
+0.0400943f, 0.3956889f, 0.0584577f, -0.1721698f, 0.6538065f, 0.8053632f, -0.2047127f, 0.0614056f,
+
+-0.0163868f, 0.3634550f, 0.2204598f, -0.0661291f, 0.0176474f, -0.0784532f, 0.6456324f, -0.1936639f,
+0.0516816f, -0.1215507f, 0.4554813f, 0.0815266f, -0.0217564f, -0.0217564f, 0.0815266f, 0.4554813f,
+
+-0.1215507f, 0.0516816f, -0.1936639f, 0.6456324f, -0.0784532f, 0.0176474f, -0.0661291f, 0.2204598f,
+0.3634550f, -0.0163868f, 0.0614056f, -0.2047127f, 0.8053632f, 0.8815933f, -0.2045389f, 0.0750646f,
+
+-0.0215593f, 0.0044532f, 0.2706439f, 0.4675174f, -0.1715762f, 0.0492785f, -0.0101788f, -0.1695884f,
+0.8210233f, -0.1598191f, 0.0459017f, -0.0094813f, -0.0123115f, 0.0596032f, 0.7563307f, -0.2172260f,
+
+0.0448696f, 0.0448696f, -0.2172260f, 0.7563307f, 0.0596032f, -0.0123115f, -0.0094813f, 0.0459017f,
+-0.1598191f, 0.8210233f, -0.1695884f, -0.0101788f, 0.0492785f, -0.1715762f, 0.4675174f, 0.2706439f,
+
+0.0044532f, -0.0215593f, 0.0750646f, -0.2045389f, 0.8815933f, 0.9672751f, -0.2873513f, 0.0769021f,
+-0.0186696f, 0.0054320f, -0.0009586f, 0.1047195f, 0.9195243f, -0.2460869f, 0.0597428f, -0.0173824f,
+
+0.0030675f, -0.1279904f, 0.6539148f, 0.3007728f, -0.0730190f, 0.0212451f, -0.0037491f, 0.0649557f,
+-0.3318644f, 1.0073659f, -0.1058330f, 0.0307925f, -0.0054340f, -0.0067231f, 0.0343492f, -0.1042661f,
+
+0.9963973f, -0.2899053f, 0.0511598f, -0.0051125f, 0.0261200f, -0.0792867f, 0.3231576f, 0.5710128f,
+-0.1007670f, 0.0038343f, -0.0195900f, 0.0594650f, -0.2423682f, 0.9050737f, 0.0755752f, -0.0009586f,
+
+0.0048975f, -0.0148663f, 0.0605921f, -0.2262684f, 0.9811062f, 0.9997144f, -0.1402052f, 0.0544243f,
+-0.0244578f, 0.0084673f, -0.0019448f, 0.0002094f, 0.0022848f, 1.1216413f, -0.4353943f, 0.1956628f,
+
+-0.0677386f, 0.0155583f, -0.0016755f, -0.0063974f, 0.0594046f, 1.2191039f, -0.5478558f, 0.1896682f,
+-0.0435634f, 0.0046914f, 0.0100531f, -0.0933501f, 0.3699796f, 0.8609163f, -0.2980500f, 0.0684567f,
+
+-0.0073723f, -0.0100531f, 0.0933501f, -0.3699796f, 0.9168615f, 0.2980500f, -0.0684567f, 0.0073723f,
+0.0058643f, -0.0544542f, 0.2158214f, -0.5348359f, 1.1594708f, 0.0399331f, -0.0043005f, -0.0016755f,
+
+0.0155583f, -0.0616633f, 0.1528102f, -0.3312774f, 1.1314477f, 0.0012287f, 0.0002094f, -0.0019448f,
+0.0077079f, -0.0191013f, 0.0414097f, -0.1414310f, 0.9998464f, 0.3539683f, -0.1539683f, 0.2904762f,
+
+-0.0904762f, 0.2269841f, -0.0269841f, 0.1952381f, 0.0047619f, 0.1317460f, 0.0682540f, 0.0682540f,
+0.1317460f, 0.0047619f, 0.1952381f, -0.0269841f, 0.2269841f, -0.0904762f, 0.2904762f, -0.1539683f,
+
+0.3539683f, 0.5617812f, -0.1572600f, 0.0543796f, 0.3820210f, -0.0145911f, 0.0050455f, 0.2472007f,
+0.0924105f, -0.0319550f, 0.0674404f, 0.2350793f, -0.0812891f, -0.0673798f, 0.3420810f, -0.1182897f,
+
+-0.1308050f, 0.3438689f, -0.0590948f, -0.0911226f, 0.2395491f, 0.0666980f, -0.0382127f, 0.1004561f,
+0.2344217f, 0.0146972f, -0.0386369f, 0.4021455f, 0.0543796f, -0.1429567f, 0.5279383f, 0.6755751f,
+
+-0.1416472f, 0.0300853f, -0.0082323f, 0.4201931f, 0.0643851f, -0.0136751f, 0.0037420f, 0.1137348f,
+0.3116238f, -0.0661876f, 0.0181110f, -0.1416472f, 0.5176560f, -0.1099481f, 0.0300853f, -0.0879808f,
+
+0.3215297f, 0.0861782f, -0.0235811f, -0.0235811f, 0.0861782f, 0.3215297f, -0.0879808f, 0.0300853f,
+-0.1099481f, 0.5176560f, -0.1416472f, 0.0181110f, -0.0661876f, 0.3116238f, 0.1137348f, 0.0037420f,
+
+-0.0136751f, 0.0643851f, 0.4201931f, -0.0082323f, 0.0300853f, -0.1416472f, 0.6755751f, 0.8069659f,
+-0.2107186f, 0.0594210f, -0.0171096f, 0.0045659f, 0.3617290f, 0.2269277f, -0.0639919f, 0.0184257f,
+
+-0.0049171f, -0.0835080f, 0.6645741f, -0.1874048f, 0.0539610f, -0.0144002f, -0.1263000f, 0.4732779f,
+0.1065327f, -0.0306749f, 0.0081860f, 0.0087699f, -0.0328629f, 0.6332502f, -0.1823370f, 0.0486588f,
+
+0.0402884f, -0.1509708f, 0.5632743f, 0.0167249f, -0.0044632f, 0.0068062f, -0.0255047f, 0.0951583f,
+0.4646113f, -0.1239872f, -0.0144002f, 0.0539610f, -0.2013295f, 0.6602938f, -0.0823658f, -0.0049171f,
+
+0.0184257f, -0.0687466f, 0.2254662f, 0.3621190f, 0.0045659f, -0.0171096f, 0.0638362f, -0.2093614f,
+0.8066038f, 0.8810968f, -0.2021351f, 0.0666954f, -0.0202770f, 0.0058238f, -0.0012029f, 0.2717788f,
+
+0.4620232f, -0.1524466f, 0.0463474f, -0.0133115f, 0.0027496f, -0.1685313f, 0.8159056f, -0.1420003f,
+0.0431715f, -0.0123993f, 0.0025612f, -0.0173142f, 0.0838225f, 0.6720048f, -0.2043056f, 0.0586787f,
+
+-0.0121205f, 0.0449523f, -0.2176261f, 0.7577241f, -0.0067165f, 0.0019290f, -0.0003985f, -0.0039697f,
+0.0192183f, -0.0669137f, 0.7472055f, -0.2146051f, 0.0443283f, -0.0121205f, 0.0586787f, -0.2043056f,
+
+0.5968578f, 0.1054055f, -0.0217723f, 0.0025612f, -0.0123993f, 0.0431715f, -0.1261211f, 0.8113450f,
+-0.1675893f, 0.0027496f, -0.0133115f, 0.0463474f, -0.1353993f, 0.4571270f, 0.2727902f, -0.0012029f,
+
+0.0058238f, -0.0202770f, 0.0592372f, -0.1999931f, 0.8806543f, 0.9554276f, -0.2268213f, 0.0425861f,
+-0.0101123f, 0.0019080f, -0.0005029f, 0.0001033f, 0.1426315f, 0.7258282f, -0.1362755f, 0.0323593f,
+
+-0.0061055f, 0.0016092f, -0.0003307f, -0.1426315f, 0.7287173f, 0.1362755f, -0.0323593f, 0.0061055f,
+-0.0016092f, 0.0003307f, 0.0425861f, -0.2175763f, 0.9147495f, -0.2172119f, 0.0409829f, -0.0108020f,
+
+0.0022196f, 0.0063560f, -0.0324732f, 0.1365260f, 0.7274374f, -0.1372507f, 0.0361755f, -0.0074333f,
+-0.0063560f, 0.0324732f, -0.1365260f, 0.7271081f, 0.1372507f, -0.0361755f, 0.0074333f, 0.0019080f,
+
+-0.0097479f, 0.0409829f, -0.2182660f, 0.9193873f, -0.2423252f, 0.0497929f, 0.0002505f, -0.0012798f,
+0.0053808f, -0.0286570f, 0.1207099f, 0.8116162f, -0.1667705f, -0.0002756f, 0.0014078f, -0.0059189f,
+
+0.0315227f, -0.1327809f, 0.7072222f, 0.1834475f, 0.0001033f, -0.0005279f, 0.0022196f, -0.0118210f,
+0.0497929f, -0.2652083f, 0.9312072f, 0.9899751f, -0.2765257f, 0.0928675f, -0.0243293f, 0.0065427f,
+
+-0.0019937f, 0.0006807f, -0.0001201f, 0.0400995f, 1.1061027f, -0.3714699f, 0.0973173f, -0.0261706f,
+0.0079748f, -0.0027227f, 0.0004805f, -0.0687420f, 0.3895382f, 0.6368056f, -0.1668297f, 0.0448639f,
+
+-0.0136711f, 0.0046674f, -0.0008237f, 0.0562435f, -0.3187130f, 0.9335227f, 0.1364970f, -0.0367068f,
+0.0111854f, -0.0038188f, 0.0006739f, -0.0204703f, 0.1159986f, -0.3397642f, 1.1149904f, -0.1461748f,
+
+0.0445429f, -0.0152074f, 0.0026837f, 0.0026837f, -0.0152074f, 0.0445429f, -0.1461748f, 1.1149904f,
+-0.3397642f, 0.1159986f, -0.0204703f, 0.0006739f, -0.0038188f, 0.0111854f, -0.0367068f, 0.1364970f,
+
+0.9335227f, -0.3187130f, 0.0562435f, -0.0008237f, 0.0046674f, -0.0136711f, 0.0448639f, -0.1668297f,
+0.6368056f, 0.3895382f, -0.0687420f, 0.0004805f, -0.0027227f, 0.0079748f, -0.0261706f, 0.0973173f,
+
+-0.3714699f, 1.1061027f, 0.0400995f, -0.0001201f, 0.0006807f, -0.0019937f, 0.0065427f, -0.0243293f,
+0.0928675f, -0.2765257f, 0.9899751f, 0.9999861f, -0.1427280f, 0.0322982f, -0.0127173f, 0.0061198f,
+
+-0.0030193f, 0.0010721f, -0.0002462f, 0.0000265f, 0.0001113f, 1.1418240f, -0.2583854f, 0.1017386f,
+-0.0489583f, 0.0241542f, -0.0085767f, 0.0019699f, -0.0002121f, -0.0005192f, 0.0048215f, 1.2057984f,
+
+-0.4747804f, 0.2284722f, -0.1127197f, 0.0400247f, -0.0091929f, 0.0009900f, 0.0013500f, -0.0125358f,
+0.0649243f, 1.2344289f, -0.5940278f, 0.2930712f, -0.1040642f, 0.0239017f, -0.0025740f, -0.0021214f,
+
+0.0196992f, -0.1020238f, 0.3458974f, 0.9334723f, -0.4605404f, 0.1635294f, -0.0375598f, 0.0040449f,
+0.0021214f, -0.0196992f, 0.1020238f, -0.3458974f, 0.8443055f, 0.4605404f, -0.1635294f, 0.0375598f,
+
+-0.0040449f, -0.0014850f, 0.0137894f, -0.0714167f, 0.2421282f, -0.5910138f, 1.2776217f, 0.1144706f,
+-0.0262918f, 0.0028314f, 0.0007425f, -0.0068947f, 0.0357083f, -0.1210641f, 0.2955069f, -0.6388109f,
+
+1.2760980f, 0.0131459f, -0.0014157f, -0.0002121f, 0.0019699f, -0.0102024f, 0.0345897f, -0.0844306f,
+0.1825174f, -0.3645994f, 1.1391011f, 0.0004045f, 0.0000265f, -0.0002462f, 0.0012753f, -0.0043237f,
+
+0.0105538f, -0.0228147f, 0.0455749f, -0.1423876f, 0.9999495f, 0.2845912f, -0.1179245f, 0.2594340f,
+-0.0927673f, 0.2091195f, -0.0424528f, 0.1839623f, -0.0172956f, 0.1336478f, 0.0330189f, 0.1084906f,
+
+0.0581761f, 0.0581761f, 0.1084906f, 0.0330189f, 0.1336478f, -0.0172956f, 0.1839623f, -0.0424528f,
+0.2091195f, -0.0927673f, 0.2594340f, -0.1179245f, 0.2845912f, 0.4784868f, -0.1190476f, 0.0453227f,
+
+0.3664491f, -0.0380952f, 0.0145033f, 0.2544114f, 0.0428571f, -0.0163162f, 0.1423737f, 0.1238095f,
+-0.0471356f, 0.0303360f, 0.2047619f, -0.0779550f, -0.0817017f, 0.2857143f, -0.1087745f, -0.1087745f,
+
+0.2857143f, -0.0817017f, -0.0779550f, 0.2047619f, 0.0303360f, -0.0471356f, 0.1238095f, 0.1423737f,
+-0.0163162f, 0.0428571f, 0.2544114f, 0.0145033f, -0.0380952f, 0.3664491f, 0.0453227f, -0.1190476f,
+
+0.4784868f, 0.6098023f, -0.1552631f, 0.0402324f, -0.0131765f, 0.4185360f, 0.0020702f, -0.0005364f,
+0.0001757f, 0.1794530f, 0.1987367f, -0.0514975f, 0.0168660f, -0.0118133f, 0.3560700f, -0.0922663f,
+
+0.0302182f, -0.1430446f, 0.4367628f, -0.0855359f, 0.0280139f, -0.0819523f, 0.2502276f, 0.1009994f,
+-0.0330784f, -0.0330784f, 0.1009994f, 0.2502276f, -0.0819523f, 0.0280139f, -0.0855359f, 0.4367628f,
+
+-0.1430446f, 0.0302182f, -0.0922663f, 0.3560700f, -0.0118133f, 0.0168660f, -0.0514975f, 0.1987367f,
+0.1794530f, 0.0001757f, -0.0005364f, 0.0020702f, 0.4185360f, -0.0131765f, 0.0402324f, -0.1552631f,
+
+0.6098023f, 0.7383369f, -0.1727899f, 0.0494924f, -0.0124893f, 0.0036259f, 0.3966643f, 0.1151932f,
+-0.0329949f, 0.0083262f, -0.0024173f, 0.0549918f, 0.4031764f, -0.1154822f, 0.0291417f, -0.0084605f,
+
+-0.1588972f, 0.5473127f, -0.1175973f, 0.0296755f, -0.0086155f, -0.0755414f, 0.2601981f, 0.1996616f,
+-0.0503842f, 0.0146277f, 0.0078145f, -0.0269165f, 0.5169204f, -0.1304439f, 0.0378708f, 0.0378708f,
+
+-0.1304439f, 0.5169204f, -0.0269165f, 0.0078145f, 0.0146277f, -0.0503842f, 0.1996616f, 0.2601981f,
+-0.0755414f, -0.0086155f, 0.0296755f, -0.1175973f, 0.5473127f, -0.1588972f, -0.0084605f, 0.0291417f,
+
+-0.1154822f, 0.4031764f, 0.0549918f, -0.0024173f, 0.0083262f, -0.0329949f, 0.1151932f, 0.3966643f,
+0.0036259f, -0.0124893f, 0.0494924f, -0.1727899f, 0.7383369f, 0.7979735f, -0.1758341f, 0.0515405f,
+
+-0.0146323f, 0.0039688f, -0.0009159f, 0.3719327f, 0.2344454f, -0.0687206f, 0.0195097f, -0.0052917f,
+0.0012212f, -0.1149711f, 0.7033362f, -0.2061619f, 0.0585292f, -0.0158751f, 0.0036635f, -0.0905686f,
+
+0.3924640f, 0.1691985f, -0.0480354f, 0.0130288f, -0.0030066f, 0.0089078f, -0.0386003f, 0.6271625f,
+-0.1780511f, 0.0482933f, -0.0111446f, 0.0349973f, -0.1516549f, 0.5591316f, 0.0305291f, -0.0082805f,
+
+0.0019109f, 0.0019109f, -0.0082805f, 0.0305291f, 0.5591316f, -0.1516549f, 0.0349973f, -0.0111446f,
+0.0482933f, -0.1780511f, 0.6271625f, -0.0386003f, 0.0089078f, -0.0030066f, 0.0130288f, -0.0480354f,
+
+0.1691985f, 0.3924640f, -0.0905686f, 0.0036635f, -0.0158751f, 0.0585292f, -0.2061619f, 0.7033362f,
+-0.1149711f, 0.0012212f, -0.0052917f, 0.0195097f, -0.0687206f, 0.2344454f, 0.3719327f, -0.0009159f,
+
+0.0039688f, -0.0146323f, 0.0515405f, -0.1758341f, 0.7979735f, 0.8748942f, -0.1721068f, 0.0498959f,
+-0.0156577f, 0.0051759f, -0.0015183f, 0.0003136f, 0.2859561f, 0.3933870f, -0.1140478f, 0.0357889f,
+
+-0.0118306f, 0.0034705f, -0.0007168f, -0.1582316f, 0.7660421f, -0.1602371f, 0.0502835f, -0.0166220f,
+0.0048760f, -0.0010072f, -0.0333551f, 0.1614811f, 0.5716645f, -0.1793921f, 0.0593008f, -0.0173957f,
+
+0.0035932f, 0.0376046f, -0.1820540f, 0.7985786f, -0.0774646f, 0.0256071f, -0.0075117f, 0.0015516f,
+0.0007307f, -0.0035377f, 0.0155179f, 0.6876691f, -0.2273195f, 0.0666833f, -0.0137739f, -0.0104493f,
+
+0.0505878f, -0.2219028f, 0.6858636f, 0.0164518f, -0.0048261f, 0.0009969f, 0.0012696f, -0.0061464f,
+0.0269613f, -0.0833326f, 0.8178639f, -0.2399172f, 0.0495567f, 0.0026656f, -0.0129047f, 0.0566062f,
+
+-0.1749600f, 0.5587704f, 0.1561008f, -0.0322438f, -0.0006815f, 0.0032996f, -0.0144735f, 0.0447351f,
+-0.1428707f, 0.7886099f, -0.1628932f, -0.0007168f, 0.0034705f, -0.0152231f, 0.0470521f, -0.1502703f,
+
+0.4469841f, 0.2748853f, 0.0003136f, -0.0015183f, 0.0066601f, -0.0205853f, 0.0657433f, -0.1955555f,
+0.8797377f, 0.9262632f, -0.2411474f, 0.0556399f, -0.0149728f, 0.0047774f, -0.0011027f, 0.0002986f,
+
+-0.0000614f, 0.1966316f, 0.6430596f, -0.1483731f, 0.0399274f, -0.0127397f, 0.0029406f, -0.0007962f,
+0.0001636f, -0.1669505f, 0.8124927f, 0.0469259f, -0.0126278f, 0.0040292f, -0.0009300f, 0.0002518f,
+
+-0.0000517f, 0.0370907f, -0.1805080f, 0.9206194f, -0.2477401f, 0.0790471f, -0.0182455f, 0.0049403f,
+-0.0010151f, 0.0159203f, -0.0774786f, 0.2861435f, 0.5380877f, -0.1716891f, 0.0396290f, -0.0107303f,
+
+0.0022049f, -0.0112696f, 0.0548455f, -0.2025551f, 0.8775509f, -0.0836501f, 0.0193080f, -0.0052280f,
+0.0010742f, 0.0010742f, -0.0052280f, 0.0193080f, -0.0836501f, 0.8775509f, -0.2025551f, 0.0548455f,
+
+-0.0112696f, 0.0022049f, -0.0107303f, 0.0396290f, -0.1716891f, 0.5380877f, 0.2861435f, -0.0774786f,
+0.0159203f, -0.0010151f, 0.0049403f, -0.0182455f, 0.0790471f, -0.2477401f, 0.9206194f, -0.1805080f,
+
+0.0370907f, -0.0000517f, 0.0002518f, -0.0009300f, 0.0040292f, -0.0126278f, 0.0469259f, 0.8124927f,
+-0.1669505f, 0.0001636f, -0.0007962f, 0.0029406f, -0.0127397f, 0.0399274f, -0.1483731f, 0.6430596f,
+
+0.1966316f, -0.0000614f, 0.0002986f, -0.0011027f, 0.0047774f, -0.0149728f, 0.0556399f, -0.2411474f,
+0.9262632f, 0.9815653f, -0.2288699f, 0.0695333f, -0.0149811f, 0.0048350f, -0.0011356f, 0.0003056f,
+
+-0.0000889f, 0.0000157f, 0.0737389f, 0.9154796f, -0.2781330f, 0.0599244f, -0.0193400f, 0.0045422f,
+-0.0012223f, 0.0003556f, -0.0000628f, -0.0983185f, 0.5571383f, 0.3708440f, -0.0798992f, 0.0257866f,
+
+-0.0060563f, 0.0016297f, -0.0004742f, 0.0000837f, 0.0536868f, -0.3042253f, 1.0456146f, -0.0337669f,
+0.0108979f, -0.0025595f, 0.0006888f, -0.0002004f, 0.0000354f, -0.0096989f, 0.0549607f, -0.1888986f,
+
+0.9982641f, -0.3221796f, 0.0756679f, -0.0203621f, 0.0059244f, -0.0010455f, -0.0025960f, 0.0147109f,
+-0.0505609f, 0.1878900f, 0.8000000f, -0.1878900f, 0.0505609f, -0.0147109f, 0.0025960f, 0.0025960f,
+
+-0.0147109f, 0.0505609f, -0.1878900f, 0.8000000f, 0.1878900f, -0.0505609f, 0.0147109f, -0.0025960f,
+-0.0010455f, 0.0059244f, -0.0203621f, 0.0756679f, -0.3221796f, 0.9982641f, -0.1888986f, 0.0549607f,
+
+-0.0096989f, 0.0000354f, -0.0002004f, 0.0006888f, -0.0025595f, 0.0108979f, -0.0337669f, 1.0456146f,
+-0.3042253f, 0.0536868f, 0.0000837f, -0.0004742f, 0.0016297f, -0.0060563f, 0.0257866f, -0.0798992f,
+
+0.3708440f, 0.5571383f, -0.0983185f, -0.0000628f, 0.0003556f, -0.0012223f, 0.0045422f, -0.0193400f,
+0.0599244f, -0.2781330f, 0.9154796f, 0.0737389f, 0.0000157f, -0.0000889f, 0.0003056f, -0.0011356f,
+
+0.0048350f, -0.0149811f, 0.0695333f, -0.2288699f, 0.9815653f, 0.9974228f, -0.2132571f, 0.0803035f,
+-0.0276114f, 0.0056224f, -0.0012128f, 0.0003452f, -0.0001335f, 0.0000402f, -0.0000059f, 0.0137454f,
+
+1.1373711f, -0.4282856f, 0.1472608f, -0.0299864f, 0.0064685f, -0.0018411f, 0.0007119f, -0.0002144f,
+0.0000315f, -0.0297817f, 0.2023627f, 0.9279522f, -0.3190651f, 0.0649705f, -0.0140151f, 0.0039891f,
+
+-0.0015424f, 0.0004644f, -0.0000684f, 0.0330908f, -0.2248475f, 0.7467198f, 0.3545168f, -0.0721895f,
+0.0155723f, -0.0044323f, 0.0017138f, -0.0005161f, 0.0000759f, -0.0193029f, 0.1311610f, -0.4355866f,
+
+1.1265318f, 0.0421105f, -0.0090838f, 0.0025855f, -0.0009997f, 0.0003010f, -0.0000443f, 0.0051952f,
+-0.0353010f, 0.1172349f, -0.3031976f, 1.0652362f, -0.1495209f, 0.0425580f, -0.0164556f, 0.0049550f,
+
+-0.0007292f, -0.0003584f, 0.0024355f, -0.0080883f, 0.0209182f, -0.0734929f, 1.1395742f, -0.3243563f,
+0.1254161f, -0.0377645f, 0.0055578f, -0.0000443f, 0.0003010f, -0.0009997f, 0.0025855f, -0.0090838f,
+
+0.0488706f, 1.1246078f, -0.4348426f, 0.1309370f, -0.0192700f, 0.0000759f, -0.0005161f, 0.0017138f,
+-0.0044323f, 0.0155723f, -0.0837781f, 0.3578153f, 0.7454444f, -0.2244634f, 0.0330342f, -0.0000684f,
+
+0.0004644f, -0.0015424f, 0.0039891f, -0.0140151f, 0.0754003f, -0.3220338f, 0.9291000f, 0.2020171f,
+-0.0297308f, 0.0000315f, -0.0002144f, 0.0007119f, -0.0018411f, 0.0064685f, -0.0348001f, 0.1486310f,
+
+-0.4288154f, 1.1375306f, 0.0137219f, -0.0000059f, 0.0000402f, -0.0001335f, 0.0003452f, -0.0012128f,
+0.0065250f, -0.0278683f, 0.0804029f, -0.2132870f, 0.9974272f, 0.9999995f, -0.0666571f, 0.0153275f,
+
+-0.0049158f, 0.0024855f, -0.0011966f, 0.0005915f, -0.0002124f, 0.0000571f, -0.0000096f, 0.0000006f,
+0.0000089f, 1.0665138f, -0.2452399f, 0.0786531f, -0.0397674f, 0.0191453f, -0.0094634f, 0.0033982f,
+
+-0.0009139f, 0.0001529f, -0.0000089f, -0.0000446f, 0.0007645f, 1.2261996f, -0.3932655f, 0.1988369f,
+-0.0957265f, 0.0473169f, -0.0169909f, 0.0045696f, -0.0007645f, 0.0000446f, 0.0001450f, -0.0024845f,
+
+0.0148512f, 1.2781130f, -0.6462200f, 0.3111111f, -0.1537800f, 0.0552203f, -0.0148512f, 0.0024845f,
+-0.0001450f, -0.0002900f, 0.0049690f, -0.0297024f, 0.1104406f, 1.2924401f, -0.6222222f, 0.3075600f,
+
+-0.1104406f, 0.0297024f, -0.0049690f, 0.0002900f, 0.0004143f, -0.0070986f, 0.0424320f, -0.1577723f,
+0.4393714f, 0.8888889f, -0.4393714f, 0.1577723f, -0.0424320f, 0.0070986f, -0.0004143f, -0.0004143f,
+
+0.0070986f, -0.0424320f, 0.1577723f, -0.4393714f, 0.8888889f, 0.4393714f, -0.1577723f, 0.0424320f,
+-0.0070986f, 0.0004143f, 0.0002900f, -0.0049690f, 0.0297024f, -0.1104406f, 0.3075600f, -0.6222222f,
+
+1.2924401f, 0.1104406f, -0.0297024f, 0.0049690f, -0.0002900f, -0.0001450f, 0.0024845f, -0.0148512f,
+0.0552203f, -0.1537800f, 0.3111111f, -0.6462200f, 1.2781130f, 0.0148512f, -0.0024845f, 0.0001450f,
+
+0.0000446f, -0.0007645f, 0.0045696f, -0.0169909f, 0.0473169f, -0.0957265f, 0.1988369f, -0.3932655f,
+1.2261996f, 0.0007645f, -0.0000446f, -0.0000089f, 0.0001529f, -0.0009139f, 0.0033982f, -0.0094634f,
+
+0.0191453f, -0.0397674f, 0.0786531f, -0.2452399f, 1.0665138f, 0.0000089f, 0.0000006f, -0.0000096f,
+0.0000571f, -0.0002124f, 0.0005915f, -0.0011966f, 0.0024855f, -0.0049158f, 0.0153275f, -0.0666571f,
+
+0.9999995f
diff --git a/encoder/basisu_bc15_spmd.cpp b/encoder/basisu_bc15_spmd.cpp
new file mode 100644
index 0000000..4bac1c1
--- /dev/null
+++ b/encoder/basisu_bc15_spmd.cpp
@@ -0,0 +1,441 @@
+// basisu_bc15_spmd.cpp -- standalone opaque 4-color BC1 encoder (see basisu_bc15_spmd.h).
+//
+// This TU holds the public API, the solid-color omatch tables, and the scalar reference encoder. The SSE4.1
+// cppspmd kernel lives in basisu_bc15_spmd_kernels.inl, compiled by basisu_bc15_spmd_sse.cpp; we only declare +
+// call its wrapper here (gated on g_cpu_supports_sse41), with the scalar path as the fallback.
+
+#include "basisu_bc15_spmd.h"
+#include "basisu_enc.h"
+#include <math.h>
+
+namespace basisu
+{
+	extern bool g_cpu_supports_sse41; // set by detect_sse41() in basisu_encoder_init()
+
+	namespace bc_spmd
+	{
+		// Baked-in benchmarked defaults (not exposed as parameters by design).
+		static const int kLsRounds = 2; // least-squares alternation rounds
+		static const int kAvgVar = 16;  // try the avg-solid 2nd seed when (rng_r+rng_g+rng_b) < kAvgVar
+
+#if BASISU_SUPPORT_SSE
+		// Defined in basisu_bc15_spmd_kernels.inl (the SSE4.1 TU). Encodes up to 4 blocks at once, writing num_write
+		// of them directly at pOut + j*out_stride (no temp buffer).
+		void encode_bc1_blocks_4_sse41(const color_rgba* pBlocks, uint8_t* pOut,
+			int* om5lo, int* om5hi, int* om6lo, int* om6hi, int ls_rounds, int avgvar,
+			uint32_t out_stride, uint32_t num_write);
+
+		// BC4: encodes up to 4 single-channel blocks at once, writing num_write of them at pOut + j*out_stride.
+		// do_ls!=0 adds the 1-D least-squares refit. src_stride = byte stride between texel values in pBlocks (1 or 4).
+		void encode_bc4_blocks_4_sse41(const uint8_t* pBlocks, uint8_t* pOut, uint32_t out_stride, uint32_t num_write, uint32_t do_ls, uint32_t src_stride);
+#endif
+
+		// ---- solid-color omatch tables (stb_dxt/basisu 3% span penalty), as int[256] for the kernel's gather ----
+		static int s_om5_lo[256], s_om5_hi[256], s_om6_lo[256], s_om6_hi[256];
+
+		static void build_omatch_tables()
+		{
+			for (int bits = 5; bits <= 6; bits++)
+			{
+				const int levels = 1 << bits;
+				int* lo = (bits == 5) ? s_om5_lo : s_om6_lo;
+				int* hi = (bits == 5) ? s_om5_hi : s_om6_hi;
+				for (int target = 0; target < 256; target++)
+				{
+					int best = 0x7FFFFFFF, bc0 = 0, bc1 = 0;
+					for (int c0 = 0; c0 < levels; c0++)
+					{
+						const int e0 = (bits == 5) ? ((c0 << 3) | (c0 >> 2)) : ((c0 << 2) | (c0 >> 4));
+						for (int c1 = 0; c1 < levels; c1++) // FULL cross product (matches encode_bc1's prepare_bc1_single_color_table)
+						{
+							const int e1 = (bits == 5) ? ((c1 << 3) | (c1 >> 2)) : ((c1 << 2) | (c1 >> 4));
+							const int interp = (2 * e0 + e1) / 3;          // selector-2 (2/3 toward c0), truncated
+							int err = (interp > target) ? (interp - target) : (target - interp);
+							err += ((e0 > e1 ? e0 - e1 : e1 - e0) * 3) / 100; // 3% linear span penalty (abs: e0 may be < e1 now)
+							if (err < best) { best = err; bc0 = c0; bc1 = c1; }
+						}
+					}
+					lo[target] = bc0; hi[target] = bc1;
+				}
+			}
+		}
+
+		// Thread-safe one-time table init: C++11 guarantees a function-local static's initializer runs exactly once,
+		// race-free, even under concurrent first calls (no double-checked-locking bug). init() just forces it.
+		static inline void ensure_init() { static const bool s_built = (build_omatch_tables(), true); (void)s_built; }
+		void init() { ensure_init(); }
+
+		// ---- scalar reference encoder (the oracle) -- identical algorithm/op-order to the SPMD kernel ----
+		static inline void unpack565(uint32_t c, int& r, int& g, int& b)
+		{
+			r = (c >> 11) & 31; g = (c >> 5) & 63; b = c & 31;
+			r = (r << 3) | (r >> 2); g = (g << 2) | (g >> 4); b = (b << 3) | (b >> 2);
+		}
+		static inline uint32_t f_to_565(float r, float g, float b)
+		{
+			const float cr = (r < 0.0f) ? 0.0f : (r > 255.0f ? 255.0f : r);
+			const float cg = (g < 0.0f) ? 0.0f : (g > 255.0f ? 255.0f : g);
+			const float cb = (b < 0.0f) ? 0.0f : (b > 255.0f ? 255.0f : b);
+			return ((int)rintf(cr * (31.0f / 255.0f)) << 11) | ((int)rintf(cg * (63.0f / 255.0f)) << 5) | (int)rintf(cb * (31.0f / 255.0f));
+		}
+		static inline int thresh_sel(const int* pr, const int* pg, const int* pb, int r, int g, int b)
+		{
+			const int ar = pr[1] - pr[0], ag = pg[1] - pg[0], ab = pb[1] - pb[0];
+			const int dp0 = pr[0] * ar + pg[0] * ag + pb[0] * ab;
+			const int dp2 = pr[2] * ar + pg[2] * ag + pb[2] * ab;
+			const int dp3 = pr[3] * ar + pg[3] * ag + pb[3] * ab;
+			const int dp1 = pr[1] * ar + pg[1] * ag + pb[1] * ab;
+			const int t0 = dp0 + dp2, t1 = dp2 + dp3, t2 = dp3 + dp1;
+			const int d = r * (ar + ar) + g * (ag + ag) + b * (ab + ab);
+			return (d <= t0) ? 0 : (d < t1) ? 2 : (d < t2) ? 3 : 1;
+		}
+		static void ls_round(const color_rgba* blk, uint32_t& c0, uint32_t& c1)
+		{
+			int r0, g0, b0, r1, g1, b1;
+			unpack565(c0, r0, g0, b0); unpack565(c1, r1, g1, b1);
+			const int pr[4] = { r0, r1, (2 * r0 + r1) / 3, (r0 + 2 * r1) / 3 };
+			const int pg[4] = { g0, g1, (2 * g0 + g1) / 3, (g0 + 2 * g1) / 3 };
+			const int pb[4] = { b0, b1, (2 * b0 + b1) / 3, (b0 + 2 * b1) / 3 };
+			float m00 = 0, m01 = 0, m11 = 0, a0r = 0, a0g = 0, a0b = 0, a1r = 0, a1g = 0, a1b = 0;
+			for (uint32_t i = 0; i < 16; i++)
+			{
+				const int sel = thresh_sel(pr, pg, pb, blk[i].r, blk[i].g, blk[i].b);
+				const float tb = (sel == 0) ? 0.0f : (sel == 1) ? 1.0f : (sel == 2) ? (1.0f / 3.0f) : (2.0f / 3.0f);
+				const float ta = 1.0f - tb;
+				m00 += ta * ta; m01 += ta * tb; m11 += tb * tb;
+				const float cr = (float)blk[i].r, cg = (float)blk[i].g, cb = (float)blk[i].b;
+				a0r += ta * cr; a0g += ta * cg; a0b += ta * cb;
+				a1r += tb * cr; a1g += tb * cg; a1b += tb * cb;
+			}
+			const float det = m00 * m11 - m01 * m01;
+			const float inv = 1.0f / basisu::maximum(det, 1e-6f);
+			const float e0r = (a0r * m11 - a1r * m01) * inv, e0g = (a0g * m11 - a1g * m01) * inv, e0b = (a0b * m11 - a1b * m01) * inv;
+			const float e1r = (a1r * m00 - a0r * m01) * inv, e1g = (a1g * m00 - a0g * m01) * inv, e1b = (a1b * m00 - a0b * m01) * inv;
+			uint32_t nc0 = f_to_565(e0r, e0g, e0b), nc1 = f_to_565(e1r, e1g, e1b);
+			{ const uint32_t hi = basisu::maximum(nc0, nc1), lo = basisu::minimum(nc0, nc1); nc0 = hi; nc1 = lo; }
+			if (det > 1e-6f) { c0 = nc0; c1 = nc1; }
+		}
+		static int eval_error(const color_rgba* blk, uint32_t c0, uint32_t c1)
+		{
+			int r0, g0, b0, r1, g1, b1;
+			unpack565(c0, r0, g0, b0); unpack565(c1, r1, g1, b1);
+			const int pr[4] = { r0, r1, (2 * r0 + r1) / 3, (r0 + 2 * r1) / 3 };
+			const int pg[4] = { g0, g1, (2 * g0 + g1) / 3, (g0 + 2 * g1) / 3 };
+			const int pb[4] = { b0, b1, (2 * b0 + b1) / 3, (b0 + 2 * b1) / 3 };
+			int err = 0;
+			for (uint32_t i = 0; i < 16; i++)
+			{
+				const int sel = thresh_sel(pr, pg, pb, blk[i].r, blk[i].g, blk[i].b);
+				const int dr = blk[i].r - pr[sel], dg = blk[i].g - pg[sel], db = blk[i].b - pb[sel];
+				err += dr * dr + dg * dg + db * db;
+			}
+			return err;
+		}
+		static void encode_block_scalar(const color_rgba* blk, uint8_t out[8])
+		{
+			int mnr = 255, mng = 255, mnb = 255, mxr = 0, mxg = 0, mxb = 0, sum_r = 0, sum_g = 0, sum_b = 0;
+			int sum_rr = 0, sum_gg = 0, sum_bb = 0, sum_rg = 0, sum_rb = 0, sum_gb = 0;
+			for (uint32_t i = 0; i < 16; i++)
+			{
+				const int r = blk[i].r, g = blk[i].g, b = blk[i].b;
+				mnr = basisu::minimum<int>(mnr, r); mng = basisu::minimum<int>(mng, g); mnb = basisu::minimum<int>(mnb, b);
+				mxr = basisu::maximum<int>(mxr, r); mxg = basisu::maximum<int>(mxg, g); mxb = basisu::maximum<int>(mxb, b);
+				sum_r += r; sum_g += g; sum_b += b;
+				sum_rr += r * r; sum_gg += g * g; sum_bb += b * b;
+				sum_rg += r * g; sum_rb += r * b; sum_gb += g * b;
+			}
+
+			// Solid block: clone of basist::encode_bc1_solid_block -- omatch endpoints from the (full-cross-product)
+			// single-color tables, then the same swap + degenerate handling that GUARANTEES a 4-color block
+			// (color0 > color1), never 3-color (required so this is safe for BC3/BC5 color blocks).
+			if ((mnr == mxr) && (mng == mxg) && (mnb == mxb))
+			{
+				const int ar = (sum_r + 8) >> 4, ag = (sum_g + 8) >> 4, ab = (sum_b + 8) >> 4;
+				uint32_t max16 = (s_om5_lo[ar] << 11) | (s_om6_lo[ag] << 5) | s_om5_lo[ab]; // lo[] = 2x-weighted endpoint (encode_bc1 m_hi)
+				uint32_t min16 = (s_om5_hi[ar] << 11) | (s_om6_hi[ag] << 5) | s_om5_hi[ab]; // hi[] = 1x endpoint (encode_bc1 m_lo)
+				uint32_t mask = 0xAA;
+				if (min16 == max16)
+				{
+					mask = 0; // selector 0 = color0 directly; force max16 > min16 so the block stays 4-color
+					if (min16 > 0) min16--;
+					else { max16 = 1; min16 = 0; mask = 0x55; } // l == h == 0
+				}
+				if (max16 < min16) { const uint32_t t = max16; max16 = min16; min16 = t; mask ^= 0x55; } // selector 2<->3 on swap
+				out[0] = (uint8_t)max16; out[1] = (uint8_t)(max16 >> 8); out[2] = (uint8_t)min16; out[3] = (uint8_t)(min16 >> 8);
+				out[4] = out[5] = out[6] = out[7] = (uint8_t)mask;
+				return;
+			}
+
+			// PCA seed (METHOD 2, on-axis, sqrt-free).
+			const float cxx = (float)(16 * sum_rr - sum_r * sum_r), cxy = (float)(16 * sum_rg - sum_r * sum_g), cxz = (float)(16 * sum_rb - sum_r * sum_b);
+			const float cyy = (float)(16 * sum_gg - sum_g * sum_g), cyz = (float)(16 * sum_gb - sum_g * sum_b), czz = (float)(16 * sum_bb - sum_b * sum_b);
+			float ax = (float)(mxr - mnr), ay = (float)(mxg - mng), az = (float)(mxb - mnb);
+			for (int it = 0; it < 4; it++)
+			{
+				const float nr = ax * cxx + ay * cxy + az * cxz;
+				const float ng = ax * cxy + ay * cyy + az * cyz;
+				const float nb = ax * cxz + ay * cyz + az * czz;
+				ax = nr; ay = ng; az = nb;
+			}
+			const float kk = basisu::maximum(basisu::maximum(fabsf(ax), fabsf(ay)), fabsf(az));
+			const float mm = 1024.0f / basisu::maximum(kk, 1e-3f);
+			ax *= mm; ay *= mm; az *= mm;
+			const float inv_len2 = 1.0f / (ax * ax + ay * ay + az * az + 0.0000125f);
+			const float meanx = (float)sum_r * (1.0f / 16.0f), meany = (float)sum_g * (1.0f / 16.0f), meanz = (float)sum_b * (1.0f / 16.0f);
+			float minp = 1e30f, maxp = -1e30f;
+			for (uint32_t i = 0; i < 16; i++)
+			{
+				const float prj = ((float)blk[i].r - meanx) * ax + ((float)blk[i].g - meany) * ay + ((float)blk[i].b - meanz) * az;
+				minp = basisu::minimum(minp, prj); maxp = basisu::maximum(maxp, prj);
+			}
+			const float tlo = minp * inv_len2, thi = maxp * inv_len2;
+			uint32_t color0 = f_to_565(meanx + tlo * ax, meany + tlo * ay, meanz + tlo * az);
+			uint32_t color1 = f_to_565(meanx + thi * ax, meany + thi * ay, meanz + thi * az);
+			{ const uint32_t hi = basisu::maximum(color0, color1), lo = basisu::minimum(color0, color1); color0 = hi; color1 = lo; }
+			if (color0 == color1) { if (color1 != 0) color1--; else color0++; }
+
+			for (int r = 0; r < kLsRounds; r++) ls_round(blk, color0, color1);
+			if (color0 == color1) { if (color1 != 0) color1--; else color0++; }
+
+			// Low-variance avg-solid 2nd seed (keep-better).
+			const int var_proxy = (mxr - mnr) + (mxg - mng) + (mxb - mnb);
+			if (var_proxy < kAvgVar)
+			{
+				const int ar = (sum_r + 8) >> 4, ag = (sum_g + 8) >> 4, ab = (sum_b + 8) >> 4;
+				uint32_t c0b = (s_om5_lo[ar] << 11) | (s_om6_lo[ag] << 5) | s_om5_lo[ab];
+				uint32_t c1b = (s_om5_hi[ar] << 11) | (s_om6_hi[ag] << 5) | s_om5_hi[ab];
+				{ const uint32_t hi = basisu::maximum(c0b, c1b), lo = basisu::minimum(c0b, c1b); c0b = hi; c1b = lo; }
+				if (c0b == c1b) { if (c1b != 0) c1b--; else c0b++; }
+				ls_round(blk, c0b, c1b);
+				if (c0b == c1b) { if (c1b != 0) c1b--; else c0b++; }
+				if (eval_error(blk, c0b, c1b) < eval_error(blk, color0, color1)) { color0 = c0b; color1 = c1b; }
+			}
+
+			int r0, g0, b0, r1, g1, b1;
+			unpack565(color0, r0, g0, b0); unpack565(color1, r1, g1, b1);
+			const int pr[4] = { r0, r1, (2 * r0 + r1) / 3, (r0 + 2 * r1) / 3 };
+			const int pg[4] = { g0, g1, (2 * g0 + g1) / 3, (g0 + 2 * g1) / 3 };
+			const int pb[4] = { b0, b1, (2 * b0 + b1) / 3, (b0 + 2 * b1) / 3 };
+			uint32_t sw = 0;
+			for (uint32_t i = 0; i < 16; i++)
+				sw |= ((uint32_t)thresh_sel(pr, pg, pb, blk[i].r, blk[i].g, blk[i].b)) << (i * 2);
+			out[0] = (uint8_t)color0; out[1] = (uint8_t)(color0 >> 8);
+			out[2] = (uint8_t)color1; out[3] = (uint8_t)(color1 >> 8);
+			out[4] = (uint8_t)sw; out[5] = (uint8_t)(sw >> 8); out[6] = (uint8_t)(sw >> 16); out[7] = (uint8_t)(sw >> 24);
+		}
+
+		// ---- public API ----
+		void encode_bc1_scalar(void* pBlocks, const color_rgba* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride)
+		{
+			ensure_init();
+			uint8_t* pOut = (uint8_t*)pBlocks;
+			const uint32_t out_stride = 8 * block_stride;
+			for (uint32_t b = 0; b < num_blocks; b++)
+				encode_block_scalar(pSrc_pixels + b * 16, pOut + b * out_stride);
+		}
+
+		void encode_bc1_spmd(void* pBlocks, const color_rgba* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride)
+		{
+			ensure_init();
+			uint8_t* pOut = (uint8_t*)pBlocks;
+			(void)pOut;
+			const uint32_t out_stride = 8 * block_stride;
+			(void)out_stride;
+#if BASISU_SUPPORT_SSE
+			if (g_cpu_supports_sse41)
+			{
+				// Full groups of 4: the kernel writes all 4 blocks directly at the (possibly strided) output.
+				uint32_t base = 0;
+				for (; base + 4 <= num_blocks; base += 4)
+					encode_bc1_blocks_4_sse41(pSrc_pixels + base * 16, pOut + base * out_stride, s_om5_lo, s_om5_hi, s_om6_lo, s_om6_hi, kLsRounds, kAvgVar, out_stride, 4);
+
+				// Final partial group: pad the input to 4, but write only the n valid blocks (still direct, no temp).
+				const uint32_t n = num_blocks - base; // 0..3
+				if (n)
+				{
+					color_rgba grp[64];
+					for (uint32_t gi = 0; gi < 4; gi++)
+						memcpy(grp + gi * 16, pSrc_pixels + (base + ((gi < n) ? gi : (n - 1))) * 16, 16 * sizeof(color_rgba));
+					encode_bc1_blocks_4_sse41(grp, pOut + base * out_stride, s_om5_lo, s_om5_hi, s_om6_lo, s_om6_hi, kLsRounds, kAvgVar, out_stride, n);
+				}
+				return;
+			}
+#endif
+			encode_bc1_scalar(pBlocks, pSrc_pixels, num_blocks, block_stride); // scalar fallback (no SSE4.1)
+		}
+
+		// ================================ BC4 (RGTC1, single channel) ================================
+
+		// Scalar reference (oracle) for one BC4 block -- clone of basist::encode_bc4 (raw bbox min/max, exact
+		// nearest-of-8 integer-threshold selector), but the solid case is forced to 8-value mode (red0 > red1) so
+		// every block is 8-color. v = 16 contiguous channel bytes (texel i = y*4+x). out = 8 bytes.
+		// Compute the nearest-of-8 selectors + the resulting decoded SSE for an 8-value endpoint pair (r0 > r1).
+		// Optionally accumulates the 1-D least-squares moments (rank = interpolation weight, 0..7).
+		static int bc4_eval(const uint8_t* v, int r0, int r1, uint64_t& sel_out, int* pSw, int* pSww, int* pSv, int* pSwv, uint32_t src_stride)
+		{
+			const int delta = r0 - r1;
+			const int t0 = delta * 13, t1 = delta * 11, t2 = delta * 9, t3 = delta * 7, t4 = delta * 5, t5 = delta * 3, t6 = delta;
+			const int bias = 4 - r1 * 14;
+			uint64_t sel = 0; int sse = 0, Sw = 0, Sww = 0, Sv = 0, Swv = 0;
+			for (int i = 0; i < 16; i++)
+			{
+				const int val = v[i * src_stride];
+				const int sv = val * 14 + bias;
+				const int rank = (sv >= t0) + (sv >= t1) + (sv >= t2) + (sv >= t3) + (sv >= t4) + (sv >= t5) + (sv >= t6);
+				const int idx = (rank == 7) ? 0 : (rank == 0) ? 1 : (8 - rank);
+				sel |= (uint64_t)idx << (i * 3);
+				const int recon = (rank * r0 + (7 - rank) * r1) / 7; // == decoded value for this selector (matches unpack_bc4)
+				const int d = val - recon; sse += d * d;
+				Sw += rank; Sww += rank * rank; Sv += val; Swv += rank * val;
+			}
+			sel_out = sel;
+			if (pSw) { *pSw = Sw; *pSww = Sww; *pSv = Sv; *pSwv = Swv; }
+			return sse;
+		}
+
+		static void encode_bc4_block(const uint8_t* v, uint8_t out[8], bool do_ls, uint32_t src_stride)
+		{
+			int mn = v[0], mx = v[0];
+			for (int i = 1; i < 16; i++) { const int x = v[i * src_stride]; mn = basisu::minimum(mn, x); mx = basisu::maximum(mx, x); }
+
+			// Solid -> force an 8-value block (red0 > red1) that still reconstructs the exact value.
+			if (mn == mx)
+			{
+				int r0, r1, idx;
+				if (mx > 0) { r0 = mx; r1 = mx - 1; idx = 0; } // all selectors index 0 = red0 = value
+				else { r0 = 1; r1 = 0; idx = 1; }             // value 0: all selectors index 1 = red1 = 0
+				out[0] = (uint8_t)r0; out[1] = (uint8_t)r1;
+				uint64_t sel = 0; for (int i = 0; i < 16; i++) sel |= (uint64_t)idx << (i * 3);
+				for (int i = 0; i < 6; i++) out[2 + i] = (uint8_t)(sel >> (i * 8));
+				return;
+			}
+
+			// Candidate A: raw bbox endpoints (== basist::encode_bc4) + (for HQ) its LS moments.
+			int Sw, Sww, Sv, Swv; uint64_t sel;
+			int r0 = mx, r1 = mn;
+			int sse = bc4_eval(v, r0, r1, sel, &Sw, &Sww, &Sv, &Swv, src_stride);
+
+			// Candidate B (HQ only): one 1-D least-squares endpoint refit (value ~= a + (rank/7)*b), adopted only if
+			// it lowers SSE -- monotonic, can never regress below bbox/encode_bc4. Always 8-value (require nr0 > nr1).
+			if (do_ls)
+			{
+				const int detI = 16 * Sww - Sw * Sw;
+				if (detI != 0)
+				{
+					const float b = 7.0f * (float)(16 * Swv - Sw * Sv) / (float)detI; // slope = red0 - red1
+					const float bsw7 = b * (float)Sw * (1.0f / 7.0f);
+					const float a = ((float)Sv - bsw7) / 16.0f;                        // intercept = red1
+					const int nr1 = basisu::clamp<int>((int)rintf(a), 0, 255);
+					const int nr0 = basisu::clamp<int>((int)rintf(a + b), 0, 255);
+					if (nr0 > nr1)
+					{
+						uint64_t selB;
+						const int sseB = bc4_eval(v, nr0, nr1, selB, nullptr, nullptr, nullptr, nullptr, src_stride);
+						if (sseB < sse) { r0 = nr0; r1 = nr1; sel = selB; sse = sseB; }
+					}
+				}
+			}
+
+			out[0] = (uint8_t)r0; out[1] = (uint8_t)r1;
+			for (int i = 0; i < 6; i++) out[2 + i] = (uint8_t)(sel >> (i * 8));
+		}
+
+		void encode_bc4_scalar(void* pBlocks, const uint8_t* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride, bool high_quality, uint32_t src_stride)
+		{
+			uint8_t* pOut = (uint8_t*)pBlocks;
+			const uint32_t out_stride = 8 * block_stride;
+			const uint32_t in_block = 16 * src_stride; // bytes between consecutive blocks in the source
+			for (uint32_t b = 0; b < num_blocks; b++)
+				encode_bc4_block(pSrc_pixels + (size_t)b * in_block, pOut + b * out_stride, high_quality, src_stride);
+		}
+
+		void encode_bc4_spmd(void* pBlocks, const uint8_t* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride, bool high_quality, uint32_t src_stride)
+		{
+			uint8_t* pOut = (uint8_t*)pBlocks;
+			(void)pOut;
+			const uint32_t out_stride = 8 * block_stride;
+			(void)out_stride;
+			const uint32_t in_block = 16 * src_stride;
+			(void)in_block;
+#if BASISU_SUPPORT_SSE
+			if (g_cpu_supports_sse41)
+			{
+				const uint32_t do_ls = high_quality ? 1 : 0;
+				// Full groups read straight from the (possibly strided) source -- no copy.
+				uint32_t base = 0;
+				for (; base + 4 <= num_blocks; base += 4)
+					encode_bc4_blocks_4_sse41(pSrc_pixels + (size_t)base * in_block, pOut + base * out_stride, out_stride, 4, do_ls, src_stride);
+
+				// Tail (<4): gather the channel into a tightly-packed 64-byte stack buffer (pad with last block), then
+				// encode with src_stride=1. Only the tail copies; the bulk above is zero-copy.
+				const uint32_t n = num_blocks - base; // 0..3
+				if (n)
+				{
+					uint8_t grp[64];
+					for (uint32_t gi = 0; gi < 4; gi++)
+					{
+						const uint8_t* sp = pSrc_pixels + (size_t)(base + ((gi < n) ? gi : (n - 1))) * in_block;
+						for (uint32_t t = 0; t < 16; t++) grp[gi * 16 + t] = sp[t * src_stride];
+					}
+					encode_bc4_blocks_4_sse41(grp, pOut + base * out_stride, out_stride, n, do_ls, 1);
+				}
+				return;
+			}
+#endif
+			encode_bc4_scalar(pBlocks, pSrc_pixels, num_blocks, block_stride, high_quality, src_stride); // scalar fallback
+		}
+
+		// ================================ High-level RGBA format helpers ================================
+		// NO allocations, NO channel-extraction copies: the BC4 path reads the requested channel straight out of the
+		// RGBA pixels via src_stride=4 (the channel pointer is &pPixels->r/g/b/a). encode_bc4_* handles the <4 tail
+		// without overrunning the (possibly strided) output.
+
+		void encode_bc1(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd)
+		{
+			if (use_spmd) encode_bc1_spmd(pBlocks, pPixels, num_blocks, 1);
+			else encode_bc1_scalar(pBlocks, pPixels, num_blocks, 1);
+		}
+
+		void encode_bc4(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd, bool high_quality)
+		{
+			const uint8_t* pR = &pPixels[0].r; // R channel, stride 4, contiguous output
+			if (use_spmd) encode_bc4_spmd(pBlocks, pR, num_blocks, 1, high_quality, 4);
+			else encode_bc4_scalar(pBlocks, pR, num_blocks, 1, high_quality, 4);
+		}
+
+		void encode_bc5(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd, bool high_quality)
+		{
+			uint8_t* pOut = (uint8_t*)pBlocks;  // [0..7] = BC4 of R, [8..15] = BC4 of G  (each block_stride 2)
+			const uint8_t* pR = &pPixels[0].r, * pG = &pPixels[0].g;
+			if (use_spmd) { encode_bc4_spmd(pOut, pR, num_blocks, 2, high_quality, 4); encode_bc4_spmd(pOut + 8, pG, num_blocks, 2, high_quality, 4); }
+			else { encode_bc4_scalar(pOut, pR, num_blocks, 2, high_quality, 4); encode_bc4_scalar(pOut + 8, pG, num_blocks, 2, high_quality, 4); }
+		}
+
+		void encode_bc3(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd, bool high_quality)
+		{
+			uint8_t* pOut = (uint8_t*)pBlocks;  // [0..7] = BC4 alpha block, [8..15] = BC1 color block
+			const uint8_t* pA = &pPixels[0].a;  // A channel, stride 4
+			if (use_spmd) { encode_bc4_spmd(pOut, pA, num_blocks, 2, high_quality, 4); encode_bc1_spmd(pOut + 8, pPixels, num_blocks, 2); }
+			else { encode_bc4_scalar(pOut, pA, num_blocks, 2, high_quality, 4); encode_bc1_scalar(pOut + 8, pPixels, num_blocks, 2); }
+		}
+
+		void encode_bc2(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd)
+		{
+			uint8_t* pOut = (uint8_t*)pBlocks; // [0..7] = explicit 4-bit alpha (scalar), [8..15] = BC1 color block
+			for (uint32_t b = 0; b < num_blocks; b++)
+			{
+				uint8_t* o = pOut + b * 16;
+				const color_rgba* p = pPixels + b * 16;
+				for (uint32_t y = 0; y < 4; y++)
+				{
+					uint32_t row = 0; // one little-endian 16-bit word per row; texel x in nibble x
+					for (uint32_t x = 0; x < 4; x++) { const int a8 = p[y * 4 + x].a; const int a4 = (a8 * 2 + 17) / 34; row |= (uint32_t)a4 << (x * 4); } // round(a8/17)
+					o[y * 2] = (uint8_t)row; o[y * 2 + 1] = (uint8_t)(row >> 8);
+				}
+			}
+			if (use_spmd) encode_bc1_spmd(pOut + 8, pPixels, num_blocks, 2);
+			else encode_bc1_scalar(pOut + 8, pPixels, num_blocks, 2);
+		}
+
+	} // namespace bc_spmd
+} // namespace basisu
diff --git a/encoder/basisu_bc15_spmd.h b/encoder/basisu_bc15_spmd.h
new file mode 100644
index 0000000..4507610
--- /dev/null
+++ b/encoder/basisu_bc15_spmd.h
@@ -0,0 +1,73 @@
+// basisu_bc15_spmd.h
+// Standalone opaque 4-color BC1 (DXT1) encoder for the Basis Universal encoder library.
+//
+// Two entry points share the SAME algorithm (omatch-solid fast path -> PCA endpoint seed -> 2x integer-threshold
+// least-squares -> low-variance avg-solid 2nd seed (keep-better) -> integer-threshold selectors):
+//   - encode_bc1_scalar : portable scalar reference ("oracle"), no SIMD.
+//   - encode_bc1_spmd   : SSE4.1 cppspmd kernel, 4 blocks per vector; falls back to the scalar path when SSE4.1
+//                         is unavailable (e.g. WASM without SIMD).
+// Benchmarked (vs basist::encode_bc1): ~3.4-4.7x faster on photos, ~+0.06 dB average, and it beats encode_bc1
+// on low-dynamic-range / gradient blocks. Opaque 4-color only (no 3-color / punchthrough).
+#pragma once
+
+#include "basisu_enc.h" // basisu::color_rgba
+
+namespace basisu
+{
+	namespace bc_spmd
+	{
+		// Build the solid-color omatch tables. Call once before encoding (idempotent). Not safe to call
+		// concurrently with itself or the encoders on first use -- call it once at startup (e.g. right after
+		// basisu_encoder_init(), which also performs the CPU-feature detection encode_bc1_spmd dispatches on).
+		void init();
+
+		// Encode num_blocks opaque BC1 blocks.
+		//   pSrc_pixels  : num_blocks * 16 color_rgba, block-contiguous; texel index within a block = y*4 + x.
+		//   pBlocks      : output; BC1 block b is written at (uint8_t*)pBlocks + b * 8 * block_stride.
+		//   num_blocks   : any count; counts not divisible by 4 are handled internally (no caller padding needed).
+		//   block_stride : output spacing in units of 8-byte BC1 blocks. 1 = contiguous BC1. 2 = write into every
+		//                  other 8-byte slot, e.g. the color half of 16-byte BC3/BC5 blocks (pass pBlocks already
+		//                  offset to that half). Input layout is unaffected.
+		// The caller is responsible for extracting the 16 pixels per block from its source image.
+		void encode_bc1_scalar(void* pBlocks, const color_rgba* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride = 1);
+		void encode_bc1_spmd(void* pBlocks, const color_rgba* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride = 1);
+
+		// Encode num_blocks single-channel BC4 (RGTC1) blocks. Clone of basist::encode_bc4's algorithm: raw bbox
+		// min/max endpoints + exact nearest-of-8 integer-threshold selector, ALWAYS 8-value mode (red0 > red1) --
+		// including a forced-8-value solid-block path so we NEVER emit a 6-value block (safe for BC3/BC5). Quality
+		// matches encode_bc4 (bit-exact on non-solid blocks; identical decode on solid). (Least-squares endpoint
+		// refinement may be layered on later.)
+		//   pSrc_pixels  : num_blocks * 16 uint8_t, block-contiguous; texel index within a block = y*4 + x. The
+		//                  caller extracts the 16 single-channel values per block itself (e.g. one channel of RGBA).
+		//   pBlocks      : output; BC4 block b is written at (uint8_t*)pBlocks + b * 8 * block_stride.
+		//   num_blocks   : any count; the <4 tail is handled internally.
+		//   block_stride : output spacing in units of 8-byte BC4 blocks. 1 = contiguous BC4. 2 = write into every
+		//                  other 8-byte slot, e.g. the alpha half of a 16-byte BC3 block or one half of a BC5 pair.
+		//   high_quality : false (default) = fast bbox encoder, matches encode_bc4 quality, ~1.4x faster. true = add a
+		//                  1-D least-squares endpoint refit (keep-best, monotonic): ~+0.8 dB on photos but ~0.6x speed.
+		//   src_stride   : BYTE stride between consecutive texel values in pSrc_pixels. 1 (default) = tightly packed
+		//                  single channel. 4 = one channel of color_rgba (point pSrc_pixels at the desired channel
+		//                  byte, e.g. &rgba[0].g) -- lets callers encode a channel in place with no extraction copy.
+		void encode_bc4_scalar(void* pBlocks, const uint8_t* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride = 1, bool high_quality = false, uint32_t src_stride = 1);
+		void encode_bc4_spmd(void* pBlocks, const uint8_t* pSrc_pixels, uint32_t num_blocks, uint32_t block_stride = 1, bool high_quality = false, uint32_t src_stride = 1);
+
+		// ---------------- High-level format helpers (RGBA in -> complete GPU blocks out) ----------------
+		// Convenience wrappers over the low-level encoders. Each takes pPixels = num_blocks * 16 color_rgba,
+		// block-contiguous (texel index = y*4 + x), and writes complete blocks. num_blocks may be ANY count >= 1
+		// (not necessarily a multiple of 4); the <4 tail is handled internally with NO output overrun.
+		//   use_spmd     : true = SSE4.1 SPMD path (auto-falls back to scalar when SSE4.1 is unavailable); false = scalar.
+		//   high_quality : enables the BC4 channel least-squares refit for the alpha/red/green block(s) (BC3/BC4/BC5).
+		// Output block sizes / layout (matches the transcoder's bcu unpackers):
+		//   BC1: 8 bytes/block  -- RGB, opaque 4-color.
+		//   BC2: 16 bytes/block -- explicit 4-bit alpha [0..7] (scalar) + BC1 color [8..15].
+		//   BC3: 16 bytes/block -- BC4 alpha block [0..7] + BC1 color [8..15].
+		//   BC4: 8 bytes/block  -- single channel = R.
+		//   BC5: 16 bytes/block -- BC4 of R [0..7] + BC4 of G [8..15].
+		void encode_bc1(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd = true);
+		void encode_bc2(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd = true);
+		void encode_bc3(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd = true, bool high_quality = false);
+		void encode_bc4(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd = true, bool high_quality = false);
+		void encode_bc5(void* pBlocks, const color_rgba* pPixels, uint32_t num_blocks, bool use_spmd = true, bool high_quality = false);
+
+	} // namespace bc_spmd
+} // namespace basisu
diff --git a/encoder/basisu_bc15_spmd_kernels.inl b/encoder/basisu_bc15_spmd_kernels.inl
new file mode 100644
index 0000000..e6f80d2
--- /dev/null
+++ b/encoder/basisu_bc15_spmd_kernels.inl
@@ -0,0 +1,428 @@
+// basisu_bc15_spmd_kernels.inl -- Do NOT directly include.
+//
+// SSE4.1 cppspmd kernel for the standalone BC1 encoder. Included by basisu_bc15_spmd_sse.cpp from file scope after
+// cppspmd_sse.h + "using namespace CPPSPMD;". LANES = BLOCKS: 4 independent BC1 blocks per vector, one per lane;
+// each lane runs the same scalar-looking program on its block's 16 texels. Mirrors encoder/basisu_kernels_imp.h.
+
+namespace bc_spmd_kern
+{
+	// Opaque 4-color BC1 encode of 4 blocks at once (one block per lane). pBlocks = 4 blocks x 16 texels,
+	// block-contiguous; pOut = 4*8 bytes. om5*/om6* = solid-color omatch tables (int[256]); ls_rounds, avgvar tune.
+	struct encode_bc1_4color_blocks : spmd_kernel
+	{
+		inline vint div3(const vint& x) { return VUINT_SHIFT_RIGHT(x * vint(43691), 17); } // exact floor(x/3), x small
+		inline vint expand5(const vint& c) { return VINT_SHIFT_LEFT(c, 3) | VUINT_SHIFT_RIGHT(c, 2); }
+		inline vint expand6(const vint& c) { return VINT_SHIFT_LEFT(c, 2) | VUINT_SHIFT_RIGHT(c, 4); }
+
+		// Round per-lane float RGB (0..255) STRAIGHT into 5/6/5-bit space (single round, like encode_bc1's
+		// *31/255+0.5) -- avoids the double-rounding (float->8bit->565) that biases endpoints inward.
+		inline vint f_to_565(const vfloat& r, const vfloat& g, const vfloat& b)
+		{
+			const vint r5 = vint(round_nearest(min(max(r, vfloat(0.0f)), vfloat(255.0f)) * vfloat(31.0f / 255.0f)));
+			const vint g6 = vint(round_nearest(min(max(g, vfloat(0.0f)), vfloat(255.0f)) * vfloat(63.0f / 255.0f)));
+			const vint b5 = vint(round_nearest(min(max(b, vfloat(0.0f)), vfloat(255.0f)) * vfloat(31.0f / 255.0f)));
+			return VINT_SHIFT_LEFT(r5, 11) | VINT_SHIFT_LEFT(g6, 5) | b5;
+		}
+
+		// Load texel i of all 4 blocks into per-channel vints (lane j = block j's texel i).
+		inline void load_texel(const int32_t* p, int i, vint& r, vint& g, vint& b)
+		{
+			vint rgba; rgba.m_value = _mm_setr_epi32(p[i], p[16 + i], p[32 + i], p[48 + i]);
+			r = rgba & vint(0xFF);
+			g = VINT_SHIFT_RIGHT(rgba, 8) & vint(0xFF);
+			b = VINT_SHIFT_RIGHT(rgba, 16) & vint(0xFF);
+			// Opaque 4-color encoder: alpha is ignored entirely.
+		}
+
+		// Integer-threshold selector (encode_bc1's bc1_find_sels scheme): 1 dot + 3 compares/texel. The asymmetric
+		// (d<=t0) biases extreme texels onto the OUTER selectors so the LS step reaches wider endpoints.
+		// Palette order is [pal0,pal2,pal3,pal1] along the c0->c1 axis; lut4={1,3,2,0} via the ternary cascade.
+		struct thresholds { vint t0, t1, t2, ar2, ag2, ab2; };
+		inline thresholds make_thresholds(
+			const vint& pr0, const vint& pg0, const vint& pb0, const vint& pr1, const vint& pg1, const vint& pb1,
+			const vint& pr2, const vint& pg2, const vint& pb2, const vint& pr3, const vint& pg3, const vint& pb3)
+		{
+			const vint ar = pr1 - pr0, ag = pg1 - pg0, ab = pb1 - pb0;
+			const vint dp0 = pr0 * ar + pg0 * ag + pb0 * ab;
+			const vint dp2 = pr2 * ar + pg2 * ag + pb2 * ab;
+			const vint dp3 = pr3 * ar + pg3 * ag + pb3 * ab;
+			const vint dp1 = pr1 * ar + pg1 * ag + pb1 * ab;
+			thresholds T;
+			T.t0 = dp0 + dp2; T.t1 = dp2 + dp3; T.t2 = dp3 + dp1;
+			T.ar2 = ar + ar; T.ag2 = ag + ag; T.ab2 = ab + ab;
+			return T;
+		}
+		inline vint thresh_sel(const thresholds& T, const vint& r, const vint& g, const vint& b)
+		{
+			const vint d = r * T.ar2 + g * T.ag2 + b * T.ab2;
+			return spmd_ternaryi(d <= T.t0, 0, spmd_ternaryi(d < T.t1, 2, spmd_ternaryi(d < T.t2, 3, 1)));
+		}
+
+		// One least-squares round (per lane): assign integer-threshold selectors for the current endpoints, refit
+		// the endpoint line via the 2x2 normal equations (Cramer), adopt only where well-conditioned (det).
+		inline void ls_round(const vint* R, const vint* G, const vint* B, vint& c0, vint& c1)
+		{
+			const vint pr0 = expand5(VINT_SHIFT_RIGHT(c0, 11) & vint(31)), pg0 = expand6(VINT_SHIFT_RIGHT(c0, 5) & vint(63)), pb0 = expand5(c0 & vint(31));
+			const vint pr1 = expand5(VINT_SHIFT_RIGHT(c1, 11) & vint(31)), pg1 = expand6(VINT_SHIFT_RIGHT(c1, 5) & vint(63)), pb1 = expand5(c1 & vint(31));
+			const vint pr2 = div3(vint(2) * pr0 + pr1), pg2 = div3(vint(2) * pg0 + pg1), pb2 = div3(vint(2) * pb0 + pb1);
+			const vint pr3 = div3(pr0 + vint(2) * pr1), pg3 = div3(pg0 + vint(2) * pg1), pb3 = div3(pb0 + vint(2) * pb1);
+
+			const thresholds T = make_thresholds(pr0, pg0, pb0, pr1, pg1, pb1, pr2, pg2, pb2, pr3, pg3, pb3);
+			vfloat m00(0.0f), m01(0.0f), m11(0.0f);
+			vfloat a0r(0.0f), a0g(0.0f), a0b(0.0f), a1r(0.0f), a1g(0.0f), a1b(0.0f);
+			for (int i = 0; i < 16; i++)
+			{
+				const vint& r = R[i]; const vint& g = G[i]; const vint& b = B[i];
+				const vint sel = thresh_sel(T, r, g, b);
+				// fraction toward c1 (the LS weight b): {0, 1, 1/3, 2/3}[sel]; a = 1 - b.
+				const vfloat tb = spmd_ternaryf(sel == vint(0), vfloat(0.0f), spmd_ternaryf(sel == vint(1), vfloat(1.0f), spmd_ternaryf(sel == vint(2), vfloat(1.0f / 3.0f), vfloat(2.0f / 3.0f))));
+				const vfloat ta = vfloat(1.0f) - tb;
+				m00 = m00 + ta * ta; m01 = m01 + ta * tb; m11 = m11 + tb * tb;
+				const vfloat cr = (vfloat)r, cg = (vfloat)g, cb = (vfloat)b;
+				a0r = a0r + ta * cr; a0g = a0g + ta * cg; a0b = a0b + ta * cb;
+				a1r = a1r + tb * cr; a1g = a1g + tb * cg; a1b = a1b + tb * cb;
+			}
+			const vfloat det = m00 * m11 - m01 * m01;
+			const vfloat inv = vfloat(1.0f) / max(det, vfloat(1e-6f)); // clamp not bias: exact 1/det for real blocks, never /0
+			const vfloat e0r = (a0r * m11 - a1r * m01) * inv, e0g = (a0g * m11 - a1g * m01) * inv, e0b = (a0b * m11 - a1b * m01) * inv;
+			const vfloat e1r = (a1r * m00 - a0r * m01) * inv, e1g = (a1g * m00 - a0g * m01) * inv, e1b = (a1b * m00 - a0b * m01) * inv;
+			vint nc0 = f_to_565(e0r, e0g, e0b), nc1 = f_to_565(e1r, e1g, e1b);
+			{ const vint hi = max(nc0, nc1), lo = min(nc0, nc1); nc0 = hi; nc1 = lo; }
+			const vbool ok = det > vfloat(1e-6f); // only adopt a well-conditioned refit; degenerate lane keeps current
+			c0 = spmd_ternaryi(ok, nc0, c0);
+			c1 = spmd_ternaryi(ok, nc1, c1);
+		}
+
+		// Block RGB SSE for an endpoint pair (integer-threshold selectors + squared distance). For keep-better.
+		inline vint eval_error(const vint* R, const vint* G, const vint* B, const vint& c0, const vint& c1)
+		{
+			const vint pr0 = expand5(VINT_SHIFT_RIGHT(c0, 11) & vint(31)), pg0 = expand6(VINT_SHIFT_RIGHT(c0, 5) & vint(63)), pb0 = expand5(c0 & vint(31));
+			const vint pr1 = expand5(VINT_SHIFT_RIGHT(c1, 11) & vint(31)), pg1 = expand6(VINT_SHIFT_RIGHT(c1, 5) & vint(63)), pb1 = expand5(c1 & vint(31));
+			const vint pr2 = div3(vint(2) * pr0 + pr1), pg2 = div3(vint(2) * pg0 + pg1), pb2 = div3(vint(2) * pb0 + pb1);
+			const vint pr3 = div3(pr0 + vint(2) * pr1), pg3 = div3(pg0 + vint(2) * pg1), pb3 = div3(pb0 + vint(2) * pb1);
+			const thresholds T = make_thresholds(pr0, pg0, pb0, pr1, pg1, pb1, pr2, pg2, pb2, pr3, pg3, pb3);
+			vint err(0);
+			for (int i = 0; i < 16; i++)
+			{
+				const vint& r = R[i]; const vint& g = G[i]; const vint& b = B[i];
+				const vint sel = thresh_sel(T, r, g, b);
+				const vint psr = spmd_ternaryi(sel == vint(0), pr0, spmd_ternaryi(sel == vint(1), pr1, spmd_ternaryi(sel == vint(2), pr2, pr3)));
+				const vint psg = spmd_ternaryi(sel == vint(0), pg0, spmd_ternaryi(sel == vint(1), pg1, spmd_ternaryi(sel == vint(2), pg2, pg3)));
+				const vint psb = spmd_ternaryi(sel == vint(0), pb0, spmd_ternaryi(sel == vint(1), pb1, spmd_ternaryi(sel == vint(2), pb2, pb3)));
+				const vint dr = r - psr, dg = g - psg, db = b - psb;
+				err = err + dr * dr + dg * dg + db * db;
+			}
+			return err;
+		}
+
+		void _call(const basisu::color_rgba* pBlocks, uint8_t* pOut,
+			int* om5lo, int* om5hi, int* om6lo, int* om6hi, int ls_rounds, int avgvar,
+			uint32_t out_stride, uint32_t num_write)
+		{
+			const int32_t* p = (const int32_t*)pBlocks;
+
+			// Stage source pixels to SoA ONCE (lane = block): R[i]/G[i]/B[i] hold texel i of all 4 blocks.
+			vint R[16], G[16], B[16];
+			for (int i = 0; i < 16; i++) load_texel(p, i, R[i], G[i], B[i]);
+
+			// Pass 1: per-lane bounding box + channel sums + integer moments for covariance.
+			vint mnr(255), mng(255), mnb(255), mxr(0), mxg(0), mxb(0), sum_r(0), sum_g(0), sum_b(0);
+			vint sum_rr(0), sum_gg(0), sum_bb(0), sum_rg(0), sum_rb(0), sum_gb(0);
+			for (int i = 0; i < 16; i++)
+			{
+				const vint& r = R[i]; const vint& g = G[i]; const vint& b = B[i];
+				mnr = min(mnr, r); mng = min(mng, g); mnb = min(mnb, b);
+				mxr = max(mxr, r); mxg = max(mxg, g); mxb = max(mxb, b);
+				sum_r = sum_r + r; sum_g = sum_g + g; sum_b = sum_b + b;
+				sum_rr = sum_rr + r * r; sum_gg = sum_gg + g * g; sum_bb = sum_bb + b * b;
+				sum_rg = sum_rg + r * g; sum_rb = sum_rb + r * b; sum_gb = sum_gb + g * b;
+			}
+
+			vint color0(0), color1(0), sel_word(0);
+
+			// SOLID lanes -> omatch endpoints; all-solid groups skip the non-solid path via SPMD_SELSE's any() check.
+			const vbool solid = (mnr == mxr) && (mng == mxg) && (mnb == mxb);
+			SPMD_SIF(solid)
+			{
+				// Clone of basist::encode_bc1_solid_block: omatch endpoints from the (full-cross-product) single-color
+				// tables, then the swap + degenerate handling that GUARANTEES 4-color (color0 > color1), never 3-color.
+				const vint ar = VUINT_SHIFT_RIGHT(sum_r + vint(8), 4), ag = VUINT_SHIFT_RIGHT(sum_g + vint(8), 4), ab = VUINT_SHIFT_RIGHT(sum_b + vint(8), 4);
+				vint max16 = VINT_SHIFT_LEFT(load_all(ar[om5lo]), 11) | VINT_SHIFT_LEFT(load_all(ag[om6lo]), 5) | load_all(ab[om5lo]); // 2x-weighted (m_hi)
+				vint min16 = VINT_SHIFT_LEFT(load_all(ar[om5hi]), 11) | VINT_SHIFT_LEFT(load_all(ag[om6hi]), 5) | load_all(ab[om5hi]); // 1x (m_lo)
+				vint mask = vint(0xAA);
+				SPMD_SIF(min16 == max16) // force max16 > min16 (stay 4-color, never 3-color)
+				{
+					store(mask, vint(0));
+					SPMD_SIF(min16 > vint(0)) { store(min16, min16 - vint(1)); }
+					SPMD_SELSE(min16 > vint(0)) { store(max16, vint(1)); store(min16, vint(0)); store(mask, vint(0x55)); }
+					SPMD_SENDIF
+				}
+				SPMD_SENDIF
+				SPMD_SIF(max16 < min16) // ensure color0 > color1; selector 2<->3 flips with the swap
+				{
+					const vint a = max16, b = min16;
+					store(max16, b); store(min16, a);
+					store(mask, mask ^ vint(0x55));
+				}
+				SPMD_SENDIF
+				store(color0, max16); store(color1, min16);
+				store(sel_word, mask | VINT_SHIFT_LEFT(mask, 8) | VINT_SHIFT_LEFT(mask, 16) | VINT_SHIFT_LEFT(mask, 24));
+			}
+			SPMD_SELSE(solid)
+			{
+				// PCA seed (METHOD 2, on-axis, sqrt-free): integer-moment covariance -> no-renorm power iteration
+				// (bbox-diagonal seed, can't overflow at 4 iters) -> scale 1024/max -> endpoints = mean +- extent.
+				const vfloat cxx = (vfloat)(vint(16) * sum_rr - sum_r * sum_r), cxy = (vfloat)(vint(16) * sum_rg - sum_r * sum_g), cxz = (vfloat)(vint(16) * sum_rb - sum_r * sum_b);
+				const vfloat cyy = (vfloat)(vint(16) * sum_gg - sum_g * sum_g), cyz = (vfloat)(vint(16) * sum_gb - sum_g * sum_b), czz = (vfloat)(vint(16) * sum_bb - sum_b * sum_b);
+				vfloat ax = (vfloat)(mxr - mnr), ay = (vfloat)(mxg - mng), az = (vfloat)(mxb - mnb);
+				// Overflow-safe: covariance entries <= 4.16e6, 4 iters -> |v| <= ~6.2e30 << float max. Do not exceed 5 iters.
+				for (int it = 0; it < 4; it++)
+				{
+					const vfloat nr = ax * cxx + ay * cxy + az * cxz;
+					const vfloat ng = ax * cxy + ay * cyy + az * cyz;
+					const vfloat nb = ax * cxz + ay * cyz + az * czz;
+					ax = nr; ay = ng; az = nb;
+				}
+				const vfloat kk = max(max(abs(ax), abs(ay)), abs(az));
+				const vfloat mm = vfloat(1024.0f) / max(kk, vfloat(1e-3f)); // clamp guards the near-degenerate lane
+				ax = ax * mm; ay = ay * mm; az = az * mm;
+				const vfloat inv_len2 = vfloat(1.0f) / (ax * ax + ay * ay + az * az + vfloat(0.0000125f)); // tiny bias: never /0
+				const vfloat meanx = (vfloat)sum_r * vfloat(1.0f / 16.0f), meany = (vfloat)sum_g * vfloat(1.0f / 16.0f), meanz = (vfloat)sum_b * vfloat(1.0f / 16.0f);
+				vfloat minp(1e30f), maxp(-1e30f);
+				for (int i = 0; i < 16; i++)
+				{
+					const vfloat pr = ((vfloat)R[i] - meanx) * ax + ((vfloat)G[i] - meany) * ay + ((vfloat)B[i] - meanz) * az;
+					minp = min(minp, pr); maxp = max(maxp, pr);
+				}
+				const vfloat tlo = minp * inv_len2, thi = maxp * inv_len2;
+				vint c0 = f_to_565(meanx + tlo * ax, meany + tlo * ay, meanz + tlo * az);
+				vint c1 = f_to_565(meanx + thi * ax, meany + thi * ay, meanz + thi * az);
+				{ const vint hi = max(c0, c1), lo = min(c0, c1); c0 = hi; c1 = lo; }
+				SPMD_SIF(c0 == c1) // 4-color needs color0 > color1; nudge the degenerate lanes
+				{
+					SPMD_SIF(c1 != vint(0)) { store(c1, c1 - vint(1)); }
+					SPMD_SELSE(c1 != vint(0)) { store(c0, c0 + vint(1)); }
+					SPMD_SENDIF
+				}
+				SPMD_SENDIF
+
+				// Least-squares refinement (the big quality lever).
+				for (int r = 0; r < ls_rounds; r++) ls_round(R, G, B, c0, c1);
+				SPMD_SIF(c0 == c1)
+				{
+					SPMD_SIF(c1 != vint(0)) { store(c1, c1 - vint(1)); }
+					SPMD_SELSE(c1 != vint(0)) { store(c0, c0 + vint(1)); }
+					SPMD_SENDIF
+				}
+				SPMD_SENDIF
+
+				// Low-variance blocks (ramps) collapse the PCA seed: try an omatch-solid-of-average 2nd seed (which
+				// survives the collapse), 1 LS round, and keep whichever has lower error. Gated to where it's needed.
+				const vint var_proxy = (mxr - mnr) + (mxg - mng) + (mxb - mnb);
+				SPMD_SIF(var_proxy < vint(avgvar))
+				{
+					const vint ar = VUINT_SHIFT_RIGHT(sum_r + vint(8), 4), ag = VUINT_SHIFT_RIGHT(sum_g + vint(8), 4), ab = VUINT_SHIFT_RIGHT(sum_b + vint(8), 4);
+					vint c0b = VINT_SHIFT_LEFT(load_all(ar[om5lo]), 11) | VINT_SHIFT_LEFT(load_all(ag[om6lo]), 5) | load_all(ab[om5lo]);
+					vint c1b = VINT_SHIFT_LEFT(load_all(ar[om5hi]), 11) | VINT_SHIFT_LEFT(load_all(ag[om6hi]), 5) | load_all(ab[om5hi]);
+					{ const vint hi = max(c0b, c1b), lo = min(c0b, c1b); c0b = hi; c1b = lo; }
+					SPMD_SIF(c0b == c1b)
+					{
+						SPMD_SIF(c1b != vint(0)) { store(c1b, c1b - vint(1)); }
+						SPMD_SELSE(c1b != vint(0)) { store(c0b, c0b + vint(1)); }
+						SPMD_SENDIF
+					}
+					SPMD_SENDIF
+					ls_round(R, G, B, c0b, c1b);
+					SPMD_SIF(c0b == c1b)
+					{
+						SPMD_SIF(c1b != vint(0)) { store(c1b, c1b - vint(1)); }
+						SPMD_SELSE(c1b != vint(0)) { store(c0b, c0b + vint(1)); }
+						SPMD_SENDIF
+					}
+					SPMD_SENDIF
+					const vbool use_avg = eval_error(R, G, B, c0b, c1b) < eval_error(R, G, B, c0, c1);
+					store(c0, spmd_ternaryi(use_avg, c0b, c0));
+					store(c1, spmd_ternaryi(use_avg, c1b, c1));
+				}
+				SPMD_SENDIF
+
+				// Final palette + integer-threshold selector pass.
+				const vint pr0 = expand5(VINT_SHIFT_RIGHT(c0, 11) & vint(31)), pg0 = expand6(VINT_SHIFT_RIGHT(c0, 5) & vint(63)), pb0 = expand5(c0 & vint(31));
+				const vint pr1 = expand5(VINT_SHIFT_RIGHT(c1, 11) & vint(31)), pg1 = expand6(VINT_SHIFT_RIGHT(c1, 5) & vint(63)), pb1 = expand5(c1 & vint(31));
+				const vint pr2 = div3(vint(2) * pr0 + pr1), pg2 = div3(vint(2) * pg0 + pg1), pb2 = div3(vint(2) * pb0 + pb1);
+				const vint pr3 = div3(pr0 + vint(2) * pr1), pg3 = div3(pg0 + vint(2) * pg1), pb3 = div3(pb0 + vint(2) * pb1);
+				const thresholds T = make_thresholds(pr0, pg0, pb0, pr1, pg1, pb1, pr2, pg2, pb2, pr3, pg3, pb3);
+				vint sw(0);
+				for (int i = 0; i < 16; i++)
+				{
+					const vint bestsel = thresh_sel(T, R[i], G[i], B[i]);
+					sw = sw | (bestsel << (i * 2));
+				}
+				store(color0, c0); store(color1, c1); store(sel_word, sw);
+			}
+			SPMD_SENDIF
+
+			// Emit one BC1 block per lane directly to the (possibly strided) output -- no temp buffer. num_write < 4
+			// for the final partial group so the padded lanes aren't written; out_stride lets BC3/BC5 interleave.
+			CPPSPMD_DECL(int, c0a[4]); CPPSPMD_DECL(int, c1a[4]); CPPSPMD_DECL(int, swa[4]);
+			storeu_linear_all(c0a, color0);
+			storeu_linear_all(c1a, color1);
+			storeu_linear_all(swa, sel_word);
+			for (uint32_t j = 0; j < num_write; j++)
+			{
+				uint8_t* o = pOut + j * out_stride;
+				const uint32_t c0 = (uint32_t)c0a[j], c1 = (uint32_t)c1a[j], sw = (uint32_t)swa[j];
+				o[0] = (uint8_t)c0; o[1] = (uint8_t)(c0 >> 8);
+				o[2] = (uint8_t)c1; o[3] = (uint8_t)(c1 >> 8);
+				o[4] = (uint8_t)sw; o[5] = (uint8_t)(sw >> 8); o[6] = (uint8_t)(sw >> 16); o[7] = (uint8_t)(sw >> 24);
+			}
+		}
+	};
+} // namespace bc_spmd_kern
+
+namespace bc_spmd_kern
+{
+	// BC4 (RGTC1) -- one single-channel block per lane, 4 blocks/call. Clone of basist::encode_bc4: raw bbox
+	// min/max, exact nearest-of-8 integer-threshold selector, ALWAYS 8-value mode (solid forced to red0>red1).
+	// All-integer, GATHER-FREE: texels via manual load; rank->index by arithmetic (no s_tran gather); the 48-bit
+	// selector packed via uniform shifts into two vints (texels 0-9 in lo, 10-15 in hi), combined with <<30 in scalar.
+	struct encode_bc4_blocks : spmd_kernel
+	{
+		// Nearest-of-8 selectors + decoded SSE for an 8-value endpoint pair (r0 > r1). Packs texels 0-9 into slo,
+		// 10-15 into shi. Optionally accumulates the 1-D least-squares moments (rank = interpolation weight 0..7).
+		inline vint bc4_eval(const vint* A, const vint& r0, const vint& r1, vint& slo, vint& shi, vint* pSw, vint* pSww, vint* pSv, vint* pSwv)
+		{
+			const vint delta = r0 - r1;
+			const vint bias = vint(4) - r1 * vint(14);
+			const vint t0 = delta * vint(13), t1 = delta * vint(11), t2 = delta * vint(9), t3 = delta * vint(7), t4 = delta * vint(5), t5 = delta * vint(3), t6 = delta;
+			vint sse(0), Sw(0), Sww(0), Sv(0), Swv(0);
+			slo = vint(0); shi = vint(0);
+			for (int i = 0; i < 16; i++)
+			{
+				const vint val = A[i];
+				const vint sv = val * vint(14) + bias;
+				const vint rank = spmd_ternaryi(sv >= t0, 1, 0) + spmd_ternaryi(sv >= t1, 1, 0) + spmd_ternaryi(sv >= t2, 1, 0)
+					+ spmd_ternaryi(sv >= t3, 1, 0) + spmd_ternaryi(sv >= t4, 1, 0) + spmd_ternaryi(sv >= t5, 1, 0) + spmd_ternaryi(sv >= t6, 1, 0);
+				const vint idx = spmd_ternaryi(rank == vint(7), vint(0), spmd_ternaryi(rank == vint(0), vint(1), vint(8) - rank));
+				if (i < 10) slo = slo | (idx << (i * 3));
+				else        shi = shi | (idx << ((i - 10) * 3));
+				const vint num = rank * r0 + (vint(7) - rank) * r1;
+				const vint recon = VUINT_SHIFT_RIGHT(num * vint(9363), 16); // == num/7 floor for num<=1785 (matches scalar /7)
+				const vint d = val - recon; sse = sse + d * d;
+				Sw = Sw + rank; Sww = Sww + rank * rank; Sv = Sv + val; Swv = Swv + rank * val;
+			}
+			if (pSw) { *pSw = Sw; *pSww = Sww; *pSv = Sv; *pSwv = Swv; }
+			return sse;
+		}
+
+		void _call(const uint8_t* pBlocks, uint8_t* pOut, uint32_t out_stride, uint32_t num_write, uint32_t do_ls, uint32_t src_stride)
+		{
+			// Stage 16 channel values to SoA: A[i] = texel i of the 4 lane-blocks. Source values are src_stride bytes
+			// apart (1 = packed channel, 4 = one channel of RGBA); blocks are 16*src_stride bytes apart.
+			const uint32_t S = src_stride;
+			vint A[16];
+			for (int i = 0; i < 16; i++)
+				A[i].m_value = _mm_setr_epi32(pBlocks[i * S], pBlocks[(16 + i) * S], pBlocks[(32 + i) * S], pBlocks[(48 + i) * S]);
+
+			vint mn = A[0], mx = A[0];
+			for (int i = 1; i < 16; i++) { mn = min(mn, A[i]); mx = max(mx, A[i]); }
+
+			vint color0(0), color1(0), sel_lo(0), sel_hi(0);
+
+			const vbool solid = (mx == mn);
+			SPMD_SIF(solid)
+			{
+				// Force 8-value solid: value>0 -> (max, max-1, all idx 0); value==0 -> (1, 0, all idx 1).
+				const vbool pos = (mx > vint(0));
+				store(color0, spmd_ternaryi(pos, mx, vint(1)));
+				store(color1, spmd_ternaryi(pos, mx - vint(1), vint(0)));
+				const vint idx = spmd_ternaryi(pos, vint(0), vint(1)); // replicated into every 3-bit field
+				store(sel_lo, idx * vint(0x9249249)); // 10 fields (texels 0-9) all = idx
+				store(sel_hi, idx * vint(0x9249));     // 6 fields  (texels 10-15) all = idx
+			}
+			SPMD_SELSE(solid)
+			{
+				if (do_ls)
+				{
+					// HQ: candidate A = raw bbox (== encode_bc4) + LS moments; candidate B = 1-D LS endpoint refit
+					// (value ~= a + (rank/7)*b). detf guards /0 for degenerate lanes (refit then rejected). Keep-best.
+					vint sloA, shiA, Sw, Sww, Sv, Swv;
+					const vint sseA = bc4_eval(A, mx, mn, sloA, shiA, &Sw, &Sww, &Sv, &Swv);
+					const vint detI = vint(16) * Sww - Sw * Sw;
+					const vfloat detf = (vfloat)spmd_ternaryi(detI == vint(0), vint(1), detI);
+					const vfloat b = vfloat(7.0f) * (vfloat)(vint(16) * Swv - Sw * Sv) / detf; // slope = red0 - red1
+					const vfloat bsw7 = b * (vfloat)Sw * vfloat(1.0f / 7.0f);
+					const vfloat a = ((vfloat)Sv - bsw7) / vfloat(16.0f);                       // intercept = red1
+					vint nr1 = vint(round_nearest(a));     nr1 = min(max(nr1, vint(0)), vint(255));
+					vint nr0 = vint(round_nearest(a + b)); nr0 = min(max(nr0, vint(0)), vint(255));
+					vint sloB, shiB;
+					const vint sseB = bc4_eval(A, nr0, nr1, sloB, shiB, nullptr, nullptr, nullptr, nullptr);
+					const vbool adopt = ((detI != vint(0)) && (nr0 > nr1)) && (sseB < sseA);
+					store(color0, spmd_ternaryi(adopt, nr0, mx));
+					store(color1, spmd_ternaryi(adopt, nr1, mn));
+					store(sel_lo, spmd_ternaryi(adopt, sloB, sloA));
+					store(sel_hi, spmd_ternaryi(adopt, shiB, shiA));
+				}
+				else
+				{
+					// FAST: raw bbox endpoints + nearest-of-8 selector only (== basist::encode_bc4 quality, ~1.4x).
+					store(color0, mx); store(color1, mn);
+					const vint delta = mx - mn;
+					const vint bias = vint(4) - mn * vint(14);
+					const vint t0 = delta * vint(13), t1 = delta * vint(11), t2 = delta * vint(9), t3 = delta * vint(7), t4 = delta * vint(5), t5 = delta * vint(3), t6 = delta;
+					vint slo(0), shi(0);
+					for (int i = 0; i < 16; i++)
+					{
+						const vint sv = A[i] * vint(14) + bias;
+						const vint rank = spmd_ternaryi(sv >= t0, 1, 0) + spmd_ternaryi(sv >= t1, 1, 0) + spmd_ternaryi(sv >= t2, 1, 0)
+							+ spmd_ternaryi(sv >= t3, 1, 0) + spmd_ternaryi(sv >= t4, 1, 0) + spmd_ternaryi(sv >= t5, 1, 0) + spmd_ternaryi(sv >= t6, 1, 0);
+						const vint idx = spmd_ternaryi(rank == vint(7), vint(0), spmd_ternaryi(rank == vint(0), vint(1), vint(8) - rank));
+						if (i < 10) slo = slo | (idx << (i * 3));
+						else        shi = shi | (idx << ((i - 10) * 3));
+					}
+					store(sel_lo, slo); store(sel_hi, shi);
+				}
+			}
+			SPMD_SENDIF
+
+			CPPSPMD_DECL(int, c0a[4]); CPPSPMD_DECL(int, c1a[4]); CPPSPMD_DECL(int, sloa[4]); CPPSPMD_DECL(int, shia[4]);
+			storeu_linear_all(c0a, color0);
+			storeu_linear_all(c1a, color1);
+			storeu_linear_all(sloa, sel_lo);
+			storeu_linear_all(shia, sel_hi);
+			for (uint32_t j = 0; j < num_write; j++)
+			{
+				uint8_t* o = pOut + j * out_stride;
+				o[0] = (uint8_t)c0a[j]; o[1] = (uint8_t)c1a[j];
+				const uint64_t W = (uint64_t)(uint32_t)sloa[j] | ((uint64_t)(uint32_t)shia[j] << 30); // 48-bit selector word
+				o[2] = (uint8_t)W; o[3] = (uint8_t)(W >> 8); o[4] = (uint8_t)(W >> 16);
+				o[5] = (uint8_t)(W >> 24); o[6] = (uint8_t)(W >> 32); o[7] = (uint8_t)(W >> 40);
+			}
+		}
+	};
+} // namespace bc_spmd_kern
+
+namespace basisu
+{
+	namespace bc_spmd
+	{
+		void encode_bc4_blocks_4_sse41(const uint8_t* pBlocks, uint8_t* pOut, uint32_t out_stride, uint32_t num_write, uint32_t do_ls, uint32_t src_stride)
+		{
+			spmd_call<bc_spmd_kern::encode_bc4_blocks>(pBlocks, pOut, out_stride, num_write, do_ls, src_stride);
+		}
+	}
+}
+
+// ISA wrapper -- encode 4 blocks at once. Defined here (inside the SSE4.1 TU), declared in basisu_bc15_spmd.cpp.
+namespace basisu
+{
+	namespace bc_spmd
+	{
+		void encode_bc1_blocks_4_sse41(const color_rgba* pBlocks, uint8_t* pOut,
+			int* om5lo, int* om5hi, int* om6lo, int* om6hi, int ls_rounds, int avgvar,
+			uint32_t out_stride, uint32_t num_write)
+		{
+			spmd_call<bc_spmd_kern::encode_bc1_4color_blocks>(pBlocks, pOut, om5lo, om5hi, om6lo, om6hi, ls_rounds, avgvar, out_stride, num_write);
+		}
+	}
+}
diff --git a/encoder/basisu_bc15_spmd_sse.cpp b/encoder/basisu_bc15_spmd_sse.cpp
new file mode 100644
index 0000000..2caeeff
--- /dev/null
+++ b/encoder/basisu_bc15_spmd_sse.cpp
@@ -0,0 +1,22 @@
+// basisu_bc15_spmd_sse.cpp -- SSE4.1 ISA translation unit for the standalone BC1 encoder.
+//
+// Mirrors basisu_kernels_sse.cpp's pattern: select the ISA, include the cppspmd framework, then pull in the
+// kernel .inl ("do not directly include"). Multiple TUs may include the framework because cppspmd_math.h's
+// print_* helpers are inline.
+
+#include "basisu_enc.h" // basisu::color_rgba + BASISU_SUPPORT_SSE
+
+#if BASISU_SUPPORT_SSE
+
+#define CPPSPMD_SSE2 0
+#ifdef _MSC_VER
+#include <intrin.h>
+#endif
+#include "cppspmd_sse.h"
+#include "cppspmd_type_aliases.h"
+
+using namespace CPPSPMD;
+
+#include "basisu_bc15_spmd_kernels.inl"
+
+#endif // BASISU_SUPPORT_SSE
diff --git a/encoder/basisu_bc7e_scalar.cpp b/encoder/basisu_bc7e_scalar.cpp
new file mode 100644
index 0000000..de312ca
--- /dev/null
+++ b/encoder/basisu_bc7e_scalar.cpp
@@ -0,0 +1,5015 @@
+// bc7e_scalar.cpp - Pure scalar C++17 port of bc7e.ispc (auto-derived, then
+// hand-fixed). De-SIMD'd: the single SPMD foreach over blocks becomes a plain
+// for loop; uniform/varying are stripped (one lane/block, no gang), and the
+// ISPC stdlib surface is provided by ispc_compat.h. Encodes one 4x4 BC7 block
+// per iteration. Logic/math and the public API are preserved verbatim.
+#include "basisu_bc7e_scalar.h"
+#include <cstdint>
+#include <cstring>   // memset, memcpy
+#include <cassert>
+#include <cmath>
+#include <cstdio>
+
+// --- ISPC compatibility shim (formerly ispc_compat.h, inlined here so this .cpp
+// --- plus basisu_bc7e_scalar.h are fully self-contained). Supplies the small ISPC
+// --- stdlib surface the de-SIMD'd code uses, the base integer type aliases, and
+// --- the coherent control-flow keyword macros. Kept in the .cpp (not the public
+// --- header) so these global templates/macros don't leak into the rest of the project.
+
+// ISPC base integer type aliases (the fixed-width *_t names come from <cstdint>).
+typedef int8_t  int8;
+typedef int16_t int16;
+typedef int32_t int32;
+typedef int64_t int64;
+
+// ISPC "coherent" control-flow keywords -> plain C++ control flow (no-op at gang width 1).
+#define cif    if
+#define cfor   for
+#define cwhile while
+
+// Mark an intentionally-unused variable/parameter (silences C4100/C4189).
+#define NOTE_UNUSED(x) (void)(x)
+
+// Range-checked cast for a float that is known to hold a small non-negative integral
+// value (e.g. an 8-bit channel min/max). Asserts the value is in [0,255] in debug, then
+// narrows to uint32_t. Used at the few float->uint sites so the truncation is explicit
+// and guarded rather than an implicit (and portability-warning-triggering) conversion.
+static inline uint32_t float_to_uint8(float v) { assert((v >= 0.0f) && (v <= 255.0f)); return (uint32_t)v; }
+
+// ISPC stdlib scalar equivalents. min/max match minss/maxss semantics (the second
+// operand is returned on an unordered/NaN comparison), and clamp() is defined as
+// min(max(v,lo),hi) -- NOT a direct ternary -- so clamp(NaN,lo,hi) yields lo just as
+// it does under ISPC (a direct ternary would yield NaN, which casts to INT_MIN and
+// becomes an out-of-bounds index).
+template <class T> static inline T min(T a, T b) { return a < b ? a : b; }
+template <class T> static inline T max(T a, T b) { return a > b ? a : b; }
+template <class T> static inline T clamp(T v, T lo, T hi) { return min(max(v, lo), hi); }
+template <class T> static inline T select(bool c, T a, T b) { return c ? a : b; }
+template <class T> static inline bool all(T v) { return (bool)v; }
+template <class T> static inline bool any(T v) { return (bool)v; }
+template <class T> static inline bool none(T v) { return !(bool)v; }
+template <class T> static inline T abs(T v) { return v < 0 ? -v : v; }
+// floor()/sqrt() on floats resolve to the global float overloads <cmath> provides under MSVC.
+
+namespace bc7e_scalar {
+
+// TEMP debug checkpoint (single-threaded runs only). Prints the source line so
+// the last value before a crash pinpoints the failing statement. Remove once verified.
+#define HERE() do { fprintf(stderr, "L%d\n", __LINE__); fflush(stderr); } while(0)
+
+// Tiny bias added to data-dependent denominators that can legitimately be zero
+// (flat blocks / flat channels -> zero endpoint deltas). The original SIMD code
+// let the divide produce +/-Inf/NaN and relied on a later min/max clamp to
+// sanitize it; in scalar code an unguarded 0/0 NaN can become an INT_MIN array
+// index. Biasing keeps the divisor finite and nonzero with negligible effect on
+// the result (denominators are otherwise >= 1 in these spots).
+static const float BC7E_DENOM_BIAS = 0.0000125f;
+
+// bc7e.ispc - Fast high quality SIMD BC7 encoder
+// Copyright (C) 2018-2020 Binomial LLC, All rights reserved. Apache 2.0 license - see LICENSE.
+// Typically compiled as: ispc -g -O2 "%(Filename).ispc" -o "$(TargetDir)%(Filename).obj" -h "$(ProjectDir)%(Filename)_ispc.h" --target=sse2,sse4,avx,avx2 --opt=fast-math --opt=disable-assertions
+// --opt=fast-math is optional (doesn't make much if any measurable difference). 
+// Thanks to ArasP for the determinism fix.
+
+#define BC7E_NON_DETERMINISTIC (0)
+#define BC7E_2SUBSET_CHECKERBOARD_PARTITION_INDEX (34)
+#define BC7E_BLOCK_SIZE (16)
+#define BC7E_MAX_PARTITIONS0 (16)
+#define BC7E_MAX_PARTITIONS1 (64)
+#define BC7E_MAX_PARTITIONS2 (64)
+#define BC7E_MAX_PARTITIONS3 (64)
+#define BC7E_MAX_PARTITIONS7 (64)
+#define BC7E_MAX_UBER_LEVEL (4)
+
+// endpoint_err::m_error is a real uint16_t (was a 64-bit alias inherited from the ISPC source).
+// Verified safe to narrow: all stores are single-channel squared errors (k-c)^2 <= 65025,
+// guarded by safe_cast_uint16(); reads only compare or sum 3-4 of them (integer-promoted to
+// int, max ~260100), so 16 bits is sufficient and no 64-bit width is relied upon.
+
+#ifndef UINT16_MAX
+#define UINT16_MAX (0xFFFF)
+#endif
+
+#ifndef UINT_MAX
+#define UINT_MAX (0xFFFFFFFFU)
+#endif
+
+#ifndef UINT64_MAX
+#define UINT64_MAX (0xFFFFFFFFFFFFFFFFULL)
+#endif
+
+#ifndef INT64_MAX
+#define INT64_MAX (0x7FFFFFFFFFFFFFFFULL)
+#endif
+
+
+static inline  int32_t clampi( int32_t value,  int32_t low,  int32_t high) { return clamp(value, low, high); }
+[[maybe_unused]] static inline  uint32_t clampu( uint32_t value,  uint32_t low,  uint32_t high) { return clamp(value, low, high); }
+static inline  float clampf( float value,  float low,  float high) { return clamp(value, low, high); }
+
+static inline  float saturate( float value) { return clampf(value, 0, 1.0f); }
+[[maybe_unused]] static inline  float saturate255( float value) { return clampf(value, 0, 255.0f); }
+
+[[maybe_unused]] static inline  uint8_t minimumub( uint8_t a,  uint8_t b) { return min(a, b); }
+static inline  int32_t minimumi( int32_t a,  int32_t b) { return min(a, b); }
+static inline  uint32_t minimumu( uint32_t a,  uint32_t b) { return min(a, b); }
+[[maybe_unused]] static inline  uint64_t minimumu64( uint64_t a,  uint64_t b) { return min(a, b); }
+static inline  float minimumf( float a,  float b) { return min(a, b); }
+				
+[[maybe_unused]] static inline  uint8_t maximumub( uint8_t a,  uint8_t b) { return max(a, b); }
+static inline  int32_t maximumi( int32_t a,  int32_t b) { return max(a, b); }
+static inline  uint32_t maximumu( uint32_t a,  uint32_t b) { return max(a, b); }
+static inline  float maximumf( float a,  float b) { return max(a, b); }
+				
+static inline  int32_t iabs32( int32_t v) {  uint32_t msk = v >> 31; return (v ^ msk) - msk; }
+
+[[maybe_unused]] static inline void swapub( uint8_t *  a,  uint8_t *  b) {  uint8_t t = *a; *a = *b; *b = t; }
+static inline void swapu( uint32_t *  a,  uint32_t *  b) {  uint32_t t = *a; *a = *b; *b = t; }
+static inline void swapi( int32_t *  a,  int32_t *  b) {  int32_t t = *a; *a = *b; *b = t; }
+static inline void swapf( float *  a,  float *  b) {  float t = *a; *a = *b; *b = t; }
+
+static inline float square(float s) { return s * s; }
+[[maybe_unused]] static inline int square(int s) { return s * s; }
+
+struct color_quad_u8
+{
+	uint8_t m_c[4];
+};
+
+struct color_quad_i
+{
+	int32_t m_c[4];
+};
+
+struct color_quad_f
+{
+	float m_c[4];
+};
+
+[[maybe_unused]] static inline color_quad_i component_min_rgb(const  color_quad_i *  pA, const  color_quad_i *  pB)
+{ 
+	color_quad_i res; 
+	res.m_c[0] = minimumi(pA->m_c[0], pB->m_c[0]);
+	res.m_c[1] = minimumi(pA->m_c[1], pB->m_c[1]);
+	res.m_c[2] = minimumi(pA->m_c[2], pB->m_c[2]);
+	res.m_c[3] = 255;
+	return res;
+}
+
+[[maybe_unused]] static inline color_quad_i component_max_rgb(const  color_quad_i *  pA, const  color_quad_i *  pB) 
+{ 
+	color_quad_i res;
+	res.m_c[0] = maximumi(pA->m_c[0], pB->m_c[0]);
+	res.m_c[1] = maximumi(pA->m_c[1], pB->m_c[1]);
+	res.m_c[2] = maximumi(pA->m_c[2], pB->m_c[2]);
+	res.m_c[3] = 255;
+	return res;
+}
+
+static inline  color_quad_i *color_quad_i_set_clamped( color_quad_i *  pRes,  int32_t r,  int32_t g,  int32_t b,  int32_t a)
+{
+	pRes->m_c[0] = clampi(r, 0, 255);
+	pRes->m_c[1] = clampi(g, 0, 255);
+	pRes->m_c[2] = clampi(b, 0, 255);
+	pRes->m_c[3] = clampi(a, 0, 255);
+	return pRes;
+}
+
+static inline  color_quad_i *color_quad_i_set( color_quad_i *  pRes,  int32_t r,  int32_t g,  int32_t b,  int32_t a)
+{
+	pRes->m_c[0] = r;
+	pRes->m_c[1] = g;
+	pRes->m_c[2] = b;
+	pRes->m_c[3] = a;
+	return pRes;
+}
+
+static inline bool color_quad_i_equals(const  color_quad_i *  pLHS, const  color_quad_i *  pRHS)
+{
+	return (pLHS->m_c[0] == pRHS->m_c[0]) && (pLHS->m_c[1] == pRHS->m_c[1]) && (pLHS->m_c[2] == pRHS->m_c[2]) && (pLHS->m_c[3] == pRHS->m_c[3]);
+}
+
+static inline bool color_quad_i_notequals(const  color_quad_i *  pLHS, const  color_quad_i *  pRHS)
+{
+	return !color_quad_i_equals(pLHS, pRHS);
+}
+
+struct vec4F
+{
+	float m_c[4];
+};
+
+static inline  vec4F *  vec4F_set_scalar( vec4F *  pV, float x)
+{
+	pV->m_c[0] = x;
+	pV->m_c[1] = x;
+	pV->m_c[2] = x;
+	pV->m_c[3] = x;
+	return pV;
+}
+
+static inline  vec4F *  vec4F_set( vec4F *  pV, float x, float y, float z, float w)
+{
+	pV->m_c[0] = x;
+	pV->m_c[1] = y;
+	pV->m_c[2] = z;
+	pV->m_c[3] = w;
+	return pV;
+}
+
+static inline  vec4F *  vec4F_saturate_in_place( vec4F *  pV)
+{
+	pV->m_c[0] = saturate(pV->m_c[0]);
+	pV->m_c[1] = saturate(pV->m_c[1]);
+	pV->m_c[2] = saturate(pV->m_c[2]);
+	pV->m_c[3] = saturate(pV->m_c[3]);
+	return pV;
+}
+
+static inline vec4F vec4F_saturate(const  vec4F *  pV)
+{
+	vec4F res;
+	res.m_c[0] = saturate(pV->m_c[0]);
+	res.m_c[1] = saturate(pV->m_c[1]);
+	res.m_c[2] = saturate(pV->m_c[2]);
+	res.m_c[3] = saturate(pV->m_c[3]);
+	return res;
+}
+
+static inline vec4F vec4F_from_color(const  color_quad_i *  pC)
+{
+	vec4F res;
+	vec4F_set(&res, (float)pC->m_c[0], (float)pC->m_c[1], (float)pC->m_c[2], (float)pC->m_c[3]);
+	return res;
+}
+
+static inline vec4F vec4F_add(const  vec4F *  pLHS, const  vec4F *  pRHS)
+{
+	vec4F res;
+	vec4F_set(&res, pLHS->m_c[0] + pRHS->m_c[0], pLHS->m_c[1] + pRHS->m_c[1], pLHS->m_c[2] + pRHS->m_c[2], pLHS->m_c[3] + pRHS->m_c[3]);
+	return res;
+}
+
+static inline vec4F vec4F_sub(const  vec4F *  pLHS, const  vec4F * pRHS)
+{
+	vec4F res;
+	vec4F_set(&res, pLHS->m_c[0] - pRHS->m_c[0], pLHS->m_c[1] - pRHS->m_c[1], pLHS->m_c[2] - pRHS->m_c[2], pLHS->m_c[3] - pRHS->m_c[3]);
+	return res;
+}
+
+static inline float vec4F_dot(const  vec4F *  pLHS, const  vec4F *  pRHS)
+{
+	return pLHS->m_c[0] * pRHS->m_c[0] + pLHS->m_c[1] * pRHS->m_c[1] + pLHS->m_c[2] * pRHS->m_c[2] + pLHS->m_c[3] * pRHS->m_c[3];
+}
+
+static inline vec4F vec4F_mul(const  vec4F *  pLHS, float s)
+{
+	vec4F res;
+	vec4F_set(&res, pLHS->m_c[0] * s, pLHS->m_c[1] * s, pLHS->m_c[2] * s, pLHS->m_c[3] * s);
+	return res;
+}
+
+static inline  vec4F *vec4F_normalize_in_place( vec4F *  pV)
+{
+	float s = pV->m_c[0] * pV->m_c[0] + pV->m_c[1] * pV->m_c[1] + pV->m_c[2] * pV->m_c[2] + pV->m_c[3] * pV->m_c[3];
+				
+	if (s != 0.0f)
+	{
+		s = 1.0f / sqrt(s);
+
+		pV->m_c[0] *= s;
+		pV->m_c[1] *= s;
+		pV->m_c[2] *= s;
+		pV->m_c[3] *= s;
+	}
+
+	return pV;
+}
+
+static const  uint32_t g_bc7_weights2[4] = { 0, 21, 43, 64 };
+static const  uint32_t g_bc7_weights3[8] = { 0, 9, 18, 27, 37, 46, 55, 64 };
+static const  uint32_t g_bc7_weights4[16] = { 0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64 };
+
+// Precomputed weight constants used during least fit determination. For each entry in g_bc7_weights[]: w * w, (1.0f - w) * w, (1.0f - w) * (1.0f - w), w
+static const  float g_bc7_weights2x[4 * 4] = { 0.000000f, 0.000000f, 1.000000f, 0.000000f, 0.107666f, 0.220459f, 0.451416f, 0.328125f, 0.451416f, 0.220459f, 0.107666f, 0.671875f, 1.000000f, 0.000000f, 0.000000f, 1.000000f };
+static const  float g_bc7_weights3x[8 * 4] = { 0.000000f, 0.000000f, 1.000000f, 0.000000f, 0.019775f, 0.120850f, 0.738525f, 0.140625f, 0.079102f, 0.202148f, 0.516602f, 0.281250f, 0.177979f, 0.243896f, 0.334229f, 0.421875f, 0.334229f, 0.243896f, 0.177979f, 0.578125f, 0.516602f, 0.202148f,
+	0.079102f, 0.718750f, 0.738525f, 0.120850f, 0.019775f, 0.859375f, 1.000000f, 0.000000f, 0.000000f, 1.000000f };
+static const  float g_bc7_weights4x[16 * 4] = { 0.000000f, 0.000000f, 1.000000f, 0.000000f, 0.003906f, 0.058594f, 0.878906f, 0.062500f, 0.019775f, 0.120850f, 0.738525f, 0.140625f, 0.041260f, 0.161865f, 0.635010f, 0.203125f, 0.070557f, 0.195068f, 0.539307f, 0.265625f, 0.107666f, 0.220459f,
+	0.451416f, 0.328125f, 0.165039f, 0.241211f, 0.352539f, 0.406250f, 0.219727f, 0.249023f, 0.282227f, 0.468750f, 0.282227f, 0.249023f, 0.219727f, 0.531250f, 0.352539f, 0.241211f, 0.165039f, 0.593750f, 0.451416f, 0.220459f, 0.107666f, 0.671875f, 0.539307f, 0.195068f, 0.070557f, 0.734375f,
+	0.635010f, 0.161865f, 0.041260f, 0.796875f, 0.738525f, 0.120850f, 0.019775f, 0.859375f, 0.878906f, 0.058594f, 0.003906f, 0.937500f, 1.000000f, 0.000000f, 0.000000f, 1.000000f };
+
+static const  int g_bc7_partition1[16] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };
+
+static const  int g_bc7_partition2[64 * 16] =
+{
+	0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,		0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,		0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,		0,0,0,1,0,0,1,1,0,0,1,1,0,1,1,1,		0,0,0,0,0,0,0,1,0,0,0,1,0,0,1,1,		0,0,1,1,0,1,1,1,0,1,1,1,1,1,1,1,		0,0,0,1,0,0,1,1,0,1,1,1,1,1,1,1,		0,0,0,0,0,0,0,1,0,0,1,1,0,1,1,1,
+	0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,1,		0,0,1,1,0,1,1,1,1,1,1,1,1,1,1,1,		0,0,0,0,0,0,0,1,0,1,1,1,1,1,1,1,		0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,1,		0,0,0,1,0,1,1,1,1,1,1,1,1,1,1,1,		0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,		0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,		0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,
+	0,0,0,0,1,0,0,0,1,1,1,0,1,1,1,1,		0,1,1,1,0,0,0,1,0,0,0,0,0,0,0,0,		0,0,0,0,0,0,0,0,1,0,0,0,1,1,1,0,		0,1,1,1,0,0,1,1,0,0,0,1,0,0,0,0,		0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0,		0,0,0,0,1,0,0,0,1,1,0,0,1,1,1,0,		0,0,0,0,0,0,0,0,1,0,0,0,1,1,0,0,		0,1,1,1,0,0,1,1,0,0,1,1,0,0,0,1,
+	0,0,1,1,0,0,0,1,0,0,0,1,0,0,0,0,		0,0,0,0,1,0,0,0,1,0,0,0,1,1,0,0,		0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,		0,0,1,1,0,1,1,0,0,1,1,0,1,1,0,0,		0,0,0,1,0,1,1,1,1,1,1,0,1,0,0,0,		0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,		0,1,1,1,0,0,0,1,1,0,0,0,1,1,1,0,		0,0,1,1,1,0,0,1,1,0,0,1,1,1,0,0,
+	0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,		0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,		0,1,0,1,1,0,1,0,0,1,0,1,1,0,1,0,		0,0,1,1,0,0,1,1,1,1,0,0,1,1,0,0,		0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,		0,1,0,1,0,1,0,1,1,0,1,0,1,0,1,0,		0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,		0,1,0,1,1,0,1,0,1,0,1,0,0,1,0,1,
+	0,1,1,1,0,0,1,1,1,1,0,0,1,1,1,0,		0,0,0,1,0,0,1,1,1,1,0,0,1,0,0,0,		0,0,1,1,0,0,1,0,0,1,0,0,1,1,0,0,		0,0,1,1,1,0,1,1,1,1,0,1,1,1,0,0,		0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,		0,0,1,1,1,1,0,0,1,1,0,0,0,0,1,1,		0,1,1,0,0,1,1,0,1,0,0,1,1,0,0,1,		0,0,0,0,0,1,1,0,0,1,1,0,0,0,0,0,
+	0,1,0,0,1,1,1,0,0,1,0,0,0,0,0,0,		0,0,1,0,0,1,1,1,0,0,1,0,0,0,0,0,		0,0,0,0,0,0,1,0,0,1,1,1,0,0,1,0,		0,0,0,0,0,1,0,0,1,1,1,0,0,1,0,0,		0,1,1,0,1,1,0,0,1,0,0,1,0,0,1,1,		0,0,1,1,0,1,1,0,1,1,0,0,1,0,0,1,		0,1,1,0,0,0,1,1,1,0,0,1,1,1,0,0,		0,0,1,1,1,0,0,1,1,1,0,0,0,1,1,0,
+	0,1,1,0,1,1,0,0,1,1,0,0,1,0,0,1,		0,1,1,0,0,0,1,1,0,0,1,1,1,0,0,1,		0,1,1,1,1,1,1,0,1,0,0,0,0,0,0,1,		0,0,0,1,1,0,0,0,1,1,1,0,0,1,1,1,		0,0,0,0,1,1,1,1,0,0,1,1,0,0,1,1,		0,0,1,1,0,0,1,1,1,1,1,1,0,0,0,0,		0,0,1,0,0,0,1,0,1,1,1,0,1,1,1,0,		0,1,0,0,0,1,0,0,0,1,1,1,0,1,1,1
+};
+
+static const  int g_bc7_table_anchor_index_second_subset[64] =
+{
+	15,15,15,15,15,15,15,15,		15,15,15,15,15,15,15,15,		15, 2, 8, 2, 2, 8, 8,15,		2, 8, 2, 2, 8, 8, 2, 2,		15,15, 6, 8, 2, 8,15,15,		2, 8, 2, 2, 2,15,15, 6,		6, 2, 6, 8,15,15, 2, 2,		15,15,15,15,15, 2, 2,15
+};
+
+static const  int g_bc7_partition3[64 * 16] =
+{
+	0,0,1,1,0,0,1,1,0,2,2,1,2,2,2,2,		0,0,0,1,0,0,1,1,2,2,1,1,2,2,2,1,		0,0,0,0,2,0,0,1,2,2,1,1,2,2,1,1,		0,2,2,2,0,0,2,2,0,0,1,1,0,1,1,1,		0,0,0,0,0,0,0,0,1,1,2,2,1,1,2,2,		0,0,1,1,0,0,1,1,0,0,2,2,0,0,2,2,		0,0,2,2,0,0,2,2,1,1,1,1,1,1,1,1,		0,0,1,1,0,0,1,1,2,2,1,1,2,2,1,1,
+	0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,		0,0,0,0,1,1,1,1,1,1,1,1,2,2,2,2,		0,0,0,0,1,1,1,1,2,2,2,2,2,2,2,2,		0,0,1,2,0,0,1,2,0,0,1,2,0,0,1,2,		0,1,1,2,0,1,1,2,0,1,1,2,0,1,1,2,		0,1,2,2,0,1,2,2,0,1,2,2,0,1,2,2,		0,0,1,1,0,1,1,2,1,1,2,2,1,2,2,2,		0,0,1,1,2,0,0,1,2,2,0,0,2,2,2,0,
+	0,0,0,1,0,0,1,1,0,1,1,2,1,1,2,2,		0,1,1,1,0,0,1,1,2,0,0,1,2,2,0,0,		0,0,0,0,1,1,2,2,1,1,2,2,1,1,2,2,		0,0,2,2,0,0,2,2,0,0,2,2,1,1,1,1,		0,1,1,1,0,1,1,1,0,2,2,2,0,2,2,2,		0,0,0,1,0,0,0,1,2,2,2,1,2,2,2,1,		0,0,0,0,0,0,1,1,0,1,2,2,0,1,2,2,		0,0,0,0,1,1,0,0,2,2,1,0,2,2,1,0,
+	0,1,2,2,0,1,2,2,0,0,1,1,0,0,0,0,		0,0,1,2,0,0,1,2,1,1,2,2,2,2,2,2,		0,1,1,0,1,2,2,1,1,2,2,1,0,1,1,0,		0,0,0,0,0,1,1,0,1,2,2,1,1,2,2,1,		0,0,2,2,1,1,0,2,1,1,0,2,0,0,2,2,		0,1,1,0,0,1,1,0,2,0,0,2,2,2,2,2,		0,0,1,1,0,1,2,2,0,1,2,2,0,0,1,1,		0,0,0,0,2,0,0,0,2,2,1,1,2,2,2,1,
+	0,0,0,0,0,0,0,2,1,1,2,2,1,2,2,2,		0,2,2,2,0,0,2,2,0,0,1,2,0,0,1,1,		0,0,1,1,0,0,1,2,0,0,2,2,0,2,2,2,		0,1,2,0,0,1,2,0,0,1,2,0,0,1,2,0,		0,0,0,0,1,1,1,1,2,2,2,2,0,0,0,0,		0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,0,		0,1,2,0,2,0,1,2,1,2,0,1,0,1,2,0,		0,0,1,1,2,2,0,0,1,1,2,2,0,0,1,1,
+	0,0,1,1,1,1,2,2,2,2,0,0,0,0,1,1,		0,1,0,1,0,1,0,1,2,2,2,2,2,2,2,2,		0,0,0,0,0,0,0,0,2,1,2,1,2,1,2,1,		0,0,2,2,1,1,2,2,0,0,2,2,1,1,2,2,		0,0,2,2,0,0,1,1,0,0,2,2,0,0,1,1,		0,2,2,0,1,2,2,1,0,2,2,0,1,2,2,1,		0,1,0,1,2,2,2,2,2,2,2,2,0,1,0,1,		0,0,0,0,2,1,2,1,2,1,2,1,2,1,2,1,
+	0,1,0,1,0,1,0,1,0,1,0,1,2,2,2,2,		0,2,2,2,0,1,1,1,0,2,2,2,0,1,1,1,		0,0,0,2,1,1,1,2,0,0,0,2,1,1,1,2,		0,0,0,0,2,1,1,2,2,1,1,2,2,1,1,2,		0,2,2,2,0,1,1,1,0,1,1,1,0,2,2,2,		0,0,0,2,1,1,1,2,1,1,1,2,0,0,0,2,		0,1,1,0,0,1,1,0,0,1,1,0,2,2,2,2,		0,0,0,0,0,0,0,0,2,1,1,2,2,1,1,2,
+	0,1,1,0,0,1,1,0,2,2,2,2,2,2,2,2,		0,0,2,2,0,0,1,1,0,0,1,1,0,0,2,2,		0,0,2,2,1,1,2,2,1,1,2,2,0,0,2,2,		0,0,0,0,0,0,0,0,0,0,0,0,2,1,1,2,		0,0,0,2,0,0,0,1,0,0,0,2,0,0,0,1,		0,2,2,2,1,2,2,2,0,2,2,2,1,2,2,2,		0,1,0,1,2,2,2,2,2,2,2,2,2,2,2,2,		0,1,1,1,2,0,1,1,2,2,0,1,2,2,2,0,
+};
+
+static const  int g_bc7_table_anchor_index_third_subset_1[64] =
+{
+	3, 3,15,15, 8, 3,15,15,		8, 8, 6, 6, 6, 5, 3, 3,		3, 3, 8,15, 3, 3, 6,10,		5, 8, 8, 6, 8, 5,15,15,		8,15, 3, 5, 6,10, 8,15,		15, 3,15, 5,15,15,15,15,		3,15, 5, 5, 5, 8, 5,10,		5,10, 8,13,15,12, 3, 3
+};
+
+static const  int g_bc7_table_anchor_index_third_subset_2[64] =
+{
+	15, 8, 8, 3,15,15, 3, 8,		15,15,15,15,15,15,15, 8,		15, 8,15, 3,15, 8,15, 8,		3,15, 6,10,15,15,10, 8,		15, 3,15,10,10, 8, 9,10,		6,15, 8,15, 3, 6, 6, 8,		15, 3,15,15,15,15,15,15,		15,15,15,15, 3,15,15, 8
+};
+
+static const  int g_bc7_num_subsets[8] = { 3, 2, 3, 2, 1, 1, 1, 2 };
+static const  int g_bc7_partition_bits[8] = { 4, 6, 6, 6, 0, 0, 0, 6 };
+[[maybe_unused]] static const  int g_bc7_rotation_bits[8] = { 0, 0, 0, 0, 2, 2, 0, 0 };
+static const  int g_bc7_color_index_bitcount[8] = { 3, 3, 2, 2, 2, 2, 4, 2 };
+static int  get_bc7_color_index_size( int mode,  int index_selection_bit) { return g_bc7_color_index_bitcount[mode] + index_selection_bit; }
+static  int g_bc7_alpha_index_bitcount[8] = { 0, 0, 0, 0, 3, 2, 4, 2 };
+static  int get_bc7_alpha_index_size( int mode,  int index_selection_bit) { return g_bc7_alpha_index_bitcount[mode] - index_selection_bit; }
+static const  int g_bc7_mode_has_p_bits[8] = { 1, 1, 0, 1, 0, 0, 1, 1 };
+static const  int g_bc7_mode_has_shared_p_bits[8] = { 0, 1, 0, 0, 0, 0, 0, 0 };
+static const  int g_bc7_color_precision_table[8] = { 4, 6, 5, 7, 5, 7, 7, 5 };
+[[maybe_unused]] static const  int g_bc7_color_precision_plus_pbit_table[8] = { 5, 7, 5, 8, 5, 7, 8, 6 };
+static const  int g_bc7_alpha_precision_table[8] = { 0, 0, 0, 0, 6, 8, 7, 5 };
+[[maybe_unused]] static const  int g_bc7_alpha_precision_plus_pbit_table[8] = { 0, 0, 0, 0, 6, 8, 8, 6 };
+static  bool get_bc7_mode_has_seperate_alpha_selectors( int mode) { return (mode == 4) || (mode == 5); }
+
+struct endpoint_err
+{
+	uint16_t m_error;
+	uint8_t m_lo;
+	uint8_t m_hi;
+};
+
+// Guarded store into endpoint_err::m_error (a real uint16_t). The stored quantity
+// is always a single-channel squared error (k-c)^2 with k,c in [0,255], so it is
+// <= 255^2 = 65025 <= UINT16_MAX by construction. This assert verifies that
+// invariant on every store; if it ever fires (e.g. a weighted error gets routed
+// here), the value would silently truncate at 16 bits, so the assert must hold.
+static inline uint16_t safe_cast_uint16(int v)
+{
+	assert(v >= 0 && v <= (int)UINT16_MAX);
+	return (uint16_t)v;
+}
+
+static  endpoint_err g_bc7_mode_1_optimal_endpoints[256][2]; // [c][pbit]
+const  uint32_t BC7E_MODE_1_OPTIMAL_INDEX = 2;
+
+static  endpoint_err g_bc7_mode_7_optimal_endpoints[256][2][2]; // [c][pbit][hp][lp]
+const  uint32_t BC7E_MODE_7_OPTIMAL_INDEX = 1;
+
+static  endpoint_err g_bc7_mode_6_optimal_endpoints[256][2][2]; // [c][hp][lp]
+const  uint32_t BC7E_MODE_6_OPTIMAL_INDEX = 5;
+
+static  uint32_t g_bc7_mode_4_optimal_endpoints3[256]; // [c]
+static  uint32_t g_bc7_mode_4_optimal_endpoints2[256]; // [c]
+const  uint32_t BC7E_MODE_4_OPTIMAL_INDEX3 = 2;
+const  uint32_t BC7E_MODE_4_OPTIMAL_INDEX2 = 1;
+
+static  uint32_t g_bc7_mode_5_optimal_endpoints[256]; // [c]
+const  uint32_t BC7E_MODE_5_OPTIMAL_INDEX = 1;
+
+static  endpoint_err g_bc7_mode_0_optimal_endpoints[256][2][2]; // [c][hp][lp]
+const  uint32_t BC7E_MODE_0_OPTIMAL_INDEX = 2;
+
+static  bool g_codec_initialized;
+
+ void bc7e_compress_block_init()
+{
+	if (g_codec_initialized)
+		return;
+
+	// Mode 0: 444.1
+	for ( int c = 0; c < 256; c++)
+	{
+		for ( uint32_t hp = 0; hp < 2; hp++)
+		{
+			for ( uint32_t lp = 0; lp < 2; lp++)
+			{
+				 endpoint_err best;
+				best.m_error = safe_cast_uint16(UINT16_MAX);
+
+				for ( uint32_t l = 0; l < 16; l++)
+				{
+					 uint32_t low = ((l << 1) | lp) << 3;
+					low |= (low >> 5);
+
+					for ( uint32_t h = 0; h < 16; h++)
+					{
+						 uint32_t high = ((h << 1) | hp) << 3;
+						high |= (high >> 5);
+
+						const  int k = (low * (64 - g_bc7_weights3[BC7E_MODE_0_OPTIMAL_INDEX]) + high * g_bc7_weights3[BC7E_MODE_0_OPTIMAL_INDEX] + 32) >> 6;
+
+						const  int err = (k - c) * (k - c);
+						if (err < best.m_error)
+						{
+							best.m_error = safe_cast_uint16(err);
+							best.m_lo = (uint8_t)l;
+							best.m_hi = (uint8_t)h;
+						}
+					} // h
+				} // l
+
+				g_bc7_mode_0_optimal_endpoints[c][hp][lp] = best;
+			} // lp
+		} // hp
+	} // c
+
+	// Mode 1: 666.1
+	for ( int c = 0; c < 256; c++)
+	{
+		for ( uint32_t lp = 0; lp < 2; lp++)
+		{
+			 endpoint_err best;
+			best.m_error = safe_cast_uint16(UINT16_MAX);
+
+			for ( uint32_t l = 0; l < 64; l++)
+			{
+				 uint32_t low = ((l << 1) | lp) << 1;
+				low |= (low >> 7);
+
+				for ( uint32_t h = 0; h < 64; h++)
+				{
+					 uint32_t high = ((h << 1) | lp) << 1;
+					high |= (high >> 7);
+
+					const  int k = (low * (64 - g_bc7_weights3[BC7E_MODE_1_OPTIMAL_INDEX]) + high * g_bc7_weights3[BC7E_MODE_1_OPTIMAL_INDEX] + 32) >> 6;
+
+					const  int err = (k - c) * (k - c);
+					if (err < best.m_error)
+					{
+						best.m_error = safe_cast_uint16(err);
+						best.m_lo = (uint8_t)l;
+						best.m_hi = (uint8_t)h;
+					}
+				} // h
+			} // l
+
+			g_bc7_mode_1_optimal_endpoints[c][lp] = best;
+		} // lp
+	} // c
+
+	// Mode 6: 777.1 4-bit indices
+	for ( int c = 0; c < 256; c++)
+	{
+		for ( uint32_t hp = 0; hp < 2; hp++)
+		{
+			for ( uint32_t lp = 0; lp < 2; lp++)
+			{
+				 endpoint_err best;
+				best.m_error = safe_cast_uint16(UINT16_MAX);
+
+				for ( uint32_t l = 0; l < 128; l++)
+				{
+					 uint32_t low = (l << 1) | lp;
+				
+					for ( uint32_t h = 0; h < 128; h++)
+					{
+						 uint32_t high = (h << 1) | hp;
+					
+						const  int k = (low * (64 - g_bc7_weights4[BC7E_MODE_6_OPTIMAL_INDEX]) + high * g_bc7_weights4[BC7E_MODE_6_OPTIMAL_INDEX] + 32) >> 6;
+
+						const  int err = (k - c) * (k - c);
+						if (err < best.m_error)
+						{
+							best.m_error = safe_cast_uint16(err);
+							best.m_lo = (uint8_t)l;
+							best.m_hi = (uint8_t)h;
+						}
+					} // h
+				} // l
+
+				g_bc7_mode_6_optimal_endpoints[c][hp][lp] = best;
+			} // lp
+		} // hp
+	} // c
+
+	// Mode 5: 777 2-bit indices
+	for ( int c = 0; c < 256; c++)
+	{
+		 endpoint_err best;
+		best.m_error = safe_cast_uint16(UINT16_MAX);
+		best.m_lo = 0;
+		best.m_hi = 0;
+
+		for ( uint32_t l = 0; l < 128; l++)
+		{
+			 uint32_t low = l << 1;
+			low |= (low >> 7);
+
+			for ( uint32_t h = 0; h < 128; h++)
+			{
+				 uint32_t high = h << 1;
+				high |= (high >> 7);
+
+				const  int k = (low * (64 - g_bc7_weights2[BC7E_MODE_5_OPTIMAL_INDEX]) + high * g_bc7_weights2[BC7E_MODE_5_OPTIMAL_INDEX] + 32) >> 6;
+
+				const  int err = (k - c) * (k - c);
+				if (err < best.m_error)
+				{
+					best.m_error = safe_cast_uint16(err);
+					best.m_lo = (uint8_t)l;
+					best.m_hi = (uint8_t)h;
+				}
+			} // h
+		} // l
+
+		g_bc7_mode_5_optimal_endpoints[c] = (uint32_t)best.m_lo | (((uint32_t)best.m_hi) << 8);
+
+	} // c
+
+
+	//Mode 4: 555 3-bit indices
+	for ( int c = 0; c < 256; c++)
+	{
+		 endpoint_err best;
+		best.m_error = safe_cast_uint16(UINT16_MAX);
+		best.m_lo = 0;
+		best.m_hi = 0;
+
+		for ( uint32_t l = 0; l < 32; l++)
+		{
+			 uint32_t low = l << 3;
+			low |= (low >> 5);
+
+			for ( uint32_t h = 0; h < 32; h++)
+			{
+				 uint32_t high = h << 3;
+				high |= (high >> 5);
+
+				const  int k = (low * (64 - g_bc7_weights3[BC7E_MODE_4_OPTIMAL_INDEX3]) + high * g_bc7_weights3[BC7E_MODE_4_OPTIMAL_INDEX3] + 32) >> 6;
+
+				const  int err = (k - c) * (k - c);
+				if (err < best.m_error)
+				{
+					best.m_error = safe_cast_uint16(err);
+					best.m_lo = (uint8_t)l;
+					best.m_hi = (uint8_t)h;
+				}
+			} // h
+		} // l
+
+		g_bc7_mode_4_optimal_endpoints3[c] = (uint32_t)best.m_lo | (((uint32_t)best.m_hi) << 8);
+
+	} // c
+	
+	// Mode 4: 555 2-bit indices
+	for ( int c = 0; c < 256; c++)
+	{
+		 endpoint_err best;
+		best.m_error = safe_cast_uint16(UINT16_MAX);
+		best.m_lo = 0;
+		best.m_hi = 0;
+
+		for ( uint32_t l = 0; l < 32; l++)
+		{
+			 uint32_t low = l << 3;
+			low |= (low >> 5);
+
+			for ( uint32_t h = 0; h < 32; h++)
+			{
+				 uint32_t high = h << 3;
+				high |= (high >> 5);
+
+				const  int k = (low * (64 - g_bc7_weights2[BC7E_MODE_4_OPTIMAL_INDEX2]) + high * g_bc7_weights2[BC7E_MODE_4_OPTIMAL_INDEX2] + 32) >> 6;
+
+				const  int err = (k - c) * (k - c);
+				if (err < best.m_error)
+				{
+					best.m_error = safe_cast_uint16(err);
+					best.m_lo = (uint8_t)l;
+					best.m_hi = (uint8_t)h;
+				}
+			} // h
+		} // l
+
+		g_bc7_mode_4_optimal_endpoints2[c] = (uint32_t)best.m_lo | (((uint32_t)best.m_hi) << 8);
+
+	} // c
+
+	// Mode 7: 555.1 2-bit indices 
+	for ( int c = 0; c < 256; c++)
+	{
+		 endpoint_err best;
+		best.m_error = safe_cast_uint16(UINT16_MAX);
+		best.m_lo = 0;
+		best.m_hi = 0;
+
+		for ( uint32_t hp = 0; hp < 2; hp++)
+		{
+			for ( uint32_t lp = 0; lp < 2; lp++)	
+			{
+				for ( uint32_t l = 0; l < 32; l++)
+				{
+					 uint32_t low = ((l << 1) | lp) << 2;
+					low |= (low >> 6);
+
+					for ( uint32_t h = 0; h < 32; h++)
+					{
+						 uint32_t high = ((h << 1) | hp) << 2;
+						high |= (high >> 6);
+
+						const  int k = (low * (64 - g_bc7_weights2[BC7E_MODE_7_OPTIMAL_INDEX]) + high * g_bc7_weights2[BC7E_MODE_7_OPTIMAL_INDEX] + 32) >> 6;
+
+						const  int err = (k - c) * (k - c);
+						if (err < best.m_error)
+						{
+							best.m_error = safe_cast_uint16(err);
+							best.m_lo = (uint8_t)l;
+							best.m_hi = (uint8_t)h;
+						}
+					} // h
+				} // l
+
+				g_bc7_mode_7_optimal_endpoints[c][hp][lp] = best;
+			
+			} // hp
+
+		} // lp
+
+	} // c
+
+	g_codec_initialized = true;
+}
+
+static void compute_least_squares_endpoints_rgba(uint32_t N, const  int * pSelectors, const  vec4F * pSelector_weights,  vec4F * pXl,  vec4F * pXh, const  color_quad_i *  pColors)
+{
+	// Least squares using normal equations: http://www.cs.cornell.edu/~bindel/class/cs3220-s12/notes/lec10.pdf 
+	// I did this in matrix form first, expanded out all the ops, then optimized it a bit.
+	float z00 = 0.0f, z01 = 0.0f, z10 = 0.0f, z11 = 0.0f;
+	float q00_r = 0.0f, q10_r = 0.0f, t_r = 0.0f;
+	float q00_g = 0.0f, q10_g = 0.0f, t_g = 0.0f;
+	float q00_b = 0.0f, q10_b = 0.0f, t_b = 0.0f;
+	float q00_a = 0.0f, q10_a = 0.0f, t_a = 0.0f;
+	for ( uint32_t i = 0; i < N; i++)
+	{
+		const uint32_t sel = pSelectors[i];
+
+		z00 += pSelector_weights[sel].m_c[0];
+		z10 += pSelector_weights[sel].m_c[1];
+		z11 += pSelector_weights[sel].m_c[2];
+
+		float w = pSelector_weights[sel].m_c[3];
+
+		q00_r += w * (int)pColors[i].m_c[0]; t_r += (int)pColors[i].m_c[0];
+		q00_g += w * (int)pColors[i].m_c[1]; t_g += (int)pColors[i].m_c[1];
+		q00_b += w * (int)pColors[i].m_c[2]; t_b += (int)pColors[i].m_c[2];
+		q00_a += w * (int)pColors[i].m_c[3]; t_a += (int)pColors[i].m_c[3];
+	}
+
+	q10_r = t_r - q00_r;
+	q10_g = t_g - q00_g;
+	q10_b = t_b - q00_b;
+	q10_a = t_a - q00_a;
+
+	z01 = z10;
+
+	float det = z00 * z11 - z01 * z10;
+	if (det != 0.0f)
+		det = 1.0f / det;
+
+	float iz00, iz01, iz10, iz11;
+	iz00 = z11 * det;
+	iz01 = -z01 * det;
+	iz10 = -z10 * det;
+	iz11 = z00 * det;
+
+	pXl->m_c[0] = (float)(iz00 * q00_r + iz01 * q10_r); pXh->m_c[0] = (float)(iz10 * q00_r + iz11 * q10_r);
+	pXl->m_c[1] = (float)(iz00 * q00_g + iz01 * q10_g); pXh->m_c[1] = (float)(iz10 * q00_g + iz11 * q10_g);
+	pXl->m_c[2] = (float)(iz00 * q00_b + iz01 * q10_b); pXh->m_c[2] = (float)(iz10 * q00_b + iz11 * q10_b);
+	pXl->m_c[3] = (float)(iz00 * q00_a + iz01 * q10_a); pXh->m_c[3] = (float)(iz10 * q00_a + iz11 * q10_a);
+}
+
+static void compute_least_squares_endpoints_rgb(uint32_t N, const  int * pSelectors, const  vec4F * pSelector_weights,  vec4F * pXl,  vec4F * pXh, const  color_quad_i * pColors)
+{
+	// Least squares using normal equations: http://www.cs.cornell.edu/~bindel/class/cs3220-s12/notes/lec10.pdf 
+	// I did this in matrix form first, expanded out all the ops, then optimized it a bit.
+	float z00 = 0.0f, z01 = 0.0f, z10 = 0.0f, z11 = 0.0f;
+	float q00_r = 0.0f, q10_r = 0.0f, t_r = 0.0f;
+	float q00_g = 0.0f, q10_g = 0.0f, t_g = 0.0f;
+	float q00_b = 0.0f, q10_b = 0.0f, t_b = 0.0f;
+	for ( uint32_t i = 0; i < N; i++)
+	{
+		const uint32_t sel = pSelectors[i];
+
+		z00 += pSelector_weights[sel].m_c[0];
+		z10 += pSelector_weights[sel].m_c[1];
+		z11 += pSelector_weights[sel].m_c[2];
+		float w = pSelector_weights[sel].m_c[3];
+
+		q00_r += w * (int)pColors[i].m_c[0]; t_r += (int)pColors[i].m_c[0];
+		q00_g += w * (int)pColors[i].m_c[1]; t_g += (int)pColors[i].m_c[1];
+		q00_b += w * (int)pColors[i].m_c[2]; t_b += (int)pColors[i].m_c[2];
+	}
+
+	q10_r = t_r - q00_r;
+	q10_g = t_g - q00_g;
+	q10_b = t_b - q00_b;
+
+	z01 = z10;
+
+	float det = z00 * z11 - z01 * z10;
+	if (det != 0.0f)
+		det = 1.0f / det;
+
+	float iz00, iz01, iz10, iz11;
+	iz00 = z11 * det;
+	iz01 = -z01 * det;
+	iz10 = -z10 * det;
+	iz11 = z00 * det;
+
+	pXl->m_c[0] = (float)(iz00 * q00_r + iz01 * q10_r); pXh->m_c[0] = (float)(iz10 * q00_r + iz11 * q10_r);
+	pXl->m_c[1] = (float)(iz00 * q00_g + iz01 * q10_g); pXh->m_c[1] = (float)(iz10 * q00_g + iz11 * q10_g);
+	pXl->m_c[2] = (float)(iz00 * q00_b + iz01 * q10_b); pXh->m_c[2] = (float)(iz10 * q00_b + iz11 * q10_b);
+}
+
+static void compute_least_squares_endpoints_a(uint32_t N, const  int * pSelectors, const vec4F * pSelector_weights,  float * pXl,  float * pXh, const  color_quad_i * pColors)
+{
+	// Least squares using normal equations: http://www.cs.cornell.edu/~bindel/class/cs3220-s12/notes/lec10.pdf 
+	// I did this in matrix form first, expanded out all the ops, then optimized it a bit.
+	float z00 = 0.0f, z01 = 0.0f, z10 = 0.0f, z11 = 0.0f;
+	float q00_a = 0.0f, q10_a = 0.0f, t_a = 0.0f;
+	for ( uint32_t i = 0; i < N; i++)
+	{
+		const uint32_t sel = pSelectors[i];
+
+		z00 += pSelector_weights[sel].m_c[0];
+		z10 += pSelector_weights[sel].m_c[1];
+		z11 += pSelector_weights[sel].m_c[2];
+		float w = pSelector_weights[sel].m_c[3];
+
+		q00_a += w * (int)pColors[i].m_c[3]; t_a += (int)pColors[i].m_c[3];
+	}
+
+	q10_a = t_a - q00_a;
+
+	z01 = z10;
+
+	float det = z00 * z11 - z01 * z10;
+	if (det != 0.0f)
+		det = 1.0f / det;
+
+	float iz00, iz01, iz10, iz11;
+	iz00 = z11 * det;
+	iz01 = -z01 * det;
+	iz10 = -z10 * det;
+	iz11 = z00 * det;
+
+	*pXl = (float)(iz00 * q00_a + iz01 * q10_a); *pXh = (float)(iz10 * q00_a + iz11 * q10_a);
+}
+
+struct color_cell_compressor_params
+{
+	 uint32_t m_num_selector_weights;
+	const uint32_t * m_pSelector_weights;
+	const vec4F * m_pSelector_weightsx;
+	 uint32_t m_comp_bits;
+	 uint32_t m_weights[4];
+	 bool m_has_alpha;
+	 bool m_has_pbits;
+	 bool m_endpoints_share_pbit;
+	 bool m_perceptual;
+};
+
+static inline void color_cell_compressor_params_clear( color_cell_compressor_params * p)
+{
+	p->m_num_selector_weights = 0;
+	p->m_pSelector_weights = NULL;
+	p->m_pSelector_weightsx = NULL;
+	p->m_comp_bits = 0;
+	p->m_perceptual = false;
+	p->m_weights[0] = 1;
+	p->m_weights[1] = 1;
+	p->m_weights[2] = 1;
+	p->m_weights[3] = 1;
+	p->m_has_alpha = false;
+	p->m_has_pbits = false;
+	p->m_endpoints_share_pbit = false;
+}
+
+struct color_cell_compressor_results
+{
+	uint64_t m_best_overall_err;
+	color_quad_i m_low_endpoint;
+	color_quad_i m_high_endpoint;
+	uint32_t m_pbits[2];
+	int *m_pSelectors;
+	int *m_pSelectors_temp;
+	// True if this subset's result was produced by the precomputed "one color"
+	// optimal-endpoint lookup tables (the solid/allSame path or the average-color
+	// candidate). Those place endpoints at extreme positions that only land at one
+	// fixed weight -- a hint to callers that this subset is "weird" (fragile under
+	// lossy weight recoding).
+	bool m_used_lut;
+};
+
+static inline color_quad_i scale_color(const  color_quad_i * pC, const  color_cell_compressor_params * pParams)
+{
+	color_quad_i results;
+
+	const uint32_t n = pParams->m_comp_bits + (pParams->m_has_pbits ? 1 : 0);
+	assert((n >= 4) && (n <= 8));
+
+	for ( uint32_t i = 0; i < 4; i++)
+	{
+		uint32_t v = pC->m_c[i] << (8 - n);
+		v |= (v >> n);
+		assert(v <= 255);
+		results.m_c[i] = v;
+	}
+
+	return results;
+}
+
+static const float pr_weight = (.5f / (1.0f - .2126f)) * (.5f / (1.0f - .2126f));
+static const float pb_weight = (.5f / (1.0f - .0722f)) * (.5f / (1.0f - .0722f));
+
+static inline uint64_t compute_color_distance_rgb(const  color_quad_i *  pE1, const  color_quad_i * pE2,  bool perceptual, const uint32_t  weights[4])
+{
+	if (perceptual)
+	{
+		const float l1 = pE1->m_c[0] * .2126f + pE1->m_c[1] * .7152f + pE1->m_c[2] * .0722f;
+		const float cr1 = pE1->m_c[0] - l1;
+		const float cb1 = pE1->m_c[2] - l1;
+
+		const float l2 = pE2->m_c[0] * .2126f + pE2->m_c[1] * .7152f + pE2->m_c[2] * .0722f;
+		const float cr2 = pE2->m_c[0] - l2;
+		const float cb2 = pE2->m_c[2] - l2;
+
+		float dl = l1 - l2;
+		float dcr = cr1 - cr2;
+		float dcb = cb1 - cb2;
+
+		return (int64_t)(weights[0] * (dl * dl) + weights[1] * pr_weight * (dcr * dcr) + weights[2] * pb_weight * (dcb * dcb));
+	}
+	else
+	{
+		float dr = (float)pE1->m_c[0] - (float)pE2->m_c[0];
+		float dg = (float)pE1->m_c[1] - (float)pE2->m_c[1];
+		float db = (float)pE1->m_c[2] - (float)pE2->m_c[2];
+		
+		return (int64_t)(weights[0] * dr * dr + weights[1] * dg * dg + weights[2] * db * db);
+	}
+}
+
+static inline uint64_t compute_color_distance_rgba(const  color_quad_i * pE1, const  color_quad_i * pE2,  bool perceptual, const uint32_t  weights[4])
+{
+	float da = (float)pE1->m_c[3] - (float)pE2->m_c[3];
+	float a_err = weights[3] * (da * da);
+
+	if (perceptual)
+	{
+		const float l1 = pE1->m_c[0] * .2126f + pE1->m_c[1] * .7152f + pE1->m_c[2] * .0722f;
+		const float cr1 = pE1->m_c[0] - l1;
+		const float cb1 = pE1->m_c[2] - l1;
+
+		const float l2 = pE2->m_c[0] * .2126f + pE2->m_c[1] * .7152f + pE2->m_c[2] * .0722f;
+		const float cr2 = pE2->m_c[0] - l2;
+		const float cb2 = pE2->m_c[2] - l2;
+
+		float dl = l1 - l2;
+		float dcr = cr1 - cr2;
+		float dcb = cb1 - cb2;
+
+		return (int64_t)(weights[0] * (dl * dl) + weights[1] * pr_weight * (dcr * dcr) + weights[2] * pb_weight * (dcb * dcb) + a_err);
+	}
+	else
+	{
+		float dr = (float)pE1->m_c[0] - (float)pE2->m_c[0];
+		float dg = (float)pE1->m_c[1] - (float)pE2->m_c[1];
+		float db = (float)pE1->m_c[2] - (float)pE2->m_c[2];
+		
+		return (int64_t)(weights[0] * dr * dr + weights[1] * dg * dg + weights[2] * db * db + a_err);
+	}
+}
+
+static uint64_t pack_mode1_to_one_color(const  color_cell_compressor_params * pParams,  color_cell_compressor_results * pResults, uint32_t r, uint32_t g, uint32_t b, 
+	 int * pSelectors, uint32_t num_pixels, const  color_quad_i * pPixels)
+{
+	uint32_t best_err = UINT_MAX;
+	uint32_t best_p = 0;
+
+	for ( uint32_t p = 0; p < 2; p++)
+	{
+		uint32_t err = g_bc7_mode_1_optimal_endpoints[r][p].m_error + g_bc7_mode_1_optimal_endpoints[g][p].m_error + g_bc7_mode_1_optimal_endpoints[b][p].m_error;
+		if (err < best_err)
+		{
+			best_err = err;
+			best_p = p;
+		}
+	}
+
+	const endpoint_err *pEr = &g_bc7_mode_1_optimal_endpoints[r][best_p];
+	const endpoint_err *pEg = &g_bc7_mode_1_optimal_endpoints[g][best_p];
+	const endpoint_err *pEb = &g_bc7_mode_1_optimal_endpoints[b][best_p];
+
+	color_quad_i_set(&pResults->m_low_endpoint, pEr->m_lo, pEg->m_lo, pEb->m_lo, 0);
+	color_quad_i_set(&pResults->m_high_endpoint, pEr->m_hi, pEg->m_hi, pEb->m_hi, 0);
+	pResults->m_pbits[0] = best_p;
+	pResults->m_pbits[1] = 0;
+
+	for ( uint32_t i = 0; i < num_pixels; i++)
+		pSelectors[i] = BC7E_MODE_1_OPTIMAL_INDEX;
+
+	color_quad_i p;
+	
+	for ( uint32_t i = 0; i < 3; i++)
+	{
+		uint32_t low = ((pResults->m_low_endpoint.m_c[i] << 1) | pResults->m_pbits[0]) << 1;
+		low |= (low >> 7);
+
+		uint32_t high = ((pResults->m_high_endpoint.m_c[i] << 1) | pResults->m_pbits[0]) << 1;
+		high |= (high >> 7);
+
+		p.m_c[i] = (low * (64 - g_bc7_weights3[BC7E_MODE_1_OPTIMAL_INDEX]) + high * g_bc7_weights3[BC7E_MODE_1_OPTIMAL_INDEX] + 32) >> 6;
+	}
+
+	p.m_c[3] = 255;
+
+	uint64_t total_err = 0;
+	for ( uint32_t i = 0; i < num_pixels; i++)
+		total_err += compute_color_distance_rgb(&p, &pPixels[i], pParams->m_perceptual, pParams->m_weights);
+
+	pResults->m_best_overall_err = total_err;
+
+	return total_err;
+}
+
+static uint64_t pack_mode24_to_one_color(const  color_cell_compressor_params * pParams,  color_cell_compressor_results * pResults, uint32_t r, uint32_t g, uint32_t b, 
+	 int * pSelectors, uint32_t num_pixels, const  color_quad_i * pPixels)
+{
+	uint32_t er, eg, eb;
+
+	if (pParams->m_num_selector_weights == 8)
+	{
+		er = g_bc7_mode_4_optimal_endpoints3[r];
+		eg = g_bc7_mode_4_optimal_endpoints3[g];
+		eb = g_bc7_mode_4_optimal_endpoints3[b];
+	}
+	else
+	{
+		er = g_bc7_mode_4_optimal_endpoints2[r];
+		eg = g_bc7_mode_4_optimal_endpoints2[g];
+		eb = g_bc7_mode_4_optimal_endpoints2[b];
+	}
+	
+	color_quad_i_set(&pResults->m_low_endpoint, er & 0xFF, eg & 0xFF, eb & 0xFF, 0);
+	color_quad_i_set(&pResults->m_high_endpoint, er >> 8, eg >> 8, eb >> 8, 0);
+
+	for ( uint32_t i = 0; i < num_pixels; i++)
+		pSelectors[i] = (pParams->m_num_selector_weights == 8) ? BC7E_MODE_4_OPTIMAL_INDEX3 : BC7E_MODE_4_OPTIMAL_INDEX2;
+
+	color_quad_i p;
+	
+	for ( uint32_t i = 0; i < 3; i++)
+	{
+		uint32_t low = pResults->m_low_endpoint.m_c[i] << 3;
+		low |= (low >> 5);
+
+		uint32_t high = pResults->m_high_endpoint.m_c[i] << 3;
+		high |= (high >> 5);
+
+		if (pParams->m_num_selector_weights == 8)
+			p.m_c[i] = (low * (64 - g_bc7_weights3[BC7E_MODE_4_OPTIMAL_INDEX3]) + high * g_bc7_weights3[BC7E_MODE_4_OPTIMAL_INDEX3] + 32) >> 6;
+		else
+			p.m_c[i] = (low * (64 - g_bc7_weights2[BC7E_MODE_4_OPTIMAL_INDEX2]) + high * g_bc7_weights2[BC7E_MODE_4_OPTIMAL_INDEX2] + 32) >> 6;
+	}
+	
+	p.m_c[3] = 255;
+
+	uint64_t total_err = 0;
+	for ( uint32_t i = 0; i < num_pixels; i++)
+		total_err += compute_color_distance_rgb(&p, &pPixels[i], pParams->m_perceptual, pParams->m_weights);
+
+	pResults->m_best_overall_err = total_err;
+
+	return total_err;
+}
+
+static uint64_t pack_mode0_to_one_color(const  color_cell_compressor_params * pParams,  color_cell_compressor_results * pResults, uint32_t r, uint32_t g, uint32_t b, 
+	 int * pSelectors, uint32_t num_pixels, const  color_quad_i * pPixels)
+{
+	uint32_t best_err = UINT_MAX;
+	uint32_t best_p = 0;
+
+	for ( uint32_t p = 0; p < 4; p++)
+	{
+		uint32_t err = g_bc7_mode_0_optimal_endpoints[r][p >> 1][p & 1].m_error + g_bc7_mode_0_optimal_endpoints[g][p >> 1][p & 1].m_error + g_bc7_mode_0_optimal_endpoints[b][p >> 1][p & 1].m_error;
+		if (err < best_err)
+		{
+			best_err = err;
+			best_p = p;
+		}
+	}
+
+	const endpoint_err *pEr = &g_bc7_mode_0_optimal_endpoints[r][best_p >> 1][best_p & 1];
+	const endpoint_err *pEg = &g_bc7_mode_0_optimal_endpoints[g][best_p >> 1][best_p & 1];
+	const endpoint_err *pEb = &g_bc7_mode_0_optimal_endpoints[b][best_p >> 1][best_p & 1];
+
+	color_quad_i_set(&pResults->m_low_endpoint, pEr->m_lo, pEg->m_lo, pEb->m_lo, 0);
+
+	color_quad_i_set(&pResults->m_high_endpoint, pEr->m_hi, pEg->m_hi, pEb->m_hi, 0);
+
+	pResults->m_pbits[0] = best_p & 1;
+	pResults->m_pbits[1] = best_p >> 1;
+
+	for ( uint32_t i = 0; i < num_pixels; i++)
+		pSelectors[i] = BC7E_MODE_0_OPTIMAL_INDEX;
+
+	color_quad_i p;
+	
+	for ( uint32_t i = 0; i < 3; i++)
+	{
+		uint32_t low = ((pResults->m_low_endpoint.m_c[i] << 1) | pResults->m_pbits[0]) << 3;
+		low |= (low >> 5);
+
+		uint32_t high = ((pResults->m_high_endpoint.m_c[i] << 1) | pResults->m_pbits[1]) << 3;
+		high |= (high >> 5);
+
+		p.m_c[i] = (low * (64 - g_bc7_weights3[BC7E_MODE_0_OPTIMAL_INDEX]) + high * g_bc7_weights3[BC7E_MODE_0_OPTIMAL_INDEX] + 32) >> 6;
+	}
+	
+	p.m_c[3] = 255;
+
+	uint64_t total_err = 0;
+	for ( uint32_t i = 0; i < num_pixels; i++)
+		total_err += compute_color_distance_rgb(&p, &pPixels[i], pParams->m_perceptual, pParams->m_weights);
+
+	pResults->m_best_overall_err = total_err;
+
+	return total_err;
+}
+
+static uint64_t pack_mode6_to_one_color(const  color_cell_compressor_params * pParams,  color_cell_compressor_results * pResults, uint32_t r, uint32_t g, uint32_t b, uint32_t a,
+	 int * pSelectors, uint32_t num_pixels, const  color_quad_i * pPixels)
+{
+	uint32_t best_err = UINT_MAX;
+	uint32_t best_p = 0;
+
+	for ( uint32_t p = 0; p < 4; p++)
+	{
+		 uint32_t hi_p = p >> 1;
+		 uint32_t lo_p = p & 1;
+		uint32_t err = g_bc7_mode_6_optimal_endpoints[r][hi_p][lo_p].m_error + g_bc7_mode_6_optimal_endpoints[g][hi_p][lo_p].m_error + g_bc7_mode_6_optimal_endpoints[b][hi_p][lo_p].m_error + g_bc7_mode_6_optimal_endpoints[a][hi_p][lo_p].m_error;
+		if (err < best_err)
+		{
+			best_err = err;
+			best_p = p;
+		}
+	}
+
+	uint32_t best_hi_p = best_p >> 1;
+	uint32_t best_lo_p = best_p & 1;
+
+	const endpoint_err *pEr = &g_bc7_mode_6_optimal_endpoints[r][best_hi_p][best_lo_p];
+	const endpoint_err *pEg = &g_bc7_mode_6_optimal_endpoints[g][best_hi_p][best_lo_p];
+	const endpoint_err *pEb = &g_bc7_mode_6_optimal_endpoints[b][best_hi_p][best_lo_p];
+	const endpoint_err *pEa = &g_bc7_mode_6_optimal_endpoints[a][best_hi_p][best_lo_p];
+
+	color_quad_i_set(&pResults->m_low_endpoint, pEr->m_lo, pEg->m_lo, pEb->m_lo, pEa->m_lo);
+
+	color_quad_i_set(&pResults->m_high_endpoint, pEr->m_hi, pEg->m_hi, pEb->m_hi, pEa->m_hi);
+
+	pResults->m_pbits[0] = best_lo_p;
+	pResults->m_pbits[1] = best_hi_p;
+
+	for ( uint32_t i = 0; i < num_pixels; i++)
+		pSelectors[i] = BC7E_MODE_6_OPTIMAL_INDEX;
+
+	color_quad_i p;
+	
+	for ( uint32_t i = 0; i < 4; i++)
+	{
+		uint32_t low = (pResults->m_low_endpoint.m_c[i] << 1) | pResults->m_pbits[0];
+		uint32_t high = (pResults->m_high_endpoint.m_c[i] << 1) | pResults->m_pbits[1];
+		
+		p.m_c[i] = (low * (64 - g_bc7_weights4[BC7E_MODE_6_OPTIMAL_INDEX]) + high * g_bc7_weights4[BC7E_MODE_6_OPTIMAL_INDEX] + 32) >> 6;
+	}
+
+	uint64_t total_err = 0;
+	for ( uint32_t i = 0; i < num_pixels; i++)
+		total_err += compute_color_distance_rgba(&p, &pPixels[i], pParams->m_perceptual, pParams->m_weights);
+
+	pResults->m_best_overall_err = total_err;
+
+	return total_err;
+}
+
+static uint64_t pack_mode7_to_one_color(const  color_cell_compressor_params * pParams,  color_cell_compressor_results * pResults, uint32_t r, uint32_t g, uint32_t b, uint32_t a,
+	 int * pSelectors, uint32_t num_pixels, const  color_quad_i * pPixels)
+{
+	uint32_t best_err = UINT_MAX;
+	uint32_t best_p = 0;
+
+	for ( uint32_t p = 0; p < 4; p++)
+	{
+		 uint32_t hi_p = p >> 1;
+		 uint32_t lo_p = p & 1;
+		uint32_t err = g_bc7_mode_7_optimal_endpoints[r][hi_p][lo_p].m_error + g_bc7_mode_7_optimal_endpoints[g][hi_p][lo_p].m_error + g_bc7_mode_7_optimal_endpoints[b][hi_p][lo_p].m_error + g_bc7_mode_7_optimal_endpoints[a][hi_p][lo_p].m_error;
+		if (err < best_err)
+		{
+			best_err = err;
+			best_p = p;
+		}
+	}
+
+	uint32_t best_hi_p = best_p >> 1;
+	uint32_t best_lo_p = best_p & 1;
+
+	const endpoint_err *pEr = &g_bc7_mode_7_optimal_endpoints[r][best_hi_p][best_lo_p];
+	const endpoint_err *pEg = &g_bc7_mode_7_optimal_endpoints[g][best_hi_p][best_lo_p];
+	const endpoint_err *pEb = &g_bc7_mode_7_optimal_endpoints[b][best_hi_p][best_lo_p];
+	const endpoint_err *pEa = &g_bc7_mode_7_optimal_endpoints[a][best_hi_p][best_lo_p];
+
+	color_quad_i_set(&pResults->m_low_endpoint, pEr->m_lo, pEg->m_lo, pEb->m_lo, pEa->m_lo);
+
+	color_quad_i_set(&pResults->m_high_endpoint, pEr->m_hi, pEg->m_hi, pEb->m_hi, pEa->m_hi);
+
+	pResults->m_pbits[0] = best_lo_p;
+	pResults->m_pbits[1] = best_hi_p;
+
+	for ( uint32_t i = 0; i < num_pixels; i++)
+		pSelectors[i] = BC7E_MODE_7_OPTIMAL_INDEX;
+
+	color_quad_i p;
+	
+	for ( uint32_t i = 0; i < 4; i++)
+	{
+		uint32_t low = (pResults->m_low_endpoint.m_c[i] << 1) | pResults->m_pbits[0];
+		uint32_t high = (pResults->m_high_endpoint.m_c[i] << 1) | pResults->m_pbits[1];
+		
+		p.m_c[i] = (low * (64 - g_bc7_weights2[BC7E_MODE_7_OPTIMAL_INDEX]) + high * g_bc7_weights2[BC7E_MODE_7_OPTIMAL_INDEX] + 32) >> 6;
+	}
+
+	uint64_t total_err = 0;
+	for ( uint32_t i = 0; i < num_pixels; i++)
+		total_err += compute_color_distance_rgba(&p, &pPixels[i], pParams->m_perceptual, pParams->m_weights);
+
+	pResults->m_best_overall_err = total_err;
+
+	return total_err;
+}
+
+static uint64_t evaluate_solution(const  color_quad_i * pLow, const  color_quad_i * pHigh, const  uint32_t * pbits, 
+	const  color_cell_compressor_params * pParams,  color_cell_compressor_results * pResults, uint32_t num_pixels, const  color_quad_i * pPixels)
+{
+	color_quad_i quantMinColor = *pLow;
+	color_quad_i quantMaxColor = *pHigh;
+
+	if (pParams->m_has_pbits)
+	{
+		uint32_t minPBit, maxPBit;
+
+		if (pParams->m_endpoints_share_pbit)
+			maxPBit = minPBit = pbits[0];
+		else
+		{
+			minPBit = pbits[0];
+			maxPBit = pbits[1];
+		}
+
+		quantMinColor.m_c[0] = (pLow->m_c[0] << 1) | minPBit;
+		quantMinColor.m_c[1] = (pLow->m_c[1] << 1) | minPBit;
+		quantMinColor.m_c[2] = (pLow->m_c[2] << 1) | minPBit;
+		quantMinColor.m_c[3] = (pLow->m_c[3] << 1) | minPBit;
+
+		quantMaxColor.m_c[0] = (pHigh->m_c[0] << 1) | maxPBit;
+		quantMaxColor.m_c[1] = (pHigh->m_c[1] << 1) | maxPBit;
+		quantMaxColor.m_c[2] = (pHigh->m_c[2] << 1) | maxPBit;
+		quantMaxColor.m_c[3] = (pHigh->m_c[3] << 1) | maxPBit;
+	}
+
+	color_quad_i actualMinColor = scale_color(&quantMinColor, pParams);
+	color_quad_i actualMaxColor = scale_color(&quantMaxColor, pParams);
+
+	const  uint32_t N = pParams->m_num_selector_weights;
+	const  uint32_t nc = pParams->m_has_alpha ? 4 : 3;		
+
+	float total_errf = 0;
+
+	float wr = (float)(pParams->m_weights[0]);
+	float wg = (float)(pParams->m_weights[1]);
+	float wb = (float)(pParams->m_weights[2]);
+	float wa = (float)(pParams->m_weights[3]);
+
+	color_quad_f weightedColors[16];
+	weightedColors[0].m_c[0] = (float)(actualMinColor.m_c[0]);
+	weightedColors[0].m_c[1] = (float)(actualMinColor.m_c[1]);
+	weightedColors[0].m_c[2] = (float)(actualMinColor.m_c[2]);
+	weightedColors[0].m_c[3] = (float)(actualMinColor.m_c[3]);
+
+	weightedColors[N - 1].m_c[0] = (float)(actualMaxColor.m_c[0]);
+	weightedColors[N - 1].m_c[1] = (float)(actualMaxColor.m_c[1]);
+	weightedColors[N - 1].m_c[2] = (float)(actualMaxColor.m_c[2]);
+	weightedColors[N - 1].m_c[3] = (float)(actualMaxColor.m_c[3]);
+		
+	for ( uint32_t i = 1; i < (N - 1); i++)
+		for ( uint32_t j = 0; j < nc; j++)
+			weightedColors[i].m_c[j] = floor((weightedColors[0].m_c[j] * (64.0f - pParams->m_pSelector_weights[i]) + weightedColors[N - 1].m_c[j] * pParams->m_pSelector_weights[i] + 32) * (1.0f / 64.0f));
+
+	if (!pParams->m_perceptual)
+	{
+		if (!pParams->m_has_alpha)
+		{
+			if (N == 16)
+			{
+				float lr = (float)(actualMinColor.m_c[0]);
+				float lg = (float)(actualMinColor.m_c[1]);
+				float lb = (float)(actualMinColor.m_c[2]);
+
+				float dr = actualMaxColor.m_c[0] - lr;
+				float dg = actualMaxColor.m_c[1] - lg;
+				float db = actualMaxColor.m_c[2] - lb;
+			
+				const float f = N / (dr * dr + dg * dg + db * db + BC7E_DENOM_BIAS);
+
+				lr *= -dr;
+				lg *= -dg;
+				lb *= -db;
+
+				cfor ( uint32_t i = 0; i < num_pixels; i++)
+				{
+					const  color_quad_i * pC = &pPixels[i];
+					float r = (float)(pC->m_c[0]);
+					float g = (float)(pC->m_c[1]);
+					float b = (float)(pC->m_c[2]);
+
+					float best_sel = floor(((r * dr + lr) + (g * dg + lg) + (b * db + lb)) * f + .5f);
+					best_sel = clamp(best_sel, (float)1, (float)(N - 1));
+
+					float best_sel0 = best_sel - 1;
+
+					float dr0 = weightedColors[(int)best_sel0].m_c[0] - r;
+
+					float dg0 = weightedColors[(int)best_sel0].m_c[1] - g;
+
+					float db0 = weightedColors[(int)best_sel0].m_c[2] - b;
+
+					float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0;
+
+					float dr1 = weightedColors[(int)best_sel].m_c[0] - r;
+
+					float dg1 = weightedColors[(int)best_sel].m_c[1] - g;
+
+					float db1 = weightedColors[(int)best_sel].m_c[2] - b;
+
+					float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1;
+
+					float min_err = min(err0, err1);
+					total_errf += min_err;
+					pResults->m_pSelectors_temp[i] = (int)select(min_err == err0, best_sel0, best_sel);
+				}
+			}
+			else if (N == 8)
+			{
+				cfor ( uint32_t i = 0; i < num_pixels; i++)
+				{
+					float pr = (float)pPixels[i].m_c[0];
+					float pg = (float)pPixels[i].m_c[1];
+					float pb = (float)pPixels[i].m_c[2];
+				
+					float best_err;
+					int best_sel;
+
+					{
+						float dr0 = weightedColors[0].m_c[0] - pr;
+						float dg0 = weightedColors[0].m_c[1] - pg;
+						float db0 = weightedColors[0].m_c[2] - pb;
+						float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0;
+
+						float dr1 = weightedColors[1].m_c[0] - pr;
+						float dg1 = weightedColors[1].m_c[1] - pg;
+						float db1 = weightedColors[1].m_c[2] - pb;
+						float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1;
+
+						float dr2 = weightedColors[2].m_c[0] - pr;
+						float dg2 = weightedColors[2].m_c[1] - pg;
+						float db2 = weightedColors[2].m_c[2] - pb;
+						float err2 = wr * dr2 * dr2 + wg * dg2 * dg2 + wb * db2 * db2;
+
+						float dr3 = weightedColors[3].m_c[0] - pr;
+						float dg3 = weightedColors[3].m_c[1] - pg;
+						float db3 = weightedColors[3].m_c[2] - pb;
+						float err3 = wr * dr3 * dr3 + wg * dg3 * dg3 + wb * db3 * db3;
+
+						best_err = min(min(min(err0, err1), err2), err3);
+									
+						best_sel = select(best_err == err1, 1, 0);
+						best_sel = select(best_err == err2, 2, best_sel);
+						best_sel = select(best_err == err3, 3, best_sel);
+					}
+
+					{
+						float dr0 = weightedColors[4].m_c[0] - pr;
+						float dg0 = weightedColors[4].m_c[1] - pg;
+						float db0 = weightedColors[4].m_c[2] - pb;
+						float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0;
+
+						float dr1 = weightedColors[5].m_c[0] - pr;
+						float dg1 = weightedColors[5].m_c[1] - pg;
+						float db1 = weightedColors[5].m_c[2] - pb;
+						float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1;
+
+						float dr2 = weightedColors[6].m_c[0] - pr;
+						float dg2 = weightedColors[6].m_c[1] - pg;
+						float db2 = weightedColors[6].m_c[2] - pb;
+						float err2 = wr * dr2 * dr2 + wg * dg2 * dg2 + wb * db2 * db2;
+
+						float dr3 = weightedColors[7].m_c[0] - pr;
+						float dg3 = weightedColors[7].m_c[1] - pg;
+						float db3 = weightedColors[7].m_c[2] - pb;
+						float err3 = wr * dr3 * dr3 + wg * dg3 * dg3 + wb * db3 * db3;
+
+						best_err = min(best_err, min(min(min(err0, err1), err2), err3));
+
+						best_sel = select(best_err == err0, 4, best_sel);
+						best_sel = select(best_err == err1, 5, best_sel);
+						best_sel = select(best_err == err2, 6, best_sel);
+						best_sel = select(best_err == err3, 7, best_sel);
+					}
+				
+					total_errf += best_err;
+
+					pResults->m_pSelectors_temp[i] = best_sel;
+				}
+			}
+			else // if (N == 4)
+			{
+				cfor ( uint32_t i = 0; i < num_pixels; i++)
+				{
+					float pr = (float)pPixels[i].m_c[0];
+					float pg = (float)pPixels[i].m_c[1];
+					float pb = (float)pPixels[i].m_c[2];
+				
+					float dr0 = weightedColors[0].m_c[0] - pr;
+					float dg0 = weightedColors[0].m_c[1] - pg;
+					float db0 = weightedColors[0].m_c[2] - pb;
+					float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0;
+
+					float dr1 = weightedColors[1].m_c[0] - pr;
+					float dg1 = weightedColors[1].m_c[1] - pg;
+					float db1 = weightedColors[1].m_c[2] - pb;
+					float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1;
+
+					float dr2 = weightedColors[2].m_c[0] - pr;
+					float dg2 = weightedColors[2].m_c[1] - pg;
+					float db2 = weightedColors[2].m_c[2] - pb;
+					float err2 = wr * dr2 * dr2 + wg * dg2 * dg2 + wb * db2 * db2;
+
+					float dr3 = weightedColors[3].m_c[0] - pr;
+					float dg3 = weightedColors[3].m_c[1] - pg;
+					float db3 = weightedColors[3].m_c[2] - pb;
+					float err3 = wr * dr3 * dr3 + wg * dg3 * dg3 + wb * db3 * db3;
+
+					float best_err = min(min(min(err0, err1), err2), err3);
+
+					int best_sel = select(best_err == err1, 1, 0);
+					best_sel = select(best_err == err2, 2, best_sel);
+					best_sel = select(best_err == err3, 3, best_sel);
+								
+					total_errf += best_err;
+
+					pResults->m_pSelectors_temp[i] = best_sel;
+				}
+			}
+		}
+		else
+		{
+			// alpha
+			if (N == 16)
+			{
+				float lr = (float)(actualMinColor.m_c[0]);
+				float lg = (float)(actualMinColor.m_c[1]);
+				float lb = (float)(actualMinColor.m_c[2]);
+				float la = (float)(actualMinColor.m_c[3]);
+
+				float dr = actualMaxColor.m_c[0] - lr;
+				float dg = actualMaxColor.m_c[1] - lg;
+				float db = actualMaxColor.m_c[2] - lb;
+				float da = actualMaxColor.m_c[3] - la;
+			
+				const float f = N / (dr * dr + dg * dg + db * db + da * da + BC7E_DENOM_BIAS);
+
+				lr *= -dr;
+				lg *= -dg;
+				lb *= -db;
+				la *= -da;
+
+				cfor ( uint32_t i = 0; i < num_pixels; i++)
+				{
+					const  color_quad_i * pC = &pPixels[i];
+					float r = (float)(pC->m_c[0]);
+					float g = (float)(pC->m_c[1]);
+					float b = (float)(pC->m_c[2]);
+					float a = (float)(pC->m_c[3]);
+
+					float best_sel = floor(((r * dr + lr) + (g * dg + lg) + (b * db + lb) + (a * da + la)) * f + .5f);
+					best_sel = clamp(best_sel, (float)1, (float)(N - 1));
+
+					float best_sel0 = best_sel - 1;
+
+					float dr0 = weightedColors[(int)best_sel0].m_c[0] - r;
+					float dg0 = weightedColors[(int)best_sel0].m_c[1] - g;
+					float db0 = weightedColors[(int)best_sel0].m_c[2] - b;
+					float da0 = weightedColors[(int)best_sel0].m_c[3] - a;
+					float err0 = (wr * dr0 * dr0) + (wg * dg0 * dg0) + (wb * db0 * db0) + (wa * da0 * da0);
+
+					float dr1 = weightedColors[(int)best_sel].m_c[0] - r;
+					float dg1 = weightedColors[(int)best_sel].m_c[1] - g;
+					float db1 = weightedColors[(int)best_sel].m_c[2] - b;
+					float da1 = weightedColors[(int)best_sel].m_c[3] - a;
+
+					float err1 = (wr * dr1 * dr1) + (wg * dg1 * dg1) + (wb * db1 * db1) + (wa * da1 * da1);
+
+					float min_err = min(err0, err1);
+					total_errf += min_err;
+					pResults->m_pSelectors_temp[i] = (int)select(min_err == err0, best_sel0, best_sel);
+				}
+			}
+			else if (N == 8)
+			{
+				cfor ( uint32_t i = 0; i < num_pixels; i++)
+				{
+					float pr = (float)pPixels[i].m_c[0];
+					float pg = (float)pPixels[i].m_c[1];
+					float pb = (float)pPixels[i].m_c[2];
+					float pa = (float)pPixels[i].m_c[3];
+				
+					float best_err;
+					int best_sel;
+
+					{
+						float dr0 = weightedColors[0].m_c[0] - pr;
+						float dg0 = weightedColors[0].m_c[1] - pg;
+						float db0 = weightedColors[0].m_c[2] - pb;
+						float da0 = weightedColors[0].m_c[3] - pa;
+						float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0 + wa * da0 * da0;
+
+						float dr1 = weightedColors[1].m_c[0] - pr;
+						float dg1 = weightedColors[1].m_c[1] - pg;
+						float db1 = weightedColors[1].m_c[2] - pb;
+						float da1 = weightedColors[1].m_c[3] - pa;
+						float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1 + wa * da1 * da1;
+
+						float dr2 = weightedColors[2].m_c[0] - pr;
+						float dg2 = weightedColors[2].m_c[1] - pg;
+						float db2 = weightedColors[2].m_c[2] - pb;
+						float da2 = weightedColors[2].m_c[3] - pa;
+						float err2 = wr * dr2 * dr2 + wg * dg2 * dg2 + wb * db2 * db2 + wa * da2 * da2;
+
+						float dr3 = weightedColors[3].m_c[0] - pr;
+						float dg3 = weightedColors[3].m_c[1] - pg;
+						float db3 = weightedColors[3].m_c[2] - pb;
+						float da3 = weightedColors[3].m_c[3] - pa;
+						float err3 = wr * dr3 * dr3 + wg * dg3 * dg3 + wb * db3 * db3 + wa * da3 * da3;
+
+						best_err = min(min(min(err0, err1), err2), err3);
+									
+						best_sel = select(best_err == err1, 1, 0);
+						best_sel = select(best_err == err2, 2, best_sel);
+						best_sel = select(best_err == err3, 3, best_sel);
+					}
+
+					{
+						float dr0 = weightedColors[4].m_c[0] - pr;
+						float dg0 = weightedColors[4].m_c[1] - pg;
+						float db0 = weightedColors[4].m_c[2] - pb;
+						float da0 = weightedColors[4].m_c[3] - pa;
+						float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0 + wa * da0 * da0;
+
+						float dr1 = weightedColors[5].m_c[0] - pr;
+						float dg1 = weightedColors[5].m_c[1] - pg;
+						float db1 = weightedColors[5].m_c[2] - pb;
+						float da1 = weightedColors[5].m_c[3] - pa;
+						float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1 + wa * da1 * da1;
+
+						float dr2 = weightedColors[6].m_c[0] - pr;
+						float dg2 = weightedColors[6].m_c[1] - pg;
+						float db2 = weightedColors[6].m_c[2] - pb;
+						float da2 = weightedColors[6].m_c[3] - pa;
+						float err2 = wr * dr2 * dr2 + wg * dg2 * dg2 + wb * db2 * db2 + wa * da2 * da2;
+
+						float dr3 = weightedColors[7].m_c[0] - pr;
+						float dg3 = weightedColors[7].m_c[1] - pg;
+						float db3 = weightedColors[7].m_c[2] - pb;
+						float da3 = weightedColors[7].m_c[3] - pa;
+						float err3 = wr * dr3 * dr3 + wg * dg3 * dg3 + wb * db3 * db3 + wa * da3 * da3;
+
+						best_err = min(best_err, min(min(min(err0, err1), err2), err3));
+
+						best_sel = select(best_err == err0, 4, best_sel);
+						best_sel = select(best_err == err1, 5, best_sel);
+						best_sel = select(best_err == err2, 6, best_sel);
+						best_sel = select(best_err == err3, 7, best_sel);
+					}
+				
+					total_errf += best_err;
+
+					pResults->m_pSelectors_temp[i] = best_sel;
+				}
+			}
+			else // if (N == 4)
+			{
+				cfor ( uint32_t i = 0; i < num_pixels; i++)
+				{
+					float pr = (float)pPixels[i].m_c[0];
+					float pg = (float)pPixels[i].m_c[1];
+					float pb = (float)pPixels[i].m_c[2];
+					float pa = (float)pPixels[i].m_c[3];
+				
+					float dr0 = weightedColors[0].m_c[0] - pr;
+					float dg0 = weightedColors[0].m_c[1] - pg;
+					float db0 = weightedColors[0].m_c[2] - pb;
+					float da0 = weightedColors[0].m_c[3] - pa;
+					float err0 = wr * dr0 * dr0 + wg * dg0 * dg0 + wb * db0 * db0 + wa * da0 * da0;
+
+					float dr1 = weightedColors[1].m_c[0] - pr;
+					float dg1 = weightedColors[1].m_c[1] - pg;
+					float db1 = weightedColors[1].m_c[2] - pb;
+					float da1 = weightedColors[1].m_c[3] - pa;
+					float err1 = wr * dr1 * dr1 + wg * dg1 * dg1 + wb * db1 * db1 + wa * da1 * da1;
+
+					float dr2 = weightedColors[2].m_c[0] - pr;
+					float dg2 = weightedColors[2].m_c[1] - pg;
+					float db2 = weightedColors[2].m_c[2] - pb;
+					float da2 = weightedColors[2].m_c[3] - pa;
+					float err2 = wr * dr2 * dr2 + wg * dg2 * dg2 + wb * db2 * db2 + wa * da2 * da2;
+
+					float dr3 = weightedColors[3].m_c[0] - pr;
+					float dg3 = weightedColors[3].m_c[1] - pg;
+					float db3 = weightedColors[3].m_c[2] - pb;
+					float da3 = weightedColors[3].m_c[3] - pa;
+					float err3 = wr * dr3 * dr3 + wg * dg3 * dg3 + wb * db3 * db3 + wa * da3 * da3;
+
+					float best_err = min(min(min(err0, err1), err2), err3);
+
+					int best_sel = select(best_err == err1, 1, 0);
+					best_sel = select(best_err == err2, 2, best_sel);
+					best_sel = select(best_err == err3, 3, best_sel);
+								
+					total_errf += best_err;
+
+					pResults->m_pSelectors_temp[i] = best_sel;
+				}
+			}
+		}
+	}
+	else
+	{
+		wg *= pr_weight;
+		wb *= pb_weight;
+
+		float weightedColorsY[16], weightedColorsCr[16], weightedColorsCb[16];
+		
+		for ( uint32_t i = 0; i < N; i++)
+		{
+			float r = weightedColors[i].m_c[0];
+			float g = weightedColors[i].m_c[1];
+			float b = weightedColors[i].m_c[2];
+
+			float y = r * .2126f + g * .7152f + b * .0722f;
+									
+			weightedColorsY[i] = y;
+			weightedColorsCr[i] = r - y;
+			weightedColorsCb[i] = b - y;
+		}
+
+		if (pParams->m_has_alpha)
+		{
+			for ( uint32_t i = 0; i < num_pixels; i++)
+			{
+				float r = (float)(pPixels[i].m_c[0]);
+				float g = (float)(pPixels[i].m_c[1]);
+				float b = (float)(pPixels[i].m_c[2]);
+				float a = (float)(pPixels[i].m_c[3]);
+
+				float y = r * .2126f + g * .7152f + b * .0722f;
+				float cr = r - y;
+				float cb = b - y;
+
+				float best_err = 1e+10f;
+				int32_t best_sel = 0;
+								
+				for ( uint32_t j = 0; j < N; j++)
+				{
+					float dl = y - weightedColorsY[j];
+					float dcr = cr - weightedColorsCr[j];
+					float dcb = cb - weightedColorsCb[j];
+					float da = a - weightedColors[j].m_c[3];
+
+					float err = (wr * dl * dl) + (wg * dcr * dcr) + (wb * dcb * dcb) + (wa * da * da);
+					if (err < best_err)
+					{
+						best_err = err;
+						best_sel = j;
+					}
+				}
+				
+				total_errf += best_err;
+
+				pResults->m_pSelectors_temp[i] = best_sel;
+			}
+		}
+		else
+		{
+			for ( uint32_t i = 0; i < num_pixels; i++)
+			{
+				float r = (float)(pPixels[i].m_c[0]);
+				float g = (float)(pPixels[i].m_c[1]);
+				float b = (float)(pPixels[i].m_c[2]);
+
+				float y = r * .2126f + g * .7152f + b * .0722f;
+				float cr = r - y;
+				float cb = b - y;
+
+				float best_err = 1e+10f;
+				int32_t best_sel = 0;
+								
+				for ( uint32_t j = 0; j < N; j++)
+				{
+					float dl = y - weightedColorsY[j];
+					float dcr = cr - weightedColorsCr[j];
+					float dcb = cb - weightedColorsCb[j];
+
+					float err = (wr * dl * dl) + (wg * dcr * dcr) + (wb * dcb * dcb);
+					if (err < best_err)
+					{
+						best_err = err;
+						best_sel = j;
+					}
+				}
+				
+				total_errf += best_err;
+
+				pResults->m_pSelectors_temp[i] = best_sel;
+			}
+		}
+	}
+
+	uint64_t total_err = (int64)total_errf;
+
+	if (total_err < pResults->m_best_overall_err)
+	{
+		pResults->m_best_overall_err = total_err;
+
+		pResults->m_low_endpoint = *pLow;
+		pResults->m_high_endpoint = *pHigh;
+
+		pResults->m_pbits[0] = pbits[0];
+		pResults->m_pbits[1] = pbits[1];
+
+		for ( uint32_t i = 0; i < num_pixels; i++)
+			pResults->m_pSelectors[i] = pResults->m_pSelectors_temp[i];
+	}
+				
+	return total_err;
+}
+
+static void fixDegenerateEndpoints( uint32_t mode,  color_quad_i * pTrialMinColor,  color_quad_i * pTrialMaxColor, const  vec4F * pXl, const  vec4F * pXh,  uint32_t iscale)
+{
+	if ((mode == 1) || (mode == 4)) // also mode 2
+	{
+		// fix degenerate case where the input collapses to a single colorspace voxel, and we loose all freedom (test with grayscale ramps)
+		for ( uint32_t i = 0; i < 3; i++)
+		{
+			if (pTrialMinColor->m_c[i] == pTrialMaxColor->m_c[i])
+			{
+				if (abs(pXl->m_c[i] - pXh->m_c[i]) > 0.0f)
+				{
+					if (pTrialMinColor->m_c[i] > (int)(iscale >> 1))
+					{
+						if (pTrialMinColor->m_c[i] > 0)
+							pTrialMinColor->m_c[i]--;
+						else
+							if (pTrialMaxColor->m_c[i] < (int)iscale)
+								pTrialMaxColor->m_c[i]++;
+					}
+					else
+					{
+						if (pTrialMaxColor->m_c[i] < (int)iscale)
+							pTrialMaxColor->m_c[i]++;
+						else if (pTrialMinColor->m_c[i] > 0)
+							pTrialMinColor->m_c[i]--;
+					}
+
+					if (mode == 4)
+					{
+						if (pTrialMinColor->m_c[i] > (int)(iscale >> 1))
+						{
+							if (pTrialMaxColor->m_c[i] < (int)iscale)
+								pTrialMaxColor->m_c[i]++;
+							else if (pTrialMinColor->m_c[i] > 0)
+								pTrialMinColor->m_c[i]--;
+						}
+						else
+						{
+							if (pTrialMinColor->m_c[i] > 0)
+								pTrialMinColor->m_c[i]--;
+							else if (pTrialMaxColor->m_c[i] < (int)iscale)
+								pTrialMaxColor->m_c[i]++;
+						}
+					}
+				}
+			}
+		}
+	}
+}
+
+static uint64_t find_optimal_solution( uint32_t mode,  vec4F * pXl,  vec4F * pXh, const  color_cell_compressor_params * pParams,  color_cell_compressor_results * pResults, 
+	 bool pbit_search, uint32_t num_pixels, const  color_quad_i * pPixels)
+{
+	vec4F xl = *pXl;
+	vec4F xh = *pXh;
+
+	vec4F_saturate_in_place(&xl);
+	vec4F_saturate_in_place(&xh);
+		
+	if (pParams->m_has_pbits)
+	{
+		if (pbit_search)
+		{
+			// compensated rounding+pbit search
+			const  int iscalep = (1 << (pParams->m_comp_bits + 1)) - 1;
+			const  float scalep = (float)iscalep;
+
+			const  int32_t totalComps = pParams->m_has_alpha ? 4 : 3;
+			NOTE_UNUSED(totalComps);
+
+			if (!pParams->m_endpoints_share_pbit)
+			{
+				color_quad_i lo[2], hi[2];
+								
+				for ( int p = 0; p < 2; p++)
+				{
+					color_quad_i xMinColor, xMaxColor;
+
+					// Notes: The pbit controls which quantization intervals are selected.
+					// total_levels=2^(comp_bits+1), where comp_bits=4 for mode 0, etc.
+					// pbit 0: v=(b*2)/(total_levels-1), pbit 1: v=(b*2+1)/(total_levels-1) where b is the component bin from [0,total_levels/2-1] and v is the [0,1] component value
+					// rearranging you get for pbit 0: b=floor(v*(total_levels-1)/2+.5)
+					// rearranging you get for pbit 1: b=floor((v*(total_levels-1)-1)/2+.5)
+					for ( uint32_t c = 0; c < 4; c++)
+					{
+						xMinColor.m_c[c] = (int)((xl.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p;
+						xMinColor.m_c[c] = clamp(xMinColor.m_c[c], p, iscalep - 1 + p);
+
+						xMaxColor.m_c[c] = (int)((xh.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p;
+						xMaxColor.m_c[c] = clamp(xMaxColor.m_c[c], p, iscalep - 1 + p);
+					}
+																				
+					lo[p] = xMinColor;
+					hi[p] = xMaxColor;
+
+					for ( int c = 0; c < 4; c++)
+					{
+						lo[p].m_c[c] >>= 1;
+						hi[p].m_c[c] >>= 1;
+					}
+				}
+
+				fixDegenerateEndpoints(mode, &lo[0], &hi[0], &xl, &xh, iscalep >> 1);
+				fixDegenerateEndpoints(mode, &lo[1], &hi[1], &xl, &xh, iscalep >> 1);
+
+				uint32_t pbits[2];
+				
+				pbits[0] = 0; pbits[1] = 0;
+				evaluate_solution(&lo[0], &hi[0], pbits, pParams, pResults, num_pixels, pPixels);
+
+				pbits[0] = 0; pbits[1] = 1;
+				evaluate_solution(&lo[0], &hi[1], pbits, pParams, pResults, num_pixels, pPixels);
+
+				pbits[0] = 1; pbits[1] = 0;
+				evaluate_solution(&lo[1], &hi[0], pbits, pParams, pResults, num_pixels, pPixels);
+
+				pbits[0] = 1; pbits[1] = 1;
+				evaluate_solution(&lo[1], &hi[1], pbits, pParams, pResults, num_pixels, pPixels);
+			}
+			else
+			{
+				// Endpoints share pbits
+				color_quad_i lo[2], hi[2];
+
+				for ( int p = 0; p < 2; p++)
+				{
+					color_quad_i xMinColor, xMaxColor;
+								
+					for ( uint32_t c = 0; c < 4; c++)
+					{
+						xMinColor.m_c[c] = (int)((xl.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p;
+						xMinColor.m_c[c] = clamp(xMinColor.m_c[c], p, iscalep - 1 + p);
+
+						xMaxColor.m_c[c] = (int)((xh.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p;
+						xMaxColor.m_c[c] = clamp(xMaxColor.m_c[c], p, iscalep - 1 + p);
+					}
+										
+					lo[p] = xMinColor;
+					hi[p] = xMaxColor;
+
+					for ( int c = 0; c < 4; c++)
+					{
+						lo[p].m_c[c] >>= 1;
+						hi[p].m_c[c] >>= 1;
+					}
+				}
+
+				fixDegenerateEndpoints(mode, &lo[0], &hi[0], &xl, &xh, iscalep >> 1);
+				fixDegenerateEndpoints(mode, &lo[1], &hi[1], &xl, &xh, iscalep >> 1);
+				
+				uint32_t pbits[2];
+				
+				pbits[0] = 0; pbits[1] = 0;
+				evaluate_solution(&lo[0], &hi[0], pbits, pParams, pResults, num_pixels, pPixels);
+
+				pbits[0] = 1; pbits[1] = 1;
+				evaluate_solution(&lo[1], &hi[1], pbits, pParams, pResults, num_pixels, pPixels);
+			}
+		}
+		else
+		{
+			// compensated rounding
+			const  int iscalep = (1 << (pParams->m_comp_bits + 1)) - 1;
+			const  float scalep = (float)iscalep;
+
+			const  int32_t totalComps = pParams->m_has_alpha ? 4 : 3;
+
+			uint32_t best_pbits[2];
+			color_quad_i bestMinColor, bestMaxColor;
+						
+			if (!pParams->m_endpoints_share_pbit)
+			{
+				float best_err0 = 1e+9;
+				float best_err1 = 1e+9;
+								
+				for ( int p = 0; p < 2; p++)
+				{
+					color_quad_i xMinColor, xMaxColor;
+
+					// Notes: The pbit controls which quantization intervals are selected.
+					// total_levels=2^(comp_bits+1), where comp_bits=4 for mode 0, etc.
+					// pbit 0: v=(b*2)/(total_levels-1), pbit 1: v=(b*2+1)/(total_levels-1) where b is the component bin from [0,total_levels/2-1] and v is the [0,1] component value
+					// rearranging you get for pbit 0: b=floor(v*(total_levels-1)/2+.5)
+					// rearranging you get for pbit 1: b=floor((v*(total_levels-1)-1)/2+.5)
+					for ( uint32_t c = 0; c < 4; c++)
+					{
+						xMinColor.m_c[c] = (int)((xl.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p;
+						xMinColor.m_c[c] = clamp(xMinColor.m_c[c], p, iscalep - 1 + p);
+
+						xMaxColor.m_c[c] = (int)((xh.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p;
+						xMaxColor.m_c[c] = clamp(xMaxColor.m_c[c], p, iscalep - 1 + p);
+					}
+																				
+					color_quad_i scaledLow = scale_color(&xMinColor, pParams);
+					color_quad_i scaledHigh = scale_color(&xMaxColor, pParams);
+
+					float err0 = 0;
+					float err1 = 0;
+					for ( int i = 0; i < totalComps; i++)
+					{
+						err0 += square(scaledLow.m_c[i] - xl.m_c[i]*255.0f);
+						err1 += square(scaledHigh.m_c[i] - xh.m_c[i]*255.0f);
+					}
+
+					if (err0 < best_err0)
+					{
+						best_err0 = err0;
+						best_pbits[0] = p;
+						
+						bestMinColor.m_c[0] = xMinColor.m_c[0] >> 1;
+						bestMinColor.m_c[1] = xMinColor.m_c[1] >> 1;
+						bestMinColor.m_c[2] = xMinColor.m_c[2] >> 1;
+						bestMinColor.m_c[3] = xMinColor.m_c[3] >> 1;
+					}
+
+					if (err1 < best_err1)
+					{
+						best_err1 = err1;
+						best_pbits[1] = p;
+
+						bestMaxColor.m_c[0] = xMaxColor.m_c[0] >> 1;
+						bestMaxColor.m_c[1] = xMaxColor.m_c[1] >> 1;
+						bestMaxColor.m_c[2] = xMaxColor.m_c[2] >> 1;
+						bestMaxColor.m_c[3] = xMaxColor.m_c[3] >> 1;
+					}
+				}
+			}
+			else
+			{
+				// Endpoints share pbits
+				float best_err = 1e+9;
+
+				for ( int p = 0; p < 2; p++)
+				{
+					color_quad_i xMinColor, xMaxColor;
+								
+					for ( uint32_t c = 0; c < 4; c++)
+					{
+						xMinColor.m_c[c] = (int)((xl.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p;
+						xMinColor.m_c[c] = clamp(xMinColor.m_c[c], p, iscalep - 1 + p);
+
+						xMaxColor.m_c[c] = (int)((xh.m_c[c] * scalep - p) / 2.0f + .5f) * 2 + p;
+						xMaxColor.m_c[c] = clamp(xMaxColor.m_c[c], p, iscalep - 1 + p);
+					}
+										
+					color_quad_i scaledLow = scale_color(&xMinColor, pParams);
+					color_quad_i scaledHigh = scale_color(&xMaxColor, pParams);
+
+					float err = 0;
+					for ( int i = 0; i < totalComps; i++)
+						err += square((scaledLow.m_c[i]/255.0f) - xl.m_c[i]) + square((scaledHigh.m_c[i]/255.0f) - xh.m_c[i]);
+
+					if (err < best_err)
+					{
+						best_err = err;
+						best_pbits[0] = p;
+						best_pbits[1] = p;
+						
+						bestMinColor.m_c[0] = xMinColor.m_c[0] >> 1;
+						bestMinColor.m_c[1] = xMinColor.m_c[1] >> 1;
+						bestMinColor.m_c[2] = xMinColor.m_c[2] >> 1;
+						bestMinColor.m_c[3] = xMinColor.m_c[3] >> 1;
+
+						bestMaxColor.m_c[0] = xMaxColor.m_c[0] >> 1;
+						bestMaxColor.m_c[1] = xMaxColor.m_c[1] >> 1;
+						bestMaxColor.m_c[2] = xMaxColor.m_c[2] >> 1;
+						bestMaxColor.m_c[3] = xMaxColor.m_c[3] >> 1;
+					}
+				}
+			}
+
+			fixDegenerateEndpoints(mode, &bestMinColor, &bestMaxColor, &xl, &xh, iscalep >> 1);
+
+			if ((pResults->m_best_overall_err == UINT64_MAX) || color_quad_i_notequals(&bestMinColor, &pResults->m_low_endpoint) || color_quad_i_notequals(&bestMaxColor, &pResults->m_high_endpoint) || (best_pbits[0] != pResults->m_pbits[0]) || (best_pbits[1] != pResults->m_pbits[1]))
+			{
+				evaluate_solution(&bestMinColor, &bestMaxColor, best_pbits, pParams, pResults, num_pixels, pPixels);
+			}
+		}
+	}
+	else
+	{
+		const  int iscale = (1 << pParams->m_comp_bits) - 1;
+		const  float scale = (float)iscale;
+
+		color_quad_i trialMinColor, trialMaxColor;
+		color_quad_i_set_clamped(&trialMinColor, (int)(xl.m_c[0] * scale + .5f), (int)(xl.m_c[1] * scale + .5f), (int)(xl.m_c[2] * scale + .5f), (int)(xl.m_c[3] * scale + .5f));
+		color_quad_i_set_clamped(&trialMaxColor, (int)(xh.m_c[0] * scale + .5f), (int)(xh.m_c[1] * scale + .5f), (int)(xh.m_c[2] * scale + .5f), (int)(xh.m_c[3] * scale + .5f));
+
+		fixDegenerateEndpoints(mode, &trialMinColor, &trialMaxColor, &xl, &xh, iscale);
+
+		if ((pResults->m_best_overall_err == UINT64_MAX) || color_quad_i_notequals(&trialMinColor, &pResults->m_low_endpoint) || color_quad_i_notequals(&trialMaxColor, &pResults->m_high_endpoint))
+		{
+			uint32_t pbits[2];
+			pbits[0] = 0;
+			pbits[1] = 0;
+
+			evaluate_solution(&trialMinColor, &trialMaxColor, pbits, pParams, pResults, num_pixels, pPixels);
+		}
+	}
+
+	return pResults->m_best_overall_err;
+}
+
+// Note: In mode 6, m_has_alpha will only be true for transparent blocks.
+static uint64_t color_cell_compression( uint32_t mode, const  color_cell_compressor_params * pParams,  color_cell_compressor_results * pResults,
+	const  bc7e_compress_block_params * pComp_params, uint32_t num_pixels, const  color_quad_i * pPixels,  bool refinement)
+{
+	pResults->m_best_overall_err = UINT64_MAX;
+	pResults->m_used_lut = false; // set true below only if a one-color LUT result wins
+
+	if ((mode != 6) && (mode != 7))
+	{
+		assert(!pParams->m_has_alpha);
+	}
+
+	if ((mode <= 2) || (mode == 4) || (mode >= 6))
+	{
+		const uint32_t cr = pPixels[0].m_c[0];
+		const uint32_t cg = pPixels[0].m_c[1];
+		const uint32_t cb = pPixels[0].m_c[2];
+		const uint32_t ca = pPixels[0].m_c[3];
+
+		bool allSame = true;
+		for ( uint32_t i = 1; i < num_pixels; i++)
+		{
+			if ((cr != (uint32_t)pPixels[i].m_c[0]) || (cg != (uint32_t)pPixels[i].m_c[1]) || (cb != (uint32_t)pPixels[i].m_c[2]) || (ca != (uint32_t)pPixels[i].m_c[3]))
+			{
+				allSame = false;
+				break;
+			}
+		}
+
+		cif (allSame && pComp_params->m_use_luts)
+		{
+			pResults->m_used_lut = true; // solid/allSame subset -> one-color optimal-endpoint LUT
+			if (mode == 0)
+				return pack_mode0_to_one_color(pParams, pResults, cr, cg, cb, pResults->m_pSelectors, num_pixels, pPixels);
+			if (mode == 1)
+				return pack_mode1_to_one_color(pParams, pResults, cr, cg, cb, pResults->m_pSelectors, num_pixels, pPixels);
+			else if (mode == 6)
+				return pack_mode6_to_one_color(pParams, pResults, cr, cg, cb, ca, pResults->m_pSelectors, num_pixels, pPixels);
+			else if (mode == 7)
+				return pack_mode7_to_one_color(pParams, pResults, cr, cg, cb, ca, pResults->m_pSelectors, num_pixels, pPixels);
+			else
+				return pack_mode24_to_one_color(pParams, pResults, cr, cg, cb, pResults->m_pSelectors, num_pixels, pPixels);
+		}
+	}
+
+	vec4F meanColor, axis;
+	vec4F_set_scalar(&meanColor, 0.0f);
+
+	for ( uint32_t i = 0; i < num_pixels; i++)
+	{
+		vec4F color = vec4F_from_color(&pPixels[i]);
+		meanColor = vec4F_add(&meanColor, &color);
+	}
+				
+	vec4F meanColorScaled = vec4F_mul(&meanColor, 1.0f / (float)((int)num_pixels));
+
+	meanColor = vec4F_mul(&meanColor, 1.0f / (float)((int)num_pixels * 255.0f));
+	vec4F_saturate_in_place(&meanColor);
+
+	if (pParams->m_has_alpha)
+	{
+		vec4F v;
+		vec4F_set_scalar(&v, 0.0f);
+		cfor ( uint32_t i = 0; i < num_pixels; i++)
+		{
+			vec4F color = vec4F_from_color(&pPixels[i]);
+			color = vec4F_sub(&color, &meanColorScaled);
+
+			vec4F a = vec4F_mul(&color, color.m_c[0]);
+			vec4F b = vec4F_mul(&color, color.m_c[1]);
+			vec4F c = vec4F_mul(&color, color.m_c[2]);
+			vec4F d = vec4F_mul(&color, color.m_c[3]);
+
+			vec4F n = i ? v : color;
+			vec4F_normalize_in_place(&n);
+
+			v.m_c[0] += vec4F_dot(&a, &n);
+			v.m_c[1] += vec4F_dot(&b, &n);
+			v.m_c[2] += vec4F_dot(&c, &n);
+			v.m_c[3] += vec4F_dot(&d, &n);
+		}
+		axis = v;
+		vec4F_normalize_in_place(&axis);
+	}
+	else
+	{
+		float cov[6];
+		cov[0] = 0; cov[1] = 0; cov[2] = 0; 
+		cov[3] = 0; cov[4] = 0;	cov[5] = 0;
+
+		cfor ( uint32_t i = 0; i < num_pixels; i++)
+		{
+			const  color_quad_i * pV = &pPixels[i];
+
+			float r = pV->m_c[0] - meanColorScaled.m_c[0];
+			float g = pV->m_c[1] - meanColorScaled.m_c[1];
+			float b = pV->m_c[2] - meanColorScaled.m_c[2];
+				
+			cov[0] += r*r;
+			cov[1] += r*g;
+			cov[2] += r*b;
+			cov[3] += g*g;
+			cov[4] += g*b;
+			cov[5] += b*b;
+		}
+
+		float vfr, vfg, vfb;
+		//vfr = hi[0] - lo[0];
+		//vfg = hi[1] - lo[1];
+		//vfb = hi[2] - lo[2];
+		// This is more stable.
+		vfr = .9f;
+		vfg = 1.0f;
+		vfb = .7f;
+
+		for ( uint32_t iter = 0; iter < 3; iter++)
+		{
+			float r = vfr*cov[0] + vfg*cov[1] + vfb*cov[2];
+			float g = vfr*cov[1] + vfg*cov[3] + vfb*cov[4];
+			float b = vfr*cov[2] + vfg*cov[4] + vfb*cov[5];
+
+			float m = maximumf(maximumf(abs(r), abs(g)), abs(b));
+			if (m > 1e-10f)
+			{
+				m = 1.0f / m;
+				r *= m;
+				g *= m;
+				b *= m;
+			}
+
+			//float delta = square(vfr - r) + square(vfg - g) + square(vfb - b);
+
+			vfr = r;
+			vfg = g;
+			vfb = b;
+
+			//if ((iter > 1) && (delta < 1e-8f))
+			//	break;
+		}
+
+		float len = vfr*vfr + vfg*vfg + vfb*vfb;
+
+		if (len < 1e-10f)
+			vec4F_set_scalar(&axis, 0.0f);
+		else
+		{
+			len = 1.0f / sqrt(len);
+			vfr *= len;
+			vfg *= len;
+			vfb *= len;
+			vec4F_set(&axis, vfr, vfg, vfb, 0);
+		}
+	}
+
+	cif (vec4F_dot(&axis, &axis) < .5f)
+	{
+		if (pParams->m_perceptual)
+			vec4F_set(&axis, .213f, .715f, .072f, pParams->m_has_alpha ? .715f : 0);
+		else
+			vec4F_set(&axis, 1.0f, 1.0f, 1.0f, pParams->m_has_alpha ? 1.0f : 0);
+		vec4F_normalize_in_place(&axis);
+	}
+
+	float l = 1e+9f, h = -1e+9f;
+
+	cfor ( uint32_t i = 0; i < num_pixels; i++)
+	{
+		vec4F color = vec4F_from_color(&pPixels[i]);
+
+		vec4F q = vec4F_sub(&color, &meanColorScaled);
+		float d = vec4F_dot(&q, &axis);
+
+		l = minimumf(l, d);
+		h = maximumf(h, d);
+	}
+
+	l *= (1.0f / 255.0f);
+	h *= (1.0f / 255.0f);
+
+	vec4F b0 = vec4F_mul(&axis, l);
+	vec4F b1 = vec4F_mul(&axis, h);
+	vec4F c0 = vec4F_add(&meanColor, &b0);
+	vec4F c1 = vec4F_add(&meanColor, &b1);
+	vec4F minColor = vec4F_saturate(&c0);
+	vec4F maxColor = vec4F_saturate(&c1);
+				
+	vec4F whiteVec;
+	vec4F_set_scalar(&whiteVec, 1.0f);
+	if (vec4F_dot(&minColor, &whiteVec) > vec4F_dot(&maxColor, &whiteVec))
+	{
+		vec4F temp = minColor;
+		minColor = maxColor;
+		maxColor = temp;
+	}
+
+	if (!find_optimal_solution(mode, &minColor, &maxColor, pParams, pResults, pComp_params->m_pbit_search, num_pixels, pPixels))
+		return 0;
+
+	if (!refinement)
+		return pResults->m_best_overall_err;
+
+	for ( uint32_t i = 0; i < pComp_params->m_refinement_passes; i++)
+	{
+		vec4F xl, xh;
+		vec4F_set_scalar(&xl, 0.0f);
+		vec4F_set_scalar(&xh, 0.0f);
+		if (pParams->m_has_alpha)
+			compute_least_squares_endpoints_rgba(num_pixels, pResults->m_pSelectors, pParams->m_pSelector_weightsx, &xl, &xh, pPixels);
+		else
+		{
+			compute_least_squares_endpoints_rgb(num_pixels, pResults->m_pSelectors, pParams->m_pSelector_weightsx, &xl, &xh, pPixels);
+
+			xl.m_c[3] = 255.0f;
+			xh.m_c[3] = 255.0f;
+		}
+
+		xl = vec4F_mul(&xl, (1.0f / 255.0f));
+		xh = vec4F_mul(&xh, (1.0f / 255.0f));
+
+		if (!find_optimal_solution(mode, &xl, &xh, pParams, pResults, pComp_params->m_pbit_search, num_pixels, pPixels))
+			return 0;
+	}
+
+	if (pComp_params->m_uber_level > 0)
+	{
+		int selectors_temp[16], selectors_temp1[16];
+		for ( uint32_t i = 0; i < num_pixels; i++)
+			selectors_temp[i] = pResults->m_pSelectors[i];
+
+		const  int max_selector = pParams->m_num_selector_weights - 1;
+
+		uint32_t min_sel = 16;
+		uint32_t max_sel = 0;
+		for ( uint32_t i = 0; i < num_pixels; i++)
+		{
+			uint32_t sel = selectors_temp[i];
+			min_sel = minimumu(min_sel, sel);
+			max_sel = maximumu(max_sel, sel);
+		}
+
+		vec4F xl, xh;
+		vec4F_set_scalar(&xl, 0.0f);
+		vec4F_set_scalar(&xh, 0.0f);
+
+		if (pComp_params->m_uber1_mask & 1)
+		{
+			for ( uint32_t i = 0; i < num_pixels; i++)
+			{
+				uint32_t sel = selectors_temp[i];
+				if ((sel == min_sel) && (sel < (pParams->m_num_selector_weights - 1)))
+					sel++;
+				selectors_temp1[i] = sel;
+			}
+						
+			if (pParams->m_has_alpha)
+				compute_least_squares_endpoints_rgba(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels);
+			else
+			{
+				compute_least_squares_endpoints_rgb(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels);
+				xl.m_c[3] = 255.0f;
+				xh.m_c[3] = 255.0f;
+			}
+
+			xl = vec4F_mul(&xl, (1.0f / 255.0f));
+			xh = vec4F_mul(&xh, (1.0f / 255.0f));
+
+			if (!find_optimal_solution(mode, &xl, &xh, pParams, pResults, pComp_params->m_pbit_search, num_pixels, pPixels))
+				return 0;
+		}
+
+		if (pComp_params->m_uber1_mask & 2)
+		{
+			for ( uint32_t i = 0; i < num_pixels; i++)
+			{
+				uint32_t sel = selectors_temp[i];
+				if ((sel == max_sel) && (sel > 0))
+					sel--;
+				selectors_temp1[i] = sel;
+			}
+
+			if (pParams->m_has_alpha)
+				compute_least_squares_endpoints_rgba(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels);
+			else
+			{
+				compute_least_squares_endpoints_rgb(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels);
+				xl.m_c[3] = 255.0f;
+				xh.m_c[3] = 255.0f;
+			}
+
+			xl = vec4F_mul(&xl, (1.0f / 255.0f));
+			xh = vec4F_mul(&xh, (1.0f / 255.0f));
+
+			if (!find_optimal_solution(mode, &xl, &xh, pParams, pResults, pComp_params->m_pbit_search, num_pixels, pPixels))
+				return 0;
+		}
+
+		if (pComp_params->m_uber1_mask & 4)
+		{
+			for ( uint32_t i = 0; i < num_pixels; i++)
+			{
+				uint32_t sel = selectors_temp[i];
+				if ((sel == min_sel) && (sel < (pParams->m_num_selector_weights - 1)))
+					sel++;
+				else if ((sel == max_sel) && (sel > 0))
+					sel--;
+				selectors_temp1[i] = sel;
+			}
+
+			if (pParams->m_has_alpha)
+				compute_least_squares_endpoints_rgba(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels);
+			else
+			{
+				compute_least_squares_endpoints_rgb(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels);
+				xl.m_c[3] = 255.0f;
+				xh.m_c[3] = 255.0f;
+			}
+
+			xl = vec4F_mul(&xl, (1.0f / 255.0f));
+			xh = vec4F_mul(&xh, (1.0f / 255.0f));
+
+			if (!find_optimal_solution(mode, &xl, &xh, pParams, pResults, pComp_params->m_pbit_search, num_pixels, pPixels))
+				return 0;
+		}
+
+		const uint32_t uber_err_thresh = (num_pixels * 56) >> 4;
+		if ((pComp_params->m_uber_level >= 2) && (pResults->m_best_overall_err > uber_err_thresh))
+		{
+			const  int Q = (pComp_params->m_uber_level >= 4) ? (pComp_params->m_uber_level - 2) : 1;
+			for ( int ly = -Q; ly <= 1; ly++)
+			{
+				for ( int hy = max_selector - 1; hy <= (max_selector + Q); hy++)
+				{
+					if ((ly == 0) && (hy == max_selector))
+						continue;
+
+					for ( uint32_t i = 0; i < num_pixels; i++)
+						selectors_temp1[i] = (int)clampf(floor((float)max_selector * ((float)(int)selectors_temp[i] - (float)ly) / ((float)hy - (float)ly) + .5f), 0, (float)max_selector);
+
+					vec4F_set_scalar(&xl, 0.0f);
+					vec4F_set_scalar(&xh, 0.0f);
+					if (pParams->m_has_alpha)
+						compute_least_squares_endpoints_rgba(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels);
+					else
+					{
+						compute_least_squares_endpoints_rgb(num_pixels, selectors_temp1, pParams->m_pSelector_weightsx, &xl, &xh, pPixels);
+						xl.m_c[3] = 255.0f;
+						xh.m_c[3] = 255.0f;
+					}
+
+					xl = vec4F_mul(&xl, (1.0f / 255.0f));
+					xh = vec4F_mul(&xh, (1.0f / 255.0f));
+
+					if (!find_optimal_solution(mode, &xl, &xh, pParams, pResults, pComp_params->m_pbit_search && (pComp_params->m_uber_level >= 2), num_pixels, pPixels))
+						return 0;
+				}
+			}
+		}
+	}
+
+	if (((mode <= 2) || (mode == 4) || (mode >= 6)) && pComp_params->m_use_luts)
+	{
+		color_cell_compressor_results avg_results;
+		memset(&avg_results, 0, sizeof(avg_results));
+							
+		avg_results.m_best_overall_err = pResults->m_best_overall_err;
+		avg_results.m_pSelectors = pResults->m_pSelectors;
+		avg_results.m_pSelectors_temp = pResults->m_pSelectors_temp;
+								 
+		const uint32_t r = (int)(.5f + meanColor.m_c[0] * 255.0f);
+		const uint32_t g = (int)(.5f + meanColor.m_c[1] * 255.0f);
+		const uint32_t b = (int)(.5f + meanColor.m_c[2] * 255.0f);
+		const uint32_t a = (int)(.5f + meanColor.m_c[3] * 255.0f);
+
+		uint64_t avg_err;
+		if (mode == 0)
+			avg_err = pack_mode0_to_one_color(pParams, &avg_results, r, g, b, pResults->m_pSelectors_temp, num_pixels, pPixels);
+		else if (mode == 1)
+			avg_err = pack_mode1_to_one_color(pParams, &avg_results, r, g, b, pResults->m_pSelectors_temp, num_pixels, pPixels);
+		else if (mode == 6)
+			avg_err = pack_mode6_to_one_color(pParams, &avg_results, r, g, b, a, pResults->m_pSelectors_temp, num_pixels, pPixels);
+		else if (mode == 7)
+			avg_err = pack_mode7_to_one_color(pParams, &avg_results, r, g, b, a, pResults->m_pSelectors_temp, num_pixels, pPixels);
+		else
+			avg_err = pack_mode24_to_one_color(pParams, &avg_results, r, g, b, pResults->m_pSelectors_temp, num_pixels, pPixels);
+
+		if (avg_err < pResults->m_best_overall_err)
+		{
+			pResults->m_best_overall_err = avg_err;
+			pResults->m_low_endpoint = avg_results.m_low_endpoint;
+			pResults->m_high_endpoint = avg_results.m_high_endpoint;
+			pResults->m_pbits[0] = avg_results.m_pbits[0];
+			pResults->m_pbits[1] = avg_results.m_pbits[1];
+
+			for ( uint32_t i = 0; i < num_pixels; i++)
+				pResults->m_pSelectors[i] = pResults->m_pSelectors_temp[i];
+
+			pResults->m_used_lut = true; // average-color one-color LUT candidate won
+		}
+	}
+
+	return pResults->m_best_overall_err;
+}
+
+static uint64_t color_cell_compression_est( uint32_t mode, const  color_cell_compressor_params * pParams, uint64_t best_err_so_far,  uint32_t num_pixels, const  color_quad_i * pPixels)
+{
+	NOTE_UNUSED(best_err_so_far);
+	assert((pParams->m_num_selector_weights == 4) || (pParams->m_num_selector_weights == 8));
+
+	float lr = 255, lg = 255, lb = 255;
+	float hr = 0, hg = 0, hb = 0;
+	for ( uint32_t i = 0; i < num_pixels; i++)
+	{
+		const  color_quad_i * pC = &pPixels[i];
+
+		float r = (float)(pC->m_c[0]);
+		float g = (float)(pC->m_c[1]);
+		float b = (float)(pC->m_c[2]);
+		
+		lr = min(lr, r);
+		lg = min(lg, g);
+		lb = min(lb, b);
+
+		hr = max(hr, r);
+		hg = max(hg, g);
+		hb = max(hb, b);
+	}
+			
+	const  uint32_t N = 1 << g_bc7_color_index_bitcount[mode];
+						
+	uint64_t total_err = 0;
+	
+	float sr = lr;
+	float sg = lg;
+	float sb = lb;
+
+	float dir = hr - lr;
+	float dig = hg - lg;
+	float dib = hb - lb;	
+
+	float far = dir;
+	float fag = dig;
+	float fab = dib;
+
+	float low = far * sr + fag * sg + fab * sb;
+	float high = far * hr + fag * hg + fab * hb;
+
+	float scale = ((float)N - 1) / ((float)(high - low) + BC7E_DENOM_BIAS);
+	float inv_n = 1.0f / ((float)N - 1);
+
+	float total_errf = 0;
+
+	// We don't handle perceptual very well here, but the difference is very slight (<.05 dB avg Luma PSNR across a large corpus) and the perf lost was high (2x slower).
+	if ((pParams->m_weights[0] != 1) || (pParams->m_weights[1] != 1) || (pParams->m_weights[2] != 1))
+	{
+		float wr = (float)(pParams->m_weights[0]);
+		float wg = (float)(pParams->m_weights[1]);
+		float wb = (float)(pParams->m_weights[2]);
+
+		for ( uint32_t i = 0; i < num_pixels; i++)
+		{
+			const  color_quad_i * pC = &pPixels[i];
+
+			float d = far * (float)pC->m_c[0] + fag * (float)pC->m_c[1] + fab * (float)pC->m_c[2];
+
+			float s = clamp(floorf((d - low) * scale + .5f) * inv_n, 0.0f, 1.0f);
+
+			float itr = sr + dir * s;
+			float itg = sg + dig * s;
+			float itb = sb + dib * s;
+
+			float dr = itr - (float)pC->m_c[0];
+			float dg = itg - (float)pC->m_c[1];
+			float db = itb - (float)pC->m_c[2];
+
+			total_errf += wr * dr * dr + wg * dg * dg + wb * db * db;
+		}
+	}
+	else
+	{
+		for ( uint32_t i = 0; i < num_pixels; i++)
+		{
+			const  color_quad_i * pC = &pPixels[i];
+
+			float d = far * (float)pC->m_c[0] + fag * (float)pC->m_c[1] + fab * (float)pC->m_c[2];
+
+			float s = clamp(floorf((d - low) * scale + .5f) * inv_n, 0.0f, 1.0f);
+
+			float itr = sr + dir * s;
+			float itg = sg + dig * s;
+			float itb = sb + dib * s;
+
+			float dr = itr - (float)pC->m_c[0];
+			float dg = itg - (float)pC->m_c[1];
+			float db = itb - (float)pC->m_c[2];
+
+			total_errf += dr * dr + dg * dg + db * db;
+		}
+	}
+
+	total_err = (int64_t)total_errf;
+
+	return total_err;
+}
+
+static uint64_t color_cell_compression_est_mode7( uint32_t mode, const  color_cell_compressor_params * pParams, uint64_t best_err_so_far,  uint32_t num_pixels, const  color_quad_i * pPixels)
+{
+	NOTE_UNUSED(best_err_so_far);
+	NOTE_UNUSED(mode); // only referenced by the assert below (compiled out under NDEBUG)
+	assert((mode == 7) && (pParams->m_num_selector_weights == 4));
+
+	float lr = 255, lg = 255, lb = 255, la = 255;
+	float hr = 0, hg = 0, hb = 0, ha = 0;
+	for ( uint32_t i = 0; i < num_pixels; i++)
+	{
+		const  color_quad_i * pC = &pPixels[i];
+
+		float r = (float)(pC->m_c[0]);
+		float g = (float)(pC->m_c[1]);
+		float b = (float)(pC->m_c[2]);
+		float a = (float)(pC->m_c[3]);
+		
+		lr = min(lr, r);
+		lg = min(lg, g);
+		lb = min(lb, b);
+		la = min(la, a);
+
+		hr = max(hr, r);
+		hg = max(hg, g);
+		hb = max(hb, b);
+		ha = max(ha, a);
+	}
+			
+	const  uint32_t N = 4;
+						
+	uint64_t total_err = 0;
+	
+	float sr = lr;
+	float sg = lg;
+	float sb = lb;
+	float sa = la;
+
+	float dir = hr - lr;
+	float dig = hg - lg;
+	float dib = hb - lb;	
+	float dia = ha - la;	
+
+	float far = dir;
+	float fag = dig;
+	float fab = dib;
+	float faa = dia;
+
+	float low = far * sr + fag * sg + fab * sb + faa * sa;
+	float high = far * hr + fag * hg + fab * hb + faa * ha;
+
+	float scale = ((float)N - 1) / ((float)(high - low) + BC7E_DENOM_BIAS);
+	float inv_n = 1.0f / ((float)N - 1);
+
+	float total_errf = 0;
+
+	// We don't handle perceptual very well here, but the difference is very slight (<.05 dB avg Luma PSNR across a large corpus) and the perf lost was high (2x slower).
+	if ( (!pParams->m_perceptual) && ((pParams->m_weights[0] != 1) || (pParams->m_weights[1] != 1) || (pParams->m_weights[2] != 1) || (pParams->m_weights[3] != 1)) )
+	{
+		float wr = (float)(pParams->m_weights[0]);
+		float wg = (float)(pParams->m_weights[1]);
+		float wb = (float)(pParams->m_weights[2]);
+		float wa = (float)(pParams->m_weights[3]);
+
+		for ( uint32_t i = 0; i < num_pixels; i++)
+		{
+			const  color_quad_i * pC = &pPixels[i];
+
+			float d = far * (float)pC->m_c[0] + fag * (float)pC->m_c[1] + fab * (float)pC->m_c[2] + faa * (float)pC->m_c[3];
+
+			float s = clamp(floorf((d - low) * scale + .5f) * inv_n, 0.0f, 1.0f);
+
+			float itr = sr + dir * s;
+			float itg = sg + dig * s;
+			float itb = sb + dib * s;
+			float ita = sa + dia * s;
+
+			float dr = itr - (float)pC->m_c[0];
+			float dg = itg - (float)pC->m_c[1];
+			float db = itb - (float)pC->m_c[2];
+			float da = ita - (float)pC->m_c[3];
+
+			total_errf += wr * dr * dr + wg * dg * dg + wb * db * db + wa * da * da;
+		}
+	}
+	else
+	{
+		for ( uint32_t i = 0; i < num_pixels; i++)
+		{
+			const  color_quad_i * pC = &pPixels[i];
+
+			float d = far * (float)pC->m_c[0] + fag * (float)pC->m_c[1] + fab * (float)pC->m_c[2] + faa * (float)pC->m_c[3];
+
+			float s = clamp(floorf((d - low) * scale + .5f) * inv_n, 0.0f, 1.0f);
+
+			float itr = sr + dir * s;
+			float itg = sg + dig * s;
+			float itb = sb + dib * s;
+			float ita = sa + dia * s;
+
+			float dr = itr - (float)pC->m_c[0];
+			float dg = itg - (float)pC->m_c[1];
+			float db = itb - (float)pC->m_c[2];
+			float da = ita - (float)pC->m_c[3];
+
+			total_errf += dr * dr + dg * dg + db * db + da * da;
+		}
+	}
+
+	total_err = (int64_t)total_errf;
+
+	return total_err;
+}
+
+static uint32_t estimate_partition( uint32_t mode, const  color_quad_i * pPixels, const  bc7e_compress_block_params * pComp_params)
+{
+	const  uint32_t total_subsets = g_bc7_num_subsets[mode];
+	 uint32_t total_partitions = minimumu(pComp_params->m_max_partitions_mode[mode], 1U << g_bc7_partition_bits[mode]);
+
+	if (total_partitions <= 1)
+		return 0;
+
+	uint64_t best_err = UINT64_MAX;
+	uint32_t best_partition = 0;
+
+	 color_cell_compressor_params params;
+	color_cell_compressor_params_clear(&params);
+
+	params.m_pSelector_weights = (g_bc7_color_index_bitcount[mode] == 2) ? g_bc7_weights2 : g_bc7_weights3;
+	params.m_num_selector_weights = 1 << g_bc7_color_index_bitcount[mode];
+
+	memcpy(params.m_weights, pComp_params->m_weights, sizeof(params.m_weights));
+	
+	if (mode >= 6)
+	{
+		params.m_weights[0] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[0];
+		params.m_weights[1] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[1];
+		params.m_weights[2] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[2];
+		params.m_weights[3] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[3];
+	}
+
+	params.m_perceptual = pComp_params->m_perceptual;
+
+	for ( uint32_t partition = 0; partition < total_partitions; partition++)
+	{
+		const int * pPartition = (total_subsets == 3) ? &g_bc7_partition3[partition * 16] : &g_bc7_partition2[partition * 16];
+
+		 color_quad_i subset_colors[3][16];
+		 uint32_t subset_total_colors[3];
+		subset_total_colors[0] = 0;
+		subset_total_colors[1] = 0;
+		subset_total_colors[2] = 0;
+		
+		for ( uint32_t index = 0; index < 16; index++)
+		{
+			const  uint32_t p = pPartition[index];
+
+			subset_colors[p][subset_total_colors[p]] = pPixels[index];
+			subset_total_colors[p]++;
+		}
+
+		uint64_t total_subset_err = 0;
+
+		for ( uint32_t subset = 0; subset < total_subsets; subset++)
+		{
+			uint64_t err;
+			if (mode == 7)
+				err = color_cell_compression_est_mode7(mode, &params, best_err, subset_total_colors[subset], &subset_colors[subset][0]);
+			else
+				err = color_cell_compression_est(mode, &params, best_err, subset_total_colors[subset], &subset_colors[subset][0]);
+
+			total_subset_err += err;
+
+		} // subset
+
+		if (total_subset_err < best_err)
+		{
+			best_err = total_subset_err;
+			best_partition = partition;
+			if (!best_err)
+				break;
+		}
+
+		if (total_subsets == 2)
+		{
+			if ((partition == BC7E_2SUBSET_CHECKERBOARD_PARTITION_INDEX) && (best_partition != BC7E_2SUBSET_CHECKERBOARD_PARTITION_INDEX))
+				break;
+		}
+
+	} // partition
+
+	return best_partition;
+}
+
+struct solution
+{
+	uint32_t m_index;
+	uint64_t m_err;
+};
+
+static  uint32_t estimate_partition_list( uint32_t mode, const  color_quad_i * pPixels, const  bc7e_compress_block_params * pComp_params, 
+	 solution * pSolutions,  int32_t max_solutions)
+{
+	const  int32_t orig_max_solutions = max_solutions;
+
+	const  uint32_t total_subsets = g_bc7_num_subsets[mode];
+	 uint32_t total_partitions = minimumu(pComp_params->m_max_partitions_mode[mode], 1U << g_bc7_partition_bits[mode]);
+
+	if (total_partitions <= 1)
+	{
+		pSolutions[0].m_index = 0;
+		pSolutions[0].m_err = 0;
+		return 1;
+	}
+	else if (max_solutions >= (int)total_partitions)
+	{
+		for ( int i = 0; i < (int)total_partitions; i++)
+		{
+			pSolutions[i].m_index = i;
+			pSolutions[i].m_err = i;
+		}
+		return total_partitions;
+	}
+
+	const  int32_t HIGH_FREQUENCY_SORTED_PARTITION_THRESHOLD = 4;
+	if (total_subsets == 2)
+	{
+		if (max_solutions < HIGH_FREQUENCY_SORTED_PARTITION_THRESHOLD)
+			max_solutions = HIGH_FREQUENCY_SORTED_PARTITION_THRESHOLD;
+	}
+						
+	 color_cell_compressor_params params;
+	color_cell_compressor_params_clear(&params);
+
+	params.m_pSelector_weights = (g_bc7_color_index_bitcount[mode] == 2) ? g_bc7_weights2 : g_bc7_weights3;
+	params.m_num_selector_weights = 1 << g_bc7_color_index_bitcount[mode];
+
+	memcpy(params.m_weights, pComp_params->m_weights, sizeof(params.m_weights));
+
+	if (mode >= 6)
+	{
+		params.m_weights[0] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[0];
+		params.m_weights[1] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[1];
+		params.m_weights[2] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[2];
+		params.m_weights[3] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[3];
+	}
+
+	params.m_perceptual = pComp_params->m_perceptual;
+
+	 int32_t num_solutions = 0;
+
+	for ( uint32_t partition = 0; partition < total_partitions; partition++)
+	{
+		const int * pPartition = (total_subsets == 3) ? &g_bc7_partition3[partition * 16] : &g_bc7_partition2[partition * 16];
+
+		 color_quad_i subset_colors[3][16];
+		 uint32_t subset_total_colors[3];
+		subset_total_colors[0] = 0;
+		subset_total_colors[1] = 0;
+		subset_total_colors[2] = 0;
+
+		for ( uint32_t index = 0; index < 16; index++)
+		{
+			const  uint32_t p = pPartition[index];
+
+			subset_colors[p][subset_total_colors[p]] = pPixels[index];
+			subset_total_colors[p]++;
+		}
+				
+		uint64_t total_subset_err = 0;
+
+		for ( uint32_t subset = 0; subset < total_subsets; subset++)
+		{
+			uint64_t err;
+			if (mode == 7)
+				err = color_cell_compression_est_mode7(mode, &params, UINT64_MAX, subset_total_colors[subset], &subset_colors[subset][0]);
+			else
+				err = color_cell_compression_est(mode, &params, UINT64_MAX, subset_total_colors[subset], &subset_colors[subset][0]);
+
+			total_subset_err += err;
+
+		} // subset
+
+		int32_t i;
+		for (i = 0; i < num_solutions; i++)
+		{
+			if (total_subset_err < pSolutions[i].m_err)
+				break;
+		}
+						
+		if (i < num_solutions)
+		{
+			int32_t solutions_to_move = (max_solutions - 1) - i;
+			int32_t num_elements_at_i = num_solutions - i;
+			if (solutions_to_move > num_elements_at_i)
+				solutions_to_move = num_elements_at_i;
+																
+			assert(((i + 1) + solutions_to_move) <= max_solutions);
+			assert((i + solutions_to_move) <= num_solutions);
+			
+			for (int32_t j = solutions_to_move - 1; j >= 0; --j)
+			{
+				pSolutions[i + j + 1] = pSolutions[i + j];
+			}
+		}
+
+		if (num_solutions < max_solutions)
+			num_solutions++;
+
+		if (i < num_solutions)
+		{
+			pSolutions[i].m_err = total_subset_err;
+
+			pSolutions[i].m_index = partition;
+		}
+
+#if BC7E_NON_DETERMINISTIC
+		if ((total_subsets == 2) && (partition == BC7E_2SUBSET_CHECKERBOARD_PARTITION_INDEX))
+		{
+			if (all(i >= HIGH_FREQUENCY_SORTED_PARTITION_THRESHOLD))
+				break;
+		}
+#endif
+
+	} // partition
+
+#if 0
+	for ( int i = 0; i < num_solutions; i++)
+	{
+		assert(pSolutions[i].m_index < total_partitions);
+	}
+
+	for ( int i = 0; i < (num_solutions - 1); i++)
+	{
+		assert(pSolutions[i].m_err <= pSolutions[i + 1].m_err);
+	}
+#endif
+
+	return min(num_solutions, orig_max_solutions);
+}
+
+static inline void set_block_bits(uint8_t *pBytes, uint32_t val, uint32_t num_bits,  uint32_t * pCur_ofs)
+{
+	assert(num_bits < 32);
+	uint32_t limit = 1U << num_bits;
+	assert(val < limit);
+	NOTE_UNUSED(limit); // only referenced by the assert above (compiled out under NDEBUG)
+
+	while (num_bits)
+	{
+		const uint32_t n = minimumu(8 - (*pCur_ofs & 7), num_bits);
+
+		pBytes[*pCur_ofs >> 3] |= (uint8_t)(val << (*pCur_ofs & 7));
+
+		val >>= n;
+		num_bits -= n;
+		*pCur_ofs += n;
+	}
+
+	assert(*pCur_ofs <= 128);
+}
+
+struct bc7_optimization_results
+{
+	uint32_t m_mode;
+	uint32_t m_partition;
+	int m_selectors[16];
+	int m_alpha_selectors[16];
+	color_quad_i m_low[3];
+	color_quad_i m_high[3];
+	uint32_t m_pbits[3][2];
+	uint32_t m_rotation;
+	uint32_t m_index_selector;
+	// True if the WINNING encoding used the one-color optimal-endpoint LUT on any
+	// subset (OR'd across the winning mode's subsets). Set at each winner-update.
+	bool m_used_lut;
+};
+
+static void encode_bc7_block(void *pBlock, const  bc7_optimization_results * pResults)
+{
+	const uint32_t best_mode = pResults->m_mode;
+
+	const uint32_t total_subsets = g_bc7_num_subsets[best_mode];
+
+	const uint32_t total_partitions = 1 << g_bc7_partition_bits[best_mode];
+
+	const int *pPartition;
+	if (total_subsets == 1)
+		pPartition = &g_bc7_partition1[0];
+	else if (total_subsets == 2)
+		pPartition = &g_bc7_partition2[pResults->m_partition * 16];
+	else
+		pPartition = &g_bc7_partition3[pResults->m_partition * 16];
+
+	int color_selectors[16];
+	for ( int i = 0; i < 16; i++)
+		color_selectors[i] = pResults->m_selectors[i];
+
+	int alpha_selectors[16];
+	for ( int i = 0; i < 16; i++)
+		alpha_selectors[i] = pResults->m_alpha_selectors[i];
+
+	color_quad_i low[3], high[3];
+	low[0] = pResults->m_low[0];
+	low[1] = pResults->m_low[1];
+	low[2] = pResults->m_low[2];
+
+	high[0] = pResults->m_high[0];
+	high[1] = pResults->m_high[1];
+	high[2] = pResults->m_high[2];
+	
+	uint32_t pbits[3][2];
+	for ( int i = 0; i < 3; i++)
+	{
+		pbits[i][0] = pResults->m_pbits[i][0];
+		pbits[i][1] = pResults->m_pbits[i][1];
+	}
+
+	int anchor[3];
+	anchor[0] = -1;
+	anchor[1] = -1;
+	anchor[2] = -1;
+
+	for ( uint32_t k = 0; k < total_subsets; k++)
+	{
+		uint32_t anchor_index = 0;
+		if (k)
+		{
+			if ((total_subsets == 3) && (k == 1))
+			{
+				anchor_index = g_bc7_table_anchor_index_third_subset_1[pResults->m_partition];
+			}
+			else if ((total_subsets == 3) && (k == 2))
+			{
+				anchor_index = g_bc7_table_anchor_index_third_subset_2[pResults->m_partition];
+			}
+			else
+			{
+				anchor_index = g_bc7_table_anchor_index_second_subset[pResults->m_partition];
+			}
+		}
+
+		anchor[k] = anchor_index;
+
+		const uint32_t color_index_bits = get_bc7_color_index_size(best_mode, pResults->m_index_selector);
+		const uint32_t num_color_indices = 1 << color_index_bits;
+
+		if (color_selectors[anchor_index] & (num_color_indices >> 1))
+		{
+			for ( uint32_t i = 0; i < 16; i++)
+			{
+				if ((uint32_t)pPartition[i] == k)
+					color_selectors[i] = (num_color_indices - 1) - color_selectors[i];
+			}
+
+			if (get_bc7_mode_has_seperate_alpha_selectors(best_mode))
+			{
+				for ( uint32_t q = 0; q < 3; q++)
+				{
+					int t = low[k].m_c[q];
+					low[k].m_c[q] = high[k].m_c[q];
+					high[k].m_c[q] = t;
+				}
+			}
+			else
+			{
+				color_quad_i tmp = low[k];
+				low[k] = high[k];
+				high[k] = tmp;
+			}
+
+			if (!g_bc7_mode_has_shared_p_bits[best_mode])
+			{
+				uint32_t t = pbits[k][0];
+				pbits[k][0] = pbits[k][1];
+				pbits[k][1] = t;
+			}
+		}
+
+		if (get_bc7_mode_has_seperate_alpha_selectors(best_mode))
+		{
+			const uint32_t alpha_index_bits = get_bc7_alpha_index_size(best_mode, pResults->m_index_selector);
+			const uint32_t num_alpha_indices = 1 << alpha_index_bits;
+
+			if (alpha_selectors[anchor_index] & (num_alpha_indices >> 1))
+			{
+				for ( uint32_t i = 0; i < 16; i++)
+				{
+					if ((uint32_t)pPartition[i] == k)
+						alpha_selectors[i] = (num_alpha_indices - 1) - alpha_selectors[i];
+				}
+
+				int t = low[k].m_c[3];
+				low[k].m_c[3] = high[k].m_c[3];
+				high[k].m_c[3] = t;
+			}
+		}
+	}
+
+	uint8_t *pBlock_bytes = (uint8_t *)(pBlock);
+	memset(pBlock_bytes, 0, BC7E_BLOCK_SIZE);
+
+	uint32_t cur_bit_ofs = 0;
+		
+	set_block_bits(pBlock_bytes, 1 << best_mode, best_mode + 1, &cur_bit_ofs);
+
+	if ((best_mode == 4) || (best_mode == 5))
+		set_block_bits(pBlock_bytes, pResults->m_rotation, 2, &cur_bit_ofs);
+
+	if (best_mode == 4)
+		set_block_bits(pBlock_bytes, pResults->m_index_selector, 1, &cur_bit_ofs);
+
+	if (total_partitions > 1)
+		set_block_bits(pBlock_bytes, pResults->m_partition, (total_partitions == 64) ? 6 : 4, &cur_bit_ofs);
+
+	const uint32_t total_comps = (best_mode >= 4) ? 4 : 3;
+	for ( uint32_t comp = 0; comp < total_comps; comp++)
+	{
+		for ( uint32_t subset = 0; subset < total_subsets; subset++)
+		{
+			set_block_bits(pBlock_bytes, low[subset].m_c[comp], (comp == 3) ? g_bc7_alpha_precision_table[best_mode] : g_bc7_color_precision_table[best_mode], &cur_bit_ofs);
+			set_block_bits(pBlock_bytes, high[subset].m_c[comp], (comp == 3) ? g_bc7_alpha_precision_table[best_mode] : g_bc7_color_precision_table[best_mode], &cur_bit_ofs);
+		}
+	}
+
+	if (g_bc7_mode_has_p_bits[best_mode])
+	{
+		for ( uint32_t subset = 0; subset < total_subsets; subset++)
+		{
+			set_block_bits(pBlock_bytes, pbits[subset][0], 1, &cur_bit_ofs);
+			if (!g_bc7_mode_has_shared_p_bits[best_mode])
+				set_block_bits(pBlock_bytes, pbits[subset][1], 1, &cur_bit_ofs);
+		}
+	}
+
+	for ( uint32_t y = 0; y < 4; y++)
+	{
+		for ( uint32_t x = 0; x < 4; x++)
+		{
+			 int idx = x + y * 4;
+
+			uint32_t n = pResults->m_index_selector ? get_bc7_alpha_index_size(best_mode, pResults->m_index_selector) : get_bc7_color_index_size(best_mode, pResults->m_index_selector);
+
+			if ((idx == anchor[0]) || (idx == anchor[1]) || (idx == anchor[2]))
+				n--;
+
+			set_block_bits(pBlock_bytes, pResults->m_index_selector ? alpha_selectors[idx] : color_selectors[idx], n, &cur_bit_ofs);
+		}
+	}
+
+	if (get_bc7_mode_has_seperate_alpha_selectors(best_mode))
+	{
+		for ( uint32_t y = 0; y < 4; y++)
+		{
+			for ( uint32_t x = 0; x < 4; x++)
+			{
+				int idx = x + y * 4;
+
+				uint32_t n = pResults->m_index_selector ? get_bc7_color_index_size(best_mode, pResults->m_index_selector) : get_bc7_alpha_index_size(best_mode, pResults->m_index_selector);
+
+				if ((idx == anchor[0]) || (idx == anchor[1]) || (idx == anchor[2]))
+					n--;
+
+				set_block_bits(pBlock_bytes, pResults->m_index_selector ? color_selectors[idx] : alpha_selectors[idx], n, &cur_bit_ofs);
+			}
+		}
+	}
+
+	assert(cur_bit_ofs == 128);
+}
+
+static inline void encode_bc7_block_mode6(void *pBlock,  bc7_optimization_results * pResults)
+{
+	color_quad_i low, high;
+	uint32_t pbits[2];
+		
+	uint32_t invert_selectors = 0;
+	if (pResults->m_selectors[0] & 8)
+	{
+		invert_selectors = 15;
+							
+		low = pResults->m_high[0];
+		high = pResults->m_low[0];
+
+		pbits[0] = pResults->m_pbits[0][1];
+		pbits[1] = pResults->m_pbits[0][0];
+	}
+	else
+	{
+		low = pResults->m_low[0];
+		high = pResults->m_high[0];
+
+		pbits[0] = pResults->m_pbits[0][0];
+		pbits[1] = pResults->m_pbits[0][1];
+	}
+
+	uint64_t l = 0, h = 0;
+
+	l = 1 << 6;
+
+	l |= (low.m_c[0] << 7);
+	l |= (high.m_c[0] << 14);
+
+	l |= (low.m_c[1] << 21);
+	l |= ((uint64_t)high.m_c[1] << 28);
+
+	l |= ((uint64_t)low.m_c[2] << 35);
+	l |= ((uint64_t)high.m_c[2] << 42);
+
+	l |= ((uint64_t)low.m_c[3] << 49);
+	l |= ((uint64_t)high.m_c[3] << 56);
+
+	l |= ((uint64_t)pbits[0] << 63);
+		
+	h = pbits[1];
+		
+	h |= ((invert_selectors ^ pResults->m_selectors[0]) << 1);
+
+	// TODO: Just invert all these bits in one single operation, not as individual
+	h |= ((invert_selectors ^ pResults->m_selectors[1]) << 4);
+	h |= ((invert_selectors ^ pResults->m_selectors[2]) << 8);
+	h |= ((invert_selectors ^ pResults->m_selectors[3]) << 12);
+	h |= ((invert_selectors ^ pResults->m_selectors[4]) << 16);
+	
+	h |= ((invert_selectors ^ pResults->m_selectors[5]) << 20);
+	h |= ((invert_selectors ^ pResults->m_selectors[6]) << 24);
+	h |= ((invert_selectors ^ pResults->m_selectors[7]) << 28);
+	h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[8]) << 32);
+
+	h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[9]) << 36);
+	h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[10]) << 40);
+	h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[11]) << 44);
+	h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[12]) << 48);
+
+	h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[13]) << 52);
+	h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[14]) << 56);
+	h |= ((uint64_t)(invert_selectors ^ pResults->m_selectors[15]) << 60);
+
+	((uint64_t *)(pBlock))[0] = l;
+
+	((uint64_t *)(pBlock))[1] = h;
+}
+
+static void handle_alpha_block_mode4(const  color_quad_i * pPixels, const  bc7e_compress_block_params * pComp_params,  color_cell_compressor_params * pParams, uint32_t lo_a, uint32_t hi_a, 
+	 bc7_optimization_results * pOpt_results4,  uint64_t * pMode4_err)
+{
+	pParams->m_has_alpha = false;
+	pParams->m_comp_bits = 5;
+	pParams->m_has_pbits = false;
+	pParams->m_endpoints_share_pbit = false;				
+	pParams->m_perceptual = pComp_params->m_perceptual;
+
+	for ( uint32_t index_selector = 0; index_selector < 2; index_selector++)
+	{
+		if ((pComp_params->m_mode4_index_mask & (1 << index_selector)) == 0)
+			continue;
+
+		if (index_selector)
+		{
+			pParams->m_pSelector_weights = g_bc7_weights3;
+			pParams->m_pSelector_weightsx = (const vec4F * )&g_bc7_weights3x[0];
+			pParams->m_num_selector_weights = 8;
+		}
+		else
+		{
+			pParams->m_pSelector_weights = g_bc7_weights2;
+			pParams->m_pSelector_weightsx = (const vec4F * )&g_bc7_weights2x[0];
+			pParams->m_num_selector_weights = 4;
+		}
+								
+		color_cell_compressor_results results;
+		
+		int selectors[16];
+		results.m_pSelectors = selectors;
+
+		int selectors_temp[16];
+		results.m_pSelectors_temp = selectors_temp;
+				
+		uint64_t trial_err = color_cell_compression(4, pParams, &results, pComp_params, 16, pPixels, true);
+		assert(trial_err == results.m_best_overall_err);
+
+		uint32_t la = minimumi((lo_a + 2) >> 2, 63);
+		uint32_t ha = minimumi((hi_a + 2) >> 2, 63);
+
+		if (la == ha)
+		{
+			if (lo_a != hi_a)
+			{
+				if (ha != 63)
+					ha++;
+				else if (la != 0)
+					la--;
+			}
+		}
+
+		uint64_t best_alpha_err = UINT64_MAX;
+		uint32_t best_la = 0, best_ha = 0;
+		int best_alpha_selectors[16] = {};
+						
+		for ( int32_t pass = 0; pass < 2; pass++)
+		{
+			int32_t vals[8];
+
+			if (index_selector == 0)
+			{
+				vals[0] = (la << 2) | (la >> 4);
+				vals[7] = (ha << 2) | (ha >> 4);
+
+				for ( uint32_t i = 1; i < 7; i++)
+					vals[i] = (vals[0] * (64 - g_bc7_weights3[i]) + vals[7] * g_bc7_weights3[i] + 32) >> 6;
+			}
+			else
+			{
+				vals[0] = (la << 2) | (la >> 4);
+				vals[3] = (ha << 2) | (ha >> 4);
+
+				const  int32_t w_s1 = 21, w_s2 = 43;
+				vals[1] = (vals[0] * (64 - w_s1) + vals[3] * w_s1 + 32) >> 6;
+				vals[2] = (vals[0] * (64 - w_s2) + vals[3] * w_s2 + 32) >> 6;
+			}
+
+			uint64_t trial_alpha_err = 0;
+
+			int trial_alpha_selectors[16];
+			for ( uint32_t i = 0; i < 16; i++)
+			{
+				const int32_t a = pPixels[i].m_c[3];
+
+				int s = 0;
+				int32_t be = iabs32(a - vals[0]);
+
+				int e = iabs32(a - vals[1]); if (e < be) { be = e; s = 1; }
+				e = iabs32(a - vals[2]); if (e < be) { be = e; s = 2; }
+				e = iabs32(a - vals[3]); if (e < be) { be = e; s = 3; }
+
+				if (index_selector == 0)
+				{
+					e = iabs32(a - vals[4]); if (e < be) { be = e; s = 4; }
+					e = iabs32(a - vals[5]); if (e < be) { be = e; s = 5; }
+					e = iabs32(a - vals[6]); if (e < be) { be = e; s = 6; }
+					e = iabs32(a - vals[7]); if (e < be) { be = e; s = 7; }
+				}
+
+				trial_alpha_err += (be * be) * pParams->m_weights[3];
+
+				trial_alpha_selectors[i] = s;
+			}
+
+			if (trial_alpha_err < best_alpha_err)
+			{
+				best_alpha_err = trial_alpha_err;
+				best_la = la;
+				best_ha = ha;
+				for ( uint32_t i = 0; i < 16; i++)
+					best_alpha_selectors[i] = trial_alpha_selectors[i];
+			}
+
+			if (pass == 0) 
+			{
+				float xl, xh;
+				compute_least_squares_endpoints_a(16, trial_alpha_selectors, index_selector ? (const vec4F * )&g_bc7_weights2x[0] : (const vec4F * )&g_bc7_weights3x[0], &xl, &xh, pPixels);
+				if (xl > xh)
+					swapf(&xl, &xh);
+				la = clampi((int)floor(xl * (63.0f / 255.0f) + .5f), 0, 63);
+				ha = clampi((int)floor(xh * (63.0f / 255.0f) + .5f), 0, 63);
+			}
+						
+		} // pass
+
+		if (pComp_params->m_uber_level > 0)
+		{
+			const  int D = min((int)pComp_params->m_uber_level, 3);
+			for ( int ld = -D; ld <= D; ld++)
+			{
+				for ( int hd = -D; hd <= D; hd++)
+				{
+					la = clamp((int)best_la + ld, 0, 63);
+					ha = clamp((int)best_ha + hd, 0, 63);
+					
+					int32_t vals[8];
+
+					if (index_selector == 0)
+					{
+						vals[0] = (la << 2) | (la >> 4);
+						vals[7] = (ha << 2) | (ha >> 4);
+
+						for ( uint32_t i = 1; i < 7; i++)
+							vals[i] = (vals[0] * (64 - g_bc7_weights3[i]) + vals[7] * g_bc7_weights3[i] + 32) >> 6;
+					}
+					else
+					{
+						vals[0] = (la << 2) | (la >> 4);
+						vals[3] = (ha << 2) | (ha >> 4);
+
+						const  int32_t w_s1 = 21, w_s2 = 43;
+						vals[1] = (vals[0] * (64 - w_s1) + vals[3] * w_s1 + 32) >> 6;
+						vals[2] = (vals[0] * (64 - w_s2) + vals[3] * w_s2 + 32) >> 6;
+					}
+
+					uint64_t trial_alpha_err = 0;
+
+					int trial_alpha_selectors[16];
+					for ( uint32_t i = 0; i < 16; i++)
+					{
+						const int32_t a = pPixels[i].m_c[3];
+
+						int s = 0;
+						int32_t be = iabs32(a - vals[0]);
+
+						int e = iabs32(a - vals[1]); if (e < be) { be = e; s = 1; }
+						e = iabs32(a - vals[2]); if (e < be) { be = e; s = 2; }
+						e = iabs32(a - vals[3]); if (e < be) { be = e; s = 3; }
+
+						if (index_selector == 0)
+						{
+							e = iabs32(a - vals[4]); if (e < be) { be = e; s = 4; }
+							e = iabs32(a - vals[5]); if (e < be) { be = e; s = 5; }
+							e = iabs32(a - vals[6]); if (e < be) { be = e; s = 6; }
+							e = iabs32(a - vals[7]); if (e < be) { be = e; s = 7; }
+						}
+
+						trial_alpha_err += (be * be) * pParams->m_weights[3];
+
+						trial_alpha_selectors[i] = s;
+					}
+
+					if (trial_alpha_err < best_alpha_err)
+					{
+						best_alpha_err = trial_alpha_err;
+						best_la = la;
+						best_ha = ha;
+						for ( uint32_t i = 0; i < 16; i++)
+							best_alpha_selectors[i] = trial_alpha_selectors[i];
+					}
+				
+				} // hd
+
+			} // ld
+		}
+
+		trial_err += best_alpha_err;
+
+		if (trial_err < *pMode4_err)
+		{
+			*pMode4_err = trial_err;
+
+			pOpt_results4->m_mode = 4;
+			pOpt_results4->m_index_selector = index_selector;
+			pOpt_results4->m_rotation = 0;
+			pOpt_results4->m_partition = 0;
+			pOpt_results4->m_used_lut = results.m_used_lut;
+
+			pOpt_results4->m_low[0] = results.m_low_endpoint;
+			pOpt_results4->m_high[0] = results.m_high_endpoint;
+			pOpt_results4->m_low[0].m_c[3] = best_la;
+			pOpt_results4->m_high[0].m_c[3] = best_ha;
+
+			for ( uint32_t i = 0; i < 16; i++)
+				pOpt_results4->m_selectors[i] = selectors[i];
+
+			for ( uint32_t i = 0; i < 16; i++)
+				pOpt_results4->m_alpha_selectors[i] = best_alpha_selectors[i];
+		}
+
+	} // index_selector
+}
+
+static void handle_alpha_block_mode5(const  color_quad_i * pPixels, const  bc7e_compress_block_params * pComp_params,  color_cell_compressor_params * pParams, uint32_t lo_a, uint32_t hi_a, 
+	 bc7_optimization_results * pOpt_results5,  uint64_t * pMode5_err)
+{
+	pParams->m_pSelector_weights = g_bc7_weights2;
+	pParams->m_pSelector_weightsx = (const vec4F * )&g_bc7_weights2x[0];
+	pParams->m_num_selector_weights = 4;
+
+	pParams->m_comp_bits = 7;
+	pParams->m_has_alpha = false;
+	pParams->m_has_pbits = false;
+	pParams->m_endpoints_share_pbit = false;				
+	
+	pParams->m_perceptual = pComp_params->m_perceptual;
+		
+	color_cell_compressor_results results5;
+	results5.m_pSelectors = pOpt_results5->m_selectors;
+
+	int selectors_temp[16];
+	results5.m_pSelectors_temp = selectors_temp;
+
+	*pMode5_err = color_cell_compression(5, pParams, &results5, pComp_params, 16, pPixels, true);
+	assert(*pMode5_err == results5.m_best_overall_err);
+
+	pOpt_results5->m_low[0] = results5.m_low_endpoint;
+	pOpt_results5->m_high[0] = results5.m_high_endpoint;
+
+	cif (lo_a == hi_a)
+	{
+		pOpt_results5->m_low[0].m_c[3] = lo_a;
+		pOpt_results5->m_high[0].m_c[3] = hi_a;
+		for ( uint32_t i = 0; i < 16; i++)
+			pOpt_results5->m_alpha_selectors[i] = 0;
+	}
+	else
+	{
+		uint64_t mode5_alpha_err = UINT64_MAX;
+
+		for ( uint32_t pass = 0; pass < 2; pass++)
+		{
+			int32_t vals[4];
+			vals[0] = lo_a;
+			vals[3] = hi_a;
+
+			const  int32_t w_s1 = 21, w_s2 = 43;
+			vals[1] = (vals[0] * (64 - w_s1) + vals[3] * w_s1 + 32) >> 6;
+			vals[2] = (vals[0] * (64 - w_s2) + vals[3] * w_s2 + 32) >> 6;
+
+			int trial_alpha_selectors[16];
+
+			uint64_t trial_alpha_err = 0;
+			for ( uint32_t i = 0; i < 16; i++)
+			{
+				const int32_t a = pPixels[i].m_c[3];
+
+				int s = 0;
+				int32_t be = iabs32(a - vals[0]);
+				int e = iabs32(a - vals[1]); if (e < be) { be = e; s = 1; }
+				e = iabs32(a - vals[2]); if (e < be) { be = e; s = 2; }
+				e = iabs32(a - vals[3]); if (e < be) { be = e; s = 3; }
+
+				trial_alpha_selectors[i] = s;
+								
+				trial_alpha_err += (be * be) * pParams->m_weights[3];
+			}
+
+			if (trial_alpha_err < mode5_alpha_err)
+			{
+				mode5_alpha_err = trial_alpha_err;
+				pOpt_results5->m_low[0].m_c[3] = lo_a;
+				pOpt_results5->m_high[0].m_c[3] = hi_a;
+				for ( uint32_t i = 0; i < 16; i++)
+					pOpt_results5->m_alpha_selectors[i] = trial_alpha_selectors[i];
+			}
+
+			if (!pass)
+			{
+				float xl, xh;
+				compute_least_squares_endpoints_a(16, trial_alpha_selectors, (const vec4F * )&g_bc7_weights2x[0], &xl, &xh, pPixels);
+
+				uint32_t new_lo_a = clampi((int)floor(xl + .5f), 0, 255);
+				uint32_t new_hi_a = clampi((int)floor(xh + .5f), 0, 255);
+				if (new_lo_a > new_hi_a)
+					swapu(&new_lo_a, &new_hi_a);
+
+				if ((new_lo_a == lo_a) && (new_hi_a == hi_a))
+					break;
+
+				lo_a = new_lo_a;
+				hi_a = new_hi_a;
+			}
+		}
+
+		if (pComp_params->m_uber_level > 0)
+		{
+			const  int D = min((int)pComp_params->m_uber_level, 3);
+			for ( int ld = -D; ld <= D; ld++)
+			{
+				for ( int hd = -D; hd <= D; hd++)
+				{
+					lo_a = clamp((int)pOpt_results5->m_low[0].m_c[3] + ld, 0, 255);
+					hi_a = clamp((int)pOpt_results5->m_high[0].m_c[3] + hd, 0, 255);
+					
+					int32_t vals[4];
+					vals[0] = lo_a;
+					vals[3] = hi_a;
+
+					const  int32_t w_s1 = 21, w_s2 = 43;
+					vals[1] = (vals[0] * (64 - w_s1) + vals[3] * w_s1 + 32) >> 6;
+					vals[2] = (vals[0] * (64 - w_s2) + vals[3] * w_s2 + 32) >> 6;
+
+					int trial_alpha_selectors[16];
+
+					uint64_t trial_alpha_err = 0;
+					for ( uint32_t i = 0; i < 16; i++)
+					{
+						const int32_t a = pPixels[i].m_c[3];
+
+						int s = 0;
+						int32_t be = iabs32(a - vals[0]);
+						int e = iabs32(a - vals[1]); if (e < be) { be = e; s = 1; }
+						e = iabs32(a - vals[2]); if (e < be) { be = e; s = 2; }
+						e = iabs32(a - vals[3]); if (e < be) { be = e; s = 3; }
+
+						trial_alpha_selectors[i] = s;
+								
+						trial_alpha_err += (be * be) * pParams->m_weights[3];
+					}
+
+					if (trial_alpha_err < mode5_alpha_err)
+					{
+						mode5_alpha_err = trial_alpha_err;
+						pOpt_results5->m_low[0].m_c[3] = lo_a;
+						pOpt_results5->m_high[0].m_c[3] = hi_a;
+						for ( uint32_t i = 0; i < 16; i++)
+							pOpt_results5->m_alpha_selectors[i] = trial_alpha_selectors[i];
+					}
+				
+				} // hd
+
+			} // ld
+		}
+
+		*pMode5_err += mode5_alpha_err;
+	}
+
+	pOpt_results5->m_mode = 5;
+	pOpt_results5->m_index_selector = 0;
+	pOpt_results5->m_rotation = 0;
+	pOpt_results5->m_partition = 0;
+	pOpt_results5->m_used_lut = results5.m_used_lut;
+}
+
+static void handle_alpha_block(void * pBlock, const  color_quad_i * pPixels, const  bc7e_compress_block_params * pComp_params,  color_cell_compressor_params * pParams, uint32_t lo_a, uint32_t hi_a, int forced_partition = -1, uint64_t* pBest_err = nullptr, bool* pUsed_lut = nullptr)
+{
+	pParams->m_perceptual = pComp_params->m_perceptual;
+
+	bc7_optimization_results opt_results = {};
+	
+	uint64_t best_err = UINT64_MAX;
+		
+	// Mode 4
+	if (pComp_params->m_alpha_settings.m_use_mode4)
+	{
+		 color_cell_compressor_params params4 = *pParams;
+
+		const  int num_rotations = (pComp_params->m_perceptual || (!pComp_params->m_alpha_settings.m_use_mode4_rotation)) ? 1 : 4;
+		for ( uint32_t rotation = 0; rotation < (uint32_t)num_rotations; rotation++)
+		{
+			if ((pComp_params->m_mode4_rotation_mask & (1 << rotation)) == 0)
+				continue;
+
+			memcpy(params4.m_weights, pParams->m_weights, sizeof(params4.m_weights));
+			if (rotation)
+				swapu(&params4.m_weights[rotation - 1], &params4.m_weights[3]);
+							
+			color_quad_i rot_pixels[16];
+			const  color_quad_i * pTrial_pixels = pPixels;
+			uint32_t trial_lo_a = lo_a, trial_hi_a = hi_a;
+			if (rotation)
+			{
+				trial_lo_a = 255;
+				trial_hi_a = 0;
+
+				for ( uint32_t i = 0; i < 16; i++)
+				{
+					color_quad_i c = pPixels[i];
+					swapi(&c.m_c[3], &c.m_c[rotation - 1]);
+					rot_pixels[i] = c;
+
+					trial_lo_a = minimumu(trial_lo_a, c.m_c[3]);
+					trial_hi_a = maximumu(trial_hi_a, c.m_c[3]);
+				}
+
+				pTrial_pixels = rot_pixels;
+			}
+
+			bc7_optimization_results trial_opt_results4;
+
+			uint64_t trial_mode4_err = best_err;
+
+			handle_alpha_block_mode4(pTrial_pixels, pComp_params, &params4, trial_lo_a, trial_hi_a, &trial_opt_results4, &trial_mode4_err);
+
+			if (trial_mode4_err < best_err)
+			{
+				best_err = trial_mode4_err;
+
+				opt_results.m_mode = 4;
+				opt_results.m_index_selector = trial_opt_results4.m_index_selector;
+				opt_results.m_rotation = rotation;
+				opt_results.m_partition = 0;
+				opt_results.m_used_lut = trial_opt_results4.m_used_lut;
+
+				opt_results.m_low[0] = trial_opt_results4.m_low[0];
+				opt_results.m_high[0] = trial_opt_results4.m_high[0];
+
+				for ( uint32_t i = 0; i < 16; i++)
+					opt_results.m_selectors[i] = trial_opt_results4.m_selectors[i];
+				
+				for ( uint32_t i = 0; i < 16; i++)
+					opt_results.m_alpha_selectors[i] = trial_opt_results4.m_alpha_selectors[i];
+			}
+		} // rotation
+	}
+	
+	// Mode 6
+	if (pComp_params->m_alpha_settings.m_use_mode6)
+	{
+		 color_cell_compressor_params params6 = *pParams;
+
+		params6.m_weights[0] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[0];
+		params6.m_weights[1] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[1];
+		params6.m_weights[2] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[2];
+		params6.m_weights[3] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[3];
+
+		color_cell_compressor_results results6;
+		
+		params6.m_pSelector_weights = g_bc7_weights4;
+		params6.m_pSelector_weightsx = (const vec4F *)&g_bc7_weights4x[0];
+		params6.m_num_selector_weights = 16;
+
+		params6.m_comp_bits = 7;
+		params6.m_has_pbits = true;
+		params6.m_endpoints_share_pbit = false;
+		params6.m_has_alpha = true;
+				
+		int selectors[16];
+		results6.m_pSelectors = selectors;
+
+		int selectors_temp[16];
+		results6.m_pSelectors_temp = selectors_temp;
+				
+		uint64_t mode6_err = color_cell_compression(6, &params6, &results6, pComp_params, 16, pPixels, true);
+		assert(mode6_err == results6.m_best_overall_err);
+		
+		if (mode6_err < best_err)
+		{
+			best_err = mode6_err;
+
+			opt_results.m_mode = 6;
+			opt_results.m_index_selector = 0;
+			opt_results.m_rotation = 0;
+			opt_results.m_partition = 0;
+
+			opt_results.m_low[0] = results6.m_low_endpoint;
+			opt_results.m_high[0] = results6.m_high_endpoint;
+
+			opt_results.m_pbits[0][0] = results6.m_pbits[0];
+			opt_results.m_pbits[0][1] = results6.m_pbits[1];
+			opt_results.m_used_lut = results6.m_used_lut;
+
+			for ( int i = 0; i < 16; i++)
+				opt_results.m_selectors[i] = selectors[i];
+		}
+	}
+
+	// Mode 5
+	if (pComp_params->m_alpha_settings.m_use_mode5)
+	{
+		 color_cell_compressor_params params5 = *pParams;
+
+		const  int num_rotations = (pComp_params->m_perceptual || (!pComp_params->m_alpha_settings.m_use_mode5_rotation)) ? 1 : 4;
+		for ( uint32_t rotation = 0; rotation < (uint32_t)num_rotations; rotation++)
+		{
+			if ((pComp_params->m_mode5_rotation_mask & (1 << rotation)) == 0)
+				continue;
+
+			memcpy(params5.m_weights, pParams->m_weights, sizeof(params5.m_weights));
+			if (rotation)
+				swapu(&params5.m_weights[rotation - 1], &params5.m_weights[3]);
+
+			color_quad_i rot_pixels[16];
+			const  color_quad_i * pTrial_pixels = pPixels;
+			uint32_t trial_lo_a = lo_a, trial_hi_a = hi_a;
+			if (rotation)
+			{
+				trial_lo_a = 255;
+				trial_hi_a = 0;
+
+				for ( uint32_t i = 0; i < 16; i++)
+				{
+					color_quad_i c = pPixels[i];
+					swapi(&c.m_c[3], &c.m_c[rotation - 1]);
+					rot_pixels[i] = c;
+
+					trial_lo_a = minimumu(trial_lo_a, c.m_c[3]);
+					trial_hi_a = maximumu(trial_hi_a, c.m_c[3]);
+				}
+
+				pTrial_pixels = rot_pixels;
+			}
+
+			bc7_optimization_results trial_opt_results5;
+
+			uint64_t trial_mode5_err = 0;
+
+			handle_alpha_block_mode5(pTrial_pixels, pComp_params, &params5, trial_lo_a, trial_hi_a, &trial_opt_results5, &trial_mode5_err);
+
+			if (trial_mode5_err < best_err)
+			{
+				best_err = trial_mode5_err;
+
+				opt_results = trial_opt_results5;
+				opt_results.m_rotation = rotation;
+			}
+		} // rotation
+	}
+
+	// Mode 7
+	if (pComp_params->m_alpha_settings.m_use_mode7)
+	{
+		solution solutions[BC7E_MAX_PARTITIONS7];
+		 uint32_t num_solutions;
+		if (forced_partition >= 0)
+		{
+			solutions[0].m_index = forced_partition;
+			num_solutions = 1;
+		}
+		else
+			num_solutions = estimate_partition_list(7, pPixels, pComp_params, solutions, pComp_params->m_alpha_settings.m_max_mode7_partitions_to_try);
+
+		 color_cell_compressor_params params7 = *pParams;
+		
+		params7.m_weights[0] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[0];
+		params7.m_weights[1] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[1];
+		params7.m_weights[2] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[2];
+		params7.m_weights[3] *= pComp_params->m_alpha_settings.m_mode67_error_weight_mul[3];
+		
+		params7.m_pSelector_weights = g_bc7_weights2;
+		params7.m_pSelector_weightsx = (const vec4F *)&g_bc7_weights2x[0];
+		params7.m_num_selector_weights = 4;
+
+		params7.m_comp_bits = 5;
+		params7.m_has_pbits = true;
+		params7.m_endpoints_share_pbit = false;
+				
+		params7.m_has_alpha = true;
+
+		int selectors_temp[16];
+
+		const  bool disable_faster_part_selection = false;
+
+		for ( uint32_t solution_index = 0; solution_index < num_solutions; solution_index++)
+		{
+			const uint32_t trial_partition = solutions[solution_index].m_index;
+			assert(trial_partition < 64);
+
+			const int *pPartition = &g_bc7_partition2[trial_partition * 16];
+
+			color_quad_i subset_colors[2][16];
+
+			uint32_t subset_total_colors7[2];
+			subset_total_colors7[0] = 0;
+			subset_total_colors7[1] = 0;
+			 
+			int subset_pixel_index7[2][16];
+			int subset_selectors7[2][16];
+			color_cell_compressor_results subset_results7[2];
+
+			for ( uint32_t idx = 0; idx < 16; idx++)
+			{
+				const uint32_t p = pPartition[idx];
+				assert(p < 2);
+
+				subset_colors[p][subset_total_colors7[p]] = pPixels[idx];
+				subset_pixel_index7[p][subset_total_colors7[p]] = idx;
+				subset_total_colors7[p]++;
+			}
+
+			uint64_t trial_err = 0;
+			for ( uint32_t subset = 0; subset < 2; subset++)
+			{
+				 color_cell_compressor_results * pResults = &subset_results7[subset];
+
+				pResults->m_pSelectors = &subset_selectors7[subset][0];
+				pResults->m_pSelectors_temp = selectors_temp;
+
+				uint64_t err = color_cell_compression(7, &params7, pResults, pComp_params, subset_total_colors7[subset], &subset_colors[subset][0], (num_solutions <= 2) || disable_faster_part_selection);
+				assert(err == pResults->m_best_overall_err);
+
+				trial_err += err;
+				if (trial_err > best_err)
+					break;
+			} // subset
+
+			if (trial_err < best_err)
+			{
+				best_err = trial_err;
+										
+				opt_results.m_mode = 7;
+				opt_results.m_index_selector = 0;
+				opt_results.m_rotation = 0;
+				opt_results.m_partition = trial_partition;
+
+				for ( uint32_t subset = 0; subset < 2; subset++)
+				{
+					for ( uint32_t i = 0; i < subset_total_colors7[subset]; i++)
+					{
+						const uint32_t pixel_index = subset_pixel_index7[subset][i];
+
+						opt_results.m_selectors[pixel_index] = subset_selectors7[subset][i];
+					}
+
+					opt_results.m_low[subset] = subset_results7[subset].m_low_endpoint;
+					opt_results.m_high[subset] = subset_results7[subset].m_high_endpoint;
+
+					opt_results.m_pbits[subset][0] = subset_results7[subset].m_pbits[0];
+					opt_results.m_pbits[subset][1] = subset_results7[subset].m_pbits[1];
+				}
+				opt_results.m_used_lut = subset_results7[0].m_used_lut || subset_results7[1].m_used_lut;
+			}
+
+		} // solution_index
+
+		if ((num_solutions > 2) && (opt_results.m_mode == 7) && (!disable_faster_part_selection))
+		{
+			const uint32_t trial_partition = opt_results.m_partition;
+			assert(trial_partition < 64);
+
+			const int *pPartition = &g_bc7_partition2[trial_partition * 16];
+
+			color_quad_i subset_colors[2][16];
+
+			uint32_t subset_total_colors7[2];
+			subset_total_colors7[0] = 0;
+			subset_total_colors7[1] = 0;
+			 
+			int subset_pixel_index7[2][16];
+			int subset_selectors7[2][16];
+			color_cell_compressor_results subset_results7[2];
+
+			for ( uint32_t idx = 0; idx < 16; idx++)
+			{
+				const uint32_t p = pPartition[idx];
+				assert(p < 2);
+
+				subset_colors[p][subset_total_colors7[p]] = pPixels[idx];
+				subset_pixel_index7[p][subset_total_colors7[p]] = idx;
+				subset_total_colors7[p]++;
+			}
+
+			uint64_t trial_err = 0;
+			for ( uint32_t subset = 0; subset < 2; subset++)
+			{
+				 color_cell_compressor_results * pResults = &subset_results7[subset];
+
+				pResults->m_pSelectors = &subset_selectors7[subset][0];
+				pResults->m_pSelectors_temp = selectors_temp;
+
+				uint64_t err = color_cell_compression(7, &params7, pResults, pComp_params, subset_total_colors7[subset], &subset_colors[subset][0], true);
+				assert(err == pResults->m_best_overall_err);
+
+				trial_err += err;
+				if (trial_err > best_err)
+					break;
+			} // subset
+
+			if (trial_err < best_err)
+			{
+				best_err = trial_err;
+										
+				for ( uint32_t subset = 0; subset < 2; subset++)
+				{
+					for ( uint32_t i = 0; i < subset_total_colors7[subset]; i++)
+					{
+						const uint32_t pixel_index = subset_pixel_index7[subset][i];
+
+						opt_results.m_selectors[pixel_index] = subset_selectors7[subset][i];
+					}
+
+					opt_results.m_low[subset] = subset_results7[subset].m_low_endpoint;
+					opt_results.m_high[subset] = subset_results7[subset].m_high_endpoint;
+
+					opt_results.m_pbits[subset][0] = subset_results7[subset].m_pbits[0];
+					opt_results.m_pbits[subset][1] = subset_results7[subset].m_pbits[1];
+				}
+				opt_results.m_used_lut = subset_results7[0].m_used_lut || subset_results7[1].m_used_lut;
+			}
+		}
+	}
+
+	if (pBest_err) *pBest_err = best_err;
+	if (pUsed_lut) *pUsed_lut = opt_results.m_used_lut;
+	encode_bc7_block(pBlock, &opt_results);
+}
+
+static void handle_opaque_block(void * pBlock, const  color_quad_i * pPixels, const  bc7e_compress_block_params * pComp_params,  color_cell_compressor_params * pParams, int forced_partition = -1, uint64_t* pBest_err = nullptr, bool* pUsed_lut = nullptr)
+{
+	int selectors_temp[16];
+		
+	bc7_optimization_results opt_results = {};
+		
+	uint64_t best_err = UINT64_MAX;
+
+	// Mode 6
+	if (pComp_params->m_opaque_settings.m_use_mode[6])
+	{
+		pParams->m_pSelector_weights = g_bc7_weights4;
+		pParams->m_pSelector_weightsx = (const vec4F * )&g_bc7_weights4x[0];
+		pParams->m_num_selector_weights = 16;
+
+		pParams->m_comp_bits = 7;
+		pParams->m_has_pbits = true;
+		pParams->m_endpoints_share_pbit = false;
+
+		pParams->m_perceptual = pComp_params->m_perceptual;
+				
+		color_cell_compressor_results results6;						
+		results6.m_pSelectors = opt_results.m_selectors;
+		results6.m_pSelectors_temp = selectors_temp;
+
+		best_err = color_cell_compression(6, pParams, &results6, pComp_params, 16, pPixels, true);
+						
+		opt_results.m_mode = 6;
+		opt_results.m_index_selector = 0;
+		opt_results.m_rotation = 0;
+		opt_results.m_partition = 0;
+		opt_results.m_used_lut = results6.m_used_lut;
+
+		opt_results.m_low[0] = results6.m_low_endpoint;
+		opt_results.m_high[0] = results6.m_high_endpoint;
+
+		opt_results.m_pbits[0][0] = results6.m_pbits[0];
+		opt_results.m_pbits[0][1] = results6.m_pbits[1];
+	}
+
+	solution solutions2[BC7E_MAX_PARTITIONS3];
+	 uint32_t num_solutions2 = 0;
+	if (pComp_params->m_opaque_settings.m_use_mode[1] || pComp_params->m_opaque_settings.m_use_mode[3])
+	{
+		if (forced_partition >= 0)
+		{
+			solutions2[0].m_index = forced_partition;
+			num_solutions2 = 1;
+		}
+		else if (pComp_params->m_opaque_settings.m_max_mode13_partitions_to_try == 1)
+		{
+			solutions2[0].m_index = estimate_partition(1, pPixels, pComp_params);
+			num_solutions2 = 1;
+		}
+		else
+		{
+			num_solutions2 = estimate_partition_list(1, pPixels, pComp_params, solutions2, pComp_params->m_opaque_settings.m_max_mode13_partitions_to_try);
+		}
+	}
+		
+	const  bool disable_faster_part_selection = false;
+								
+	// Mode 1
+	if (pComp_params->m_opaque_settings.m_use_mode[1])
+	{
+		pParams->m_pSelector_weights = g_bc7_weights3;
+		pParams->m_pSelector_weightsx = (const vec4F *)&g_bc7_weights3x[0];
+		pParams->m_num_selector_weights = 8;
+
+		pParams->m_comp_bits = 6;
+		pParams->m_has_pbits = true;
+		pParams->m_endpoints_share_pbit = true;
+
+		pParams->m_perceptual = pComp_params->m_perceptual;
+
+		for ( uint32_t solution_index = 0; solution_index < num_solutions2; solution_index++)
+		{
+			const uint32_t trial_partition = solutions2[solution_index].m_index;
+			assert(trial_partition < 64);
+
+			const int *pPartition = &g_bc7_partition2[trial_partition * 16];
+						
+			color_quad_i subset_colors[2][16];
+
+			uint32_t subset_total_colors1[2];
+			subset_total_colors1[0] = 0;
+			subset_total_colors1[1] = 0;
+				
+			int subset_pixel_index1[2][16];
+			int subset_selectors1[2][16];
+			color_cell_compressor_results subset_results1[2];
+
+			for ( uint32_t idx = 0; idx < 16; idx++)
+			{
+				const uint32_t p = pPartition[idx];
+				assert(p < 2);
+
+				subset_colors[p][subset_total_colors1[p]] = pPixels[idx];
+				subset_pixel_index1[p][subset_total_colors1[p]] = idx;
+				subset_total_colors1[p]++;
+			}
+								
+			uint64_t trial_err = 0;
+			for ( uint32_t subset = 0; subset < 2; subset++)
+			{
+				 color_cell_compressor_results * pResults = &subset_results1[subset];
+
+				pResults->m_pSelectors = &subset_selectors1[subset][0];
+				pResults->m_pSelectors_temp = selectors_temp;
+
+				uint64_t err = color_cell_compression(1, pParams, pResults, pComp_params, subset_total_colors1[subset], &subset_colors[subset][0], (num_solutions2 <= 2) || disable_faster_part_selection);
+				assert(err == pResults->m_best_overall_err);
+
+				trial_err += err;
+				if (trial_err > best_err)
+					break;
+					
+			} // subset
+
+			if (trial_err < best_err)
+			{
+				best_err = trial_err;
+
+				opt_results.m_mode = 1;
+				opt_results.m_index_selector = 0;
+				opt_results.m_rotation = 0;
+				opt_results.m_partition = trial_partition;
+				opt_results.m_used_lut = subset_results1[0].m_used_lut || subset_results1[1].m_used_lut;
+
+				for ( uint32_t subset = 0; subset < 2; subset++)
+				{
+					for ( uint32_t i = 0; i < subset_total_colors1[subset]; i++)
+					{
+						const uint32_t pixel_index = subset_pixel_index1[subset][i];
+
+						opt_results.m_selectors[pixel_index] = subset_selectors1[subset][i];
+					}
+
+					opt_results.m_low[subset] = subset_results1[subset].m_low_endpoint;
+					opt_results.m_high[subset] = subset_results1[subset].m_high_endpoint;
+
+					opt_results.m_pbits[subset][0] = subset_results1[subset].m_pbits[0];
+				}
+			}
+		}
+
+		if ((num_solutions2 > 2) && (opt_results.m_mode == 1) && (!disable_faster_part_selection))
+		{
+			const uint32_t trial_partition = opt_results.m_partition;
+			assert(trial_partition < 64);
+
+			const int *pPartition = &g_bc7_partition2[trial_partition * 16];
+						
+			color_quad_i subset_colors[2][16];
+
+			uint32_t subset_total_colors1[2];
+			subset_total_colors1[0] = 0;
+			subset_total_colors1[1] = 0;
+				
+			int subset_pixel_index1[2][16];
+			int subset_selectors1[2][16];
+			color_cell_compressor_results subset_results1[2];
+
+			for ( uint32_t idx = 0; idx < 16; idx++)
+			{
+				const uint32_t p = pPartition[idx];
+				assert(p < 2);
+
+				subset_colors[p][subset_total_colors1[p]] = pPixels[idx];
+				subset_pixel_index1[p][subset_total_colors1[p]] = idx;
+				subset_total_colors1[p]++;
+			}
+								
+			uint64_t trial_err = 0;
+			for ( uint32_t subset = 0; subset < 2; subset++)
+			{
+				 color_cell_compressor_results * pResults = &subset_results1[subset];
+
+				pResults->m_pSelectors = &subset_selectors1[subset][0];
+				pResults->m_pSelectors_temp = selectors_temp;
+
+				uint64_t err = color_cell_compression(1, pParams, pResults, pComp_params, subset_total_colors1[subset], &subset_colors[subset][0], true);
+				assert(err == pResults->m_best_overall_err);
+
+				trial_err += err;
+				if (trial_err > best_err)
+					break;
+					
+			} // subset
+
+			if (trial_err < best_err)
+			{
+				best_err = trial_err;
+				opt_results.m_used_lut = subset_results1[0].m_used_lut || subset_results1[1].m_used_lut;
+
+				for ( uint32_t subset = 0; subset < 2; subset++)
+				{
+					for ( uint32_t i = 0; i < subset_total_colors1[subset]; i++)
+					{
+						const uint32_t pixel_index = subset_pixel_index1[subset][i];
+						opt_results.m_selectors[pixel_index] = subset_selectors1[subset][i];
+					}
+
+					opt_results.m_low[subset] = subset_results1[subset].m_low_endpoint;
+					opt_results.m_high[subset] = subset_results1[subset].m_high_endpoint;
+
+					opt_results.m_pbits[subset][0] = subset_results1[subset].m_pbits[0];
+				}
+			}
+		}
+	}
+		
+	// Mode 0
+	if (pComp_params->m_opaque_settings.m_use_mode[0])
+	{
+		solution solutions3[BC7E_MAX_PARTITIONS0];
+		 uint32_t num_solutions3 = 0;
+		if (forced_partition >= 0)
+		{
+			solutions3[0].m_index = forced_partition;
+			num_solutions3 = 1;
+		}
+		else if (pComp_params->m_opaque_settings.m_max_mode0_partitions_to_try == 1)
+		{
+			solutions3[0].m_index = estimate_partition(0, pPixels, pComp_params);
+			num_solutions3 = 1;
+		}
+		else
+		{
+			num_solutions3 = estimate_partition_list(0, pPixels, pComp_params, solutions3, pComp_params->m_opaque_settings.m_max_mode0_partitions_to_try);
+		}
+
+		pParams->m_pSelector_weights = g_bc7_weights3;
+		pParams->m_pSelector_weightsx = (const vec4F *)&g_bc7_weights3x[0];
+		pParams->m_num_selector_weights = 8;
+
+		pParams->m_comp_bits = 4;
+		pParams->m_has_pbits = true;
+		pParams->m_endpoints_share_pbit = false;
+
+		pParams->m_perceptual = pComp_params->m_perceptual;
+				
+		for ( uint32_t solution_index = 0; solution_index < num_solutions3; solution_index++)
+		{
+			const uint32_t best_partition0 = solutions3[solution_index].m_index;
+
+			const int *pPartition = &g_bc7_partition3[best_partition0 * 16];
+
+			color_quad_i subset_colors[3][16];
+						
+			uint32_t subset_total_colors0[3];
+			subset_total_colors0[0] = 0;
+			subset_total_colors0[1] = 0;
+			subset_total_colors0[2] = 0;
+
+			int subset_pixel_index0[3][16];
+						
+			for ( uint32_t idx = 0; idx < 16; idx++)
+			{
+				const uint32_t p = pPartition[idx];
+
+				subset_colors[p][subset_total_colors0[p]] = pPixels[idx];
+				subset_pixel_index0[p][subset_total_colors0[p]] = idx;
+				subset_total_colors0[p]++;
+			}
+									
+			color_cell_compressor_results subset_results0[3];
+			int subset_selectors0[3][16];			
+
+			uint64_t mode0_err = 0;
+			for ( uint32_t subset = 0; subset < 3; subset++)
+			{
+				 color_cell_compressor_results * pResults = &subset_results0[subset];
+
+				pResults->m_pSelectors = &subset_selectors0[subset][0];
+				pResults->m_pSelectors_temp = selectors_temp;
+
+				uint64_t err = color_cell_compression(0, pParams, pResults, pComp_params, subset_total_colors0[subset], &subset_colors[subset][0], true);
+				assert(err == pResults->m_best_overall_err);
+
+				mode0_err += err;
+				if (mode0_err > best_err)
+					break;
+			} // subset
+
+			if (mode0_err < best_err)
+			{
+				best_err = mode0_err;
+
+				opt_results.m_mode = 0;
+				opt_results.m_index_selector = 0;
+				opt_results.m_rotation = 0;
+				opt_results.m_partition = best_partition0;
+
+				for ( uint32_t subset = 0; subset < 3; subset++)
+				{
+					for ( uint32_t i = 0; i < subset_total_colors0[subset]; i++)
+					{
+						const uint32_t pixel_index = subset_pixel_index0[subset][i];
+
+						opt_results.m_selectors[pixel_index] = subset_selectors0[subset][i];
+					}
+
+					opt_results.m_low[subset] = subset_results0[subset].m_low_endpoint;
+					opt_results.m_high[subset] = subset_results0[subset].m_high_endpoint;
+
+					opt_results.m_pbits[subset][0] = subset_results0[subset].m_pbits[0];
+					opt_results.m_pbits[subset][1] = subset_results0[subset].m_pbits[1];
+				}
+				opt_results.m_used_lut = subset_results0[0].m_used_lut || subset_results0[1].m_used_lut || subset_results0[2].m_used_lut;
+			}
+		}
+	}
+		
+	// Mode 3
+	if (pComp_params->m_opaque_settings.m_use_mode[3])
+	{
+		pParams->m_pSelector_weights = g_bc7_weights2;
+		pParams->m_pSelector_weightsx = (const vec4F *)&g_bc7_weights2x[0];
+		pParams->m_num_selector_weights = 4;
+
+		pParams->m_comp_bits = 7;
+		pParams->m_has_pbits = true;
+		pParams->m_endpoints_share_pbit = false;
+
+		pParams->m_perceptual = pComp_params->m_perceptual;
+
+		for ( uint32_t solution_index = 0; solution_index < num_solutions2; solution_index++)
+		{
+			const uint32_t trial_partition = solutions2[solution_index].m_index;
+			assert(trial_partition < 64);
+
+			const int *pPartition = &g_bc7_partition2[trial_partition * 16];
+
+			color_quad_i subset_colors[2][16];
+
+			uint32_t subset_total_colors3[2];
+			subset_total_colors3[0] = 0;
+			subset_total_colors3[1] = 0;
+			 
+			int subset_pixel_index3[2][16];
+			int subset_selectors3[2][16];
+			color_cell_compressor_results subset_results3[2];
+
+			for ( uint32_t idx = 0; idx < 16; idx++)
+			{
+				const uint32_t p = pPartition[idx];
+				assert(p < 2);
+
+				subset_colors[p][subset_total_colors3[p]] = pPixels[idx];
+
+				subset_pixel_index3[p][subset_total_colors3[p]] = idx;
+
+				subset_total_colors3[p]++;
+			}
+
+			uint64_t trial_err = 0;
+			for ( uint32_t subset = 0; subset < 2; subset++)
+			{
+				 color_cell_compressor_results * pResults = &subset_results3[subset];
+
+				pResults->m_pSelectors = &subset_selectors3[subset][0];
+				pResults->m_pSelectors_temp = selectors_temp;
+
+				uint64_t err = color_cell_compression(3, pParams, pResults, pComp_params, subset_total_colors3[subset], &subset_colors[subset][0], (num_solutions2 <= 2) || disable_faster_part_selection);
+				assert(err == pResults->m_best_overall_err);
+
+				trial_err += err;
+				if (trial_err > best_err)
+					break;
+			} // subset
+
+			if (trial_err < best_err)
+			{
+				best_err = trial_err;
+										
+				opt_results.m_mode = 3;
+				opt_results.m_index_selector = 0;
+				opt_results.m_rotation = 0;
+				opt_results.m_partition = trial_partition;
+
+				for ( uint32_t subset = 0; subset < 2; subset++)
+				{
+					for ( uint32_t i = 0; i < subset_total_colors3[subset]; i++)
+					{
+						const uint32_t pixel_index = subset_pixel_index3[subset][i];
+
+						opt_results.m_selectors[pixel_index] = subset_selectors3[subset][i];
+					}
+
+					opt_results.m_low[subset] = subset_results3[subset].m_low_endpoint;
+					opt_results.m_high[subset] = subset_results3[subset].m_high_endpoint;
+
+					opt_results.m_pbits[subset][0] = subset_results3[subset].m_pbits[0];
+					opt_results.m_pbits[subset][1] = subset_results3[subset].m_pbits[1];
+				}
+				opt_results.m_used_lut = subset_results3[0].m_used_lut || subset_results3[1].m_used_lut;
+			}
+
+		} // solution_index
+
+		if ((num_solutions2 > 2) && (opt_results.m_mode == 3) && (!disable_faster_part_selection))
+		{
+			const uint32_t trial_partition = opt_results.m_partition;
+			assert(trial_partition < 64);
+
+			const int *pPartition = &g_bc7_partition2[trial_partition * 16];
+
+			color_quad_i subset_colors[2][16];
+
+			uint32_t subset_total_colors3[2];
+			subset_total_colors3[0] = 0;
+			subset_total_colors3[1] = 0;
+			 
+			int subset_pixel_index3[2][16];
+			int subset_selectors3[2][16];
+			color_cell_compressor_results subset_results3[2];
+
+			for ( uint32_t idx = 0; idx < 16; idx++)
+			{
+				const uint32_t p = pPartition[idx];
+				assert(p < 2);
+
+				subset_colors[p][subset_total_colors3[p]] = pPixels[idx];
+
+				subset_pixel_index3[p][subset_total_colors3[p]] = idx;
+
+				subset_total_colors3[p]++;
+			}
+
+			uint64_t trial_err = 0;
+			for ( uint32_t subset = 0; subset < 2; subset++)
+			{
+				 color_cell_compressor_results * pResults = &subset_results3[subset];
+
+				pResults->m_pSelectors = &subset_selectors3[subset][0];
+				pResults->m_pSelectors_temp = selectors_temp;
+
+				uint64_t err = color_cell_compression(3, pParams, pResults, pComp_params, subset_total_colors3[subset], &subset_colors[subset][0], true);
+				assert(err == pResults->m_best_overall_err);
+
+				trial_err += err;
+				if (trial_err > best_err)
+					break;
+			} // subset
+
+			if (trial_err < best_err)
+			{
+				best_err = trial_err;
+										
+				for ( uint32_t subset = 0; subset < 2; subset++)
+				{
+					for ( uint32_t i = 0; i < subset_total_colors3[subset]; i++)
+					{
+						const uint32_t pixel_index = subset_pixel_index3[subset][i];
+
+						opt_results.m_selectors[pixel_index] = subset_selectors3[subset][i];
+					}
+
+					opt_results.m_low[subset] = subset_results3[subset].m_low_endpoint;
+					opt_results.m_high[subset] = subset_results3[subset].m_high_endpoint;
+
+					opt_results.m_pbits[subset][0] = subset_results3[subset].m_pbits[0];
+					opt_results.m_pbits[subset][1] = subset_results3[subset].m_pbits[1];
+				}
+				opt_results.m_used_lut = subset_results3[0].m_used_lut || subset_results3[1].m_used_lut;
+			}
+		}
+	}
+
+	// Mode 5
+	if ((!pComp_params->m_perceptual) && (pComp_params->m_opaque_settings.m_use_mode[5]))
+	{
+		 color_cell_compressor_params params5 = *pParams;
+
+		for ( uint32_t rotation = 0; rotation < 4; rotation++)
+		{
+			if ((pComp_params->m_mode5_rotation_mask & (1 << rotation)) == 0)
+				continue;
+
+			memcpy(params5.m_weights, pParams->m_weights, sizeof(params5.m_weights));
+			if (rotation)
+				swapu(&params5.m_weights[rotation - 1], &params5.m_weights[3]);
+
+			color_quad_i rot_pixels[16];
+			const  color_quad_i * pTrial_pixels = pPixels;
+			uint32_t trial_lo_a = 255, trial_hi_a = 255;
+			if (rotation)
+			{
+				trial_lo_a = 255;
+				trial_hi_a = 0;
+
+				for ( uint32_t i = 0; i < 16; i++)
+				{
+					color_quad_i c = pPixels[i];
+					swapi(&c.m_c[3], &c.m_c[rotation - 1]);
+					rot_pixels[i] = c;
+
+					trial_lo_a = minimumu(trial_lo_a, c.m_c[3]);
+					trial_hi_a = maximumu(trial_hi_a, c.m_c[3]);
+				}
+
+				pTrial_pixels = rot_pixels;
+			}
+
+			bc7_optimization_results trial_opt_results5;
+
+			uint64_t trial_mode5_err = 0;
+
+			handle_alpha_block_mode5(pTrial_pixels, pComp_params, &params5, trial_lo_a, trial_hi_a, &trial_opt_results5, &trial_mode5_err);
+
+			if (trial_mode5_err < best_err)
+			{
+				best_err = trial_mode5_err;
+
+				opt_results = trial_opt_results5;
+				opt_results.m_rotation = rotation;
+			}
+		} // rotation
+	}
+
+	// Mode 2
+	if (pComp_params->m_opaque_settings.m_use_mode[2])
+	{
+		solution solutions3[BC7E_MAX_PARTITIONS2];
+		 uint32_t num_solutions3 = 0;
+		if (forced_partition >= 0)
+		{
+			solutions3[0].m_index = forced_partition;
+			num_solutions3 = 1;
+		}
+		else if (pComp_params->m_opaque_settings.m_max_mode2_partitions_to_try == 1)
+		{
+			solutions3[0].m_index = estimate_partition(2, pPixels, pComp_params);
+			num_solutions3 = 1;
+		}
+		else
+		{
+			num_solutions3 = estimate_partition_list(2, pPixels, pComp_params, solutions3, pComp_params->m_opaque_settings.m_max_mode2_partitions_to_try);
+		}
+
+		pParams->m_pSelector_weights = g_bc7_weights2;
+		pParams->m_pSelector_weightsx = (const vec4F *)&g_bc7_weights2x[0];
+		pParams->m_num_selector_weights = 4;
+
+		pParams->m_comp_bits = 5;
+		pParams->m_has_pbits = false;
+		pParams->m_endpoints_share_pbit = false;
+
+		pParams->m_perceptual = pComp_params->m_perceptual;
+
+		for ( uint32_t solution_index = 0; solution_index < num_solutions3; solution_index++)
+		{
+			const int32_t best_partition2 = solutions3[solution_index].m_index;
+						
+			uint32_t subset_total_colors2[3];
+			subset_total_colors2[0] = 0;
+			subset_total_colors2[1] = 0;
+			subset_total_colors2[2] = 0;
+
+			int subset_pixel_index2[3][16];
+							
+			const int *pPartition = &g_bc7_partition3[best_partition2 * 16];
+
+			color_quad_i subset_colors[3][16];
+
+			for ( uint32_t idx = 0; idx < 16; idx++)
+			{
+				const uint32_t p = pPartition[idx];
+
+				subset_colors[p][subset_total_colors2[p]] = pPixels[idx];
+
+				subset_pixel_index2[p][subset_total_colors2[p]] = idx;
+
+				subset_total_colors2[p]++;
+			}
+			
+			int subset_selectors2[3][16];
+			color_cell_compressor_results subset_results2[3];
+						
+			uint64_t mode2_err = 0;
+			for ( uint32_t subset = 0; subset < 3; subset++)
+			{
+				 color_cell_compressor_results * pResults = &subset_results2[subset];
+
+				pResults->m_pSelectors = &subset_selectors2[subset][0];
+				pResults->m_pSelectors_temp = selectors_temp;
+
+				uint64_t err = color_cell_compression(2, pParams, pResults, pComp_params, subset_total_colors2[subset], &subset_colors[subset][0], true);
+				assert(err == pResults->m_best_overall_err);
+
+				mode2_err += err;
+				if (mode2_err > best_err)
+					break;
+			} // subset
+
+			if (mode2_err < best_err)
+			{
+				best_err = mode2_err;
+
+				opt_results.m_mode = 2;
+				opt_results.m_index_selector = 0;
+				opt_results.m_rotation = 0;
+				opt_results.m_partition = best_partition2;
+
+				for ( uint32_t subset = 0; subset < 3; subset++)
+				{
+					for ( uint32_t i = 0; i < subset_total_colors2[subset]; i++)
+					{
+						const uint32_t pixel_index = subset_pixel_index2[subset][i];
+
+						opt_results.m_selectors[pixel_index] = subset_selectors2[subset][i];
+					}
+
+					opt_results.m_low[subset] = subset_results2[subset].m_low_endpoint;
+					opt_results.m_high[subset] = subset_results2[subset].m_high_endpoint;
+				}
+				opt_results.m_used_lut = subset_results2[0].m_used_lut || subset_results2[1].m_used_lut || subset_results2[2].m_used_lut;
+			}
+		}
+	}
+
+	// Mode 4
+	if ((!pComp_params->m_perceptual) && (pComp_params->m_opaque_settings.m_use_mode[4]))
+	{
+		 color_cell_compressor_params params4 = *pParams;
+
+		for ( uint32_t rotation = 0; rotation < 4; rotation++)
+		{
+			if ((pComp_params->m_mode4_rotation_mask & (1 << rotation)) == 0)
+				continue;
+
+			memcpy(params4.m_weights, pParams->m_weights, sizeof(params4.m_weights));
+			if (rotation)
+				swapu(&params4.m_weights[rotation - 1], &params4.m_weights[3]);
+							
+			color_quad_i rot_pixels[16];
+			const  color_quad_i * pTrial_pixels = pPixels;
+			uint32_t trial_lo_a = 255, trial_hi_a = 255;
+			if (rotation)
+			{
+				trial_lo_a = 255;
+				trial_hi_a = 0;
+
+				for ( uint32_t i = 0; i < 16; i++)
+				{
+					color_quad_i c = pPixels[i];
+					swapi(&c.m_c[3], &c.m_c[rotation - 1]);
+					rot_pixels[i] = c;
+
+					trial_lo_a = minimumu(trial_lo_a, c.m_c[3]);
+					trial_hi_a = maximumu(trial_hi_a, c.m_c[3]);
+				}
+
+				pTrial_pixels = rot_pixels;
+			}
+
+			bc7_optimization_results trial_opt_results4;
+
+			uint64_t trial_mode4_err = best_err;
+
+			handle_alpha_block_mode4(pTrial_pixels, pComp_params, &params4, trial_lo_a, trial_hi_a, &trial_opt_results4, &trial_mode4_err);
+
+			if (trial_mode4_err < best_err)
+			{
+				best_err = trial_mode4_err;
+
+				opt_results.m_mode = 4;
+				opt_results.m_index_selector = trial_opt_results4.m_index_selector;
+				opt_results.m_rotation = rotation;
+				opt_results.m_partition = 0;
+				opt_results.m_used_lut = trial_opt_results4.m_used_lut;
+
+				opt_results.m_low[0] = trial_opt_results4.m_low[0];
+				opt_results.m_high[0] = trial_opt_results4.m_high[0];
+
+				for ( uint32_t i = 0; i < 16; i++)
+					opt_results.m_selectors[i] = trial_opt_results4.m_selectors[i];
+				
+				for ( uint32_t i = 0; i < 16; i++)
+					opt_results.m_alpha_selectors[i] = trial_opt_results4.m_alpha_selectors[i];
+			}
+		} // rotation
+	}
+	
+	if (pBest_err) *pBest_err = best_err;
+	if (pUsed_lut) *pUsed_lut = opt_results.m_used_lut;
+	encode_bc7_block(pBlock, &opt_results);
+}
+
+// all solid color blocks can be 100% perfectly encoded with just mode 5
+static void handle_block_solid(void * pBlock, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t ca, bool* pUsed_lut = nullptr)
+{
+	if (pUsed_lut) *pUsed_lut = true; // solid blocks always use the mode-5 optimal-endpoint LUT
+	uint32_t er = g_bc7_mode_5_optimal_endpoints[cr];
+	uint32_t eg = g_bc7_mode_5_optimal_endpoints[cg];
+	uint32_t eb = g_bc7_mode_5_optimal_endpoints[cb];
+	color_quad_i lp, hp;
+	color_quad_i_set(&lp, er & 0xFF, eg & 0xFF, eb & 0xFF, ca);
+	color_quad_i_set(&hp, er >> 8, eg >> 8, eb >> 8, ca);
+
+	bc7_optimization_results opt;
+	opt.m_mode = 5;
+	opt.m_low[0] = lp;
+	opt.m_high[0] = hp;
+	opt.m_pbits[0][0] = 0;
+	opt.m_pbits[0][1] = 0;
+	opt.m_index_selector = 0;
+	opt.m_rotation = 0;
+	opt.m_partition = 0;
+	for ( int i = 0; i < 16; ++i)
+		opt.m_selectors[i] = BC7E_MODE_5_OPTIMAL_INDEX;
+	for ( int i = 0; i < 16; ++i)
+		opt.m_alpha_selectors[i] = 0;
+	encode_bc7_block(pBlock, &opt);
+}
+
+static void handle_opaque_block_mode6(void * pBlock, const  color_quad_i * pPixels, const  bc7e_compress_block_params * pComp_params,  color_cell_compressor_params * pParams, bool* pUsed_lut = nullptr)
+{
+	int selectors_temp[16];
+		
+	bc7_optimization_results opt_results = {};
+		
+	uint64_t best_err = UINT64_MAX;
+
+	// Mode 6
+	pParams->m_pSelector_weights = g_bc7_weights4;
+	pParams->m_pSelector_weightsx = (const vec4F * )&g_bc7_weights4x[0];
+	pParams->m_num_selector_weights = 16;
+
+	pParams->m_comp_bits = 7;
+	pParams->m_has_pbits = true;
+	pParams->m_endpoints_share_pbit = false;
+
+	pParams->m_perceptual = pComp_params->m_perceptual;
+				
+	color_cell_compressor_results results6;						
+	results6.m_pSelectors = opt_results.m_selectors;
+	results6.m_pSelectors_temp = selectors_temp;
+
+	best_err = color_cell_compression(6, pParams, &results6, pComp_params, 16, pPixels, true);
+	NOTE_UNUSED(best_err); // mode 6 here is unconditional; the returned error isn't compared - silence clang -Wunused-but-set-variable
+						
+	opt_results.m_mode = 6;
+	opt_results.m_index_selector = 0;
+	opt_results.m_rotation = 0;
+	opt_results.m_partition = 0;
+
+	opt_results.m_low[0] = results6.m_low_endpoint;
+	opt_results.m_high[0] = results6.m_high_endpoint;
+
+	opt_results.m_pbits[0][0] = results6.m_pbits[0];
+	opt_results.m_pbits[0][1] = results6.m_pbits[1];
+
+	if (pUsed_lut) *pUsed_lut = results6.m_used_lut;
+	encode_bc7_block_mode6(pBlock, &opt_results);
+}
+
+// Compress a single 4x4 RGBA block to ONE specified BC7 mode only. Additive API: it does
+// not touch bc7e_compress_blocks(). Writes the 16-byte block to pBlock and returns its error.
+//   mode           : 0..7
+//   partition      : modes 0,1,2,3,7 -> partition pattern index, or -1 to auto-select the
+//                    optimal one (existing estimate_partition* logic). Ignored for 4,5,6.
+//   rotation       : modes 4,5 -> dual-plane component rotation [0..3] (0 = none).
+//   index_selector : mode 4 -> 0 or 1 (which index set is the scalar channel).
+// Forcing an opaque mode (0-3) on a block with alpha drops alpha (those modes force A=255).
+uint64_t bc7e_compress_block_single_mode(uint64_t* pBlock, const uint32_t* pPixelsRGBA, const bc7e_compress_block_params* pComp_params,
+	uint32_t mode, int partition, uint32_t rotation, uint32_t index_selector)
+{
+	assert(g_codec_initialized);
+	assert(mode <= 7);
+
+	// Extract the 16 pixels + alpha range (mirrors bc7e_compress_blocks).
+	const color_quad_u8* pSrcPixels = (const color_quad_u8*)pPixelsRGBA;
+	color_quad_i temp_pixels[16];
+	int lo_a = 255, hi_a = 0;
+	for (uint32_t i = 0; i < 16; i++)
+	{
+		color_quad_u8 c = pSrcPixels[i];
+		temp_pixels[i].m_c[0] = c.m_c[0];
+		temp_pixels[i].m_c[1] = c.m_c[1];
+		temp_pixels[i].m_c[2] = c.m_c[2];
+		temp_pixels[i].m_c[3] = c.m_c[3];
+		lo_a = min(lo_a, (int)c.m_c[3]);
+		hi_a = max(hi_a, (int)c.m_c[3]);
+	}
+
+	color_cell_compressor_params ccparams;
+	color_cell_compressor_params_clear(&ccparams);
+	memcpy(ccparams.m_weights, pComp_params->m_weights, sizeof(ccparams.m_weights));
+
+	// Single-mode params: copy caller's settings, disable every mode, enable only this one.
+	bc7e_compress_block_params p = *pComp_params;
+	for (uint32_t i = 0; i < 7; i++)
+		p.m_opaque_settings.m_use_mode[i] = false;
+	p.m_alpha_settings.m_use_mode4 = false;
+	p.m_alpha_settings.m_use_mode5 = false;
+	p.m_alpha_settings.m_use_mode6 = false;
+	p.m_alpha_settings.m_use_mode7 = false;
+	p.m_mode6_only = false;
+
+	// Validate/clamp a caller-supplied partition to the mode's valid range; -1 means auto-select.
+	// Partition field width: mode 0 is 4 bits (16 patterns); modes 1,2,3,7 are 6 bits (64 patterns);
+	// modes 4,5,6 have no partition field.
+	int forced_partition = partition;
+	if (forced_partition >= 0)
+	{
+		const int num_partitions = 1 << g_bc7_partition_bits[mode]; // 1 for non-partitioned modes
+		assert((num_partitions > 1) && "partition index supplied for a non-partitioned BC7 mode (4/5/6)");
+		assert((forced_partition < num_partitions) && "partition index out of range for this BC7 mode (mode 0: 0-15, modes 1/2/3/7: 0-63)");
+		if (num_partitions <= 1)
+			forced_partition = -1;              // mode has no partition field; ignore
+		else if (forced_partition >= num_partitions)
+			forced_partition = num_partitions - 1;
+	}
+
+	uint64_t best_err = UINT64_MAX;
+
+	if (mode <= 3)
+	{
+		// Opaque-only modes (alpha dropped / forced to 255).
+		p.m_opaque_settings.m_use_mode[mode] = true;
+		handle_opaque_block(pBlock, temp_pixels, &p, &ccparams, forced_partition, &best_err);
+	}
+	else if ((mode == 4) || (mode == 5))
+	{
+		// A non-zero rotation is only reachable under the handler's linear-metric gate, so
+		// force linear when one is explicitly requested (keeps the result valid + deterministic).
+		if (rotation != 0)
+			p.m_perceptual = false;
+
+		if (mode == 4)
+		{
+			p.m_alpha_settings.m_use_mode4 = true;
+			p.m_alpha_settings.m_use_mode4_rotation = true;
+			p.m_mode4_rotation_mask = 1u << (rotation & 3);
+			p.m_mode4_index_mask = 1u << (index_selector & 1);
+		}
+		else
+		{
+			p.m_alpha_settings.m_use_mode5 = true;
+			p.m_alpha_settings.m_use_mode5_rotation = true;
+			p.m_mode5_rotation_mask = 1u << (rotation & 3);
+		}
+		handle_alpha_block(pBlock, temp_pixels, &p, &ccparams, (uint32_t)lo_a, (uint32_t)hi_a, -1, &best_err);
+	}
+	else if (mode == 6)
+	{
+		p.m_alpha_settings.m_use_mode6 = true;
+		handle_alpha_block(pBlock, temp_pixels, &p, &ccparams, (uint32_t)lo_a, (uint32_t)hi_a, -1, &best_err);
+	}
+	else // mode == 7
+	{
+		p.m_alpha_settings.m_use_mode7 = true;
+		handle_alpha_block(pBlock, temp_pixels, &p, &ccparams, (uint32_t)lo_a, (uint32_t)hi_a, forced_partition, &best_err);
+	}
+
+	return best_err;
+}
+
+ void bc7e_compress_blocks( uint32_t num_blocks,  uint64_t *  pBlocks, const  uint32_t *  pPixelsRGBA, const  bc7e_compress_block_params *  pComp_params, uint8_t * pUsed_lut)
+{
+	if (!g_codec_initialized)
+	{
+		// Caller has forgotten to initialize the codec, or another thread is still working on that. We can't continue.
+		// What do we do here?
+		assert(0);
+
+		memset(pBlocks, 0, num_blocks * 16);
+		return;
+	}
+
+	 color_cell_compressor_params params;
+	color_cell_compressor_params_clear(&params);
+
+	memcpy(params.m_weights, pComp_params->m_weights, sizeof(params.m_weights));
+
+	assert(pComp_params->m_mode4_rotation_mask != 0);
+	assert(pComp_params->m_mode4_index_mask != 0);
+	assert(pComp_params->m_mode5_rotation_mask != 0);
+	assert(pComp_params->m_uber1_mask != 0);
+	
+	for (int32_t block_index = 0; block_index < (int32_t)num_blocks; block_index++)
+	{
+		const  color_quad_u8 * pSrcPixels = &((const  color_quad_u8 *)(pPixelsRGBA))[block_index * 16];
+
+		 color_quad_i temp_pixels[16];
+
+		int lo_r = 255, hi_r = 0;
+		int lo_g = 255, hi_g = 0;
+		int lo_b = 255, hi_b = 0;
+		float lo_a = 255, hi_a = 0;
+		
+		for ( uint32_t i = 0; i < 16; i++)
+		{
+			color_quad_u8 c = pSrcPixels[i];
+
+			int r = c.m_c[0];
+			int g = c.m_c[1];
+			int b = c.m_c[2];
+			int a = c.m_c[3];
+												
+			temp_pixels[i].m_c[0] = r;
+			temp_pixels[i].m_c[1] = g;
+			temp_pixels[i].m_c[2] = b;
+			temp_pixels[i].m_c[3] = a;
+
+			lo_r = min(lo_r, r); hi_r = max(hi_r, r);
+			lo_g = min(lo_g, g); hi_g = max(hi_g, g);
+			lo_b = min(lo_b, b); hi_b = max(hi_b, b);
+
+			float fa = (float)(a);
+
+			lo_a = min(lo_a, fa);
+			hi_a = max(hi_a, fa);
+		}
+
+		bool all_same = lo_r==hi_r && lo_g==hi_g && lo_b==hi_b && lo_a==hi_a;
+
+		 uint64_t * pBlock = &pBlocks[block_index * 2];
+
+		bool block_used_lut = false;
+
+		cif (all_same)
+			handle_block_solid(pBlock, lo_r, lo_g, lo_b, float_to_uint8(lo_a), &block_used_lut);
+		else
+		{
+			const bool has_alpha = (lo_a < 255);
+			// TODO: alpha block mode 6 only
+			cif (has_alpha)
+				handle_alpha_block(pBlock, temp_pixels, pComp_params, &params, (int)lo_a, (int)hi_a, -1, nullptr, &block_used_lut);
+			else
+			{
+				if (pComp_params->m_mode6_only)
+					handle_opaque_block_mode6(pBlock, temp_pixels, pComp_params, &params, &block_used_lut);
+				else
+					handle_opaque_block(pBlock, temp_pixels, pComp_params, &params, -1, nullptr, &block_used_lut);
+			}
+		}
+
+		if (pUsed_lut)
+			pUsed_lut[block_index] = block_used_lut ? 1 : 0;
+	}
+}
+
+ void bc7e_compress_block_params_init(bc7e_compress_block_params *  p,  bool perceptual)
+{
+	p->m_max_partitions_mode[0] = BC7E_MAX_PARTITIONS0;
+	p->m_max_partitions_mode[1] = BC7E_MAX_PARTITIONS1;
+	p->m_max_partitions_mode[2] = BC7E_MAX_PARTITIONS2;
+	p->m_max_partitions_mode[3] = BC7E_MAX_PARTITIONS3;
+	p->m_max_partitions_mode[4] = 0;
+	p->m_max_partitions_mode[5] = 0;
+	p->m_max_partitions_mode[6] = 0;
+	p->m_max_partitions_mode[7] = BC7E_MAX_PARTITIONS7;
+
+	p->m_use_luts = true; // default ON, matching bc7e's original behavior; callers may disable (e.g. XBC7 turns it off below lossless Q)
+
+	p->m_perceptual = perceptual;
+	if (perceptual)
+	{
+		p->m_weights[0] = 128;
+		p->m_weights[1] = 64;
+		p->m_weights[2] = 16;
+		p->m_weights[3] = 256;
+	}
+	else
+	{
+		p->m_weights[0] = 1;
+		p->m_weights[1] = 1;
+		p->m_weights[2] = 1;
+		p->m_weights[3] = 1;
+	}
+
+	p->m_pbit_search = false;
+	p->m_mode6_only = false;
+	p->m_refinement_passes = 1;
+	p->m_mode4_rotation_mask = 0xF;
+	p->m_mode4_index_mask = 3;
+	p->m_mode5_rotation_mask = 0xF;
+	p->m_uber1_mask = 7;
+	for ( uint32_t i = 0; i < 7; i++)
+		p->m_opaque_settings.m_use_mode[i] = true;
+	p->m_opaque_settings.m_max_mode13_partitions_to_try = 1;
+	p->m_opaque_settings.m_max_mode0_partitions_to_try = 1;
+	p->m_opaque_settings.m_max_mode2_partitions_to_try = 1;
+	p->m_alpha_settings.m_use_mode4 = true;
+	p->m_alpha_settings.m_use_mode5 = true;
+	p->m_alpha_settings.m_use_mode6 = true;
+	p->m_alpha_settings.m_use_mode7 = true;
+	p->m_alpha_settings.m_use_mode4_rotation = true;
+	p->m_alpha_settings.m_use_mode5_rotation = true;
+	p->m_alpha_settings.m_max_mode7_partitions_to_try = 1;
+	p->m_alpha_settings.m_mode67_error_weight_mul[0] = 1;
+	p->m_alpha_settings.m_mode67_error_weight_mul[1] = 1;
+	p->m_alpha_settings.m_mode67_error_weight_mul[2] = 1;
+	p->m_alpha_settings.m_mode67_error_weight_mul[3] = 1;
+	p->m_uber_level = 0;
+}
+
+ void bc7e_compress_block_params_init_slowest(bc7e_compress_block_params *  p,  bool perceptual)
+{
+	bc7e_compress_block_params_init(p, perceptual);
+
+	p->m_opaque_settings.m_max_mode13_partitions_to_try = 4;
+	p->m_opaque_settings.m_max_mode0_partitions_to_try = 4;
+	p->m_opaque_settings.m_max_mode2_partitions_to_try = 4;
+	p->m_alpha_settings.m_max_mode7_partitions_to_try = 4;
+
+	p->m_pbit_search = true;
+	p->m_uber_level = 4;
+}
+
+ void bc7e_compress_block_params_init_veryslow(bc7e_compress_block_params *  p,  bool perceptual)
+{
+	bc7e_compress_block_params_init(p, perceptual);
+
+	p->m_opaque_settings.m_max_mode13_partitions_to_try = 2;
+	p->m_opaque_settings.m_max_mode0_partitions_to_try = 2;
+	p->m_opaque_settings.m_max_mode2_partitions_to_try = 2;
+	p->m_alpha_settings.m_max_mode7_partitions_to_try = 2;
+
+	p->m_pbit_search = true;
+	p->m_uber_level = 2;
+}
+
+ void bc7e_compress_block_params_init_slow(bc7e_compress_block_params *  p,  bool perceptual)
+{
+	bc7e_compress_block_params_init(p, perceptual);
+
+	p->m_alpha_settings.m_max_mode7_partitions_to_try = 2;
+
+	p->m_pbit_search = true;
+	p->m_uber_level = 0;
+}
+
+ void bc7e_compress_block_params_init_basic(bc7e_compress_block_params *  p,  bool perceptual)
+{
+	bc7e_compress_block_params_init(p, perceptual);
+
+	if (perceptual)
+	{
+		p->m_opaque_settings.m_use_mode[0] = false;
+		p->m_opaque_settings.m_use_mode[2] = false;
+		p->m_opaque_settings.m_use_mode[3] = false;
+		p->m_opaque_settings.m_use_mode[4] = false;
+		p->m_opaque_settings.m_use_mode[5] = false;
+		
+		p->m_pbit_search = false;
+		p->m_uber_level = 1;
+	}
+	else
+	{
+		p->m_max_partitions_mode[1] = 32;
+		p->m_max_partitions_mode[2] = 32;
+		p->m_max_partitions_mode[3] = 32;
+		p->m_max_partitions_mode[7] = 32;
+
+		p->m_opaque_settings.m_use_mode[2] = false;
+		
+		p->m_pbit_search = false;
+		p->m_uber_level = 1;
+	}
+}
+
+ void bc7e_compress_block_params_init_fast(bc7e_compress_block_params *  p,  bool perceptual)
+{
+	bc7e_compress_block_params_init(p, perceptual);
+
+	if (perceptual)
+	{
+		p->m_opaque_settings.m_use_mode[0] = false;
+		p->m_opaque_settings.m_use_mode[2] = false;
+		p->m_opaque_settings.m_use_mode[3] = false;
+		p->m_opaque_settings.m_use_mode[4] = false;
+		p->m_opaque_settings.m_use_mode[5] = false;
+
+		p->m_alpha_settings.m_use_mode5 = false;
+	
+		p->m_opaque_settings.m_max_mode13_partitions_to_try = 1;
+		
+		p->m_pbit_search = false;
+		p->m_uber_level = 0;
+	}		
+	else
+	{
+		p->m_opaque_settings.m_use_mode[0] = false;
+		p->m_opaque_settings.m_use_mode[2] = false;
+		p->m_opaque_settings.m_use_mode[4] = false;
+		p->m_opaque_settings.m_use_mode[5] = false;
+
+		p->m_alpha_settings.m_use_mode5 = false;
+	
+		p->m_opaque_settings.m_max_mode13_partitions_to_try = 2;
+		
+		p->m_pbit_search = false;
+		p->m_uber_level = 0;
+	}
+}
+
+ void bc7e_compress_block_params_init_veryfast(bc7e_compress_block_params *  p,  bool perceptual)
+{
+	bc7e_compress_block_params_init(p, perceptual);
+
+	if (perceptual)
+	{
+		p->m_opaque_settings.m_use_mode[0] = false;
+		p->m_opaque_settings.m_use_mode[2] = false;
+		p->m_opaque_settings.m_use_mode[3] = false;
+		p->m_opaque_settings.m_use_mode[4] = false;
+		p->m_opaque_settings.m_use_mode[5] = false;
+
+		p->m_alpha_settings.m_use_mode5 = false;
+		
+		p->m_pbit_search = false;
+		p->m_uber_level = 0;
+	}
+	else
+	{
+		p->m_opaque_settings.m_use_mode[2] = false;
+		p->m_opaque_settings.m_use_mode[4] = false;
+		p->m_opaque_settings.m_use_mode[5] = false;
+
+		p->m_alpha_settings.m_use_mode5 = false;
+		
+		p->m_pbit_search = false;
+		p->m_uber_level = 0;
+	}
+}
+
+ void bc7e_compress_block_params_init_ultrafast(bc7e_compress_block_params *  p,  bool perceptual)
+{
+	bc7e_compress_block_params_init(p, perceptual);
+
+	p->m_mode6_only = true;
+
+	p->m_alpha_settings.m_use_mode4 = true;
+	p->m_alpha_settings.m_use_mode5 = true;
+	p->m_alpha_settings.m_use_mode7 = false;
+
+	p->m_mode4_rotation_mask = 1+4;
+	p->m_mode4_index_mask = 3;
+	p->m_mode5_rotation_mask = 1;
+				
+	p->m_pbit_search = false;
+	p->m_uber_level = 0;
+}
+
+} // namespace bc7e_scalar
diff --git a/encoder/basisu_bc7e_scalar.h b/encoder/basisu_bc7e_scalar.h
new file mode 100644
index 0000000..8914bb0
--- /dev/null
+++ b/encoder/basisu_bc7e_scalar.h
@@ -0,0 +1,94 @@
+// bc7e_scalar.h - Pure scalar C++17 port of bc7e.ispc (no ISPC, no SIMD).
+// Drop-in alternative BC7 encoder exposing the same surface as the ISPC build,
+// but in the bc7e_scalar:: namespace so it can run side-by-side with ispc:: for
+// quality comparison. See bc7e.ispc for the original; this is de-SIMD'd to
+// encode one 4x4 block at a time.
+#pragma once
+
+#include <stdint.h>
+
+namespace bc7e_scalar
+{
+	// Mirrors ispc::bc7e_compress_block_params (identical layout/semantics).
+	struct bc7e_compress_block_params
+	{
+		uint32_t m_max_partitions_mode[8];
+
+		uint32_t m_weights[4];
+
+		uint32_t m_uber_level;
+		uint32_t m_refinement_passes;
+
+		uint32_t m_mode4_rotation_mask;
+		uint32_t m_mode4_index_mask;
+		uint32_t m_mode5_rotation_mask;
+		uint32_t m_uber1_mask;
+
+		bool m_perceptual;
+		bool m_pbit_search;
+		bool m_mode6_only;
+		// When false, color_cell_compression() is NOT allowed to use the precomputed
+		// "one color" optimal-endpoint lookup tables (the solid/allSame and average-
+		// color paths). Disabling them avoids the extreme-endpoint "trap" blocks that
+		// decode badly under lossy weight coding. Default TRUE (matches bc7e's original
+		// behavior); callers disable it when needed (e.g. XBC7 below lossless Q).
+		// (Repurposed from the former m_unused0 padding bool.)
+		bool m_use_luts;
+
+		struct
+		{
+			uint32_t m_max_mode13_partitions_to_try;
+			uint32_t m_max_mode0_partitions_to_try;
+			uint32_t m_max_mode2_partitions_to_try;
+			bool m_use_mode[7];
+			bool m_unused1;
+		} m_opaque_settings;
+
+		struct
+		{
+			uint32_t m_max_mode7_partitions_to_try;
+			uint32_t m_mode67_error_weight_mul[4];
+
+			bool m_use_mode4;
+			bool m_use_mode5;
+			bool m_use_mode6;
+			bool m_use_mode7;
+
+			bool m_use_mode4_rotation;
+			bool m_use_mode5_rotation;
+			bool m_unused2;
+			bool m_unused3;
+		} m_alpha_settings;
+	};
+
+	void bc7e_compress_block_init();
+
+	void bc7e_compress_block_params_init(bc7e_compress_block_params* p, bool perceptual);
+	void bc7e_compress_block_params_init_basic(bc7e_compress_block_params* p, bool perceptual);
+	void bc7e_compress_block_params_init_fast(bc7e_compress_block_params* p, bool perceptual);
+	void bc7e_compress_block_params_init_slow(bc7e_compress_block_params* p, bool perceptual);
+	void bc7e_compress_block_params_init_slowest(bc7e_compress_block_params* p, bool perceptual);
+	void bc7e_compress_block_params_init_ultrafast(bc7e_compress_block_params* p, bool perceptual);
+	void bc7e_compress_block_params_init_veryfast(bc7e_compress_block_params* p, bool perceptual);
+	void bc7e_compress_block_params_init_veryslow(bc7e_compress_block_params* p, bool perceptual);
+
+	// pUsed_lut (optional, one byte per block): set to 1 if the WINNING encoding of that
+	// block used the precomputed "one color" optimal-endpoint lookup tables on any subset
+	// (solid/allSame or average-color path) -- a hint that the block placed endpoints at
+	// extreme positions and may be "weird"/fragile under lossy weight recoding. 0 otherwise.
+	void bc7e_compress_blocks(uint32_t num_blocks, uint64_t* pBlocks, const uint32_t* pPixelsRGBA, const bc7e_compress_block_params* pComp_params, uint8_t* pUsed_lut = nullptr);
+
+	// Compress a single 4x4 RGBA block (16 pixels) to ONE specified BC7 mode only, using
+	// the given settings. Writes the 16-byte block to pBlock and returns its encoding error.
+	// Additive testing API - does not affect bc7e_compress_blocks().
+	//   mode           : 0..7
+	//   partition      : modes 0,1,2,3,7 -> partition pattern index; -1 = auto-select optimal
+	//                    (existing logic). Ignored for modes 4,5,6.
+	//   rotation       : modes 4,5 -> dual-plane component rotation [0..3] (0 = none). A non-zero
+	//                    rotation forces linear (non-perceptual) error internally. Ignored otherwise.
+	//   index_selector : mode 4 -> 0 or 1 index-set selector. Ignored otherwise.
+	// Note: forcing an opaque mode (0-3) on a block with alpha drops alpha (A becomes 255).
+	uint64_t bc7e_compress_block_single_mode(uint64_t* pBlock, const uint32_t* pPixelsRGBA, const bc7e_compress_block_params* pComp_params,
+		uint32_t mode, int partition = -1, uint32_t rotation = 0, uint32_t index_selector = 0);
+
+} // namespace bc7e_scalar
diff --git a/encoder/basisu_dds_export.cpp b/encoder/basisu_dds_export.cpp
new file mode 100644
index 0000000..22663eb
--- /dev/null
+++ b/encoder/basisu_dds_export.cpp
@@ -0,0 +1,569 @@
+// basisu_dds_export.cpp
+// Copyright (C) 2019-2025 Binomial LLC. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// See basisu_dds_export.h for an overview.
+#include "basisu_dds_export.h"
+
+#include "basisu_comp.h"
+#include "basisu_bc7e_scalar.h"
+#include "../transcoder/basisu_transcoder_internal.h"
+#include "../transcoder/basisu_transcoder_uastc.h"
+
+namespace basisu
+{
+	// --- DXGI format codes (only the ones we emit). ---
+	enum
+	{
+		DXGI_FORMAT_R8G8B8A8_UNORM = 28,
+		DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29,
+		DXGI_FORMAT_R8G8_UNORM = 49,
+		DXGI_FORMAT_R8_UNORM = 61,
+		DXGI_FORMAT_BC1_UNORM = 71,
+		DXGI_FORMAT_BC1_UNORM_SRGB = 72,
+		DXGI_FORMAT_BC2_UNORM = 74,
+		DXGI_FORMAT_BC2_UNORM_SRGB = 75,
+		DXGI_FORMAT_BC3_UNORM = 77,
+		DXGI_FORMAT_BC3_UNORM_SRGB = 78,
+		DXGI_FORMAT_BC4_UNORM = 80,
+		DXGI_FORMAT_BC5_UNORM = 83,
+		DXGI_FORMAT_B5G6R5_UNORM = 85,
+		DXGI_FORMAT_B5G5R5A1_UNORM = 86,
+		DXGI_FORMAT_B8G8R8A8_UNORM = 87,
+		DXGI_FORMAT_B8G8R8A8_UNORM_SRGB = 91,
+		DXGI_FORMAT_BC7_UNORM = 98,
+		DXGI_FORMAT_BC7_UNORM_SRGB = 99,
+		DXGI_FORMAT_B4G4R4A4_UNORM = 115
+	};
+
+	// --- DDS header flag/cap constants. ---
+	enum
+	{
+		DDSD_CAPS = 0x1, DDSD_HEIGHT = 0x2, DDSD_WIDTH = 0x4, DDSD_PITCH = 0x8,
+		DDSD_PIXELFORMAT = 0x1000, DDSD_MIPMAPCOUNT = 0x20000, DDSD_LINEARSIZE = 0x80000,
+
+		DDPF_FOURCC = 0x4,
+
+		DDSCAPS_COMPLEX = 0x8, DDSCAPS_TEXTURE = 0x1000, DDSCAPS_MIPMAP = 0x400000,
+
+		DDSCAPS2_CUBEMAP = 0x200, DDSCAPS2_CUBEMAP_ALLFACES = 0xFC00,
+
+		DDS_MAGIC = 0x20534444,				// "DDS "
+		DDS_DX10_FOURCC = 0x30315844,		// "DX10"
+
+		DDS_DIMENSION_TEXTURE2D = 3,
+		DDS_RESOURCE_MISC_TEXTURECUBE = 0x4
+	};
+
+	// Per-format static info.
+	struct dds_format_info
+	{
+		const char* m_pToken;
+		bool m_block_compressed;
+		uint32_t m_bytes;			// bytes per 4x4 block (compressed) or bytes per pixel (uncompressed)
+		uint32_t m_dxgi_unorm;
+		uint32_t m_dxgi_srgb;		// 0 if no sRGB variant exists
+	};
+
+	static const dds_format_info g_dds_format_info[cDDSFmtTotal] =
+	{
+		// token        block  bytes  unorm                          srgb
+		{ "bc1",        true,  8,     DXGI_FORMAT_BC1_UNORM,         DXGI_FORMAT_BC1_UNORM_SRGB },
+		{ "bc2",        true,  16,    DXGI_FORMAT_BC2_UNORM,         DXGI_FORMAT_BC2_UNORM_SRGB },
+		{ "bc3",        true,  16,    DXGI_FORMAT_BC3_UNORM,         DXGI_FORMAT_BC3_UNORM_SRGB },
+		{ "bc4",        true,  8,     DXGI_FORMAT_BC4_UNORM,         0 },
+		{ "bc5",        true,  16,    DXGI_FORMAT_BC5_UNORM,         0 },
+		{ "bc7",        true,  16,    DXGI_FORMAT_BC7_UNORM,         DXGI_FORMAT_BC7_UNORM_SRGB },
+		{ "a8r8g8b8",   false, 4,     DXGI_FORMAT_B8G8R8A8_UNORM,    DXGI_FORMAT_B8G8R8A8_UNORM_SRGB },
+		{ "a8b8g8r8",   false, 4,     DXGI_FORMAT_R8G8B8A8_UNORM,    DXGI_FORMAT_R8G8B8A8_UNORM_SRGB },
+		{ "r8",         false, 1,     DXGI_FORMAT_R8_UNORM,          0 },
+		{ "r8g8",       false, 2,     DXGI_FORMAT_R8G8_UNORM,        0 },
+		{ "r5g6b5",     false, 2,     DXGI_FORMAT_B5G6R5_UNORM,      0 },
+		{ "a1r5g5b5",   false, 2,     DXGI_FORMAT_B5G5R5A1_UNORM,    0 },
+		{ "a4r4g4b4",   false, 2,     DXGI_FORMAT_B4G4R4A4_UNORM,    0 }
+	};
+
+	bool parse_dds_output_format(const char* pToken, dds_output_format& fmt)
+	{
+		fmt = cDDSFmtInvalid;
+		if (!pToken)
+			return false;
+
+		for (int i = 0; i < (int)cDDSFmtTotal; i++)
+		{
+			if (strcasecmp(pToken, g_dds_format_info[i].m_pToken) == 0)
+			{
+				fmt = (dds_output_format)i;
+				return true;
+			}
+		}
+
+		// Tolerate the (geometrically impossible) "a1r5g6b5" spelling as an alias for a1r5g5b5.
+		if (strcasecmp(pToken, "a1r5g6b5") == 0)
+		{
+			fmt = cDDSFmtA1R5G5B5;
+			return true;
+		}
+
+		return false;
+	}
+
+	const char* get_dds_output_format_string(dds_output_format fmt)
+	{
+		if ((fmt < 0) || (fmt >= cDDSFmtTotal))
+			return "?";
+		return g_dds_format_info[fmt].m_pToken;
+	}
+
+	bool dds_output_format_has_srgb_variant(dds_output_format fmt)
+	{
+		if ((fmt < 0) || (fmt >= cDDSFmtTotal))
+			return false;
+		return g_dds_format_info[fmt].m_dxgi_srgb != 0;
+	}
+
+	// --- Little-endian append helpers. ---
+	static inline void append_u16(uint8_vec& v, uint32_t x)
+	{
+		v.push_back((uint8_t)(x & 0xFF));
+		v.push_back((uint8_t)((x >> 8) & 0xFF));
+	}
+	static inline void append_u32(uint8_vec& v, uint32_t x)
+	{
+		v.push_back((uint8_t)(x & 0xFF));
+		v.push_back((uint8_t)((x >> 8) & 0xFF));
+		v.push_back((uint8_t)((x >> 16) & 0xFF));
+		v.push_back((uint8_t)((x >> 24) & 0xFF));
+	}
+
+	// Packs a single RGBA8 texel into the uncompressed output bytes for fmt (truncating 8->N bits, which is
+	// the exact inverse of the reader's bit-replication expansion). Channel byte orders match the DXGI formats.
+	static void pack_uncompressed_pixel(uint8_vec& out, const color_rgba& c, dds_output_format fmt)
+	{
+		switch (fmt)
+		{
+		case cDDSFmtA8R8G8B8:		// DXGI B8G8R8A8 -> memory order B,G,R,A
+			out.push_back(c.b); out.push_back(c.g); out.push_back(c.r); out.push_back(c.a);
+			break;
+		case cDDSFmtA8B8G8R8:		// DXGI R8G8B8A8 -> memory order R,G,B,A
+			out.push_back(c.r); out.push_back(c.g); out.push_back(c.b); out.push_back(c.a);
+			break;
+		case cDDSFmtR8:				// DXGI R8 -> just R
+			out.push_back(c.r);
+			break;
+		case cDDSFmtR8G8:			// DXGI R8G8 -> source R into R, source G into G (matches BC5; swizzle input for other mappings)
+			out.push_back(c.r); out.push_back(c.g);
+			break;
+		case cDDSFmtR5G6B5:
+			append_u16(out, (uint32_t)(((c.r >> 3) << 11) | ((c.g >> 2) << 5) | (c.b >> 3)));
+			break;
+		case cDDSFmtA1R5G5B5:
+			append_u16(out, (uint32_t)((c.a >= 128 ? 0x8000 : 0) | ((c.r >> 3) << 10) | ((c.g >> 3) << 5) | (c.b >> 3)));
+			break;
+		case cDDSFmtA4R4G4B4:
+			append_u16(out, (uint32_t)(((c.a >> 4) << 12) | ((c.r >> 4) << 8) | ((c.g >> 4) << 4) | (c.b >> 4)));
+			break;
+		default:
+			assert(0);
+			break;
+		}
+	}
+
+	// Prebuilt BC7 packing context (built once per build_dds, shared read-only across slices).
+	struct bc7_pack_context
+	{
+		dds_bc7_encoder m_encoder;
+		uint32_t m_bc7f_flags;									// bc7f packer flags (when m_encoder == bc7f)
+		bc7e_scalar::bc7e_compress_block_params m_bc7e_params;	// initialized only when m_encoder == bc7e_scalar
+	};
+
+	// Packs one prepared slice image into the bytes for fmt, using logical dims (orig_width/orig_height).
+	// For block formats this iterates whole 4x4 blocks (the slice image is already block-padded); for
+	// uncompressed it emits tight rows of exactly orig_width*orig_height pixels.
+	static void pack_slice(uint8_vec& out, const image& img, uint32_t orig_width, uint32_t orig_height, dds_output_format fmt, const bc7_pack_context& bc7ctx)
+	{
+		const dds_format_info& info = g_dds_format_info[fmt];
+
+		if (info.m_block_compressed)
+		{
+			const uint32_t blocks_x = (orig_width + 3) / 4;
+			const uint32_t blocks_y = (orig_height + 3) / 4;
+
+			for (uint32_t by = 0; by < blocks_y; by++)
+			{
+				for (uint32_t bx = 0; bx < blocks_x; bx++)
+				{
+					color_rgba blk[16];
+					img.extract_block_clamped(blk, bx * 4, by * 4, 4, 4);
+
+					uint8_t dst[16];
+					switch (fmt)
+					{
+					case cDDSFmtBC1:
+						basist::encode_bc1(dst, (const uint8_t*)blk, basist::cEncodeBC1HighQuality);
+						break;
+					case cDDSFmtBC2:
+						// 8 bytes explicit 4-bit alpha (one LE 16-bit word per row, texel x at nibble x), then a
+						// BC1 color block (encode_bc1 emits 4-color blocks, which BC2's always-4-color decode needs).
+						for (uint32_t ry = 0; ry < 4; ry++)
+						{
+							uint32_t row = 0;
+							for (uint32_t rx = 0; rx < 4; rx++)
+								row |= ((uint32_t)(blk[ry * 4 + rx].a >> 4)) << (rx * 4);
+							dst[ry * 2] = (uint8_t)(row & 0xFF);
+							dst[ry * 2 + 1] = (uint8_t)(row >> 8);
+						}
+						basist::encode_bc1(dst + 8, (const uint8_t*)blk, basist::cEncodeBC1HighQuality);
+						break;
+					case cDDSFmtBC3:
+						basist::encode_bc4(dst, &blk[0].a, sizeof(color_rgba));
+						basist::encode_bc1(dst + 8, (const uint8_t*)blk, basist::cEncodeBC1HighQuality);
+						break;
+					case cDDSFmtBC4:
+						basist::encode_bc4(dst, &blk[0].r, sizeof(color_rgba));
+						break;
+					case cDDSFmtBC5:
+						basist::encode_bc4(dst, &blk[0].r, sizeof(color_rgba));
+						basist::encode_bc4(dst + 8, &blk[0].g, sizeof(color_rgba));
+						break;
+					case cDDSFmtBC7:
+						// basist::color_rgba and basisu::color_rgba have identical layout (r,g,b,a uint8_t).
+						if (bc7ctx.m_encoder == cDDSBC7Encoder_BC7E_Scalar)
+						{
+							uint64_t blk64[2];
+							bc7e_scalar::bc7e_compress_blocks(1, blk64, reinterpret_cast<const uint32_t*>(blk), &bc7ctx.m_bc7e_params, nullptr);
+							memcpy(dst, blk64, 16);
+						}
+						else
+						{
+							basist::bc7f::fast_pack_bc7_auto_rgba(dst, reinterpret_cast<const basist::color_rgba*>(blk), bc7ctx.m_bc7f_flags);
+						}
+						break;
+					default:
+						assert(0);
+						break;
+					}
+
+					out.append(dst, info.m_bytes);
+				}
+			}
+		}
+		else
+		{
+			for (uint32_t y = 0; y < orig_height; y++)
+				for (uint32_t x = 0; x < orig_width; x++)
+					pack_uncompressed_pixel(out, img(x, y), fmt);
+		}
+	}
+
+	// Builds the DDS magic + DDS_HEADER + DDS_HEADER_DXT10 (148 bytes total).
+	static void build_dx10_header(uint8_vec& out, uint32_t width, uint32_t height, uint32_t mip_count,
+		uint32_t array_size, bool is_cubemap, uint32_t dxgi_format, const dds_format_info& info)
+	{
+		// dwPitchOrLinearSize (informational; readers generally recompute).
+		uint32_t pitch_or_linear_size;
+		uint32_t flags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT;
+		if (info.m_block_compressed)
+		{
+			pitch_or_linear_size = ((width + 3) / 4) * ((height + 3) / 4) * info.m_bytes;
+			flags |= DDSD_LINEARSIZE;
+		}
+		else
+		{
+			pitch_or_linear_size = width * info.m_bytes;
+			flags |= DDSD_PITCH;
+		}
+		if (mip_count > 1)
+			flags |= DDSD_MIPMAPCOUNT;
+
+		uint32_t caps = DDSCAPS_TEXTURE;
+		if ((mip_count > 1) || is_cubemap || (array_size > 1))
+			caps |= DDSCAPS_COMPLEX;
+		if (mip_count > 1)
+			caps |= DDSCAPS_MIPMAP;
+
+		const uint32_t caps2 = is_cubemap ? (DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_ALLFACES) : 0;
+
+		append_u32(out, DDS_MAGIC);
+
+		// DDS_HEADER (124 bytes).
+		append_u32(out, 124);				// dwSize
+		append_u32(out, flags);				// dwFlags
+		append_u32(out, height);			// dwHeight
+		append_u32(out, width);				// dwWidth
+		append_u32(out, pitch_or_linear_size);
+		append_u32(out, 0);					// dwDepth
+		append_u32(out, mip_count);			// dwMipMapCount
+		for (uint32_t i = 0; i < 11; i++)	// dwReserved1[11]
+			append_u32(out, 0);
+
+		// DDS_PIXELFORMAT (32 bytes) - DX10 indirection.
+		append_u32(out, 32);				// ddspf.dwSize
+		append_u32(out, DDPF_FOURCC);		// ddspf.dwFlags
+		append_u32(out, DDS_DX10_FOURCC);	// ddspf.dwFourCC = "DX10"
+		append_u32(out, 0);					// dwRGBBitCount
+		append_u32(out, 0);					// dwRBitMask
+		append_u32(out, 0);					// dwGBitMask
+		append_u32(out, 0);					// dwBBitMask
+		append_u32(out, 0);					// dwABitMask
+
+		append_u32(out, caps);				// dwCaps
+		append_u32(out, caps2);				// dwCaps2
+		append_u32(out, 0);					// dwCaps3
+		append_u32(out, 0);					// dwCaps4
+		append_u32(out, 0);					// dwReserved2
+
+		// DDS_HEADER_DXT10 (20 bytes).
+		append_u32(out, dxgi_format);		// dxgiFormat
+		append_u32(out, DDS_DIMENSION_TEXTURE2D);
+		append_u32(out, is_cubemap ? DDS_RESOURCE_MISC_TEXTURECUBE : 0);	// miscFlag
+		append_u32(out, array_size);		// arraySize (number of array elements; cubes for a cubemap)
+		append_u32(out, 0);					// miscFlags2
+	}
+
+	bool build_dds(uint8_vec& dds_data, const basis_compressor& comp, const dds_export_params& params,
+		std::string& error_msg,
+		uint32_t* pOut_width, uint32_t* pOut_height,
+		uint32_t* pOut_levels, uint32_t* pOut_layers, uint32_t* pOut_faces)
+	{
+		error_msg.clear();
+		dds_data.resize(0);
+
+		const dds_output_format fmt = params.m_format;
+
+		if ((fmt < 0) || (fmt >= cDDSFmtTotal))
+		{
+			error_msg = "invalid output format";
+			return false;
+		}
+
+		const dds_format_info& info = g_dds_format_info[fmt];
+
+		const basisu::vector<image>& slices = comp.get_slice_images();
+		const basisu_backend_slice_desc_vec& descs = comp.get_slice_descs();
+
+		if (!slices.size() || (slices.size() != descs.size()))
+		{
+			error_msg = "no prepared slices (was process_source_images() run successfully?)";
+			return false;
+		}
+
+		const bool is_cubemap = (comp.get_params().m_tex_type == basist::cBASISTexTypeCubemapArray);
+
+		// Determine base dims, layer count, mip level count, and face count - mirrors create_ktx2_file().
+		uint32_t base_width = 0, base_height = 0, total_layers = 0, total_levels = 0, total_faces = 1;
+		for (uint32_t i = 0; i < descs.size(); i++)
+		{
+			if ((descs[i].m_mip_index == 0) && (!base_width))
+			{
+				base_width = descs[i].m_orig_width;
+				base_height = descs[i].m_orig_height;
+			}
+
+			total_layers = maximum<uint32_t>(total_layers, descs[i].m_source_file_index + 1);
+
+			if (!descs[i].m_source_file_index)
+				total_levels = maximum<uint32_t>(total_levels, descs[i].m_mip_index + 1);
+		}
+
+		if (is_cubemap)
+		{
+			if ((total_layers % 6) != 0)
+			{
+				error_msg = "cubemap source image count is not a multiple of 6";
+				return false;
+			}
+			total_layers /= 6;
+			total_faces = 6;
+		}
+
+		if (!base_width || !base_height || !total_layers || !total_levels)
+		{
+			error_msg = "could not determine texture dimensions from the prepared slices";
+			return false;
+		}
+
+		// Build a (layer, face, level) -> slice index map using the same decomposition as create_ktx2_file().
+		const uint32_t total_subresources = total_layers * total_faces * total_levels;
+		basisu::vector<int> slice_map(total_subresources);
+		for (uint32_t i = 0; i < total_subresources; i++)
+			slice_map[i] = -1;
+
+		for (uint32_t i = 0; i < descs.size(); i++)
+		{
+			// Note: descs[i].m_alpha just flags that the slice contains alpha; in the XUBC7 (m_uastc) path each
+			// slice holds full RGBA, so it is NOT a separate alpha-only slice (that only happens for ETC1S).
+
+			const uint32_t level_index = descs[i].m_mip_index;
+			uint32_t layer_index = descs[i].m_source_file_index;
+			uint32_t face_index = 0;
+			if (is_cubemap)
+			{
+				face_index = layer_index % 6;
+				layer_index /= 6;
+			}
+
+			if ((layer_index >= total_layers) || (face_index >= total_faces) || (level_index >= total_levels))
+			{
+				error_msg = "slice descriptor out of range (internal error)";
+				return false;
+			}
+
+			const uint32_t map_index = (layer_index * total_faces + face_index) * total_levels + level_index;
+
+			// Two slices mapping to the same subresource would indicate RGB/alpha slice splitting (ETC1S), which
+			// we don't expect here since we force the XUBC7 path.
+			if (slice_map[map_index] >= 0)
+			{
+				error_msg = "multiple slices map to the same (layer, face, level) - RGB/alpha slice splitting is not supported";
+				return false;
+			}
+
+			slice_map[map_index] = (int)i;
+		}
+
+		for (uint32_t i = 0; i < total_subresources; i++)
+		{
+			if (slice_map[i] < 0)
+			{
+				error_msg = "missing slice for a (layer, face, level) - source images are not all the same size / mip count";
+				return false;
+			}
+		}
+
+		// Select the DXGI variant (UNORM vs sRGB).
+		const bool want_srgb = comp.get_params().m_ktx2_and_basis_srgb_transfer_function;
+		const uint32_t dxgi_format = (want_srgb && info.m_dxgi_srgb) ? info.m_dxgi_srgb : info.m_dxgi_unorm;
+		if (want_srgb && !info.m_dxgi_srgb && comp.get_params().m_status_output)
+			printf("Note: format %s has no sRGB DXGI variant; writing UNORM (texel data is unchanged regardless).\n", info.m_pToken);
+
+		// Build the BC7 packing context once (only relevant for the bc7 format).
+		bc7_pack_context bc7ctx;
+		memset(&bc7ctx, 0, sizeof(bc7ctx));
+		bc7ctx.m_encoder = params.m_bc7_encoder;
+		switch (clamp<int>(params.m_bc7f_level, cDDSBC7FLevel_Analytical, cDDSBC7FLevel_NonAnalytical))
+		{
+		case cDDSBC7FLevel_Analytical:    bc7ctx.m_bc7f_flags = basist::bc7f::cPackBC7FlagDefault; break;
+		case cDDSBC7FLevel_NonAnalytical: bc7ctx.m_bc7f_flags = basist::bc7f::cPackBC7FlagDefaultNonAnalytical; break;
+		default:                          bc7ctx.m_bc7f_flags = basist::bc7f::cPackBC7FlagDefaultPartiallyAnalytical; break;
+		}
+		if ((fmt == cDDSFmtBC7) && (params.m_bc7_encoder == cDDSBC7Encoder_BC7E_Scalar))
+		{
+			bc7e_scalar::bc7e_compress_block_init();
+
+			typedef void (*bc7e_init_func)(bc7e_scalar::bc7e_compress_block_params*, bool);
+			static const bc7e_init_func s_bc7e_level_init[7] =
+			{
+				&bc7e_scalar::bc7e_compress_block_params_init_ultrafast,	// 0
+				&bc7e_scalar::bc7e_compress_block_params_init_veryfast,	// 1
+				&bc7e_scalar::bc7e_compress_block_params_init_fast,		// 2
+				&bc7e_scalar::bc7e_compress_block_params_init_basic,		// 3
+				&bc7e_scalar::bc7e_compress_block_params_init_slow,		// 4
+				&bc7e_scalar::bc7e_compress_block_params_init_veryslow,	// 5
+				&bc7e_scalar::bc7e_compress_block_params_init_slowest,	// 6
+			};
+			const int lvl = clamp<int>(params.m_bc7e_scalar_level, 0, 6);
+
+			// Mirrors the XUBC7 path: for perceptual (sRGB) sources run bc7e in its built-in perceptual error
+			// mode (it ignores m_weights then); for linear sources run linear and hand it the same RGBA channel
+			// weights XUASTC LDR / XUBC7 use.
+			const bool perceptual = comp.get_params().m_perceptual;
+			s_bc7e_level_init[lvl](&bc7ctx.m_bc7e_params, perceptual);
+
+			if (!perceptual)
+			{
+				for (uint32_t i = 0; i < 4; i++)
+					bc7ctx.m_bc7e_params.m_weights[i] = comp.get_params().m_xuastc_ldr_channel_weights[i];
+			}
+		}
+
+		// Optional debug logging (gated by the compressor's m_debug param, same flag -debug/-verbose set).
+		const bool debug = comp.get_params().m_debug;
+
+		if (debug)
+		{
+			const bool wrote_srgb_dbg = want_srgb && (info.m_dxgi_srgb != 0);
+			debug_printf("DDS export: format \"%s\" (%s), DXGI=%u, %ux%u, %u level(s), %u layer(s), %u face(s) (%s), %u subresource(s)\n",
+				info.m_pToken, wrote_srgb_dbg ? "sRGB" : "UNORM", dxgi_format, base_width, base_height,
+				total_levels, total_layers, total_faces, is_cubemap ? "cubemap" : "2D", total_subresources);
+			debug_printf("DDS export: %s, %u byte(s) per %s\n",
+				info.m_block_compressed ? "block-compressed" : "uncompressed", info.m_bytes,
+				info.m_block_compressed ? "block" : "pixel");
+
+			if (fmt == cDDSFmtBC7)
+			{
+				// Print the FULL BC7 configuration (both encoders' settings) regardless of which encoder
+				// is active, so the actual values used can be verified at a glance. The "encoder=" line
+				// states which one is in effect; the line for the other encoder is informational.
+				const bool using_bc7e = (params.m_bc7_encoder == cDDSBC7Encoder_BC7E_Scalar);
+
+				const int bc7f_lvl = clamp<int>(params.m_bc7f_level, cDDSBC7FLevel_Analytical, cDDSBC7FLevel_NonAnalytical);
+				const char* pBc7fLvl = (bc7f_lvl == cDDSBC7FLevel_Analytical) ? "analytical" :
+					(bc7f_lvl == cDDSBC7FLevel_NonAnalytical) ? "non-analytical" : "partially-analytical";
+
+				const int bc7e_lvl = clamp<int>(params.m_bc7e_scalar_level, 0, 6);
+				const bool perceptual = comp.get_params().m_perceptual;
+				const uint32_t* pW = comp.get_params().m_xuastc_ldr_channel_weights;
+
+				debug_printf("DDS export: BC7 encoder=%s\n", using_bc7e ? "bc7e_scalar" : "bc7f");
+				debug_printf("DDS export:   bc7f level=%d (%s), pack flags=0x%X%s\n",
+					bc7f_lvl, pBc7fLvl, bc7ctx.m_bc7f_flags, using_bc7e ? " (inactive)" : " (active)");
+				debug_printf("DDS export:   bc7e_scalar level=%d, perceptual=%u, weights=[%u %u %u %u]%s%s\n",
+					bc7e_lvl, (uint32_t)perceptual, pW[0], pW[1], pW[2], pW[3],
+					perceptual ? " (weights ignored in perceptual mode)" : "",
+					using_bc7e ? " (active)" : " (inactive)");
+			}
+		}
+
+		// Assemble the file into the caller's buffer: header then subresources in DDS order (layer, face, mip).
+		build_dx10_header(dds_data, base_width, base_height, total_levels, total_layers, is_cubemap, dxgi_format, info);
+
+		if (comp.get_params().m_status_output)
+			printf("Writing DDS (format \"%s\", %ux%u): %u subresource(s) [%u level(s), %u layer(s), %u face(s)]\n",
+				info.m_pToken, base_width, base_height, total_subresources, total_levels, total_layers, total_faces);
+
+		for (uint32_t layer = 0; layer < total_layers; layer++)
+		{
+			for (uint32_t face = 0; face < total_faces; face++)
+			{
+				for (uint32_t level = 0; level < total_levels; level++)
+				{
+					const uint32_t map_index = (layer * total_faces + face) * total_levels + level;
+					const int slice_index = slice_map[map_index];
+
+					const basisu_backend_slice_desc& desc = descs[slice_index];
+
+					if (comp.get_params().m_status_output)
+						printf("DDS: packing subresource %u/%u (layer %u, face %u, level %u): %ux%u\n",
+							map_index + 1, total_subresources, layer, face, level, desc.m_orig_width, desc.m_orig_height);
+
+					const size_t before_size = dds_data.size();
+					pack_slice(dds_data, slices[slice_index], desc.m_orig_width, desc.m_orig_height, fmt, bc7ctx);
+
+					if (debug)
+						debug_printf("  subresource [layer %u, face %u, level %u]: %ux%u -> %u byte(s)\n",
+							layer, face, level, desc.m_orig_width, desc.m_orig_height, (uint32_t)(dds_data.size() - before_size));
+				}
+			}
+		}
+
+		if (pOut_width) *pOut_width = base_width;
+		if (pOut_height) *pOut_height = base_height;
+		if (pOut_levels) *pOut_levels = total_levels;
+		if (pOut_layers) *pOut_layers = total_layers;
+		if (pOut_faces) *pOut_faces = total_faces;
+
+		return true;
+	}
+
+} // namespace basisu
diff --git a/encoder/basisu_dds_export.h b/encoder/basisu_dds_export.h
new file mode 100644
index 0000000..8b48fce
--- /dev/null
+++ b/encoder/basisu_dds_export.h
@@ -0,0 +1,123 @@
+// basisu_dds_export.h
+// Copyright (C) 2019-2025 Binomial LLC. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Basic DX10-style .DDS writer for basisu_tool's -dds command. It consumes the prepared source image
+// slices from a basis_compressor that has been run in process_source_images() mode (so all the image
+// loading, mipmap generation, and cubemap/array layout is done by the encoder), then packs each slice
+// into the requested output format and writes a modern DX10 (DDS_HEADER_DXT10) .dds file.
+//
+// Supported -dds_format tokens (see parse_dds_output_format):
+//   Block compressed: bc1 bc2 bc3 bc4 bc5 bc7
+//   Uncompressed:     a8r8g8b8 r8 r8g8 r5g6b5 a1r5g5b5 a4r4g4b4
+//
+// The sRGB-vs-UNORM DXGI variant is selected from the compressor's transfer function flag
+// (m_ktx2_and_basis_srgb_transfer_function, set by -srgb/-photo/-linear); only bc1/bc2/bc3/bc7/a8r8g8b8/a8b8g8r8
+// have an sRGB DXGI variant, the rest are always written as UNORM. No transfer-function conversion is
+// ever applied to the texel data - we only pick the format tag and pack the bytes as-is.
+#pragma once
+
+#include <string>
+#include "../transcoder/basisu.h"		// for basisu::uint8_vec
+
+namespace basisu
+{
+	class basis_compressor;
+
+	// The output formats supported by the -dds command.
+	enum dds_output_format
+	{
+		cDDSFmtInvalid = -1,
+
+		// Block compressed (packed with the real-time BC encoders).
+		cDDSFmtBC1,
+		cDDSFmtBC2,
+		cDDSFmtBC3,
+		cDDSFmtBC4,
+		cDDSFmtBC5,
+		cDDSFmtBC7,
+
+		// Uncompressed (simple pixel format conversion).
+		cDDSFmtA8R8G8B8,	// DXGI B8G8R8A8 (32bpp, BGRA in memory), has sRGB variant
+		cDDSFmtA8B8G8R8,	// DXGI R8G8B8A8 (32bpp, RGBA in memory), has sRGB variant
+		cDDSFmtR8,			// DXGI R8 (8bpp)
+		cDDSFmtR8G8,		// DXGI R8G8 (16bpp), source R->R, source G->G (like BC5)
+		cDDSFmtR5G6B5,		// DXGI B5G6R5 (16bpp)
+		cDDSFmtA1R5G5B5,	// DXGI B5G5R5A1 (16bpp)
+		cDDSFmtA4R4G4B4,	// DXGI B4G4R4A4 (16bpp)
+
+		cDDSFmtTotal
+	};
+
+	// Parses a -dds_format token (case-insensitive) into a dds_output_format. Returns false (and sets
+	// fmt to cDDSFmtInvalid) on an unrecognized token.
+	bool parse_dds_output_format(const char* pToken, dds_output_format& fmt);
+
+	// Returns the canonical token string for a format (for messages), or "?" if invalid.
+	const char* get_dds_output_format_string(dds_output_format fmt);
+
+	// True if the format has a distinct sRGB DXGI variant (bc1/bc3/bc7/a8r8g8b8); false for the formats
+	// that are always written UNORM. Lets a caller report the actually-emitted variant accurately.
+	bool dds_output_format_has_srgb_variant(dds_output_format fmt);
+
+	// Which BC7 base packer to use for bc7 output. (Eventually a third encoder may be added.)
+	enum dds_bc7_encoder
+	{
+		cDDSBC7Encoder_Default = -1,		// "not specified" sentinel (CLI use); resolves to the dds_export_params default
+		cDDSBC7Encoder_BC7F = 0,			// built-in fast real-time packer (basist::bc7f), default
+		cDDSBC7Encoder_BC7E_Scalar = 1		// higher-quality (slower) scalar bc7e encoder
+	};
+
+	// bc7f quality level -> which analytical mode flags the bc7f packer uses (higher = slower/better).
+	enum dds_bc7f_level
+	{
+		cDDSBC7FLevel_Analytical = 0,			// cPackBC7FlagDefault
+		cDDSBC7FLevel_PartiallyAnalytical = 1,	// cPackBC7FlagDefaultPartiallyAnalytical (default)
+		cDDSBC7FLevel_NonAnalytical = 2			// cPackBC7FlagDefaultNonAnalytical (slowest/best)
+	};
+
+	// Input parameters for build_dds(). Intended to grow as more -dds options are added (BC1 quality, etc.).
+	struct dds_export_params
+	{
+		dds_output_format m_format;
+
+		// BC7-only knobs (ignored for non-bc7 formats):
+		dds_bc7_encoder m_bc7_encoder;		// which BC7 base packer
+		int m_bc7f_level;					// dds_bc7f_level [0,2]: analytical / partially / non analytical (bc7f only)
+		int m_bc7e_scalar_level;			// bc7e_scalar quality level [0,6], 0=ultrafast..6=slowest (bc7e_scalar only)
+
+		dds_export_params()
+			: m_format(cDDSFmtInvalid),
+			  m_bc7_encoder(cDDSBC7Encoder_BC7F),
+			  m_bc7f_level(cDDSBC7FLevel_PartiallyAnalytical),
+			  m_bc7e_scalar_level(2)
+		{ }
+		explicit dds_export_params(dds_output_format format)
+			: m_format(format),
+			  m_bc7_encoder(cDDSBC7Encoder_BC7F),
+			  m_bc7f_level(cDDSBC7FLevel_PartiallyAnalytical),
+			  m_bc7e_scalar_level(2)
+		{ }
+	};
+
+	// Builds an in-memory DX10 .dds file from the prepared slices of comp (which must have had a successful
+	// process_source_images() call) into dds_data (which is resize(0)'d first, then appended to). The caller
+	// is responsible for writing dds_data out (or using it however it likes). Picks the UNORM vs sRGB DXGI
+	// variant from comp's transfer function flag. On failure returns false and sets error_msg. On success,
+	// the optional out_* values report the texture's dimensions/structure (for a caller summary).
+	bool build_dds(uint8_vec& dds_data, const basis_compressor& comp, const dds_export_params& params,
+		std::string& error_msg,
+		uint32_t* pOut_width = nullptr, uint32_t* pOut_height = nullptr,
+		uint32_t* pOut_levels = nullptr, uint32_t* pOut_layers = nullptr, uint32_t* pOut_faces = nullptr);
+
+} // namespace basisu
diff --git a/encoder/basisu_xbc7_encode.cpp b/encoder/basisu_xbc7_encode.cpp
new file mode 100644
index 0000000..035e722
--- /dev/null
+++ b/encoder/basisu_xbc7_encode.cpp
@@ -0,0 +1,3643 @@
+// File: basisu_xbc7_encode.cpp
+#include "basisu_xbc7_encode.h"
+#include "../transcoder/basisu_transcoder.h"
+#include "../zstd/zstd.h"
+#include "../transcoder/basisu_xbc7_decoder.h"
+#include "basisu_bc7e_scalar.h"
+
+using basist::fixed16_16;
+
+namespace basisu {
+
+namespace xbc7
+{
+	using namespace basist::xbc7; // shared XBC7 defs (DCT/syms/enums/eval) now live in basist::xbc7
+
+		
+
+	class blob_stream_writer
+	{
+	public:
+		blob_stream_writer() {}
+
+		void clear() { m_blobs.clear(); }
+
+		inline uint8_vec& get_blob_vec(uint32_t id)
+		{
+			assert(id < BLOB_STREAM_MAX_IDS);
+			if (id >= m_blobs.size())
+				m_blobs.resize(id + 1);
+			return m_blobs[id];
+		}
+
+		// Hot path: append one byte to blob `id`. id must be < 128.
+		inline void put_byte(uint32_t id, uint8_t b)
+		{
+			assert(id < BLOB_STREAM_MAX_IDS);
+			if (id >= m_blobs.size())
+				m_blobs.resize(id + 1);
+			m_blobs[id].push_back(b);
+		}
+
+		inline void put_bytes(uint32_t id, const void* p, size_t n)
+		{
+			assert(id < BLOB_STREAM_MAX_IDS);
+			if (id >= m_blobs.size())
+				m_blobs.resize(id + 1);
+			m_blobs[id].append(static_cast<const uint8_t*>(p), n);
+		}
+
+		void append_bytes(uint32_t id, const uint8_vec& blob)
+		{
+			assert(id < BLOB_STREAM_MAX_IDS);
+			if (id >= m_blobs.size())
+				m_blobs.resize(id + 1);
+			m_blobs[id].append(blob);
+		}
+
+		// for the per-stream byte accounting / stats dashboard
+		inline size_t get_blob_size(uint32_t id) const { return (id < m_blobs.size()) ? m_blobs[id].size() : 0; }
+		inline uint32_t get_num_ids() const { return m_blobs.size_u32(); }
+		inline const uint8_vec* get_blob_data(uint32_t id) const { return (id < m_blobs.size()) ? &m_blobs[id] : nullptr; }
+
+		// Compress (where it helps) and serialize everything to `out`.
+		// Returns false only on internal error (blob > 4GB etc).
+		bool serialize(uint8_vec& out, int zstd_level = 19, uint32_t* pStored_sizes = nullptr) const // pStored_sizes: optional [BLOB_STREAM_MAX_IDS] array, receives stored (post-compression) payload bytes per id
+		{
+			out.resize(0);
+
+			uint32_t num_stored = 0;
+			for (uint32_t id = 0; id < m_blobs.size(); id++)
+				if (m_blobs[id].size())
+					num_stored++;
+
+			if (num_stored > 255)
+				return false;
+
+			out.push_back(BLOB_STREAM_MAGIC_BEGIN);
+			out.push_back((uint8_t)num_stored);
+
+			uint8_vec comp_buf;
+
+			for (uint32_t id = 0; id < m_blobs.size(); id++)
+			{
+				const uint8_vec& blob = m_blobs[id];
+				if (!blob.size())
+					continue;
+
+				if (blob.size() > UINT32_MAX)
+					return false;
+
+				const uint32_t uncomp_size = (uint32_t)blob.size();
+
+				// Try Zstd; keep it only if it's worth it. Small blobs keep any
+				// win, but a LARGE blob must shrink by at least 1/400 (0.25%):
+				// raw blobs are zero-copy at decode (no Zstd frame to walk, no
+				// arena bytes), so a marginal win costs more than it saves.
+				const uint8_t* pStored = blob.data();
+				uint32_t stored_size = 0; // 0 == raw
+
+				const size_t bound = ZSTD_compressBound(blob.size());
+				comp_buf.resize(bound);
+
+				const size_t comp_res = ZSTD_compress(comp_buf.data(), bound, blob.data(), blob.size(), zstd_level);
+				if ((!ZSTD_isError(comp_res)) && (comp_res < blob.size()))
+				{
+					const uint32_t BLOB_RATIO_CHECK_MIN_SIZE = 128;	// blobs <= this keep any win
+					const uint32_t BLOB_MIN_SAVINGS_DENOM = 400;	// larger blobs must save >= size/400
+
+					const uint64_t saved_bytes = (uint64_t)blob.size() - comp_res;
+
+					if ((blob.size() <= BLOB_RATIO_CHECK_MIN_SIZE) || ((saved_bytes * BLOB_MIN_SAVINGS_DENOM) >= (uint64_t)blob.size()))
+					{
+						pStored = comp_buf.data();
+						stored_size = (uint32_t)comp_res;
+					}
+				}
+
+				if (pStored_sizes)
+					pStored_sizes[id] = stored_size ? stored_size : uncomp_size;
+
+				// directory entry: id+flag byte, then varint size(s)
+				out.push_back((uint8_t)(id | (stored_size ? 0x80u : 0u)));
+				if (stored_size)
+				{
+					push_varint(out, uncomp_size);
+					push_varint(out, stored_size);
+				}
+				else
+					push_varint(out, uncomp_size);
+
+				out.append(pStored, stored_size ? stored_size : uncomp_size);
+			}
+
+			out.push_back(BLOB_STREAM_MAGIC_END);
+
+			return true;
+		}
+
+	private:
+		basisu::vector<uint8_vec> m_blobs;
+
+		static inline void push_varint(uint8_vec& v, uint32_t x)
+		{
+			while (x >= 0x80u)
+			{
+				v.push_back((uint8_t)(x | 0x80u));
+				x >>= 7;
+			}
+			v.push_back((uint8_t)x);
+		}
+	};
+
+
+
+static const char* g_cand_names[cCandFirstXYDelta] =
+{
+	"absolute", "left edge", "upper edge", "L+U blend", "reflect left", "reflect upper",
+	"L+U avg", "L+U strong blend", "gradient", "damped gradient", "diag avg",
+	"diag edge blend", "upper+diag edge blend", "MED", "GAB", "plane fit",
+	"diag down-left", "diag down-right"
+};
+
+
+// Post-quantization DCT coefficient statistics, collected from the WINNING
+// candidate of each non-solid block, per coded plane. Gives the picture the
+// run/coeff/sign stream design needs: where surviving ACs sit in zigzag
+// order, their amplitudes and signs, run lengths, and DC behavior.
+struct xbc7_dct_stats
+{
+	uint64_t m_total_planes = 0;
+
+	// DC (range is [-256, 256] by construction)
+	uint64_t m_dc_zero = 0, m_dc_pos = 0, m_dc_neg = 0;
+	uint64_t m_dc_sum_abs = 0;
+	int m_dc_min = 257, m_dc_max = -257;
+	uint64_t m_dc_mag_hist[10] = {};
+
+	// AC
+	uint64_t m_total_acs = 0;
+	uint64_t m_ac_pos = 0, m_ac_neg = 0;
+	uint64_t m_ac_sum_abs = 0;
+	int m_ac_min = 257, m_ac_max = -257;
+	uint64_t m_ac_mag_hist[10] = {};
+	uint64_t m_ac_count_hist[16] = {};	// nonzero ACs per plane, 0..15
+	uint64_t m_zig_pos_hist[16] = {};	// zigzag position (1..15) of each nonzero AC
+	uint64_t m_run_hist[16] = {};		// zero-run length coded before each nonzero AC
+	uint64_t m_eob_markers = 0;			// trailing-zeros records
+
+	// |v| -> bucket: 1, 2, 3, 4, 5-8, 9-16, 17-32, 33-64, 65-128, 129+
+	static uint32_t mag_bucket(uint32_t m)
+	{
+		if (m <= 4) return m - 1;
+		if (m <= 8) return 4;
+		if (m <= 16) return 5;
+		if (m <= 32) return 6;
+		if (m <= 64) return 7;
+		if (m <= 128) return 8;
+		return 9;
+	}
+
+	void record_plane(const dct_syms& syms)
+	{
+		m_total_planes++;
+
+		const int dc = syms.m_dc;
+		if (!dc) m_dc_zero++; else if (dc > 0) m_dc_pos++; else m_dc_neg++;
+		m_dc_sum_abs += (uint64_t)basisu::iabs(dc);
+		m_dc_min = basisu::minimum(m_dc_min, dc);
+		m_dc_max = basisu::maximum(m_dc_max, dc);
+		if (dc)
+			m_dc_mag_hist[mag_bucket((uint32_t)basisu::iabs(dc))]++;
+
+		uint32_t num_acs = 0;
+		uint32_t zig_idx = 1;
+
+		for (uint32_t i = 0; i < syms.m_ac_vals.size_u32(); i++)
+		{
+			const uint32_t run_len = (uint32_t)syms.m_ac_vals[i].m_num_zeros;
+			const int c = syms.m_ac_vals[i].m_coeff;
+
+			if (c == INT16_MAX)
+			{
+				m_eob_markers++;
+				break;
+			}
+
+			zig_idx += run_len;
+			assert(zig_idx < 16);
+			if (zig_idx >= 16)
+				break;
+
+			m_run_hist[basisu::minimum<uint32_t>(run_len, 15)]++;
+			m_zig_pos_hist[zig_idx]++;
+
+			if (c > 0) m_ac_pos++; else m_ac_neg++;
+			m_ac_sum_abs += (uint64_t)basisu::iabs(c);
+			m_ac_min = basisu::minimum(m_ac_min, c);
+			m_ac_max = basisu::maximum(m_ac_max, c);
+			m_ac_mag_hist[mag_bucket((uint32_t)basisu::iabs(c))]++;
+
+			num_acs++;
+			zig_idx++;
+		}
+
+		m_total_acs += num_acs;
+		m_ac_count_hist[basisu::minimum<uint32_t>(num_acs, 15)]++;
+	}
+
+	// accumulate another collector (per-job stats -> totals); counters add,
+	// extrema take min/max
+	void merge(const xbc7_dct_stats& o)
+	{
+		m_total_planes += o.m_total_planes;
+
+		m_dc_zero += o.m_dc_zero; m_dc_pos += o.m_dc_pos; m_dc_neg += o.m_dc_neg;
+		m_dc_sum_abs += o.m_dc_sum_abs;
+		m_dc_min = basisu::minimum(m_dc_min, o.m_dc_min);
+		m_dc_max = basisu::maximum(m_dc_max, o.m_dc_max);
+		for (uint32_t i = 0; i < 10; i++)
+			m_dc_mag_hist[i] += o.m_dc_mag_hist[i];
+
+		m_total_acs += o.m_total_acs;
+		m_ac_pos += o.m_ac_pos; m_ac_neg += o.m_ac_neg;
+		m_ac_sum_abs += o.m_ac_sum_abs;
+		m_ac_min = basisu::minimum(m_ac_min, o.m_ac_min);
+		m_ac_max = basisu::maximum(m_ac_max, o.m_ac_max);
+		for (uint32_t i = 0; i < 10; i++)
+			m_ac_mag_hist[i] += o.m_ac_mag_hist[i];
+		for (uint32_t i = 0; i < 16; i++)
+		{
+			m_ac_count_hist[i] += o.m_ac_count_hist[i];
+			m_zig_pos_hist[i] += o.m_zig_pos_hist[i];
+			m_run_hist[i] += o.m_run_hist[i];
+		}
+
+		m_eob_markers += o.m_eob_markers;
+	}
+
+	void print() const
+	{
+		static const char* s_mag_labels[10] = { "1", "2", "3", "4", "5-8", "9-16", "17-32", "33-64", "65-128", "129+" };
+
+		fmt_debug_printf("---- WEIGHTS: DCT coefficient statistics (winning candidates, per coded plane) ----\n");
+
+		const float inv_p = m_total_planes ? (100.0f / (float)m_total_planes) : 0.0f;
+		const float inv_a = m_total_acs ? (100.0f / (float)m_total_acs) : 0.0f;
+
+		fmt_debug_printf("Coded planes: {}, total nonzero ACs: {}, avg ACs/plane: {}\n",
+			m_total_planes, m_total_acs, m_total_planes ? ((float)m_total_acs / (float)m_total_planes) : 0.0f);
+
+		fmt_debug_printf("DC: zero: {} ({}%), pos: {} ({}%), neg: {} ({}%), range [{}, {}], mean |DC| (all planes): {}\n",
+			m_dc_zero, (float)m_dc_zero * inv_p, m_dc_pos, (float)m_dc_pos * inv_p, m_dc_neg, (float)m_dc_neg * inv_p,
+			m_dc_min, m_dc_max, m_total_planes ? ((float)m_dc_sum_abs / (float)m_total_planes) : 0.0f);
+
+		fmt_debug_printf("DC magnitude histogram (nonzero DCs): ");
+		for (uint32_t i = 0; i < 10; i++)
+			if (m_dc_mag_hist[i])
+				fmt_debug_printf("{}:{} ", s_mag_labels[i], m_dc_mag_hist[i]);
+		fmt_debug_printf("\n");
+
+		fmt_debug_printf("AC: pos: {} ({}%), neg: {} ({}%), range [{}, {}], mean |AC| (per nonzero AC): {}\n",
+			m_ac_pos, (float)m_ac_pos * inv_a, m_ac_neg, (float)m_ac_neg * inv_a,
+			m_ac_min, m_ac_max, m_total_acs ? ((float)m_ac_sum_abs / (float)m_total_acs) : 0.0f);
+
+		fmt_debug_printf("AC magnitude histogram: ");
+		for (uint32_t i = 0; i < 10; i++)
+			if (m_ac_mag_hist[i])
+				fmt_debug_printf("{}:{} ({}%) ", s_mag_labels[i], m_ac_mag_hist[i], (float)m_ac_mag_hist[i] * inv_a);
+		fmt_debug_printf("\n");
+
+		fmt_debug_printf("Nonzero ACs per plane: ");
+		for (uint32_t i = 0; i < 16; i++)
+			if (m_ac_count_hist[i])
+				fmt_debug_printf("{}:{} ({}%) ", i, m_ac_count_hist[i], (float)m_ac_count_hist[i] * inv_p);
+		fmt_debug_printf("\n");
+
+		fmt_debug_printf("Zigzag position of nonzero ACs (1..15): ");
+		for (uint32_t i = 1; i < 16; i++)
+			fmt_debug_printf("{}:{} ", i, m_zig_pos_hist[i]);
+		fmt_debug_printf("\n");
+
+		fmt_debug_printf("Zero-run length before each AC: ");
+		for (uint32_t i = 0; i < 16; i++)
+			if (m_run_hist[i])
+				fmt_debug_printf("{}:{} ({}%) ", i, m_run_hist[i], (float)m_run_hist[i] * inv_a);
+		fmt_debug_printf("\n");
+
+		fmt_debug_printf("Trailing-zeros (EOB) markers: {} ({}% of planes)\n", m_eob_markers, (float)m_eob_markers * inv_p);
+	}
+};
+
+
+	// ---- debug-image color palettes (encode-time visualization only) ----
+	// The command byte is split into three field-specific color images rather
+	// than one hard-to-read grayscale; each image carries a drawn legend.
+
+	// CORE command id (xbc7_command_id, command bits 0-2).
+	static const color_rgba g_xbc7_command_vis_colors[8] =
+	{
+		color_rgba(  0,   0, 255, 255), // 0 RepeatLast               - blue
+		color_rgba(  0, 255, 255, 255), // 1 RepeatUpper              - cyan
+		color_rgba(128, 128, 128, 255), // 2 SolidDPCM                - gray
+		color_rgba(255,   0,   0, 255), // 3 NewConfig                - red
+		color_rgba(  0, 255,   0, 255), // 4 ReuseConfigLeft          - green
+		color_rgba(255, 255,   0, 255), // 5 ReuseConfigUpper         - yellow
+		color_rgba(255,   0, 255, 255), // 6 ReuseConfigLeftDiagonal  - magenta
+		color_rgba(255, 128,   0, 255), // 7 ReuseConfigRightDiagonal - orange
+	};
+
+	// ENDPOINT prediction mode (xbc7_command_endpoint_mode, command bits 3-5).
+	static const color_rgba g_xbc7_endpoint_mode_vis_colors[8] =
+	{
+		color_rgba(255,   0,   0, 255), // 0 Raw            - red
+		color_rgba(  0, 200,   0, 255), // 1 DPCM-Left      - green
+		color_rgba(  0, 128, 255, 255), // 2 DPCM-Up        - blue
+		color_rgba(255, 255,   0, 255), // 3 DPCM-LeftDiag  - yellow
+		color_rgba(255,   0, 255, 255), // 4 DPCM-RightDiag - magenta
+		color_rgba(  0, 255, 255, 255), // 5 DPCM-Index     - cyan
+		color_rgba(255, 128,   0, 255), // 6 DPCM-Left-Sub1 - orange
+		color_rgba(160,   0, 255, 255), // 7 DPCM-Up-Sub1   - purple
+	};
+
+	// WEIGHT mode (xbc7_command_weight_mode, command bit 6).
+	static const color_rgba g_xbc7_weight_mode_vis_colors[2] =
+	{
+		color_rgba(  0, 200,   0, 255), // 0 Raw/DPCM (lossless) - green
+		color_rgba(255,   0,   0, 255), // 1 DCT     (lossy)     - red
+	};
+
+	// Blocks with no endpoint/weight fields (fully predicted: RepeatLast/
+	// RepeatUpper/SolidDPCM) in the endpoint/weight-mode images.
+	static const color_rgba g_xbc7_vis_na_color(40, 40, 40, 255);
+
+	// AC-COUNT heatmap: number of nonzero DCT weight ACs in a block (DCT-coded
+	// blocks only). Bucketed via xbc7_ac_count_bucket().
+	static const color_rgba g_xbc7_ac_count_vis_colors[6] =
+	{
+		color_rgba(  0,   0, 128, 255), // 0     ACs - dark blue (flat / DC only)
+		color_rgba(  0, 128, 255, 255), // 1-2   ACs - blue
+		color_rgba(  0, 200,   0, 255), // 3-5   ACs - green
+		color_rgba(255, 255,   0, 255), // 6-10  ACs - yellow
+		color_rgba(255, 128,   0, 255), // 11-20 ACs - orange
+		color_rgba(255,   0,   0, 255), // 21+   ACs - red
+	};
+
+	static inline uint32_t xbc7_ac_count_bucket(uint32_t num_acs)
+	{
+		if (num_acs == 0)  return 0;
+		if (num_acs <= 2)  return 1;
+		if (num_acs <= 5)  return 2;
+		if (num_acs <= 10) return 3;
+		if (num_acs <= 20) return 4;
+		return 5;
+	}
+
+	// PREDICTOR categories: the ~50 weight predictors (absolute, 17 synthetic,
+	// 32 block references) collapsed into a handful of buckets; the 32 block
+	// refs are split by their causal direction.
+	enum xbc7_pred_category
+	{
+		cPredCatAbsolute = 0, // no prediction
+		cPredCatSynthetic,    // edge/gradient synthetic predictors
+		cPredCatRefLeft,      // block ref, same row (dy==0)
+		cPredCatRefUp,        // block ref, straight up (dx==0)
+		cPredCatRefUpLeft,    // block ref, up-and-left
+		cPredCatRefUpRight,   // block ref, up-and-right
+		cNumPredCategories
+	};
+
+	static const color_rgba g_xbc7_predictor_vis_colors[cNumPredCategories] =
+	{
+		color_rgba(255,   0,   0, 255), // Absolute         - red
+		color_rgba(255, 128,   0, 255), // Synthetic        - orange
+		color_rgba(  0, 200,   0, 255), // BlockRef Left     - green
+		color_rgba(  0, 128, 255, 255), // BlockRef Up       - blue
+		color_rgba(  0, 255, 255, 255), // BlockRef Up-Left  - cyan
+		color_rgba(255,   0, 255, 255), // BlockRef Up-Right - magenta
+	};
+
+	// Map a (cand + amp*cTotalCandidates) predictor index to its category.
+	static inline uint32_t xbc7_predictor_category(uint32_t pred_index)
+	{
+		const uint32_t cand = pred_index % cTotalCandidates;
+		if (cand == cCandAbsolute)
+			return cPredCatAbsolute;
+		if (cand < cCandFirstXYDelta)
+			return cPredCatSynthetic;
+		const xbc7_xy_delta d = g_xbc7_xy_deltas[cand - cCandFirstXYDelta];
+		if (d.m_dy == 0) return cPredCatRefLeft; // dx<0, same row
+		if (d.m_dx == 0) return cPredCatRefUp;   // straight up
+		return (d.m_dx < 0) ? cPredCatRefUpLeft : cPredCatRefUpRight;
+	}
+
+	// Weighted RGBA PSNR of a 16-pixel (4x4) block vs the source, using the
+	// caller's per-channel weights (pack_options::m_weights), exactly mirroring
+	// basisu_astc_ldr_encode.cpp's WSSE->PSNR: per-pixel WSSE via
+	// color_rgba::get_weighted_dist2(), wmse = wsse / (sum_weights * num_pixels),
+	// then 20*log10(255/sqrt(wmse)), capped for ~zero error. Used by the BC7
+	// alt-pack and endpoint-DPCM RDO passes.
+	static inline float xbc7_block_wsse_psnr(const color_rgba* pOrig, const color_rgba* pDec, const uint32_t comp_weights[4])
+	{
+		uint64_t wsse = 0;
+		for (uint32_t i = 0; i < 16; i++)
+			wsse += pDec[i].get_weighted_dist2(pOrig[i], comp_weights);
+
+		const float total_comp_weights = (float)(comp_weights[0] + comp_weights[1] + comp_weights[2] + comp_weights[3]);
+		const float wmse = (float)wsse / (total_comp_weights * 16.0f);
+		return (wmse > 1e-5f) ? (20.0f * log10f(255.0f / sqrtf(wmse))) : 10000.0f;
+	}
+
+	// Standalone: recompute the optimal per-texel weights of a BC7 logical block
+	// for its FIXED config + endpoints, ignoring its existing (possibly stale)
+	// weights. out_blk receives in_blk's config + endpoints with freshly optimized
+	// weights. comp_weights[4] are the per-channel RGBA error weights. No
+	// dependency on pack_options, so this can be lifted into shared code later.
+	//
+	// Per plane: each texel's weight independently selects its interpolation
+	// between the fixed endpoints, so setting all texels to the same value w and
+	// decoding reveals every texel's value at w in one decode; sweeping w over all
+	// 1<<m_weight_bits[plane] values and keeping each texel's lowest WSSE yields
+	// the per-texel optimum. Error is summed ONLY over the channels this plane
+	// drives (get_decoded_channel_weight_plane) -- critical for dual-plane modes,
+	// where planes 0/1 control disjoint channels (with mode 4/5 rotation/CCS).
+	[[maybe_unused]] static bool optimize_block_weights(const basist::bc7u::log_bc7_block& in_blk, const color_rgba orig_pixels[16], const uint32_t comp_weights[4], basist::bc7u::log_bc7_block& out_blk)
+	{
+		out_blk = in_blk; // same config + endpoints; weights overwritten below
+
+		for (uint32_t p = 0; p < in_blk.m_num_planes; p++)
+		{
+			const uint32_t num_weight_vals = in_blk.get_num_weight_vals(p); // 4, 8, or 16
+
+			basist::bc7u::log_bc7_block scratch(in_blk);
+
+			// neutralize the OTHER plane so the decode is well-defined; the
+			// channel masking below means it can't affect this plane's WSSE anyway
+			if (in_blk.is_dual_plane())
+				memset(scratch.m_weights[1u - p], 0, sizeof(scratch.m_weights[0]));
+
+			uint64_t best_err[16];
+			uint8_t best_weight[16];
+			for (uint32_t i = 0; i < 16; i++)
+			{
+				best_err[i] = UINT64_MAX;
+				best_weight[i] = 0;
+			}
+
+			for (uint32_t w = 0; w < num_weight_vals; w++)
+			{
+				for (uint32_t i = 0; i < 16; i++)
+					scratch.m_weights[p][i] = (uint8_t)w;
+
+				basist::color_rgba dec[16];
+				if (!basist::bc7u::unpack_bc7(scratch, dec))
+				{
+					assert(0); // a valid config+endpoints block must always decode
+					return false;
+				}
+
+				for (uint32_t i = 0; i < 16; i++)
+				{
+					uint64_t err = 0;
+					for (uint32_t c = 0; c < 4; c++)
+					{
+						if (in_blk.get_decoded_channel_weight_plane(c) != p)
+							continue; // only the channels this plane drives
+
+						const int d = (int)dec[i][c] - (int)orig_pixels[i][c];
+						err += (uint64_t)(d * d) * comp_weights[c];
+					}
+
+					if (err < best_err[i])
+					{
+						best_err[i] = err;
+						best_weight[i] = (uint8_t)w;
+					}
+				}
+			}
+
+			for (uint32_t i = 0; i < 16; i++)
+				out_blk.m_weights[p][i] = best_weight[i];
+		}
+
+		return true;
+	}
+
+	// Encode-time visualization targets (debug only). Each member is null unless
+	// m_debug_images is set. One 4x4 pixel block per BC7 block; stripes fill
+	// disjoint block rows, so writing these from stripe jobs is thread-safe.
+	struct xbc7_debug_image_set
+	{
+		image* m_pCommand = nullptr;       // core command id (bits 0-2)
+		image* m_pEndpoint_mode = nullptr; // endpoint pred mode (bits 3-5); n/a for predicted blocks
+		image* m_pWeight_mode = nullptr;   // weight mode (bit 6): DCT vs lossless; n/a for predicted blocks
+		image* m_pAC_count = nullptr;      // # nonzero DCT weight ACs; n/a unless DCT-coded
+		image* m_pPredictor = nullptr;     // weight predictor category; n/a for predicted blocks
+	};
+
+	// Fill one BC7 block's 4x4 region in a (possibly null) debug image.
+	static inline void xbc7_vis_fill_block(image* pImg, uint32_t bx, uint32_t by, const color_rgba& c)
+	{
+		if (pImg)
+			pImg->fill_box(bx * 4, by * 4, 4, 4, c);
+	}
+
+	// Color-fill a block across all debug images from its final command byte.
+	// has_ep_wt is false for fully-predicted blocks (RepeatLast/RepeatUpper/
+	// SolidDPCM), which carry no endpoint/weight fields. pred_index is the weight
+	// predictor (UINT32_MAX == none); ac_count is the nonzero DCT-weight AC count
+	// (UINT32_MAX == not DCT-coded). UINT32_MAX maps to the n/a color.
+	static inline void xbc7_vis_fill_command(const xbc7_debug_image_set& dbg, uint32_t bx, uint32_t by,
+		uint8_t command_byte, bool has_ep_wt, uint32_t pred_index, uint32_t ac_count)
+	{
+		xbc7_vis_fill_block(dbg.m_pCommand, bx, by, g_xbc7_command_vis_colors[command_byte & 7]);
+
+		if (has_ep_wt)
+		{
+			xbc7_vis_fill_block(dbg.m_pEndpoint_mode, bx, by, g_xbc7_endpoint_mode_vis_colors[(command_byte >> XBC7_COMMAND_ENDPOINT_MODE_SHIFT) & 7]);
+			xbc7_vis_fill_block(dbg.m_pWeight_mode, bx, by, g_xbc7_weight_mode_vis_colors[(command_byte >> XBC7_COMMAND_WEIGHT_MODE_SHIFT) & 1]);
+		}
+		else
+		{
+			xbc7_vis_fill_block(dbg.m_pEndpoint_mode, bx, by, g_xbc7_vis_na_color);
+			xbc7_vis_fill_block(dbg.m_pWeight_mode, bx, by, g_xbc7_vis_na_color);
+		}
+
+		xbc7_vis_fill_block(dbg.m_pPredictor, bx, by,
+			(pred_index == UINT32_MAX) ? g_xbc7_vis_na_color : g_xbc7_predictor_vis_colors[xbc7_predictor_category(pred_index)]);
+
+		xbc7_vis_fill_block(dbg.m_pAC_count, bx, by,
+			(ac_count == UINT32_MAX) ? g_xbc7_vis_na_color : g_xbc7_ac_count_vis_colors[xbc7_ac_count_bucket(ac_count)]);
+	}
+
+	// ---- legend drawn into a strip at the bottom of each debug image ----
+	const uint32_t XBC7_VIS_LEGEND_ROW_H = 10; // px per entry (8px glyph + 2px gap)
+
+	static inline uint32_t xbc7_vis_legend_height(uint32_t num_entries) { return num_entries * XBC7_VIS_LEGEND_ROW_H + 4; }
+
+	struct xbc7_vis_legend_entry { color_rgba m_color; const char* m_pLabel; };
+
+	// Draws a swatch+label list into the BOTTOM strip of img, overwriting the
+	// block pixels there (the image stays the source resolution). Everything
+	// clips, so it's safe even if the strip is taller than the whole image.
+	static void xbc7_vis_draw_legend(image& img, const xbc7_vis_legend_entry* pEntries, uint32_t num_entries)
+	{
+		const uint32_t legend_h = xbc7_vis_legend_height(num_entries);
+		const uint32_t top_y = (img.get_height() >= legend_h) ? (img.get_height() - legend_h) : 0;
+
+		img.fill_box(0, top_y, img.get_width(), img.get_height() - top_y, color_rgba(16, 16, 16, 255));
+
+		for (uint32_t i = 0; i < num_entries; i++)
+		{
+			const uint32_t y = top_y + 2 + i * XBC7_VIS_LEGEND_ROW_H;
+			img.fill_box(2, y, 8, 8, pEntries[i].m_color);
+			img.debug_text(14, y, 1, 1, color_rgba(255, 255, 255, 255), nullptr, false, "%s", pEntries[i].m_pLabel);
+		}
+	}
+
+	// ---------------- encoder striping ----------------
+	// The main coding pass splits into 1..XBC7_MAX_ENCODER_STRIPES horizontal
+	// stripes of whole block rows, one job per stripe. Each stripe is coded
+	// causally from scratch (the encoder never emits references above a
+	// stripe's first row), so thin stripes cost compression: small images stay
+	// serial, and once striping kicks in every stripe gets at least
+	// XBC7_MIN_STRIPE_BLOCK_ROWS rows.
+
+	// (XBC7_MAX_ENCODER_STRIPES is shared with the decoder -- defined in
+	// basisu_xbc7_decode.h.)
+
+	// images shorter than this many TEXEL rows always encode as one stripe
+	const uint32_t XBC7_MIN_IMAGE_TEXEL_ROWS_TO_STRIPE = 128;
+
+	// minimum BLOCK rows per stripe once striping kicks in (bounds the
+	// per-seam compression loss); 16 block rows == 64 texel rows
+	const uint32_t XBC7_MIN_STRIPE_BLOCK_ROWS = 16;
+
+
+	// Computes the encoder's stripe count (1..XBC7_MAX_ENCODER_STRIPES) and
+	// each stripe's contiguous block-row range. Deliberately a function of
+	// the image dimensions ONLY -- not of thread availability -- so the
+	// encoded output is reproducible across machines; with fewer pool threads
+	// than stripes the extra jobs simply queue.
+	static uint32_t compute_encoder_stripes(
+		uint32_t height_in_texels, uint32_t num_blocks_y,
+		basisu::vector<stripe_range>& stripes)
+	{
+		uint32_t num_stripes = 1;
+
+		if (height_in_texels >= XBC7_MIN_IMAGE_TEXEL_ROWS_TO_STRIPE)
+		{
+			num_stripes = num_blocks_y / XBC7_MIN_STRIPE_BLOCK_ROWS;
+			num_stripes = basisu::clamp<uint32_t>(num_stripes, 1, XBC7_MAX_ENCODER_STRIPES);
+		}
+
+		num_stripes = basisu::minimum(num_stripes, basisu::maximum(1u, num_blocks_y));
+
+		compute_stripe_ranges(num_blocks_y, num_stripes, stripes);
+
+		return num_stripes;
+	}
+
+	// Highest stripe count this image can carry without violating the striping
+	// rules: at least XBC7_MIN_STRIPE_BLOCK_ROWS per stripe (so none is tiny or
+	// empty), no more than XBC7_MAX_ENCODER_STRIPES, and 1 for images too short
+	// to stripe usefully. Mirrors the bound compute_encoder_stripes uses.
+	static uint32_t max_stripes_for_image(uint32_t height_in_texels, uint32_t num_blocks_y)
+	{
+		if (height_in_texels < XBC7_MIN_IMAGE_TEXEL_ROWS_TO_STRIPE)
+			return 1;
+
+		const uint32_t by_min_size = num_blocks_y / XBC7_MIN_STRIPE_BLOCK_ROWS; // each stripe >= min block rows
+		return basisu::clamp<uint32_t>(by_min_size, 1, XBC7_MAX_ENCODER_STRIPES);
+	}
+
+	uint32_t pack_options::set_num_stripes_for_image(const image& img, uint32_t desired_num_stripes)
+	{
+		const uint32_t height = img.get_height();
+		const uint32_t num_blocks_y = (height + 3) / 4;
+
+		const uint32_t max_stripes = max_stripes_for_image(height, num_blocks_y);
+
+		m_num_stripes = basisu::clamp<uint32_t>(desired_num_stripes, 1, max_stripes);
+		return m_num_stripes;
+	}
+
+	void pack_options::set_rdo_level(uint32_t rdo_level)
+	{
+		rdo_level = basisu::minimum<uint32_t>(rdo_level, 100);
+
+		if (!rdo_level)
+		{
+			// RDO fully disabled: flags off, all drops zero.
+			m_bc7_alt_pack_enabled = false;
+			m_endpoint_rdo_enabled = false;
+			m_repeat_rdo_enabled = false;
+			m_solid_rdo_enabled = false;
+
+			m_bc7_alt_max_psnr_drop = 0.0f;
+			m_endpoint_rdo_max_psnr_drop = 0.0f;
+			m_repeat_rdo_max_psnr_drop = 0.0f;
+			m_solid_rdo_max_psnr_drop = 0.0f;
+			m_ac_trunc_rdo_max_psnr_drop = 0.0f;
+			return;
+		}
+
+		const float frac = (float)rdo_level / 100.0f; // (0, 1]
+		const float general_drop = 10.0f * frac;  // BC7 alt-pack / endpoint-DPCM / AC-trunc -> up to 10 dB
+		const float reuse_drop = 4.0f * frac;     // repeat / solid block reuse        -> up to 4 dB
+
+		m_bc7_alt_pack_enabled = true;
+		m_bc7_alt_max_psnr_drop = general_drop;
+
+		m_endpoint_rdo_enabled = true;
+		m_endpoint_rdo_max_psnr_drop = general_drop;
+
+		m_ac_trunc_rdo_max_psnr_drop = general_drop; // no enable flag; >0 enables
+
+		m_repeat_rdo_enabled = true;
+		m_repeat_rdo_max_psnr_drop = reuse_drop;
+
+		m_solid_rdo_enabled = true;
+		m_solid_rdo_max_psnr_drop = reuse_drop;
+	}
+
+	// ------------------------- XBC7 encoding statistics -------------------------
+	// Populated inline during pack_image (negligible cost), printed when
+	// opts.m_print_stats is set. Three layers: where the bytes go (per-blob
+	// accounting), what the encoder chose (command/mode/predictor histograms),
+	// and why (residual and coefficient distributions).
+	struct xbc7_pack_stats
+	{
+		// context
+		uint32_t m_width = 0, m_height = 0, m_total_blocks = 0, m_dct_q = 0;
+		bool m_has_alpha = false;
+		basisu::vector<stripe_range> m_stripes;
+
+		// choices
+		uint64_t m_cmd_hist[8] = {};
+		uint64_t m_mode_hist[8] = {};					// coded (non-repeat, non-solid) blocks
+		uint64_t m_pred_hist[cTotalCandidates * 4] = {};
+		uint64_t m_ep_raw_blocks = 0, m_ep_dpcm_blocks = 0, m_ep_dpcm_subsets = 0;
+
+		// AC-truncation RDO
+		uint64_t m_ac_trunc_pruned = 0;	// weight-DCT AC coeffs zeroed
+		uint64_t m_ac_trunc_blocks = 0;	// blocks where >=1 coeff was pruned
+		uint64_t m_ep_mode_hist[8] = {};				// xbc7_command_endpoint_mode of every coded block
+		uint64_t m_ep_index_hist[NUM_XY_DELTAS] = {};	// XY-delta table index of every EP block reference
+		uint64_t m_pbit_delta_bits = 0, m_pbit_delta_nonzero = 0;
+
+		// distributions: EP residuals by [width class][stream channel slot],
+		// wrapped bytes interpreted as int8 (the zero-peaked reading)
+		uint64_t m_ep_resid_count[2][4] = {};			// [0]=fine, [1]=coarse
+		uint64_t m_ep_resid_sum_abs[2][4] = {};
+		uint64_t m_ep_resid_zero[2][4] = {};
+
+		uint64_t m_solid_delta_count[4] = {};
+		uint64_t m_solid_delta_sum_abs[4] = {};
+		uint64_t m_solid_delta_zero[4] = {};
+
+		// weights: residual DCT vs lossless DPCM decision + tuning data
+		uint32_t m_wt_alpha_pct = 0;
+		uint64_t m_wt_dct_blocks = 0, m_wt_dpcm_blocks = 0;
+		uint64_t m_wt_dct_est_bits_total = 0, m_wt_dpcm_est_bits_total = 0, m_wt_chosen_est_bits = 0;
+
+		uint64_t m_wt_block_acs_hist_dct[33] = {};		// total ACs per block (clamped), split by which mode won
+		uint64_t m_wt_block_acs_hist_dpcm[33] = {};
+		uint64_t m_wt_dpcm_pred_hist[cTotalCandidates * 4] = {};
+		uint64_t m_wt_resid_count[3] = {}, m_wt_resid_zero[3] = {}, m_wt_resid_sum_abs[3] = {};	// [weight_bits - 2]
+		uint64_t m_wt_resid_mag_hist[3][9] = {};	// [weight_bits - 2][min(|centered resid|, 8)]
+
+		xbc7_dct_stats m_dct_stats;						// winning-candidate coefficient stats
+
+		// where the bytes go (filled at serialize time)
+		uint32_t m_blob_raw_size[BLOB_STREAM_MAX_IDS] = {};
+		uint32_t m_blob_stored_size[BLOB_STREAM_MAX_IDS] = {};
+		uint32_t m_total_file_size = 0;
+
+		// order-0 byte histograms of the pre-Zstd blob contents
+		uint64_t m_blob_hist[cBlobFirstUnused][256] = {};
+
+		void record_blob_bytes(uint32_t id, const uint8_vec* pBlob)
+		{
+			if ((id >= (uint32_t)cBlobFirstUnused) || (!pBlob))
+				return;
+			for (size_t i = 0; i < pBlob->size(); i++)
+				m_blob_hist[id][(*pBlob)[i]]++;
+		}
+
+		void record_ep_residuals(bool fine_class, const uint8_t* pResiduals, uint32_t num_residuals)
+		{
+			const uint32_t cls = fine_class ? 0 : 1;
+			for (uint32_t i = 0; i < num_residuals; i++)
+			{
+				const uint32_t chan = i >> 1;
+				const int v = (int8_t)pResiduals[i];
+				m_ep_resid_count[cls][chan]++;
+				m_ep_resid_sum_abs[cls][chan] += (uint64_t)basisu::iabs(v);
+				if (!v)
+					m_ep_resid_zero[cls][chan]++;
+			}
+		}
+
+		// Accumulate a per-stripe (per-job) collector into this one. Pure
+		// counters only: the context fields (dims/Q/stripes/alpha) and the
+		// blob size/histogram arrays are owned by the final stats object and
+		// filled outside the jobs.
+		void merge(const xbc7_pack_stats& o)
+		{
+			for (uint32_t i = 0; i < 8; i++)
+			{
+				m_cmd_hist[i] += o.m_cmd_hist[i];
+				m_mode_hist[i] += o.m_mode_hist[i];
+				m_ep_mode_hist[i] += o.m_ep_mode_hist[i];
+			}
+
+			for (uint32_t i = 0; i < cTotalCandidates * 4; i++)
+			{
+				m_pred_hist[i] += o.m_pred_hist[i];
+				m_wt_dpcm_pred_hist[i] += o.m_wt_dpcm_pred_hist[i];
+			}
+
+			m_ep_raw_blocks += o.m_ep_raw_blocks;
+			m_ep_dpcm_blocks += o.m_ep_dpcm_blocks;
+			m_ep_dpcm_subsets += o.m_ep_dpcm_subsets;
+
+			m_ac_trunc_pruned += o.m_ac_trunc_pruned;
+			m_ac_trunc_blocks += o.m_ac_trunc_blocks;
+
+			for (uint32_t i = 0; i < NUM_XY_DELTAS; i++)
+				m_ep_index_hist[i] += o.m_ep_index_hist[i];
+
+			m_pbit_delta_bits += o.m_pbit_delta_bits;
+			m_pbit_delta_nonzero += o.m_pbit_delta_nonzero;
+
+			for (uint32_t cls = 0; cls < 2; cls++)
+			{
+				for (uint32_t c = 0; c < 4; c++)
+				{
+					m_ep_resid_count[cls][c] += o.m_ep_resid_count[cls][c];
+					m_ep_resid_sum_abs[cls][c] += o.m_ep_resid_sum_abs[cls][c];
+					m_ep_resid_zero[cls][c] += o.m_ep_resid_zero[cls][c];
+				}
+			}
+
+			for (uint32_t c = 0; c < 4; c++)
+			{
+				m_solid_delta_count[c] += o.m_solid_delta_count[c];
+				m_solid_delta_sum_abs[c] += o.m_solid_delta_sum_abs[c];
+				m_solid_delta_zero[c] += o.m_solid_delta_zero[c];
+			}
+
+			m_wt_dct_blocks += o.m_wt_dct_blocks;
+			m_wt_dpcm_blocks += o.m_wt_dpcm_blocks;
+			m_wt_dct_est_bits_total += o.m_wt_dct_est_bits_total;
+			m_wt_dpcm_est_bits_total += o.m_wt_dpcm_est_bits_total;
+			m_wt_chosen_est_bits += o.m_wt_chosen_est_bits;
+
+			for (uint32_t i = 0; i <= 32; i++)
+			{
+				m_wt_block_acs_hist_dct[i] += o.m_wt_block_acs_hist_dct[i];
+				m_wt_block_acs_hist_dpcm[i] += o.m_wt_block_acs_hist_dpcm[i];
+			}
+
+			for (uint32_t cls = 0; cls < 3; cls++)
+			{
+				m_wt_resid_count[cls] += o.m_wt_resid_count[cls];
+				m_wt_resid_zero[cls] += o.m_wt_resid_zero[cls];
+				m_wt_resid_sum_abs[cls] += o.m_wt_resid_sum_abs[cls];
+
+				for (uint32_t m = 0; m < 9; m++)
+					m_wt_resid_mag_hist[cls][m] += o.m_wt_resid_mag_hist[cls][m];
+			}
+
+			m_dct_stats.merge(o.m_dct_stats);
+		}
+
+		// fmt_variants rejects format specs on strings, so pad manually
+		static std::string pad(const char* pStr, size_t n)
+		{
+			std::string s(pStr);
+			if (s.size() < n)
+				s.resize(n, ' ');
+			return s;
+		}
+
+		void print() const
+		{
+			static const char* s_blob_names[cBlobFirstUnused] =
+			{
+				"header", "commands", "cfg: block_config", "cfg: partition2", "cfg: partition3",
+				"wt: predictors", "wt-dct: dc_small", "wt-dct: dc_large", "wt-dct: ac_coeffs",
+				"wt-dct: signs", "ep: pbits",
+				"ep: fine_r", "ep: fine_g", "ep: fine_b", "ep: fine_a",
+				"ep: coarse_r", "ep: coarse_g", "ep: coarse_b", "ep: coarse_a",
+				"ep: raw", "ep: blk_index", "wt-dpcm: absolute", "solid: rgba_deltas",
+				"wt-dpcm: resid_2", "wt-dpcm: resid_3", "wt-dpcm: resid_4", "seek_table"
+			};
+			static const char* s_cmd_names[8] =
+			{
+				"repeat_last", "repeat_upper", "solid_dpcm", "new_config",
+				"reuse_left", "reuse_upper", "reuse_ldiag", "reuse_rdiag"
+			};
+
+			const float inv_blocks = m_total_blocks ? (100.0f / (float)m_total_blocks) : 0.0f;
+
+			fmt_debug_printf("\n========== XBC7 pack stats ==========\n");
+			fmt_debug_printf("{}x{}, {} blocks, Q={}, has_alpha={}\n",
+				m_width, m_height, m_total_blocks, m_dct_q, m_has_alpha);
+
+			{
+				std::string s(string_format("encoder stripes: %u", m_stripes.size_u32()));
+				for (uint32_t i = 0; i < m_stripes.size_u32(); i++)
+					s += string_format("%s block rows %u-%u", i ? "," : " --", m_stripes[i].m_first_block_row, m_stripes[i].m_first_block_row + m_stripes[i].m_num_block_rows - 1);
+				fmt_debug_printf("{}\n", s);
+			}
+
+			// ---- per-blob byte accounting ----
+			fmt_debug_printf("\n---- blob accounting (raw -> stored after Zstd) ----\n");
+			fmt_debug_printf("{}{}{}{}{}{}\n",
+				pad("blob", 20), pad("raw", 10), pad("stored", 10), pad("ratio", 8), pad("%file", 8), "bits/blk");
+
+			uint64_t total_raw = 0, total_stored = 0;
+			for (uint32_t id = 0; id < (uint32_t)cBlobFirstUnused; id++)
+			{
+				if (!m_blob_raw_size[id])
+					continue;
+
+				total_raw += m_blob_raw_size[id];
+				total_stored += m_blob_stored_size[id];
+
+				fmt_debug_printf("{}{}{}{}{}{.3}\n",
+					pad(s_blob_names[id], 20),
+					pad(string_format("%u", m_blob_raw_size[id]).c_str(), 10),
+					pad(string_format("%u", m_blob_stored_size[id]).c_str(), 10),
+					pad(string_format("%.3f", m_blob_stored_size[id] ? ((float)m_blob_raw_size[id] / (float)m_blob_stored_size[id]) : 0.0f).c_str(), 8),
+					pad(string_format("%.2f", m_total_file_size ? ((float)m_blob_stored_size[id] * 100.0f / (float)m_total_file_size) : 0.0f).c_str(), 8),
+					m_total_blocks ? ((float)m_blob_stored_size[id] * 8.0f / (float)m_total_blocks) : 0.0f);
+			}
+
+			const uint64_t overhead = (uint64_t)m_total_file_size - total_stored;
+			fmt_debug_printf("{}{}{}\n", pad("TOTAL", 20),
+				pad(string_format("%llu", (unsigned long long)total_raw).c_str(), 10),
+				pad(string_format("%llu", (unsigned long long)total_stored).c_str(), 10));
+			fmt_debug_printf("container+dir overhead: {} bytes\n", overhead);
+			fmt_debug_printf("file: {} bytes, {.3} bits/block, {.4} bpp\n",
+				m_total_file_size,
+				m_total_blocks ? ((float)m_total_file_size * 8.0f / (float)m_total_blocks) : 0.0f,
+				(m_width && m_height) ? ((float)m_total_file_size * 8.0f / (float)(m_width * m_height)) : 0.0f);
+
+			// ---- group rollup: where the bytes go, raw vs stored ----
+			{
+				// m_super buckets the fine groups into the coarse summary:
+				// 0 = commands+config, 1 = endpoints/pbits, 2 = weights, 3 = other
+				struct group_def { const char* m_pName; uint8_t m_super; uint8_t m_ids[12]; uint32_t m_num_ids; };
+				static const group_def s_groups[] =
+				{
+					{ "commands",         0, { cBlobCommands }, 1 },
+					{ "config/partition", 0, { cBlobBC7BlockConfig, cBlobPartition2, cBlobPartition3 }, 3 },
+					{ "endpoints/pbits",  1, { cBlobEPDeltaFineR, cBlobEPDeltaFineG, cBlobEPDeltaFineB, cBlobEPDeltaFineA,
+											   cBlobEPDeltaCoarseR, cBlobEPDeltaCoarseG, cBlobEPDeltaCoarseB, cBlobEPDeltaCoarseA,
+											   cBlobEPRaw, cBlobEPBlockIndex, cBlobPBits }, 11 },
+					{ "wt predictors",    2, { cBlobWeightPredictors }, 1 },
+					{ "wt dct",           2, { cBlobDCCoeffsSmall, cBlobDCCoeffsLarge, cBlobACCoeffs, cBlobCoeffSigns }, 4 },
+					{ "wt dpcm",          2, { cBlobRawWeightBits, cBlobDPCMWeightResid2, cBlobDPCMWeightResid3, cBlobDPCMWeightResid4 }, 4 },
+					{ "solid",            3, { cBlobSolidRGBADeltas }, 1 },
+					{ "header",           3, { cBlobHeader }, 1 },
+					{ "seek_table",       3, { cBlobStripeSeekTable }, 1 },
+				};
+				const uint32_t num_groups = (uint32_t)(sizeof(s_groups) / sizeof(s_groups[0]));
+
+				const float inv_file = m_total_file_size ? (100.0f / (float)m_total_file_size) : 0.0f;
+
+				fmt_debug_printf("\n---- group rollup ----\n");
+				fmt_debug_printf("{}{}{}{}\n", pad("group", 19), pad("raw", 10), pad("stored", 10), "%file");
+
+				uint64_t group_stored_total = 0;
+				uint64_t super_raw[4] = {}, super_stored[4] = {};
+
+				for (uint32_t g = 0; g < num_groups; g++)
+				{
+					uint64_t raw = 0, stored = 0;
+					for (uint32_t i = 0; i < s_groups[g].m_num_ids; i++)
+					{
+						raw += m_blob_raw_size[s_groups[g].m_ids[i]];
+						stored += m_blob_stored_size[s_groups[g].m_ids[i]];
+					}
+					group_stored_total += stored;
+
+					super_raw[s_groups[g].m_super] += raw;
+					super_stored[s_groups[g].m_super] += stored;
+
+					if (!raw)
+						continue;
+
+					fmt_debug_printf("{}{}{}{.2}\n",
+						pad(s_groups[g].m_pName, 19),
+						pad(string_format("%llu", (unsigned long long)raw).c_str(), 10),
+						pad(string_format("%llu", (unsigned long long)stored).c_str(), 10),
+						(float)stored * inv_file);
+				}
+
+				const uint64_t container_ovh = (uint64_t)m_total_file_size - group_stored_total;
+				fmt_debug_printf("{}{}{}{.2}\n", pad("container", 19), pad("", 10),
+					pad(string_format("%llu", (unsigned long long)container_ovh).c_str(), 10), (float)container_ovh * inv_file);
+				fmt_debug_printf("{}{}{}{}\n", pad("TOTAL", 19), pad("", 10),
+					pad(string_format("%u", m_total_file_size).c_str(), 10), "100.00");
+
+				// coarse three-bucket summary
+				static const char* s_super_names[4] = { "= commands+config", "= endpoints/pbits", "= weights (all)", "= other" };
+
+				fmt_debug_printf("\n");
+				for (uint32_t s = 0; s < 4; s++)
+				{
+					if (!super_raw[s])
+						continue;
+
+					fmt_debug_printf("{}{}{}{.2}\n",
+						pad(s_super_names[s], 19),
+						pad(string_format("%llu", (unsigned long long)super_raw[s]).c_str(), 10),
+						pad(string_format("%llu", (unsigned long long)super_stored[s]).c_str(), 10),
+						(float)super_stored[s] * inv_file);
+				}
+			}
+
+			// ---- per-blob order-0 symbol stats ----
+			// H = Shannon entropy of the raw byte stream (bits/byte); ideal = the
+			// order-0 entropy-coded size. stored/ideal < 1 means Zstd's matches +
+			// higher-order context beat a memoryless coder; > 1 means an entropy
+			// coder (FSE/rANS over this alphabet) would beat what Zstd achieves.
+			fmt_debug_printf("\n---- blob symbol stats (order-0, pre-Zstd) ----\n");
+			fmt_debug_printf("{}{}{}{}{}{}{}\n",
+				pad("blob", 20), pad("bytes", 10), pad("syms", 6), pad("H b/byte", 10), pad("ideal", 10), pad("stored", 10), "st/id");
+
+			for (uint32_t id = 0; id < (uint32_t)cBlobFirstUnused; id++)
+			{
+				uint64_t n = 0;
+				uint32_t distinct = 0;
+				for (uint32_t s = 0; s < 256; s++)
+				{
+					n += m_blob_hist[id][s];
+					if (m_blob_hist[id][s])
+						distinct++;
+				}
+				if (!n)
+					continue;
+
+				double entropy = 0.0;
+				for (uint32_t s = 0; s < 256; s++)
+				{
+					if (!m_blob_hist[id][s])
+						continue;
+					const double p = (double)m_blob_hist[id][s] / (double)n;
+					entropy -= p * std::log2(p);
+				}
+
+				const uint64_t ideal_bytes = (uint64_t)std::ceil(entropy * (double)n / 8.0);
+
+				fmt_debug_printf("{}{}{}{}{}{}{.3}\n",
+					pad(s_blob_names[id], 20),
+					pad(string_format("%llu", (unsigned long long)n).c_str(), 10),
+					pad(string_format("%u", distinct).c_str(), 6),
+					pad(string_format("%.3f", entropy).c_str(), 10),
+					pad(string_format("%llu", (unsigned long long)ideal_bytes).c_str(), 10),
+					pad(string_format("%u", m_blob_stored_size[id]).c_str(), 10),
+					ideal_bytes ? ((float)m_blob_stored_size[id] / (float)ideal_bytes) : 0.0f);
+
+				// condensed top symbols: up to 6, plus a count of the rest
+				uint64_t hist[256];
+				memcpy(hist, m_blob_hist[id], sizeof(hist));
+
+				std::string tops("  top:");
+				uint32_t shown = 0;
+				uint64_t shown_count = 0;
+				for (; shown < 6; shown++)
+				{
+					uint32_t best_s = 0;
+					uint64_t best_c = 0;
+					for (uint32_t s = 0; s < 256; s++)
+					{
+						if (hist[s] > best_c)
+						{
+							best_c = hist[s];
+							best_s = s;
+						}
+					}
+					if (!best_c)
+						break;
+
+					hist[best_s] = 0;
+					shown_count += best_c;
+					tops += string_format(" %u:%llu(%.1f%%)", best_s, (unsigned long long)best_c, (double)best_c * 100.0 / (double)n);
+				}
+				if (distinct > shown)
+					tops += string_format("  +%u more (%.1f%%)", distinct - shown, (double)(n - shown_count) * 100.0 / (double)n);
+
+				fmt_debug_printf("{}\n", tops);
+			}
+
+			// ---- commands / modes ----
+			fmt_debug_printf("\n---- COMMANDS: per-block command histogram ----\n");
+			for (uint32_t i = 0; i < 8; i++)
+				if (m_cmd_hist[i])
+					fmt_debug_printf("{}: {} ({.2}%)\n", pad(s_cmd_names[i], 13), m_cmd_hist[i], (float)m_cmd_hist[i] * inv_blocks);
+
+			fmt_debug_printf("\n---- CONFIG: BC7 modes of coded blocks ----\n");
+			for (uint32_t i = 0; i < 8; i++)
+				if (m_mode_hist[i])
+					fmt_debug_printf("mode {}: {} ({.2}%)\n", i, m_mode_hist[i], (float)m_mode_hist[i] * inv_blocks);
+
+			// ---- endpoints ----
+			fmt_debug_printf("\n---- ENDPOINTS: predictor modes + residuals ----\n");
+			fmt_debug_printf("raw blocks: {}, dpcm blocks: {} ({} subsets)\n", m_ep_raw_blocks, m_ep_dpcm_blocks, m_ep_dpcm_subsets);
+			fmt_debug_printf("ep modes: raw {}, left {}, up {}, ldiag {}, rdiag {}, index {}, left_s1 {}, up_s1 {}\n",
+				m_ep_mode_hist[0], m_ep_mode_hist[1], m_ep_mode_hist[2], m_ep_mode_hist[3], m_ep_mode_hist[4], m_ep_mode_hist[5],
+				m_ep_mode_hist[6], m_ep_mode_hist[7]);
+
+			if (m_ep_mode_hist[(uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMBlockIndex])
+			{
+				std::string idx_line("ep index deltas:");
+				for (uint32_t i = 0; i < NUM_XY_DELTAS; i++)
+					if (m_ep_index_hist[i])
+						idx_line += string_format(" (%i,%i):%llu", (int)g_xbc7_xy_deltas[i].m_dx, (int)g_xbc7_xy_deltas[i].m_dy, (unsigned long long)m_ep_index_hist[i]);
+				fmt_debug_printf("{}\n", idx_line);
+			}
+			if (m_pbit_delta_bits)
+				fmt_debug_printf("pbit deltas: {} bits, nonzero: {} ({.2}%)\n",
+					m_pbit_delta_bits, m_pbit_delta_nonzero, (float)m_pbit_delta_nonzero * 100.0f / (float)m_pbit_delta_bits);
+
+			static const char* s_class_names[2] = { "fine", "coarse" };
+			static const char* s_chan_names[4] = { "R", "G", "B", "A" };
+			for (uint32_t cls = 0; cls < 2; cls++)
+			{
+				for (uint32_t c = 0; c < 4; c++)
+				{
+					if (!m_ep_resid_count[cls][c])
+						continue;
+					fmt_debug_printf("ep resid {} {}: n={}, mean|d|={.3}, zero={.2}%\n",
+						pad(s_class_names[cls], 6), s_chan_names[c],
+						m_ep_resid_count[cls][c],
+						(float)m_ep_resid_sum_abs[cls][c] / (float)m_ep_resid_count[cls][c],
+						(float)m_ep_resid_zero[cls][c] * 100.0f / (float)m_ep_resid_count[cls][c]);
+				}
+			}
+
+			// ---- weight predictors (DCT-mode blocks; DPCM-mode predictors are in the next section) ----
+			fmt_debug_printf("\n---- WEIGHTS: DCT-mode predictors ----\n");
+			{
+				uint64_t class_totals[3] = { 0, 0, 0 }; // absolute / synthetic / dictionary
+				uint64_t amp_totals[4] = { 0, 0, 0, 0 };
+				uint64_t coded_blocks = 0;
+
+				for (uint32_t c = 0; c < cTotalCandidates; c++)
+				{
+					for (uint32_t a = 0; a < 4; a++)
+					{
+						const uint64_t t = m_pred_hist[c + a * cTotalCandidates];
+						amp_totals[a] += t;
+						coded_blocks += t;
+
+						if (c == cCandAbsolute)
+							class_totals[0] += t;
+						else if (c >= cCandFirstXYDelta)
+							class_totals[2] += t;
+						else
+							class_totals[1] += t;
+					}
+				}
+
+				const float inv_coded = coded_blocks ? (100.0f / (float)coded_blocks) : 0.0f;
+
+				fmt_debug_printf("classes: absolute {} ({.2}%), synthetic {} ({.2}%), dictionary {} ({.2}%)\n",
+					class_totals[0], (float)class_totals[0] * inv_coded,
+					class_totals[1], (float)class_totals[1] * inv_coded,
+					class_totals[2], (float)class_totals[2] * inv_coded);
+				fmt_debug_printf("amp codes: identity {}, flip {}, half {}, half-flip {}\n",
+					amp_totals[0], amp_totals[1], amp_totals[2], amp_totals[3]);
+
+				// top 12 joint (cand, amp) symbols
+				fmt_debug_printf("top predictors:\n");
+
+				uint32_t idx[cTotalCandidates * 4];
+				for (uint32_t i = 0; i < cTotalCandidates * 4; i++)
+					idx[i] = i;
+				std::sort(idx, idx + cTotalCandidates * 4,
+					[&](uint32_t a, uint32_t b) { return m_pred_hist[a] > m_pred_hist[b]; });
+
+				for (uint32_t r = 0; r < 12; r++)
+				{
+					const uint32_t i = idx[r];
+					if (!m_pred_hist[i])
+						break;
+
+					const uint32_t cand = i % cTotalCandidates, amp = i / cTotalCandidates;
+					static const char* s_amp_names[4] = { "", " (flip)", " (half)", " (half-flip)" };
+
+					std::string name;
+					if (cand < cCandFirstXYDelta)
+						name = g_cand_names[cand];
+					else
+					{
+						const xbc7_xy_delta& dlt = g_xbc7_xy_deltas[cand - cCandFirstXYDelta];
+						name = string_format("copy (%i,%i)", (int)dlt.m_dx, (int)dlt.m_dy);
+					}
+					name += s_amp_names[amp];
+
+					fmt_debug_printf("  {}: {} ({.2}%)\n", pad(name.c_str(), 28), m_pred_hist[i], (float)m_pred_hist[i] * inv_coded);
+				}
+			}
+
+			// ---- weight coding mode ----
+			fmt_debug_printf("\n---- WEIGHTS: DCT vs lossless DPCM decision ----\n");
+			{
+				const uint64_t wt_coded = m_wt_dct_blocks + m_wt_dpcm_blocks;
+				const float inv_wt = wt_coded ? (100.0f / (float)wt_coded) : 0.0f;
+
+				fmt_debug_printf("dct: {} ({.2}%), dpcm: {} ({.2}%) [decision: dpcm_bits*100 <= dct_bits*{}]\n",
+					m_wt_dct_blocks, (float)m_wt_dct_blocks * inv_wt, m_wt_dpcm_blocks, (float)m_wt_dpcm_blocks * inv_wt, m_wt_alpha_pct);
+				fmt_debug_printf("est bits -- all DCT: {}, all DPCM: {}, chosen: {}\n",
+					m_wt_dct_est_bits_total / 8, m_wt_dpcm_est_bits_total / 8, m_wt_chosen_est_bits / 8);
+
+				std::string l1("ACs/block when DCT won: ");
+				std::string l2("ACs/block when DPCM won: ");
+				for (uint32_t i = 0; i <= 32; i++)
+				{
+					if (m_wt_block_acs_hist_dct[i])
+						l1 += string_format("%u:%llu ", i, (unsigned long long)m_wt_block_acs_hist_dct[i]);
+					if (m_wt_block_acs_hist_dpcm[i])
+						l2 += string_format("%u:%llu ", i, (unsigned long long)m_wt_block_acs_hist_dpcm[i]);
+				}
+				fmt_debug_printf("{}\n", l1);
+				if (m_wt_dpcm_blocks)
+					fmt_debug_printf("{}\n", l2);
+
+				for (uint32_t cls = 0; cls < 3; cls++)
+				{
+					if (!m_wt_resid_count[cls])
+						continue;
+					fmt_debug_printf("dpcm resid {}b: n={}, mean|d|={.3}, zero={.2}%\n",
+						cls + 2, m_wt_resid_count[cls],
+						(float)m_wt_resid_sum_abs[cls] / (float)m_wt_resid_count[cls],
+						(float)m_wt_resid_zero[cls] * 100.0f / (float)m_wt_resid_count[cls]);
+
+					std::string mh(string_format("dpcm resid %ub |mag| hist:", cls + 2));
+					for (uint32_t m = 0; m < 9; m++)
+						if (m_wt_resid_mag_hist[cls][m])
+							mh += string_format(" %u:%.3f%%", m, (double)m_wt_resid_mag_hist[cls][m] * 100.0 / (double)m_wt_resid_count[cls]);
+					fmt_debug_printf("{}\n", mh);
+				}
+
+				if (m_wt_dpcm_blocks)
+				{
+					uint32_t idx[cTotalCandidates * 4];
+					for (uint32_t i = 0; i < cTotalCandidates * 4; i++)
+						idx[i] = i;
+					std::sort(idx, idx + cTotalCandidates * 4,
+						[&](uint32_t a, uint32_t b) { return m_wt_dpcm_pred_hist[a] > m_wt_dpcm_pred_hist[b]; });
+
+					std::string tops("top dpcm predictors:");
+					for (uint32_t r = 0; r < 8; r++)
+					{
+						const uint32_t i = idx[r];
+						if (!m_wt_dpcm_pred_hist[i])
+							break;
+
+						const uint32_t cand = i % cTotalCandidates, amp = i / cTotalCandidates;
+						static const char* s_amp_suffix[4] = { "", "/flip", "/half", "/half-flip" };
+
+						std::string name;
+						if (cand < cCandFirstXYDelta)
+							name = g_cand_names[cand];
+						else
+						{
+							const xbc7_xy_delta& dlt = g_xbc7_xy_deltas[cand - cCandFirstXYDelta];
+							name = string_format("copy(%i,%i)", (int)dlt.m_dx, (int)dlt.m_dy);
+						}
+
+						tops += string_format(" %s%s:%llu", name.c_str(), s_amp_suffix[amp], (unsigned long long)m_wt_dpcm_pred_hist[i]);
+					}
+					fmt_debug_printf("{}\n", tops);
+				}
+			}
+
+			// ---- solid ----
+			if (m_cmd_hist[(uint32_t)xbc7_command_id::cCmdSolidDPCM])
+			{
+				fmt_debug_printf("\n---- SOLID: color deltas vs neighbor edge prediction ----\n");
+				for (uint32_t c = 0; c < 4; c++)
+				{
+					if (!m_solid_delta_count[c])
+						continue;
+					fmt_debug_printf("{}: n={}, mean|d|={.3}, zero={.2}%\n",
+						s_chan_names[c], m_solid_delta_count[c],
+						(float)m_solid_delta_sum_abs[c] / (float)m_solid_delta_count[c],
+						(float)m_solid_delta_zero[c] * 100.0f / (float)m_solid_delta_count[c]);
+				}
+			}
+
+			// ---- DCT coefficient stats ----
+			fmt_debug_printf("\n");
+			m_dct_stats.print();
+
+			fmt_debug_printf("=====================================\n\n");
+		}
+	};
+
+
+
+	// NOTE: prev-emission coherence biasing of the predictor/mode sweeps was
+	// tried here (prefer the previously-emitted symbol within a cost margin,
+	// to cluster the predictor/command/index streams into Zstd-friendly runs)
+	// and MEASURED WORSE at every margin including pure tie-breaking: the
+	// existing earliest-candidate tie-break already concentrates ties onto a
+	// few globally-frequent symbols, and that global skew compresses better
+	// than local runs. Don't re-try without a new mechanism.
+
+	// The mechanism that DOES work, generalized: a later sweep candidate must
+	// beat the incumbent by more than this margin to displace it, so ties and
+	// near-ties collapse onto the lowest-index candidates GLOBALLY (global
+	// symbol skew is what Zstd prices). For the endpoint sweep the low-index
+	// candidates are also the dedicated neighbor modes, which don't spend an
+	// index byte. The weight-DCT sweep has its own long-standing equivalent
+	// (ERR_SWITCH_MARGIN_PERMILLE).
+	const uint32_t XBC7_SWEEP_SWITCH_MARGIN_PERMILLE = 30;
+
+	// ---- effort-driven weight-predictor pruning ----------------------------
+	// The per-block weight-grid sweeps (pack_weights / pack_weights_dpcm) try
+	// every (cand_index, amp_code) predictor; the DCT sweep re-runs a forward+
+	// inverse DCT per candidate, so it dominates encode time at Q < 100. The
+	// effort knob prunes WHICH candidates are tried (not the iteration ORDER --
+	// the sweeps still walk cand_index 0..cTotalCandidates and merely SKIP the
+	// disabled ones, so the earliest-candidate tie-break is preserved and effort
+	// 10 reproduces the pre-knob output exactly).
+	//
+	// g_xbc7_pred_priority lists candidates most-valuable first, derived from
+	// winning-predictor histograms across many images at Q=1/10/80 (opaque +
+	// alpha). The dictionary "copy(dx,dy)" predictors collectively win ~2/3 of
+	// blocks, so the high-value core deliberately mixes the strongest synthetic
+	// predictors (left/upper edge, diag avg) with the nearest causal copies.
+	// cCandAbsolute is first -- it's the no-prediction fallback and must always
+	// be available. Any candidate not listed here is appended in natural order
+	// by xbc7_build_pred_mask (so the table need only spell out the head).
+	static const uint8_t g_xbc7_pred_priority[] =
+	{
+		cCandAbsolute, cCandLeftEdge, cCandUpperEdge,
+		cCandFirstXYDelta + 0, cCandFirstXYDelta + 1, cCandDiagAvg,   // <-- core 6 (effort 0)
+		cCandFirstXYDelta + 6, cCandFirstXYDelta + 2, cCandFirstXYDelta + 7,
+		cCandFirstXYDelta + 8, cCandReflectLeft, cCandFirstXYDelta + 4,
+		cCandFirstXYDelta + 5, cCandReflectUpper, cCandPlaneFit,
+		cCandFirstXYDelta + 3, cCandLUBlendStrong, cCandDDR, cCandDDL,
+		cCandGradient, cCandGradientDamped, cCandLUBlend, cCandLUAvg,
+		cCandUpperDiagEdgeBlend, cCandDiagEdgeBlend, cCandMED, cCandGAB,
+	};
+
+	// Build the enabled-candidate bitmask for an effort level [0,10]. effort 0
+	// enables the core 6; the count ramps linearly to the full set at effort 10
+	// (where the mask is all-ones -> the sweeps are byte-for-byte unchanged).
+	// cTotalCandidates < 64, so a single uint64_t holds the whole set.
+	static uint64_t xbc7_build_pred_mask(uint32_t effort)
+	{
+		static_assert(cTotalCandidates <= 64, "predictor mask must fit in uint64_t");
+
+		effort = basisu::minimum<uint32_t>(effort, 10);
+
+		const uint32_t cCoreCands = 6;
+		uint32_t count = cCoreCands + ((cTotalCandidates - cCoreCands) * effort + 5) / 10;
+		count = basisu::minimum<uint32_t>(count, cTotalCandidates);
+
+		// priority head, then any remaining candidate in natural order -> a valid
+		// permutation of [0, cTotalCandidates) regardless of what the head omits
+		uint8_t order[cTotalCandidates];
+		bool used[cTotalCandidates] = { false };
+		uint32_t n = 0;
+		for (uint32_t i = 0; i < sizeof(g_xbc7_pred_priority); i++)
+		{
+			const uint8_t c = g_xbc7_pred_priority[i];
+			assert((c < cTotalCandidates) && !used[c]); // head must be unique & in-range
+			used[c] = true;
+			order[n++] = c;
+		}
+		for (uint32_t c = 0; c < cTotalCandidates; c++)
+			if (!used[c])
+				order[n++] = (uint8_t)c;
+		assert(n == cTotalCandidates);
+
+		uint64_t mask = 0;
+		for (uint32_t i = 0; i < count; i++)
+			mask |= (1ull << order[i]);
+
+		assert(mask & (1ull << cCandAbsolute)); // fallback always present
+		return mask;
+	}
+
+	// Amp codes the per-block sweeps try, by effort [0,10]. The amp loop runs in
+	// descending win-share order (0=identity, 1=flip, 2=half-contrast, 3=half-
+	// flip -- confirmed by histograms), so trying the first N keeps the most
+	// valuable. effort 10 -> 4 (the full set -> output unchanged vs pre-knob).
+	static uint32_t xbc7_amp_codes_for_effort(uint32_t effort)
+	{
+		static const uint8_t tbl[11] = { 1, 1, 1, 1, 2, 2, 3, 3, 4, 4, 4 };
+		return tbl[basisu::minimum<uint32_t>(effort, 10)];
+	}
+
+	bool pack_weights(
+		uint32_t bx, uint32_t by, uint32_t num_blocks_x,
+		const tile_bounds& tile,
+		const color_rgba orig_block[16],
+		const vector2D<basist::bc7u::log_bc7_block> &log_blks,
+		dct_syms best_syms[2], basist::bc7u::log_bc7_block &best_cand_log_blk, uint32_t& best_predictor_index,
+		xbc7_weight_grid_dct_fixed &weight_grid_dct_fixed, fxvec &dct_work_fixed, const pack_options& opts,
+		uint64_t enabled_pred_mask, uint32_t num_amp_codes)
+	{
+		best_predictor_index = 0;
+
+		const uint32_t global_q = opts.m_dct_q;
+
+		const uint64_t ERR_SWITCH_MARGIN_PERMILLE = 20;
+		const uint32_t TOTAL_AMP_CODES = num_amp_codes; // 0=identity, 1=flip, 2=half contrast, 3=half of flip (effort-pruned)
+
+		const basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by);
+
+		best_syms[0].clear();
+		best_syms[1].clear();
+		best_cand_log_blk = log_blk;
+		
+		uint64_t best_err = UINT64_MAX;
+		uint32_t best_num_ac_syms = UINT32_MAX;
+
+		for (uint32_t amp_code = 0; amp_code < TOTAL_AMP_CODES; amp_code++)
+		{
+			for (uint32_t cand_index = 0; cand_index < cTotalCandidates; cand_index++)
+			{
+				if ((amp_code) && (cand_index == cCandAbsolute))
+					continue;
+
+				// effort pruning: skip predictors not in this effort's enabled set
+				// (mask is all-ones at effort 10, so this never fires there)
+				if (!((enabled_pred_mask >> cand_index) & 1u))
+					continue;
+
+				basist::bc7u::log_bc7_block cand_log_blk(log_blk);
+
+				uint32_t cand_total_ac_syms = 0;
+
+				dct_syms cand_syms[2];
+				cand_syms[0].clear();
+				cand_syms[1].clear();
+
+				for (uint32_t p = 0; p < cand_log_blk.m_num_planes; p++)
+				{
+					int weight_preds[16];
+					int* pWeight_predictions = nullptr;
+
+					if (cand_index != cCandAbsolute)
+					{
+						bool eval_status = eval_weight_predictor(
+							cand_index, amp_code,
+							bx, by, num_blocks_x,
+							tile,
+							log_blks,
+							p, weight_preds);
+
+						if (!eval_status)
+							goto skip_cand;
+
+						pWeight_predictions = weight_preds;
+					}
+
+					dct_syms syms;
+
+					weight_grid_dct_fixed.forward(basist::fixed16_16::from_int(global_q), p, pWeight_predictions, cand_log_blk, syms, dct_work_fixed);
+
+					memset(cand_log_blk.m_weights[p], 0, 16);
+
+					bool status = weight_grid_dct_fixed.inverse(basist::fixed16_16::from_int(global_q), p, pWeight_predictions, syms, cand_log_blk, dct_work_fixed);
+
+					if (!status)
+					{
+						assert(0);
+						return false;
+					}
+
+					uint32_t total_acs = syms.m_ac_vals.size_u32();
+					if (total_acs && (syms.m_ac_vals[total_acs - 1].m_coeff == INT16_MAX))
+						total_acs--;
+					cand_total_ac_syms += total_acs;
+
+					cand_syms[p] = syms;
+
+				} // p
+
+				// scoped so 'goto skip_cand' leaps over (not into) these inits -- required under /permissive-
+				{
+				color_rgba cand_block_pixels[16];
+				if (!basist::bc7u::unpack_bc7(cand_log_blk, (basist::color_rgba*)cand_block_pixels))
+				{
+					assert(0);
+					return false;
+				}
+
+				uint64_t cand_err = 0;
+				for (uint32_t i = 0; i < 16; i++)
+					cand_err += cand_block_pixels[i].get_weighted_dist2(orig_block[i], opts.m_weights);
+
+				if ((cand_total_ac_syms < best_num_ac_syms) ||
+					((cand_total_ac_syms == best_num_ac_syms) && ((cand_err * 1000) < (best_err * (1000 - ERR_SWITCH_MARGIN_PERMILLE)))))
+				{
+					best_cand_log_blk = cand_log_blk;
+					best_predictor_index = cand_index + amp_code * cTotalCandidates;
+					best_num_ac_syms = cand_total_ac_syms;
+					best_err = cand_err;
+					best_syms[0] = cand_syms[0];
+					best_syms[1] = cand_syms[1];
+				}
+				} // end candidate-eval scope
+
+			skip_cand:
+				;
+
+			} // cand_index
+
+		} // amp_code
+
+		assert(best_syms[0].m_ac_vals.size());
+
+		return true;
+	}
+
+	// Estimated per-symbol coding costs in 1/8-bit units ("octobits"), measured
+	// on the kodim corpus via the stats dashboard.
+	//
+	// DPCM weight residual cost, [weight_bits - 2][min(|centered resid|, 8)]:
+	// -log2 of each symbol's probability (the magnitude's mass split across
+	// +/-), measured at Q=100 where EVERY block codes DPCM, so the
+	// distributions carry no mode-decision selection bias. Order-0 model;
+	// Zstd lands within ~13% of it on these streams.
+	static const uint32_t g_dpcm_resid_cost_obits[3][9] =
+	{
+		{ 8, 17, 47, 47, 47, 47, 47, 47, 47 },		// 2-bit (centered range [-2,1])
+		{ 12, 19, 27, 38, 63, 63, 63, 63, 63 },	// 3-bit (centered range [-4,3])
+		{ 12, 25, 29, 33, 38, 44, 52, 61, 81 },	// 4-bit (centered range [-8,7])
+	};
+
+	// DCT stream bytes priced at their measured post-Zstd cost (Q 60..95):
+	// DC bytes ~4.7 bits with the 6-bit DC quantization below (was ~6.4 at
+	// 8-bit DC), AC run/magnitude stream bytes 3.1..3.8 bits (3.5 used).
+	// Sign bits are incompressible noise: exactly 1 bit each.
+	const uint32_t DCT_DC_BYTE_COST_OBITS = 38;
+	const uint32_t DCT_AC_BYTE_COST_OBITS = 28;
+
+	// Lossless residual DPCM weight coding: predict the dequantized [0,64] grid
+	// with the shared predictor bank, quantize the prediction to the plane's bit
+	// depth (quant_weight), then wrap-difference the quantized indices mod 2^n.
+	// Reconstruction is exact, so the predictor choice affects ONLY compressed
+	// size: pick the candidate minimizing the estimated emitted cost (octobit
+	// table above) -- a content-adaptive estimate, since zero-heavy residual
+	// grids are far cheaper than the stream average. cand 0 (absolute,
+	// predictor index 0) is always available, emits the raw indices to a
+	// separate blob, and is priced at its raw stored size.
+	struct dpcm_weights
+	{
+		uint32_t m_pred_index = 0;		// joint (cand + amp * cTotalCandidates) byte
+		uint8_t m_syms[2][16] = {};		// wrapped n-bit symbols (raw indices if absolute), [plane][texel]
+		uint64_t m_est_cost_obits = 0;	// estimated emitted cost of the winning candidate, 1/8-bit units
+	};
+
+	static void pack_weights_dpcm(
+		uint32_t bx, uint32_t by, uint32_t num_blocks_x,
+		const tile_bounds& tile,
+		const vector2D<basist::bc7u::log_bc7_block>& log_blks,
+		dpcm_weights& result,
+		uint64_t enabled_pred_mask, uint32_t num_amp_codes)
+	{
+		const uint32_t TOTAL_AMP_CODES = num_amp_codes;
+
+		const basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by);
+
+		uint64_t best_cost = UINT64_MAX;
+
+		for (uint32_t amp_code = 0; amp_code < TOTAL_AMP_CODES; amp_code++)
+		{
+			for (uint32_t cand_index = 0; cand_index < cTotalCandidates; cand_index++)
+			{
+				if ((amp_code) && (cand_index == cCandAbsolute))
+					continue;
+
+				// effort pruning (see pack_weights): all-ones at effort 10
+				if (!((enabled_pred_mask >> cand_index) & 1u))
+					continue;
+
+				uint64_t cost = 0;
+				uint8_t syms[2][16] = {}; // zeroed so single-plane blocks never copy indeterminate plane-1 bytes
+				bool valid = true;
+
+				for (uint32_t p = 0; p < log_blk.m_num_planes; p++)
+				{
+					const uint32_t num_bits = log_blk.m_weight_bits[p];
+					const uint32_t mask = (1u << num_bits) - 1;
+					const int half = 1 << (num_bits - 1);
+
+					int weight_preds[16];
+					int* pWeight_predictions = nullptr;
+
+					if (cand_index != cCandAbsolute)
+					{
+						if (!eval_weight_predictor(cand_index, amp_code, bx, by, num_blocks_x, tile, log_blks, p, weight_preds))
+						{
+							valid = false;
+							break;
+						}
+						pWeight_predictions = weight_preds;
+					}
+
+					for (uint32_t i = 0; i < 16; i++)
+					{
+						const uint32_t pred_index = pWeight_predictions ? basist::bc7u::quant_weight(pWeight_predictions[i], num_bits) : 0;
+						const uint32_t sym = (log_blk.m_weights[p][i] - pred_index) & mask;
+						syms[p][i] = (uint8_t)sym;
+
+						if (pWeight_predictions)
+						{
+							const int v = (sym >= (uint32_t)half) ? ((int)sym - (int)(mask + 1)) : (int)sym;
+							cost += g_dpcm_resid_cost_obits[num_bits - 2][basisu::minimum<int>(basisu::iabs(v), 8)];
+						}
+						else
+						{
+							// absolute: raw indices at their stored size (2 bits,
+							// or 4 for the nibble-expanded 3-bit class, or 4)
+							cost += 8u * ((num_bits == 2) ? 2u : 4u);
+						}
+					}
+				} // p
+
+				if (!valid)
+					continue;
+
+				// later candidates must beat the incumbent by the switch margin
+				// (UINT64_MAX guard: the multiply would wrap before any winner)
+				if ((best_cost == UINT64_MAX) || ((cost * 1000) < (best_cost * (1000 - (uint64_t)XBC7_SWEEP_SWITCH_MARGIN_PERMILLE))))
+				{
+					best_cost = cost;
+					result.m_pred_index = cand_index + amp_code * cTotalCandidates;
+					memcpy(result.m_syms, syms, sizeof(syms));
+				}
+
+			} // cand_index
+		} // amp_code
+
+		result.m_est_cost_obits = best_cost;
+	}
+
+	// per-stripe (per-job) output state; merged in stripe order after the jobs join
+	struct stripe_output
+	{
+		blob_stream_writer m_blob_writer;
+		basisu::bitwise_coder m_coeff_signs;
+		basisu::bitwise_coder m_pbits;
+		basisu::bitwise_coder m_raw_endpoint_bits;
+		xbc7_pack_stats m_stats;
+	};
+
+	// Optional block-reuse RDO pre-pass: runs after the BC7 base pack, BEFORE the
+	// endpoint RDO (once a whole block is reused there's no point trying cheaper
+	// endpoints). In raster order per stripe (honoring boundaries), for each
+	// non-solid block it tries the cheapest whole-block representations and adopts
+	// the best acceptable one: a REPEAT (exact copy of the left/upper causal
+	// neighbor -> 1-byte command; preferred) or SOLID (the block's mean color ->
+	// cheap SolidDPCM). "Acceptable" = weighted RGBA PSNR >= the shared floor AND
+	// within the per-mode tolerated drop vs the block's current PSNR. Modifies
+	// log_blks in place; serial (causal). Returns false on a decode failure.
+	static bool block_reuse_rdo_pass(
+		const image& orig_img,
+		basisu::vector2D<basist::bc7u::log_bc7_block>& log_blks,
+		uint32_t num_blocks_x,
+		const basisu::vector<stripe_range>& stripes,
+		const pack_options& opts)
+	{
+		const uint32_t* comp_weights = opts.m_weights;
+		const float min_block_psnr = opts.m_rdo_min_block_psnr;
+		const bool has_alpha = orig_img.has_alpha();
+
+		uint32_t changed_repeat = 0, changed_solid = 0;
+
+		for (uint32_t st = 0; st < stripes.size(); st++)
+		{
+			const uint32_t first_row = stripes[st].m_first_block_row;
+			const uint32_t end_row = first_row + stripes[st].m_num_block_rows;
+
+			for (uint32_t by = first_row; by < end_row; by++)
+			{
+				for (uint32_t bx = 0; bx < num_blocks_x; bx++)
+				{
+					basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by);
+
+					// NOTE: solid blocks are NOT skipped -- a RepeatLast/RepeatUpper
+					// command (1 byte) is cheaper than SolidDPCM, and pack_stripe checks
+					// Repeat before Solid, so we aggressively reuse a neighbor even for a
+					// solid block when it passes the PSNR test (VQ-style).
+					color_rgba src_pixels[16];
+					orig_img.extract_block_clamped(src_pixels, bx * 4, by * 4, 4, 4);
+
+					color_rgba dec[16];
+					if (!basist::bc7u::unpack_bc7(log_blk, (basist::color_rgba*)dec))
+					{
+						assert(0);
+						return false;
+					}
+					const float orig_psnr = xbc7_block_wsse_psnr(src_pixels, dec, comp_weights);
+
+					// ---- Repeat: copy a causal neighbor wholesale (cheapest command) ----
+					if (opts.m_repeat_rdo_enabled)
+					{
+						const basist::bc7u::log_bc7_block* preds[2] = { nullptr, nullptr };
+						if (bx >= 1)        preds[0] = &log_blks(bx - 1, by); // left  -> RepeatLast
+						if (by > first_row) preds[1] = &log_blks(bx, by - 1); // upper -> RepeatUpper
+
+						const basist::bc7u::log_bc7_block* best = nullptr;
+						float best_psnr = 0.0f;
+						for (uint32_t i = 0; i < 2; i++)
+						{
+							if (!preds[i])
+								continue;
+							if (!basist::bc7u::unpack_bc7(*preds[i], (basist::color_rgba*)dec))
+							{
+								assert(0);
+								return false;
+							}
+							const float p = xbc7_block_wsse_psnr(src_pixels, dec, comp_weights);
+							if ((p >= min_block_psnr) && (p >= orig_psnr - opts.m_repeat_rdo_max_psnr_drop) && ((!best) || (p > best_psnr)))
+							{
+								best = preds[i];
+								best_psnr = p;
+							}
+						}
+
+						if (best)
+						{
+							log_blk = *best; // bit-identical -> pack_stripe emits Repeat
+							changed_repeat++;
+							continue; // Repeat is the cheapest -- done with this block
+						}
+					}
+
+					// ---- Solid: replace with the block's mean color (next cheapest) ----
+					// (skip if already solid -- it already codes cheaply as SolidDPCM)
+					if (opts.m_solid_rdo_enabled && !basist::bc7u::is_solid_blk(log_blk))
+					{
+						uint32_t sum[4] = { 0, 0, 0, 0 };
+						for (uint32_t i = 0; i < 16; i++)
+							for (uint32_t c = 0; c < 4; c++)
+								sum[c] += src_pixels[i][c];
+
+						color_rgba mean((sum[0] + 8) >> 4, (sum[1] + 8) >> 4, (sum[2] + 8) >> 4, (sum[3] + 8) >> 4);
+						if (!has_alpha)
+							mean.a = 255;
+
+						basist::bc7u::log_bc7_block cand;
+						cand.clear();
+						basist::bc7u::create_solid_blk(cand, (const basist::color_rgba&)mean);
+
+						if (!basist::bc7u::unpack_bc7(cand, (basist::color_rgba*)dec))
+						{
+							assert(0);
+							return false;
+						}
+						const float p = xbc7_block_wsse_psnr(src_pixels, dec, comp_weights);
+						if ((p >= min_block_psnr) && (p >= orig_psnr - opts.m_solid_rdo_max_psnr_drop))
+						{
+							log_blk = cand;
+							changed_solid++;
+						}
+					}
+				}
+			}
+		}
+
+		if (opts.m_debug_output)
+		{
+			uint32_t total_blocks = 0;
+			for (uint32_t st = 0; st < stripes.size(); st++)
+				total_blocks += num_blocks_x * stripes[st].m_num_block_rows;
+			if (opts.m_repeat_rdo_enabled)
+				fmt_debug_printf("repeat RDO: {} blocks changed ({.2}%)\n", changed_repeat, total_blocks ? (changed_repeat * 100.0f / (float)total_blocks) : 0.0f);
+			if (opts.m_solid_rdo_enabled)
+				fmt_debug_printf("solid RDO: {} blocks changed ({.2}%)\n", changed_solid, total_blocks ? (changed_solid * 100.0f / (float)total_blocks) : 0.0f);
+		}
+
+		return true;
+	}
+
+	// Optional endpoint-prediction RDO pre-pass: runs after the BC7 base pack but
+	// BEFORE stripe coding. In raster order (per stripe, honoring its boundaries)
+	// it tries forcing each block's endpoints to a causal neighbor's prediction
+	// (left/upper/left-diag/right-diag) via a zero-residual endpoint DPCM, and
+	// re-optimizes that candidate's weights for the new endpoints, and adopts the
+	// best candidate when its weighted RGBA PSNR drops by no more than
+	// max_psnr_drop dB. The matching zero residuals then cost almost nothing in
+	// the coding pass (often collapsing to a Repeat). Solid blocks are skipped --
+	// they have their own cheaper SolidDPCM path. Modifies log_blks in place;
+	// serial, since each block predicts from already-finalized causal neighbors.
+	// Returns false on an (unexpected) decode failure.
+	static bool endpoint_dpcm_rdo_pass(
+		const image& orig_img,
+		basisu::vector2D<basist::bc7u::log_bc7_block>& log_blks,
+		uint32_t num_blocks_x,
+		const basisu::vector<stripe_range>& stripes,
+		const pack_options& opts)
+	{
+		const uint32_t* comp_weights = opts.m_weights;
+		const float max_psnr_drop = opts.m_endpoint_rdo_max_psnr_drop;
+		const float min_block_psnr = opts.m_rdo_min_block_psnr;
+
+		uint32_t changed_blocks = 0; // blocks whose endpoints+weights were rewritten
+
+#if defined(DEBUG) || defined(_DEBUG)
+		// sanity: re-optimizing a candidate's weights can never RAISE its WSSE
+		// (the original weights are in the swept set); count any case where it does
+		uint64_t dbg_weight_opt_cands = 0, dbg_weight_opt_worse = 0;
+#endif
+
+		for (uint32_t st = 0; st < stripes.size(); st++)
+		{
+			const uint32_t first_row = stripes[st].m_first_block_row;
+			const uint32_t end_row = first_row + stripes[st].m_num_block_rows;
+
+			for (uint32_t by = first_row; by < end_row; by++)
+			{
+				for (uint32_t bx = 0; bx < num_blocks_x; bx++)
+				{
+					basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by);
+
+					// solid blocks have a dedicated, cheaper command -- leave them alone
+					if (basist::bc7u::is_solid_blk(log_blk))
+						continue;
+
+					// already identical to a causal neighbor (e.g. from the block-reuse
+					// RDO, or naturally) -- it'll code as a cheap Repeat, so don't
+					// disturb its endpoints
+					if (((bx >= 1) && basist::bc7u::compare_block_full(log_blk, log_blks(bx - 1, by))) ||
+						((by > first_row) && basist::bc7u::compare_block_full(log_blk, log_blks(bx, by - 1))))
+						continue;
+
+					color_rgba src_pixels[16];
+					orig_img.extract_block_clamped(src_pixels, bx * 4, by * 4, 4, 4);
+
+					color_rgba orig_pixels[16];
+					if (!basist::bc7u::unpack_bc7(log_blk, (basist::color_rgba*)orig_pixels))
+					{
+						assert(0);
+						return false;
+					}
+					const float orig_psnr = xbc7_block_wsse_psnr(src_pixels, orig_pixels, comp_weights);
+
+					// causal neighbor predictors valid within this stripe, in order:
+					// left, upper, left-diag, right-diag (upper rows must stay in-stripe)
+					const basist::bc7u::log_bc7_block* preds[4] = { nullptr, nullptr, nullptr, nullptr };
+					if (bx >= 1)                                     preds[0] = &log_blks(bx - 1, by);
+					if (by > first_row)                              preds[1] = &log_blks(bx, by - 1);
+					if ((bx >= 1) && (by > first_row))               preds[2] = &log_blks(bx - 1, by - 1);
+					if ((bx + 1 < num_blocks_x) && (by > first_row)) preds[3] = &log_blks(bx + 1, by - 1);
+
+					basist::bc7u::log_bc7_block best_cand{}; // value-init silences a C4701 false positive (only read when have_cand)
+					float best_cand_psnr = 0.0f;
+					bool have_cand = false;
+
+					for (uint32_t i = 0; i < 4; i++)
+					{
+						if (!preds[i])
+							continue;
+
+						// copy keeps mode/config/weights; only endpoints+pbits change
+						basist::bc7u::log_bc7_block cand(log_blk);
+
+						for (uint32_t s = 0; s < cand.m_num_partitions; s++)
+						{
+							uint8_t residuals[8] = { 0 }; // zero residuals -> endpoints become the prediction
+							uint8_t pbits[2] = { 0 };
+							uint32_t num_residuals = 0, num_pbits = 0;
+							basist::bc7u::endpoint_dpcm(true, *preds[i], 0, cand, s, residuals, num_residuals, pbits, num_pbits);
+						}
+
+#if defined(DEBUG) || defined(_DEBUG)
+						// WSSE with the now-stale weights, before re-optimization
+						uint64_t dbg_wsse_before = 0;
+						{
+							color_rgba pre[16];
+							if (!basist::bc7u::unpack_bc7(cand, (basist::color_rgba*)pre))
+							{
+								assert(0);
+								return false;
+							}
+							for (uint32_t k = 0; k < 16; k++)
+								dbg_wsse_before += pre[k].get_weighted_dist2(src_pixels[k], comp_weights);
+						}
+#endif
+
+						// the slammed endpoints leave the copied weights stale -- recompute
+						// the optimal weights for the new endpoints (can only help vs. stale)
+						basist::bc7u::log_bc7_block cand_opt;
+						if (!optimize_block_weights(cand, src_pixels, comp_weights, cand_opt))
+							return false; // already asserted inside
+						cand = cand_opt;
+
+						color_rgba cand_pixels[16];
+						if (!basist::bc7u::unpack_bc7(cand, (basist::color_rgba*)cand_pixels))
+						{
+							assert(0);
+							return false;
+						}
+
+#if defined(DEBUG) || defined(_DEBUG)
+						{
+							uint64_t dbg_wsse_after = 0;
+							for (uint32_t k = 0; k < 16; k++)
+								dbg_wsse_after += cand_pixels[k].get_weighted_dist2(src_pixels[k], comp_weights);
+
+							dbg_weight_opt_cands++;
+							if (dbg_wsse_after > dbg_wsse_before)
+								dbg_weight_opt_worse++; // weight re-optimization must never make WSSE worse
+						}
+#endif
+
+						const float cand_psnr = xbc7_block_wsse_psnr(src_pixels, cand_pixels, comp_weights);
+						if ((!have_cand) || (cand_psnr > best_cand_psnr))
+						{
+							have_cand = true;
+							best_cand_psnr = cand_psnr;
+							best_cand = cand;
+						}
+					}
+
+					// adopt the best candidate if quality drops by no more than allowed
+					// (a gain is of course also accepted)
+					if (have_cand && (best_cand_psnr >= min_block_psnr) && (best_cand_psnr >= orig_psnr - max_psnr_drop))
+					{
+						log_blk = best_cand;
+						changed_blocks++;
+					}
+				}
+			}
+		}
+
+		if (opts.m_debug_output)
+		{
+			uint32_t total_blocks = 0;
+			for (uint32_t st = 0; st < stripes.size(); st++)
+				total_blocks += num_blocks_x * stripes[st].m_num_block_rows;
+			fmt_debug_printf("endpoint RDO: {} blocks changed ({.2}%)\n", changed_blocks, total_blocks ? (changed_blocks * 100.0f / (float)total_blocks) : 0.0f);
+		}
+
+#if defined(DEBUG) || defined(_DEBUG)
+		fmt_debug_printf("endpoint RDO weight re-opt: {} candidates, {} got WORSE (expected 0)\n", dbg_weight_opt_cands, dbg_weight_opt_worse);
+		// assert(!dbg_weight_opt_worse); // intentionally NOT asserted -- probably too strong; observe via the stat above instead
+#endif
+
+		return true;
+	}
+
+	// Optional weight-DCT AC-truncation RDO. Run ONCE on the winning DCT symbols
+	// (after pack_weights, NOT inside its candidate sweep). Zeros the highest-
+	// frequency weight-DCT AC coefficients one at a time in reverse zigzag order,
+	// protecting the 2x2 low-freq corner (DC,(1,0),(0,1),(1,1) -> zigzag {0,1,2,4}),
+	// while the decoded weighted RGBA PSNR stays within m_ac_trunc_rdo_max_psnr_drop
+	// dB of the un-truncated block AND >= the shared floor. Updates best_syms
+	// (pruned) and best_cand_log_blk (weights rebuilt via the same inverse() the
+	// decoder uses, so it round-trips). Returns false on an unexpected decode failure.
+	static bool ac_truncate_rdo(
+		dct_syms best_syms[2],
+		basist::bc7u::log_bc7_block& best_cand_log_blk,
+		uint32_t best_predictor_index,
+		const color_rgba orig_block[16],
+		uint32_t bx, uint32_t by, uint32_t num_blocks_x,
+		const tile_bounds& tile,
+		const vector2D<basist::bc7u::log_bc7_block>& log_blks,
+		xbc7_weight_grid_dct_fixed& weight_grid_dct_fixed,
+		fxvec& dct_work,
+		const pack_options& opts,
+		xbc7_pack_stats& stats)
+	{
+		const uint32_t num_planes = best_cand_log_blk.m_num_planes;
+
+		// winning predictor -> per-plane predictions (pack_weights left untouched;
+		// eval_weight_predictor asserts non-absolute, so absolute -> null preds)
+		const uint32_t cand_index = best_predictor_index % cTotalCandidates;
+		const uint32_t amp_code = best_predictor_index / cTotalCandidates;
+
+		int preds[2][16];
+		const int* pPreds[2] = { nullptr, nullptr };
+		if (cand_index != cCandAbsolute)
+		{
+			for (uint32_t p = 0; p < num_planes; p++)
+			{
+				if (!eval_weight_predictor(cand_index, amp_code, bx, by, num_blocks_x, tile, log_blks, p, preds[p]))
+				{
+					assert(0);
+					return false;
+				}
+				pPreds[p] = preds[p];
+			}
+		}
+
+		// baseline PSNR of the un-truncated DCT-coded block
+		color_rgba dec[16];
+		if (!basist::bc7u::unpack_bc7(best_cand_log_blk, (basist::color_rgba*)dec))
+		{
+			assert(0);
+			return false;
+		}
+		const float orig_psnr = xbc7_block_wsse_psnr(orig_block, dec, opts.m_weights);
+
+		int flat[2][16];
+		for (uint32_t p = 0; p < num_planes; p++)
+			xbc7_syms_to_flat(best_syms[p], flat[p]);
+
+		const float drop = opts.m_ac_trunc_rdo_max_psnr_drop;
+		const float floor_psnr = opts.m_rdo_min_block_psnr;
+
+		uint64_t pruned_here = 0;
+
+		// reverse zigzag, skipping protected low-freq slots (zigzag 1,2,4; 0 = DC)
+		for (uint32_t zz = 15; zz >= 1; zz--)
+		{
+			if ((zz == 1) || (zz == 2) || (zz == 4))
+				continue;
+
+			const uint32_t nat = g_zigzag4x4_xy[zz][0] + g_zigzag4x4_xy[zz][1] * 4;
+
+			// skip if this coefficient is already zero in every plane
+			bool any_nonzero = false;
+			for (uint32_t p = 0; p < num_planes; p++)
+				if (flat[p][nat] != 0)
+					any_nonzero = true;
+			if (!any_nonzero)
+				continue;
+
+			int saved[2] = { 0, 0 };
+			for (uint32_t p = 0; p < num_planes; p++)
+			{
+				saved[p] = flat[p][nat];
+				flat[p][nat] = 0;
+			}
+
+			// rebuild the candidate from the tentatively-truncated grids
+			basist::bc7u::log_bc7_block trial_blk = best_cand_log_blk; // config + endpoints kept
+			dct_syms trial_syms[2];
+			for (uint32_t p = 0; p < num_planes; p++)
+			{
+				xbc7_flat_to_syms(flat[p], trial_syms[p]);
+				memset(trial_blk.m_weights[p], 0, 16);
+				if (!weight_grid_dct_fixed.inverse(basist::fixed16_16::from_int(opts.m_dct_q), p, pPreds[p], trial_syms[p], trial_blk, dct_work))
+				{
+					assert(0);
+					return false;
+				}
+			}
+
+			if (!basist::bc7u::unpack_bc7(trial_blk, (basist::color_rgba*)dec))
+			{
+				assert(0);
+				return false;
+			}
+			const float psnr = xbc7_block_wsse_psnr(orig_block, dec, opts.m_weights);
+
+			if ((psnr >= floor_psnr) && (psnr >= orig_psnr - drop))
+			{
+				best_cand_log_blk = trial_blk;
+				for (uint32_t p = 0; p < num_planes; p++)
+				{
+					best_syms[p] = trial_syms[p];
+					if (saved[p])
+						pruned_here++;
+				}
+			}
+			else
+			{
+				for (uint32_t p = 0; p < num_planes; p++)
+					flat[p][nat] = saved[p]; // revert this coefficient
+				break; // stop at the first unacceptable truncation
+			}
+		}
+
+		stats.m_ac_trunc_pruned += pruned_here;
+		if (pruned_here)
+			stats.m_ac_trunc_blocks++;
+
+		return true;
+	}
+
+	// with every causal reference clamped to the stripe (the decoder mirrors
+	// the one IMPLICIT cross-row prediction -- solid blocks -- via the header
+	// stripe count; explicit references are simply never emitted across a
+	// stripe boundary). All scratch state is local or lives in `out`.
+	static bool pack_stripe(
+		const image& orig_img,
+		const pack_options& opts,
+		bool has_alpha,
+		uint32_t num_blocks_x,
+		const stripe_range& stripe,
+		vector2D<basist::bc7u::log_bc7_block>& log_blks,
+		vector2D<basist::bc7u::log_bc7_block>* pCoded_log_blocks,
+		const vector2D<uint8_t>& base_used_lut, // per-block: 1 if the bc7e_scalar base pack used its endpoint LUTs (force DPCM)
+		stripe_output& out,
+		const xbc7_debug_image_set& dbg_imgs) // debug only; members null unless m_debug_images. Stripes write disjoint rows, so filling is thread-safe.
+	{
+		const uint32_t first_row = stripe.m_first_block_row;
+		const uint32_t end_row = stripe.m_first_block_row + stripe.m_num_block_rows;
+
+		// this stripe as an inclusive block AABB (full width, for now); ALL
+		// causal references in this pass are gated through tile.contains()
+		const tile_bounds tile = { 0, (int)first_row, (int)num_blocks_x - 1, (int)end_row - 1 };
+
+		blob_stream_writer& blob_writer = out.m_blob_writer;
+		basisu::bitwise_coder& coeff_signs = out.m_coeff_signs;
+		basisu::bitwise_coder& pbits = out.m_pbits;
+		basisu::bitwise_coder& raw_endpoint_bits = out.m_raw_endpoint_bits;
+		xbc7_pack_stats& stats = out.m_stats;
+
+		xbc7_weight_grid_dct_fixed weight_grid_dct_fixed;
+		weight_grid_dct_fixed.init();
+
+		fxvec dct_work_fixed;
+
+		// hoisted so the per-block visualization calls compile out entirely when disabled
+		const bool debug_images = opts.m_debug_images;
+
+		// effort -> enabled weight-predictor set + amp-code count (constant for the
+		// whole stripe; at effort 10 both are full so the sweeps are unchanged)
+		const uint64_t enabled_pred_mask = xbc7_build_pred_mask(opts.m_effort_level);
+		const uint32_t num_amp_codes = xbc7_amp_codes_for_effort(opts.m_effort_level);
+
+		for (uint32_t by = first_row; by < end_row; by++)
+		{
+			for (uint32_t bx = 0; bx < num_blocks_x; bx++)
+			{
+				// causal neighbors, clamped to the tile AABB: blocks outside it
+				// belong to another coding unit and are off limits
+				const basist::bc7u::log_bc7_block* pLeft_log_blk = tile.contains((int)bx - 1, (int)by) ? &log_blks(bx - 1, by) : nullptr;
+				const basist::bc7u::log_bc7_block* pUp_log_blk = tile.contains((int)bx, (int)by - 1) ? &log_blks(bx, by - 1) : nullptr;
+
+				const basist::bc7u::log_bc7_block* pLeft_diag_log_blk = tile.contains((int)bx - 1, (int)by - 1) ? &log_blks(bx - 1, by - 1) : nullptr;
+				const basist::bc7u::log_bc7_block* pRight_diag_log_blk = tile.contains((int)bx + 1, (int)by - 1) ? &log_blks(bx + 1, by - 1) : nullptr;
+
+				basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by);
+
+				if (pUp_log_blk && basist::bc7u::compare_block_full(log_blk, *pUp_log_blk))
+				{
+					blob_writer.put_byte(cBlobCommands, (uint8_t)xbc7_command_id::cCmdRepeatUpper);
+					stats.m_cmd_hist[(uint32_t)xbc7_command_id::cCmdRepeatUpper]++;
+
+					if (debug_images)
+						xbc7_vis_fill_command(dbg_imgs, bx, by, (uint8_t)xbc7_command_id::cCmdRepeatUpper, false, UINT32_MAX, UINT32_MAX);
+
+					if (pCoded_log_blocks)
+					{
+						(*pCoded_log_blocks)(bx, by) = log_blk;
+					}
+
+					continue;
+				}
+
+				if (pLeft_log_blk && basist::bc7u::compare_block_full(log_blk, *pLeft_log_blk))
+				{
+					blob_writer.put_byte(cBlobCommands, (uint8_t)xbc7_command_id::cCmdRepeatLast);
+					stats.m_cmd_hist[(uint32_t)xbc7_command_id::cCmdRepeatLast]++;
+
+					if (debug_images)
+						xbc7_vis_fill_command(dbg_imgs, bx, by, (uint8_t)xbc7_command_id::cCmdRepeatLast, false, UINT32_MAX, UINT32_MAX);
+
+					if (pCoded_log_blocks)
+					{
+						(*pCoded_log_blocks)(bx, by) = log_blk;
+					}
+
+					continue;
+				}
+								
+				if (basist::bc7u::is_solid_blk(log_blk))
+				{
+					int preds[4] = {};
+					int num_preds = 0;
+
+					if (pLeft_log_blk)
+					{
+						for (uint32_t y = 0; y < 4; y++)
+						{
+							basist::color_rgba p;
+							if (!basist::bc7u::unpack_bc7_texel(*pLeft_log_blk, p, 3, y))
+							{
+								assert(0);
+								return false;
+							}
+							preds[0] += p.r;
+							preds[1] += p.g;
+							preds[2] += p.b;
+							preds[3] += p.a;
+						}
+						num_preds += 4;
+					}
+
+					if (pUp_log_blk)
+					{
+						for (uint32_t x = 0; x < 4; x++)
+						{
+							basist::color_rgba p;
+							if (!basist::bc7u::unpack_bc7_texel(*pUp_log_blk, p, x, 3))
+							{
+								assert(0);
+								return false;
+							}
+							preds[0] += p.r;
+							preds[1] += p.g;
+							preds[2] += p.b;
+							preds[3] += p.a;
+						}
+						num_preds += 4;
+					}
+
+					if (num_preds)
+					{
+						for (uint32_t c = 0; c < 4; c++)
+							preds[c] = (preds[c] + (num_preds / 2)) / num_preds;
+					}
+
+					blob_writer.put_byte(cBlobCommands, (uint8_t)xbc7_command_id::cCmdSolidDPCM);
+					stats.m_cmd_hist[(uint32_t)xbc7_command_id::cCmdSolidDPCM]++;
+
+					if (debug_images)
+						xbc7_vis_fill_command(dbg_imgs, bx, by, (uint8_t)xbc7_command_id::cCmdSolidDPCM, false, UINT32_MAX, UINT32_MAX);
+
+					basist::color_rgba solid_color;
+					if (!basist::bc7u::unpack_bc7_texel(log_blk, solid_color, 0, 0))
+					{
+						assert(0);
+						return false;
+					}
+
+					if (!has_alpha)
+						solid_color.a = 255; // important in case a p-bit set a=254
+					
+					for (uint32_t c = 0; c < (has_alpha ? 4u : 3u); c++)
+					{
+						const uint8_t delta = (uint8_t)(solid_color[c] - preds[c]);
+						blob_writer.put_byte(cBlobSolidRGBADeltas, delta);
+
+						stats.m_solid_delta_count[c]++;
+						stats.m_solid_delta_sum_abs[c] += (uint64_t)basisu::iabs((int8_t)delta);
+						if (!delta)
+							stats.m_solid_delta_zero[c]++;
+					}
+
+					basist::bc7u::create_solid_blk(log_blk, solid_color);
+
+					if (pCoded_log_blocks)
+					{
+						(*pCoded_log_blocks)(bx, by) = log_blk;
+					}
+
+					continue;
+				}
+
+				color_rgba orig_block[16];
+				orig_img.extract_block_clamped(orig_block, bx * 4, by * 4, 4, 4);
+
+				uint8_t command_byte = 0;
+
+				// Code config
+				if ((pLeft_log_blk) && (basist::bc7u::compare_block_configs(log_blk, *pLeft_log_blk, false)))
+				{
+					command_byte = (uint8_t)xbc7_command_id::cCmdReuseConfigLeft;
+				}
+				else if ((pUp_log_blk) && (basist::bc7u::compare_block_configs(log_blk, *pUp_log_blk, false)))
+				{
+					command_byte = (uint8_t)xbc7_command_id::cCmdReuseConfigUpper;
+				}
+				else if ((pLeft_diag_log_blk) && (basist::bc7u::compare_block_configs(log_blk, *pLeft_diag_log_blk, false)))
+				{
+					command_byte = (uint8_t)xbc7_command_id::cCmdReuseConfigLeftDiagonal;
+				}
+				else if ((pRight_diag_log_blk) && (basist::bc7u::compare_block_configs(log_blk, *pRight_diag_log_blk, false)))
+				{
+					command_byte = (uint8_t)xbc7_command_id::cCmdReuseConfigRightDiagonal;
+				}
+				else
+				{
+					command_byte = (uint8_t)xbc7_command_id::cCmdNewConfig;
+
+					uint32_t config_byte = log_blk.m_mode | (log_blk.m_dp_rotation_index << 3) | (log_blk.m_mode4_index_selector << 5);
+
+					blob_writer.put_byte(cBlobBC7BlockConfig, (uint8_t)config_byte);
+				}
+								
+				stats.m_cmd_hist[command_byte & 7]++;
+				stats.m_mode_hist[log_blk.m_mode]++;
+
+
+				// Code partition pattern index
+				if (log_blk.m_num_partitions == 2)
+					blob_writer.put_byte(cBlobPartition2, log_blk.m_pattern_index);
+				else if (log_blk.m_num_partitions == 3)
+					blob_writer.put_byte(cBlobPartition3, log_blk.m_pattern_index);
+
+				// Endpoint coding: sweep every causal DPCM predictor -- the four
+				// dedicated neighbor modes, then the 32 XY-delta block references --
+				// and keep the one minimizing the SSE of the residual bytes that
+				// will actually be emitted (int8 reading, post G-decorrelation,
+				// alpha bytes that the opaque mode 6 rule suppresses excluded).
+				// Dedicated modes are evaluated first and replacement is strict
+				// less-than, so an XY delta aliasing a dedicated neighbor can never
+				// win a tie and waste an index byte.
+				const basist::bc7u::log_bc7_block* pEP_pred_blk = nullptr;
+				uint32_t ep_pred_mode = (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointRaw;
+				uint32_t ep_pred_delta_index = 0;
+				uint32_t ep_pred_subset = 0;
+
+				{
+					uint64_t best_ep_cost = UINT64_MAX;
+
+					auto eval_ep_predictor = [&](const basist::bc7u::log_bc7_block& pred_blk, uint32_t pred_subset) -> uint64_t
+						{
+							uint64_t cost = 0;
+
+							for (uint32_t s = 0; s < log_blk.m_num_partitions; s++)
+							{
+								uint8_t residuals[8];
+								uint32_t num_residuals;
+								uint8_t residual_pbits[2];
+								uint32_t num_residual_pbits;
+
+								basist::bc7u::endpoint_dpcm(false,
+									pred_blk, pred_subset, // mirrors the emission below
+									log_blk, s,
+									residuals, num_residuals, residual_pbits, num_residual_pbits);
+
+								uint32_t num_costed = num_residuals;
+								if ((log_blk.m_mode == 6) && (!has_alpha))
+									num_costed = 6; // alpha residuals are never sent
+
+								for (uint32_t i = 0; i < num_costed; i++)
+								{
+									const int v = (int8_t)residuals[i];
+									cost += (uint64_t)((int64_t)v * v);
+								}
+
+								// NOTE: residual_pbits are deliberately NOT costed.
+								// Penalizing pbit mismatches was tried and measured
+								// a wash at best (penalty 1: ~0%, penalty 2: +0.4%
+								// WORSE): the delta stream is raw 1 bit/pbit either
+								// way, so the only possible win is making its packed
+								// bytes Zstd-compressible -- but at 8 deltas/byte
+								// even a 30% nonzero rate leaves ~7 bits/byte of
+								// entropy, and the residual bytes traded away to
+								// get there cost more.
+							}
+
+							return cost;
+						};
+
+					const basist::bc7u::log_bc7_block* neighbor_preds[4] = { pLeft_log_blk, pUp_log_blk, pLeft_diag_log_blk, pRight_diag_log_blk };
+					const xbc7_command_endpoint_mode neighbor_modes[4] =
+					{
+						xbc7_command_endpoint_mode::cCmdEndpointDPCMLeft, xbc7_command_endpoint_mode::cCmdEndpointDPCMUp,
+						xbc7_command_endpoint_mode::cCmdEndpointDPCMLeftDiagonal, xbc7_command_endpoint_mode::cCmdEndpointDPCMRightDiagonal
+					};
+
+					for (uint32_t i = 0; i < 4; i++)
+					{
+						if (!neighbor_preds[i])
+							continue;
+
+						const uint64_t cost = eval_ep_predictor(*neighbor_preds[i], 0);
+						if ((!pEP_pred_blk) || ((cost * 1000) < (best_ep_cost * (1000 - (uint64_t)XBC7_SWEEP_SWITCH_MARGIN_PERMILLE))))
+						{
+							best_ep_cost = cost;
+							pEP_pred_blk = neighbor_preds[i];
+							ep_pred_mode = (uint32_t)neighbor_modes[i];
+							ep_pred_subset = 0;
+						}
+					}
+
+					// Subset-1 variants of left/up (EP modes 6/7), legal only when
+					// the neighbor is partitioned; swept AFTER the subset-0 modes
+					// so the switch margin keeps them losing ties. NOTE: a much
+					// stricter dedicated margin was tried and measured WORSE --
+					// these modes' SSE wins are nearly always decisive, so the
+					// filter barely fires; the (small) net cost on rare content
+					// with hyper-skewed command streams is an SSE-vs-bytes
+					// mismatch that only rate-based EP selection would fix.
+					const basist::bc7u::log_bc7_block* s1_preds[2] = { pLeft_log_blk, pUp_log_blk };
+					const xbc7_command_endpoint_mode s1_modes[2] =
+					{
+						xbc7_command_endpoint_mode::cCmdEndpointDPCMLeftSubset1, xbc7_command_endpoint_mode::cCmdEndpointDPCMUpSubset1
+					};
+
+					for (uint32_t i = 0; i < 2; i++)
+					{
+						if ((!s1_preds[i]) || (s1_preds[i]->m_num_partitions < 2))
+							continue;
+
+						const uint64_t cost = eval_ep_predictor(*s1_preds[i], 1);
+						if ((!pEP_pred_blk) || ((cost * 1000) < (best_ep_cost * (1000 - (uint64_t)XBC7_SWEEP_SWITCH_MARGIN_PERMILLE))))
+						{
+							best_ep_cost = cost;
+							pEP_pred_blk = s1_preds[i];
+							ep_pred_mode = (uint32_t)s1_modes[i];
+							ep_pred_subset = 1;
+						}
+					}
+
+					for (uint32_t i = 0; i < NUM_XY_DELTAS; i++)
+					{
+						const xbc7_xy_delta& delta = g_xbc7_xy_deltas[i];
+						const int nx = (int)bx + delta.m_dx;
+						const int ny = (int)by + delta.m_dy;
+
+						if (!tile.contains(nx, ny))
+							continue;
+
+						const basist::bc7u::log_bc7_block& pred_blk = log_blks(nx, ny);
+
+						const uint64_t cost = eval_ep_predictor(pred_blk, 0);
+						if ((!pEP_pred_blk) || ((cost * 1000) < (best_ep_cost * (1000 - (uint64_t)XBC7_SWEEP_SWITCH_MARGIN_PERMILLE))))
+						{
+							best_ep_cost = cost;
+							pEP_pred_blk = &pred_blk;
+							ep_pred_mode = (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMBlockIndex;
+							ep_pred_delta_index = i;
+							ep_pred_subset = 0;
+						}
+					}
+				}
+
+				if (pEP_pred_blk)
+				{
+					command_byte |= (uint8_t)(ep_pred_mode << XBC7_COMMAND_ENDPOINT_MODE_SHIFT);
+					stats.m_ep_dpcm_blocks++;
+					stats.m_ep_mode_hist[ep_pred_mode]++;
+
+					if (ep_pred_mode == (uint32_t)xbc7_command_endpoint_mode::cCmdEndpointDPCMBlockIndex)
+					{
+						blob_writer.put_byte(cBlobEPBlockIndex, (uint8_t)ep_pred_delta_index);
+						stats.m_ep_index_hist[ep_pred_delta_index]++;
+					}
+
+
+					for (uint32_t s = 0; s < log_blk.m_num_partitions; s++)
+					{
+						uint8_t residuals[8];
+						uint32_t num_residuals;
+						uint8_t residual_pbits[2];
+						uint32_t num_residual_pbits;
+
+						basist::bc7u::endpoint_dpcm(false, 
+							*pEP_pred_blk, ep_pred_subset, // subset 1 for EP modes 6/7
+							log_blk, s,
+							residuals, num_residuals, residual_pbits, num_residual_pbits);
+
+						{
+							basist::bc7u::log_bc7_block temp_log_blk(log_blk);
+							memset(temp_log_blk.m_endpoints, 0, sizeof(temp_log_blk.m_endpoints));
+
+							basist::bc7u::endpoint_dpcm(true,
+								*pEP_pred_blk, ep_pred_subset, // subset 1 for EP modes 6/7
+								temp_log_blk, s,
+								residuals, num_residuals, residual_pbits, num_residual_pbits);
+
+#if defined(DEBUG) || defined(_DEBUG)
+							for (uint32_t e = 0; e < 2; e++)
+							{
+								for (uint32_t c = 0; c < 4; c++)
+								{
+									if (log_blk.m_endpoints[s][e][c] != temp_log_blk.m_endpoints[s][e][c])
+									{
+										fmt_error_printf("DPCM endpoint decomp failed");
+										assert(0);
+										return false;
+									}
+								}
+							}
+#endif
+						}
+
+						assert((num_residuals % 2) == 0);
+
+						if ((log_blk.m_mode == 6) && (!has_alpha))
+						{
+							assert(num_residuals == 8);
+
+							if ((log_blk.m_endpoints[0][0][3] != 127) || (log_blk.m_endpoints[0][1][3] != 127))
+							{
+								assert(0);
+								return false;
+							}
+
+#if 0
+							if ((residuals[6] != 0) || (residuals[7] != 0))
+							{
+								assert(0);
+								return false;
+							}
+#endif
+
+							num_residuals = 6;
+						}
+
+						stats.m_ep_dpcm_subsets++;
+						stats.record_ep_residuals(log_blk.m_endpoint_bits[0] >= 6, residuals, num_residuals);
+
+						if (log_blk.m_endpoint_bits[0] >= 6)
+						{
+							for (uint32_t i = 0; i < num_residuals; i += 2)
+							{
+								const uint32_t chan = i >> 1;
+								blob_writer.put_byte(cBlobEPDeltaFineR + chan, residuals[i + 0]);
+								blob_writer.put_byte(cBlobEPDeltaFineR + chan, residuals[i + 1]);
+							}
+						}
+						else
+						{
+							for (uint32_t i = 0; i < num_residuals; i += 2)
+							{
+								const uint32_t chan = i >> 1;
+								blob_writer.put_byte(cBlobEPDeltaCoarseR + chan, residuals[i + 0]);
+								blob_writer.put_byte(cBlobEPDeltaCoarseR + chan, residuals[i + 1]);
+							}
+						}
+
+						for (uint32_t p = 0; p < num_residual_pbits; p++)
+						{
+							pbits.put_bits(residual_pbits[p], 1);
+
+							stats.m_pbit_delta_bits++;
+							stats.m_pbit_delta_nonzero += residual_pbits[p];
+						}
+					}
+				}
+				else
+				{
+					command_byte |= ((uint8_t)xbc7::xbc7_command_endpoint_mode::cCmdEndpointRaw << XBC7_COMMAND_ENDPOINT_MODE_SHIFT);
+					stats.m_ep_raw_blocks++;
+					stats.m_ep_mode_hist[(uint32_t)xbc7_command_endpoint_mode::cCmdEndpointRaw]++;
+
+					for (uint32_t s = 0; s < log_blk.m_num_partitions; s++)
+					{
+						for (uint32_t c = 0; c < log_blk.get_num_comps(); c++)
+						{
+							for (uint32_t e = 0; e < 2; e++)
+							{
+								raw_endpoint_bits.put_bits(log_blk.m_endpoints[s][e][c], log_blk.m_endpoint_bits[c == 3]);
+							}
+						}
+					}
+
+					for (uint32_t p = 0; p < log_blk.m_num_pbits; p++)
+						raw_endpoint_bits.put_bits(log_blk.m_pbits[p], 1);
+				}
+
+				// Weights: residual DCT vs lossless residual DPCM, chosen per
+				// block by estimated emitted size (see decision below). Q >= 100
+				// is the lossless mode: weights are ALWAYS residual DPCM -- the
+				// whole file is then exact relative to the input BC7 logical
+				// blocks -- and the expensive DCT candidate search is skipped.
+				//
+				// We ALSO force lossless DPCM (and skip the DCT search) for any block
+				// the bc7e_scalar base pack flagged as using its one-color endpoint
+				// LUTs: those extreme endpoints only "land" with exact weights, so the
+				// lossy weight DCT would decode them to "trap" artifacts. (LUTs are
+				// already disabled at Q < 100; this also covers the always-LUT solid
+				// path. At Q == 100 DPCM is forced anyway, so LUTs are harmless there.)
+				const bool force_dpcm = (opts.m_dct_q >= 100) || (base_used_lut(bx, by) != 0);
+
+				dct_syms best_syms[2];
+				// value-init silences a C4701 false positive: best_cand_log_blk is only
+				// read on the DCT path (!force_dpcm), where pack_weights() assigns it
+				// first; the DPCM path 'continue's before the read.
+				basist::bc7u::log_bc7_block best_cand_log_blk{};
+				uint32_t best_predictor_index = 0;
+
+				uint64_t dct_est_obits = 0;
+				uint32_t block_total_acs = 0;
+
+				if (!force_dpcm)
+				{
+					bool pack_status = pack_weights(bx, by, num_blocks_x,
+						tile,
+						orig_block,
+						log_blks,
+						best_syms, best_cand_log_blk, best_predictor_index,
+						weight_grid_dct_fixed, dct_work_fixed, opts, enabled_pred_mask, num_amp_codes);
+
+					if (!pack_status)
+						return false;
+
+					// Estimated DCT emission cost, mirroring the writes below:
+					// each stream byte at its measured post-Zstd cost, plus the
+					// exact (incompressible) sign bits.
+					for (uint32_t p = 0; p < log_blk.m_num_planes; p++)
+					{
+						uint32_t num_acs = 0, num_eobs = 0;
+						for (uint32_t c = 0; c < best_syms[p].m_ac_vals.size(); c++)
+						{
+							if (best_syms[p].m_ac_vals[c].m_coeff == INT16_MAX)
+								num_eobs++;
+							else
+								num_acs++;
+						}
+						dct_est_obits += DCT_DC_BYTE_COST_OBITS + (2 * (uint64_t)DCT_AC_BYTE_COST_OBITS) * num_acs + (uint64_t)DCT_AC_BYTE_COST_OBITS * num_eobs +
+							8 * ((uint64_t)num_acs + ((best_predictor_index != cCandAbsolute) ? 1 : 0));
+						block_total_acs += num_acs;
+					}
+
+					stats.m_wt_dct_est_bits_total += dct_est_obits;
+				}
+
+				dpcm_weights dpcm;
+				pack_weights_dpcm(bx, by, num_blocks_x, tile, log_blks, dpcm, enabled_pred_mask, num_amp_codes);
+
+				stats.m_wt_dpcm_est_bits_total += dpcm.m_est_cost_obits;
+
+				// Both estimates are in measured post-Zstd octobits, so 100 is
+				// the neutral operating point. Ties go to DPCM: it's lossless,
+				// so a tie is a free quality win.
+				bool use_dpcm = force_dpcm || ((dpcm.m_est_cost_obits * 100) <= (dct_est_obits * opts.m_wt_dpcm_alpha_pct));
+
+				if (use_dpcm)
+				{
+					// the weight-mode command bit is cCmdWeightRaw == 0: nothing to OR in
+					stats.m_wt_dpcm_blocks++;
+					stats.m_wt_block_acs_hist_dpcm[basisu::minimum<uint32_t>(block_total_acs, 32)]++;
+					stats.m_wt_chosen_est_bits += dpcm.m_est_cost_obits;
+					stats.m_wt_dpcm_pred_hist[dpcm.m_pred_index]++;
+
+					blob_writer.put_byte(cBlobWeightPredictors, (uint8_t)dpcm.m_pred_index);
+
+					const bool is_absolute = (dpcm.m_pred_index == (uint32_t)cCandAbsolute);
+
+					for (uint32_t p = 0; p < log_blk.m_num_planes; p++)
+					{
+						const uint32_t num_bits = log_blk.m_weight_bits[p];
+						const uint32_t blob_id = is_absolute ? (uint32_t)cBlobRawWeightBits :
+							((num_bits == 2) ? (uint32_t)cBlobDPCMWeightResid2 :
+							(num_bits == 3) ? (uint32_t)cBlobDPCMWeightResid3 : (uint32_t)cBlobDPCMWeightResid4);
+
+						if (!is_absolute)
+						{
+							const uint32_t cls = num_bits - 2;
+							const int half = 1 << (num_bits - 1);
+							for (uint32_t i = 0; i < 16; i++)
+							{
+								const int sym = dpcm.m_syms[p][i];
+								const int v = (sym >= half) ? (sym - (1 << num_bits)) : sym;
+								stats.m_wt_resid_count[cls]++;
+								stats.m_wt_resid_sum_abs[cls] += (uint64_t)iabs(v);
+								if (!v)
+									stats.m_wt_resid_zero[cls]++;
+								stats.m_wt_resid_mag_hist[cls][basisu::minimum<int>(iabs(v), 8)]++;
+							}
+						}
+
+						if (num_bits == 2)
+						{
+							// 4 per byte, LSB first
+							for (uint32_t i = 0; i < 16; i += 4)
+							{
+								blob_writer.put_byte(blob_id, (uint8_t)(dpcm.m_syms[p][i] |
+									(dpcm.m_syms[p][i + 1] << 2) | (dpcm.m_syms[p][i + 2] << 4) | (dpcm.m_syms[p][i + 3] << 6)));
+							}
+						}
+						else
+						{
+							// 3-bit expanded to nibbles / 4-bit: 2 per byte, LSB first
+							for (uint32_t i = 0; i < 16; i += 2)
+							{
+								blob_writer.put_byte(blob_id, (uint8_t)(dpcm.m_syms[p][i] | (dpcm.m_syms[p][i + 1] << 4)));
+							}
+						}
+					} // p
+
+					// lossless: reconstruction == input, no weight write-back needed
+					if (pCoded_log_blocks)
+					{
+						(*pCoded_log_blocks)(bx, by) = log_blk;
+					}
+
+					if (debug_images)
+						xbc7_vis_fill_command(dbg_imgs, bx, by, command_byte, true, dpcm.m_pred_index, UINT32_MAX); // lossless DPCM weights: no DCT ACs
+
+					blob_writer.put_byte(cBlobCommands, command_byte);
+
+					continue;
+				}
+
+				command_byte |= ((uint8_t)xbc7::xbc7_command_weight_mode::cCmdWeightDCT << XBC7_COMMAND_WEIGHT_MODE_SHIFT);
+
+				// optional AC-truncation RDO: prune high-freq weight-DCT ACs from the
+				// winning symbols (once per block), within the configured PSNR drop
+				if (opts.m_ac_trunc_rdo_max_psnr_drop > 0.0f)
+				{
+					if (!ac_truncate_rdo(best_syms, best_cand_log_blk, best_predictor_index, orig_block,
+						bx, by, num_blocks_x, tile, log_blks, weight_grid_dct_fixed, dct_work_fixed, opts, stats))
+						return false;
+
+					// refresh the AC count for the histogram below (pruning changed it)
+					block_total_acs = 0;
+					for (uint32_t p = 0; p < log_blk.m_num_planes; p++)
+					{
+						uint32_t n = best_syms[p].m_ac_vals.size_u32();
+						if (n && (best_syms[p].m_ac_vals[n - 1].m_coeff == INT16_MAX))
+							n--;
+						block_total_acs += n;
+					}
+				}
+
+				stats.m_wt_dct_blocks++;
+				stats.m_wt_block_acs_hist_dct[basisu::minimum<uint32_t>(block_total_acs, 32)]++;
+				stats.m_wt_chosen_est_bits += dct_est_obits;
+
+				blob_writer.put_byte(cBlobWeightPredictors, (uint8_t)best_predictor_index);
+				stats.m_pred_hist[best_predictor_index]++;
+
+				for (uint32_t p = 0; p < log_blk.m_num_planes; p++)
+				{
+					stats.m_dct_stats.record_plane(best_syms[p]);
+
+					assert(iabs(best_syms[p].m_dc) <= 255);
+
+					// TODO: Always writing full precision DC
+					blob_writer.put_byte(cBlobDCCoeffsSmall, (uint8_t)iabs(best_syms[p].m_dc));
+
+					if (best_predictor_index == cCandAbsolute)
+					{
+						assert(best_syms[p].m_dc >= 0);
+					}
+					else
+					{
+						coeff_signs.put_bits((best_syms[p].m_dc < 0) ? 1 : 0, 1);
+					}
+
+					assert(best_syms[p].m_ac_vals.size());
+
+					for (uint32_t c = 0; c < best_syms[p].m_ac_vals.size(); c++)
+					{
+						uint32_t num_zeros = best_syms[p].m_ac_vals[c].m_num_zeros;
+						
+						int ac_coeff = best_syms[p].m_ac_vals[c].m_coeff;
+												
+						if (ac_coeff == INT16_MAX)
+						{
+							blob_writer.put_byte(cBlobACCoeffs, 0xFF);
+						}
+						else
+						{
+							assert(iabs(ac_coeff) <= 255);
+
+							blob_writer.put_byte(cBlobACCoeffs, (uint8_t)num_zeros);
+							blob_writer.put_byte(cBlobACCoeffs, (uint8_t)iabs(ac_coeff));
+							coeff_signs.put_bits((ac_coeff < 0) ? 1 : 0, 1);
+						}
+					} // c
+				} // p
+
+				log_blk = best_cand_log_blk;
+
+				if (pCoded_log_blocks)
+				{
+					(*pCoded_log_blocks)(bx, by) = best_cand_log_blk;
+				}
+
+				if (debug_images)
+					xbc7_vis_fill_command(dbg_imgs, bx, by, command_byte, true, best_predictor_index, block_total_acs); // DCT weights
+
+				blob_writer.put_byte(cBlobCommands, command_byte);
+
+			} // bx
+
+		} // by
+
+		return true;
+	}
+
+	// Packs a single 4x4 RGBA block into a physical BC7 block using whichever BC7
+	// base encoder is selected in opts: cBC7F is the built-in fast real-time
+	// packer; cBC7E_Scalar is the higher-quality scalar bc7e encoder (whose
+	// per-level params must be prebuilt once into bc7e_params before threading).
+	// Either way it returns a standard physical BC7 block, identical in form to
+	// what bc7f produces, so the rest of the pipeline is unaffected.
+	static inline void xbc7_pack_bc7_base_block(
+		basist::bc7u::phys_bc7_block& phys_blk,
+		const color_rgba* pPixels,
+		const pack_options& opts,
+		const bc7e_scalar::bc7e_compress_block_params& bc7e_params,
+		bool& used_lut)
+	{
+		used_lut = false;
+		if (opts.m_bc7_encoder == bc7_encoder_type::cBC7E_Scalar)
+		{
+			// bc7e writes via a uint64_t*; phys_bc7_block::m_bytes has no alignment
+			// guarantee, so encode into an aligned local and copy the 16 bytes back.
+			// pUsed_lut reports whether the winning encoding used bc7e's one-color
+			// endpoint LUTs on any subset (a "this block may be weird" hint).
+			uint64_t blk64[2];
+			uint8_t lut_flag = 0;
+			bc7e_scalar::bc7e_compress_blocks(1, blk64, (const uint32_t*)pPixels, &bc7e_params, &lut_flag);
+			memcpy(phys_blk.m_bytes, blk64, sizeof(phys_blk.m_bytes));
+			used_lut = (lut_flag != 0);
+		}
+		else
+		{
+			// bc7f does not use the one-color endpoint LUTs.
+			basist::bc7f::fast_pack_bc7_auto_rgba(phys_blk.m_bytes, (basist::color_rgba*)pPixels, opts.m_bc7_pack_flags);
+		}
+	}
+
+	bool pack_image(
+		const image& orig_img,
+		const pack_options& opts,
+		uint8_vec &comp_bytes,
+		vector2D<basist::bc7u::log_bc7_block> &coded_log_blocks)
+	{
+#if !BASISD_SUPPORT_KTX2_ZSTD
+		// XBC7 is entirely zstd-based: the stream is zstd-compressed (serialize) and
+		// decoding/validation requires zstd too. We do not support uncompressed XBC7
+		// streams, so without BASISD_SUPPORT_KTX2_ZSTD there is nothing we can do --
+		// fail the encode cleanly. (The zstd-using body below isn't compiled here, so
+		// the encoder still links: serialize() ends up unreferenced.)
+		BASISU_NOTE_UNUSED(orig_img); BASISU_NOTE_UNUSED(opts);
+		BASISU_NOTE_UNUSED(comp_bytes); BASISU_NOTE_UNUSED(coded_log_blocks);
+		fmt_error_printf("XBC7 pack_image: XBC7 requires zstd (BASISD_SUPPORT_KTX2_ZSTD=0) -- cannot encode\n");
+		return false;
+#else
+		// Internal pointer alias for the caller-provided reference (guaranteed
+		// non-null): pack_stripe() and the threaded lambdas below thread the coded
+		// logical blocks through as a pointer. The encoder always populates these
+		// and then validates the encoded stream decodes back to them exactly.
+		vector2D<basist::bc7u::log_bc7_block>* pCoded_log_blocks = &coded_log_blocks;
+
+		const uint32_t width = orig_img.get_width();
+		const uint32_t height = orig_img.get_height();
+
+		if ((!width) || (width > UINT16_MAX) || (!height) || (height > UINT16_MAX))
+		{
+			assert(0);
+			return false;
+		}
+
+		if ((opts.m_dct_q < 1) || (opts.m_dct_q > 100))
+		{
+			assert(0);
+			return false;
+		}
+
+		if (!opts.m_pJob_pool)
+		{
+			assert(0);
+			return false;
+		}
+
+		const bool use_threading = (opts.m_pJob_pool->get_total_threads() > 1);
+
+		const uint32_t block_width = 4;
+		const uint32_t block_height = 4;
+
+		const uint32_t num_blocks_x = (width + block_width - 1) / block_width;
+		const uint32_t num_blocks_y = (height + block_height - 1) / block_height;
+		const uint32_t total_blocks = num_blocks_x * num_blocks_y;
+
+		const bool has_alpha = orig_img.has_alpha();
+
+		if (opts.m_debug_output)
+			fmt_debug_printf("XBC7 pack: {}x{} texels, alpha: {}, block: {}x{}, blocks: {}x{} = {} total\n",
+				width, height, has_alpha, block_width, block_height, num_blocks_x, num_blocks_y, total_blocks);
+
+		// Dump the full set of pack_options that were passed in.
+		if (opts.m_debug_output)
+		{
+			fmt_debug_printf("---- XBC7 pack_options ----\n");
+			fmt_debug_printf("  dct_q: {}\n", opts.m_dct_q);
+			{
+				const uint64_t dbg_mask = xbc7_build_pred_mask(opts.m_effort_level);
+				uint32_t dbg_num_preds = 0;
+				for (uint32_t c = 0; c < cTotalCandidates; c++)
+					dbg_num_preds += (uint32_t)((dbg_mask >> c) & 1u);
+				fmt_debug_printf("  effort_level: {} (weight predictors: {}/{}, amp codes: {}/4)\n",
+					opts.m_effort_level, dbg_num_preds, (uint32_t)cTotalCandidates, xbc7_amp_codes_for_effort(opts.m_effort_level));
+			}
+			fmt_debug_printf("  weights[RGBA]: {} {} {} {}\n", opts.m_weights[0], opts.m_weights[1], opts.m_weights[2], opts.m_weights[3]);
+			fmt_debug_printf("  optimize_weights_after_bc7f: {}\n", opts.m_optimize_weights_after_bc7f);
+			fmt_debug_printf("  bc7_encoder: {} (bc7e_scalar level: {}, perceptual: {})\n",
+				(opts.m_bc7_encoder == bc7_encoder_type::cBC7E_Scalar) ? "bc7e_scalar" : "bc7f", opts.m_bc7e_scalar_level, opts.m_perceptual);
+			fmt_debug_printf("  self_validate: {}\n", opts.m_self_validate);
+			fmt_debug_printf("  bc7_pack_flags: 0x{X}\n", opts.m_bc7_pack_flags);
+			fmt_debug_printf("  bc7_alt_pack_enabled: {}, bc7_pack_flags_alt: 0x{X}, bc7_alt_max_psnr_drop: {.3}\n",
+				opts.m_bc7_alt_pack_enabled, opts.m_bc7_pack_flags_alt, opts.m_bc7_alt_max_psnr_drop);
+			fmt_debug_printf("  repeat_rdo_enabled: {}, repeat_rdo_max_psnr_drop: {.3}\n", opts.m_repeat_rdo_enabled, opts.m_repeat_rdo_max_psnr_drop);
+			fmt_debug_printf("  solid_rdo_enabled: {}, solid_rdo_max_psnr_drop: {.3}\n", opts.m_solid_rdo_enabled, opts.m_solid_rdo_max_psnr_drop);
+			fmt_debug_printf("  endpoint_rdo_enabled: {}, endpoint_rdo_max_psnr_drop: {.3}\n", opts.m_endpoint_rdo_enabled, opts.m_endpoint_rdo_max_psnr_drop);
+			fmt_debug_printf("  ac_trunc_rdo_max_psnr_drop: {.3}\n", opts.m_ac_trunc_rdo_max_psnr_drop);
+			fmt_debug_printf("  rdo_min_block_psnr: {.3}\n", opts.m_rdo_min_block_psnr);
+			fmt_debug_printf("  wt_dpcm_alpha_pct: {}\n", opts.m_wt_dpcm_alpha_pct);
+			fmt_debug_printf("  num_stripes (0=auto): {}\n", opts.m_num_stripes);
+			fmt_debug_printf("  job_pool total threads: {}\n", opts.m_pJob_pool ? (uint32_t)opts.m_pJob_pool->get_total_threads() : 0);
+			fmt_debug_printf("  print_stats: {}, debug_images: {}, debug_file_prefix: '{}'\n",
+				opts.m_print_stats, opts.m_debug_images, opts.m_debug_file_prefix.c_str());
+		}
+
+		vector2D<basist::bc7u::log_bc7_block> log_blks(num_blocks_x, num_blocks_y);
+
+		// Per-block flag: 1 if the bc7e_scalar base pack used its one-color endpoint
+		// LUTs on the block (pack_stripe forces lossless DPCM for these). Function
+		// scope so it survives from the base pack into the stripe coding pass.
+		vector2D<uint8_t> base_used_lut(num_blocks_x, num_blocks_y);
+
+		if (opts.m_debug_output)
+			fmt_debug_printf("XBC7 progress: BC7 base pack ({} blocks{})...\n", total_blocks, opts.m_bc7_alt_pack_enabled ? ", alt-pack RDO" : "");
+				
+		// BC7 base pack + canonicalize. Every block is independent and writes
+		// only its own pre-sized log_blks slot, so this pass parallelizes with
+		// simple atomic row stealing; all per-block state lives on each job's
+		// stack. The packer/unpack/canonicalize helpers are pure functions of
+		// their inputs (shared state is const tables initialized at startup).
+		{
+			// Prebuild the bc7e_scalar params once (shared, read-only across the
+			// pack jobs); only used when that encoder is selected. The level [0,6]
+			// picks one of its speed/quality presets (0=ultrafast .. 6=slowest).
+			// A one-time global table init is required before the first encode.
+			bc7e_scalar::bc7e_compress_block_params bc7e_params;
+			memset(&bc7e_params, 0, sizeof(bc7e_params));
+			if (opts.m_bc7_encoder == bc7_encoder_type::cBC7E_Scalar)
+			{
+				bc7e_scalar::bc7e_compress_block_init();
+
+				typedef void (*bc7e_params_init_func)(bc7e_scalar::bc7e_compress_block_params*, bool);
+				static const bc7e_params_init_func s_bc7e_level_init[BC7E_SCALAR_MAX_LEVEL + 1] =
+				{
+					&bc7e_scalar::bc7e_compress_block_params_init_ultrafast, // 0
+					&bc7e_scalar::bc7e_compress_block_params_init_veryfast,  // 1
+					&bc7e_scalar::bc7e_compress_block_params_init_fast,      // 2
+					&bc7e_scalar::bc7e_compress_block_params_init_basic,     // 3
+					&bc7e_scalar::bc7e_compress_block_params_init_slow,      // 4
+					&bc7e_scalar::bc7e_compress_block_params_init_veryslow,  // 5
+					&bc7e_scalar::bc7e_compress_block_params_init_slowest,   // 6
+				};
+
+				const uint32_t lvl = clamp<uint32_t>(opts.m_bc7e_scalar_level, BC7E_SCALAR_MIN_LEVEL, BC7E_SCALAR_MAX_LEVEL);
+
+				// For perceptual (sRGB) sources, run bc7e in its built-in perceptual
+				// error mode and leave its channel weights at the preset defaults. For
+				// linear sources, run bc7e in linear mode and honor the explicit XBC7
+				// channel weights. (bc7f supports neither, so this only affects bc7e.)
+				s_bc7e_level_init[lvl](&bc7e_params, opts.m_perceptual);
+
+				if (!opts.m_perceptual)
+				{
+					for (uint32_t i = 0; i < 4; i++)
+						bc7e_params.m_weights[i] = opts.m_weights[i];
+				}
+
+				// bc7e's aggressive one-color endpoint LUTs place endpoints at extreme
+				// positions that only "land" with exact weights. Only allow them at
+				// lossless Q (m_dct_q == 100, weights coded losslessly); at lower Q the
+				// lossy weight DCT can't reproduce them and the block decodes to a
+				// "trap" artifact, so disable the LUTs there.
+				bc7e_params.m_use_luts = (opts.m_dct_q >= 100);
+			}
+
+			image raw_bc7_debug_image;
+			if (opts.m_debug_images || (opts.m_debug_output && opts.m_print_stats))
+			{
+				raw_bc7_debug_image.resize(orig_img.get_width(), orig_img.get_height());
+			}
+
+			std::atomic<uint32_t> cur_row;
+			cur_row.store(0);
+
+			std::atomic<bool> pack_failed_flag;
+			pack_failed_flag.store(false);
+
+			std::atomic<uint32_t> alt_changed_blocks; // BC7 alt-pack RDO: blocks where the alternate was kept
+			alt_changed_blocks.store(0);
+
+			std::atomic<uint32_t> lut_block_count; // bc7e_scalar: blocks whose winning encoding used the endpoint LUTs
+			lut_block_count.store(0);
+
+			std::atomic<uint32_t> lut_block_count_nonsolid; // same, but excluding solid blocks (all 16 source pixels equal)
+			lut_block_count_nonsolid.store(0);
+
+			auto pack_rows_func = [num_blocks_x, num_blocks_y, &opts,
+				&cur_row, &pack_failed_flag, &alt_changed_blocks, &lut_block_count, &lut_block_count_nonsolid,
+				&orig_img, &log_blks, &raw_bc7_debug_image, &bc7e_params, &base_used_lut]()
+				{
+					for ( ; ; )
+					{
+						if (pack_failed_flag)
+							return;
+
+						const uint32_t by = cur_row.fetch_add(1);
+						if (by >= num_blocks_y)
+							break;
+
+						for (uint32_t bx = 0; bx < num_blocks_x; bx++)
+						{
+							color_rgba orig_block[16];
+							orig_img.extract_block_clamped(orig_block, bx * 4, by * 4, 4, 4);
+
+							basist::bc7u::phys_bc7_block phys_blk;
+							bool used_lut = false;
+							xbc7_pack_bc7_base_block(phys_blk, orig_block, opts, bc7e_params, used_lut);
+							base_used_lut(bx, by) = used_lut ? (uint8_t)1 : (uint8_t)0;
+							if (used_lut)
+							{
+								lut_block_count.fetch_add(1, std::memory_order_relaxed);
+
+								// A block is "solid" if all 16 source pixels are equal.
+								bool is_solid = true;
+								for (uint32_t i = 1; i < 16; i++)
+								{
+									if ((orig_block[i].r != orig_block[0].r) || (orig_block[i].g != orig_block[0].g) ||
+										(orig_block[i].b != orig_block[0].b) || (orig_block[i].a != orig_block[0].a))
+									{
+										is_solid = false;
+										break;
+									}
+								}
+								if (!is_solid)
+									lut_block_count_nonsolid.fetch_add(1, std::memory_order_relaxed);
+							}
+
+							if (raw_bc7_debug_image.get_width())
+							{
+								basist::color_rgba unpacked_block[16];
+								bool as = basist::bc7u::unpack_bc7(&phys_blk, unpacked_block);
+								assert(as);
+								BASISU_NOTE_UNUSED(as);
+
+								raw_bc7_debug_image.set_block_clipped((color_rgba *)unpacked_block, bx * 4, by * 4, 4, 4);
+							}
+
+							// Optional poor-man's RDO: also pack with the alternate
+							// (typically cheaper) flags and keep IT unless the primary's
+							// RGBA PSNR is at least the threshold dB higher.
+							if (opts.m_bc7_alt_pack_enabled)
+							{
+								basist::bc7u::phys_bc7_block phys_alt;
+								basist::bc7f::fast_pack_bc7_auto_rgba(phys_alt.m_bytes, (basist::color_rgba*)orig_block, opts.m_bc7_pack_flags_alt);
+
+								basist::color_rgba dec_primary[16], dec_alt[16];
+								if ((!basist::bc7u::unpack_bc7(phys_blk.m_bytes, dec_primary)) ||
+									(!basist::bc7u::unpack_bc7(phys_alt.m_bytes, dec_alt)))
+								{
+									assert(0);
+									pack_failed_flag.store(true);
+									return;
+								}
+
+								const float psnr_primary = xbc7_block_wsse_psnr(orig_block, (const color_rgba*)dec_primary, opts.m_weights);
+								const float psnr_alt = xbc7_block_wsse_psnr(orig_block, (const color_rgba*)dec_alt, opts.m_weights);
+
+								if ((psnr_alt >= opts.m_rdo_min_block_psnr) && ((psnr_primary - psnr_alt) < opts.m_bc7_alt_max_psnr_drop))
+								{
+									phys_blk = phys_alt; // above the quality floor and drop within tolerance -- keep the cheaper block
+									alt_changed_blocks.fetch_add(1, std::memory_order_relaxed);
+								}
+							}
+
+							basist::bc7u::log_bc7_block& log_blk = log_blks(bx, by);
+
+							bool unpack_status = basist::bc7u::unpack_bc7(&phys_blk, log_blk);
+							if (!unpack_status)
+							{
+								assert(0);
+								pack_failed_flag.store(true);
+								return;
+							}
+
+#if defined(DEBUG) || defined(_DEBUG)
+							{
+								// sanity check canonicalization code
+								basist::bc7u::log_bc7_block temp_log_blk(log_blk);
+								basist::bc7u::canonicalize_endpoints(temp_log_blk);
+
+								color_rgba ap[16], bp[16];
+								bool as = basist::bc7u::unpack_bc7(log_blk, (basist::color_rgba*)ap);
+								assert(as);
+
+								bool bs = basist::bc7u::unpack_bc7(temp_log_blk, (basist::color_rgba*)bp);
+								assert(bs);
+
+								if (memcmp(ap, bp, sizeof(color_rgba) * 16) != 0)
+								{
+									assert(0);
+									pack_failed_flag.store(true);
+									return;
+								}
+							}
+#endif
+							basist::bc7u::canonicalize_endpoints(log_blk);
+
+							// optional: re-optimize the weights for bc7f's endpoints.
+							// bc7f is a fast real-time packer, so its weights aren't
+							// per-texel optimal -- this exhaustively re-derives them
+							// (slower, but threaded with the base pack). Endpoints/config
+							// unchanged, so it can only hold or improve quality.
+							if (opts.m_optimize_weights_after_bc7f)
+							{
+								basist::bc7u::log_bc7_block opt_blk;
+								if (!optimize_block_weights(log_blk, orig_block, opts.m_weights, opt_blk))
+								{
+									assert(0);
+									pack_failed_flag.store(true);
+									return;
+								}
+								log_blk = opt_blk;
+							}
+
+						} // bx
+					}
+				};
+
+			if ((use_threading) && (num_blocks_y > 1))
+			{
+				const uint32_t num_threads = (uint32_t)opts.m_pJob_pool->get_total_threads();
+
+				// one job per worker; each drains rows via the atomic counter
+				for (uint32_t job_index = 0; job_index < num_threads; job_index++)
+					opts.m_pJob_pool->add_job(pack_rows_func);
+
+				opts.m_pJob_pool->wait_for_all();
+			}
+			else
+			{
+				pack_rows_func();
+			}
+
+			if (pack_failed_flag)
+				return false;
+
+			if (opts.m_debug_output && opts.m_bc7_alt_pack_enabled)
+			{
+				const uint32_t n = alt_changed_blocks.load();
+				fmt_debug_printf("BC7 alt-pack RDO: {} blocks changed ({.2}%)\n", n, total_blocks ? (n * 100.0f / (float)total_blocks) : 0.0f);
+			}
+
+			if (opts.m_debug_output && (opts.m_bc7_encoder == bc7_encoder_type::cBC7E_Scalar))
+			{
+				const uint32_t n = lut_block_count.load();
+				const uint32_t n_nonsolid = lut_block_count_nonsolid.load();
+				fmt_debug_printf("bc7e_scalar: {} of {} blocks used endpoint LUTs ({.2}%); {} excluding solid blocks ({.2}%) [m_use_luts={}]\n",
+					n, total_blocks, total_blocks ? (n * 100.0f / (float)total_blocks) : 0.0f,
+					n_nonsolid, total_blocks ? (n_nonsolid * 100.0f / (float)total_blocks) : 0.0f, bc7e_params.m_use_luts);
+			}
+
+			if (raw_bc7_debug_image.get_width())
+			{
+				if (opts.m_debug_images)
+					save_png(opts.m_debug_file_prefix + "vis_raw_bc7.png", raw_bc7_debug_image);
+
+				if ((opts.m_debug_output) && (opts.m_print_stats))
+				{
+					fmt_debug_printf("\nRaw packed BC7 image stats:\n");
+					print_image_metrics(orig_img, raw_bc7_debug_image);
+				}
+			}
+		}
+
+		// Stripe geometry for the main coding pass. The count is a pure function
+		// of m_num_stripes (or the image dimensions when it's 0), NOT of the pool
+		// size, so the output is reproducible regardless of thread count. Pass
+		// m_num_stripes = 1 for the single-stripe (no seam cost, no seek table)
+		// format. More stripes than pool threads simply queue.
+		basisu::vector<stripe_range> stripes;
+		uint32_t num_stripes;
+
+		if (opts.m_num_stripes)
+		{
+			// caller forced a specific count -- clamp to what the format and the
+			// image can hold (can't exceed the block-row count or the max)
+			num_stripes = basisu::clamp<uint32_t>(opts.m_num_stripes, 1, basisu::minimum(num_blocks_y, XBC7_MAX_ENCODER_STRIPES));
+			compute_stripe_ranges(num_blocks_y, num_stripes, stripes);
+		}
+		else
+		{
+			num_stripes = compute_encoder_stripes(height, num_blocks_y, stripes);
+		}
+
+		// optional block-reuse RDO: replace whole blocks with a Repeat of a neighbor
+		// or a solid color where quality allows -- runs FIRST (cheapest commands)
+		if (opts.m_repeat_rdo_enabled || opts.m_solid_rdo_enabled)
+		{
+			if (opts.m_debug_output)
+				fmt_debug_printf("XBC7 progress: block-reuse RDO pass (repeat/solid)...\n");
+
+			if (!block_reuse_rdo_pass(orig_img, log_blks, num_blocks_x, stripes, opts))
+				return false;
+		}
+
+		// optional endpoint-prediction RDO: rewrite block endpoints toward their
+		// causal neighbors (zero-residual DPCM) + re-optimize weights where quality
+		// allows, BEFORE coding
+		if (opts.m_endpoint_rdo_enabled)
+		{
+			if (opts.m_debug_output)
+				fmt_debug_printf("XBC7 progress: endpoint-DPCM RDO pass...\n");
+
+			if (!endpoint_dpcm_rdo_pass(orig_img, log_blks, num_blocks_x, stripes, opts))
+				return false;
+		}
+
+		if (pCoded_log_blocks)
+		{
+			pCoded_log_blocks->resize(num_blocks_x, num_blocks_y);
+			for (uint32_t y = 0; y < num_blocks_y; y++)
+			{
+				for (uint32_t x = 0; x < num_blocks_x; x++)
+				{
+					(*pCoded_log_blocks)(x, y).clear();
+					(*pCoded_log_blocks)(x, y).m_mode = -1;
+				}
+			}
+		}
+
+		blob_stream_writer blob_writer;
+
+		basisu::bitwise_coder coeff_signs;
+		coeff_signs.reserve(1024);
+
+		basisu::bitwise_coder pbits;
+		pbits.reserve(1024);
+
+		basisu::bitwise_coder raw_endpoint_bits;
+		raw_endpoint_bits.reserve(1024);
+
+		xbc7_pack_stats stats;
+		stats.m_width = width;
+		stats.m_height = height;
+		stats.m_total_blocks = total_blocks;
+		stats.m_dct_q = opts.m_dct_q;
+		stats.m_has_alpha = has_alpha;
+		stats.m_wt_alpha_pct = opts.m_wt_dpcm_alpha_pct;
+		stats.m_stripes = stripes;
+
+		// ---- main coding pass: one job per stripe ----
+		// Each stripe codes into its own stripe_output (disjoint vector
+		// elements, pre-sized here, so the jobs never touch shared mutable
+		// state outside their own log_blks/pCoded rows).
+		basisu::vector<stripe_output> stripe_outputs(num_stripes);
+
+		// optional per-block debug visualizations (one 4x4 pixel block per BC7
+		// block), each with a drawn legend strip below the block grid. Shared
+		// across stripe jobs, but each stripe fills disjoint rows.
+		image vis_command_img, vis_ep_mode_img, vis_wt_mode_img, vis_ac_count_img, vis_predictor_img;
+		xbc7_debug_image_set dbg_imgs;
+		if (opts.m_debug_images)
+		{
+			// same resolution as the source image; the legend is drawn into the
+			// bottom strip afterward (overwriting a few block rows -- all clipped)
+			vis_command_img.resize(width, height);
+			vis_ep_mode_img.resize(width, height);
+			vis_wt_mode_img.resize(width, height);
+			vis_ac_count_img.resize(width, height);
+			vis_predictor_img.resize(width, height);
+			dbg_imgs.m_pAC_count = &vis_ac_count_img;
+			dbg_imgs.m_pPredictor = &vis_predictor_img;
+			dbg_imgs.m_pCommand = &vis_command_img;
+			dbg_imgs.m_pEndpoint_mode = &vis_ep_mode_img;
+			dbg_imgs.m_pWeight_mode = &vis_wt_mode_img;
+		}
+
+		std::atomic<bool> stripe_failed_flag;
+		stripe_failed_flag.store(false);
+
+		if (opts.m_debug_output)
+			fmt_debug_printf("XBC7 progress: main coding pass ({} stripe{}, {})...\n",
+				num_stripes, (num_stripes == 1) ? "" : "s", ((use_threading) && (num_stripes > 1)) ? "threaded" : "serial");
+
+		if ((use_threading) && (num_stripes > 1))
+		{
+			for (uint32_t stripe_index = 0; stripe_index < num_stripes; stripe_index++)
+			{
+				opts.m_pJob_pool->add_job([stripe_index, has_alpha, num_blocks_x,
+					&stripes, &stripe_outputs, &stripe_failed_flag,
+					&orig_img, &opts, &log_blks, pCoded_log_blocks, &base_used_lut, dbg_imgs]
+					{
+						if (!pack_stripe(orig_img, opts, has_alpha, num_blocks_x,
+							stripes[stripe_index], log_blks, pCoded_log_blocks, base_used_lut, stripe_outputs[stripe_index], dbg_imgs))
+						{
+							stripe_failed_flag.store(true);
+						}
+					});
+			}
+
+			opts.m_pJob_pool->wait_for_all();
+		}
+		else
+		{
+			for (uint32_t stripe_index = 0; stripe_index < num_stripes; stripe_index++)
+			{
+				if (!pack_stripe(orig_img, opts, has_alpha, num_blocks_x,
+					stripes[stripe_index], log_blks, pCoded_log_blocks, base_used_lut, stripe_outputs[stripe_index], dbg_imgs))
+				{
+					stripe_failed_flag.store(true);
+					break;
+				}
+			}
+		}
+
+		if (stripe_failed_flag)
+			return false;
+
+		if (opts.m_debug_images)
+		{
+			const xbc7_vis_legend_entry cmd_legend[8] =
+			{
+				{ g_xbc7_command_vis_colors[0], "0 RepeatLast" },
+				{ g_xbc7_command_vis_colors[1], "1 RepeatUpper" },
+				{ g_xbc7_command_vis_colors[2], "2 SolidDPCM" },
+				{ g_xbc7_command_vis_colors[3], "3 NewConfig" },
+				{ g_xbc7_command_vis_colors[4], "4 ReuseConfig Left" },
+				{ g_xbc7_command_vis_colors[5], "5 ReuseConfig Up" },
+				{ g_xbc7_command_vis_colors[6], "6 ReuseConfig LeftDiag" },
+				{ g_xbc7_command_vis_colors[7], "7 ReuseConfig RightDiag" },
+			};
+
+			const xbc7_vis_legend_entry ep_legend[9] =
+			{
+				{ g_xbc7_endpoint_mode_vis_colors[0], "0 Raw" },
+				{ g_xbc7_endpoint_mode_vis_colors[1], "1 DPCM Left" },
+				{ g_xbc7_endpoint_mode_vis_colors[2], "2 DPCM Up" },
+				{ g_xbc7_endpoint_mode_vis_colors[3], "3 DPCM LeftDiag" },
+				{ g_xbc7_endpoint_mode_vis_colors[4], "4 DPCM RightDiag" },
+				{ g_xbc7_endpoint_mode_vis_colors[5], "5 DPCM BlockIndex" },
+				{ g_xbc7_endpoint_mode_vis_colors[6], "6 DPCM Left Subset1" },
+				{ g_xbc7_endpoint_mode_vis_colors[7], "7 DPCM Up Subset1" },
+				{ g_xbc7_vis_na_color,                "n/a (fully predicted block)" },
+			};
+
+			const xbc7_vis_legend_entry wt_legend[3] =
+			{
+				{ g_xbc7_weight_mode_vis_colors[0], "0 Raw/DPCM weights (lossless)" },
+				{ g_xbc7_weight_mode_vis_colors[1], "1 DCT weights (lossy)" },
+				{ g_xbc7_vis_na_color,              "n/a (fully predicted block)" },
+			};
+
+			const xbc7_vis_legend_entry ac_legend[7] =
+			{
+				{ g_xbc7_ac_count_vis_colors[0], "0 ACs (flat / DC only)" },
+				{ g_xbc7_ac_count_vis_colors[1], "1-2 ACs" },
+				{ g_xbc7_ac_count_vis_colors[2], "3-5 ACs" },
+				{ g_xbc7_ac_count_vis_colors[3], "6-10 ACs" },
+				{ g_xbc7_ac_count_vis_colors[4], "11-20 ACs" },
+				{ g_xbc7_ac_count_vis_colors[5], "21+ ACs" },
+				{ g_xbc7_vis_na_color,           "n/a (not DCT-coded)" },
+			};
+
+			const xbc7_vis_legend_entry pred_legend[7] =
+			{
+				{ g_xbc7_predictor_vis_colors[cPredCatAbsolute],   "Absolute (no prediction)" },
+				{ g_xbc7_predictor_vis_colors[cPredCatSynthetic],  "Synthetic edge/gradient" },
+				{ g_xbc7_predictor_vis_colors[cPredCatRefLeft],    "BlockRef Left" },
+				{ g_xbc7_predictor_vis_colors[cPredCatRefUp],      "BlockRef Up" },
+				{ g_xbc7_predictor_vis_colors[cPredCatRefUpLeft],  "BlockRef Up-Left" },
+				{ g_xbc7_predictor_vis_colors[cPredCatRefUpRight], "BlockRef Up-Right" },
+				{ g_xbc7_vis_na_color,                             "n/a (fully predicted block)" },
+			};
+
+			xbc7_vis_draw_legend(vis_command_img, cmd_legend, 8);
+			xbc7_vis_draw_legend(vis_ep_mode_img, ep_legend, 9);
+			xbc7_vis_draw_legend(vis_wt_mode_img, wt_legend, 3);
+			xbc7_vis_draw_legend(vis_ac_count_img, ac_legend, 7);
+			xbc7_vis_draw_legend(vis_predictor_img, pred_legend, 7);
+
+			save_png(opts.m_debug_file_prefix + "vis_xbc7_command.png", vis_command_img);
+			save_png(opts.m_debug_file_prefix + "vis_xbc7_endpoint_mode.png", vis_ep_mode_img);
+			save_png(opts.m_debug_file_prefix + "vis_xbc7_weight_mode.png", vis_wt_mode_img);
+			save_png(opts.m_debug_file_prefix + "vis_xbc7_weight_ac_count.png", vis_ac_count_img);
+			save_png(opts.m_debug_file_prefix + "vis_xbc7_weight_predictor.png", vis_predictor_img);
+		}
+
+		if (opts.m_debug_output)
+			fmt_debug_printf("XBC7 progress: merging stripe streams{}...\n", (num_stripes > 1) ? " + building seek table" : "");
+
+		// Per-stripe seek table: for each stripe, the start offset of its data
+		// in every per-stripe stream id 1..25 -- a BYTE offset for byte blobs,
+		// a BIT offset for the three bit blobs (signs/pbits/ep_raw, which stay
+		// bit-merged with no padding). Recorded BEFORE each stripe is appended.
+		// Built only when there's more than one stripe.
+		const uint32_t SEEK_NUM_STREAMS = (uint32_t)cBlobStripeSeekTable - 1; // ids 1..25
+		basisu::vector<basisu::packed_uint<4>> seek_table; // [stripe * SEEK_NUM_STREAMS + (id-1)], little-endian DELTAS from prev stripe
+		if (num_stripes > 1)
+			seek_table.resize(num_stripes * SEEK_NUM_STREAMS);
+
+		// running start offset of the PREVIOUS stripe in each stream (indexed by
+		// blob id), so each table entry can be stored as a small delta
+		uint64_t prev_seek_ofs[(uint32_t)cBlobStripeSeekTable] = {};
+
+		// ---- merge the per-stripe streams, strictly in stripe (raster) order ----
+		for (uint32_t stripe_index = 0; stripe_index < num_stripes; stripe_index++)
+		{
+			const stripe_output& so = stripe_outputs[stripe_index];
+
+			if (num_stripes > 1)
+			{
+				for (uint32_t id = 1; id < (uint32_t)cBlobStripeSeekTable; id++)
+				{
+					uint64_t ofs;
+					if (id == cBlobCoeffSigns)
+						ofs = coeff_signs.get_total_bits();		// bit offset
+					else if (id == cBlobPBits)
+						ofs = pbits.get_total_bits();			// bit offset
+					else if (id == cBlobEPRaw)
+						ofs = raw_endpoint_bits.get_total_bits();	// bit offset
+					else
+						ofs = blob_writer.get_blob_size(id);	// byte offset
+
+					// store the delta from the previous stripe's start (stripe 0 -> 0).
+					// offsets are monotonic, so the delta is non-negative and small;
+					// packed_uint<4> keeps the low 32 bits, little-endian.
+					seek_table[stripe_index * SEEK_NUM_STREAMS + (id - 1)] = ofs - prev_seek_ofs[id];
+					prev_seek_ofs[id] = ofs;
+				}
+			}
+
+			for (uint32_t id = 0; id < BLOB_STREAM_MAX_IDS; id++)
+			{
+				const uint8_vec* pBlob = so.m_blob_writer.get_blob_data(id);
+				if ((pBlob) && (pBlob->size()))
+					blob_writer.append_bytes(id, *pBlob);
+			}
+
+			// bit streams: append() carries each stripe's unflushed partial bit
+			// buffer, so the merged streams contain NO padding at the seams --
+			// the decoder still reads single continuous LSB-first streams, and
+			// the seek table addresses stripe starts at BIT granularity
+			coeff_signs.append(so.m_coeff_signs);
+			pbits.append(so.m_pbits);
+			raw_endpoint_bits.append(so.m_raw_endpoint_bits);
+
+			stats.merge(so.m_stats);
+		}
+
+		if (raw_endpoint_bits.get_total_bits())
+		{
+			raw_endpoint_bits.flush();
+			blob_writer.get_blob_vec(cBlobEPRaw) = raw_endpoint_bits.get_bytes();
+		}
+
+		if (pbits.get_total_bits())
+		{
+			pbits.flush();
+			blob_writer.get_blob_vec(cBlobPBits) = pbits.get_bytes();
+		}
+
+		if (coeff_signs.get_total_bits())
+		{
+			coeff_signs.flush();
+			blob_writer.get_blob_vec(cBlobCoeffSigns) = coeff_signs.get_bytes();
+		}
+
+		// emit the seek table, byte-plane (SoA) transposed: all entries' byte 0
+		// first, then every byte 1, then byte 2, then byte 3. The deltas are
+		// small, so the upper planes are near-all-zero and Zstd crushes them --
+		// much better than the interleaved little-endian layout. Lossless
+		// reordering; the decoder reverses it before reading the deltas.
+		if (num_stripes > 1)
+		{
+			const uint32_t num_entries = (uint32_t)seek_table.size(); // num_stripes * SEEK_NUM_STREAMS
+			const uint8_t* pSrc = (const uint8_t*)seek_table.data();  // little-endian, 4 bytes/entry
+			uint8_vec planed(num_entries * 4);
+			for (uint32_t plane = 0; plane < 4; plane++)
+				for (uint32_t e = 0; e < num_entries; e++)
+					planed[plane * num_entries + e] = pSrc[e * 4 + plane];
+			blob_writer.put_bytes(cBlobStripeSeekTable, planed.data(), planed.size());
+		}
+
+		xbc7_header hdr;
+		clear_obj(hdr);
+		hdr.m_dct_q = (uint8_t)opts.m_dct_q;
+		
+		hdr.m_flags = 0;
+		if (has_alpha)
+			hdr.m_flags = hdr.m_flags | (uint8_t)XBC7_FLAG_HAS_ALPHA;
+
+		hdr.m_width_in_texels = orig_img.get_width();
+		hdr.m_height_in_texels = orig_img.get_height();
+		hdr.m_num_stripes = (uint8_t)num_stripes;
+
+		blob_writer.put_bytes(cBlobHeader, &hdr, sizeof(hdr));
+
+		for (uint32_t id = 0; id < BLOB_STREAM_MAX_IDS; id++)
+		{
+			stats.m_blob_raw_size[id] = (uint32_t)blob_writer.get_blob_size(id);
+			stats.record_blob_bytes(id, blob_writer.get_blob_data(id));
+		}
+
+		if (opts.m_debug_output)
+			fmt_debug_printf("XBC7 progress: serializing + Zstd compressing blobs...\n");
+
+		if (!blob_writer.serialize(comp_bytes, 19, stats.m_blob_stored_size))
+			return false;
+
+		stats.m_total_file_size = comp_bytes.size_u32();
+
+		if (opts.m_debug_output && (opts.m_ac_trunc_rdo_max_psnr_drop > 0.0f))
+			fmt_debug_printf("AC-trunc RDO: {} coeffs pruned across {} blocks ({.2}% of blocks)\n",
+				stats.m_ac_trunc_pruned, stats.m_ac_trunc_blocks, total_blocks ? (stats.m_ac_trunc_blocks * 100.0f / (float)total_blocks) : 0.0f);
+
+		// m_debug_output is the master gate; m_print_stats selects the dashboard
+		if (opts.m_debug_output && opts.m_print_stats)
+			stats.print();
+
+		if (pCoded_log_blocks)
+		{
+			for (uint32_t by = 0; by < num_blocks_y; by++)
+			{
+				for (uint32_t bx = 0; bx < num_blocks_x; bx++)
+				{
+					if (!basist::bc7u::compare_block_full((*pCoded_log_blocks)(bx, by), log_blks(bx, by)))
+					{
+						assert(0);
+						return false;
+					}
+				}
+			}
+		}
+
+		if (opts.m_debug_output)
+			fmt_debug_printf("XBC7 progress: tiny-mip size check...\n");
+
+		// Tiny-mip fallback: for the smallest levels the blob container's fixed
+		// overhead can exceed a raw packed-BC7 encoding. If a tiny-mip stream --
+		// [marker][num_blocks_x:u8][num_blocks_y:u8] followed by 16 bytes per BC7
+		// block -- is smaller (and the block dims fit a byte), emit that instead.
+		// The decoder distinguishes streams by the leading byte: 0xB7 = blob,
+		// 0xB8 = tiny-mip without alpha, 0xB9 = tiny-mip with alpha (so the
+		// has_alpha bit rides in the marker). The packed blocks are the FINAL
+		// logical blocks (identical to pCoded_log_blocks), so the decode round-
+		// trips exactly (pack<->unpack of a logical block is lossless).
+		if ((num_blocks_x <= 255) && (num_blocks_y <= 255))
+		{
+			const size_t tiny_size = 3 + (size_t)total_blocks * 16;
+			const uint32_t blob_size = comp_bytes.size_u32();
+
+			if (tiny_size < (size_t)blob_size)
+			{
+				comp_bytes.resize(0);
+				comp_bytes.reserve((uint32_t)tiny_size);
+				comp_bytes.push_back(has_alpha ? (uint8_t)0xB9 : (uint8_t)0xB8); // marker carries the alpha bit
+				comp_bytes.push_back((uint8_t)num_blocks_x);
+				comp_bytes.push_back((uint8_t)num_blocks_y);
+
+				for (uint32_t by = 0; by < num_blocks_y; by++)
+				{
+					for (uint32_t bx = 0; bx < num_blocks_x; bx++)
+					{
+						basist::bc7u::phys_bc7_block phys;
+						if (!basist::bc7u::pack_bc7(log_blks(bx, by), &phys))
+						{
+							assert(0);
+							return false;
+						}
+						comp_bytes.append(phys.m_bytes, sizeof(phys.m_bytes));
+
+						// The tiny-mip decoder reconstructs each block by UNPACKING
+						// the stored physical block. BC7 pack<->unpack is pixel-
+						// lossless but can re-canonicalize the LOGICAL fields
+						// (endpoint swap + weight complement -> identical pixels,
+						// different struct). So the coded reference must be the
+						// unpacked form -- exactly what the decoder will yield --
+						// not the original log_blks, or a per-block logical compare
+						// would spuriously fail.
+						if (pCoded_log_blocks)
+						{
+							if (!basist::bc7u::unpack_bc7(&phys, (*pCoded_log_blocks)(bx, by)))
+							{
+								assert(0);
+								return false;
+							}
+						}
+					}
+				}
+
+				assert(comp_bytes.size() == tiny_size);
+
+				if (opts.m_debug_output)
+					fmt_debug_printf("XBC7 tiny-mip selected: {} blocks, {} bytes (blob path was {} bytes)\n",
+						total_blocks, comp_bytes.size_u32(), blob_size);
+			}
+		}
+
+		if (opts.m_debug_output)
+			fmt_debug_printf("XBC7 output: {} bytes, {} bits/pixel\n",
+				comp_bytes.size_u32(), ((float)comp_bytes.size() * 8.0f) / (float)orig_img.get_total_pixels());
+
+		// Self-validation (opts.m_self_validate, ON by default): decode the stream we
+		// just produced through the transcoder and verify EVERY logical BC7 block
+		// round-trips exactly to the coded reference (coded_log_blocks). This
+		// guarantees that every emitted XBC7 stream is valid and unpacks correctly
+		// during encoding. It exercises whichever stream format was written above --
+		// the blob format or the tiny-mip format (coded_log_blocks already holds the
+		// form the decoder yields in each case, including the tiny-mip's
+		// re-canonicalized blocks).
+		if (opts.m_self_validate)
+		{
+			if (opts.m_debug_output)
+				fmt_debug_printf("XBC7 progress: decode self-validation...\n");
+
+			struct verify_ctx
+			{
+				const vector2D<basist::bc7u::log_bc7_block>* m_pRef;
+				uint32_t m_expected_nbx, m_expected_nby;
+				bool m_dims_ok;
+				bool m_blocks_ok;
+			} vctx;
+			vctx.m_pRef = &coded_log_blocks;
+			vctx.m_expected_nbx = num_blocks_x;
+			vctx.m_expected_nby = num_blocks_y;
+			vctx.m_dims_ok = true;
+			vctx.m_blocks_ok = true;
+
+			auto verify_init_cb = [](uint32_t nbx, uint32_t nby, uint32_t, uint32_t, uint32_t, bool, void* pData) -> bool
+			{
+				verify_ctx& c = *(verify_ctx*)pData;
+				if ((nbx != c.m_expected_nbx) || (nby != c.m_expected_nby))
+				{
+					c.m_dims_ok = false;
+					return false; // abort decode: dimensions disagree with what we encoded
+				}
+				return true;
+			};
+
+			auto verify_block_cb = [](uint32_t bx, uint32_t by, const basist::bc7u::log_bc7_block& decoded_blk, void* pData) -> bool
+			{
+				verify_ctx& c = *(verify_ctx*)pData;
+				if (!basist::bc7u::compare_block_full((*c.m_pRef)(bx, by), decoded_blk))
+				{
+					c.m_blocks_ok = false;
+					return false; // abort decode: a block did not round-trip
+				}
+				return true;
+			};
+
+			const bool decode_ok = basist::xbc7::unpack_image(
+				basist::xbc7::byte_span(comp_bytes),
+				verify_init_cb, &vctx,
+				verify_block_cb, &vctx);
+
+			if ((!decode_ok) || (!vctx.m_dims_ok) || (!vctx.m_blocks_ok))
+			{
+				assert(0 && "XBC7 pack_image: encoded stream failed decode self-validation");
+				fmt_error_printf("XBC7 pack_image: SELF-VALIDATION FAILED -- the encoded stream did not decode back to the expected BC7 logical blocks (decode_ok={}, dims_ok={}, blocks_ok={})\n",
+					decode_ok, vctx.m_dims_ok, vctx.m_blocks_ok);
+				return false;
+			}
+
+			if (opts.m_debug_output)
+				fmt_debug_printf("XBC7 self-validation: decode round-trip OK ({} blocks)\n", total_blocks);
+		}
+
+		return true;
+#endif // BASISD_SUPPORT_KTX2_ZSTD
+	}
+
+
+
+} // namespace xbc7
+
+} // namespace basisu
diff --git a/encoder/basisu_xbc7_encode.h b/encoder/basisu_xbc7_encode.h
new file mode 100644
index 0000000..5db7575
--- /dev/null
+++ b/encoder/basisu_xbc7_encode.h
@@ -0,0 +1,200 @@
+// File: basisu_xbc7_encode.h
+// XBC7: experimental supercompressed-BC7 codec (prototype moved out of basisu_tool.cpp).
+#pragma once
+#include "basisu_enc.h"
+#include "../transcoder/basisu_transcoder_internal.h"
+
+namespace basisu {
+namespace xbc7 {
+
+	const uint32_t DEFAULT_EFFORT_LEVEL = 3;
+
+	// Default quality "level" for the optional bc7e_scalar BC7 base encoder.
+	const uint32_t DEFAULT_BC7E_SCALAR_LEVEL = 2;
+
+	// Supported bc7e_scalar quality level range (higher = slower/better). Maps to the
+	// encoder's ultrafast..slowest presets where it's invoked; clamp to this range.
+	const uint32_t BC7E_SCALAR_MIN_LEVEL = 0;
+	const uint32_t BC7E_SCALAR_MAX_LEVEL = 6;
+
+	// Which BC7 base encoder XBC7 uses to pack the initial BC7 blocks before
+	// supercompression. cBC7F is the built-in fast real-time packer (the default);
+	// cBC7E_Scalar is the higher-quality (slower) scalar bc7e encoder.
+	enum class bc7_encoder_type
+	{
+		cBC7F = 0,
+		cBC7E_Scalar = 1
+	};
+
+	struct pack_options
+	{
+		uint32_t m_dct_q = 100; // [1,100]; 100 == lossless mode (weights always residual DPCM)
+		uint32_t m_weights[4] = { 1, 1, 1, 1 };
+
+		// Encoder effort/speed knob [0,10]: 0 == fastest, 10 == slowest/best
+		// (9 is the usual practical max; 10 exists for very high core counts).
+		// Currently controls how many weight-grid predictors the per-block DCT and
+		// DPCM sweeps evaluate: at 10 the full candidate set is searched (identical
+		// output to before this knob existed); as effort drops the sweep is pruned
+		// down to a small core of empirically high-value predictors (~6 at effort
+		// 0), trading a little ratio for a large drop in encode time -- the DCT
+		// predictor search (Q < 100) is the dominant cost. Default
+		// DEFAULT_EFFORT_LEVEL (a balanced speed/quality point, NOT the full
+		// search); pass 10 to reproduce the pre-knob output exactly.
+		uint32_t m_effort_level = DEFAULT_EFFORT_LEVEL;
+
+		// Optimize weights after encoding with bc7f. bc7f is a fast real-time BC7
+		// packer whose per-texel weights aren't optimal; when true, every block's
+		// weights are exhaustively re-derived for its (unchanged) config+endpoints
+		// right after the base pack. Slower (but threaded with the base pack);
+		// endpoints/config are untouched, so quality only holds or improves.
+		// Default off.
+		bool m_optimize_weights_after_bc7f = false;
+
+		// Flags for the underlying real-time BC7 base packer
+		// (basist::bc7f::fast_pack_bc7_auto_rgba). See bc7f::cPackBC7Flag* /
+		// cPackBC7FlagDefault* in basisu_transcoder_internal.h -- controls the
+		// quality/speed of the BC7 blocks XBC7 then supercompresses.
+		uint32_t m_bc7_pack_flags = basist::bc7f::cPackBC7FlagDefaultPartiallyAnalytical;
+
+		// Selects which BC7 base encoder packs the initial blocks. Default cBC7F
+		// (the existing fast built-in packer); cBC7E_Scalar selects the higher-quality
+		// scalar bc7e encoder at m_bc7e_scalar_level. Affects only the primary base
+		// pack -- the optional alt-pack RDO path still uses bc7f for now.
+		bc7_encoder_type m_bc7_encoder = bc7_encoder_type::cBC7F;
+
+		// Quality level for the bc7e_scalar encoder (only used when m_bc7_encoder ==
+		// cBC7E_Scalar). Higher = slower/better. Default DEFAULT_BC7E_SCALAR_LEVEL (2).
+		uint32_t m_bc7e_scalar_level = DEFAULT_BC7E_SCALAR_LEVEL;
+
+		// True if the source is perceptual (sRGB) data. Currently only consulted by
+		// the bc7e_scalar base encoder: when true it runs in its built-in perceptual
+		// error mode (and ignores m_weights); when false it runs linear and honors
+		// m_weights. Mirror basis_compressor_params::m_perceptual here.
+		bool m_perceptual = false;
+
+		// When true (the default), the encoder decodes the stream it just produced
+		// (via the transcoder) and verifies every logical BC7 block round-trips
+		// exactly to the coded blocks; a failure fails the encode. Guarantees every
+		// emitted stream is valid/decodable. Disable only to skip the extra decode.
+		bool m_self_validate = true;
+
+		// Optional "poor man's RDO" on the BC7 base pack. When enabled, every
+		// block is ALSO packed with m_bc7_pack_flags_alt (typically a cheaper,
+		// e.g. mode-6-only set). Both candidates are unpacked and their RGBA PSNR
+		// vs the source measured; the cheaper alternate is KEPT as long as it's no
+		// more than m_bc7_alt_max_psnr_drop dB worse than the primary. Trades a
+		// little quality for fewer command/config bits downstream. Default off.
+		bool m_bc7_alt_pack_enabled = false;
+		uint32_t m_bc7_pack_flags_alt = basist::bc7f::cPackBC7FlagPBitOpt | basist::bc7f::cPackBC7FlagUseTrivialMode6;
+		float m_bc7_alt_max_psnr_drop = 0.5f; // dB the alternate may be worse than the primary and still be kept
+
+		// Optional block-reuse RDO pre-pass (runs after the BC7 base pack, BEFORE
+		// the endpoint RDO -- once a whole block is reused there's no point trying
+		// cheaper endpoints). For each non-solid block it tries replacing it with:
+		//  - REPEAT: an exact copy of its left/upper causal neighbor (the cheapest
+		//    command -- one byte, no config/endpoints/weights), kept if within
+		//    m_repeat_rdo_max_psnr_drop dB; and/or
+		//  - SOLID: its mean color (cheap SolidDPCM command), kept if within
+		//    m_solid_rdo_max_psnr_drop dB.
+		// Repeat is preferred over Solid (cheaper). Each independently enabled; off
+		// by default.
+		bool m_repeat_rdo_enabled = false;
+		float m_repeat_rdo_max_psnr_drop = 0.5f; // dB a block may drop to become a Repeat of a neighbor
+		bool m_solid_rdo_enabled = false;
+		float m_solid_rdo_max_psnr_drop = 0.5f;  // dB a block may drop to become a solid-color block
+
+		// Optional endpoint-prediction RDO pre-pass (runs after the BC7 base pack,
+		// before stripe coding). For each block it tries forcing the endpoints to
+		// each valid causal neighbor's prediction (left/upper/left-diag/right-diag)
+		// via a zero-residual endpoint DPCM and keeps the best whose weighted RGBA
+		// PSNR drops by no more than m_endpoint_rdo_max_psnr_drop dB -- those zero
+		// residuals then cost almost nothing downstream. Default off.
+		bool m_endpoint_rdo_enabled = false;
+		float m_endpoint_rdo_max_psnr_drop = 0.5f; // dB the block PSNR may drop to adopt a neighbor's endpoints
+
+		// Optional weight-DCT AC-truncation RDO (DCT-coded blocks only). After a
+		// block is DCT-coded, its highest-frequency weight-DCT AC coefficients are
+		// zeroed one at a time in reverse zigzag order (the 2x2 low-freq corner DC,
+		// (1,0),(0,1),(1,1) is protected) while the decoded PSNR stays within this
+		// dB drop -- fewer coded ACs. 0 == disabled.
+		float m_ac_trunc_rdo_max_psnr_drop = 0.0f;
+
+		// Quality floor shared by ALL RDO passes (block-reuse + endpoint + alt-pack):
+		// a candidate whose absolute weighted RGBA PSNR is below this is rejected
+		// outright, even if its drop is within tolerance -- prevents accepting
+		// excessive distortion in already-degraded blocks.
+		float m_rdo_min_block_psnr = 33.0f; // dB
+
+		// Master switch for ALL development/debug console output (printed via
+		// fmt_debug_printf, so it also respects the global enable_debug_printf()
+		// and stays silent in normal builds). Default false. Nothing prints
+		// unless this is true.
+		bool m_debug_output = false;
+
+		// Print the detailed pack statistics dashboard. Requires m_debug_output
+		// to also be set (m_debug_output gates everything).
+		bool m_print_stats = false;
+
+		// When true, the encoder writes visualization PNG(s) -- one pixel block
+		// per BC7 block -- to (m_debug_file_prefix + "<name>.png"). Encode-time
+		// only; never affects the compressed output. Default off.
+		bool m_debug_images = false;
+		std::string m_debug_file_prefix;
+
+		// Weight DCT-vs-DPCM decision: choose lossless DPCM when
+		// dpcm_cost * 100 <= dct_cost * alpha. Both costs are measured-rate
+		// estimates (see g_dpcm_resid_cost_obits + the DCT byte cost
+		// constants), so 100 is the neutral operating point; raising alpha
+		// flips more blocks to lossless DPCM (quality up, size up).
+		uint32_t m_wt_dpcm_alpha_pct = 100;
+
+		// REQUIRED, never null: the caller's job pool (total threads INCLUDES
+		// the caller, basisu convention). The encoder always uses it; a pool of 1
+		// just runs serially. Pool size affects scheduling only, never the emitted
+		// bytes (the stripe count is independent of it).
+		job_pool* m_pJob_pool = nullptr;
+
+		// Stripe count for the main coding pass: 0 == auto (derived from image
+		// dimensions); else force exactly this many, clamped to
+		// [1, min(block_rows, XBC7_MAX_ENCODER_STRIPES)]. 1 == single-stripe (max
+		// ratio: no seam cost, no seek table). More stripes == more decode
+		// parallelism but a slightly larger file (per-seam prediction reset +
+		// seek-table cost); more stripes than pool threads simply queue. Prefer
+		// set_num_stripes_for_image() over assigning this directly.
+		uint32_t m_num_stripes = 0;
+
+		// Validate a desired stripe count against an image and store it in
+		// m_num_stripes. Clamps so every stripe holds at least the minimum
+		// efficient number of block rows (never tiny or empty stripes) and never
+		// exceeds the format max; images too short to stripe usefully collapse to
+		// a single stripe. Returns the actual count stored (>= 1).
+		uint32_t set_num_stripes_for_image(const image& img, uint32_t desired_num_stripes);
+
+		// Configure ALL "poor man's RDO" knobs from a single level [0,100].
+		//  0       -> RDO fully OFF: every enable flag false, every PSNR-drop 0.
+		//  [1,100] -> enables the RDO passes and linearly ramps their tolerated
+		//             per-block PSNR drop with the level: the general passes
+		//             (BC7 alt-pack, endpoint-DPCM, weight-DCT AC-truncation) up to
+		//             10 dB at 100, and the cheaper block-reuse passes (repeat,
+		//             solid) up to 4 dB at 100.
+		// The absolute quality floor (m_rdo_min_block_psnr) is left untouched -- it
+		// still gates every candidate regardless of level.
+		void set_rdo_level(uint32_t rdo_level);
+	};
+
+	// coded_log_blocks (REQUIRED, by reference): receives the final coded logical
+	// BC7 blocks (resized + filled by the encoder). It's mandatory because the
+	// encoder always decodes the stream it just produced and validates every block
+	// round-trips to these -- see the self-validation at the end of pack_image().
+	bool pack_image(
+		const image& orig_img,
+		const pack_options& opts,
+		uint8_vec& comp_bytes,
+		vector2D<basist::bc7u::log_bc7_block>& coded_log_blocks);
+
+	// NOTE: the decoder API (unpack_image / unpack_image_threaded) now lives in
+	// basisu_xbc7_decode.h.
+
+} // namespace xbc7
+} // namespace basisu