From 1152785ab69346e69eb2748f8e658bd1dfd38b24 Mon Sep 17 00:00:00 2001 From: Richard Geldreich Date: Wed, 1 Jul 2026 13:30:21 -0400 Subject: [PATCH] modifications for v2.5 --- CMakeLists.txt | 58 +- basisu.vcxproj | 13 +- basisu.vcxproj.filters | 7 + basisu_tool.cpp | 5223 ++++++++++++--- basisu_tool_help.h | 244 +- encoder/3rdparty/android_astc_decomp.cpp | 27 +- encoder/basisu_astc_hdr_common.h | 2 +- encoder/basisu_astc_ldr_common.cpp | 995 ++- encoder/basisu_astc_ldr_common.h | 86 +- encoder/basisu_astc_ldr_encode.cpp | 7733 +++++++++++++++++++--- encoder/basisu_astc_ldr_encode.h | 44 +- encoder/basisu_basis_file.cpp | 155 +- encoder/basisu_basis_file.h | 31 +- encoder/basisu_comp.cpp | 774 ++- encoder/basisu_comp.h | 316 +- encoder/basisu_enc.cpp | 1148 +++- encoder/basisu_enc.h | 445 +- encoder/basisu_gpu_texture.cpp | 980 ++- encoder/basisu_gpu_texture.h | 94 +- encoder/basisu_math.h | 1610 ++++- encoder/basisu_tinyexr.cpp | 18 +- encoder/basisu_wasm_api_common.h | 31 +- encoder/basisu_wasm_transcoder_api.cpp | 48 +- encoder/basisu_wasm_transcoder_api.h | 10 + encoder/cppspmd_math.h | 10 +- encoder/pvpngreader.cpp | 29 +- 26 files changed, 17789 insertions(+), 2342 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 23ccb14..e165dd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,7 @@ option(BASISU_EXAMPLES "build examples" TRUE) option(BASISU_WASM_THREADING "Enable WASI threading support" OFF) option(BASISU_BUILD_PYTHON "Build native Python module via pybind11" OFF) option(BASISU_DISABLE_ANDROID_ASTC_DECOMP "Disable Android ASTC decompressor" OFF) +option(BASISU_SUPPORT_ASTCENC "Enable astcenc support in encoder" FALSE) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bin) @@ -97,6 +98,7 @@ message("Initial BASISU_OPENCL=${BASISU_OPENCL}") message("Initial BASISU_SAN=${BASISU_SAN}") message("Initial BASISU_EXAMPLES=${BASISU_EXAMPLES}") message("Initial BASISU_DISABLE_ANDROID_ASTC_DECOMP=${BASISU_DISABLE_ANDROID_ASTC_DECOMP}") +message("Initial BASISU_SUPPORT_ASTCENC=${BASISU_SUPPORT_ASTCENC}") if ((NOT MSVC) AND BASISU_OPENCL) # With MSVC builds we use the Khronos lib/include files in the project's "OpenCL" directory, to completely avoid requiring fiddly to install vendor SDK's. @@ -237,9 +239,13 @@ set(ENCODER_LIB_SRC_LIST encoder/basisu_resample_filters.cpp encoder/basisu_ssim.cpp encoder/basisu_uastc_enc.cpp + encoder/basisu_bc7e_scalar.cpp + encoder/basisu_dds_export.cpp encoder/basisu_bc7enc.cpp encoder/jpgd.cpp encoder/basisu_kernels_sse.cpp + encoder/basisu_bc15_spmd.cpp + encoder/basisu_bc15_spmd_sse.cpp encoder/basisu_opencl.cpp encoder/pvpngreader.cpp encoder/basisu_uastc_hdr_4x4_enc.cpp @@ -247,6 +253,8 @@ set(ENCODER_LIB_SRC_LIST encoder/basisu_astc_hdr_common.cpp encoder/basisu_astc_ldr_common.cpp encoder/basisu_astc_ldr_encode.cpp + encoder/basisu_astc_ldr_fencode.cpp + encoder/basisu_xbc7_encode.cpp encoder/basisu_tinyexr.cpp transcoder/basisu_transcoder.cpp encoder/basisu_astc_hdr_6x6_enc.h @@ -270,9 +278,13 @@ set(ENCODER_LIB_SRC_LIST encoder/basisu_resampler.h encoder/basisu_ssim.h encoder/basisu_uastc_enc.h + encoder/basisu_bc7e_scalar.h + encoder/basisu_dds_export.h encoder/basisu_uastc_hdr_4x4_enc.h encoder/basisu_astc_ldr_common.h encoder/basisu_astc_ldr_encode.h + encoder/basisu_astc_ldr_fencode.h + encoder/basisu_xbc7_encode.h encoder/cppspmd_flow.h encoder/cppspmd_math_declares.h encoder/cppspmd_math.h @@ -301,6 +313,33 @@ if (NOT BASISU_DISABLE_ANDROID_ASTC_DECOMP) ) endif() +if (BASISU_SUPPORT_ASTCENC) + list(APPEND ENCODER_LIB_SRC_LIST + encoder/3rdparty/astc-encoder-main/Source/astcenc_averages_and_directions.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_block_sizes.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_color_quantize.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_color_unquantize.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_compress_symbolic.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_compute_variance.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_decompress_symbolic.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_diagnostic_trace.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_entry.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_find_best_partitioning.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_ideal_endpoints_and_weights.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_image.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_integer_sequence.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_mathlib.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_mathlib_softfloat.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_partition_tables.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_percentile_tables.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_pick_best_endpoint_format.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_quantization.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_symbolic_physical.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_weight_align.cpp + encoder/3rdparty/astc-encoder-main/Source/astcenc_weight_quant_xfer_tables.cpp + ) +endif() + if (BASISU_ZSTD) set(ENCODER_LIB_SRC_LIST ${ENCODER_LIB_SRC_LIST} zstd/zstd.c) endif() @@ -308,8 +347,14 @@ endif() # Create the static library add_library(basisu_encoder STATIC ${ENCODER_LIB_SRC_LIST}) +if (BASISU_SUPPORT_ASTCENC) + target_compile_definitions(basisu_encoder PRIVATE BASISU_SUPPORT_ASTCENC=1) +else() + target_compile_definitions(basisu_encoder PRIVATE BASISU_SUPPORT_ASTCENC=0) +endif() + # Create the basisu executable and link against the static library -add_executable(basisu basisu_tool.cpp) +add_executable(basisu basisu_tool.cpp basisu_text_image.cpp basisu_tool_test_codecs.inl) target_link_libraries(basisu PRIVATE basisu_encoder) # Create the new example executable and link against the static library @@ -355,7 +400,7 @@ if (BASISU_BUILD_WASM) endif() - # 256 MB initial, 3.5 GB max � safe defaults for BasisU + # 256 MB initial, 3.5 GB max - safe defaults for BasisU target_link_options(basisu PRIVATE -Wl,--initial-memory=268435456 -Wl,--max-memory=3758096384 @@ -387,19 +432,24 @@ endif() #endif() if (BASISU_ZSTD) + # Note: the static encoder library MUST be compiled with the same value, otherwise its TUs + # (transcoder, basisu_comp, basisu_xbc7_encode, basisu_astc_ldr_encode) default to ZSTD=1 and + # emit ZSTD_* calls that won't link when zstd.c is excluded. + target_compile_definitions(basisu_encoder PRIVATE BASISD_SUPPORT_KTX2_ZSTD=1) target_compile_definitions(basisu PRIVATE BASISD_SUPPORT_KTX2_ZSTD=1) if(BASISU_EXAMPLES) target_compile_definitions(example PRIVATE BASISD_SUPPORT_KTX2_ZSTD=1) target_compile_definitions(example_capi PRIVATE BASISD_SUPPORT_KTX2_ZSTD=1) target_compile_definitions(example_transcoding PRIVATE BASISD_SUPPORT_KTX2_ZSTD=1) - endif() + endif() else() + target_compile_definitions(basisu_encoder PRIVATE BASISD_SUPPORT_KTX2_ZSTD=0) target_compile_definitions(basisu PRIVATE BASISD_SUPPORT_KTX2_ZSTD=0) if(BASISU_EXAMPLES) target_compile_definitions(example PRIVATE BASISD_SUPPORT_KTX2_ZSTD=0) target_compile_definitions(example_capi PRIVATE BASISD_SUPPORT_KTX2_ZSTD=0) target_compile_definitions(example_transcoding PRIVATE BASISD_SUPPORT_KTX2_ZSTD=0) - endif() + endif() endif() if (NOT MSVC) diff --git a/basisu.vcxproj b/basisu.vcxproj index 1e52b46..f2d6f02 100644 --- a/basisu.vcxproj +++ b/basisu.vcxproj @@ -146,7 +146,7 @@ OpenCL true true - _MBCS;%(PreprocessorDefinitions);BASISU_SUPPORT_SSE=1;BASISU_SUPPORT_OPENCL=1; + _MBCS;%(PreprocessorDefinitions);BASISU_SUPPORT_SSE=1;BASISU_SUPPORT_OPENCL=1;_HAS_EXCEPTIONS=0 AdvancedVectorExtensions stdcpp17 Level4 @@ -155,7 +155,7 @@ Console OpenCL\lib opencl64.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) - false + true @@ -165,7 +165,7 @@ OpenCL true true - _MBCS;%(PreprocessorDefinitions);BASISU_SUPPORT_SSE=0;BASISU_SUPPORT_OPENCL=1; + _MBCS;%(PreprocessorDefinitions);BASISU_SUPPORT_SSE=0;BASISU_SUPPORT_OPENCL=1;_HAS_EXCEPTIONS=0 Level4 stdcpp17 @@ -257,6 +257,7 @@ + @@ -268,6 +269,12 @@ + + + + + + diff --git a/basisu.vcxproj.filters b/basisu.vcxproj.filters index 5281d48..1b472b4 100644 --- a/basisu.vcxproj.filters +++ b/basisu.vcxproj.filters @@ -2,11 +2,18 @@ + + + + + + + \ No newline at end of file diff --git a/basisu_tool.cpp b/basisu_tool.cpp index 67941e6..941c98f 100644 --- a/basisu_tool.cpp +++ b/basisu_tool.cpp @@ -22,6 +22,8 @@ #include "transcoder/basisu.h" #include "transcoder/basisu_transcoder_internal.h" #include "encoder/basisu_enc.h" +#include "encoder/basisu_xbc7_encode.h" +#include "transcoder/basisu_xbc7_decoder.h" #include "encoder/basisu_etc.h" #include "encoder/basisu_gpu_texture.h" #include "encoder/basisu_frontend.h" @@ -38,9 +40,14 @@ #include #include +#include +#include #include "encoder/basisu_resampler.h" #include "encoder/basisu_resampler_filters.h" +#include "basisu_text_image.h" +#include "encoder/basisu_dds_export.h" +#include "encoder/pvpngreader.h" #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -57,7 +64,7 @@ using namespace basisu; using namespace buminiz; -#define BASISU_TOOL_VERSION "2.10.0" +#define BASISU_TOOL_VERSION "2.50.0" #if defined(DEBUG) #pragma message("DEBUG defined") @@ -87,7 +94,9 @@ enum tool_mode cInfo, cUnpack, cCompare, + cCompareHVS, cHDRCompare, + cImageDumpStats, cVersion, cBench, cCompSize, @@ -96,10 +105,24 @@ enum tool_mode cTestHDR_6x6, cTestHDR_6x6i, cTestXUASTCLDR, + cTestCodecs, + cTestCodecsGen, cCLBench, cSplitImage, cCombineImages, - cTonemapImage + cExtractChannel, + cExtractSwizzle, + cExtractRegion, + cTextToPng, + cPngToText, + cTonemapImage, + cDDS, + cExportDDS, + cExportKTX, + cTinyDDSInfo, + cKTXInfo, + cBenchmarkSingle, + cBenchmarkSweep }; static void print_usage() @@ -182,6 +205,66 @@ static bool load_listing_file(const std::string &f, basisu::vector return true; } +// Command line option matching helper. Recognizes an option by its canonical +// name (given with a single leading dash, e.g. "-etc1s"), accepting both the +// "-name" and Unix-style "--name" forms, case-insensitively. +static inline bool opt_match(const char *pArg, const char *pName) +{ + if (strcasecmp(pArg, pName) == 0) + return true; + // Accept the "--name" variant when the canonical name is "-name". + if ((pArg[0] == '-') && (pArg[1] == '-') && (pName[0] == '-') && (strcasecmp(pArg + 1, pName) == 0)) + return true; + return false; +} + +// Same, but matches against any of several alias names, e.g. +// opt_match(pArg, { "-uastc", "-uastc_ldr", "-uastc_ldr_4x4" }). +static inline bool opt_match(const char *pArg, std::initializer_list names) +{ + for (const char *pName : names) + if (opt_match(pArg, pName)) + return true; + return false; +} + +// Optional sRGB<->linear transform applied by the -extract_* modes. +enum extract_color_xform +{ + cExtractXformNone = 0, + cExtractXformToLinear, // treat input as sRGB, output linear light + cExtractXformToSRGB // treat input as linear light, output sRGB +}; + +// True if the argument is a non-empty run of decimal digits (an unsigned integer). Used to decide whether an +// optional region rectangle follows a command, so that a positional filename beginning with a digit (e.g. +// "5texture.png") is NOT mistaken for a region coordinate. +static bool arg_is_unsigned_int(const char* pArg) +{ + if ((!pArg) || (!pArg[0])) + return false; + for (const char* p = pArg; *p; p++) + if ((*p < '0') || (*p > '9')) + return false; + return true; +} + +// Maps a single channel token to a source channel index in [0,3] (R/G/B/A), or -1 if it is not a valid channel. +static int channel_token_to_index(char c) +{ + switch (c) + { + case 'r': case 'R': case '0': return 0; + case 'g': case 'G': case '1': return 1; + case 'b': case 'B': case '2': return 2; + case 'a': case 'A': case '3': return 3; + default: return -1; + } +} + +// Parses a -benchmark_single format token into a basis_tex_format (definition is down by codec_benchmark()). +static bool parse_benchmark_tex_format(const std::string& s, basist::basis_tex_format& out_fmt); + class command_line_params { BASISU_NO_EQUALS_OR_COPY_CONSTRUCT(command_line_params); @@ -195,7 +278,7 @@ class command_line_params BASISU_NOTE_UNUSED(num_remaining_args); BASISU_NOTE_UNUSED(arg_count); - if (strcasecmp(pArg, "-wasi_threads") == 0) + if (opt_match(pArg, "-wasi_threads")) { REMAINING_ARGS_CHECK(1); int num_threads = atoi(arg_v[arg_index + 1]); @@ -208,57 +291,52 @@ class command_line_params arg_count++; return true; } - else if (strcasecmp(pArg, "-higher_quality_transcoding") == 0) + else if (opt_match(pArg, "-higher_quality_transcoding")) { m_higher_quality_transcoding = true; return true; } - else if (strcasecmp(pArg, "-no_fast_xuastc_ldr_bc7_transcoding") == 0) + else if (opt_match(pArg, "-no_fast_xuastc_ldr_bc7_transcoding")) { m_xuastc_ldr_disable_bc7_transcoding = true; return true; } - else if (strcasecmp(pArg, "-fast_xuastc_ldr_bc7_transcoding") == 0) + else if (opt_match(pArg, "-fast_xuastc_ldr_bc7_transcoding")) { m_xuastc_ldr_disable_bc7_transcoding = false; return true; } - else if (strcasecmp(pArg, "-no_etc1s_chroma_filtering") == 0) + else if (opt_match(pArg, "-no_etc1s_chroma_filtering")) { m_no_etc1s_transcoding_chroma_filtering = true; return true; } - else if (strcasecmp(pArg, "-force_deblocking") == 0) + else if (opt_match(pArg, "-transcode_force_deblocking")) { - m_force_deblocking = true; + m_transcode_force_deblocking = true; return true; } - else if ((strcasecmp(pArg, "-disable_deblocking") == 0) || (strcasecmp(pArg, "-no_deblocking") == 0)) + else if (opt_match(pArg, {"-transcode_disable_deblocking", "-transcode_no_deblocking"})) { - m_disable_deblocking = true; + m_transcode_disable_deblocking = true; return true; } - else if (strcasecmp(pArg, "-stronger_deblocking") == 0) - { - m_stronger_deblocking = true; - return true; - } - + return false; } bool check_for_xuastc_options(const char** arg_v, const char* pArg, int arg_index, const int num_remaining_args, int& arg_count) { // New unified -quality level which works across all codecs - if (strcasecmp(pArg, "-quality") == 0) + if (opt_match(pArg, "-quality")) { REMAINING_ARGS_CHECK(1); - m_quality_level = clamp(atoi(arg_v[arg_index + 1]), 0, 100); + m_quality_level = clamp(atoi(arg_v[arg_index + 1]), 1, 100); arg_count++; return true; } // New unified -effort level, which works across all codecs - else if (strcasecmp(pArg, "-effort") == 0) + else if (opt_match(pArg, "-effort")) { REMAINING_ARGS_CHECK(1); m_effort_level = clamp(atoi(arg_v[arg_index + 1]), 0, 10); @@ -266,258 +344,410 @@ class command_line_params arg_count++; return true; } - else if (strcasecmp(pArg, "-xuastc_blurring") == 0) // experimental, not recommended, very slow + else if ((opt_match(pArg, "-xuastc_debug_block"))) + { + REMAINING_ARGS_CHECK(2); + m_comp_params.m_xuastc_ldr_debug_block_x = clamp(atoi(arg_v[arg_index + 1]), 0, 4096); + m_comp_params.m_xuastc_ldr_debug_block_y = clamp(atoi(arg_v[arg_index + 2]), 0, 4096); + + arg_count += 2; + return true; + } + else if (opt_match(pArg, {"-xuastc_blurring", "-xuastc_ldr_blurring"})) // experimental, not recommended, very slow { m_comp_params.m_xuastc_ldr_blurring = true; return true; } - else if (strcasecmp(pArg, "-weights") == 0) + else if (opt_match(pArg, {"-xuastc_no_blurring", "-xuastc_ldr_no_blurring"})) // experimental, not recommended, very slow + { + m_comp_params.m_xuastc_ldr_blurring = false; + return true; + } + else if (opt_match(pArg, {"-xuastc_use_auto_comp", "-xuastc_ldr_use_auto_comp"})) + { + m_comp_params.m_xuastc_ldr_astc_comp_selection = (int)xuastc_ldr_astc_comp_selection::cAuto; + return true; + } + else if (opt_match(pArg, {"-xuastc_use_basisu", "-xuastc_ldr_use_basisu"})) + { + m_comp_params.m_xuastc_ldr_astc_comp_selection = (int)xuastc_ldr_astc_comp_selection::cBasisU; + return true; + } + else if (opt_match(pArg, {"-xuastc_use_astcenc", "-xuastc_ldr_use_astcenc"})) // experimental/development, must be enabled at compilation time by setting BASISU_SUPPORT_ASTCENC=1 + { + m_comp_params.m_xuastc_ldr_astc_comp_selection = (int)xuastc_ldr_astc_comp_selection::cASTCENC; + return true; + } + else if (opt_match(pArg, {"-xuastc_use_astcf", "-xuastc_ldr_use_astcf"})) + { + m_comp_params.m_xuastc_ldr_astc_comp_selection = (int)xuastc_ldr_astc_comp_selection::cASTCF; + return true; + } + else if (opt_match(pArg, {"-xuastc_merge_astcenc", "-xuastc_ldr_merge_astcenc"})) // experimental/development, must be enabled at compilation time by setting BASISU_SUPPORT_ASTCENC=1 + { + m_comp_params.m_xuastc_ldr_astc_comp_selection = (int)xuastc_ldr_astc_comp_selection::cBasisU_and_ASTCENC; + return true; + } + else if (opt_match(pArg, {"-xuastc_merge_astcf", "-xuastc_ldr_merge_astcf"})) // experimental/development, must be enabled at compilation time by setting BASISU_SUPPORT_ASTCENC=1 + { + m_comp_params.m_xuastc_ldr_astc_comp_selection = (int)xuastc_ldr_astc_comp_selection::cBasisU_and_ASTCF; + return true; + } + else if (opt_match(pArg, {"-xuastc_merge_all", "-xuastc_ldr_merge_all"})) // experimental/development, must be enabled at compilation time by setting BASISU_SUPPORT_ASTCENC=1 + { + m_comp_params.m_xuastc_ldr_astc_comp_selection = (int)xuastc_ldr_astc_comp_selection::cUseAll; + return true; + } + else if (opt_match(pArg, {"-xuastc_no_deblocking", "-xuastc_ldr_no_deblocking"})) + { + m_comp_params.m_xuastc_ldr_deblocking_mode = (int)xuastc_ldr_deblocking_mode::cDisabled; + return true; + } + else if (opt_match(pArg, {"-xuastc_deblocking_largest", "-xuastc_ldr_deblocking_largest"})) + { + m_comp_params.m_xuastc_ldr_deblocking_mode = (int)xuastc_ldr_deblocking_mode::cUseSCDAndFilteringOnlyLargestBlocks; // codec default + return true; + } + else if (opt_match(pArg, {"-xuastc_deblocking_all", "-xuastc_ldr_deblocking_all"})) + { + m_comp_params.m_xuastc_ldr_deblocking_mode = (int)xuastc_ldr_deblocking_mode::cUseSCDAndFilteringAllBlockSizes; + return true; + } + else if (opt_match(pArg, {"-xuastc_deblocking_scd_no_filtering", "-xuastc_ldr_deblocking_scd_no_filtering"})) + { + m_comp_params.m_xuastc_ldr_deblocking_mode = (int)xuastc_ldr_deblocking_mode::cUseSCDNoFiltering; + return true; + } + else if (opt_match(pArg, {"-xuastc_deblocking_no_scd_filtering_largest", "-xuastc_ldr_deblocking_no_scd_filtering_largest"})) + { + m_comp_params.m_xuastc_ldr_deblocking_mode = (int)xuastc_ldr_deblocking_mode::cNoSCDButEnableFilteringOnLargestBlocks; + return true; + } + else if (opt_match(pArg, {"-xuastc_deblocking_no_scd_filtering_all", "-xuastc_ldr_deblocking_no_scd_filtering_all"})) + { + m_comp_params.m_xuastc_ldr_deblocking_mode = (int)xuastc_ldr_deblocking_mode::cNoSCDButEnableFilteringOnAllBlocks; + return true; + } + else if (opt_match(pArg, {"-xuastc_deblocking_num_passes", "-xuastc_ldr_deblocking_num_passes"})) + { + REMAINING_ARGS_CHECK(1); + m_comp_params.m_xuastc_ldr_num_deblocking_passes = clamp(atoi(arg_v[arg_index + 1]), 2, 256); + arg_count++; + return true; + } + else if (opt_match(pArg, {"-xuastc_sharpen", "-xuastc_ldr_sharpen"})) + { + REMAINING_ARGS_CHECK(1); + m_comp_params.m_xuastc_ldr_sharpen_amount = (float)atof(arg_v[arg_index + 1]); + arg_count++; + + // setting to 0 disables + if (m_comp_params.m_xuastc_ldr_sharpen_amount <= 0.0f) + m_comp_params.m_xuastc_ldr_sharpen_mode = (int)xuastc_ldr_sharpen_mode::cDisabled; + else + m_comp_params.m_xuastc_ldr_sharpen_mode = (int)xuastc_ldr_sharpen_mode::cAllBlockSizes; // if they've specified -xuastc_sharpen, they want it no matter what block size + + return true; + } + else if (opt_match(pArg, {"-xuastc_no_sharpen", "-xuastc_ldr_no_sharpen"})) + { + m_comp_params.m_xuastc_ldr_sharpen_mode = (int)xuastc_ldr_sharpen_mode::cDisabled; + return true; + } + else if (opt_match(pArg, {"-xuastc_weights", "-xuastc_ldr_weights", "-weights"})) { REMAINING_ARGS_CHECK(4); - m_comp_params.m_xuastc_ldr_channel_weights[0] = (uint32_t)clamp((float)atof(arg_v[arg_index + 1]), 0.0f, 1024.0f); - m_comp_params.m_xuastc_ldr_channel_weights[1] = (uint32_t)clamp((float)atof(arg_v[arg_index + 2]), 0.0f, 1024.0f); - m_comp_params.m_xuastc_ldr_channel_weights[2] = (uint32_t)clamp((float)atof(arg_v[arg_index + 3]), 0.0f, 1024.0f); - m_comp_params.m_xuastc_ldr_channel_weights[3] = (uint32_t)clamp((float)atof(arg_v[arg_index + 4]), 0.0f, 1024.0f); + m_comp_params.m_xuastc_ldr_channel_weights[0] = (uint32_t)clamp((float)atof(arg_v[arg_index + 1]), 1.0f, 1024.0f); + m_comp_params.m_xuastc_ldr_channel_weights[1] = (uint32_t)clamp((float)atof(arg_v[arg_index + 2]), 1.0f, 1024.0f); + m_comp_params.m_xuastc_ldr_channel_weights[2] = (uint32_t)clamp((float)atof(arg_v[arg_index + 3]), 1.0f, 1024.0f); + m_comp_params.m_xuastc_ldr_channel_weights[3] = (uint32_t)clamp((float)atof(arg_v[arg_index + 4]), 1.0f, 1024.0f); arg_count += 4; return true; } - else if (strcasecmp(pArg, "-ls_min_psnr") == 0) + else if (opt_match(pArg, "-ls_min_psnr")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_ls_min_psnr = (float)atof(arg_v[arg_index + 1]); arg_count++; return true; } - else if (strcasecmp(pArg, "-ls_min_alpha_psnr") == 0) + else if (opt_match(pArg, "-ls_min_alpha_psnr")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_ls_min_alpha_psnr = (float)atof(arg_v[arg_index + 1]); arg_count++; return true; } - else if (strcasecmp(pArg, "-ls_thresh_psnr") == 0) + else if (opt_match(pArg, "-ls_thresh_psnr")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_ls_thresh_psnr = (float)atof(arg_v[arg_index + 1]); arg_count++; return true; } - else if (strcasecmp(pArg, "-ls_thresh_alpha_psnr") == 0) + else if (opt_match(pArg, "-ls_thresh_alpha_psnr")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_ls_thresh_alpha_psnr = (float)atof(arg_v[arg_index + 1]); arg_count++; return true; } - else if (strcasecmp(pArg, "-ls_thresh_edge_psnr") == 0) + else if (opt_match(pArg, "-ls_thresh_edge_psnr")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_ls_thresh_edge_psnr = (float)atof(arg_v[arg_index + 1]); arg_count++; return true; } - else if (strcasecmp(pArg, "-ls_thresh_edge_alpha_psnr") == 0) + else if (opt_match(pArg, "-ls_thresh_edge_alpha_psnr")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_ls_thresh_edge_alpha_psnr = (float)atof(arg_v[arg_index + 1]); arg_count++; return true; } - else if (strcasecmp(pArg, "-xuastc_arith") == 0) + else if (opt_match(pArg, {"-xuastc_arith", "-xuastc_ldr_arith"})) { m_comp_params.m_xuastc_ldr_syntax = (int)basist::astc_ldr_t::xuastc_ldr_syntax::cFullArith; return true; } - else if (strcasecmp(pArg, "-xuastc_zstd") == 0) + else if (opt_match(pArg, {"-xuastc_zstd", "-xuastc_ldr_zstd"})) { m_comp_params.m_xuastc_ldr_syntax = (int)basist::astc_ldr_t::xuastc_ldr_syntax::cFullZStd; return true; } - else if (strcasecmp(pArg, "-xuastc_hybrid") == 0) + else if (opt_match(pArg, {"-xuastc_hybrid", "-xuastc_ldr_hybrid"})) { m_comp_params.m_xuastc_ldr_syntax = (int)basist::astc_ldr_t::xuastc_ldr_syntax::cHybridArithZStd; return true; } - else if (strcasecmp(pArg, "-xy") == 0) + else if (opt_match(pArg, {"-xuastc_heavy_subset_usage", "-xuastc_ldr_heavy_subset_usage"})) + { + m_comp_params.m_xuastc_ldr_heavy_subset_usage = true; + return true; + } + else if (opt_match(pArg, {"-xuastc_no_heavy_subset_usage", "-xuastc_ldr_no_heavy_subset_usage"})) + { + m_comp_params.m_xuastc_ldr_heavy_subset_usage = false; + return true; + } + else if (opt_match(pArg, "-xy")) { m_comp_params.m_xuastc_ldr_use_lossy_supercompression = true; return true; } - else if (strcasecmp(pArg, "-xyd") == 0) + else if (opt_match(pArg, "-xyd")) { m_comp_params.m_xuastc_ldr_use_lossy_supercompression = false; return true; } - else if (strcasecmp(pArg, "-xs") == 0) + else if (opt_match(pArg, "-xs")) { m_comp_params.m_xuastc_ldr_force_disable_subsets = true; return true; } - else if (strcasecmp(pArg, "-xsu") == 0) + else if (opt_match(pArg, "-xsu")) { m_comp_params.m_xuastc_ldr_force_disable_subsets = false; return true; } - else if (strcasecmp(pArg, "-xp") == 0) + else if (opt_match(pArg, "-xp")) { m_comp_params.m_xuastc_ldr_force_disable_rgb_dual_plane = true; return true; } - else if (strcasecmp(pArg, "-xpu") == 0) + else if (opt_match(pArg, "-xpu")) { m_comp_params.m_xuastc_ldr_force_disable_rgb_dual_plane = false; return true; } - else if (strcasecmp(pArg, "-ts") == 0) + else if (opt_match(pArg, "-ts")) { m_comp_params.m_perceptual = true; m_comp_params.m_ktx2_and_basis_srgb_transfer_function = true; return true; } - else if (strcasecmp(pArg, "-tl") == 0) + else if (opt_match(pArg, "-tl")) { m_comp_params.m_perceptual = false; m_comp_params.m_ktx2_and_basis_srgb_transfer_function = false; return true; } // Supercompressed XUASTC LDR 4x4-12x12 - else if ((strcasecmp(pArg, "-ldr_4x4i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_4x4") == 0)) + else if (opt_match(pArg, {"-ldr_4x4i", "-xuastc_ldr_4x4"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_4x4); return true; } - else if ((strcasecmp(pArg, "-ldr_5x4i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_5x4") == 0)) + else if (opt_match(pArg, {"-ldr_5x4i", "-xuastc_ldr_5x4"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_5x4); return true; } - else if ((strcasecmp(pArg, "-ldr_5x5i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_5x5") == 0)) + else if (opt_match(pArg, {"-ldr_5x5i", "-xuastc_ldr_5x5"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_5x5); return true; } - else if ((strcasecmp(pArg, "-ldr_6x5i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_6x5") == 0)) + else if (opt_match(pArg, {"-ldr_6x5i", "-xuastc_ldr_6x5"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_6x5); return true; } - else if ((strcasecmp(pArg, "-ldr_6x6i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_6x6") == 0)) + else if (opt_match(pArg, {"-ldr_6x6i", "-xuastc_ldr_6x6"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_6x6); return true; } - else if ((strcasecmp(pArg, "-ldr_8x5i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_8x5") == 0)) + else if (opt_match(pArg, {"-ldr_8x5i", "-xuastc_ldr_8x5"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_8x5); return true; } - else if ((strcasecmp(pArg, "-ldr_8x6i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_8x6") == 0)) + else if (opt_match(pArg, {"-ldr_8x6i", "-xuastc_ldr_8x6"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_8x6); return true; } - else if ((strcasecmp(pArg, "-ldr_10x5i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_10x5") == 0)) + else if (opt_match(pArg, {"-ldr_10x5i", "-xuastc_ldr_10x5"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_10x5); return true; } - else if ((strcasecmp(pArg, "-ldr_10x6i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_10x6") == 0)) + else if (opt_match(pArg, {"-ldr_10x6i", "-xuastc_ldr_10x6"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_10x6); return true; } - else if ((strcasecmp(pArg, "-ldr_8x8i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_8x8") == 0)) + else if (opt_match(pArg, {"-ldr_8x8i", "-xuastc_ldr_8x8"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_8x8); return true; } - else if ((strcasecmp(pArg, "-ldr_10x8i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_10x8") == 0)) + else if (opt_match(pArg, {"-ldr_10x8i", "-xuastc_ldr_10x8"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_10x8); return true; } - else if ((strcasecmp(pArg, "-ldr_10x10i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_10x10") == 0)) + else if (opt_match(pArg, {"-ldr_10x10i", "-xuastc_ldr_10x10"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_10x10); return true; } - else if ((strcasecmp(pArg, "-ldr_12x10i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_12x10") == 0)) + else if (opt_match(pArg, {"-ldr_12x10i", "-xuastc_ldr_12x10"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_12x10); return true; } - else if ((strcasecmp(pArg, "-ldr_12x12i") == 0) || (strcasecmp(pArg, "-xuastc_ldr_12x12") == 0)) + else if (opt_match(pArg, {"-ldr_12x12i", "-xuastc_ldr_12x12"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cXUASTC_LDR_12x12); return true; } + else if (opt_match(pArg, "-xubc7")) + { + m_comp_params.set_format_mode(basist::basis_tex_format::cXUBC7); + return true; + } + else if (opt_match(pArg, {"-xubc7_rdo", "-xubc7_rdo_level"})) + { + REMAINING_ARGS_CHECK(1); + m_comp_params.m_xubc7_rdo_level = clamp(atoi(arg_v[arg_index + 1]), 0, 100); + arg_count++; + return true; + } + else if (opt_match(pArg, {"-xubc7_stripes", "-xubc7_num_stripes"})) + { + REMAINING_ARGS_CHECK(1); + m_comp_params.m_xubc7_num_stripes = clamp(atoi(arg_v[arg_index + 1]), 1, 16); + arg_count++; + return true; + } + else if (opt_match(pArg, "-xubc7_bc7f")) + { + m_comp_params.m_xubc7_encoder = (int)basisu::xbc7::bc7_encoder_type::cBC7F; + return true; + } + else if (opt_match(pArg, {"-xubc7_bc7e_scalar", "-xubc7_bc7e"})) + { + m_comp_params.m_xubc7_encoder = (int)basisu::xbc7::bc7_encoder_type::cBC7E_Scalar; + return true; + } + else if (opt_match(pArg, "-xubc7_bc7e_scalar_level")) + { + REMAINING_ARGS_CHECK(1); + m_comp_params.m_xubc7_encoder = (int)basisu::xbc7::bc7_encoder_type::cBC7E_Scalar; + m_comp_params.m_xubc7_bc7e_scalar_level = clamp(atoi(arg_v[arg_index + 1]), (int)basisu::xbc7::BC7E_SCALAR_MIN_LEVEL, (int)basisu::xbc7::BC7E_SCALAR_MAX_LEVEL); + arg_count++; + return true; + } // Plain ASTC LDR 4x4-12x12 - else if ((strcasecmp(pArg, "-ldr_4x4") == 0) || (strcasecmp(pArg, "-astc_ldr_4x4") == 0)) + else if (opt_match(pArg, {"-ldr_4x4", "-astc_ldr_4x4"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_4x4); return true; } - else if ((strcasecmp(pArg, "-ldr_5x4") == 0) || (strcasecmp(pArg, "-astc_ldr_5x4") == 0)) + else if (opt_match(pArg, {"-ldr_5x4", "-astc_ldr_5x4"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_5x4); return true; } - else if ((strcasecmp(pArg, "-ldr_5x5") == 0) || (strcasecmp(pArg, "-astc_ldr_5x5") == 0)) + else if (opt_match(pArg, {"-ldr_5x5", "-astc_ldr_5x5"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_5x5); return true; } - else if ((strcasecmp(pArg, "-ldr_6x5") == 0) || (strcasecmp(pArg, "-astc_ldr_6x5") == 0)) + else if (opt_match(pArg, {"-ldr_6x5", "-astc_ldr_6x5"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_6x5); return true; } - else if ((strcasecmp(pArg, "-ldr_6x6") == 0) || (strcasecmp(pArg, "-astc_ldr_6x6") == 0)) + else if (opt_match(pArg, {"-ldr_6x6", "-astc_ldr_6x6"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_6x6); return true; } - else if ((strcasecmp(pArg, "-ldr_8x5") == 0) || (strcasecmp(pArg, "-astc_ldr_8x5") == 0)) + else if (opt_match(pArg, {"-ldr_8x5", "-astc_ldr_8x5"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_8x5); return true; } - else if ((strcasecmp(pArg, "-ldr_8x6") == 0) || (strcasecmp(pArg, "-astc_ldr_8x6") == 0)) + else if (opt_match(pArg, {"-ldr_8x6", "-astc_ldr_8x6"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_8x6); return true; } - else if ((strcasecmp(pArg, "-ldr_10x5") == 0) || (strcasecmp(pArg, "-astc_ldr_10x5") == 0)) + else if (opt_match(pArg, {"-ldr_10x5", "-astc_ldr_10x5"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_10x5); return true; } - else if ((strcasecmp(pArg, "-ldr_10x6") == 0) || (strcasecmp(pArg, "-astc_ldr_10x6") == 0)) + else if (opt_match(pArg, {"-ldr_10x6", "-astc_ldr_10x6"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_10x6); return true; } - else if ((strcasecmp(pArg, "-ldr_8x8") == 0) || (strcasecmp(pArg, "-astc_ldr_8x8") == 0)) + else if (opt_match(pArg, {"-ldr_8x8", "-astc_ldr_8x8"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_8x8); return true; } - else if ((strcasecmp(pArg, "-ldr_10x8") == 0) || (strcasecmp(pArg, "-astc_ldr_10x8") == 0)) + else if (opt_match(pArg, {"-ldr_10x8", "-astc_ldr_10x8"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_10x8); return true; } - else if ((strcasecmp(pArg, "-ldr_10x10") == 0) || (strcasecmp(pArg, "-astc_ldr_10x10") == 0)) + else if (opt_match(pArg, {"-ldr_10x10", "-astc_ldr_10x10"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_10x10); return true; } - else if ((strcasecmp(pArg, "-ldr_12x10") == 0) || (strcasecmp(pArg, "-astc_ldr_12x10") == 0)) + else if (opt_match(pArg, {"-ldr_12x10", "-astc_ldr_12x10"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_12x10); return true; } - else if ((strcasecmp(pArg, "-ldr_12x12") == 0) || (strcasecmp(pArg, "-astc_ldr_12x12") == 0)) + else if (opt_match(pArg, {"-ldr_12x12", "-astc_ldr_12x12"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_LDR_12x12); return true; @@ -526,43 +756,89 @@ class command_line_params return false; } + // -dds (basic DX10 DDS writer) options. Kept in its own function so the big option chain in parse() stays + // under the compiler's block-nesting limit. See basisu_dds_export.h. + bool check_for_dds_options(const char** arg_v, const char* pArg, int arg_index, const int num_remaining_args, int& arg_count) + { + if (opt_match(pArg, "-dds")) + { + m_mode = cDDS; + return true; + } + else if (opt_match(pArg, "-dds_format")) + { + REMAINING_ARGS_CHECK(1); + m_dds_format = arg_v[arg_index + 1]; + arg_count++; + return true; + } + else if (opt_match(pArg, "-dds_bc7f")) + { + m_dds_bc7_encoder = cDDSBC7Encoder_BC7F; + return true; + } + else if (opt_match(pArg, {"-dds_bc7e_scalar", "-dds_bc7e"})) + { + m_dds_bc7_encoder = cDDSBC7Encoder_BC7E_Scalar; + return true; + } + else if (opt_match(pArg, "-dds_bc7f_level")) + { + REMAINING_ARGS_CHECK(1); + m_dds_bc7_encoder = cDDSBC7Encoder_BC7F; + m_dds_bc7f_level = clamp(atoi(arg_v[arg_index + 1]), 0, 2); + arg_count++; + return true; + } + else if (opt_match(pArg, "-dds_bc7e_scalar_level")) + { + REMAINING_ARGS_CHECK(1); + m_dds_bc7_encoder = cDDSBC7Encoder_BC7E_Scalar; + m_dds_bc7e_scalar_level = clamp(atoi(arg_v[arg_index + 1]), 0, 6); + arg_count++; + return true; + } + + return false; + } + bool check_for_hdr_options(const char** arg_v, const char* pArg, int arg_index, const int num_remaining_args, int& arg_count) { - if ((strcasecmp(pArg, "-hdr") == 0) || (strcasecmp(pArg, "-hdr_4x4") == 0) || (strcasecmp(pArg, "-uastc_hdr_4x4") == 0)) + if (opt_match(pArg, {"-hdr", "-hdr_4x4", "-uastc_hdr_4x4"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cUASTC_HDR_4x4); return true; } - else if (strcasecmp(pArg, "-rec_2020") == 0) + else if (opt_match(pArg, "-rec_2020")) { m_comp_params.m_astc_hdr_6x6_options.m_rec2020_bt2100_color_gamut = true; return true; } - else if (strcasecmp(pArg, "-hdr_6x6i_16_compatibility") == 0) + else if (opt_match(pArg, "-hdr_6x6i_16_compatibility")) { // UASTC HDR 6x6i: Write v1.60 compatible files vs. 2.0. m_comp_params.m_astc_hdr_6x6_options.m_write_basisu_1_6_compatible_files = true; return true; } - else if (strcasecmp(pArg, "-hdr_6x6i_20_compatibility") == 0) + else if (opt_match(pArg, "-hdr_6x6i_20_compatibility")) { // UASTC HDR 6x6i: Write v2.00 compatible files vs. 2.0. m_comp_params.m_astc_hdr_6x6_options.m_write_basisu_1_6_compatible_files = false; return true; } - else if ((strcasecmp(pArg, "-hdr_6x6") == 0) || (strcasecmp(pArg, "-astc_hdr_6x6") == 0)) + else if (opt_match(pArg, {"-hdr_6x6", "-astc_hdr_6x6"})) { // max quality (if -lambda=0) or RDO UASTC HDR 6x6 m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_HDR_6x6); return true; } - else if ((strcasecmp(pArg, "-hdr_6x6i") == 0) || (strcasecmp(pArg, "-uastc_hdr_6x6") == 0) || (strcasecmp(pArg, "-uastc_hdr_6x6i") == 0)) + else if (opt_match(pArg, {"-hdr_6x6i", "-uastc_hdr_6x6", "-uastc_hdr_6x6i"})) { // intermediate format UASTC HDR 6x6 m_comp_params.set_format_mode(basist::basis_tex_format::cUASTC_HDR_6x6_INTERMEDIATE); return true; } - else if (strcasecmp(pArg, "-lambda") == 0) + else if (opt_match(pArg, "-lambda")) { REMAINING_ARGS_CHECK(1); @@ -584,7 +860,7 @@ class command_line_params arg_count++; return true; } - else if (strcasecmp(pArg, "-hdr_6x6_jnd") == 0) + else if (opt_match(pArg, "-hdr_6x6_jnd")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_astc_hdr_6x6_options.m_jnd_optimization = true; @@ -592,7 +868,7 @@ class command_line_params arg_count++; return true; } - else if (strcasecmp(pArg, "-hdr_6x6_level") == 0) + else if (opt_match(pArg, "-hdr_6x6_level")) { REMAINING_ARGS_CHECK(1); const int level = atoi(arg_v[arg_index + 1]); @@ -604,7 +880,7 @@ class command_line_params arg_count++; return true; } - else if (strcasecmp(pArg, "-hdr_6x6i_level") == 0) + else if (opt_match(pArg, "-hdr_6x6i_level")) { REMAINING_ARGS_CHECK(1); const int level = atoi(arg_v[arg_index + 1]); @@ -616,17 +892,17 @@ class command_line_params arg_count++; return true; } - else if (strcasecmp(pArg, "-hdr_6x6_extra_pats") == 0) + else if (opt_match(pArg, "-hdr_6x6_extra_pats")) { m_comp_params.m_astc_hdr_6x6_options.m_extra_patterns_flag = true; return true; } - else if (strcasecmp(pArg, "-hdr_6x6_brute_force_pats") == 0) + else if (opt_match(pArg, "-hdr_6x6_brute_force_pats")) { m_comp_params.m_astc_hdr_6x6_options.m_brute_force_partition_matching = true; return true; } - else if ((strcasecmp(pArg, "-hdr_6x6_comp_levels") == 0) || (strcasecmp(pArg, "-hdr_6x6i_comp_levels") == 0)) + else if (opt_match(pArg, {"-hdr_6x6_comp_levels", "-hdr_6x6i_comp_levels"})) { REMAINING_ARGS_CHECK(2); @@ -637,7 +913,7 @@ class command_line_params m_comp_params.m_astc_hdr_6x6_options.m_master_comp_level = minimum(lo_level, hi_level); m_comp_params.m_astc_hdr_6x6_options.m_highest_comp_level = maximum(lo_level, hi_level); - if (strcasecmp(pArg, "-hdr_6x6_comp_levels") == 0) + if (opt_match(pArg, "-hdr_6x6_comp_levels")) m_comp_params.set_format_mode(basist::basis_tex_format::cASTC_HDR_6x6); else m_comp_params.set_format_mode(basist::basis_tex_format::cUASTC_HDR_6x6_INTERMEDIATE); @@ -647,54 +923,56 @@ class command_line_params arg_count += 2; return true; } - else if (strcasecmp(pArg, "-hdr_6x6_no_gaussian") == 0) + else if (opt_match(pArg, "-hdr_6x6_no_gaussian")) { m_comp_params.m_astc_hdr_6x6_options.m_gaussian1_fallback = false; m_comp_params.m_astc_hdr_6x6_options.m_gaussian2_fallback = false; return true; } - else if (strcasecmp(pArg, "-hdr_6x6_gaussian1") == 0) + else if (opt_match(pArg, "-hdr_6x6_gaussian1")) { + REMAINING_ARGS_CHECK(1); m_comp_params.m_astc_hdr_6x6_options.m_gaussian1_strength = (float)atof(arg_v[arg_index + 1]); arg_count++; return true; } - else if (strcasecmp(pArg, "-hdr_6x6_gaussian2") == 0) + else if (opt_match(pArg, "-hdr_6x6_gaussian2")) { + REMAINING_ARGS_CHECK(1); m_comp_params.m_astc_hdr_6x6_options.m_gaussian2_strength = (float)atof(arg_v[arg_index + 1]); arg_count++; return true; } - else if ((strcasecmp(pArg, "-hdr_ldr_no_srgb_to_linear") == 0) || (strcasecmp(pArg, "-hdr_ldr_upconversion_no_srgb_to_linear") == 0)) + else if (opt_match(pArg, {"-hdr_ldr_no_srgb_to_linear", "-hdr_ldr_upconversion_no_srgb_to_linear"})) { m_comp_params.m_ldr_hdr_upconversion_srgb_to_linear = false; return true; } - else if (strcasecmp(pArg, "-hdr_ldr_upconversion_black_bias") == 0) + else if (opt_match(pArg, "-hdr_ldr_upconversion_black_bias")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_ldr_hdr_upconversion_black_bias = (float)atof(arg_v[arg_index + 1]); arg_count++; return true; } - else if (strcasecmp(pArg, "-hdr_ldr_upconversion_nit_multiplier") == 0) + else if (opt_match(pArg, "-hdr_ldr_upconversion_nit_multiplier")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_ldr_hdr_upconversion_nit_multiplier = (float)atof(arg_v[arg_index + 1]); arg_count++; return true; } - else if (strcasecmp(pArg, "-hdr_uber_mode") == 0) + else if (opt_match(pArg, "-hdr_uber_mode")) { m_comp_params.m_uastc_hdr_4x4_options.m_allow_uber_mode = true; return true; } - else if (strcasecmp(pArg, "-hdr_ultra_quant") == 0) + else if (opt_match(pArg, "-hdr_ultra_quant")) { m_comp_params.m_uastc_hdr_4x4_options.m_ultra_quant = true; return true; } - else if (strcasecmp(pArg, "-hdr_favor_astc") == 0) + else if (opt_match(pArg, "-hdr_favor_astc")) { m_comp_params.m_hdr_favor_astc = true; return true; @@ -706,17 +984,18 @@ class command_line_params // ETC1S or UASTC LDR 4x4 specific options bool check_for_etc1s_or_uastc_options(const char** arg_v, const char* pArg, int arg_index, const int num_remaining_args, int& arg_count) { - if (strcasecmp(pArg, "-etc1s") == 0) + if (opt_match(pArg, {"-etc1s", "-ldr"})) { + // -ldr selects the LDR/SDR default codec (ETC1S), symmetric with -hdr. m_comp_params.set_format_mode(basist::basis_tex_format::cETC1S); return true; } - else if ((strcasecmp(pArg, "-uastc") == 0) || (strcasecmp(pArg, "-uastc_ldr") == 0) || (strcasecmp(pArg, "-uastc_ldr_4x4") == 0)) + else if (opt_match(pArg, {"-uastc", "-uastc_ldr", "-uastc_ldr_4x4"})) { m_comp_params.set_format_mode(basist::basis_tex_format::cUASTC_LDR_4x4); return true; } - else if (strcasecmp(pArg, "-uastc_level") == 0) + else if (opt_match(pArg, "-uastc_level")) { REMAINING_ARGS_CHECK(1); @@ -737,7 +1016,7 @@ class command_line_params arg_count++; return true; } - else if (strcasecmp(pArg, "-uastc_rdo_l") == 0) + else if (opt_match(pArg, "-uastc_rdo_l")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_rdo_uastc_ldr_4x4_quality_scalar = (float)atof(arg_v[arg_index + 1]); @@ -748,43 +1027,43 @@ class command_line_params arg_count++; return true; } - else if (strcasecmp(pArg, "-uastc_rdo_d") == 0) + else if (opt_match(pArg, "-uastc_rdo_d")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_rdo_uastc_ldr_4x4_dict_size = atoi(arg_v[arg_index + 1]); arg_count++; return true; } - else if (strcasecmp(pArg, "-uastc_rdo_b") == 0) + else if (opt_match(pArg, "-uastc_rdo_b")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_rdo_uastc_ldr_4x4_max_smooth_block_error_scale = (float)atof(arg_v[arg_index + 1]); arg_count++; return true; } - else if (strcasecmp(pArg, "-uastc_rdo_s") == 0) + else if (opt_match(pArg, "-uastc_rdo_s")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_rdo_uastc_ldr_4x4_smooth_block_max_std_dev = (float)atof(arg_v[arg_index + 1]); arg_count++; return true; } - else if (strcasecmp(pArg, "-uastc_rdo_f") == 0) + else if (opt_match(pArg, "-uastc_rdo_f")) { m_comp_params.m_rdo_uastc_ldr_4x4_favor_simpler_modes_in_rdo_mode = false; return true; } - else if (strcasecmp(pArg, "-uastc_rdo_m") == 0) + else if (opt_match(pArg, "-uastc_rdo_m")) { m_comp_params.m_rdo_uastc_ldr_4x4_multithreading = false; return true; } - else if (strcasecmp(pArg, "-validate_etc1s") == 0) + else if (opt_match(pArg, "-validate_etc1s")) { m_comp_params.m_validate_etc1s = true; return true; } - else if (strcasecmp(pArg, "-comp_level") == 0) + else if (opt_match(pArg, "-comp_level")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_etc1s_compression_level = atoi(arg_v[arg_index + 1]); @@ -794,7 +1073,7 @@ class command_line_params arg_count++; return true; } - else if (strcasecmp(pArg, "-max_endpoints") == 0) + else if (opt_match(pArg, "-max_endpoints")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_etc1s_max_endpoint_clusters = clamp(atoi(arg_v[arg_index + 1]), 1, BASISU_MAX_ENDPOINT_CLUSTERS); @@ -804,7 +1083,7 @@ class command_line_params arg_count++; return true; } - else if (strcasecmp(pArg, "-max_selectors") == 0) + else if (opt_match(pArg, "-max_selectors")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_etc1s_max_selector_clusters = clamp(atoi(arg_v[arg_index + 1]), 1, BASISU_MAX_SELECTOR_CLUSTERS); @@ -815,30 +1094,30 @@ class command_line_params return true; } #if 0 - else if (strcasecmp(pArg, "-gen_global_codebooks") == 0) + else if (opt_match(pArg, "-gen_global_codebooks")) { // TODO } #endif - else if (strcasecmp(pArg, "-use_global_codebooks") == 0) + else if (opt_match(pArg, "-use_global_codebooks")) { REMAINING_ARGS_CHECK(1); m_etc1s_use_global_codebooks_file = arg_v[arg_index + 1]; arg_count++; return true; } - else if (strcasecmp(pArg, "-etc1_only") == 0) + else if (opt_match(pArg, "-etc1_only")) { m_etc1_only = true; m_unpack_format_only = (int)basist::transcoder_texture_format::cTFETC1_RGB; return true; } - else if (strcasecmp(pArg, "-disable_hierarchical_endpoint_codebooks") == 0) + else if (opt_match(pArg, "-disable_hierarchical_endpoint_codebooks")) { m_comp_params.m_disable_hierarchical_endpoint_codebooks = true; return true; } - else if (strcasecmp(pArg, "-q") == 0) // old-style -q, prefer -quality instead + else if (opt_match(pArg, "-q")) // old-style ETC1S -q X option, prefer -quality instead { REMAINING_ARGS_CHECK(1); m_comp_params.m_quality_level = clamp(atoi(arg_v[arg_index + 1]), BASISU_QUALITY_MIN, BASISU_QUALITY_MAX); @@ -848,24 +1127,24 @@ class command_line_params arg_count++; return true; } - else if (strcasecmp(pArg, "-no_selector_rdo") == 0) + else if (opt_match(pArg, "-no_selector_rdo")) { m_comp_params.m_no_selector_rdo = true; return true; } - else if (strcasecmp(pArg, "-selector_rdo_thresh") == 0) + else if (opt_match(pArg, "-selector_rdo_thresh")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_selector_rdo_thresh = (float)atof(arg_v[arg_index + 1]); arg_count++; return true; } - else if (strcasecmp(pArg, "-no_endpoint_rdo") == 0) + else if (opt_match(pArg, "-no_endpoint_rdo")) { m_comp_params.m_no_endpoint_rdo = true; return true; } - else if (strcasecmp(pArg, "-endpoint_rdo_thresh") == 0) + else if (opt_match(pArg, "-endpoint_rdo_thresh")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_endpoint_rdo_thresh = (float)atof(arg_v[arg_index + 1]); @@ -879,6 +1158,22 @@ class command_line_params public: command_line_params() : m_mode(cDefault), + m_benchmark_fmt(basist::basis_tex_format::cXUASTC_LDR_6x6), + m_benchmark_effort(0), + m_benchmark_quality(100), + m_benchmark_path("../test_files/kodim"), + m_benchmark_first(1), + m_benchmark_last(24), + m_benchmark_path_set(false), + m_benchmark_range_set(false), + m_benchmark_linear(false), + m_benchmark_xubc7_encoder(0), + m_benchmark_xuastc_profile(0), + m_benchmark_sweep_block_set(false), + m_benchmark_sweep_block_lo(0), m_benchmark_sweep_block_hi(0), + m_benchmark_sweep_effort_lo(3), m_benchmark_sweep_effort_hi(3), + m_benchmark_sweep_quality_lo(10), m_benchmark_sweep_quality_hi(10), + m_benchmark_sweep_csv("benchmark_sweep.csv"), m_ktx2_mode(true), m_ktx2_zstandard(true), m_ktx2_zstandard_level(6), @@ -897,14 +1192,27 @@ public: m_fuzz_testing(false), m_compare_ssim(false), m_compare_plot(false), + m_dump_image_stats(false), + m_dump_pixel_x(-1), + m_dump_pixel_y(-1), + m_dump_block_x(-1), + m_dump_block_y(-1), + m_stats_region_x(-1), + m_stats_region_y(-1), + m_stats_region_w(-1), + m_stats_region_h(-1), + m_extract_channel(-1), + m_extract_region_x(-1), + m_extract_region_y(-1), + m_extract_region_w(-1), + m_extract_region_h(-1), m_parallel_compression(false), m_tonemap_dither_flag(false), m_xuastc_ldr_disable_bc7_transcoding(false), m_no_etc1s_transcoding_chroma_filtering(false), m_higher_quality_transcoding(false), - m_force_deblocking(false), - m_disable_deblocking(false), - m_stronger_deblocking(false), + m_transcode_force_deblocking(false), + m_transcode_disable_deblocking(false), m_effort_level(-1), m_quality_level(-1), m_used_old_style_codec_config_param(false) @@ -930,7 +1238,7 @@ public: const int num_remaining_args = arg_c - (arg_index + 1); int arg_count = 1; - if ((strcasecmp(pArg, "-help") == 0) || (strcasecmp(pArg, "--help") == 0)) + if (opt_match(pArg, {"-help", "--help"})) { print_usage(); exit(EXIT_SUCCESS); @@ -948,308 +1256,676 @@ public: else if (check_for_general_options(arg_v, pArg, arg_index, num_remaining_args, arg_count)) { } - else if (strcasecmp(pArg, "-ktx2") == 0) + else if (check_for_dds_options(arg_v, pArg, arg_index, num_remaining_args, arg_count)) + { + } + else if (opt_match(pArg, "-ktx2")) { m_ktx2_mode = true; } - else if (strcasecmp(pArg, "-basis") == 0) + else if (opt_match(pArg, "-basis")) { m_ktx2_mode = false; } - else if (strcasecmp(pArg, "-ktx2_no_zstandard") == 0) + else if (opt_match(pArg, "-ktx2_no_zstandard")) { m_ktx2_zstandard = false; } - else if (strcasecmp(pArg, "-ktx2_zstandard_level") == 0) + else if (opt_match(pArg, "-ktx2_zstandard_level")) { REMAINING_ARGS_CHECK(1); m_ktx2_zstandard_level = atoi(arg_v[arg_index + 1]); arg_count++; } - else if (strcasecmp(pArg, "-ktx2_animdata_duration") == 0) + else if (opt_match(pArg, "-ktx2_animdata_duration")) { REMAINING_ARGS_CHECK(1); m_ktx2_animdata_duration = atoi(arg_v[arg_index + 1]); arg_count++; } - else if (strcasecmp(pArg, "-ktx2_animdata_timescale") == 0) + else if (opt_match(pArg, "-ktx2_animdata_timescale")) { REMAINING_ARGS_CHECK(1); m_ktx2_animdata_timescale = atoi(arg_v[arg_index + 1]); arg_count++; } - else if (strcasecmp(pArg, "-ktx2_animdata_loopcount") == 0) + else if (opt_match(pArg, "-ktx2_animdata_loopcount")) { REMAINING_ARGS_CHECK(1); m_ktx2_animdata_loopcount = atoi(arg_v[arg_index + 1]); arg_count++; } - else if (strcasecmp(pArg, "-compress") == 0) + else if (opt_match(pArg, "-compress")) m_mode = cCompress; - else if (strcasecmp(pArg, "-compare") == 0) + else if (opt_match(pArg, "-compare")) m_mode = cCompare; - else if ((strcasecmp(pArg, "-hdr_compare") == 0) || (strcasecmp(pArg, "-compare_hdr") == 0)) + else if (opt_match(pArg, "-compare_hvs")) + m_mode = cCompareHVS; + else if (opt_match(pArg, {"-hdr_compare", "-compare_hdr"})) m_mode = cHDRCompare; - else if (strcasecmp(pArg, "-split") == 0) + else if (opt_match(pArg, "-dump_image_stats")) + { + m_mode = cImageDumpStats; + m_dump_image_stats = true; + + // Optional region: -dump_image_stats . If the next argument looks like a + // number then all four must follow; otherwise the stats cover the whole image. + if ((num_remaining_args >= 1) && arg_is_unsigned_int(arg_v[arg_index + 1])) + { + REMAINING_ARGS_CHECK(4); + m_stats_region_x = atoi(arg_v[arg_index + 1]); + m_stats_region_y = atoi(arg_v[arg_index + 2]); + m_stats_region_w = atoi(arg_v[arg_index + 3]); + m_stats_region_h = atoi(arg_v[arg_index + 4]); + arg_count += 4; + if ((m_stats_region_x < 0) || (m_stats_region_y < 0) || (m_stats_region_w <= 0) || (m_stats_region_h <= 0)) + { + error_printf("-dump_image_stats region requires non-negative x,y and positive w,h: -dump_image_stats \n"); + return false; + } + } + } + else if (opt_match(pArg, "-dump_pixel")) + { + REMAINING_ARGS_CHECK(2); + m_mode = cImageDumpStats; + m_dump_pixel_x = atoi(arg_v[arg_index + 1]); + m_dump_pixel_y = atoi(arg_v[arg_index + 2]); + arg_count += 2; + if ((m_dump_pixel_x < 0) || (m_dump_pixel_y < 0)) + { + error_printf("-dump_pixel requires two non-negative integer coordinates: -dump_pixel \n"); + return false; + } + } + else if (opt_match(pArg, "-dump_block")) + { + REMAINING_ARGS_CHECK(2); + m_mode = cImageDumpStats; + m_dump_block_x = atoi(arg_v[arg_index + 1]); + m_dump_block_y = atoi(arg_v[arg_index + 2]); + arg_count += 2; + if ((m_dump_block_x < 0) || (m_dump_block_y < 0)) + { + error_printf("-dump_block requires two non-negative integer block coordinates: -dump_block \n"); + return false; + } + } + else if (opt_match(pArg, "-split")) m_mode = cSplitImage; - else if (strcasecmp(pArg, "-combine") == 0) + else if (opt_match(pArg, "-combine")) m_mode = cCombineImages; - else if (strcasecmp(pArg, "-tonemap") == 0) + else if (opt_match(pArg, "-extract_channel")) + { + REMAINING_ARGS_CHECK(2); + m_mode = cExtractChannel; + + // Channel: r/g/b/a (case-insensitive) or 0..3. Always assign (don't leave a stale value from a prior -extract_channel). + const char* pCh = arg_v[arg_index + 1]; + m_extract_channel = ((pCh[0] != '\0') && (pCh[1] == '\0')) ? channel_token_to_index(pCh[0]) : -1; + if (m_extract_channel < 0) + { + error_printf("-extract_channel requires a channel (r/g/b/a or 0..3): -extract_channel [x y w h]\n"); + return false; + } + + m_extract_filename = arg_v[arg_index + 2]; + arg_count += 2; + + // Optional region: -extract_channel . + if ((num_remaining_args >= 3) && arg_is_unsigned_int(arg_v[arg_index + 3])) + { + REMAINING_ARGS_CHECK(6); + m_extract_region_x = atoi(arg_v[arg_index + 3]); + m_extract_region_y = atoi(arg_v[arg_index + 4]); + m_extract_region_w = atoi(arg_v[arg_index + 5]); + m_extract_region_h = atoi(arg_v[arg_index + 6]); + arg_count += 4; + if ((m_extract_region_x < 0) || (m_extract_region_y < 0) || (m_extract_region_w <= 0) || (m_extract_region_h <= 0)) + { + error_printf("-extract_channel region requires non-negative x,y and positive w,h: -extract_channel \n"); + return false; + } + } + } + else if (opt_match(pArg, "-extract_swizzle")) + { + REMAINING_ARGS_CHECK(2); + m_mode = cExtractSwizzle; + + // Swizzle: comma-separated source channels for output R,G,B,A, e.g. "0,1,2,3" (identity) or "r,g,b,a" or "2,1,0". + // Each token selects a source channel in [0,3]; 1..4 tokens. Any output channel not listed passes through unchanged. + const char* p = arg_v[arg_index + 1]; + int count = 0; + bool swizzle_ok = true; + while (*p) + { + const int idx = channel_token_to_index(*p); + if ((idx < 0) || (count >= 4)) + { + swizzle_ok = false; + break; + } + m_extract_swizzle[count++] = idx; + p++; + if (*p == ',') + { + p++; + if (*p == '\0') { swizzle_ok = false; break; } // trailing comma + } + else if (*p != '\0') + { + swizzle_ok = false; // token longer than a single channel char + break; + } + } + if (!swizzle_ok || (count == 0)) + { + error_printf("-extract_swizzle requires 1 to 4 comma-separated source channels (each r/g/b/a or 0..3), e.g. \"0,1,2,3\"\n"); + return false; + } + m_extract_swizzle_count = count; + + m_extract_filename = arg_v[arg_index + 2]; + arg_count += 2; + + // Optional region: -extract_swizzle . + if ((num_remaining_args >= 3) && arg_is_unsigned_int(arg_v[arg_index + 3])) + { + REMAINING_ARGS_CHECK(6); + m_extract_region_x = atoi(arg_v[arg_index + 3]); + m_extract_region_y = atoi(arg_v[arg_index + 4]); + m_extract_region_w = atoi(arg_v[arg_index + 5]); + m_extract_region_h = atoi(arg_v[arg_index + 6]); + arg_count += 4; + if ((m_extract_region_x < 0) || (m_extract_region_y < 0) || (m_extract_region_w <= 0) || (m_extract_region_h <= 0)) + { + error_printf("-extract_swizzle region requires non-negative x,y and positive w,h: -extract_swizzle \n"); + return false; + } + } + } + else if (opt_match(pArg, "-extract_region")) + { + REMAINING_ARGS_CHECK(5); + m_mode = cExtractRegion; + m_extract_filename = arg_v[arg_index + 1]; + m_extract_region_x = atoi(arg_v[arg_index + 2]); + m_extract_region_y = atoi(arg_v[arg_index + 3]); + m_extract_region_w = atoi(arg_v[arg_index + 4]); + m_extract_region_h = atoi(arg_v[arg_index + 5]); + arg_count += 5; + if ((m_extract_region_x < 0) || (m_extract_region_y < 0) || (m_extract_region_w <= 0) || (m_extract_region_h <= 0)) + { + error_printf("-extract_region requires non-negative x,y and positive w,h: -extract_region \n"); + return false; + } + } + // Optional color transforms applied by the -extract_* modes (modifiers; order vs. the -extract_* option does not matter). + else if (opt_match(pArg, "-extract_to_linear")) + m_extract_color_xform = cExtractXformToLinear; + else if (opt_match(pArg, "-extract_to_srgb")) + m_extract_color_xform = cExtractXformToSRGB; + else if (opt_match(pArg, "-extract_xform_alpha")) + m_extract_xform_alpha = true; + else if (opt_match(pArg, "-text_to_png")) + { + REMAINING_ARGS_CHECK(2); + m_mode = cTextToPng; + m_text_image_in = arg_v[arg_index + 1]; + m_text_image_out = arg_v[arg_index + 2]; + arg_count += 2; + } + else if (opt_match(pArg, "-png_to_text")) + { + REMAINING_ARGS_CHECK(2); + m_mode = cPngToText; + m_text_image_in = arg_v[arg_index + 1]; + m_text_image_out = arg_v[arg_index + 2]; + arg_count += 2; + } + else if (opt_match(pArg, "-tonemap")) m_mode = cTonemapImage; - else if (strcasecmp(pArg, "-unpack") == 0) + else if (opt_match(pArg, "-export_dds")) + { + REMAINING_ARGS_CHECK(1); + m_mode = cExportDDS; + m_export_dds_format = arg_v[arg_index + 1]; + arg_count++; + } + else if (opt_match(pArg, "-export_ktx")) + { + REMAINING_ARGS_CHECK(1); + m_mode = cExportKTX; + m_export_ktx_format = arg_v[arg_index + 1]; + arg_count++; + } + // -benchmark_single : developer codec_benchmark() single run (not in help text yet). + // FORMAT is any codec name with underscores, e.g. XUASTC_LDR_4x4..12x12, XUBC7, ETC1S, ASTC_LDR_6x6, UASTC_LDR_4x4. + else if (opt_match(pArg, "-benchmark_single")) + { + REMAINING_ARGS_CHECK(3); + m_mode = cBenchmarkSingle; + if (!parse_benchmark_tex_format(arg_v[arg_index + 1], m_benchmark_fmt)) + { + error_printf("-benchmark_single: unrecognized format \"%s\"\n", arg_v[arg_index + 1]); + return false; + } + // Clamp to the same ranges as -effort [0,10] and -quality [1,100] (matches lines above); also keeps the cast non-negative. + m_benchmark_effort = (uint32_t)clamp(atoi(arg_v[arg_index + 2]), 0, 10); + m_benchmark_quality = (uint32_t)clamp(atoi(arg_v[arg_index + 3]), 1, 100); + arg_count += 3; + } + // Optional overrides for -benchmark_single's base path and file index range (defaults: "../test_files/kodim", 1..24). + else if (opt_match(pArg, "-benchmark_path")) + { + REMAINING_ARGS_CHECK(1); + m_benchmark_path = arg_v[arg_index + 1]; + m_benchmark_path_set = true; + arg_count++; + } + else if (opt_match(pArg, "-benchmark_range")) + { + REMAINING_ARGS_CHECK(2); + m_benchmark_first = (uint32_t)clamp(atoi(arg_v[arg_index + 1]), 0, 1000000); + m_benchmark_last = (uint32_t)clamp(atoi(arg_v[arg_index + 2]), 0, 1000000); + if (m_benchmark_last < m_benchmark_first) // keep first<=last so codec_benchmark's (last-first+1) can't underflow + std::swap(m_benchmark_first, m_benchmark_last); + m_benchmark_range_set = true; + arg_count += 2; + } + else if (opt_match(pArg, "-benchmark_linear")) + m_benchmark_linear = true; + // XUBC7-only BC7 base encoder: 0 = bc7f (default), 1-7 = bc7e_scalar level 0-6 (ignored for non-XUBC7 formats). + else if (opt_match(pArg, "-benchmark_xubc7_encoder")) + { + REMAINING_ARGS_CHECK(1); + m_benchmark_xubc7_encoder = (uint32_t)clamp(atoi(arg_v[arg_index + 1]), 0, (int)basisu::cFlagXUBC7BaseEncoderMask); + arg_count++; + } + // XUASTC LDR entropy syntax/profile: 0 = full arith (default), 1 = hybrid, 2 = full zstd (ignored for non-XUASTC formats). + else if (opt_match(pArg, "-benchmark_xuastc_profile")) + { + REMAINING_ARGS_CHECK(1); + m_benchmark_xuastc_profile = (uint32_t)clamp(atoi(arg_v[arg_index + 1]), 0, (int)basist::astc_ldr_t::xuastc_ldr_syntax::cTotal - 1); + arg_count++; + } + // -benchmark_sweep : sweep block size/effort/quality for one codec, dumping a CSV (see benchmark_sweep_mode()). + else if (opt_match(pArg, "-benchmark_sweep")) + { + REMAINING_ARGS_CHECK(1); + m_mode = cBenchmarkSweep; + if (!parse_benchmark_tex_format(arg_v[arg_index + 1], m_benchmark_fmt)) + { + error_printf("-benchmark_sweep: unrecognized format \"%s\"\n", arg_v[arg_index + 1]); + return false; + } + arg_count++; + } + // -benchmark_sweep axis ranges (inclusive low/high). Block index [0,5] (4x4..12x12 squares); effort [0,10]; quality [0,10] (x10 -> [1,100]). + else if (opt_match(pArg, "-benchmark_sweep_block")) + { + REMAINING_ARGS_CHECK(2); + m_benchmark_sweep_block_set = true; + m_benchmark_sweep_block_lo = (uint32_t)clamp(atoi(arg_v[arg_index + 1]), 0, 5); + m_benchmark_sweep_block_hi = (uint32_t)clamp(atoi(arg_v[arg_index + 2]), 0, 5); + arg_count += 2; + } + else if (opt_match(pArg, "-benchmark_sweep_effort")) + { + REMAINING_ARGS_CHECK(2); + m_benchmark_sweep_effort_lo = (uint32_t)clamp(atoi(arg_v[arg_index + 1]), 0, 10); + m_benchmark_sweep_effort_hi = (uint32_t)clamp(atoi(arg_v[arg_index + 2]), 0, 10); + arg_count += 2; + } + else if (opt_match(pArg, "-benchmark_sweep_quality")) + { + REMAINING_ARGS_CHECK(2); + m_benchmark_sweep_quality_lo = (uint32_t)clamp(atoi(arg_v[arg_index + 1]), 0, 10); + m_benchmark_sweep_quality_hi = (uint32_t)clamp(atoi(arg_v[arg_index + 2]), 0, 10); + arg_count += 2; + } + else if (opt_match(pArg, "-benchmark_sweep_csv")) + { + REMAINING_ARGS_CHECK(1); + m_benchmark_sweep_csv = arg_v[arg_index + 1]; + arg_count++; + } + else if (opt_match(pArg, "-tinydds_info")) // opt_match also accepts the "--tinydds_info" form + m_mode = cTinyDDSInfo; + else if (opt_match(pArg, "-ktx_info")) + m_mode = cKTXInfo; + else if (opt_match(pArg, "-unpack")) m_mode = cUnpack; - else if (strcasecmp(pArg, "-validate") == 0) + else if (opt_match(pArg, "-validate")) m_mode = cValidate; - else if (strcasecmp(pArg, "-info") == 0) + else if (opt_match(pArg, "-info")) m_mode = cInfo; - else if ((strcasecmp(pArg, "-version") == 0) || (strcasecmp(pArg, "--version") == 0)) + else if (opt_match(pArg, {"-version", "--version"})) m_mode = cVersion; - else if (strcasecmp(pArg, "-compare_ssim") == 0) + else if (opt_match(pArg, "-compare_ssim")) m_compare_ssim = true; - else if (strcasecmp(pArg, "-compare_plot") == 0) + else if (opt_match(pArg, "-compare_plot")) m_compare_plot = true; - else if (strcasecmp(pArg, "-bench") == 0) + else if (opt_match(pArg, "-bench")) m_mode = cBench; - else if (strcasecmp(pArg, "-comp_size") == 0) + else if (opt_match(pArg, "-comp_size")) m_mode = cCompSize; - else if ((strcasecmp(pArg, "-test") == 0) || (strcasecmp(pArg, "-test_ldr") == 0)) + else if (opt_match(pArg, {"-test", "-test_ldr"})) m_mode = cTestLDR; - else if ((strcasecmp(pArg, "-test_xuastc") == 0) || (strcasecmp(pArg, "-test_xuastc_ldr") == 0)) + else if (opt_match(pArg, {"-test_xuastc", "-test_xuastc_ldr"})) m_mode = cTestXUASTCLDR; - else if (strcasecmp(pArg, "-test_hdr_4x4") == 0) + else if (opt_match(pArg, {"-test_xuastc_dump", "-test_xuastc_ldr_dump"})) + { + // Run the XUASTC LDR test across all block sizes and print the expected-value + // tables (for copy/paste into the code) instead of validating. + m_mode = cTestXUASTCLDR; + m_xuastc_test_dump = true; + } + else if (opt_match(pArg, "-test_hdr_4x4")) m_mode = cTestHDR_4x4; - else if (strcasecmp(pArg, "-test_hdr_6x6") == 0) + else if (opt_match(pArg, "-test_hdr_6x6")) m_mode = cTestHDR_6x6; - else if (strcasecmp(pArg, "-test_hdr_6x6i") == 0) + else if (opt_match(pArg, "-test_hdr_6x6i")) m_mode = cTestHDR_6x6i; - else if (strcasecmp(pArg, "-clbench") == 0) + else if (opt_match(pArg, "-test_codecs")) + { + // Sweep all LDR+HDR codecs vs the golden .inl table. Optional trailing codec + // filter (not beginning with '-'), e.g. "-test_codecs XUASTC". + m_mode = cTestCodecs; + if ((num_remaining_args >= 1) && (arg_v[arg_index + 1][0] != '-')) + { + m_codec_filter = std::string(arg_v[arg_index + 1]); + arg_count++; + } + } + else if (opt_match(pArg, "-test_codecs_gen")) + { + // (Re)generate the golden .inl table. Optional trailing output filename, then an + // optional codec filter, both not beginning with '-'. + m_mode = cTestCodecsGen; + if ((num_remaining_args >= 1) && (arg_v[arg_index + 1][0] != '-')) + { + m_test_codecs_gen_filename = std::string(arg_v[arg_index + 1]); + arg_count++; + + if ((num_remaining_args >= 2) && (arg_v[arg_index + 2][0] != '-')) + { + m_codec_filter = std::string(arg_v[arg_index + 2]); + arg_count++; + } + } + } + else if (opt_match(pArg, "-clbench")) m_mode = cCLBench; - else if (strcasecmp(pArg, "-test_dir") == 0) + else if (opt_match(pArg, "-test_dir")) { REMAINING_ARGS_CHECK(1); m_test_file_dir = std::string(arg_v[arg_index + 1]); + m_test_dir_explicit = true; arg_count++; } - else if (strcasecmp(pArg, "-no_sse") == 0) + else if (opt_match(pArg, "-no_sse")) { #if BASISU_SUPPORT_SSE g_cpu_supports_sse41 = false; #endif } - else if ((strcasecmp(pArg, "-no_status_output") == 0) || (strcasecmp(pArg, "-quiet") == 0)) + else if (opt_match(pArg, {"-no_status_output", "-quiet"})) { m_comp_params.m_status_output = false; } - else if (strcasecmp(pArg, "-file") == 0) + else if (opt_match(pArg, "-file")) { REMAINING_ARGS_CHECK(1); m_input_filenames.push_back(std::string(arg_v[arg_index + 1])); arg_count++; } - else if (strcasecmp(pArg, "-alpha_file") == 0) + else if (opt_match(pArg, "-alpha_file")) { REMAINING_ARGS_CHECK(1); m_input_alpha_filenames.push_back(std::string(arg_v[arg_index + 1])); arg_count++; } - else if (strcasecmp(pArg, "-multifile_printf") == 0) + else if (opt_match(pArg, "-multifile_printf")) { REMAINING_ARGS_CHECK(1); m_multifile_printf = std::string(arg_v[arg_index + 1]); arg_count++; } - else if (strcasecmp(pArg, "-multifile_first") == 0) + else if (opt_match(pArg, "-multifile_first")) { REMAINING_ARGS_CHECK(1); m_multifile_first = atoi(arg_v[arg_index + 1]); arg_count++; } - else if (strcasecmp(pArg, "-multifile_num") == 0) + else if (opt_match(pArg, "-multifile_num")) { REMAINING_ARGS_CHECK(1); m_multifile_num = atoi(arg_v[arg_index + 1]); arg_count++; } - else if (strcasecmp(pArg, "-resample") == 0) + else if (opt_match(pArg, "-resample")) { REMAINING_ARGS_CHECK(2); m_comp_params.m_resample_width = atoi(arg_v[arg_index + 1]); m_comp_params.m_resample_height = atoi(arg_v[arg_index + 2]); arg_count += 2; } - else if (strcasecmp(pArg, "-resample_factor") == 0) + else if (opt_match(pArg, "-resample_factor")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_resample_factor = (float)atof(arg_v[arg_index + 1]); arg_count++; } - else if (strcasecmp(pArg, "-linear") == 0) - { - m_comp_params.m_perceptual = false; - m_comp_params.m_ktx2_and_basis_srgb_transfer_function = false; - } - else if (strcasecmp(pArg, "-srgb") == 0) - { - m_comp_params.m_perceptual = true; - m_comp_params.m_ktx2_and_basis_srgb_transfer_function = true; - } - else if (strcasecmp(pArg, "-output_file") == 0) + else if (opt_match(pArg, { "-output_file", "-out_file" })) { REMAINING_ARGS_CHECK(1); m_output_filename = arg_v[arg_index + 1]; arg_count++; } - else if (strcasecmp(pArg, "-output_path") == 0) + else if (opt_match(pArg, { "-output_path", "-out_path" } )) { REMAINING_ARGS_CHECK(1); m_output_path = arg_v[arg_index + 1]; arg_count++; } - else if ((strcasecmp(pArg, "-debug") == 0) || (strcasecmp(pArg, "-verbose") == 0)) + else if (opt_match(pArg, "-debug")) { m_comp_params.m_debug = true; enable_debug_printf(true); } - else if (strcasecmp(pArg, "-validate_output") == 0) + else if (opt_match(pArg, "-verbose")) + { + // -verbose is shorthand for -debug -stats + m_comp_params.m_debug = true; + enable_debug_printf(true); + m_comp_params.m_compute_stats = true; + m_comp_params.m_psnr_hvs_m_stats = true; + } + else if (opt_match(pArg, "-validate_output")) { m_comp_params.m_validate_output_data = true; } - else if (strcasecmp(pArg, "-debug_images") == 0) + else if (opt_match(pArg, "-debug_images")) m_comp_params.m_debug_images = true; - else if (strcasecmp(pArg, "-stats") == 0) - m_comp_params.m_compute_stats = true; - else if (strcasecmp(pArg, "-y_flip") == 0) - m_comp_params.m_y_flip = true; - else if (strcasecmp(pArg, "-normal_map") == 0) + else if (opt_match(pArg, {"-stats", "-image_stats"})) { - m_comp_params.m_perceptual = false; - m_comp_params.m_ktx2_and_basis_srgb_transfer_function = false; - m_comp_params.m_mip_srgb = false; + m_comp_params.m_compute_stats = true; + m_comp_params.m_psnr_hvs_m_stats = true; + } + else if (opt_match(pArg, "-no_stats")) + { + // Turn stats back off (e.g. to override -verbose or -csv_file, which enable them). + m_comp_params.m_compute_stats = false; + m_comp_params.m_psnr_hvs_m_stats = false; + } + else if (opt_match(pArg, "-y_flip")) + m_comp_params.m_y_flip = true; + else if (opt_match(pArg, {"-normal_map", "-texture"})) + { + // Normal map/plain texture preset - disable all fancy things, linear weights, no sharpening largest blocks, no deblocking awareness. + m_comp_params.set_srgb_options(false); + m_comp_params.m_no_selector_rdo = true; m_comp_params.m_no_endpoint_rdo = true; + + m_comp_params.set_xuastc_ldr_srgb_channel_weights(false); + + m_comp_params.m_xuastc_ldr_sharpen_mode = (int)xuastc_ldr_sharpen_mode::cDisabled; + m_comp_params.m_xuastc_ldr_deblocking_mode = (int)xuastc_ldr_deblocking_mode::cDisabled; + + // TODO: Once we support some sort of angular metric, enable them for -normal_map. } - else if (strcasecmp(pArg, "-no_alpha") == 0) + else if (opt_match(pArg, "-photo")) + { + // Photo preset (the codec's defaults, opposite of -normal_map/-texture) + m_comp_params.set_srgb_options(true); + + // -normal_map sets these to true, this preset sets them to false + m_comp_params.m_no_selector_rdo = false; + m_comp_params.m_no_endpoint_rdo = false; + + m_comp_params.set_xuastc_ldr_srgb_channel_weights(true); + + m_comp_params.m_xuastc_ldr_sharpen_mode = (int)xuastc_ldr_sharpen_mode::cDisabled; + m_comp_params.m_xuastc_ldr_deblocking_mode = (int)xuastc_ldr_deblocking_mode::cUseSCDAndFilteringOnlyLargestBlocks; + } + else if (opt_match(pArg, "-linear")) + { + // linear preset (opposite of -srgb) + m_comp_params.set_srgb_options(false); + + m_comp_params.set_xuastc_ldr_srgb_channel_weights(false); + } + else if (opt_match(pArg, "-srgb")) + { + // sRGB preset (opposite of -linear) + m_comp_params.set_srgb_options(true); + + m_comp_params.set_xuastc_ldr_srgb_channel_weights(true); + } + else if (opt_match(pArg, "-no_alpha")) m_comp_params.m_check_for_alpha = false; - else if (strcasecmp(pArg, "-force_alpha") == 0) + else if (opt_match(pArg, "-force_alpha")) m_comp_params.m_force_alpha = true; - else if ((strcasecmp(pArg, "-separate_rg_to_color_alpha") == 0) || - (strcasecmp(pArg, "-seperate_rg_to_color_alpha") == 0)) // was mispelled for a while - whoops! + else if (opt_match(pArg, {"-separate_rg_to_color_alpha", "-seperate_rg_to_color_alpha"})) // was mispelled for a while - whoops! { m_comp_params.m_swizzle[0] = 0; m_comp_params.m_swizzle[1] = 0; m_comp_params.m_swizzle[2] = 0; m_comp_params.m_swizzle[3] = 1; } - else if (strcasecmp(pArg, "-swizzle") == 0) + else if (opt_match(pArg, "-swizzle")) { REMAINING_ARGS_CHECK(1); const char *swizzle = arg_v[arg_index + 1]; if (strlen(swizzle) != 4) { - error_printf("Swizzle requires exactly 4 characters\n"); + error_printf("Swizzle requires exactly 4 characters (each one of [rgba] or [0123])\n"); return false; } for (int i=0; i<4; ++i) { - if (swizzle[i] == 'r') + if ((tolower(swizzle[i]) == 'r') || (swizzle[i] == '0')) m_comp_params.m_swizzle[i] = 0; - else if (swizzle[i] == 'g') + else if ((tolower(swizzle[i]) == 'g') || (swizzle[i] == '1')) m_comp_params.m_swizzle[i] = 1; - else if (swizzle[i] == 'b') + else if ((tolower(swizzle[i]) == 'b') || (swizzle[i] == '2')) m_comp_params.m_swizzle[i] = 2; - else if (swizzle[i] == 'a') + else if ((tolower(swizzle[i]) == 'a') || (swizzle[i] == '3')) m_comp_params.m_swizzle[i] = 3; else { - error_printf("Swizzle must be one of [rgba]"); + error_printf("Swizzle must be one of [rgba] or [0123]"); return false; } } arg_count++; } - else if (strcasecmp(pArg, "-renorm") == 0) + else if (opt_match(pArg, "-renorm")) m_comp_params.m_renormalize = true; - else if ((strcasecmp(pArg, "-no_multithreading") == 0) || (strcasecmp(pArg, "-no_threading") == 0)) + else if (opt_match(pArg, {"-no_multithreading", "-no_threading", "-no_threads"})) { m_comp_params.m_multithreading = false; } - else if (strcasecmp(pArg, "-parallel") == 0) + else if (opt_match(pArg, "-parallel")) { m_parallel_compression = true; } - else if (strcasecmp(pArg, "-max_threads") == 0) + else if (opt_match(pArg, "-max_threads")) { REMAINING_ARGS_CHECK(1); m_max_threads = maximum(1, atoi(arg_v[arg_index + 1])); arg_count++; } - else if (strcasecmp(pArg, "-mipmap") == 0) + else if (opt_match(pArg, "-mipmap")) m_comp_params.m_mip_gen = true; - else if (strcasecmp(pArg, "-no_ktx") == 0) + else if (opt_match(pArg, {"-no_ktx", "-no_dds"})) m_no_ktx = true; - else if (strcasecmp(pArg, "-ktx_only") == 0) + else if (opt_match(pArg, {"-ktx_only", "-dds_only"})) m_ktx_only = true; - else if (strcasecmp(pArg, "-write_out") == 0) + else if (opt_match(pArg, "-write_out")) m_write_out = true; - else if (strcasecmp(pArg, "-tonemap_dither") == 0) + else if (opt_match(pArg, "-tonemap_dither")) m_tonemap_dither_flag = true; - else if (strcasecmp(pArg, "-format_only") == 0) + else if (opt_match(pArg, "-format_only")) { REMAINING_ARGS_CHECK(1); m_unpack_format_only = atoi(arg_v[arg_index + 1]); arg_count++; } - else if (strcasecmp(pArg, "-opencl") == 0) + else if (opt_match(pArg, "-opencl")) { m_comp_params.m_use_opencl = true; } - else if (strcasecmp(pArg, "-opencl_serialize") == 0) + else if (opt_match(pArg, "-opencl_serialize")) { } - else if (strcasecmp(pArg, "-mip_scale") == 0) + else if (opt_match(pArg, "-mip_scale")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_mip_scale = (float)atof(arg_v[arg_index + 1]); arg_count++; } - else if (strcasecmp(pArg, "-mip_filter") == 0) + else if (opt_match(pArg, "-mip_filter")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_mip_filter = arg_v[arg_index + 1]; // TODO: Check filter arg_count++; } - else if (strcasecmp(pArg, "-mip_renorm") == 0) + else if (opt_match(pArg, "-mip_renorm")) m_comp_params.m_mip_renormalize = true; - else if (strcasecmp(pArg, "-mip_clamp") == 0) + else if (opt_match(pArg, "-mip_clamp")) m_comp_params.m_mip_wrapping = false; - else if (strcasecmp(pArg, "-mip_fast") == 0) + else if (opt_match(pArg, "-mip_fast")) m_comp_params.m_mip_fast = true; - else if (strcasecmp(pArg, "-mip_slow") == 0) + else if (opt_match(pArg, "-mip_slow")) m_comp_params.m_mip_fast = false; - else if (strcasecmp(pArg, "-mip_smallest") == 0) + else if (opt_match(pArg, "-mip_smallest")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_mip_smallest_dimension = atoi(arg_v[arg_index + 1]); arg_count++; } - else if (strcasecmp(pArg, "-mip_srgb") == 0) + else if (opt_match(pArg, "-mip_srgb")) m_comp_params.m_mip_srgb = true; - else if (strcasecmp(pArg, "-mip_linear") == 0) + else if (opt_match(pArg, "-mip_linear")) m_comp_params.m_mip_srgb = false; - else if (strcasecmp(pArg, "-userdata0") == 0) + else if (opt_match(pArg, "-userdata0")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_userdata0 = atoi(arg_v[arg_index + 1]); arg_count++; } - else if (strcasecmp(pArg, "-userdata1") == 0) + else if (opt_match(pArg, "-userdata1")) { REMAINING_ARGS_CHECK(1); m_comp_params.m_userdata1 = atoi(arg_v[arg_index + 1]); arg_count++; } - else if (strcasecmp(pArg, "-framerate") == 0) + else if (opt_match(pArg, "-framerate")) { REMAINING_ARGS_CHECK(1); double fps = atof(arg_v[arg_index + 1]); @@ -1260,12 +1936,12 @@ public: m_comp_params.m_us_per_frame = clamp(static_cast(us_per_frame + .5f), 0, basist::cBASISMaxUSPerFrame); arg_count++; } - else if (strcasecmp(pArg, "-cubemap") == 0) + else if (opt_match(pArg, "-cubemap")) { m_comp_params.m_tex_type = basist::cBASISTexTypeCubemapArray; m_individual = false; } - else if (strcasecmp(pArg, "-tex_type") == 0) + else if (opt_match(pArg, "-tex_type")) { REMAINING_ARGS_CHECK(1); const char* pType = arg_v[arg_index + 1]; @@ -1299,17 +1975,18 @@ public: } arg_count++; } - else if (strcasecmp(pArg, "-individual") == 0) + else if (opt_match(pArg, "-individual")) m_individual = true; - else if ((strcasecmp(pArg, "-tex_array") == 0) || (strcasecmp(pArg, "-texarray") == 0)) + else if (opt_match(pArg, {"-tex_array", "-texarray"})) m_individual = false; - else if (strcasecmp(pArg, "-fuzz_testing") == 0) + else if (opt_match(pArg, "-fuzz_testing")) m_fuzz_testing = true; - else if (strcasecmp(pArg, "-csv_file") == 0) + else if (opt_match(pArg, "-csv_file")) { REMAINING_ARGS_CHECK(1); m_csv_file = arg_v[arg_index + 1]; m_comp_params.m_compute_stats = true; + m_comp_params.m_psnr_hvs_m_stats = true; arg_count++; } @@ -1328,31 +2005,47 @@ public: arg_index += arg_count; assert(arg_index <= arg_c); } + + const bool lossy_supercompression_changed = m_comp_params.m_xuastc_ldr_use_lossy_supercompression.was_changed(); + const bool lossy_supercompression_value = m_comp_params.m_xuastc_ldr_use_lossy_supercompression; - if (m_comp_params.m_quality_level != -1) // old-style -q X option + if (m_comp_params.m_quality_level != -1) // they set the old-style "-q X" option, which is the legacy ETC1S-specific quality level which ranges from [1,255] { m_comp_params.m_etc1s_max_endpoint_clusters = 0; m_comp_params.m_etc1s_max_selector_clusters = 0; - - // -q also controls XUASTC LDR weight grid DCT quality level - m_comp_params.m_xuastc_ldr_use_dct = true; + + if (basis_tex_format_is_xuastc_ldr(m_comp_params.get_format_mode())) + { + printf("WARNING: Using \"-q [1,255]\" is not recommended for XUASTC LDR, instead use \"-quality [1,100]\"\n"); - // Automatically enable lossy XUASTC supercompression if DCT is enabled. - if (!m_comp_params.m_xuastc_ldr_use_lossy_supercompression.was_changed()) - m_comp_params.m_xuastc_ldr_use_lossy_supercompression = true; + // -q also controls XUASTC LDR/XUBC7 weight grid DCT quality level + if (m_comp_params.m_quality_level < 100) + m_comp_params.m_xuastc_ldr_use_dct = true; + + // Automatically enable lossy XUASTC supercompression if DCT is enabled. + if ((!lossy_supercompression_changed) && (m_comp_params.m_quality_level < 100)) + m_comp_params.m_xuastc_ldr_use_lossy_supercompression = true; + } + else if (basis_tex_format_is_xubc7(m_comp_params.get_format_mode())) + { + printf("WARNING: Using \"-q [1,255]\" is not recommended for XUBC7, instead use \"-quality [1,100]\"\n"); + } } else if ((!m_comp_params.m_etc1s_max_endpoint_clusters) || (!m_comp_params.m_etc1s_max_selector_clusters)) { m_comp_params.m_etc1s_max_endpoint_clusters = 0; m_comp_params.m_etc1s_max_selector_clusters = 0; - m_comp_params.m_quality_level = 128; + // Don't slam to 128 unless it's ETC1S + if (m_comp_params.get_format_mode() == basist::basis_tex_format::cETC1S) + m_comp_params.m_quality_level = 128; } // Ensure mip_srgb is set to match the perceptual flag if the user didn't explicitly set it. if (!m_comp_params.m_mip_srgb.was_changed()) { // They didn't specify what colorspace to do mipmap filtering in, so choose sRGB if they've specified that the texture is sRGB. + // 4/26/2026: m_mip_srgb now defaults to true, unsure if this is needed now. if (m_comp_params.m_perceptual) m_comp_params.m_mip_srgb = true; else @@ -1369,10 +2062,7 @@ public: { fmt_printf("WARNING: Mixing old and new-style (-effort and/or -quality) codec configuration parameters.\nNew-style parameters may overwrite your old-style codec configuration settings.\nPrefer using -effort X and -quality X.\n"); } - - const bool lossy_supercompression_changed = m_comp_params.m_xuastc_ldr_use_lossy_supercompression.was_changed(); - const bool lossy_supercompression_value = m_comp_params.m_xuastc_ldr_use_lossy_supercompression; - + // Set the new-style effort/quality level, but importantly don't override any settings already changed if they haven't explictly specified -effort or -quality. m_comp_params.set_format_mode_and_quality_effort(m_comp_params.get_format_mode(), m_quality_level, m_effort_level, false); @@ -1420,7 +2110,31 @@ public: basis_compressor_params m_comp_params; tool_mode m_mode; - + + // -benchmark_single parameters (cBenchmarkSingle). + basist::basis_tex_format m_benchmark_fmt; + uint32_t m_benchmark_effort; + uint32_t m_benchmark_quality; + // Optional overrides for the codec_benchmark() base path and file index range: -benchmark_path, -benchmark_range. + // The _set flags record whether the user explicitly overrode them (so HDR can fall back to its own default set). + std::string m_benchmark_path; + uint32_t m_benchmark_first; + uint32_t m_benchmark_last; + bool m_benchmark_path_set; + bool m_benchmark_range_set; + // -benchmark_linear: use linear (not sRGB/perceptual) metrics for the benchmark. Default is sRGB. + bool m_benchmark_linear; + // -benchmark_xubc7_encoder [0,7]: XUBC7-only BC7 base encoder selection. 0 = bc7f (default), 1-7 = bc7e_scalar level 0-6. + uint32_t m_benchmark_xubc7_encoder; + // -benchmark_xuastc_profile [0,2]: XUASTC LDR entropy syntax/profile. 0 = full arith (default), 1 = hybrid, 2 = full zstd. + uint32_t m_benchmark_xuastc_profile; + // -benchmark_sweep axis ranges (cBenchmarkSweep). Block index [0,5] (4x4..12x12 squares), effort [0,10], quality [0,10] (x10 -> [1,100]). + bool m_benchmark_sweep_block_set; + uint32_t m_benchmark_sweep_block_lo, m_benchmark_sweep_block_hi; + uint32_t m_benchmark_sweep_effort_lo, m_benchmark_sweep_effort_hi; + uint32_t m_benchmark_sweep_quality_lo, m_benchmark_sweep_quality_hi; + std::string m_benchmark_sweep_csv; + bool m_ktx2_mode; bool m_ktx2_zstandard; int m_ktx2_zstandard_level; @@ -1434,6 +2148,20 @@ public: std::string m_output_filename; std::string m_output_path; + // Target texture format string for -export_dds (e.g. "BC1", "BC7", "RGBA32"). + std::string m_export_dds_format; + + // Target output format string for -dds (e.g. "bc7", "a8r8g8b8"); see basisu_dds_export.h. + std::string m_dds_format; + + // -dds BC7 packer options (defaults mean "not specified" -> use the dds_export_params default). See basisu_dds_export.h. + dds_bc7_encoder m_dds_bc7_encoder = cDDSBC7Encoder_Default; // which BC7 packer + int m_dds_bc7f_level = -1; // 0 = analytical, 1 = partially analytical, 2 = non analytical (bc7f); -1 = unset + int m_dds_bc7e_scalar_level = -1; // 0..6 (bc7e_scalar); -1 = unset + + // Target texture format string for -export_ktx (e.g. "BC7", "ETC2", "ASTC_6x6"). + std::string m_export_ktx_format; + int m_unpack_format_only; std::string m_multifile_printf; @@ -1445,7 +2173,17 @@ public: std::string m_etc1s_use_global_codebooks_file; std::string m_test_file_dir; - + bool m_test_dir_explicit = false; // true if -test_dir was explicitly given (so -test_codecs can default to test_files) + + // When true, -test_xuastc runs the compressor across all block sizes and prints the + // expected-value tables (to copy/paste into the code) instead of validating. + bool m_xuastc_test_dump = false; + + // -test_codecs: optional codec name filter (token-boundary, case-insensitive), e.g. "ETC1S", "XUASTC", "ASTC", "HDR". + std::string m_codec_filter; + // -test_codecs_gen: output .inl path (defaults to "basisu_tool_test_codecs.inl"). + std::string m_test_codecs_gen_filename; + uint32_t m_max_threads; bool m_individual; @@ -1456,17 +2194,30 @@ public: bool m_fuzz_testing; bool m_compare_ssim; bool m_compare_plot; + + // -dump_image_stats / -dump_pixel / -dump_block (diagnostic image inspection; see image_stats_mode()) + bool m_dump_image_stats; + int m_dump_pixel_x, m_dump_pixel_y; // -1 == not requested + int m_dump_block_x, m_dump_block_y; // -1 == not requested (4x4 texel block coords) + int m_stats_region_x, m_stats_region_y, m_stats_region_w, m_stats_region_h; // -1 == stats cover the whole image; otherwise restrict to this rectangle + int m_extract_channel; // -extract_channel: 0=R,1=G,2=B,3=A; -1 == not requested + int m_extract_swizzle[4] = { 0, 1, 2, 3 }; // -extract_swizzle: source channel index feeding each output R/G/B/A (identity by default) + int m_extract_swizzle_count = -1; // number of swizzle tokens given (1..4); -1 == not requested. Output channels >= count pass through unchanged + std::string m_extract_filename; // output PNG for -extract_channel / -extract_swizzle / -extract_region + int m_extract_region_x, m_extract_region_y, m_extract_region_w, m_extract_region_h; // region rectangle; w<=0 means "whole image" (optional for -extract_channel/-extract_swizzle, required for -extract_region) + int m_extract_color_xform = cExtractXformNone; // optional sRGB<->linear transform applied to the extracted output + bool m_extract_xform_alpha = false; // default: transform RGB only (A passes through); true == also transform the alpha channel + std::string m_text_image_in, m_text_image_out; // -text_to_png / -png_to_text: input and output filenames (see basisu_text_image.h) bool m_parallel_compression; bool m_tonemap_dither_flag; bool m_xuastc_ldr_disable_bc7_transcoding; bool m_no_etc1s_transcoding_chroma_filtering; bool m_higher_quality_transcoding; - bool m_force_deblocking; - bool m_disable_deblocking; - bool m_stronger_deblocking; - + bool m_transcode_force_deblocking; + bool m_transcode_disable_deblocking; + int m_effort_level; - int m_quality_level; + int m_quality_level; // new-style -quality X bool m_used_old_style_codec_config_param; // true if the user has specified low-level or old-style codec configuration parameters }; @@ -1564,12 +2315,11 @@ static uint32_t get_transcode_flags_from_options(const command_line_params& opts { uint32_t transcode_flags = opts.m_higher_quality_transcoding ? basist::cDecodeFlagsHighQuality : 0; - if (opts.m_disable_deblocking) + if (opts.m_transcode_disable_deblocking) transcode_flags |= basist::cDecodeFlagsNoDeblockFiltering; - else if (opts.m_force_deblocking) + else if (opts.m_transcode_force_deblocking) transcode_flags |= basist::cDecodeFlagsForceDeblockFiltering; - if (opts.m_stronger_deblocking) - transcode_flags |= basist::cDecodeFlagsStrongerDeblockFiltering; + if (opts.m_no_etc1s_transcoding_chroma_filtering) transcode_flags |= basist::cDecodeFlagsNoETC1SChromaFiltering; if (opts.m_xuastc_ldr_disable_bc7_transcoding) @@ -1633,7 +2383,7 @@ static bool compress_mode(command_line_params &opts) { // Create KTXanimData key value entry // TODO: Move this to basisu_comp.h - basist::ktx2_transcoder::key_value kv; + basist::key_value kv; const char* pAD = "KTXanimData"; kv.m_key.resize(strlen(pAD) + 1); @@ -1647,7 +2397,7 @@ static bool compress_mode(command_line_params &opts) kv.m_value.resize(sizeof(ad)); memcpy(kv.m_value.data(), &ad, sizeof(ad)); - params.m_ktx2_key_values.push_back(kv); + params.m_key_values.push_back(kv); } // TODO- expose this to command line. @@ -2005,10 +2755,12 @@ static bool unpack_and_validate_ktx2_file( printf("Texture Array Size (layers): %u\n", dec.get_layers()); printf("Total Faces: %u (%s)\n", dec.get_faces(), (dec.get_faces() == 6) ? "CUBEMAP" : "2D"); printf("Is Texture Video: %u\n", dec.is_video()); - + if (dec.is_hdr()) fmt_printf("LDR to HDR upconversion nit multiplier: {}\n", dec.get_ldr_hdr_upconversion_nit_multiplier()); - + + printf("Deblocking filter ID: %u\n", dec.get_deblocking_filter_index()); + const bool is_etc1s = (dec.get_basis_tex_format() == basist::basis_tex_format::cETC1S); bool is_hdr = false; @@ -2093,8 +2845,15 @@ static bool unpack_and_validate_ktx2_file( pFmt_str = fmt_str_temp.c_str(); break; } + case basist::basis_tex_format::cXUBC7: + { + pFmt_str = "XUBC7"; + break; + } default: { + fmt_error_printf("Unknown/invalid format!\n"); + assert(0); return false; } @@ -2109,6 +2868,7 @@ static bool unpack_and_validate_ktx2_file( case basist::KTX2_SS_DEFLATE: printf("DEFLATE\n"); break; case basist::KTX2_SS_UASTC_HDR_6x6I: printf("UASTC_HDR_6x6I\n"); break; case basist::KTX2_SS_XUASTC_LDR: printf("XUASTC_LDR\n"); break; + case basist::KTX2_SS_XUBC7: printf("XUBC7\n"); break; default: error_printf("Invalid/unknown/unsupported\n"); return false; @@ -2160,6 +2920,7 @@ static bool unpack_and_validate_ktx2_file( bool srgb_transfer_func_astc_writing = actual_ktx2_srgb_transfer_func; const bool is_uastc_ldr_4x4 = (dec.get_basis_tex_format() == basist::basis_tex_format::cUASTC_LDR_4x4); + if ((is_etc1s) || (is_uastc_ldr_4x4)) { // The ETC1S and UASTC LDR 4x4 transcoders supply ASTC LDR 4x4 data assuming the decoder will NOT be using the sRGB read decode profile, which is likely the most common case (in geospatial rendering scenarios). @@ -2917,7 +3678,7 @@ static bool unpack_and_validate_ktx2_file( if ((!validate_flag) && (!opts.m_ktx_only)) { std::string rgba_filename(base_filename + fmt_string("_unpacked_rgba_{}_level_{}_face_{}_layer{04}.png", basist::basis_get_format_name(transcoder_tex_fmt), level_index, face_index, layer_index)); - if (!save_png(rgba_filename, img, cImageSaveIgnoreAlpha)) + if (!save_png(rgba_filename, img)) { error_printf("Failed writing to .PNG file \"%s\"\n", rgba_filename.c_str()); return false; @@ -3102,6 +3863,26 @@ static bool unpack_and_validate_ktx2_file( return true; } +static bool vec_is_ascii(const uint8_vec& v) +{ + bool is_ascii = true; + + for (uint32_t j = 0; j < v.size(); j++) + { + uint8_t c = v[j]; + if (!c) + break; + + if ((c < 32) || (c > 127)) + { + is_ascii = false; + break; + } + } // j + + return is_ascii; +} + static bool unpack_and_validate_basis_file( uint32_t file_index, const std::string &base_filename, @@ -3234,8 +4015,15 @@ static bool unpack_and_validate_basis_file( pFmt_str = fmt_str_temp.c_str(); break; } + case basist::basis_tex_format::cXUBC7: + { + pFmt_str = "XUBC7"; + break; + } default: { + fmt_error_printf("Invalid/unknown format!\n"); + assert(0); return false; } @@ -3254,6 +4042,56 @@ static bool unpack_and_validate_basis_file( printf("%u ", fileinfo.m_image_mipmap_levels[i]); printf("\n"); + basist::key_value_vec key_values; + if (!dec.get_key_values(&basis_file_data[0], (uint32_t)basis_file_data.size(), key_values)) + { + error_printf("get_key_values() failed!\n"); + return false; + } + + fmt_printf("\nTotal key value fields: {}\n", key_values.size_u32()); + + if (key_values.size())\ + { + for (uint32_t k = 0; k < key_values.size(); k++) + { + // Always zero terminated, always at least 1 character + if (vec_is_ascii(key_values[k].m_key)) + fmt_printf(" {}. \"{}\" - {} bytes: ", k, (const char*)key_values[k].m_key.get_ptr(), key_values[k].m_value.size_u32()); + else + fmt_printf(" {}. [Key not ASCII] - {} bytes: ", k, (const char*)key_values[k].m_key.get_ptr(), key_values[k].m_value.size_u32()); // shouldn't happen, but just in case - we could print as hex + + const uint8_vec& val = key_values[k].m_value; + + // val will always be empty, or it's guaranteed to be zero terminated (we always add an additional 0 at the end) + if (val.size() >= 100) + { + fmt_printf("[too large to print]\n"); + continue; + } + + if (vec_is_ascii(val)) + { + fmt_printf("\"{}\"\n", (const char*)val.get_ptr()); + } + else + { + for (uint32_t j = 0; j < val.size(); j++) + { + fmt_printf("0x{X} ", val[j]); + + if ((j & 15) == 15) + fmt_printf("\n"); + } // j + + fmt_printf("\n"); + } + + } // k + + //fmt_printf("\n"); + } + // the sRGB transfer function to use while astc unpacking (we want this to ideally match what we used during astc encoding) bool srgb_transfer_func_astc_unpacking = fileinfo.m_srgb; @@ -3979,6 +4817,14 @@ static bool unpack_and_validate_basis_file( if ((!validate_flag) && (!opts.m_ktx_only)) { + std::string rgba_filename(base_filename + string_format("_unpacked_rgba_%s_%u_%04u.png", basist::basis_get_format_name(transcoder_tex_fmt), level_index, image_index)); + if (!save_png(rgba_filename, img)) + { + error_printf("Failed writing to PNG file \"%s\"\n", rgba_filename.c_str()); + return false; + } + printf("Wrote .PNG file \"%s\"\n", rgba_filename.c_str()); + std::string rgb_filename(base_filename + string_format("_unpacked_rgb_%s_%u_%04u.png", basist::basis_get_format_name(transcoder_tex_fmt), level_index, image_index)); if (!save_png(rgb_filename, img, cImageSaveIgnoreAlpha)) { @@ -4112,6 +4958,14 @@ static bool unpack_and_validate_basis_file( if ((!validate_flag) && (!opts.m_ktx_only)) { + std::string rgba_filename(base_filename + string_format("_unpacked_rgba_%s_%u_%04u.png", basist::basis_get_format_name(transcoder_tex_fmt), level_index, image_index)); + if (!save_png(rgba_filename, img)) + { + error_printf("Failed writing to PNG file \"%s\"\n", rgba_filename.c_str()); + return false; + } + printf("Wrote .PNG file \"%s\"\n", rgba_filename.c_str()); + std::string rgb_filename(base_filename + string_format("_unpacked_rgb_%s_%u_%04u.png", basist::basis_get_format_name(transcoder_tex_fmt), level_index, image_index)); if (!save_png(rgb_filename, img, cImageSaveIgnoreAlpha)) { @@ -4166,6 +5020,437 @@ static bool unpack_and_validate_basis_file( return true; } +// Unpacks a Microsoft DDS file (DX9/DX10) using basist::dds_transcoder, bringing it up to the .ktx2/.basis bar. +// The DDS transcoder is LDR-only, so it transcodes the source to each DDS-supported target format and: +// (1) for the 12 block-compressed targets (BC1/3/4/5/7, ETC1/2, EAC R11/RG11, ASTC 4x4, PVRTC1) -- writes the +// GPU container files (per-image whole-mip-chain, plus cubemap and DDS texture-array shapes; .KTX for all, +// .DDS for BC1-7 only), AND round-trips each transcoded block image back to RGBA as one _unpacked_rgba_ PNG; +// (2) for the 3 uncompressed targets (RGBA32, RGB565, RGBA4444) -- writes one _unpacked_rgba_ PNG each (the +// RGBA32 decode is the source ground truth). +// -validate runs the full pipeline (transcode AND round-trip unpack, so a broken unpack path is caught too) but +// writes nothing. Honors -output_path, -no_ktx (skip GPU container files), -ktx_only (skip PNGs), and +// -unpack_format_only N (restrict to the single transcoder format whose enum index == N). BC6H/HDR DDS is future work (post v2.5). +static bool unpack_and_validate_dds_file( + const std::string& base_filename, + uint8_vec& dds_file_data, + command_line_params& opts, + uint32_t& total_unpack_warnings, + uint32_t& total_pvrtc_nonpow2_warnings) +{ + if (dds_file_data.size() > UINT32_MAX) + { + error_printf("DDS file too large!\n"); + return false; + } + + basist::dds_transcoder dec; + + if (!dec.init(dds_file_data.data(), dds_file_data.size_u32())) + { + error_printf("dds_transcoder::init() failed! File either uses an unsupported feature, is invalid, was corrupted, or this is a bug.\n"); + return false; + } + + if (!dec.start_transcoding()) + { + error_printf("dds_transcoder::start_transcoding() failed!\n"); + return false; + } + + const uint32_t levels = dec.get_levels(); + const uint32_t faces = dec.get_faces(); + const uint32_t num_layers = dec.get_layers(); // 0 == not an array (ktx2 convention) + const uint32_t eff_layers = num_layers ? num_layers : 1; + const basist::transcoder_texture_format src_fmt = dec.get_format(); + const char* pFmt_name = basist::basis_get_format_name(src_fmt); + + printf("Resolution: %ux%u\n", dec.get_width(), dec.get_height()); + printf("Mipmap Levels: %u\n", levels); + printf("Texture Array Size (layers): %u\n", num_layers); + printf("Total Faces: %u (%s)\n", faces, (faces == 6) ? "CUBEMAP" : "2D"); + printf("DDS source format: %s\n", basisu::get_dds_format_string(dec.get_dds_format())); + printf("Contained format: %s\n", pFmt_name); + // For uncompressed sources the "Contained format" is the CLOSEST-matching transcoder format, not necessarily + // the .DDS's physical pixel layout (e.g. an A8R8G8B8/X8R8G8B8 or B5G5R5A1 file reports RGBA32 / RGBA4444). + // The exact physical layout is shown above on the "DDS source format" line. + if (dec.get_source_kind() == basist::dds_transcoder::source_kind::cUncompressed) + printf(" (note: 'Contained format' is the closest matching transcoder format; use -tinydds_info for the full DDS header)\n"); + printf("Has Alpha: %u, sRGB: %u\n", dec.get_has_alpha(), dec.is_srgb() ? 1 : 0); + + // -info reports metadata only. -unpack writes files; -validate just decodes every image to exercise the path. + if (opts.m_mode == cInfo) + return true; + const bool write_files = (opts.m_mode == cUnpack); + const bool validating = (opts.m_mode == cValidate); + const bool write_gpu_files = write_files && !opts.m_no_ktx; // _transcoded_ .ktx/.dds container files + const bool write_png = write_files && !opts.m_ktx_only; // decoded _unpacked_rgba_ PNGs + const bool srgb = dec.is_srgb(); + const bool status_output = opts.m_comp_params.m_status_output; // -no_status_output / -quiet silences progress + + // -validate exercises every codec like -unpack but writes nothing. We go further than the .ktx2/.basis + // validate (which only verifies the transcode-to-GPU step): we also round-trip the block data back to pixels + // below, so a broken unpack path is caught too. + bool any_format_matched = false; // for the -unpack_format_only "nothing produced" warning + + const bool is_cubemap = (faces == 6); + const bool is_array = (num_layers != 0); // ktx2 convention: 0 == not an array + const uint32_t MAX_DDS_TEXARRAY_SIZE = 2048; // Direct3D maximum array size + + // NOTE: the DDS transcoder is currently LDR-only (sources: BC1-5 / BC7 / uncompressed). BC6H/HDR DDS support + // is planned for a future release (post v2.5); when it lands, the HDR target formats (BC6H, ASTC HDR, + // RGBA_HALF, RGB_9E5) and their EXR outputs would be added here, mirroring unpack_and_validate_ktx2_file(). + + // Block-compressed transcoder targets the DDS transcoder's get_target_info() accepts. Each goes to .ktx; + // BC1-7 additionally goes to .dds (does_dds_support_format). Listing them explicitly (rather than iterating + // the whole enum like the .ktx2/.basis unpackers) keeps us to exactly what the DDS transcoder can produce. + static const basist::transcoder_texture_format s_compressed_formats[] = + { + basist::transcoder_texture_format::cTFBC1_RGB, basist::transcoder_texture_format::cTFBC3_RGBA, + basist::transcoder_texture_format::cTFBC4_R, basist::transcoder_texture_format::cTFBC5_RG, + basist::transcoder_texture_format::cTFBC7_RGBA, basist::transcoder_texture_format::cTFETC1_RGB, + basist::transcoder_texture_format::cTFETC2_RGBA, basist::transcoder_texture_format::cTFETC2_EAC_R11, + basist::transcoder_texture_format::cTFETC2_EAC_RG11, basist::transcoder_texture_format::cTFASTC_4x4_RGBA, + basist::transcoder_texture_format::cTFPVRTC1_4_RGB, basist::transcoder_texture_format::cTFPVRTC1_4_RGBA + }; + + // Uncompressed LDR transcoder targets the DDS transcoder accepts. These have no GPU-container writer; we + // transcode them to pixels and write a single _unpacked_rgba_ PNG each (RGBA32 also serves as ground truth). + static const basist::transcoder_texture_format s_uncompressed_formats[] = + { + basist::transcoder_texture_format::cTFRGBA32, + basist::transcoder_texture_format::cTFRGB565, + basist::transcoder_texture_format::cTFRGBA4444 + }; + + // -unpack_format_only N restricts processing to the single transcoder format whose enum index == N + // (consistent with the .ktx2/.basis unpackers). Formats not in our supported tables simply produce nothing. + const int only_fmt = opts.m_unpack_format_only; + + // PVRTC1 needs power-of-2 dims; warn up front so the user understands the per-image skips below. + if (!is_pow2(dec.get_width()) || !is_pow2(dec.get_height())) + printf("Warning: dimensions %ux%u are not a power of 2 -- PVRTC1 output will be skipped (it requires power-of-2 dimensions).\n", dec.get_width(), dec.get_height()); + + // ============================ Block-compressed targets ============================ + // For each format: transcode every (layer, face) into a full mip chain, round-trip each level back to RGBA + // (one _unpacked_rgba_ PNG), and write the GPU container files in per-image / cubemap / texture-array shapes. + for (basist::transcoder_texture_format tex_fmt : s_compressed_formats) + { + if ((only_fmt > -1) && ((int)tex_fmt != only_fmt)) + continue; + any_format_matched = true; + + const basisu::texture_format gpu_fmt = basis_get_basisu_texture_format(tex_fmt); + const char* pTarget_fmt_name = basist::basis_get_format_name(tex_fmt); + const bool dds_ok = does_dds_support_format(gpu_fmt); + const bool ktx_ok = does_ktx_support_format(gpu_fmt); + if (!dds_ok && !ktx_ok) + continue; + + // Transcoded mip chains, indexed [layer * faces + face]. Built for both -unpack and -validate so + // -validate exercises every codec; only -unpack actually writes anything below. + basisu::vector chains; + chains.resize(eff_layers * faces); + + bool format_complete = true; // false if any image dropped a level (PVRTC1 on non-power-of-2 dims) + + for (uint32_t layer = 0; layer < eff_layers; layer++) + { + for (uint32_t face = 0; face < faces; face++) + { + gpu_image_vec& chain = chains[layer * faces + face]; + + for (uint32_t level = 0; level < levels; level++) + { + basist::ktx2_image_level_info level_info; + if (!dec.get_image_level_info(level_info, level, layer, face)) + { + error_printf("dds_transcoder::get_image_level_info() failed (layer %u, face %u, level %u)\n", layer, face, level); + return false; + } + + gpu_image gi(gpu_fmt, level_info.m_orig_width, level_info.m_orig_height); + // Pre-fill with pseudo-random bytes so a transcoder that fails to write part of the output + // is detectable (matches the .ktx2 unpacker's under-write detection). + fill_buffer_with_random_bytes(gi.get_ptr(), gi.get_size_in_bytes()); + + if (status_output) + printf("Transcoding to %s: layer %u, face %u, level %u, res %ux%u\n", pTarget_fmt_name, layer, face, level, level_info.m_orig_width, level_info.m_orig_height); + + interval_timer tm; + tm.start(); + const bool transcode_ok = dec.transcode_image_level(level, layer, face, gi.get_ptr(), gi.get_total_blocks(), tex_fmt); + const double transcode_ms = tm.get_elapsed_ms(); + + if (!transcode_ok) + { + // PVRTC1 on non-power-of-2 dims is an expected skip (warned up front), NOT a failure. Any + // other transcode failure is a real error -> fail (so -validate catches a broken codec path). + if ((tex_fmt == basist::transcoder_texture_format::cTFPVRTC1_4_RGB) || (tex_fmt == basist::transcoder_texture_format::cTFPVRTC1_4_RGBA)) + { + total_pvrtc_nonpow2_warnings++; + format_complete = false; + break; // can't build a partial mip chain -- drop this whole image + } + error_printf("Failed transcoding image to %s (layer %u, face %u, level %u)\n", pTarget_fmt_name, layer, face, level); + return false; + } + + if (status_output) + printf("Transcode to %s succeeded in %3.3f ms\n", pTarget_fmt_name, transcode_ms); + + chain.push_back(gi); + } + } + } + + // (a) Round-trip each transcoded block image back to RGBA. We always do the unpack when -validate (to + // exercise the unpack path), and write one _unpacked_rgba_ PNG only when actually unpacking to files. + if (write_png || validating) + { + for (uint32_t layer = 0; layer < eff_layers; layer++) + { + for (uint32_t face = 0; face < faces; face++) + { + const gpu_image_vec& chain = chains[layer * faces + face]; + for (uint32_t level = 0; level < chain.size(); level++) + { + image img; + if (!chain[level].unpack(img, srgb)) + { + // Under -unpack, mirror the .ktx2 unpacker: a failed round-trip unpack is a warning, and + // we unpack as much as possible. Under -validate it's a hard failure (catching it is the point). + error_printf("Failed unpacking %s block data to pixels (layer %u, face %u, level %u)\n", pTarget_fmt_name, layer, face, level); + if (validating) + return false; + total_unpack_warnings++; + continue; + } + + if (!write_png) + continue; // -validate: exercised the unpack above, but don't write a file + + std::string fn = (levels > 1) + ? base_filename + string_format("_unpacked_rgba_%s_level_%u_face_%u_layer_%04u.png", pTarget_fmt_name, level, face, layer) + : base_filename + string_format("_unpacked_rgba_%s_face_%u_layer_%04u.png", pTarget_fmt_name, face, layer); + if (!save_png(fn, img)) { error_printf("Failed writing to PNG file \"%s\"\n", fn.c_str()); return false; } + printf("Wrote .PNG file \"%s\"\n", fn.c_str()); + } + } + } + } + + // (b) GPU container files: cubemap (per layer), texture array (one DDS), and per-image whole-mip-chain. + // Skipped entirely if any image was incomplete (format_complete == false), since the chains don't line up. + if (write_gpu_files && format_complete) + { + // Cubemap: one file per layer holding all 6 faces (KTX for any format, DDS for BC1-7). + if (is_cubemap) + { + for (uint32_t layer = 0; layer < eff_layers; layer++) + { + basisu::vector cubemap; + for (uint32_t face = 0; face < 6; face++) + cubemap.push_back(chains[layer * faces + face]); + + if (ktx_ok) + { + std::string fn = base_filename + string_format("_transcoded_cubemap_%s_layer_%u.ktx", pTarget_fmt_name, layer); + if (!write_compressed_texture_file(fn.c_str(), cubemap, true, srgb)) { error_printf("Failed writing KTX file \"%s\"\n", fn.c_str()); return false; } + printf("Wrote .KTX cubemap file \"%s\"\n", fn.c_str()); + } + if (dds_ok) + { + std::string fn = base_filename + string_format("_transcoded_cubemap_%s_layer_%u.dds", pTarget_fmt_name, layer); + if (!write_compressed_texture_file(fn.c_str(), cubemap, true, srgb)) { error_printf("Failed writing DDS file \"%s\"\n", fn.c_str()); return false; } + printf("Wrote .DDS cubemap file \"%s\"\n", fn.c_str()); + } + } + } + + // Texture array: a single DDS holding every layer x face (DDS only, matching the .ktx2 unpacker). + if (is_array && dds_ok && (eff_layers <= MAX_DDS_TEXARRAY_SIZE)) + { + basisu::vector tex_array; + for (uint32_t layer = 0; layer < eff_layers; layer++) + for (uint32_t face = 0; face < faces; face++) + tex_array.push_back(chains[layer * faces + face]); + + std::string fn = base_filename + string_format("_transcoded_array_%s.dds", pTarget_fmt_name); + if (!write_compressed_texture_file(fn.c_str(), tex_array, is_cubemap, srgb)) { error_printf("Failed writing DDS file \"%s\"\n", fn.c_str()); return false; } + printf("Wrote .DDS texture array file \"%s\"\n", fn.c_str()); + } + + // Per-image whole-mip-chain files (one per layer/face). + for (uint32_t layer = 0; layer < eff_layers; layer++) + { + for (uint32_t face = 0; face < faces; face++) + { + const gpu_image_vec& chain = chains[layer * faces + face]; + if (!chain.size()) + continue; + + if (ktx_ok) + { + std::string fn = is_cubemap + ? base_filename + string_format("_transcoded_%s_face_%u_layer_%04u.ktx", pTarget_fmt_name, face, layer) + : base_filename + string_format("_transcoded_%s_layer_%04u.ktx", pTarget_fmt_name, layer); + if (!write_compressed_texture_file(fn.c_str(), chain, srgb)) { error_printf("Failed writing KTX file \"%s\"\n", fn.c_str()); return false; } + printf("Wrote .KTX file \"%s\"\n", fn.c_str()); + } + if (dds_ok) + { + std::string fn = is_cubemap + ? base_filename + string_format("_transcoded_%s_face_%u_layer_%04u.dds", pTarget_fmt_name, face, layer) + : base_filename + string_format("_transcoded_%s_layer_%04u.dds", pTarget_fmt_name, layer); + if (!write_compressed_texture_file(fn.c_str(), chain, srgb)) { error_printf("Failed writing DDS file \"%s\"\n", fn.c_str()); return false; } + printf("Wrote .DDS file \"%s\"\n", fn.c_str()); + } + } + } + } + } + + // ============================ Uncompressed LDR targets ============================ + // Transcode the source to each uncompressed pixel format and write one _unpacked_rgba_ PNG per image. This + // exercises those transcode paths (for -validate) and the RGBA32 decode also serves as the corruption canary. + for (basist::transcoder_texture_format tex_fmt : s_uncompressed_formats) + { + if ((only_fmt > -1) && ((int)tex_fmt != only_fmt)) + continue; + any_format_matched = true; + + const char* pTarget_fmt_name = basist::basis_get_format_name(tex_fmt); + + for (uint32_t layer = 0; layer < eff_layers; layer++) + { + for (uint32_t face = 0; face < faces; face++) + { + for (uint32_t level = 0; level < levels; level++) + { + basist::ktx2_image_level_info level_info; + if (!dec.get_image_level_info(level_info, level, layer, face)) + { + error_printf("dds_transcoder::get_image_level_info() failed (layer %u, face %u, level %u)\n", layer, face, level); + return false; + } + + const uint32_t mw = level_info.m_orig_width, mh = level_info.m_orig_height; + image img(mw, mh); + + if (status_output) + printf("Transcoding to %s: layer %u, face %u, level %u, res %ux%u\n", pTarget_fmt_name, layer, face, level, mw, mh); + + interval_timer tm; + double transcode_ms = 0.0; + + if (tex_fmt == basist::transcoder_texture_format::cTFRGBA32) + { + // Direct RGBA decode of the source -- the ground-truth pixels (and a corruption canary when + // -unpack_format_only selects RGBA32; otherwise the compressed loop above fails first). + // Pre-fill with random bytes to detect a partial (under-)write, like the .ktx2 unpacker. + fill_buffer_with_random_bytes(img.get_ptr(), (size_t)img.get_total_pixels() * sizeof(color_rgba)); + tm.start(); + const bool transcode_ok = dec.transcode_image_level(level, layer, face, img.get_ptr(), img.get_total_pixels(), tex_fmt, 0, mw, mh); + transcode_ms = tm.get_elapsed_ms(); + if (!transcode_ok) + { + error_printf("Failed decoding image to RGBA32 (layer %u, face %u, level %u) -- invalid or corrupt DDS data\n", layer, face, level); + return false; + } + } + else + { + // 16-bpp packed formats: transcode into a uint16 buffer, then expand to 8-bit RGBA for the PNG. + basisu::vector packed(mw * mh); + // Pre-fill with random bytes to detect a partial (under-)write, like the .ktx2 unpacker. + fill_buffer_with_random_bytes(packed.data(), packed.size() * sizeof(uint16_t)); + tm.start(); + const bool transcode_ok = dec.transcode_image_level(level, layer, face, packed.data(), (uint32_t)packed.size(), tex_fmt, 0, mw, mh); + transcode_ms = tm.get_elapsed_ms(); + if (!transcode_ok) + { + error_printf("Failed transcoding image to %s (layer %u, face %u, level %u)\n", pTarget_fmt_name, layer, face, level); + return false; + } + + for (uint32_t y = 0; y < mh; y++) + { + for (uint32_t x = 0; x < mw; x++) + { + const uint16_t p = packed[x + y * mw]; + if (tex_fmt == basist::transcoder_texture_format::cTFRGB565) + { + uint32_t r = p >> 11, g = (p >> 5) & 63, b = p & 31; + r = (r << 3) | (r >> 2); + g = (g << 2) | (g >> 4); + b = (b << 3) | (b >> 2); + img(x, y).set(r, g, b, 255); + } + else // cTFRGBA4444 + { + uint32_t r = p >> 12, g = (p >> 8) & 15, b = (p >> 4) & 15, a = p & 15; + img(x, y).set((r << 4) | r, (g << 4) | g, (b << 4) | b, (a << 4) | a); + } + } + } + } + + if (status_output) + printf("Transcode to %s succeeded in %3.3f ms\n", pTarget_fmt_name, transcode_ms); + + if (write_png) + { + std::string fn = (levels > 1) + ? base_filename + string_format("_unpacked_rgba_%s_level_%u_face_%u_layer_%04u.png", pTarget_fmt_name, level, face, layer) + : base_filename + string_format("_unpacked_rgba_%s_face_%u_layer_%04u.png", pTarget_fmt_name, face, layer); + if (!save_png(fn, img)) { error_printf("Failed writing to PNG file \"%s\"\n", fn.c_str()); return false; } + printf("Wrote .PNG file \"%s\"\n", fn.c_str()); + } + } + } + } + } + + // -unpack_format_only asked for a format that isn't a DDS-transcodable target -- say so rather than + // silently succeeding with no output. + if ((only_fmt > -1) && !any_format_matched) + printf("Warning: -unpack_format_only %d is not a DDS-transcodable format; no files were produced.\n", only_fmt); + + return true; +} + +// Prints basic header info about a PNG file (used by -info). Uses pv_png::get_png_info (no full decode). +static bool print_png_info(const char* pInput_filename, const uint8_vec& file_data) +{ + pv_png::png_info info; + if (!pv_png::get_png_info(file_data.data(), file_data.size(), info)) + { + error_printf("Failed parsing PNG file \"%s\"\n", pInput_filename); + return false; + } + + const char* pColor_type = "?"; + switch (info.m_color_type) + { + case pv_png::PNG_COLOR_TYPE_GREYSCALE: pColor_type = "Grayscale"; break; + case pv_png::PNG_COLOR_TYPE_TRUECOLOR: pColor_type = "Truecolor (RGB)"; break; + case pv_png::PNG_COLOR_TYPE_PALETTIZED: pColor_type = "Palettized"; break; + case pv_png::PNG_COLOR_TYPE_GREYSCALE_ALPHA: pColor_type = "Grayscale+Alpha"; break; + case pv_png::PNG_COLOR_TYPE_TRUECOLOR_ALPHA: pColor_type = "Truecolor+Alpha (RGBA)"; break; + default: break; + } + + printf("Dimensions: %ux%u\n", info.m_width, info.m_height); + printf("Channels (incl. transparency): %u\n", info.m_num_chans); + printf("Bit depth: %u\n", info.m_bit_depth); + printf("Color type: %u (%s)\n", info.m_color_type, pColor_type); + printf("Colorkey transparency (tRNS): %u\n", info.m_has_trns); + if (info.m_has_gamma) + printf("Gamma (gAMA): %f\n", info.m_gamma_value / 100000.0f); + + return true; +} + static bool unpack_and_validate_mode(command_line_params &opts) { interval_timer tm; @@ -4215,6 +5500,12 @@ static bool unpack_and_validate_mode(command_line_params &opts) std::string base_filename; string_split_path(pInput_filename, nullptr, nullptr, &base_filename, nullptr); + // Honor -output_path when unpacking: both the .basis and .KTX2 unpackers + // build all their output filenames by prefixing base_filename, so prefixing + // the output directory here redirects every unpacked file. + if (opts.m_output_path.size()) + string_combine_path(base_filename, opts.m_output_path.c_str(), base_filename.c_str()); + uint8_vec file_data; if (!basisu::read_file_to_vec(pInput_filename, file_data)) { @@ -4242,11 +5533,31 @@ static bool unpack_and_validate_mode(command_line_params &opts) bool is_ktx2 = false; if (file_data.size() >= sizeof(basist::g_ktx2_file_identifier)) - { is_ktx2 = (memcmp(file_data.data(), basist::g_ktx2_file_identifier, sizeof(basist::g_ktx2_file_identifier)) == 0); - } - printf("\nInput file \"%s\", KTX2: %u\n", pInput_filename, is_ktx2); + // Microsoft DDS magic ("DDS ", 0x20534444 little-endian). + bool is_dds = false; + if (file_data.size() >= 4) + is_dds = (memcmp(file_data.data(), "DDS ", 4) == 0); + + // KTX1 (.ktx) 12-byte identifier. KTX1 holds already-compressed GPU data we can't transcode here, + // so we just print its header info (via print_ktx_info), regardless of mode. + static const uint8_t s_ktx1_identifier[12] = { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A }; + const bool is_ktx1 = (file_data.size() >= 12) && (memcmp(file_data.data(), s_ktx1_identifier, 12) == 0); + + // .basis container 2-byte signature ("sB", i.e. cBASISSigValue stored little-endian). + bool is_basis = false; + if (file_data.size() >= 2) + is_basis = ((file_data[0] | (file_data[1] << 8)) == basist::basis_file_header::cBASISSigValue); + + // PNG 8-byte signature. + static const uint8_t s_png_identifier[8] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + const bool is_png = (file_data.size() >= 8) && (memcmp(file_data.data(), s_png_identifier, 8) == 0); + + // Detect by magic, not extension, so each file in a mixed input list routes correctly. The fallback + // (else) path below still attempts a .basis parse for an unrecognized file, which fails with a clear error. + const char* pFormat = is_ktx2 ? "KTX2" : (is_ktx1 ? "KTX1" : (is_dds ? "DDS" : (is_basis ? ".basis" : (is_png ? "PNG" : "unknown")))); + printf("\nInput file \"%s\", Format: %s\n", pInput_filename, pFormat); bool status; if (is_ktx2) @@ -4261,6 +5572,43 @@ static bool unpack_and_validate_mode(command_line_params &opts) total_unpack_warnings, total_pvrtc_nonpow2_warnings); } + else if (is_dds) + { + status = unpack_and_validate_dds_file( + base_filename, + file_data, + opts, + total_unpack_warnings, + total_pvrtc_nonpow2_warnings); + } + else if (is_ktx1) + { + // KTX1 holds already-compressed GPU data this tool can't transcode. -info dumps the header + // (same as -ktx_info); -unpack/-validate are unsupported and fail with a clear error rather + // than a misleading "Success". + if (opts.m_mode == cInfo) + { + status = print_ktx_info(pInput_filename); + } + else + { + error_printf("KTX1 (.ktx) files can't be unpacked or validated yet -- use -info (or -ktx_info) for header info\n"); + status = false; + } + } + else if (is_png) + { + // PNG is a source image, not a GPU texture container: -info dumps its header; unpack/validate don't apply. + if (opts.m_mode == cInfo) + { + status = print_png_info(pInput_filename, file_data); + } + else + { + error_printf("PNG files can't be unpacked or validated (they're source images, not GPU textures) -- use -info for header info\n"); + status = false; + } + } else { status = unpack_and_validate_basis_file( @@ -4364,6 +5712,206 @@ static bool hdr_compare_mode(command_line_params& opts) return true; } +// Reference-free image diagnostics: per-channel RGBA statistics, plus optional single-pixel / 4x4-block dumps. +// Intended to make image problems (e.g. a dropped/constant channel, an all-opaque or all-zero alpha, an +// unexpected channel swizzle) obvious WITHOUT needing a second image to diff against. LDR/8-bit inputs only +// for now (PNG/TGA/QOI/etc); HDR (.exr/.hdr) support can be added later. +static bool image_stats_mode(command_line_params& opts) +{ + if (!opts.m_input_filenames.size()) + { + error_printf("Must specify at least one image filename using -file\n"); + return false; + } + + const bool do_stats = opts.m_dump_image_stats; + const bool do_dump_pixel = (opts.m_dump_pixel_x >= 0) && (opts.m_dump_pixel_y >= 0); + const bool do_dump_block = (opts.m_dump_block_x >= 0) && (opts.m_dump_block_y >= 0); + + bool any_failed = false; + + for (uint32_t file_index = 0; file_index < opts.m_input_filenames.size(); file_index++) + { + const char* pFilename = opts.m_input_filenames[file_index].c_str(); + + std::string ext(string_get_extension(opts.m_input_filenames[file_index])); + if ((strcasecmp(ext.c_str(), "exr") == 0) || (strcasecmp(ext.c_str(), "hdr") == 0)) + { + error_printf("Skipping HDR image \"%s\" (-image_stats/-dump_* are LDR/8-bit only for now)\n", pFilename); + any_failed = true; + continue; + } + + image img; + if (!load_image(pFilename, img)) + { + error_printf("Failed loading image from file \"%s\"!\n", pFilename); + any_failed = true; + continue; + } + + const uint32_t width = img.get_width(), height = img.get_height(); + printf("\nImage \"%s\": %ux%u (%llu pixels), loader reports has_alpha: %u\n", + pFilename, width, height, (unsigned long long)((uint64_t)width * height), img.has_alpha()); + + // ---- Per-channel statistics (single pass over the pixels) ---- + if (do_stats) + { + // Resolve the region to gather stats over: the whole image by default, or the user's rectangle + // (-dump_image_stats ). The rectangle must lie fully inside the image. + uint32_t region_x0 = 0, region_y0 = 0, region_w = width, region_h = height; + bool region_valid = true; + if (opts.m_stats_region_w > 0) + { + region_x0 = (uint32_t)opts.m_stats_region_x; + region_y0 = (uint32_t)opts.m_stats_region_y; + region_w = (uint32_t)opts.m_stats_region_w; + region_h = (uint32_t)opts.m_stats_region_h; + + if (((uint64_t)region_x0 + region_w > width) || ((uint64_t)region_y0 + region_h > height)) + { + error_printf("stats region (%u,%u) %ux%u is out of range (image is %ux%u)\n", + region_x0, region_y0, region_w, region_h, width, height); + any_failed = true; + region_valid = false; + } + else + { + printf(" Stats restricted to region (%u,%u) %ux%u [x %u..%u, y %u..%u]\n", + region_x0, region_y0, region_w, region_h, + region_x0, region_x0 + region_w - 1, region_y0, region_y0 + region_h - 1); + } + } + + if (region_valid) + { + const uint32_t region_x1 = region_x0 + region_w, region_y1 = region_y0 + region_h; + const uint64_t total_pixels = (uint64_t)region_w * region_h; + + uint32_t chan_min[4] = { 255, 255, 255, 255 }; + uint32_t chan_max[4] = { 0, 0, 0, 0 }; + double chan_sum[4] = { 0, 0, 0, 0 }; + double chan_sum_sq[4] = { 0, 0, 0, 0 }; + bool seen[4][256]; + memset(seen, 0, sizeof(seen)); + uint64_t alpha_lt_255 = 0, alpha_eq_0 = 0; + bool premult_possible = true; // true while R,G,B <= A holds for every pixel (a hint of premultiplied alpha) + bool rgb_identical = true; // true while R==G==B for every pixel (effectively grayscale / duplicated channel) + + for (uint32_t y = region_y0; y < region_y1; y++) + { + for (uint32_t x = region_x0; x < region_x1; x++) + { + const color_rgba& c = img(x, y); + for (uint32_t ch = 0; ch < 4; ch++) + { + const uint32_t v = c[ch]; + if (v < chan_min[ch]) chan_min[ch] = v; + if (v > chan_max[ch]) chan_max[ch] = v; + chan_sum[ch] += (double)v; + chan_sum_sq[ch] += (double)v * (double)v; + seen[ch][v] = true; + } + if (c.a < 255) alpha_lt_255++; + if (c.a == 0) alpha_eq_0++; + if ((c.r > c.a) || (c.g > c.a) || (c.b > c.a)) premult_possible = false; + if ((c.r != c.g) || (c.g != c.b)) rgb_identical = false; + } + } + + printf(" Chan | Min | Max | Mean | Variance | StdDev | Unique | Notes\n"); + static const char chan_names[4] = { 'R', 'G', 'B', 'A' }; + for (uint32_t ch = 0; ch < 4; ch++) + { + const double mean = total_pixels ? (chan_sum[ch] / (double)total_pixels) : 0.0; + double var = total_pixels ? (chan_sum_sq[ch] / (double)total_pixels - mean * mean) : 0.0; + if (var < 0.0) var = 0.0; // guard against tiny negative floating point error + const double stddev = sqrt(var); + uint32_t unique = 0; + for (uint32_t v = 0; v < 256; v++) unique += seen[ch][v] ? 1u : 0u; + + char notes[64]; notes[0] = '\0'; + if (chan_min[ch] == chan_max[ch]) + snprintf(notes, sizeof(notes), "CONSTANT = %u", chan_min[ch]); + + printf(" %c | %4u | %4u | %11.4f | %15.4f | %10.4f | %6u | %s\n", + chan_names[ch], chan_min[ch], chan_max[ch], mean, var, stddev, unique, notes); + } + + // Alpha-focused summary -- a frequent source of bugs. + printf(" Alpha: "); + if (chan_min[3] == 255) + printf("fully opaque (all 255)\n"); + else if (chan_max[3] == 0) + printf("fully transparent (all 0)\n"); + else + printf("varies [%u..%u]; %llu px (%.2f%%) < 255, %llu px (%.2f%%) == 0\n", + chan_min[3], chan_max[3], + (unsigned long long)alpha_lt_255, total_pixels ? (100.0 * (double)alpha_lt_255 / (double)total_pixels) : 0.0, + (unsigned long long)alpha_eq_0, total_pixels ? (100.0 * (double)alpha_eq_0 / (double)total_pixels) : 0.0); + + // Flags that commonly indicate a problem. + if (img.has_alpha() && (chan_min[3] == 255)) + printf(" Note: image has an alpha channel but every alpha value is 255 (effectively opaque).\n"); + if ((chan_min[3] != 255) && premult_possible) + printf(" Note: every pixel has R,G,B <= A -- the image may use premultiplied alpha.\n"); + for (uint32_t ch = 0; ch < 3; ch++) + if (chan_min[ch] == chan_max[ch]) + printf(" Note: %c channel is constant (%u) everywhere -- possible dropped/unused channel.\n", chan_names[ch], chan_min[ch]); + if (rgb_identical) + printf(" Note: R, G, B are identical for every pixel -- image is effectively grayscale (or a duplicated/swizzled channel).\n"); + } // region_valid + } + + // ---- Single-pixel dump ---- + if (do_dump_pixel) + { + const uint32_t px = (uint32_t)opts.m_dump_pixel_x, py = (uint32_t)opts.m_dump_pixel_y; + if ((px >= width) || (py >= height)) + printf(" dump_pixel: (%u,%u) is out of range (image is %ux%u)\n", px, py, width, height); + else + { + const color_rgba& c = img(px, py); + printf(" Pixel (%u,%u): R=%3u G=%3u B=%3u A=%3u (hex 0x%02X%02X%02X%02X)\n", + px, py, c.r, c.g, c.b, c.a, c.r, c.g, c.b, c.a); + } + } + + // ---- 4x4 texel-block dump: source texels [bx*4..bx*4+3] x [by*4..by*4+3] (matches the 4x4 grid the + // block codecs use, so you can line this up against a transcoded/decoded block). ---- + if (do_dump_block) + { + const uint32_t bx = (uint32_t)opts.m_dump_block_x, by = (uint32_t)opts.m_dump_block_y; + const uint32_t num_blocks_x = (width + 3) / 4, num_blocks_y = (height + 3) / 4; + if ((bx >= num_blocks_x) || (by >= num_blocks_y)) + printf(" dump_block: block (%u,%u) out of range (image has %ux%u 4x4 blocks)\n", bx, by, num_blocks_x, num_blocks_y); + else + { + printf(" 4x4 block (%u,%u) -> texels (%u,%u)..(%u,%u) [R,G,B,A]:\n", bx, by, bx * 4, by * 4, bx * 4 + 3, by * 4 + 3); + for (uint32_t ty = 0; ty < 4; ty++) + { + const uint32_t sy = by * 4 + ty; + printf(" "); + for (uint32_t tx = 0; tx < 4; tx++) + { + const uint32_t sx = bx * 4 + tx; + if ((sx < width) && (sy < height)) + { + const color_rgba& c = img(sx, sy); + printf(" [%3u,%3u,%3u,%3u]", c.r, c.g, c.b, c.a); + } + else + printf(" [ -- oob -- ]"); + } + printf("\n"); + } + } + } + } + + return !any_failed; +} + static bool compare_mode(command_line_params &opts) { if (opts.m_input_filenames.size() != 2) @@ -4473,6 +6021,12 @@ static bool compare_mode(command_line_params &opts) } // x } // y + fmt_printf("\nPSNR-HVS and PSNR-HVS-M metrics:\n"); + psnr_hvs_metrics hvs_metrics; + psnr_hvs_compute_metrics(a, b, hvs_metrics); + psnr_hvs_print_metrics(hvs_metrics); + fmt_printf("\n"); + save_png("a_rgb.png", a, cImageSaveIgnoreAlpha); save_png("a_alpha.png", a, cImageSaveGrayscale, 3); printf("Wrote a_rgb.png and a_alpha.png\n"); @@ -4665,7 +6219,68 @@ static bool compare_mode(command_line_params &opts) } } // display_plot - + + return true; +} + +// -compare_hvs: computes and prints standard image metrics plus PSNR-HVS / PSNR-HVS-M for two LDR images (specified via +// -file). Uses the same psnr_hvs_compute_metrics()/print sequence the old dev test code used - the details matter. +static bool compare_hvs_mode(command_line_params& opts) +{ + if (opts.m_input_filenames.size() != 2) + { + error_printf("Must specify two PNG filenames using -file\n"); + return false; + } + + for (uint32_t i = 0; i < 2; i++) + { + std::string ext(string_get_extension(opts.m_input_filenames[i])); + if ((strcasecmp(ext.c_str(), "exr") == 0) || (strcasecmp(ext.c_str(), "hdr") == 0)) + { + error_printf("Can't compare HDR image files with this option (LDR only).\n"); + return false; + } + } + + image a, b; + if (!load_png(opts.m_input_filenames[0].c_str(), a)) + { + error_printf("Failed loading image from file \"%s\"!\n", opts.m_input_filenames[0].c_str()); + return false; + } + printf("Loaded \"%s\", %ux%u, has alpha: %u\n", opts.m_input_filenames[0].c_str(), a.get_width(), a.get_height(), a.has_alpha()); + + if (!load_png(opts.m_input_filenames[1].c_str(), b)) + { + error_printf("Failed loading image from file \"%s\"!\n", opts.m_input_filenames[1].c_str()); + return false; + } + printf("Loaded \"%s\", %ux%u, has alpha: %u\n", opts.m_input_filenames[1].c_str(), b.get_width(), b.get_height(), b.has_alpha()); + + if ((a.get_width() != b.get_width()) || (a.get_height() != b.get_height())) + { + printf("Images don't have the same dimensions - cropping input images to smallest common dimensions\n"); + + uint32_t w = minimum(a.get_width(), b.get_width()); + uint32_t h = minimum(a.get_height(), b.get_height()); + + a.crop(w, h); + b.crop(w, h); + } + + fmt_printf("Resolution: {}x{}\n", a.get_width(), a.get_height()); + + print_image_metrics(a, b); + + psnr_hvs_metrics hvs_metrics; + if (!psnr_hvs_compute_metrics(a, b, hvs_metrics)) + { + error_printf("psnr_hvs_compute_metrics() failed!\n"); + return false; + } + psnr_hvs_print_metrics(hvs_metrics); + return true; } @@ -4734,7 +6349,7 @@ static bool combine_images_mode(command_line_params& opts) printf("Loaded \"%s\", %ux%u, has alpha: %u\n", opts.m_input_filenames[1].c_str(), b.get_width(), b.get_height(), b.has_alpha()); const uint32_t width = minimum(a.get_width(), b.get_width()); - const uint32_t height = minimum(b.get_height(), b.get_height()); + const uint32_t height = minimum(a.get_height(), b.get_height()); image combined_img(width, height); for (uint32_t y = 0; y < height; y++) @@ -4760,6 +6375,208 @@ static bool combine_images_mode(command_line_params& opts) return true; } +// Resolves the optional rectangle for the -extract_* modes against an image's dimensions. +// If no region was given (region_w <= 0), returns the whole image. Returns false (with a message) if the rectangle lies outside the image. +static bool resolve_extract_region(const command_line_params& opts, uint32_t width, uint32_t height, + uint32_t& out_x, uint32_t& out_y, uint32_t& out_w, uint32_t& out_h) +{ + if (opts.m_extract_region_w <= 0) + { + out_x = 0; out_y = 0; out_w = width; out_h = height; + return true; + } + + out_x = (uint32_t)opts.m_extract_region_x; + out_y = (uint32_t)opts.m_extract_region_y; + out_w = (uint32_t)opts.m_extract_region_w; + out_h = (uint32_t)opts.m_extract_region_h; + + if (((uint64_t)out_x + out_w > width) || ((uint64_t)out_y + out_h > height)) + { + error_printf("Region (%u,%u) %ux%u is out of range (image is %ux%u)\n", out_x, out_y, out_w, out_h, width, height); + return false; + } + return true; +} + +// Applies one sRGB<->linear transform to an 8-bit value. The transform functions take/return normalized [0,1] +// (and assert that range), so we normalize, transform, then round-to-nearest back to 8-bit. +static uint8_t apply_extract_xform_u8(uint8_t v, int xform) +{ + const float f = (float)v * (1.0f / 255.0f); // exactly within [0,1] for v in [0,255] + const float g = (xform == cExtractXformToLinear) ? srgb_to_linear(f) : linear_to_srgb(f); + const int q = (int)(g * 255.0f + 0.5f); // round to nearest + return (uint8_t)clamp(q, 0, 255); +} + +// Applies the optional color transform to an output image in place: RGB always, alpha only if xform_alpha is set. +static void apply_extract_xform_image(image& img, int xform, bool xform_alpha) +{ + if (xform == cExtractXformNone) + return; + const uint32_t num_channels = xform_alpha ? 4u : 3u; + for (uint32_t y = 0; y < img.get_height(); y++) + for (uint32_t x = 0; x < img.get_width(); x++) + { + color_rgba& c = img(x, y); + for (uint32_t ch = 0; ch < num_channels; ch++) + c[ch] = apply_extract_xform_u8(c[ch], xform); + } +} + +// Human-readable suffix describing the active transform (for the "Wrote ..." messages). +static const char* extract_xform_desc(int xform) +{ + if (xform == cExtractXformToLinear) return " [sRGB->linear]"; + if (xform == cExtractXformToSRGB) return " [linear->sRGB]"; + return ""; +} + +static bool extract_channel_mode(command_line_params& opts) +{ + if (opts.m_input_filenames.size() != 1) + { + error_printf("Must specify one image filename using -file\n"); + return false; + } + + image img; + if (!load_image(opts.m_input_filenames[0].c_str(), img)) + { + error_printf("Failed loading image from file \"%s\"!\n", opts.m_input_filenames[0].c_str()); + return false; + } + + const uint32_t width = img.get_width(), height = img.get_height(); + printf("Loaded \"%s\", %ux%u, has alpha: %u\n", opts.m_input_filenames[0].c_str(), width, height, img.has_alpha()); + + uint32_t rx, ry, rw, rh; + if (!resolve_extract_region(opts, width, height, rx, ry, rw, rh)) + return false; + + // Copy the (possibly cropped) region; only the requested channel is later written out as grayscale. + const uint32_t channel = (uint32_t)opts.m_extract_channel; + image out_img(rw, rh); + for (uint32_t y = 0; y < rh; y++) + for (uint32_t x = 0; x < rw; x++) + out_img(x, y) = img(rx + x, ry + y); + + // Optional sRGB<->linear transform on just the extracted channel. Alpha is only transformed if -extract_xform_alpha was given, + // so for the alpha channel the transform may be requested but not actually applied (reflected in the message below). + const bool channel_is_alpha = (channel == 3); + const bool xform_applied = (opts.m_extract_color_xform != cExtractXformNone) && (!channel_is_alpha || opts.m_extract_xform_alpha); + if (xform_applied) + { + for (uint32_t y = 0; y < rh; y++) + for (uint32_t x = 0; x < rw; x++) + out_img(x, y)[channel] = apply_extract_xform_u8(out_img(x, y)[channel], opts.m_extract_color_xform); + } + + if (!save_png(opts.m_extract_filename.c_str(), out_img, cImageSaveGrayscale, channel)) + { + error_printf("Failed writing file %s\n", opts.m_extract_filename.c_str()); + return false; + } + + printf("Wrote channel %c of region (%u,%u) %ux%u as grayscale to \"%s\"%s\n", + "RGBA"[channel], rx, ry, rw, rh, opts.m_extract_filename.c_str(), xform_applied ? extract_xform_desc(opts.m_extract_color_xform) : ""); + return true; +} + +static bool extract_swizzle_mode(command_line_params& opts) +{ + if (opts.m_input_filenames.size() != 1) + { + error_printf("Must specify one image filename using -file\n"); + return false; + } + + image img; + if (!load_image(opts.m_input_filenames[0].c_str(), img)) + { + error_printf("Failed loading image from file \"%s\"!\n", opts.m_input_filenames[0].c_str()); + return false; + } + + const uint32_t width = img.get_width(), height = img.get_height(); + printf("Loaded \"%s\", %ux%u, has alpha: %u\n", opts.m_input_filenames[0].c_str(), width, height, img.has_alpha()); + + uint32_t rx, ry, rw, rh; + if (!resolve_extract_region(opts, width, height, rx, ry, rw, rh)) + return false; + + // Build the per-output-channel source map: listed channels are swizzled, the rest pass through unchanged (identity). + uint32_t src_chan[4] = { 0, 1, 2, 3 }; + for (int i = 0; i < opts.m_extract_swizzle_count; i++) + src_chan[i] = (uint32_t)opts.m_extract_swizzle[i]; + + image out_img(rw, rh); + for (uint32_t y = 0; y < rh; y++) + { + for (uint32_t x = 0; x < rw; x++) + { + const color_rgba& s = img(rx + x, ry + y); + color_rgba& d = out_img(x, y); + for (uint32_t c = 0; c < 4; c++) + d[c] = s[src_chan[c]]; + } + } + + // Optional sRGB<->linear transform: RGB always, alpha only with -extract_xform_alpha. + apply_extract_xform_image(out_img, opts.m_extract_color_xform, opts.m_extract_xform_alpha); + + if (!save_png(opts.m_extract_filename.c_str(), out_img)) + { + error_printf("Failed writing file %s\n", opts.m_extract_filename.c_str()); + return false; + } + + printf("Wrote swizzle R<-%c G<-%c B<-%c A<-%c of region (%u,%u) %ux%u to \"%s\"%s\n", + "RGBA"[src_chan[0]], "RGBA"[src_chan[1]], "RGBA"[src_chan[2]], "RGBA"[src_chan[3]], + rx, ry, rw, rh, opts.m_extract_filename.c_str(), extract_xform_desc(opts.m_extract_color_xform)); + return true; +} + +static bool extract_region_mode(command_line_params& opts) +{ + if (opts.m_input_filenames.size() != 1) + { + error_printf("Must specify one image filename using -file\n"); + return false; + } + + image img; + if (!load_image(opts.m_input_filenames[0].c_str(), img)) + { + error_printf("Failed loading image from file \"%s\"!\n", opts.m_input_filenames[0].c_str()); + return false; + } + + const uint32_t width = img.get_width(), height = img.get_height(); + printf("Loaded \"%s\", %ux%u, has alpha: %u\n", opts.m_input_filenames[0].c_str(), width, height, img.has_alpha()); + + uint32_t rx, ry, rw, rh; + if (!resolve_extract_region(opts, width, height, rx, ry, rw, rh)) + return false; + + image out_img(rw, rh); + for (uint32_t y = 0; y < rh; y++) + for (uint32_t x = 0; x < rw; x++) + out_img(x, y) = img(rx + x, ry + y); + + // Optional sRGB<->linear transform: RGB always, alpha only with -extract_xform_alpha. + apply_extract_xform_image(out_img, opts.m_extract_color_xform, opts.m_extract_xform_alpha); + + if (!save_png(opts.m_extract_filename.c_str(), out_img)) + { + error_printf("Failed writing file %s\n", opts.m_extract_filename.c_str()); + return false; + } + + printf("Wrote region (%u,%u) %ux%u to \"%s\"%s\n", rx, ry, rw, rh, opts.m_extract_filename.c_str(), extract_xform_desc(opts.m_extract_color_xform)); + return true; +} + static bool tonemap_image_mode(command_line_params& opts) { if (opts.m_input_filenames.size() != 1) @@ -4928,34 +6745,34 @@ const struct etc1s_uastc_4x4_ldr_test_file float m_etc1s_128_psnr; } g_etc1s_uastc_4x4_ldr_test_files[] = { - { "black_1x1.png", 189, 100.0f, 100.0f, 189, 100.0f }, - { "kodim01.png", 30993, 27.40f, 44.14f, 58354, 30.356064f }, - { "kodim02.png", 28529, 32.20f, 41.06f, 51411, 34.713940f }, - { "kodim03.png", 23411, 32.57f, 44.87f, 49282, 36.709675f }, - { "kodim04.png", 28256, 31.76f, 43.02f, 57003, 34.864861f }, - { "kodim05.png", 32646, 25.94f, 40.28f, 65731, 29.935091f }, - { "kodim06.png", 27336, 28.66f, 44.57f, 54963, 32.294220f }, - { "kodim07.png", 26618, 31.51f, 43.94f, 53352, 35.576595f }, - { "kodim08.png", 31133, 25.28f, 41.15f, 63347, 29.509914f }, - { "kodim09.png", 24777, 32.05f, 45.85f, 51355, 35.985966f }, - { "kodim10.png", 27247, 32.20f, 45.77f, 54291, 36.395000f }, - { "kodim11.png", 26579, 29.22f, 43.68f, 55491, 33.468971f }, - { "kodim12.png", 25102, 32.96f, 46.77f, 51465, 36.722233f }, - { "kodim13.png", 31604, 24.25f, 41.25f, 62629, 27.588623f }, - { "kodim14.png", 31162, 27.81f, 39.65f, 62866, 31.206463f }, - { "kodim15.png", 25528, 31.26f, 42.87f, 53343, 35.026314f }, - { "kodim16.png", 26894, 32.21f, 47.78f, 51325, 35.555458f }, - { "kodim17.png", 29334, 31.40f, 45.66f, 55630, 35.909283f }, - { "kodim18.png", 30929, 27.46f, 41.54f, 62421, 31.348171f }, - { "kodim19.png", 27889, 29.69f, 44.95f, 55055, 33.613987f }, - { "kodim20.png", 21104, 31.30f, 45.31f, 47136, 35.759407f }, - { "kodim21.png", 25943, 28.53f, 44.45f, 54768, 32.415817f }, - { "kodim22.png", 29277, 29.85f, 42.63f, 60889, 33.495415f }, - { "kodim23.png", 23550, 31.69f, 45.11f, 53774, 36.223492f }, - { "kodim24.png", 29613, 26.75f, 40.61f, 59014, 31.522869f }, - { "white_1x1.png", 189, 100.0f, 100.0f, 189, 100.000000f }, - { "wikipedia.png", 38961, 24.10f, 30.47f, 69558, 27.630802f }, - { "alpha0.png", 766, 100.0f, 56.16f, 747, 100.000000f } + { "black_1x1.png", 220, 100.0f, 100.0f, 220, 100.0f }, + { "kodim01.png", 31003, 27.40f, 44.14f, 58385, 30.356064f }, + { "kodim02.png", 28560, 32.20f, 41.06f, 51442, 34.713940f }, + { "kodim03.png", 23442, 32.57f, 44.87f, 49548, 36.709675f }, + { "kodim04.png", 28287, 31.76f, 43.02f, 57034, 34.864861f }, + { "kodim05.png", 32677, 25.94f, 40.28f, 65742, 29.935091f }, + { "kodim06.png", 27367, 28.66f, 44.57f, 54994, 32.294220f }, + { "kodim07.png", 26649, 31.51f, 43.94f, 53374, 35.576595f }, + { "kodim08.png", 31164, 25.28f, 41.15f, 63516, 29.509914f }, + { "kodim09.png", 24808, 32.05f, 45.85f, 51402, 35.985966f }, + { "kodim10.png", 27278, 32.20f, 45.77f, 54322, 36.395000f }, + { "kodim11.png", 26610, 29.22f, 43.68f, 55526, 33.468971f }, + { "kodim12.png", 25133, 32.96f, 46.77f, 51503, 36.722233f }, + { "kodim13.png", 31635, 24.25f, 41.25f, 62660, 27.588623f }, + { "kodim14.png", 31193, 27.81f, 39.65f, 62897, 31.206463f }, + { "kodim15.png", 25559, 31.26f, 42.87f, 53424, 35.026314f }, + { "kodim16.png", 26925, 32.21f, 47.78f, 51354, 35.555458f }, + { "kodim17.png", 29365, 31.40f, 45.66f, 55675, 35.909283f }, + { "kodim18.png", 30960, 27.46f, 41.54f, 62388, 31.348171f }, + { "kodim19.png", 27920, 29.69f, 44.95f, 55098, 33.613987f }, + { "kodim20.png", 21135, 31.30f, 45.31f, 47160, 35.759407f }, + { "kodim21.png", 25974, 28.53f, 44.45f, 54799, 32.415817f }, + { "kodim22.png", 29111, 29.85f, 42.63f, 60994, 33.495415f }, + { "kodim23.png", 23825, 31.69f, 45.11f, 53614, 36.223492f }, + { "kodim24.png", 29644, 26.75f, 40.61f, 58909, 31.522869f }, + { "white_1x1.png", 220, 100.0f, 100.0f, 220, 100.000000f }, + { "wikipedia.png", 38992, 24.10f, 30.47f, 69608, 27.630802f }, + { "alpha0.png", 807, 100.0f, 56.16f, 810, 100.000000f } }; static bool test_mode_ldr(command_line_params& opts) @@ -5276,15 +7093,15 @@ static bool test_mode_hdr(command_line_params& opts, basist::basis_tex_format te double delta1, delta2; printf("ASTC PSNR: %f (expected %f, delta %f), BC6H PSNR: %f (expected %f, delta %f)\n", - stats.m_basis_rgb_avg_log2_psnr, pTest_files[i].m_level_psnr_astc[uastc_hdr_level], delta1 = fabs(stats.m_basis_rgb_avg_log2_psnr - pTest_files[i].m_level_psnr_astc[uastc_hdr_level]), + stats.m_basis_rgb_avg_astc_hdr_log2_psnr, pTest_files[i].m_level_psnr_astc[uastc_hdr_level], delta1 = fabs(stats.m_basis_rgb_avg_astc_hdr_log2_psnr - pTest_files[i].m_level_psnr_astc[uastc_hdr_level]), stats.m_basis_rgb_avg_bc6h_log2_psnr, pTest_files[i].m_level_psnr_bc6h[uastc_hdr_level], delta2 = fabs(stats.m_basis_rgb_avg_bc6h_log2_psnr - pTest_files[i].m_level_psnr_bc6h[uastc_hdr_level])); highest_delta = maximum(highest_delta, delta1); highest_delta = maximum(highest_delta, delta2); - if (fabs(stats.m_basis_rgb_avg_log2_psnr - pTest_files[i].m_level_psnr_astc[uastc_hdr_level]) > PSNR_THRESHOLD) + if (fabs(stats.m_basis_rgb_avg_astc_hdr_log2_psnr - pTest_files[i].m_level_psnr_astc[uastc_hdr_level]) > PSNR_THRESHOLD) { - error_printf("Expected UASTC HDR RGB Avg PSNR was %f, but got %f instead!\n", pTest_files[i].m_level_psnr_astc[uastc_hdr_level], stats.m_basis_rgb_avg_log2_psnr); + error_printf("Expected UASTC HDR RGB Avg PSNR was %f, but got %f instead!\n", pTest_files[i].m_level_psnr_astc[uastc_hdr_level], stats.m_basis_rgb_avg_astc_hdr_log2_psnr); total_mismatches++; } @@ -5294,7 +7111,7 @@ static bool test_mode_hdr(command_line_params& opts, basist::basis_tex_format te total_mismatches++; } - astc_psnr(i, uastc_hdr_level) = stats.m_basis_rgb_avg_log2_psnr; + astc_psnr(i, uastc_hdr_level) = stats.m_basis_rgb_avg_astc_hdr_log2_psnr; bc6h_psnr(i, uastc_hdr_level) = stats.m_basis_rgb_avg_bc6h_log2_psnr; } } @@ -5364,36 +7181,218 @@ struct xuastc_ldr_test_file xuastc_ldr_test_file g_xuastc_ldr_test_files_6x6[] = { - { "black_1x1.png", { { 100.000000f, 111, 100.000000f }, { 75.000000f, 112, 100.000000f }, { 35.000000f, 112, 100.000000f } } }, - { "kodim01.png", { { 100.000000f, 141064, 37.188324f }, { 75.000000f, 115385, 32.893822f }, { 35.000000f, 80001, 30.057878f } } }, - { "kodim02.png", { { 100.000000f, 135146, 40.280567f }, { 75.000000f, 82435, 36.618645f }, { 35.000000f, 57365, 34.556519f } } }, - { "kodim03.png", { { 100.000000f, 133654, 42.754337f }, { 75.000000f, 72161, 38.706654f }, { 35.000000f, 51462, 36.026749f } } }, - { "kodim04.png", { { 100.000000f, 138877, 40.671108f }, { 75.000000f, 84194, 36.773575f }, { 35.000000f, 61363, 34.570110f } } }, - { "kodim05.png", { { 100.000000f, 146600, 35.842682f }, { 75.000000f, 124004, 33.176735f }, { 35.000000f, 94508, 30.148199f } } }, - { "kodim06.png", { { 100.000000f, 134928, 38.721409f }, { 75.000000f, 94356, 34.459309f }, { 35.000000f, 65904, 31.435408f } } }, - { "kodim07.png", { { 100.000000f, 136807, 41.048141f }, { 75.000000f, 85150, 38.172615f }, { 35.000000f, 64387, 35.527702f } } }, - { "kodim08.png", { { 100.000000f, 145326, 35.896526f }, { 75.000000f, 119654, 33.047630f }, { 35.000000f, 92376, 29.980146f } } }, - { "kodim09.png", { { 100.000000f, 135074, 42.271267f }, { 75.000000f, 66568, 38.262554f }, { 35.000000f, 47686, 35.810940f } } }, - { "kodim10.png", { { 100.000000f, 137184, 41.879585f }, { 75.000000f, 73560, 37.980556f }, { 35.000000f, 54453, 35.449261f } } }, - { "kodim11.png", { { 100.000000f, 138275, 38.718960f }, { 75.000000f, 91902, 35.112244f }, { 35.000000f, 66243, 32.391891f } } }, - { "kodim12.png", { { 100.000000f, 132918, 42.822681f }, { 75.000000f, 71330, 38.155998f }, { 35.000000f, 49345, 35.743179f } } }, - { "kodim13.png", { { 100.000000f, 141033, 33.948277f }, { 75.000000f, 123631, 30.678318f }, { 35.000000f, 88403, 27.592640f } } }, - { "kodim14.png", { { 100.000000f, 141117, 36.902863f }, { 75.000000f, 108060, 33.896935f }, { 35.000000f, 77104, 31.451799f } } }, - { "kodim15.png", { { 100.000000f, 135981, 40.416115f }, { 75.000000f, 76564, 36.855175f }, { 35.000000f, 55002, 34.548985f } } }, - { "kodim16.png", { { 100.000000f, 134349, 42.286755f }, { 75.000000f, 80713, 36.828140f }, { 35.000000f, 55894, 33.982174f } } }, - { "kodim17.png", { { 100.000000f, 138778, 40.653671f }, { 75.000000f, 81391, 37.024017f }, { 35.000000f, 59293, 34.429058f } } }, - { "kodim18.png", { { 100.000000f, 142690, 36.400116f }, { 75.000000f, 104323, 33.398468f }, { 35.000000f, 74051, 30.714231f } } }, - { "kodim19.png", { { 100.000000f, 138584, 39.704021f }, { 75.000000f, 87574, 35.544052f }, { 35.000000f, 63776, 33.032051f } } }, - { "kodim20.png", { { 100.000000f, 121663, 41.099850f }, { 75.000000f, 64552, 37.174721f }, { 35.000000f, 44838, 34.739983f } } }, - { "kodim21.png", { { 100.000000f, 138337, 38.284393f }, { 75.000000f, 85878, 34.727512f }, { 35.000000f, 60879, 32.004494f } } }, - { "kodim22.png", { { 100.000000f, 142142, 38.583397f }, { 75.000000f, 93914, 35.047283f }, { 35.000000f, 65592, 32.702984f } } }, - { "kodim23.png", { { 100.000000f, 140280, 42.489117f }, { 75.000000f, 74579, 39.385365f }, { 35.000000f, 57354, 37.228970f } } }, - { "kodim24.png", { { 100.000000f, 138443, 36.158039f }, { 75.000000f, 101415, 33.512146f }, { 35.000000f, 75311, 30.575174f } } }, - { "white_1x1.png", { { 100.000000f, 111, 100.000000f }, { 75.000000f, 112, 100.000000f }, { 35.000000f, 112, 100.000000f } } }, - { "wikipedia.png", { { 100.000000f, 189589, 32.205330f }, { 75.000000f, 168732, 31.926851f }, { 35.000000f, 160971, 30.209082f } } }, + { "black_1x1.png", { { 100.000000f, 142, 100.000000f }, { 75.000000f, 143, 100.000000f }, { 35.000000f, 143, 100.000000f } } }, + { "kodim01.png", { { 100.000000f, 143338, 37.631470f }, { 75.000000f, 132606, 34.858498f }, { 35.000000f, 112536, 31.930468f } } }, + { "kodim02.png", { { 100.000000f, 136308, 40.657822f }, { 75.000000f, 101839, 38.057598f }, { 35.000000f, 81085, 36.128090f } } }, + { "kodim03.png", { { 100.000000f, 133461, 43.046989f }, { 75.000000f, 86361, 39.970840f }, { 35.000000f, 69200, 37.480732f } } }, + { "kodim04.png", { { 100.000000f, 139788, 41.014957f }, { 75.000000f, 101258, 38.003296f }, { 35.000000f, 83902, 35.965012f } } }, + { "kodim05.png", { { 100.000000f, 148943, 36.247421f }, { 75.000000f, 138468, 34.564281f }, { 35.000000f, 122126, 31.844128f } } }, + { "kodim06.png", { { 100.000000f, 137452, 39.072002f }, { 75.000000f, 113603, 36.145702f }, { 35.000000f, 93972, 33.209007f } } }, + { "kodim07.png", { { 100.000000f, 137948, 41.442890f }, { 75.000000f, 98386, 39.420319f }, { 35.000000f, 83659, 37.224255f } } }, + { "kodim08.png", { { 100.000000f, 147884, 36.300182f }, { 75.000000f, 134661, 34.453938f }, { 35.000000f, 118202, 31.751554f } } }, + { "kodim09.png", { { 100.000000f, 136609, 42.575459f }, { 75.000000f, 83721, 39.448776f }, { 35.000000f, 68192, 37.360222f } } }, + { "kodim10.png", { { 100.000000f, 138614, 42.184914f }, { 75.000000f, 94047, 39.332645f }, { 35.000000f, 77621, 37.126518f } } }, + { "kodim11.png", { { 100.000000f, 139548, 39.074104f }, { 75.000000f, 113306, 36.617252f }, { 35.000000f, 91571, 34.055752f } } }, + { "kodim12.png", { { 100.000000f, 134259, 43.179798f }, { 75.000000f, 90453, 39.607330f }, { 35.000000f, 70234, 37.351040f } } }, + { "kodim13.png", { { 100.000000f, 144618, 34.267788f }, { 75.000000f, 141867, 32.225410f }, { 35.000000f, 120881, 29.205767f } } }, + { "kodim14.png", { { 100.000000f, 143775, 37.229355f }, { 75.000000f, 129872, 35.301926f }, { 35.000000f, 109048, 33.015049f } } }, + { "kodim15.png", { { 100.000000f, 136213, 40.758198f }, { 75.000000f, 92132, 38.082176f }, { 35.000000f, 73418, 35.928169f } } }, + { "kodim16.png", { { 100.000000f, 135975, 42.594410f }, { 75.000000f, 103275, 38.658211f }, { 35.000000f, 83547, 35.843338f } } }, + { "kodim17.png", { { 100.000000f, 140399, 40.927052f }, { 75.000000f, 100822, 38.284622f }, { 35.000000f, 83755, 35.921692f } } }, + { "kodim18.png", { { 100.000000f, 145298, 36.728806f }, { 75.000000f, 122486, 34.681892f }, { 35.000000f, 102720, 32.151287f } } }, + { "kodim19.png", { { 100.000000f, 140296, 40.024555f }, { 75.000000f, 107528, 36.965836f }, { 35.000000f, 89350, 34.593811f } } }, + { "kodim20.png", { { 100.000000f, 122203, 41.429142f }, { 75.000000f, 79399, 38.525173f }, { 35.000000f, 62388, 36.222507f } } }, + { "kodim21.png", { { 100.000000f, 140177, 38.664650f }, { 75.000000f, 104123, 36.225704f }, { 35.000000f, 85020, 33.629925f } } }, + { "kodim22.png", { { 100.000000f, 143846, 38.921207f }, { 75.000000f, 114520, 36.468395f }, { 35.000000f, 94574, 34.228367f } } }, + { "kodim23.png", { { 100.000000f, 139961, 42.794750f }, { 75.000000f, 89146, 40.377716f }, { 35.000000f, 75422, 38.508953f } } }, + { "kodim24.png", { { 100.000000f, 140310, 36.486050f }, { 75.000000f, 120326, 34.828285f }, { 35.000000f, 102832, 32.087605f } } }, + { "white_1x1.png", { { 100.000000f, 142, 100.000000f }, { 75.000000f, 143, 100.000000f }, { 35.000000f, 143, 100.000000f } } }, + { "wikipedia.png", { { 100.000000f, 190480, 32.429928f }, { 75.000000f, 173407, 32.292751f }, { 35.000000f, 168150, 31.088394f } } }, //{ "alpha0.png", { { 100.000000f, 1389, 49.883366f }, { 75.000000f, 1385, 49.125038f }, { 35.000000f, 1479, 42.865246f } } } // alpha0.png is minor nightmare for testing XUASTC LDR because it's very sensitive to tiny FP differences }; +xuastc_ldr_test_file g_xuastc_ldr_test_files_4x4[] = +{ + { "black_1x1.png", { { 100.000000f, 142, 100.000000f }, { 75.000000f, 143, 100.000000f }, { 35.000000f, 143, 100.000000f } } }, + { "kodim01.png", { { 100.000000f, 310218, 47.030079f }, { 75.000000f, 255162, 38.170372f }, { 35.000000f, 210620, 34.740185f } } }, + { "kodim02.png", { { 100.000000f, 294357, 47.193066f }, { 75.000000f, 194442, 40.707787f }, { 35.000000f, 163870, 38.593933f } } }, + { "kodim03.png", { { 100.000000f, 283110, 49.651531f }, { 75.000000f, 169617, 42.514809f }, { 35.000000f, 137983, 39.903645f } } }, + { "kodim04.png", { { 100.000000f, 301581, 47.235493f }, { 75.000000f, 197552, 40.555023f }, { 35.000000f, 170542, 38.400024f } } }, + { "kodim05.png", { { 100.000000f, 322641, 43.768642f }, { 75.000000f, 265551, 38.349422f }, { 35.000000f, 229721, 34.853809f } } }, + { "kodim06.png", { { 100.000000f, 297732, 46.904057f }, { 75.000000f, 222107, 39.441322f }, { 35.000000f, 183761, 36.179142f } } }, + { "kodim07.png", { { 100.000000f, 296921, 47.893196f }, { 75.000000f, 193982, 42.567802f }, { 35.000000f, 164339, 39.916515f } } }, + { "kodim08.png", { { 100.000000f, 321301, 44.123753f }, { 75.000000f, 266214, 38.408657f }, { 35.000000f, 227531, 34.939663f } } }, + { "kodim09.png", { { 100.000000f, 297129, 49.033165f }, { 75.000000f, 173342, 41.962223f }, { 35.000000f, 141332, 39.750557f } } }, + { "kodim10.png", { { 100.000000f, 299737, 48.689640f }, { 75.000000f, 185178, 41.973049f }, { 35.000000f, 156008, 39.650200f } } }, + { "kodim11.png", { { 100.000000f, 299730, 46.588585f }, { 75.000000f, 215553, 39.980545f }, { 35.000000f, 180852, 36.994793f } } }, + { "kodim12.png", { { 100.000000f, 289459, 49.914066f }, { 75.000000f, 175999, 42.131413f }, { 35.000000f, 145621, 39.855633f } } }, + { "kodim13.png", { { 100.000000f, 317109, 43.169689f }, { 75.000000f, 274743, 36.435371f }, { 35.000000f, 230455, 32.394821f } } }, + { "kodim14.png", { { 100.000000f, 311466, 44.403767f }, { 75.000000f, 241362, 38.612110f }, { 35.000000f, 206563, 35.850140f } } }, + { "kodim15.png", { { 100.000000f, 290996, 47.307865f }, { 75.000000f, 181963, 40.791069f }, { 35.000000f, 147984, 38.407547f } } }, + { "kodim16.png", { { 100.000000f, 293439, 49.771076f }, { 75.000000f, 198284, 41.343952f }, { 35.000000f, 163840, 38.560184f } } }, + { "kodim17.png", { { 100.000000f, 303429, 48.048100f }, { 75.000000f, 194161, 41.256020f }, { 35.000000f, 166376, 38.627644f } } }, + { "kodim18.png", { { 100.000000f, 316790, 43.609463f }, { 75.000000f, 237898, 38.175785f }, { 35.000000f, 205271, 35.113762f } } }, + { "kodim19.png", { { 100.000000f, 305786, 47.092293f }, { 75.000000f, 213024, 39.941761f }, { 35.000000f, 184103, 37.369053f } } }, + { "kodim20.png", { { 100.000000f, 259950, 48.652317f }, { 75.000000f, 155659, 41.508587f }, { 35.000000f, 124257, 38.938896f } } }, + { "kodim21.png", { { 100.000000f, 303954, 46.846046f }, { 75.000000f, 209365, 39.592907f }, { 35.000000f, 172113, 36.425140f } } }, + { "kodim22.png", { { 100.000000f, 312642, 45.427174f }, { 75.000000f, 221411, 39.440510f }, { 35.000000f, 189278, 36.970325f } } }, + { "kodim23.png", { { 100.000000f, 298345, 48.780674f }, { 75.000000f, 175964, 42.915535f }, { 35.000000f, 148486, 40.830143f } } }, + { "kodim24.png", { { 100.000000f, 302608, 44.029816f }, { 75.000000f, 229249, 39.054703f }, { 35.000000f, 196935, 35.450607f } } }, + { "white_1x1.png", { { 100.000000f, 142, 100.000000f }, { 75.000000f, 143, 100.000000f }, { 35.000000f, 143, 100.000000f } } }, + { "wikipedia.png", { { 100.000000f, 360418, 39.591961f }, { 75.000000f, 335408, 38.473557f }, { 35.000000f, 318023, 35.556229f } } }, +}; +xuastc_ldr_test_file g_xuastc_ldr_test_files_5x4[] = +{ + { "black_1x1.png", { { 100.000000f, 142, 100.000000f }, { 75.000000f, 143, 100.000000f }, { 35.000000f, 143, 100.000000f } } }, + { "kodim01.png", { { 100.000000f, 249211, 44.451683f }, { 75.000000f, 210509, 36.881168f }, { 35.000000f, 168587, 33.625820f } } }, + { "kodim02.png", { { 100.000000f, 238603, 45.184010f }, { 75.000000f, 159386, 39.738209f }, { 35.000000f, 129436, 37.669529f } } }, + { "kodim03.png", { { 100.000000f, 230397, 47.695618f }, { 75.000000f, 136798, 41.549713f }, { 35.000000f, 110021, 39.007256f } } }, + { "kodim04.png", { { 100.000000f, 244119, 45.411850f }, { 75.000000f, 159148, 39.640678f }, { 35.000000f, 135726, 37.497944f } } }, + { "kodim05.png", { { 100.000000f, 260814, 41.621056f }, { 75.000000f, 219953, 37.053875f }, { 35.000000f, 187075, 33.647770f } } }, + { "kodim06.png", { { 100.000000f, 240122, 44.883125f }, { 75.000000f, 179382, 38.298389f }, { 35.000000f, 146560, 35.105358f } } }, + { "kodim07.png", { { 100.000000f, 241474, 45.955803f }, { 75.000000f, 157918, 41.505169f }, { 35.000000f, 134039, 38.954472f } } }, + { "kodim08.png", { { 100.000000f, 259620, 41.868359f }, { 75.000000f, 218900, 37.081158f }, { 35.000000f, 185281, 33.697517f } } }, + { "kodim09.png", { { 100.000000f, 240072, 47.183537f }, { 75.000000f, 136701, 41.090607f }, { 35.000000f, 111111, 38.871655f } } }, + { "kodim10.png", { { 100.000000f, 242688, 46.832684f }, { 75.000000f, 148827, 41.053978f }, { 35.000000f, 124419, 38.726421f } } }, + { "kodim11.png", { { 100.000000f, 243015, 44.532112f }, { 75.000000f, 175753, 38.829491f }, { 35.000000f, 144384, 35.890057f } } }, + { "kodim12.png", { { 100.000000f, 234426, 47.991623f }, { 75.000000f, 141044, 41.192932f }, { 35.000000f, 115000, 38.965549f } } }, + { "kodim13.png", { { 100.000000f, 253290, 40.965210f }, { 75.000000f, 226116, 35.000126f }, { 35.000000f, 184823, 31.168385f } } }, + { "kodim14.png", { { 100.000000f, 252166, 42.261086f }, { 75.000000f, 198054, 37.443531f }, { 35.000000f, 165845, 34.783745f } } }, + { "kodim15.png", { { 100.000000f, 236533, 45.223949f }, { 75.000000f, 146577, 39.803493f }, { 35.000000f, 118822, 37.435871f } } }, + { "kodim16.png", { { 100.000000f, 237555, 47.910408f }, { 75.000000f, 161093, 40.372368f }, { 35.000000f, 131482, 37.610260f } } }, + { "kodim17.png", { { 100.000000f, 245639, 46.034199f }, { 75.000000f, 155299, 40.225517f }, { 35.000000f, 132135, 37.615314f } } }, + { "kodim18.png", { { 100.000000f, 256813, 41.648430f }, { 75.000000f, 194254, 36.990253f }, { 35.000000f, 164193, 33.996323f } } }, + { "kodim19.png", { { 100.000000f, 247269, 45.170345f }, { 75.000000f, 170744, 38.804966f }, { 35.000000f, 144924, 36.359180f } } }, + { "kodim20.png", { { 100.000000f, 213141, 46.714931f }, { 75.000000f, 125433, 40.431580f }, { 35.000000f, 98170, 37.925316f } } }, + { "kodim21.png", { { 100.000000f, 244810, 44.717579f }, { 75.000000f, 168321, 38.396568f }, { 35.000000f, 135060, 35.355556f } } }, + { "kodim22.png", { { 100.000000f, 252431, 43.553535f }, { 75.000000f, 179142, 38.341515f }, { 35.000000f, 150546, 35.956043f } } }, + { "kodim23.png", { { 100.000000f, 242860, 46.938763f }, { 75.000000f, 142099, 42.049194f }, { 35.000000f, 119089, 39.947498f } } }, + { "kodim24.png", { { 100.000000f, 244829, 41.747345f }, { 75.000000f, 189205, 37.679653f }, { 35.000000f, 159214, 34.212723f } } }, + { "white_1x1.png", { { 100.000000f, 142, 100.000000f }, { 75.000000f, 143, 100.000000f }, { 35.000000f, 143, 100.000000f } } }, + { "wikipedia.png", { { 100.000000f, 307678, 37.244614f }, { 75.000000f, 282188, 36.445412f }, { 35.000000f, 269165, 33.900223f } } }, +}; +xuastc_ldr_test_file g_xuastc_ldr_test_files_8x8[] = +{ + { "black_1x1.png", { { 100.000000f, 142, 100.000000f }, { 75.000000f, 143, 100.000000f }, { 35.000000f, 143, 100.000000f } } }, + { "kodim01.png", { { 100.000000f, 82815, 33.129543f }, { 75.000000f, 80163, 32.320469f }, { 35.000000f, 72960, 30.570271f } } }, + { "kodim02.png", { { 100.000000f, 78367, 37.343704f }, { 75.000000f, 64920, 36.143528f }, { 35.000000f, 51500, 34.768459f } } }, + { "kodim03.png", { { 100.000000f, 77599, 39.404751f }, { 75.000000f, 55460, 38.039928f }, { 35.000000f, 44970, 36.254837f } } }, + { "kodim04.png", { { 100.000000f, 80792, 37.649517f }, { 75.000000f, 64576, 36.303604f }, { 35.000000f, 54242, 34.725628f } } }, + { "kodim05.png", { { 100.000000f, 84711, 32.105507f }, { 75.000000f, 84081, 31.671597f }, { 35.000000f, 76634, 30.126841f } } }, + { "kodim06.png", { { 100.000000f, 79855, 34.817501f }, { 75.000000f, 69603, 33.753742f }, { 35.000000f, 61087, 31.827003f } } }, + { "kodim07.png", { { 100.000000f, 79449, 37.754784f }, { 75.000000f, 62135, 37.021587f }, { 35.000000f, 53152, 35.554993f } } }, + { "kodim08.png", { { 100.000000f, 84365, 31.781384f }, { 75.000000f, 81169, 31.304081f }, { 35.000000f, 74352, 29.816698f } } }, + { "kodim09.png", { { 100.000000f, 78971, 38.773418f }, { 75.000000f, 53689, 37.463520f }, { 35.000000f, 43699, 35.936676f } } }, + { "kodim10.png", { { 100.000000f, 80147, 38.478813f }, { 75.000000f, 59821, 37.286610f }, { 35.000000f, 49992, 35.712532f } } }, + { "kodim11.png", { { 100.000000f, 80255, 35.045494f }, { 75.000000f, 70796, 34.217140f }, { 35.000000f, 60030, 32.591511f } } }, + { "kodim12.png", { { 100.000000f, 78152, 39.493549f }, { 75.000000f, 58381, 37.853058f }, { 35.000000f, 46241, 36.070549f } } }, + { "kodim13.png", { { 100.000000f, 83603, 29.707319f }, { 75.000000f, 83231, 29.235767f }, { 35.000000f, 77262, 27.668018f } } }, + { "kodim14.png", { { 100.000000f, 82796, 33.518745f }, { 75.000000f, 80312, 32.885574f }, { 35.000000f, 70597, 31.453365f } } }, + { "kodim15.png", { { 100.000000f, 79262, 37.283611f }, { 75.000000f, 59147, 36.219261f }, { 35.000000f, 48554, 34.670193f } } }, + { "kodim16.png", { { 100.000000f, 78792, 38.528458f }, { 75.000000f, 65336, 36.765572f }, { 35.000000f, 54384, 34.544426f } } }, + { "kodim17.png", { { 100.000000f, 81297, 37.139481f }, { 75.000000f, 65278, 36.174728f }, { 35.000000f, 55032, 34.575806f } } }, + { "kodim18.png", { { 100.000000f, 83288, 32.887947f }, { 75.000000f, 75361, 32.249966f }, { 35.000000f, 65759, 30.717367f } } }, + { "kodim19.png", { { 100.000000f, 80857, 36.188713f }, { 75.000000f, 65937, 34.959888f }, { 35.000000f, 56324, 33.180534f } } }, + { "kodim20.png", { { 100.000000f, 72423, 37.353615f }, { 75.000000f, 49889, 36.328712f }, { 35.000000f, 41065, 34.822941f } } }, + { "kodim21.png", { { 100.000000f, 80646, 34.472473f }, { 75.000000f, 63150, 33.743328f }, { 35.000000f, 52850, 32.177635f } } }, + { "kodim22.png", { { 100.000000f, 82579, 35.452717f }, { 75.000000f, 71076, 34.477604f }, { 35.000000f, 60499, 32.928665f } } }, + { "kodim23.png", { { 100.000000f, 80795, 39.518650f }, { 75.000000f, 57122, 38.509109f }, { 35.000000f, 48485, 37.112495f } } }, + { "kodim24.png", { { 100.000000f, 80427, 32.505745f }, { 75.000000f, 73730, 32.054939f }, { 35.000000f, 65558, 30.470564f } } }, + { "white_1x1.png", { { 100.000000f, 142, 100.000000f }, { 75.000000f, 143, 100.000000f }, { 35.000000f, 143, 100.000000f } } }, + { "wikipedia.png", { { 100.000000f, 112186, 29.022635f }, { 75.000000f, 109799, 29.001532f }, { 35.000000f, 109059, 28.538498f } } }, +}; +xuastc_ldr_test_file g_xuastc_ldr_test_files_10x5[] = +{ + { "black_1x1.png", { { 100.000000f, 142, 100.000000f }, { 75.000000f, 143, 100.000000f }, { 35.000000f, 143, 100.000000f } } }, + { "kodim01.png", { { 100.000000f, 105404, 34.920200f }, { 75.000000f, 100965, 33.497791f }, { 35.000000f, 90611, 31.226894f } } }, + { "kodim02.png", { { 100.000000f, 99914, 38.595970f }, { 75.000000f, 80665, 36.986778f }, { 35.000000f, 64270, 35.383049f } } }, + { "kodim03.png", { { 100.000000f, 98251, 40.790932f }, { 75.000000f, 67869, 38.898525f }, { 35.000000f, 54886, 36.762691f } } }, + { "kodim04.png", { { 100.000000f, 102414, 38.946281f }, { 75.000000f, 79893, 37.033978f }, { 35.000000f, 66785, 35.246025f } } }, + { "kodim05.png", { { 100.000000f, 108302, 33.642941f }, { 75.000000f, 104798, 32.862797f }, { 35.000000f, 95050, 30.865231f } } }, + { "kodim06.png", { { 100.000000f, 101243, 36.626984f }, { 75.000000f, 85666, 34.866810f }, { 35.000000f, 71982, 32.394196f } } }, + { "kodim07.png", { { 100.000000f, 101129, 39.211941f }, { 75.000000f, 76369, 38.055550f }, { 35.000000f, 65894, 36.308075f } } }, + { "kodim08.png", { { 100.000000f, 107566, 33.244900f }, { 75.000000f, 101592, 32.423939f }, { 35.000000f, 92874, 30.506023f } } }, + { "kodim09.png", { { 100.000000f, 100574, 40.205162f }, { 75.000000f, 63935, 38.267521f }, { 35.000000f, 52720, 36.494003f } } }, + { "kodim10.png", { { 100.000000f, 101792, 39.791870f }, { 75.000000f, 74618, 38.100864f }, { 35.000000f, 60932, 36.214828f } } }, + { "kodim11.png", { { 100.000000f, 101858, 36.660149f }, { 75.000000f, 86801, 35.305096f }, { 35.000000f, 72462, 33.252529f } } }, + { "kodim12.png", { { 100.000000f, 99075, 40.883018f }, { 75.000000f, 69568, 38.559566f }, { 35.000000f, 54291, 36.616749f } } }, + { "kodim13.png", { { 100.000000f, 106502, 31.561871f }, { 75.000000f, 106013, 30.642658f }, { 35.000000f, 95531, 28.429173f } } }, + { "kodim14.png", { { 100.000000f, 105379, 35.074528f }, { 75.000000f, 98775, 33.989864f }, { 35.000000f, 84924, 32.170376f } } }, + { "kodim15.png", { { 100.000000f, 100550, 38.506466f }, { 75.000000f, 73302, 36.981686f }, { 35.000000f, 60268, 35.197380f } } }, + { "kodim16.png", { { 100.000000f, 99833, 40.386166f }, { 75.000000f, 79010, 37.707489f }, { 35.000000f, 63900, 35.129288f } } }, + { "kodim17.png", { { 100.000000f, 102975, 38.561710f }, { 75.000000f, 79270, 37.071556f }, { 35.000000f, 66965, 35.134377f } } }, + { "kodim18.png", { { 100.000000f, 107301, 34.268379f }, { 75.000000f, 95912, 33.241734f }, { 35.000000f, 83236, 31.308125f } } }, + { "kodim19.png", { { 100.000000f, 103796, 37.636776f }, { 75.000000f, 82419, 35.821537f }, { 35.000000f, 69996, 33.751148f } } }, + { "kodim20.png", { { 100.000000f, 91331, 38.943798f }, { 75.000000f, 61619, 37.297562f }, { 35.000000f, 49093, 35.451199f } } }, + { "kodim21.png", { { 100.000000f, 102857, 36.242493f }, { 75.000000f, 77121, 34.898647f }, { 35.000000f, 64434, 32.857597f } } }, + { "kodim22.png", { { 100.000000f, 104989, 36.767838f }, { 75.000000f, 88040, 35.332642f }, { 35.000000f, 74724, 33.503368f } } }, + { "kodim23.png", { { 100.000000f, 102295, 40.710884f }, { 75.000000f, 71039, 39.289032f }, { 35.000000f, 60360, 37.647507f } } }, + { "kodim24.png", { { 100.000000f, 102518, 34.120747f }, { 75.000000f, 92503, 33.321411f }, { 35.000000f, 81974, 31.218332f } } }, + { "white_1x1.png", { { 100.000000f, 142, 100.000000f }, { 75.000000f, 143, 100.000000f }, { 35.000000f, 143, 100.000000f } } }, + { "wikipedia.png", { { 100.000000f, 138617, 30.214272f }, { 75.000000f, 130237, 30.166813f }, { 35.000000f, 127678, 29.492228f } } }, +}; +xuastc_ldr_test_file g_xuastc_ldr_test_files_10x10[] = +{ + { "black_1x1.png", { { 100.000000f, 164, 100.000000f }, { 75.000000f, 165, 100.000000f }, { 35.000000f, 165, 100.000000f } } }, + { "kodim01.png", { { 100.000000f, 53866, 30.283733f }, { 75.000000f, 53616, 30.075840f }, { 35.000000f, 50347, 29.163128f } } }, + { "kodim02.png", { { 100.000000f, 51026, 35.123016f }, { 75.000000f, 46980, 34.621269f }, { 35.000000f, 37703, 33.580551f } } }, + { "kodim03.png", { { 100.000000f, 51031, 36.793110f }, { 75.000000f, 41431, 36.207535f }, { 35.000000f, 34960, 35.044708f } } }, + { "kodim04.png", { { 100.000000f, 53041, 35.332870f }, { 75.000000f, 46533, 34.796043f }, { 35.000000f, 39862, 33.541615f } } }, + { "kodim05.png", { { 100.000000f, 55189, 29.119299f }, { 75.000000f, 55108, 29.015081f }, { 35.000000f, 51969, 28.237648f } } }, + { "kodim06.png", { { 100.000000f, 51951, 31.890593f }, { 75.000000f, 48228, 31.475647f }, { 35.000000f, 43607, 30.357023f } } }, + { "kodim07.png", { { 100.000000f, 51629, 35.143024f }, { 75.000000f, 45495, 34.901260f }, { 35.000000f, 39163, 33.916348f } } }, + { "kodim08.png", { { 100.000000f, 54592, 28.505661f }, { 75.000000f, 54535, 28.359890f }, { 35.000000f, 51421, 27.674751f } } }, + { "kodim09.png", { { 100.000000f, 51854, 35.922009f }, { 75.000000f, 40015, 35.579926f }, { 35.000000f, 34664, 34.545792f } } }, + { "kodim10.png", { { 100.000000f, 52237, 35.590847f }, { 75.000000f, 44712, 35.157581f }, { 35.000000f, 37465, 34.189934f } } }, + { "kodim11.png", { { 100.000000f, 53091, 32.386562f }, { 75.000000f, 49709, 32.083656f }, { 35.000000f, 43594, 31.182316f } } }, + { "kodim12.png", { { 100.000000f, 50822, 36.772594f }, { 75.000000f, 43481, 36.005375f }, { 35.000000f, 35465, 34.854721f } } }, + { "kodim13.png", { { 100.000000f, 54490, 26.917747f }, { 75.000000f, 54727, 26.806589f }, { 35.000000f, 52410, 26.057467f } } }, + { "kodim14.png", { { 100.000000f, 54156, 30.993803f }, { 75.000000f, 53868, 30.794903f }, { 35.000000f, 50038, 29.964369f } } }, + { "kodim15.png", { { 100.000000f, 52157, 35.004284f }, { 75.000000f, 43205, 34.554562f }, { 35.000000f, 36555, 33.478485f } } }, + { "kodim16.png", { { 100.000000f, 51024, 35.731007f }, { 75.000000f, 46169, 34.875183f }, { 35.000000f, 40174, 33.368984f } } }, + { "kodim17.png", { { 100.000000f, 53562, 34.584801f }, { 75.000000f, 46775, 34.203426f }, { 35.000000f, 40370, 33.207817f } } }, + { "kodim18.png", { { 100.000000f, 54710, 30.220909f }, { 75.000000f, 52152, 30.071476f }, { 35.000000f, 47304, 29.228931f } } }, + { "kodim19.png", { { 100.000000f, 52609, 33.399029f }, { 75.000000f, 46521, 32.911697f }, { 35.000000f, 40742, 31.708160f } } }, + { "kodim20.png", { { 100.000000f, 48131, 34.454784f }, { 75.000000f, 38410, 34.073807f }, { 35.000000f, 32410, 33.349335f } } }, + { "kodim21.png", { { 100.000000f, 52704, 31.636679f }, { 75.000000f, 45878, 31.425013f }, { 35.000000f, 39147, 30.604250f } } }, + { "kodim22.png", { { 100.000000f, 53864, 33.018543f }, { 75.000000f, 49948, 32.652431f }, { 35.000000f, 42747, 31.578960f } } }, + { "kodim23.png", { { 100.000000f, 52922, 36.926800f }, { 75.000000f, 42386, 36.554390f }, { 35.000000f, 36771, 35.552139f } } }, + { "kodim24.png", { { 100.000000f, 52621, 29.655001f }, { 75.000000f, 51282, 29.539488f }, { 35.000000f, 46518, 28.826057f } } }, + { "white_1x1.png", { { 100.000000f, 164, 100.000000f }, { 75.000000f, 165, 100.000000f }, { 35.000000f, 165, 100.000000f } } }, + { "wikipedia.png", { { 100.000000f, 86446, 26.514196f }, { 75.000000f, 82331, 26.514242f }, { 35.000000f, 83217, 26.387506f } } }, +}; +xuastc_ldr_test_file g_xuastc_ldr_test_files_12x12[] = +{ + { "black_1x1.png", { { 100.000000f, 164, 100.000000f }, { 75.000000f, 165, 100.000000f }, { 35.000000f, 165, 100.000000f } } }, + { "kodim01.png", { { 100.000000f, 37797, 28.798088f }, { 75.000000f, 37597, 28.722265f }, { 35.000000f, 36100, 28.216297f } } }, + { "kodim02.png", { { 100.000000f, 35764, 33.883194f }, { 75.000000f, 34288, 33.613396f }, { 35.000000f, 28049, 32.870983f } } }, + { "kodim03.png", { { 100.000000f, 35991, 35.376232f }, { 75.000000f, 30705, 35.071907f }, { 35.000000f, 26302, 34.256138f } } }, + { "kodim04.png", { { 100.000000f, 37256, 33.985607f }, { 75.000000f, 34480, 33.726845f }, { 35.000000f, 30001, 32.846050f } } }, + { "kodim05.png", { { 100.000000f, 38362, 27.451393f }, { 75.000000f, 38445, 27.405621f }, { 35.000000f, 37828, 27.075932f } } }, + { "kodim06.png", { { 100.000000f, 36824, 30.414103f }, { 75.000000f, 35002, 30.223726f }, { 35.000000f, 32224, 29.430540f } } }, + { "kodim07.png", { { 100.000000f, 36160, 33.560291f }, { 75.000000f, 33870, 33.467850f }, { 35.000000f, 29863, 32.859840f } } }, + { "kodim08.png", { { 100.000000f, 37995, 26.794462f }, { 75.000000f, 38093, 26.760965f }, { 35.000000f, 37094, 26.328840f } } }, + { "kodim09.png", { { 100.000000f, 36401, 34.493057f }, { 75.000000f, 29870, 34.332375f }, { 35.000000f, 26454, 33.613277f } } }, + { "kodim10.png", { { 100.000000f, 36501, 33.844082f }, { 75.000000f, 32742, 33.686195f }, { 35.000000f, 28238, 33.049423f } } }, + { "kodim11.png", { { 100.000000f, 37149, 30.947369f }, { 75.000000f, 36099, 30.802794f }, { 35.000000f, 32881, 30.267765f } } }, + { "kodim12.png", { { 100.000000f, 35849, 35.354534f }, { 75.000000f, 33053, 34.918404f }, { 35.000000f, 26873, 34.069679f } } }, + { "kodim13.png", { { 100.000000f, 37949, 25.461622f }, { 75.000000f, 37959, 25.415657f }, { 35.000000f, 38216, 25.047762f } } }, + { "kodim14.png", { { 100.000000f, 37660, 29.483248f }, { 75.000000f, 37843, 29.437609f }, { 35.000000f, 36214, 28.999678f } } }, + { "kodim15.png", { { 100.000000f, 36649, 33.611992f }, { 75.000000f, 31835, 33.415279f }, { 35.000000f, 27625, 32.736401f } } }, + { "kodim16.png", { { 100.000000f, 35972, 34.145802f }, { 75.000000f, 33835, 33.685509f }, { 35.000000f, 29950, 32.574253f } } }, + { "kodim17.png", { { 100.000000f, 37435, 33.135811f }, { 75.000000f, 34680, 32.991879f }, { 35.000000f, 30935, 32.323273f } } }, + { "kodim18.png", { { 100.000000f, 37958, 28.844978f }, { 75.000000f, 37232, 28.796362f }, { 35.000000f, 34487, 28.304411f } } }, + { "kodim19.png", { { 100.000000f, 36608, 31.887293f }, { 75.000000f, 34105, 31.679951f }, { 35.000000f, 29955, 30.871086f } } }, + { "kodim20.png", { { 100.000000f, 34010, 32.896221f }, { 75.000000f, 28133, 32.748100f }, { 35.000000f, 24280, 32.296890f } } }, + { "kodim21.png", { { 100.000000f, 36704, 30.122864f }, { 75.000000f, 34125, 30.046625f }, { 35.000000f, 29272, 29.595934f } } }, + { "kodim22.png", { { 100.000000f, 37758, 31.691223f }, { 75.000000f, 35986, 31.529528f }, { 35.000000f, 32299, 30.867397f } } }, + { "kodim23.png", { { 100.000000f, 37121, 35.387283f }, { 75.000000f, 31453, 35.222927f }, { 35.000000f, 27471, 34.600704f } } }, + { "kodim24.png", { { 100.000000f, 36877, 28.162710f }, { 75.000000f, 36580, 28.106171f }, { 35.000000f, 34104, 27.782391f } } }, + { "white_1x1.png", { { 100.000000f, 164, 100.000000f }, { 75.000000f, 165, 100.000000f }, { 35.000000f, 165, 100.000000f } } }, + { "wikipedia.png", { { 100.000000f, 64288, 24.992327f }, { 75.000000f, 62309, 24.994276f }, { 35.000000f, 61714, 24.962925f } } }, +}; + + static bool test_mode_xuastc_ldr(command_line_params& opts) { uint32_t total_mismatches = 0; @@ -5401,118 +7400,178 @@ static bool test_mode_xuastc_ldr(command_line_params& opts) // Minor differences in how floating point code is optimized can result in slightly different generated files. // XUASTC LDR's IDCT is currently float - at low q's and high (>48) dB's tiny differences during decompression are noticeable - const float XUASTC_PSNR_THRESHOLD = 1.0f; + const float XUASTC_PSNR_THRESHOLD = 1.0f; const float XUASTC_FILESIZE_THRESHOLD = .045f; - struct run_stats + // Dump mode (-test_xuastc_dump): run the compressor across all block sizes and print the + // expected-value tables (copy/paste into the code below), then return without validating. + // Uses g_xuastc_ldr_test_files_6x6 for the shared file list (filenames + DCT q's). + if (opts.m_xuastc_test_dump) { - size_t m_comp_size; - image_stats m_stats; + const struct { basist::basis_tex_format m_fmt; const char* m_pName; } block_sizes[] = + { + { basist::basis_tex_format::cXUASTC_LDR_4x4, "4x4" }, + { basist::basis_tex_format::cXUASTC_LDR_5x4, "5x4" }, + { basist::basis_tex_format::cXUASTC_LDR_6x6, "6x6" }, + { basist::basis_tex_format::cXUASTC_LDR_8x8, "8x8" }, + { basist::basis_tex_format::cXUASTC_LDR_10x5, "10x5" }, + { basist::basis_tex_format::cXUASTC_LDR_10x10, "10x10" }, + { basist::basis_tex_format::cXUASTC_LDR_12x12, "12x12" }, + }; + + const uint32_t effort_level = 8; + const bool use_srgb_metrics = false; // false = linear (1,1,1,1) weights + + for (uint32_t bs = 0; bs < std::size(block_sizes); bs++) + { + fmt_printf("\nxuastc_ldr_test_file g_xuastc_ldr_test_files_{}[] =\n{{\n", block_sizes[bs].m_pName); + + for (uint32_t i = 0; i < std::size(g_xuastc_ldr_test_files_6x6); i++) + { + const auto& test_file = g_xuastc_ldr_test_files_6x6[i]; + + std::string filename(opts.m_test_file_dir); + if (filename.size()) + filename.push_back('/'); + filename += std::string(test_file.m_pFilename); + + basisu::vector source_images(1); + if (!load_png(filename.c_str(), source_images[0])) + { + error_printf("Failed loading test image \"%s\"\n", filename.c_str()); + return false; + } + + uint32_t run_sizes[XUASTC_LDR_TEST_FILE_NUM_RUNS]; + float run_psnrs[XUASTC_LDR_TEST_FILE_NUM_RUNS]; + + for (uint32_t run_index = 0; run_index < XUASTC_LDR_TEST_FILE_NUM_RUNS; run_index++) + { + const auto& test_run = test_file.m_test_runs[run_index]; + + uint32_t flags_and_quality = (opts.m_comp_params.m_multithreading ? cFlagThreaded : 0) | cFlagPrintStats | cFlagPrintStatus | (use_srgb_metrics ? cFlagSRGB : 0); + flags_and_quality |= effort_level; + + float uastc_rdo_quality = (test_run.m_dct_q < 100.0f) ? test_run.m_dct_q : 0.0f; + size_t data_size = 0; + image_stats stats; + + void* pData = basis_compress(block_sizes[bs].m_fmt, source_images, flags_and_quality, uastc_rdo_quality, &data_size, &stats); + if (!pData) + { + error_printf("basis_compress() failed!\n"); + return false; + } + basis_free_data(pData); + + run_sizes[run_index] = (uint32_t)data_size; + run_psnrs[run_index] = stats.m_basis_rgba_avg_psnr; + } + + fmt_printf("\t{{ \"{}\", {{", test_file.m_pFilename); + for (uint32_t run_index = 0; run_index < XUASTC_LDR_TEST_FILE_NUM_RUNS; run_index++) + { + fmt_printf(" {{ {}f, {}, {}f }", test_file.m_test_runs[run_index].m_dct_q, run_sizes[run_index], run_psnrs[run_index]); + if (run_index != (XUASTC_LDR_TEST_FILE_NUM_RUNS - 1)) + fmt_printf(", "); + } + fmt_printf(" } },\n"); + } + + fmt_printf("};\n"); + } + + return true; + } + + const bool use_srgb_metrics = false; // false = linear (1,1,1,1) weights, true = sRGB perceptual (9,11,1,11) + const uint32_t effort_level = 8; + + // Validate each block size against its stored expected-value table. + struct block_size_test + { + basist::basis_tex_format m_fmt; + const char* m_pName; + const xuastc_ldr_test_file* m_pFiles; + uint32_t m_num_files; }; - basisu::vector2D< run_stats > run_image_stats((uint32_t)std::size(g_xuastc_ldr_test_files_6x6), XUASTC_LDR_TEST_FILE_NUM_RUNS); - - for (uint32_t i = 0; i < std::size(g_xuastc_ldr_test_files_6x6); i++) + const block_size_test block_sizes[] = { - const auto& test_file = g_xuastc_ldr_test_files_6x6[i]; + { basist::basis_tex_format::cXUASTC_LDR_4x4, "4x4", g_xuastc_ldr_test_files_4x4, (uint32_t)std::size(g_xuastc_ldr_test_files_4x4) }, + { basist::basis_tex_format::cXUASTC_LDR_5x4, "5x4", g_xuastc_ldr_test_files_5x4, (uint32_t)std::size(g_xuastc_ldr_test_files_5x4) }, + { basist::basis_tex_format::cXUASTC_LDR_6x6, "6x6", g_xuastc_ldr_test_files_6x6, (uint32_t)std::size(g_xuastc_ldr_test_files_6x6) }, + { basist::basis_tex_format::cXUASTC_LDR_8x8, "8x8", g_xuastc_ldr_test_files_8x8, (uint32_t)std::size(g_xuastc_ldr_test_files_8x8) }, + { basist::basis_tex_format::cXUASTC_LDR_10x5, "10x5", g_xuastc_ldr_test_files_10x5, (uint32_t)std::size(g_xuastc_ldr_test_files_10x5) }, + { basist::basis_tex_format::cXUASTC_LDR_10x10, "10x10", g_xuastc_ldr_test_files_10x10, (uint32_t)std::size(g_xuastc_ldr_test_files_10x10) }, + { basist::basis_tex_format::cXUASTC_LDR_12x12, "12x12", g_xuastc_ldr_test_files_12x12, (uint32_t)std::size(g_xuastc_ldr_test_files_12x12) }, + }; - std::string filename(opts.m_test_file_dir); - if (filename.size()) + for (uint32_t bs = 0; bs < std::size(block_sizes); bs++) + { + const block_size_test& bsize = block_sizes[bs]; + + for (uint32_t i = 0; i < bsize.m_num_files; i++) { - filename.push_back('/'); - } - filename += std::string(test_file.m_pFilename); + const xuastc_ldr_test_file& test_file = bsize.m_pFiles[i]; - basisu::vector source_images(1); + std::string filename(opts.m_test_file_dir); + if (filename.size()) + filename.push_back('/'); + filename += std::string(test_file.m_pFilename); - image& source_image = source_images[0]; - if (!load_png(filename.c_str(), source_image)) - { - error_printf("Failed loading test image \"%s\"\n", filename.c_str()); - return false; - } - - printf("Loaded file \"%s\", dimensions %ux%u has alpha: %u\n", filename.c_str(), source_image.get_width(), source_image.get_height(), source_image.has_alpha()); - - image_stats stats; - - uint32_t flags_and_quality; - - // Test XUASTC LDR - flags_and_quality = (opts.m_comp_params.m_multithreading ? cFlagThreaded : 0) | cFlagPrintStats | cFlagPrintStatus | cFlagSRGB; - - for (uint32_t run_index = 0; run_index < XUASTC_LDR_TEST_FILE_NUM_RUNS; run_index++) - { - const auto& test_run = test_file.m_test_runs[run_index]; - - float uastc_rdo_quality = 0.0f; - size_t data_size = 0; - - const uint32_t effort_level = 8; - - flags_and_quality &= ~0xFF; - flags_and_quality |= effort_level; - - if (test_run.m_dct_q < 100.0f) + basisu::vector source_images(1); + image& source_image = source_images[0]; + if (!load_png(filename.c_str(), source_image)) { - uastc_rdo_quality = test_run.m_dct_q; - } - - basist::basis_tex_format tex_fmt = basist::basis_tex_format::cXUASTC_LDR_6x6; - - fmt_printf("**** Testing XUASTC LDR, DCT q {}, effort {}\n", test_run.m_dct_q, effort_level); - - void* pData = basis_compress(tex_fmt, source_images, flags_and_quality, uastc_rdo_quality, &data_size, &stats); - if (!pData) - { - error_printf("basis_compress() failed!\n"); + error_printf("Failed loading test image \"%s\"\n", filename.c_str()); return false; } - basis_free_data(pData); - fmt_printf("XUASTC Size: {} (expected {}), RGBA PSNR: {3.3} dB (expected {3.3} dB)\n", - (uint32_t)data_size, test_run.m_comp_size, - stats.m_basis_rgba_avg_psnr, test_run.m_rgba_psnr); + printf("Loaded file \"%s\", dimensions %ux%u has alpha: %u\n", filename.c_str(), source_image.get_width(), source_image.get_height(), source_image.has_alpha()); - float file_size_ratio = fabs((data_size / (float)test_run.m_comp_size) - 1.0f); + image_stats stats; - if (file_size_ratio > XUASTC_FILESIZE_THRESHOLD) + for (uint32_t run_index = 0; run_index < XUASTC_LDR_TEST_FILE_NUM_RUNS; run_index++) { - fmt_error_printf("Mismatch: Expected XUASTC LDR file size was {}, but got {} instead!\n", test_run.m_comp_size, (uint32_t)data_size); - total_mismatches++; - } + const auto& test_run = test_file.m_test_runs[run_index]; - if (fabs(stats.m_basis_rgba_avg_psnr - test_run.m_rgba_psnr) > XUASTC_PSNR_THRESHOLD) - { - fmt_error_printf("Mismatch: Expected XUASTC LDR RGBA Avg PSNR was {}, but got {} instead!\n", test_run.m_rgba_psnr, stats.m_basis_rgba_avg_psnr); - total_mismatches++; - } + uint32_t flags_and_quality = (opts.m_comp_params.m_multithreading ? cFlagThreaded : 0) | cFlagPrintStats | cFlagPrintStatus | (use_srgb_metrics ? cFlagSRGB : 0); + flags_and_quality |= effort_level; - run_image_stats(i, run_index).m_comp_size = data_size; - run_image_stats(i, run_index).m_stats = stats; + float uastc_rdo_quality = (test_run.m_dct_q < 100.0f) ? test_run.m_dct_q : 0.0f; + size_t data_size = 0; + + fmt_printf("**** Testing XUASTC LDR {}, DCT q {}, effort {}\n", bsize.m_pName, test_run.m_dct_q, effort_level); + + void* pData = basis_compress(bsize.m_fmt, source_images, flags_and_quality, uastc_rdo_quality, &data_size, &stats); + if (!pData) + { + error_printf("basis_compress() failed!\n"); + return false; + } + basis_free_data(pData); + + fmt_printf("XUASTC Size: {} (expected {}), RGBA PSNR: {3.3} dB (expected {3.3} dB)\n", + (uint32_t)data_size, test_run.m_comp_size, + stats.m_basis_rgba_avg_psnr, test_run.m_rgba_psnr); + + float file_size_ratio = fabs((data_size / (float)test_run.m_comp_size) - 1.0f); + if (file_size_ratio > XUASTC_FILESIZE_THRESHOLD) + { + fmt_error_printf("Mismatch: XUASTC LDR {} expected file size {}, but got {} instead!\n", bsize.m_pName, test_run.m_comp_size, (uint32_t)data_size); + total_mismatches++; + } + + if (fabs(stats.m_basis_rgba_avg_psnr - test_run.m_rgba_psnr) > XUASTC_PSNR_THRESHOLD) + { + fmt_error_printf("Mismatch: XUASTC LDR {} expected RGBA Avg PSNR {}, but got {} instead!\n", bsize.m_pName, test_run.m_rgba_psnr, stats.m_basis_rgba_avg_psnr); + total_mismatches++; + } + } } } -#if 0 - for (uint32_t i = 0; i < std::size(g_xuastc_ldr_test_files_6x6); i++) - { - fmt_printf("{{ \"{}\", {{", g_xuastc_ldr_test_files_6x6[i].m_pFilename); - - for (uint32_t j = 0; j < XUASTC_LDR_TEST_FILE_NUM_RUNS; j++) - { - fmt_printf(" {{ {}f, {}, {}f }", - g_xuastc_ldr_test_files_6x6[i].m_test_runs[j].m_dct_q, - run_image_stats(i, j).m_comp_size, - run_image_stats(i, j).m_stats.m_basis_rgba_avg_psnr); - - if (j != (XUASTC_LDR_TEST_FILE_NUM_RUNS - 1)) - fmt_printf(", "); - } - - fmt_printf(" } },\n"); - } -#endif - printf("Total XUASTC LDR mismatches: %u\n", total_mismatches); bool result = true; @@ -5547,6 +7606,443 @@ static bool clbench_mode(command_line_params& opts) return true; } +// --------------------------------------------------------------------------------------------------------------------- +// -test_codecs / -test_codecs_gen +// +// Sweeps every supported LDR and HDR codec across a quality x effort grid, driving the basis_compressor DIRECTLY +// (filenames in, m_compute_stats + m_validate_output_data on). The compressor transcodes each result to >=2 formats and +// computes their PSNRs. -test_codecs_gen writes the golden expected values to basisu_tool_test_codecs.inl; -test_codecs +// re-runs the sweep and compares against that table (with build/OS/compiler-divergence tolerances). +// --------------------------------------------------------------------------------------------------------------------- + +struct codec_test_case +{ + const char* m_pFilename; + basist::basis_tex_format m_fmt; + int m_quality; // [1,100] + int m_effort; // [0,100] + bool m_hdr; + uint32_t m_size; // expected KTX2 size, bytes + float m_rgb_psnr; // LDR: native RGB Avg | HDR: native ASTC-HDR log2 + float m_rgba_psnr; // LDR: native RGBA Avg | HDR: -1 (n/a) + float m_2nd_psnr; // LDR: transcoded BC7 RGBA Avg | HDR: transcoded BC6H log2 +}; + +#include "basisu_tool_test_codecs.inl" + +// The codec sweep (also used to emit the golden table). Order is LDR codecs first, then HDR. +struct codec_sweep_desc { basist::basis_tex_format m_fmt; const char* m_pEnum_name; bool m_hdr; }; +static const codec_sweep_desc g_codec_sweep[] = +{ + { basist::basis_tex_format::cETC1S, "cETC1S", false }, + { basist::basis_tex_format::cUASTC_LDR_4x4, "cUASTC_LDR_4x4", false }, + { basist::basis_tex_format::cXUBC7, "cXUBC7", false }, + { basist::basis_tex_format::cASTC_LDR_4x4, "cASTC_LDR_4x4", false }, + { basist::basis_tex_format::cASTC_LDR_6x6, "cASTC_LDR_6x6", false }, + { basist::basis_tex_format::cASTC_LDR_10x10, "cASTC_LDR_10x10", false }, + { basist::basis_tex_format::cASTC_LDR_12x12, "cASTC_LDR_12x12", false }, + { basist::basis_tex_format::cXUASTC_LDR_4x4, "cXUASTC_LDR_4x4", false }, + { basist::basis_tex_format::cXUASTC_LDR_6x6, "cXUASTC_LDR_6x6", false }, + { basist::basis_tex_format::cXUASTC_LDR_10x10, "cXUASTC_LDR_10x10", false }, + { basist::basis_tex_format::cXUASTC_LDR_12x12, "cXUASTC_LDR_12x12", false }, + { basist::basis_tex_format::cUASTC_HDR_4x4, "cUASTC_HDR_4x4", true }, + { basist::basis_tex_format::cASTC_HDR_6x6, "cASTC_HDR_6x6", true }, + { basist::basis_tex_format::cUASTC_HDR_6x6_INTERMEDIATE,"cUASTC_HDR_6x6_INTERMEDIATE", true }, +}; + +static const char* g_codec_ldr_test_files[] = { "kodim03.png", "kodim23.png", "kodim18.png", "alpha0.png", "wikipedia.png", "black_1x1.png" }; +static const char* g_codec_hdr_test_files[] = { "Desk.exr", "atrium.exr", "yucca.exr", "kodim18.png" }; // kodim18.png is auto-upconverted LDR->linear->100 nit HDR +static const int g_codec_test_qualities[] = { 10, 25, 50, 75, 100 }; +static const int g_codec_test_efforts[] = { 0, 3, 6 }; + +// Case-insensitive token-boundary match: filter matches at the start of pName or immediately after a '_'. +// So "ASTC" matches "ASTC_LDR_4x4" but NOT "XUASTC_LDR_4x4"/"UASTC_HDR_4x4"; "XUASTC" matches all XUASTC sizes; "HDR" matches all *_HDR_*. +static bool codec_name_matches_filter(const char* pName, const std::string& filter) +{ + if (filter.empty()) + return true; + std::string n(pName), f(filter); + for (char& c : n) c = (char)toupper((uint8_t)c); + for (char& c : f) c = (char)toupper((uint8_t)c); + size_t pos = 0; + while ((pos = n.find(f, pos)) != std::string::npos) + { + if ((pos == 0) || (n[pos - 1] == '_')) + return true; + pos++; + } + return false; +} + +// Short (no leading 'c') enum name for a format, for filtering/reporting. +static const char* codec_short_name(basist::basis_tex_format fmt) +{ + for (const auto& d : g_codec_sweep) + if (d.m_fmt == fmt) + return d.m_pEnum_name + 1; // skip leading 'c' + return "?"; +} + +// Pull the headline (native) + 2nd-format PSNRs out of an image_stats, per LDR/HDR. +static void codec_extract_psnrs(const basisu::image_stats& s, bool hdr, float& rgb, float& rgba, float& second) +{ + if (hdr) + { + rgb = s.m_basis_rgb_avg_astc_hdr_log2_psnr; + rgba = -1.0f; + second = s.m_basis_rgb_avg_bc6h_log2_psnr; + } + else + { + rgb = s.m_basis_rgb_avg_psnr; + rgba = s.m_basis_rgba_avg_psnr; + second = s.m_bc7_rgba_avg_psnr; + } +} + +// Run one case via the compressor directly. Returns false on compress failure. +static bool run_codec_test_case(basisu::job_pool& jp, const std::string& dir, const char* pFilename, + basist::basis_tex_format fmt, [[maybe_unused]] bool hdr, int quality, int effort, + uint32_t& out_size, basisu::image_stats& out_stats, double& out_time_ms) +{ + out_time_ms = 0.0; + + std::string path(dir); + if (path.size()) + path.push_back('/'); + path += pFilename; + + basis_compressor_params params; + params.m_pJob_pool = &jp; + params.m_multithreading = true; + params.m_read_source_images = true; // let the compressor load the file (and upconvert LDR->HDR when m_hdr is set) + params.m_source_filenames.push_back(path); + params.m_create_ktx2_file = true; // measure the .KTX2 file size + params.m_write_output_basis_or_ktx2_files = false; // keep it in memory only + params.m_compute_stats = true; // fill image_stats (native + transcoded BC7/BC6H PSNRs) + params.m_print_stats = false; + params.m_validate_output_data = true; // validate the encoded output + params.m_perceptual = true; // sRGB-style metrics (held constant for gen + run) + params.m_status_output = false; + params.m_debug = false; + + // Sets m_hdr/m_uastc and maps quality[1,100]/effort[0,100] to the codec's low-level knobs. + if (!params.set_format_mode_and_quality_effort(fmt, quality, effort, true)) + return false; + + interval_timer tm; + tm.start(); + + basis_compressor comp; + if (!comp.init(params)) + return false; + + if (comp.process() != basis_compressor::cECSuccess) + return false; + + out_time_ms = tm.get_elapsed_ms(); + + out_size = (uint32_t)comp.get_output_ktx2_file().size(); + + const basisu::vector& sv = comp.get_stats(); + if (!sv.size()) + return false; + out_stats = sv[0]; + + return true; +} + +static uint32_t codec_test_num_threads() +{ + uint32_t n = std::thread::hardware_concurrency(); + return n ? n : 1; +} + +// Returns the path prefix to the repo root so -test_codecs works whether run from the repo +// root ("") or the bin/ subdir ("../"). Probes for test_files/kodim03.png. +static std::string codec_test_root_prefix() +{ + FILE* f = fopen_safe("test_files/kodim03.png", "rb"); + if (f) { fclose(f); return ""; } + f = fopen_safe("../test_files/kodim03.png", "rb"); + if (f) { fclose(f); return "../"; } + return ""; // fallback: repo-root-relative +} + +static bool test_codecs_generate(command_line_params& opts) +{ + const std::string dir = opts.m_test_dir_explicit ? opts.m_test_file_dir : (codec_test_root_prefix() + "test_files"); + const std::string out_path = opts.m_test_codecs_gen_filename.size() ? opts.m_test_codecs_gen_filename : (codec_test_root_prefix() + "basisu_tool_test_codecs.inl"); + + fmt_printf("Generating codec test table: dir \"{}\", output \"{}\"", dir, out_path); + if (opts.m_codec_filter.size()) + fmt_printf(", codec filter \"{}\"", opts.m_codec_filter); + fmt_printf("\n"); + + FILE* pFile = fopen_safe(out_path.c_str(), "wb"); + if (!pFile) + { + error_printf("Failed creating output file \"%s\"\n", out_path.c_str()); + return false; + } + + fprintf(pFile, "// basisu_tool_test_codecs.inl\n"); + fprintf(pFile, "//\n// AUTO-GENERATED by: basisu -test_codecs_gen [outfile.inl] [-test_dir ] [codec_filter]\n"); + fprintf(pFile, "// Do NOT edit by hand. Regenerate whenever a codec changes its output.\n"); + fprintf(pFile, "//\n// Row: file, basis_tex_format, quality[1,100], effort[0,100], is_hdr, ktx2_size,\n"); + fprintf(pFile, "// rgb_psnr (LDR native RGB | HDR ASTC-HDR log2), rgba_psnr (LDR native RGBA | HDR -1),\n"); + fprintf(pFile, "// 2nd_psnr (LDR BC7 RGBA | HDR BC6H log2).\n//\n"); + fprintf(pFile, "static const codec_test_case g_codec_test_cases[] =\n{\n"); + + basisu::job_pool jp(codec_test_num_threads()); + + uint32_t total = 0, fails = 0; + + for (const auto& codec : g_codec_sweep) + { + if (!codec_name_matches_filter(codec.m_pEnum_name + 1, opts.m_codec_filter)) + continue; + + const char* const* pFiles = codec.m_hdr ? g_codec_hdr_test_files : g_codec_ldr_test_files; + const uint32_t num_files = codec.m_hdr ? (uint32_t)std::size(g_codec_hdr_test_files) : (uint32_t)std::size(g_codec_ldr_test_files); + + fprintf(pFile, "\t// ---- %s ----\n", codec.m_pEnum_name + 1); + + for (uint32_t fi = 0; fi < num_files; fi++) + { + for (int q : g_codec_test_qualities) + { + for (int e : g_codec_test_efforts) + { + uint32_t size = 0; + basisu::image_stats stats; + double elapsed_ms = 0.0; + + if (!run_codec_test_case(jp, dir, pFiles[fi], codec.m_fmt, codec.m_hdr, q, e, size, stats, elapsed_ms)) + { + fmt_printf("COMPRESS FAILED: {} {} q{} e{}\n", codec.m_pEnum_name + 1, pFiles[fi], q, e); + fprintf(pFile, "\t// COMPRESS FAILED: { \"%s\", basist::basis_tex_format::%s, %d, %d, %s },\n", + pFiles[fi], codec.m_pEnum_name, q, e, codec.m_hdr ? "true" : "false"); + fails++; + total++; + continue; + } + + float rgb, rgba, second; + codec_extract_psnrs(stats, codec.m_hdr, rgb, rgba, second); + + // Progress: print every ~10 runs unless the user passed -quiet/-no_status_output (m_status_output==false). + if (opts.m_comp_params.m_status_output && ((total % 10) == 0)) + fmt_printf("[{}] {} q{} e{} {} size {} rgb {}\n", total, codec.m_pEnum_name + 1, q, e, pFiles[fi], size, rgb); + + fprintf(pFile, "\t{ \"%s\", basist::basis_tex_format::%s, %d, %d, %s, %u, %.4ff, %.4ff, %.4ff },\n", + pFiles[fi], codec.m_pEnum_name, q, e, codec.m_hdr ? "true" : "false", size, rgb, rgba, second); + + total++; + } + } + } + } + + fprintf(pFile, "};\n"); + fclose(pFile); + + fmt_printf("\nWrote {} cases ({} compress failures) to \"{}\"\n", total, fails, out_path); + return fails == 0; +} + +static bool test_codecs_run(command_line_params& opts) +{ + const std::string dir = opts.m_test_dir_explicit ? opts.m_test_file_dir : (codec_test_root_prefix() + "test_files"); + +#ifdef USE_TIGHTER_TEST_TOLERANCES + const float PSNR_THRESHOLD = .125f; + const float XUASTC_PSNR_THRESHOLD = .5f; +#else + const float PSNR_THRESHOLD = 5.0f; + const float XUASTC_PSNR_THRESHOLD = 5.0f; // XUASTC LDR has more cross-build variance (matches test_mode_xuastc_ldr) +#endif + const float FILESIZE_THRESHOLD = .045f; // 4.5% relative, matches the other -test modes + // Tiny outputs (e.g. the XUASTC LDR runs on small alpha test images) jitter by a large RELATIVE amount across + // builds (MSVC vs clang) for a small absolute byte delta, tripping the 4.5% check spuriously. For golden sizes + // below this many bytes, relax to a much looser relative threshold so those don't fail the build. + const uint32_t SMALL_FILESIZE_BYTES = 2048; + const float SMALL_FILESIZE_THRESHOLD = .50f; // 50% relative, for golden sizes < SMALL_FILESIZE_BYTES + const float LDR_SENTINEL_PSNR = 99.0f; // lossless/degenerate inputs store ~100; don't require an exact match + const float HDR_SENTINEL_PSNR = 200.0f; + + if (opts.m_codec_filter.size()) + fmt_printf("Codec filter: \"{}\"\n", opts.m_codec_filter); + + basisu::job_pool jp(codec_test_num_threads()); + + const uint32_t kNumFmts = (uint32_t)basist::basis_tex_format::cTotalFormats; + basisu::vector highest_psnr_delta(kNumFmts); + basisu::vector highest_size_ratio(kNumFmts); + basisu::vector fmt_used(kNumFmts); + basisu::vector codec_total_ms(kNumFmts); // per-codec compression time accumulators + basisu::vector codec_max_ms(kNumFmts); + basisu::vector codec_count(kNumFmts); + for (uint32_t i = 0; i < kNumFmts; i++) { highest_psnr_delta[i] = 0.0f; highest_size_ratio[i] = 0.0f; fmt_used[i] = 0; codec_total_ms[i] = 0.0; codec_max_ms[i] = 0.0; codec_count[i] = 0; } + + uint32_t total = 0, tested = 0, mismatches = 0, run_fails = 0, total_skipped = 0; + + for (const codec_test_case& tc : g_codec_test_cases) + { + total++; + + if (!codec_name_matches_filter(codec_short_name(tc.m_fmt), opts.m_codec_filter)) + continue; + + if constexpr (sizeof(void*) == sizeof(uint32_t)) + { + // TODO: disable testing some codecs in 32-bit builds to avoid running out of memory (remove once 32-bit memory issue is addressed) + if (basist::basis_tex_format_is_xuastc_ldr(tc.m_fmt) || basis_tex_format_is_astc_ldr(tc.m_fmt)) + { + uint32_t block_width = 0, block_height = 0; + basist::get_basis_tex_format_block_size(tc.m_fmt, block_width, block_height); + if ((block_width < 6) || (block_height < 6)) + { + fmt_printf("WARNING: Skipping XUASTC/ASTC format with block size {}x{} to avoid running out of memory in 32-bit builds\n", block_width, block_height); + total_skipped++; + continue; + } + } + } + + tested++; + + uint32_t size = 0; + basisu::image_stats stats; + double elapsed_ms = 0.0; + + if (!run_codec_test_case(jp, dir, tc.m_pFilename, tc.m_fmt, tc.m_hdr, tc.m_quality, tc.m_effort, size, stats, elapsed_ms)) + { + error_printf("COMPRESS FAILED: %s %s q%d e%d\n", codec_short_name(tc.m_fmt), tc.m_pFilename, tc.m_quality, tc.m_effort); + run_fails++; + continue; + } + + float rgb, rgba, second; + codec_extract_psnrs(stats, tc.m_hdr, rgb, rgba, second); + + // Progress: print every ~10 runs unless the user passed -quiet/-no_status_output (m_status_output==false). + if (opts.m_comp_params.m_status_output && ((tested == 1) || ((tested % 10) == 0))) + printf(" [%u] %-26s %-14s q%d e%d size=%u rgb=%.2f%s time=%.1fms\n", + tested, codec_short_name(tc.m_fmt), tc.m_pFilename, tc.m_quality, tc.m_effort, size, rgb, tc.m_hdr ? " (log2)" : "", elapsed_ms); + + const float psnr_thresh = basist::basis_tex_format_is_xuastc_ldr(tc.m_fmt) ? XUASTC_PSNR_THRESHOLD : PSNR_THRESHOLD; + const float sentinel = tc.m_hdr ? HDR_SENTINEL_PSNR : LDR_SENTINEL_PSNR; + + const uint32_t fi = (uint32_t)tc.m_fmt; + fmt_used[fi] = 1; + codec_total_ms[fi] += elapsed_ms; + if (elapsed_ms > codec_max_ms[fi]) codec_max_ms[fi] = elapsed_ms; + codec_count[fi]++; + + // File size: relative ratio. + if (tc.m_size) + { + float ratio = fabsf((float)size / (float)tc.m_size - 1.0f); + if (ratio > highest_size_ratio[fi]) highest_size_ratio[fi] = ratio; + // Small golden sizes swing widely in relative terms across builds for a tiny absolute delta, so relax + // the threshold for them (see SMALL_FILESIZE_BYTES above). + const float size_thresh = (tc.m_size < SMALL_FILESIZE_BYTES) ? SMALL_FILESIZE_THRESHOLD : FILESIZE_THRESHOLD; + if (ratio > size_thresh) + { + error_printf("SIZE mismatch: %s %s q%d e%d: expected %u, got %u (%.2f%%)\n", + codec_short_name(tc.m_fmt), tc.m_pFilename, tc.m_quality, tc.m_effort, tc.m_size, size, ratio * 100.0f); + mismatches++; + } + } + + // PSNR comparisons. Sentinel (lossless) values are only required to be "high", not exact. + struct { const char* m_pName; float m_expected; float m_measured; bool m_active; } checks[3] = + { + { "RGB", tc.m_rgb_psnr, rgb, true }, + { "RGBA", tc.m_rgba_psnr, rgba, (!tc.m_hdr) && (tc.m_rgba_psnr >= 0.0f) }, + { "2nd", tc.m_2nd_psnr, second, tc.m_2nd_psnr > 1.0f }, // skip when the 2nd format wasn't computed + }; + + for (const auto& c : checks) + { + if (!c.m_active) + continue; + + if (c.m_expected >= sentinel) + { + // Lossless/degenerate: just require the measured value is also very high. + if (c.m_measured < (sentinel - 20.0f)) + { + error_printf("%s PSNR mismatch (sentinel): %s %s q%d e%d: expected >=%.1f, got %.3f\n", + c.m_pName, codec_short_name(tc.m_fmt), tc.m_pFilename, tc.m_quality, tc.m_effort, sentinel - 20.0f, c.m_measured); + mismatches++; + } + continue; + } + + float delta = fabsf(c.m_measured - c.m_expected); + if (delta > highest_psnr_delta[fi]) highest_psnr_delta[fi] = delta; + if ((c.m_measured < 55.0f) && (delta > psnr_thresh)) // ignore if PSNR is already very high + { + error_printf("%s PSNR mismatch: %s %s q%d e%d: expected %.3f, got %.3f (delta %.3f > %.3f)\n", + c.m_pName, codec_short_name(tc.m_fmt), tc.m_pFilename, tc.m_quality, tc.m_effort, c.m_expected, c.m_measured, delta, psnr_thresh); + mismatches++; + } + } + } + + // Per-codec highest deltas, grouped LDR then HDR (g_codec_sweep order). + printf("\n---- Highest deltas per codec (PSNR dB / file-size ratio) ----\n"); + for (uint32_t group = 0; group < 2; group++) + { + printf("%s:\n", group == 0 ? "LDR" : "HDR"); + for (const auto& d : g_codec_sweep) + { + if ((d.m_hdr ? 1u : 0u) != group) + continue; + const uint32_t fi = (uint32_t)d.m_fmt; + if (!fmt_used[fi]) + continue; + printf(" %-28s max PSNR delta %.4f dB, max size ratio %.2f%%\n", + d.m_pEnum_name + 1, highest_psnr_delta[fi], highest_size_ratio[fi] * 100.0f); + } + } + + // Per-codec compression timing (gross-perf-regression signal), grouped LDR then HDR. + printf("\n---- Per-codec compression time (total / avg / max) ----\n"); + double grand_total_ms = 0.0; + for (uint32_t group = 0; group < 2; group++) + { + printf("%s:\n", group == 0 ? "LDR" : "HDR"); + for (const auto& d : g_codec_sweep) + { + if ((d.m_hdr ? 1u : 0u) != group) + continue; + const uint32_t fi = (uint32_t)d.m_fmt; + if (!codec_count[fi]) + continue; + grand_total_ms += codec_total_ms[fi]; + printf(" %-28s total %.1f ms, avg %.1f ms, max %.1f ms (%u runs)\n", + d.m_pEnum_name + 1, codec_total_ms[fi], codec_total_ms[fi] / (double)codec_count[fi], codec_max_ms[fi], codec_count[fi]); + } + } + printf("Grand total compression time: %.1f ms (%.2f s)\n", grand_total_ms, grand_total_ms / 1000.0); + + if (total_skipped != 0) + fmt_printf("WARNING: {} encodes had to be skipped to avoid running out of memory in 32-bit builds\n", total_skipped); + + printf("\nCodec test: %u cases in table, %u tested, %u mismatches, %u compress failures\n", total, tested, mismatches, run_fails); + + if (mismatches || run_fails) + { + error_printf("Codec test FAILED\n"); + return false; + } + printf("Codec test succeeded\n"); + return true; +} + #ifdef FORCE_SAN_FAILURE static void force_san_failure() { @@ -5575,358 +8071,10 @@ static bool peek_astc_file(const char* pFilename) return false; } - const uint32_t total_block_pixels = block_width * block_height; - - fmt_printf("Block dimensions in pixels: {}x{}, {} total pixels\n", block_width, block_height, total_block_pixels); - fmt_printf("Image dimensions in pixels: {}x{}\n", image_width, image_height); - - fmt_printf("Extra cols/rows to pad image to ASTC block dimensions: {}x{}\n", - blocks.get_width() * block_width - image_width, - blocks.get_height() * block_height - image_height); - - image dec_image_srgb(image_width, image_height); - image dec_image_linear(image_width, image_height); - imagef dec_image_float(image_width, image_height); - - uint32_t cem_hist[16] = { }; - uint32_t cem_dp_hist[16] = { }; - uint32_t cem_used_bc_hist[16] = { }; - uint32_t total_dp = 0; - - uint32_t total_solid_blocks_ldr = 0; - uint32_t total_solid_blocks_hdr = 0; - uint32_t total_normal_blocks = 0; - - uint32_t part_hist[4] = { }; - uint32_t used_endpoint_levels_hist[astc_helpers::LAST_VALID_ENDPOINT_ISE_RANGE - astc_helpers::FIRST_VALID_ENDPOINT_ISE_RANGE + 1] = { }; - uint32_t used_weight_levels_hist[astc_helpers::LAST_VALID_WEIGHT_ISE_RANGE - astc_helpers::FIRST_VALID_WEIGHT_ISE_RANGE + 1] = { }; - - uint32_t total_unequal_cem_blocks = 0; - uint32_t total_unequal_cem_blocks_2subsets = 0; - uint32_t total_unequal_cem_blocks_3subsets = 0; - uint32_t total_unequal_cem_blocks_4subsets = 0; - - uint32_t highest_part_seed = 0; - - int min_weight_grid_width = INT_MAX, min_weight_grid_height = INT_MAX; - int max_weight_grid_width = 0, max_weight_grid_height = 0; - - uint32_t total_ldr_blocks = 0, total_hdr_blocks = 0; - - basisu::hash_map weight_grid_histogram; - - struct log_astc_block_config_cmp_t - { - bool operator()(const astc_helpers::log_astc_block& a, - const astc_helpers::log_astc_block& b) const - { - // This only compares the ASTC configuration for equality, NOT the contents. - if (a.m_error_flag != b.m_error_flag) - return false; - if (a.m_error_flag) - return true; - - if (a.m_grid_width != b.m_grid_width) - return false; - if (a.m_grid_height != b.m_grid_height) - return false; - - if (a.m_solid_color_flag_ldr != b.m_solid_color_flag_ldr) - return false; - if (a.m_solid_color_flag_hdr != b.m_solid_color_flag_hdr) - return false; - - if (a.m_solid_color_flag_ldr || a.m_solid_color_flag_hdr) - return true; - - if (a.m_dual_plane != b.m_dual_plane) - return false; - if (a.m_color_component_selector != b.m_color_component_selector) - return false; - - if (a.m_num_partitions != b.m_num_partitions) - return false; - - if (a.m_endpoint_ise_range != b.m_endpoint_ise_range) - return false; - if (a.m_weight_ise_range != b.m_weight_ise_range) - return false; - - for (uint32_t i = 0; i < a.m_num_partitions; i++) - if (a.m_color_endpoint_modes[i] != b.m_color_endpoint_modes[i]) - return false; - - return true; - } - }; - - basisu::hash_map, log_astc_block_config_cmp_t > unique_config_histogram; - - uint32_t total_subsets = 0; - - for (uint32_t by = 0; by < blocks.get_height(); by++) - { - for (uint32_t bx = 0; bx < blocks.get_width(); bx++) - { - astc_helpers::log_astc_block log_blk; - - if (!astc_helpers::unpack_block(&blocks(bx, by), log_blk, block_width, block_height)) - { - fmt_error_printf("astc_helpers::unpack_block() failed on block {}x{}\n", bx, by); - return false; - } - - if (log_blk.m_error_flag) - { - fmt_error_printf("astc_helpers::unpack_block() returned an error flag on block {}x{}\n", bx, by); - return false; - } - - { - astc_helpers::log_astc_block scrubbed_log_blk; - memset(&scrubbed_log_blk, 0, sizeof(scrubbed_log_blk)); - - // just record the config, not the contents, so only the config hashes - scrubbed_log_blk.m_solid_color_flag_ldr = log_blk.m_solid_color_flag_ldr; - scrubbed_log_blk.m_solid_color_flag_hdr = log_blk.m_solid_color_flag_hdr; - scrubbed_log_blk.m_dual_plane = log_blk.m_dual_plane; - scrubbed_log_blk.m_color_component_selector = log_blk.m_color_component_selector; - scrubbed_log_blk.m_grid_width = log_blk.m_grid_width; - scrubbed_log_blk.m_grid_height = log_blk.m_grid_height; - scrubbed_log_blk.m_num_partitions = log_blk.m_num_partitions; - scrubbed_log_blk.m_color_endpoint_modes[0] = log_blk.m_color_endpoint_modes[0]; - scrubbed_log_blk.m_color_endpoint_modes[1] = log_blk.m_color_endpoint_modes[1]; - scrubbed_log_blk.m_color_endpoint_modes[2] = log_blk.m_color_endpoint_modes[2]; - scrubbed_log_blk.m_color_endpoint_modes[3] = log_blk.m_color_endpoint_modes[3]; - scrubbed_log_blk.m_weight_ise_range = log_blk.m_weight_ise_range; - scrubbed_log_blk.m_endpoint_ise_range = log_blk.m_endpoint_ise_range; - - auto ins_res(unique_config_histogram.insert(scrubbed_log_blk, 0)); - (ins_res.first)->second = (ins_res.first)->second + 1; - } - - bool is_hdr = log_blk.m_solid_color_flag_hdr; - - if (log_blk.m_solid_color_flag_ldr) - { - total_solid_blocks_ldr++; - total_ldr_blocks++; - } - else if (log_blk.m_solid_color_flag_hdr) - { - total_solid_blocks_hdr++; - total_hdr_blocks++; - } - else - { - total_normal_blocks++; - - min_weight_grid_width = minimum(min_weight_grid_width, log_blk.m_grid_width); - min_weight_grid_height = minimum(min_weight_grid_height, log_blk.m_grid_height); - - max_weight_grid_width = maximum(max_weight_grid_width, log_blk.m_grid_width); - max_weight_grid_height = maximum(max_weight_grid_height, log_blk.m_grid_height); - - { - uint32_t weight_grid_hash_key = log_blk.m_grid_width | (log_blk.m_grid_height << 8); - auto ins_res(weight_grid_histogram.insert(weight_grid_hash_key, 0)); - (ins_res.first)->second = (ins_res.first)->second + 1; - } - - if (log_blk.m_dual_plane) - total_dp++; - - part_hist[log_blk.m_num_partitions - 1]++; - - // For debugging seed packing bugs - highest_part_seed = basisu::maximum(highest_part_seed, log_blk.m_partition_id); - - uint32_t cur_endpoint_ofs = 0; - bool has_unequal_cems = false; - - total_subsets += log_blk.m_num_partitions; - - for (uint32_t p = 0; p < log_blk.m_num_partitions; p++) - { - if (astc_helpers::is_cem_hdr(log_blk.m_color_endpoint_modes[p])) - is_hdr = true; - - cem_hist[log_blk.m_color_endpoint_modes[p]]++; - - if (log_blk.m_dual_plane) - cem_dp_hist[log_blk.m_color_endpoint_modes[p]]++; - - if ((p) && (log_blk.m_color_endpoint_modes[p] != log_blk.m_color_endpoint_modes[0])) - { - has_unequal_cems = true; - } - - if (astc_helpers::is_cem_ldr(log_blk.m_color_endpoint_modes[p])) - { - bool uses_bc = astc_helpers::used_blue_contraction(log_blk.m_color_endpoint_modes[p], log_blk.m_endpoints + cur_endpoint_ofs, log_blk.m_endpoint_ise_range); - - cem_used_bc_hist[log_blk.m_color_endpoint_modes[p]] += uses_bc; - } - - cur_endpoint_ofs += astc_helpers::get_num_cem_values(log_blk.m_color_endpoint_modes[p]); - } - - if (log_blk.m_num_partitions >= 2) - { - total_unequal_cem_blocks += has_unequal_cems; - - if (log_blk.m_num_partitions == 2) - total_unequal_cem_blocks_2subsets += has_unequal_cems; - else if (log_blk.m_num_partitions == 3) - total_unequal_cem_blocks_3subsets += has_unequal_cems; - else if (log_blk.m_num_partitions == 4) - total_unequal_cem_blocks_4subsets += has_unequal_cems; - } - - used_weight_levels_hist[open_range_check(log_blk.m_weight_ise_range - astc_helpers::FIRST_VALID_WEIGHT_ISE_RANGE, std::size(used_weight_levels_hist))]++; - used_endpoint_levels_hist[open_range_check(log_blk.m_endpoint_ise_range - astc_helpers::FIRST_VALID_ENDPOINT_ISE_RANGE, std::size(used_endpoint_levels_hist))]++; - } - - if (is_hdr) - { - total_hdr_blocks++; - } - else - { - total_ldr_blocks++; - - color_rgba block_pixels[astc_helpers::MAX_BLOCK_PIXELS]; - - // sRGB8 decode profile unpack - bool status = astc_helpers::decode_block(log_blk, block_pixels, block_width, block_height, astc_helpers::cDecodeModeSRGB8); - if (!status) - { - fmt_error_printf("astc_helpers::decode_block() failed on block {}x{}\n", bx, by); - return false; - } - - dec_image_srgb.set_block_clipped(block_pixels, bx * block_width, by * block_height, block_width, block_height); - - // linear8 decode profile unpack - status = astc_helpers::decode_block(log_blk, block_pixels, block_width, block_height, astc_helpers::cDecodeModeLDR8); - if (!status) - { - fmt_error_printf("astc_helpers::decode_block() failed on block {}x{}\n", bx, by); - return false; - } - - dec_image_linear.set_block_clipped(block_pixels, bx * block_width, by * block_height, block_width, block_height); - } - - // half float unpack - { - basist::half_float block_pixels_half[astc_helpers::MAX_BLOCK_PIXELS][4]; - - bool status = astc_helpers::decode_block(log_blk, block_pixels_half, block_width, block_height, astc_helpers::cDecodeModeHDR16); - if (!status) - { - fmt_error_printf("astc_helpers::decode_block() failed on block {}x{}\n", bx, by); - return false; - } - - vec4F block_pixels_float[astc_helpers::MAX_BLOCK_PIXELS]; - for (uint32_t i = 0; i < total_block_pixels; i++) - for (uint32_t j = 0; j < 4; j++) - block_pixels_float[i][j] = basist::half_to_float(block_pixels_half[i][j]); - - dec_image_float.set_block_clipped(block_pixels_float, bx * block_width, by * block_height, block_width, block_height); - } - - } // bx - - } //by - - fmt_printf("Total LDR blocks: {}, total HDR blocks: {}\n", total_ldr_blocks, total_hdr_blocks); - - save_png("astc_decoded_srgb8_ldr.png", dec_image_srgb); - fmt_printf("Wrote astc_decoded_srgb8_ldr.png\n"); - - save_png("astc_decoded_linear8_ldr.png", dec_image_linear); - fmt_printf("Wrote astc_decoded_linear8_ldr.png\n"); - - write_exr("astc_decoded_half.exr", dec_image_float, 4, 0); - fmt_printf("Wrote astc_decoded_half.exr\n"); - - fmt_printf("\nASTC file statistics:\n"); - - const uint32_t total_blocks = (uint32_t)blocks.size(); - - fmt_printf("Total blocks: {}, total void extent LDR: {}, total void extent HDR: {}, total normal: {}\n", total_blocks, total_solid_blocks_ldr, total_solid_blocks_hdr, total_normal_blocks); - fmt_printf("Total dual plane: {} {3.2}%\n", total_dp, total_dp * 100.0f / (float)total_blocks); - fmt_printf("Total subsets across all blocks: {}, Avg. subsets per block: {}\n", total_subsets, (float)total_subsets / (float)total_blocks); - - fmt_printf("Min weight grid dimensions: {}x{}\n", min_weight_grid_width, min_weight_grid_height); - fmt_printf("Max weight grid width: {}, height: {}\n", max_weight_grid_width, max_weight_grid_height); - - fmt_printf("\nPartition usage histogram:\n"); - for (uint32_t i = 0; i < 4; i++) - fmt_printf("{}: {} {3.2}%\n", i + 1, part_hist[i], (float)part_hist[i] * 100.0f / (float)total_blocks); - - fmt_printf("\nCEM usage histogram (percentages relative to total overall subsets used in texture):\n"); - for (uint32_t i = 0; i < 15; i++) - { - fmt_printf("{}: {} {3.2}%, total BC: {} {3.2}%, total DP: {} {3.2}%\n", i, - cem_hist[i], (float)cem_hist[i] * 100.0f / (float)total_subsets, - cem_used_bc_hist[i], (float)cem_used_bc_hist[i] * 100.0f / (float)total_subsets, - cem_dp_hist[i], (float)cem_dp_hist[i] * 100.0f / (float)total_subsets); - } - - fmt_printf("\nUsed endpoint ISE levels:\n"); - for (uint32_t i = 0; i < std::size(used_endpoint_levels_hist); i++) - fmt_printf("{} levels: {}\n", astc_helpers::get_ise_levels(astc_helpers::FIRST_VALID_ENDPOINT_ISE_RANGE + i), used_endpoint_levels_hist[i]); - - fmt_printf("\nUsed weight ISE levels:\n"); - for (uint32_t i = 0; i < std::size(used_weight_levels_hist); i++) - fmt_printf("{} levels: {}\n", astc_helpers::get_ise_levels(astc_helpers::FIRST_VALID_WEIGHT_ISE_RANGE + i), used_weight_levels_hist[i]); - - fmt_printf("\nTotal 2+ subset blocks using unequal CEM's: {} {3.2}%\n", total_unequal_cem_blocks, (float)total_unequal_cem_blocks * 100.0f / (float)total_blocks); - fmt_printf("Total 2 subset blocks using unequal CEM's: {} {3.2}%\n", total_unequal_cem_blocks_2subsets, (float)total_unequal_cem_blocks_2subsets * 100.0f / (float)total_blocks); - fmt_printf("Total 3 subset blocks using unequal CEM's: {} {3.2}%\n", total_unequal_cem_blocks_3subsets, (float)total_unequal_cem_blocks_3subsets * 100.0f / (float)total_blocks); - fmt_printf("Total 4 subset blocks using unequal CEM's: {} {3.2}%\n", total_unequal_cem_blocks_4subsets, (float)total_unequal_cem_blocks_4subsets * 100.0f / (float)total_blocks); - - fmt_printf("\nHighest part ID seed: {}, 0x{0x}\n", highest_part_seed, highest_part_seed); - - fmt_printf("\nWeight grid usage histogram:\n"); - - uint64_vec v; - for (auto it = weight_grid_histogram.begin(); it != weight_grid_histogram.end(); ++it) - v.push_back(((uint64_t)it->first << 32) | it->second); - - v.sort(); - - for (uint32_t i = 0; i < v.size(); i++) - fmt_printf(" {}x{}: total blocks {}\n", (v[i] >> 32) & 0xFF, (v[i] >> 40) & 0xFF, v[i] & UINT32_MAX); - - fmt_printf("\nTotal unique ASTC configurations: {}\n", unique_config_histogram.size_u32()); - - uint32_t config_idx = 0; - for (auto it = unique_config_histogram.begin(); it != unique_config_histogram.end(); ++it) - { - const auto& l = it->first; - const uint32_t total = it->second; - - fmt_printf(" {}. Used {} {3.2}% times: Solid LDR: {} HDR: {}, Grid: {}x{}, Dual Plane: {}, CCS: {}, NumParts: {}, CEMS: {} {} {} {}, WeightISERange: {}, EndpointISERange: {}\n", - config_idx, total, float(total) * 100.0f / total_blocks, - l.m_solid_color_flag_ldr, l.m_solid_color_flag_hdr, - l.m_grid_width, l.m_grid_height, - l.m_dual_plane, l.m_color_component_selector, - l.m_num_partitions, l.m_color_endpoint_modes[0], l.m_color_endpoint_modes[1], l.m_color_endpoint_modes[2], l.m_color_endpoint_modes[3], - l.m_weight_ise_range, l.m_endpoint_ise_range); - - config_idx++; - } - - fmt_printf("Success\n"); - - return true; + return display_astc_statistics(blocks, block_width, block_height, image_width, image_height, true); } -bool xuastc_ldr_decoder_fuzz_test() +static bool xuastc_ldr_decoder_fuzz_test() { basisu::rand rnd; rnd.seed(1); @@ -6056,6 +8204,1433 @@ bool xuastc_ldr_decoder_fuzz_test() return true; } +[[maybe_unused]] static inline uint8_t get_601_y(int r, int g, int b) +{ + return (uint8_t)std::round(16.0f + 65.481f * (float)r / 255.0f + 128.553f * (float)g / 255.0f + 24.966f * (float)b / 255.0f); +} + +// Calculates a proper average PSNR by converting each PSNR value to MSE, averaging the MSE values, and then converting back to PSNR. +// This is necessary because PSNR is a logarithmic metric, so you can't just average the PSNR values directly. +float calc_average_psnr(const float* psnr_values, uint32_t count, float lossless_db_thresh) +{ + if (!count) + return 0.0f; + + float mse_sum = 0.0; + for (uint32_t i = 0; i < count; i++) + { + if (psnr_values[i] >= lossless_db_thresh) + continue; + + mse_sum += powf(10.0f, -psnr_values[i] / 10.0f); + } + + float mean_mse = mse_sum / (float)count; + return (mean_mse > 0.0f) ? clamp(-10.0f * log10f(mean_mse), 0.0f, 100.0f) : 100.0f; +} + +static float print_psnr_stats(const float_vec &orig_psnrs, float lossless_db_thresh) +{ + const float avg_psnr = calc_average_psnr(orig_psnrs.get_ptr(), orig_psnrs.size_u32(), lossless_db_thresh); + fmt_printf(" Average (from all MSE's): {3.3} dB\n", avg_psnr); + + uint32_t total_lossless = 0; + float_vec psnrs; + for (uint32_t i = 0; i < orig_psnrs.size(); i++) + { + if (orig_psnrs[i] >= lossless_db_thresh) + total_lossless++; + else + psnrs.push_back(orig_psnrs[i]); + } + + fmt_printf(" Total > {3.2} dB (lossless): {}, lossy: {}\n", lossless_db_thresh, total_lossless, psnrs.size_u32()); + fmt_printf(" Lossy-only statistics:\n"); + + stats psnrs_stats; + psnrs_stats.calc(psnrs.size_u32(), psnrs.get_ptr(), 1, true); + + fmt_printf(" Average (of only lossy PSNR's): {3.3} dB\n", psnrs_stats.m_avg); + fmt_printf(" Std dev: {3.3} dB, Skewness: {3.3} dB\n", psnrs_stats.m_std_dev, psnrs_stats.m_skewness); + fmt_printf(" Min: {3.3} dB\n", psnrs_stats.m_min); + fmt_printf(" Median: {3.3} dB at file index: {}\n", psnrs_stats.m_median, psnrs_stats.m_median_index + 1); + fmt_printf(" Max: {3.3} dB\n", psnrs_stats.m_max); + fmt_printf(" Avg 5% low: {3.3} dB\n", psnrs_stats.m_five_percent_lo); + fmt_printf(" Avg 5% high: {3.3} dB\n", psnrs_stats.m_five_percent_hi); + + return avg_psnr; +} + +struct benchmark_results +{ + benchmark_results() + { + clear(); + } + + void clear() + { + clear_obj(*this); + } + + basist::basis_tex_format m_tex_fmt; + uint32_t m_effort, m_quality, m_flags; + + uint32_t m_total_images; + double m_total_time; + uint64_t m_total_input_texels; + uint64_t m_total_compressed_size; + + // LDR averages - all calculated from MSE's then converted to PSNR. + float m_avg_rgba_psnr; + float m_avg_luma_709_psnr; + float m_avg_y_hvs_psnr; + float m_avg_y_hvsm_psnr; + + // HDR averages: native ASTC HDR + transcoded BC6H, each in linear half-float and log2 space + // All calculated from MSE's then converted to PSNR. + float m_avg_astc_hdr_psnr; + float m_avg_astc_hdr_log2_psnr; + float m_avg_bc6h_psnr; + float m_avg_bc6h_log2_psnr; +}; + +// Parses a -benchmark_single format token (e.g. "XUASTC_LDR_4x4", "XUBC7", "ETC1S", "ASTC_LDR_6x6", "UASTC_LDR_4x4") +// into a basis_tex_format. Matches case-insensitively against basis_get_tex_format_name() with spaces normalized to +// underscores, so every codec the encoder supports (XUASTC LDR 4x4-12x12, XUBC7, ETC1S, ASTC LDR, etc.) is accepted. +// Returns false if no format matches. +static bool parse_benchmark_tex_format(const std::string& s, basist::basis_tex_format& out_fmt) +{ + auto normalize = [](std::string v) -> std::string + { + for (char& c : v) + c = (c == ' ') ? '_' : (char)toupper((uint8_t)c); + return v; + }; + + const std::string want(normalize(s)); + + for (uint32_t i = 0; i < (uint32_t)basist::basis_tex_format::cTotalFormats; i++) + { + const basist::basis_tex_format fmt = (basist::basis_tex_format)i; + if (normalize(basist::basis_get_tex_format_name(fmt)) == want) + { + out_fmt = fmt; + return true; + } + } + + return false; +} + +[[maybe_unused]] static bool codec_benchmark( + const std::string &base_filename, //("d:/dev/test_images/photo_png/kodim"); + const uint32_t first_test_file_index,// = 1; + const uint32_t last_test_file_index,// = 166, + basist::basis_tex_format tex_fmt, // = basist::basis_tex_format::cXUASTC_LDR_6x6; + uint32_t effort_level, uint32_t quality_level, uint32_t flags, + bool srgb_flag, // true = sRGB/perceptual metrics, false = linear + benchmark_results &results) +{ + results.m_tex_fmt = tex_fmt; + results.m_effort = effort_level; + results.m_quality = quality_level; + results.m_flags = flags; + + fmt_printf("Base filename: {}, first file index: {}, last file index: {}\n", base_filename, first_test_file_index, last_test_file_index); + + fmt_printf("Testing tex fmt: {}, Flags: 0x{X}, Effort: {}, Quality: {}\n", (uint32_t)tex_fmt, flags, effort_level, quality_level); + + const bool is_hdr = basist::basis_tex_format_is_hdr(tex_fmt); + + // sRGB vs. linear metrics only applies to LDR; HDR metrics are always linear half-float / log2, so don't set the + // sRGB flag or print the metric mode for HDR. + const bool srgb_metrics = srgb_flag && !is_hdr; + if (!is_hdr) + fmt_printf("Using {} metrics\n", srgb_metrics ? "sRGB (perceptual)" : "linear"); + + interval_timer tm; + tm.start(); + + // LDR metrics + float_vec rgba_psnrs, luma_709_psnrs, y_hvs_psnr, y_hvsm_psnr; + // HDR metrics: native ASTC HDR and transcoded BC6H RGB PSNRs, in linear half-float and log2 space + float_vec astc_hdr_psnrs, astc_hdr_log2_psnrs, bc6h_psnrs, bc6h_log2_psnrs; + + uint64_t total_compressed_size = 0; + uint64_t total_input_texels = 0; + + uint32_t flags_and_quality = cFlagThreaded | (srgb_metrics ? cFlagSRGB : 0) | flags; + + const uint32_t total_test_images = (last_test_file_index - first_test_file_index) + 1; + + for (uint32_t file_index = first_test_file_index; file_index <= last_test_file_index; file_index++) + { + // HDR formats benchmark against .exr inputs (loaded as float images); LDR against .png. The LDR sets (e.g. + // kodimNN.png) use 2-digit zero-padded indices; the HDR set (hdr_N.exr) is not padded. + std::string filename(base_filename); + const char* pExt = is_hdr ? ".exr" : ".png"; + if (!is_hdr && (file_index < 10)) + filename += fmt_string("{02}{}", file_index, pExt); + else + filename += fmt_string("{}{}", file_index, pExt); + + image orig_img; + imagef orig_imgf; + uint32_t width = 0, height = 0; + bool has_alpha = false; + + if (is_hdr) + { + if (!load_image_hdr(filename.c_str(), orig_imgf)) + { + fmt_error_printf("Failed to load HDR image file {}\n", filename); + return false; + } + width = orig_imgf.get_width(); + height = orig_imgf.get_height(); + } + else + { + if (!load_png(filename, orig_img)) + { + fmt_error_printf("Failed to load image file {}\n", filename); + return false; + } + width = orig_img.get_width(); + height = orig_img.get_height(); + has_alpha = orig_img.has_alpha(); + } + + fmt_printf("---- Loading image {}, {}x{}, has_alpha: {}\n", filename, width, height, has_alpha); + + size_t data_size = 0; + basisu::image_stats stats; + + // HDR uses the imagef (float) basis_compress2 overload; LDR uses the 8-bit image overload. + void* pData; + if (is_hdr) + { + basisu::vector source_images_hdr; + source_images_hdr.push_back(orig_imgf); + pData = basis_compress2(tex_fmt, source_images_hdr, flags_and_quality, quality_level, effort_level, &data_size, (flags & cFlagPrintStats) ? &stats : nullptr); + } + else + { + basisu::vector source_images; + source_images.push_back(orig_img); + pData = basis_compress2(tex_fmt, source_images, flags_and_quality, quality_level, effort_level, &data_size, (flags & cFlagPrintStats) ? &stats : nullptr); + } + + if (!pData) + { + error_printf("basis_compress() failed!\n"); + return false; + } + + basis_free_data(pData); + + const double bpp = ((double)data_size * 8.0f) / (double)(width * height); + + if (is_hdr) + { + // HDR: native ASTC HDR + transcoded BC6H RGB PSNRs, each in linear half-float and log2 space (see image_stats). + const float astc_hdr_psnr = stats.m_basis_rgb_avg_psnr; + const float astc_hdr_log2_psnr = stats.m_basis_rgb_avg_astc_hdr_log2_psnr; + const float bc6h_psnr = stats.m_basis_rgb_avg_bc6h_psnr; + const float bc6h_log2_psnr = stats.m_basis_rgb_avg_bc6h_log2_psnr; + + fmt_printf("Size: {}, bpp: {}, ASTC HDR RGB PSNR: {3.3} dB (log2: {3.3}), BC6H RGB PSNR: {3.3} dB (log2: {3.3})\n", + (uint64_t)data_size, bpp, astc_hdr_psnr, astc_hdr_log2_psnr, bc6h_psnr, bc6h_log2_psnr); + + astc_hdr_psnrs.push_back(astc_hdr_psnr); + astc_hdr_log2_psnrs.push_back(astc_hdr_log2_psnr); + bc6h_psnrs.push_back(bc6h_psnr); + bc6h_log2_psnrs.push_back(bc6h_log2_psnr); + } + else + { + // XUBC7 is a native BC7 codec, so its native RGBA/Luma quality lives in the m_bc7_* stats; the m_basis_* stats + // for XUBC7 reflect the ASTC LDR 4x4 transcode, not the native output. All other codecs use the m_basis_* stats. + const bool is_xubc7 = (tex_fmt == basist::basis_tex_format::cXUBC7); + const float rgba_psnr = is_xubc7 ? stats.m_bc7_rgba_avg_psnr : stats.m_basis_rgba_avg_psnr; + const float luma_709_psnr = is_xubc7 ? stats.m_bc7_luma_709_psnr : stats.m_basis_luma_709_psnr; + const psnr_hvs_metrics& hvs = is_xubc7 ? stats.m_hvs_metrics_bc7 : stats.m_hvs_metrics; + + fmt_printf("Size: {}, bpp: {}, RGBA PSNR: {3.3} dB, Luma 709 PSNR: {3.3} dB, Y PSNR-HVS: {3.3} dB, Y PSNR-HVS-M: {3.3} dB\n", + (uint64_t)data_size, + bpp, + rgba_psnr, + luma_709_psnr, + hvs.m_y_601_float.m_psnr_hvs, + hvs.m_y_601_float.m_psnr_hvsm); + + rgba_psnrs.push_back(rgba_psnr); + luma_709_psnrs.push_back(luma_709_psnr); + y_hvs_psnr.push_back((float)hvs.m_y_601_float.m_psnr_hvs); + y_hvsm_psnr.push_back((float)hvs.m_y_601_float.m_psnr_hvsm); + } + + total_compressed_size += data_size; + total_input_texels += (uint64_t)width * (uint64_t)height; + + } // file_index + + const double total_time = tm.get_elapsed_secs(); + + fmt_printf("\n-------------------------\n"); + + fmt_printf("tex fmt: {}, Flags: 0x{X}, Effort: {}, Quality: {}\n", (uint32_t)tex_fmt, flags_and_quality, effort_level, quality_level); + + fmt_printf("Total time: {3.3} secs, average time per image: {3.3} secs\n", total_time, total_time / (double)total_test_images); + + fmt_printf("Total input texels: {} megapixels, total compressed size: {} mebibytes, average bpp: {3.3}\n", (double)total_input_texels / (1e+6), (double)total_compressed_size / (1024.0f * 1024.0f), ((double)total_compressed_size * 8.0f) / (double)total_input_texels); + + results.m_total_images = total_test_images; + results.m_total_input_texels = total_input_texels; + results.m_total_compressed_size = total_compressed_size; + results.m_total_time = total_time; + + if (is_hdr) + { + fmt_printf("\nASTC HDR RGB PSNR:\n"); + results.m_avg_astc_hdr_psnr = print_psnr_stats(astc_hdr_psnrs, 100.0f); + + fmt_printf("\nASTC HDR RGB log2 PSNR:\n"); + results.m_avg_astc_hdr_log2_psnr = print_psnr_stats(astc_hdr_log2_psnrs, 100.0f); + + fmt_printf("\nBC6H RGB PSNR:\n"); + results.m_avg_bc6h_psnr = print_psnr_stats(bc6h_psnrs, 100.0f); + + fmt_printf("\nBC6H RGB log2 PSNR:\n"); + results.m_avg_bc6h_log2_psnr = print_psnr_stats(bc6h_log2_psnrs, 100.0f); + } + else + { + fmt_printf("\nRGBA PSNR:\n"); + results.m_avg_rgba_psnr = print_psnr_stats(rgba_psnrs, 100.0f); + + fmt_printf("\nLuma 709 PSNR:\n"); + results.m_avg_luma_709_psnr = print_psnr_stats(luma_709_psnrs, 100.0f); + + fmt_printf("\nY PSNR-HVS:\n"); + results.m_avg_y_hvs_psnr = print_psnr_stats(y_hvs_psnr, PSNR_HVS_LOSSLESS_DB); + + fmt_printf("\nY PSNR-HVSM:\n"); + results.m_avg_y_hvsm_psnr = print_psnr_stats(y_hvsm_psnr, PSNR_HVS_LOSSLESS_DB); + } + + return true; +} + +// -benchmark_single : single codec_benchmark() run over a numbered image set. +// LDR loads NN.png (zero-padded); HDR loads N.exr (not padded). Defaults: LDR "../test_files/kodim" 1..24, +// HDR "../test_files/hdr_" 1..6 (used only when -benchmark_path / -benchmark_range aren't given). Override via +// -benchmark_path and -benchmark_range MIN MAX. LDR metrics are sRGB/perceptual by default (-benchmark_linear selects +// linear); HDR metrics are always linear half-float / log2. +// -benchmark_xubc7_encoder [0,7] selects the XUBC7 BC7 base encoder (0=bc7f, 1-7=bc7e_scalar level 0-6; XUBC7 only). +// -benchmark_xuastc_profile [0,2] selects the XUASTC LDR entropy syntax/profile (0=full arith, 1=hybrid, 2=full zstd; XUASTC only). +// FORMAT/EFFORT/QUALITY come from the command line. Not in the help text yet. +// Example: basisu -benchmark_single XUBC7 9 100 [-benchmark_path ] [-benchmark_range 1 24] [-benchmark_linear] [-benchmark_xubc7_encoder 4] [-benchmark_xuastc_profile 2] +// For HDR codecs, if the user didn't explicitly override the image path/range, switch from the LDR kodim default to the +// HDR default set ("../test_files/hdr_N.exr", indices 1..6). LDR keeps its existing defaults. +static void apply_hdr_benchmark_image_defaults(command_line_params& opts) +{ + if (!basist::basis_tex_format_is_hdr(opts.m_benchmark_fmt)) + return; + if (!opts.m_benchmark_path_set) + opts.m_benchmark_path = "../test_files/hdr_"; + if (!opts.m_benchmark_range_set) + { + opts.m_benchmark_first = 1; + opts.m_benchmark_last = 6; + } +} + +// Packs the benchmark flag set (KTX2 + UASTC supercompression + print stats/status + the XUBC7 base encoder and XUASTC +// profile selections) and runs one codec_benchmark() over the configured image set. Shared by -benchmark_single and -benchmark_sweep. +static bool run_one_benchmark(const command_line_params& opts, + basist::basis_tex_format fmt, uint32_t effort, uint32_t quality, benchmark_results& results) +{ + // XUBC7-only: pack the BC7 base encoder selection into the high flag bits (0=bc7f, 1-7=bc7e_scalar level 0-6); ignored for other formats. + const uint32_t xubc7_encoder_flags = (opts.m_benchmark_xubc7_encoder & cFlagXUBC7BaseEncoderMask) << cFlagXUBC7BaseEncoderShift; + // XUASTC-only: pack the LDR entropy syntax/profile (0=full arith, 1=hybrid, 2=full zstd); ignored for other formats. + const uint32_t xuastc_profile_flags = (opts.m_benchmark_xuastc_profile & cFlagXUASTCLDRSyntaxMask) << cFlagXUASTCLDRSyntaxShift; + return codec_benchmark(opts.m_benchmark_path, opts.m_benchmark_first, opts.m_benchmark_last, + fmt, effort, quality, + cFlagKTX2 | cFlagKTX2UASTCSuperCompression | cFlagPrintStats | cFlagPrintStatus | xubc7_encoder_flags | xuastc_profile_flags, + !opts.m_benchmark_linear, // sRGB metrics by default; -benchmark_linear selects linear + results); +} + +static bool benchmark_single_mode(command_line_params& opts) +{ + apply_hdr_benchmark_image_defaults(opts); + benchmark_results bres; + return run_one_benchmark(opts, opts.m_benchmark_fmt, opts.m_benchmark_effort, opts.m_benchmark_quality, bres); +} + +// Maps a square-block-size sweep index [0,5] (0=4x4,1=5x5,2=6x6,3=8x8,4=10x10,5=12x12) to the basis_tex_format in +// base_fmt's family. Only XUASTC LDR and ASTC LDR have selectable block sizes; returns false for any other family. +static bool benchmark_block_index_to_fmt(basist::basis_tex_format base_fmt, uint32_t square_index, basist::basis_tex_format& out_fmt) +{ + typedef basist::basis_tex_format tf; + + const bool is_xuastc = basist::basis_tex_format_is_xuastc_ldr(base_fmt); + const bool is_astc = basist::basis_tex_format_is_astc_ldr(base_fmt); + if (!is_xuastc && !is_astc) + return false; + + switch (square_index) + { + case 0: out_fmt = is_xuastc ? tf::cXUASTC_LDR_4x4 : tf::cASTC_LDR_4x4; return true; + case 1: out_fmt = is_xuastc ? tf::cXUASTC_LDR_5x5 : tf::cASTC_LDR_5x5; return true; + case 2: out_fmt = is_xuastc ? tf::cXUASTC_LDR_6x6 : tf::cASTC_LDR_6x6; return true; + case 3: out_fmt = is_xuastc ? tf::cXUASTC_LDR_8x8 : tf::cASTC_LDR_8x8; return true; + case 4: out_fmt = is_xuastc ? tf::cXUASTC_LDR_10x10 : tf::cASTC_LDR_10x10; return true; + case 5: out_fmt = is_xuastc ? tf::cXUASTC_LDR_12x12 : tf::cASTC_LDR_12x12; return true; + default: return false; + } +} + +// -benchmark_sweep : sweeps any combination of block size, effort, and quality for one codec, writing a CSV +// (one row per combination) for spreadsheet analysis. Axis ranges (inclusive low/high): -benchmark_sweep_block [0,5] +// (square sizes 4x4..12x12; XUASTC_LDR/ASTC_LDR only - error otherwise; if omitted, FORMAT's own block size is used and +// no block sweep happens), -benchmark_sweep_effort [0,10] (default 3 3), -benchmark_sweep_quality [0,10] (quality = +// clamp(q*10,1,100); default 10 10 => quality 100). Loop order is block (major), then effort, then quality (minor). +// Metrics: LDR = RGBA + Y PSNR-HVS-M; HDR = non-log2 ASTC HDR RGB PSNR. Output CSV via -benchmark_sweep_csv (default +// "benchmark_sweep.csv"). Other -benchmark_* options apply per run. +// Not in the help text yet. Example: basisu -benchmark_sweep XUASTC_LDR_6x6 -benchmark_sweep_block 0 5 -benchmark_sweep_quality 0 10 +static bool benchmark_sweep_mode(command_line_params& opts) +{ + apply_hdr_benchmark_image_defaults(opts); + + const basist::basis_tex_format base_fmt = opts.m_benchmark_fmt; + const bool is_hdr = basist::basis_tex_format_is_hdr(base_fmt); + const bool block_family = basist::basis_tex_format_is_xuastc_ldr(base_fmt) || basist::basis_tex_format_is_astc_ldr(base_fmt); + + // Build the block-size axis. If -benchmark_sweep_block was given, sweep square block-size indices [lo,hi] within the + // codec's family. If it was NOT given, default to exactly the codec/block size specified (e.g. XUASTC_LDR_6x6 stays 6x6, + // ETC1S/XUBC7/HDR stay as-is) - no block sweep. + basisu::vector block_fmts; + if (opts.m_benchmark_sweep_block_set) + { + if (!block_family) + { + fmt_error_printf("-benchmark_sweep_block requires an XUASTC_LDR or ASTC_LDR codec family\n"); + return false; + } + uint32_t blo = opts.m_benchmark_sweep_block_lo, bhi = opts.m_benchmark_sweep_block_hi; + if (blo > bhi) std::swap(blo, bhi); + for (uint32_t b = blo; b <= bhi; b++) + { + basist::basis_tex_format f; + if (!benchmark_block_index_to_fmt(base_fmt, b, f)) + { + fmt_error_printf("-benchmark_sweep: invalid block size index {}\n", b); + return false; + } + block_fmts.push_back(f); + } + } + else + { + block_fmts.push_back(base_fmt); // default: the exact codec/block size specified + } + + uint32_t elo = opts.m_benchmark_sweep_effort_lo, ehi = opts.m_benchmark_sweep_effort_hi; + if (elo > ehi) std::swap(elo, ehi); + uint32_t qlo = opts.m_benchmark_sweep_quality_lo, qhi = opts.m_benchmark_sweep_quality_hi; + if (qlo > qhi) std::swap(qlo, qhi); + + const uint32_t num_blocks = (uint32_t)block_fmts.size(); + const uint32_t num_efforts = (ehi - elo) + 1; + const uint32_t num_qualities = (qhi - qlo) + 1; + + // Announce the actual sweep up front. With -benchmark_sweep_block the block axis sweeps square sizes within the codec's + // family (XUASTC/ASTC only; everything else is fixed 4x4); without it, the codec's own block size is used. + fmt_printf("Benchmark sweep: codec {}, {} block size(s), effort {}..{}, quality(x10) {}..{}, {} runs total\n", + basist::basis_get_tex_format_name(base_fmt), num_blocks, elo, ehi, qlo, qhi, num_blocks * num_efforts * num_qualities); + for (uint32_t bi = 0; bi < num_blocks; bi++) + { + uint32_t bw = 0, bh = 0; + basist::get_basis_tex_format_block_size(block_fmts[bi], bw, bh); + fmt_printf(" block[{}]: {} ({}x{})\n", bi, basist::basis_get_tex_format_name(block_fmts[bi]), bw, bh); + } + + // Open the CSV up front so a bad path fails before the (potentially long) sweep runs. + FILE* pFile = fopen(opts.m_benchmark_sweep_csv.c_str(), "w"); + if (!pFile) + { + fmt_error_printf("Failed to open output CSV file \"{}\" for writing!\n", opts.m_benchmark_sweep_csv); + return false; + } + + if (is_hdr) + { + // HDR metrics are always linear half-float / log2 (sRGB vs linear doesn't apply), so no metrics field. + fmt_fprintf(pFile, "// codec: {}, images: {}N.exr indices {}..{}\n", + basist::basis_get_tex_format_name(base_fmt), opts.m_benchmark_path, opts.m_benchmark_first, opts.m_benchmark_last); + fmt_fprintf(pFile, "// fmt, block, effort, quality, bitrate, astc_hdr_rgb_psnr\n"); + } + else + { + fmt_fprintf(pFile, "// codec: {}, metrics: {}, images: {}NN.png indices {}..{}\n", + basist::basis_get_tex_format_name(base_fmt), opts.m_benchmark_linear ? "linear" : "sRGB (perceptual)", + opts.m_benchmark_path, opts.m_benchmark_first, opts.m_benchmark_last); + fmt_fprintf(pFile, "// fmt, block, effort, quality, bitrate, rgba_psnr, y_hvsm_psnr\n"); + } + + // Run all combinations, storing results in a 3D [block][effort][quality] array, then write the CSV (one row per + // combination; loop order block major, then effort, then quality). + basisu::vector>> results; + results.resize(num_blocks); + + for (uint32_t bi = 0; bi < num_blocks; bi++) + { + results[bi].resize(num_efforts); + for (uint32_t ei = 0; ei < num_efforts; ei++) + { + results[bi][ei].resize(num_qualities); + for (uint32_t qi = 0; qi < num_qualities; qi++) + { + const uint32_t effort = elo + ei; + const uint32_t quality = (uint32_t)clamp((int)(qlo + qi) * 10, 1, 100); + + if (!run_one_benchmark(opts, block_fmts[bi], effort, quality, results[bi][ei][qi])) + { + fclose(pFile); + return false; + } + } + } + } + + for (uint32_t bi = 0; bi < num_blocks; bi++) + { + for (uint32_t ei = 0; ei < num_efforts; ei++) + { + for (uint32_t qi = 0; qi < num_qualities; qi++) + { + const benchmark_results& r = results[bi][ei][qi]; + + uint32_t bw = 0, bh = 0; + basist::get_basis_tex_format_block_size(r.m_tex_fmt, bw, bh); + const double bitrate = (double)r.m_total_compressed_size * 8.0 / (double)r.m_total_input_texels; + + if (is_hdr) + fmt_fprintf(pFile, "{}, {}x{}, {}, {}, {3.4}, {3.4}\n", + basist::basis_get_tex_format_name(r.m_tex_fmt), bw, bh, r.m_effort, r.m_quality, bitrate, r.m_avg_astc_hdr_psnr); + else + fmt_fprintf(pFile, "{}, {}x{}, {}, {}, {3.4}, {3.4}, {3.4}\n", + basist::basis_get_tex_format_name(r.m_tex_fmt), bw, bh, r.m_effort, r.m_quality, bitrate, r.m_avg_rgba_psnr, r.m_avg_y_hvsm_psnr); + } + } + } + + fclose(pFile); + + fmt_printf("Wrote benchmark sweep CSV \"{}\"\n", opts.m_benchmark_sweep_csv); + return true; +} + +// -test_dds: standalone test harness for the basist::dds_transcoder DX9/DX10 DDS reader+transcoder. +// Usage: basisu -test_dds [output_prefix] +// Generate inputs with the existing exporter, e.g.: +// basisu -ktx2 -uastc .png ; basisu -file .ktx2 -export_dds (or BC7/array/cubemap variants) +static bool test_dds(int argc, const char* argv[]) +{ + if (argc < 3) + { + fmt_error_printf("Usage: basisu -test_dds [output_prefix]\n"); + return false; + } + + const char* pFilename = argv[2]; + const std::string out_prefix = (argc >= 4) ? std::string(argv[3]) : std::string("dds_test"); + + uint8_vec file_data; + if (!read_file_to_vec(pFilename, file_data)) + { + fmt_error_printf("Failed reading file \"{}\"\n", pFilename); + return false; + } + fmt_printf("Read {} bytes from \"{}\"\n", file_data.size_u32(), pFilename); + + basist::dds_transcoder dds; + if (!dds.init(file_data.data(), file_data.size_u32())) + { + fmt_error_printf("dds_transcoder::init() failed (unsupported or malformed DDS)\n"); + return false; + } + if (!dds.start_transcoding()) + { + fmt_error_printf("dds_transcoder::start_transcoding() failed\n"); + return false; + } + + const uint32_t levels = dds.get_levels(); + const uint32_t eff_layers = dds.get_layers() ? dds.get_layers() : 1; + const uint32_t faces = dds.get_faces(); + const basist::transcoder_texture_format contained = dds.get_format(); + + fmt_printf("DDS: {}x{}, levels: {}, layers: {} ({}), faces: {} ({})\n", + dds.get_width(), dds.get_height(), levels, dds.get_layers(), dds.get_layers() ? "array" : "non-array", + faces, dds.get_is_cubemap() ? "cubemap" : "2D"); + fmt_printf("Contained format: {} (enum {}), has_alpha: {}, sRGB: {}\n", + basist::basis_get_format_name(contained), (int)contained, (uint32_t)dds.get_has_alpha(), dds.is_srgb() ? 1 : 0); + + // Exercise is_transcode_format_supported() (PVRTC1 depends on pow2 dims; cTFBC6H/PVRTC2 are unsupported). + { + const bool pow2 = is_pow2(dds.get_width()) && is_pow2(dds.get_height()); + const basist::transcoder_texture_format probe[] = { + basist::transcoder_texture_format::cTFBC7_RGBA, basist::transcoder_texture_format::cTFASTC_4x4_RGBA, + basist::transcoder_texture_format::cTFRGBA32, basist::transcoder_texture_format::cTFPVRTC1_4_RGB, + basist::transcoder_texture_format::cTFBC6H, basist::transcoder_texture_format::cTFPVRTC2_4_RGB + }; + fmt_printf("is_transcode_format_supported (pow2 dims: {}):\n", pow2 ? 1 : 0); + for (basist::transcoder_texture_format f : probe) + fmt_printf(" {}: {}\n", basist::basis_get_format_name(f), dds.is_transcode_format_supported(f) ? "yes" : "no"); + } + + // Dump per-image info. + for (uint32_t layer = 0; layer < eff_layers; layer++) + for (uint32_t face = 0; face < faces; face++) + for (uint32_t level = 0; level < levels; level++) + { + basist::ktx2_image_level_info li; + if (!dds.get_image_level_info(li, level, layer, face)) + { + fmt_error_printf("get_image_level_info failed at A{} F{} M{}\n", layer, face, level); + return false; + } + fmt_printf(" A{} F{} M{}: {}x{} ({}x{} blocks, {} total)\n", + layer, face, level, li.m_orig_width, li.m_orig_height, li.m_num_blocks_x, li.m_num_blocks_y, li.m_total_blocks); + } + + // Decode every image to RGBA32 and save as PNG (visual verification). + uint32_t png_count = 0; + for (uint32_t layer = 0; layer < eff_layers; layer++) + { + for (uint32_t face = 0; face < faces; face++) + { + for (uint32_t level = 0; level < levels; level++) + { + basist::ktx2_image_level_info li; + dds.get_image_level_info(li, level, layer, face); + + image img(li.m_orig_width, li.m_orig_height); + if (!dds.transcode_image_level(level, layer, face, img.get_ptr(), img.get_total_pixels(), + basist::transcoder_texture_format::cTFRGBA32, 0, img.get_width(), img.get_height())) + { + fmt_error_printf("transcode to RGBA32 failed at A{} F{} M{}\n", layer, face, level); + return false; + } + + char fn[512]; + snprintf(fn, sizeof(fn), "%s_A%u_F%u_M%u.png", out_prefix.c_str(), layer, face, level); + if (!save_png(fn, img)) + { + fmt_error_printf("save_png failed for \"{}\"\n", fn); + return false; + } + png_count++; + } + } + } + fmt_printf("Wrote {} decoded RGBA32 PNG(s) (prefix \"{}\")\n", png_count, out_prefix.c_str()); + + // Reference RGBA32 decode of the base image, for round-trip PSNR of re-encoded targets. + basist::ktx2_image_level_info base_li; + dds.get_image_level_info(base_li, 0, 0, 0); + image ref_img(base_li.m_orig_width, base_li.m_orig_height); + dds.transcode_image_level(0, 0, 0, ref_img.get_ptr(), ref_img.get_total_pixels(), + basist::transcoder_texture_format::cTFRGBA32, 0, ref_img.get_width(), ref_img.get_height()); + + // Exercise every supported re-encode target on the base image. + struct target_row { basist::transcoder_texture_format fmt; basisu::texture_format unpack_fmt; bool can_unpack; }; + const target_row targets[] = + { + { basist::transcoder_texture_format::cTFBC1_RGB, basisu::texture_format::cBC1, true }, + { basist::transcoder_texture_format::cTFBC3_RGBA, basisu::texture_format::cBC3, true }, + { basist::transcoder_texture_format::cTFBC4_R, basisu::texture_format::cBC4, true }, + { basist::transcoder_texture_format::cTFBC5_RG, basisu::texture_format::cBC5, true }, + { basist::transcoder_texture_format::cTFBC7_RGBA, basisu::texture_format::cBC7, true }, + { basist::transcoder_texture_format::cTFETC1_RGB, basisu::texture_format::cInvalidTextureFormat, false }, + { basist::transcoder_texture_format::cTFETC2_RGBA, basisu::texture_format::cInvalidTextureFormat, false }, + { basist::transcoder_texture_format::cTFASTC_4x4_RGBA, basisu::texture_format::cInvalidTextureFormat, false }, + { basist::transcoder_texture_format::cTFPVRTC1_4_RGB, basisu::texture_format::cInvalidTextureFormat, false }, + { basist::transcoder_texture_format::cTFRGB565, basisu::texture_format::cInvalidTextureFormat, false }, + { basist::transcoder_texture_format::cTFRGBA4444, basisu::texture_format::cInvalidTextureFormat, false }, + }; + + // For the re-encode tests use a 4x4-grid sized image (covers compressed targets cleanly). + const uint32_t grid_blocks_x = (base_li.m_orig_width + 3) / 4; + const uint32_t grid_blocks_y = (base_li.m_orig_height + 3) / 4; + + for (const target_row& t : targets) + { + const bool uncompressed = (t.fmt == basist::transcoder_texture_format::cTFRGBA32) || + (t.fmt == basist::transcoder_texture_format::cTFRGB565) || (t.fmt == basist::transcoder_texture_format::cTFRGBA4444); + const bool is_pvrtc1 = (t.fmt == basist::transcoder_texture_format::cTFPVRTC1_4_RGB) || (t.fmt == basist::transcoder_texture_format::cTFPVRTC1_4_RGBA); + + if (is_pvrtc1 && (!is_pow2(base_li.m_orig_width) || !is_pow2(base_li.m_orig_height))) + { + fmt_printf(" {}: skipped (PVRTC1 needs power-of-2 dims)\n", basist::basis_get_format_name(t.fmt)); + continue; + } + + const uint32_t bytes_per = basist::basis_get_bytes_per_block_or_pixel(t.fmt); + uint32_t buf_units, byte_size; + if (uncompressed) + { + buf_units = base_li.m_orig_width * base_li.m_orig_height; + byte_size = buf_units * bytes_per; + } + else + { + buf_units = grid_blocks_x * grid_blocks_y; + byte_size = buf_units * bytes_per; + } + + uint8_vec out_buf(byte_size); + const uint32_t row_pitch = uncompressed ? base_li.m_orig_width : 0; + const uint32_t rows = uncompressed ? base_li.m_orig_height : 0; + + if (!dds.transcode_image_level(0, 0, 0, out_buf.data(), buf_units, t.fmt, 0, row_pitch, rows)) + { + fmt_error_printf(" {}: transcode FAILED\n", basist::basis_get_format_name(t.fmt)); + return false; + } + + // Round-trip PSNR for the BC formats we can unpack directly. + if (t.can_unpack) + { + image rt(grid_blocks_x * 4, grid_blocks_y * 4); + for (uint32_t by = 0; by < grid_blocks_y; by++) + for (uint32_t bx = 0; bx < grid_blocks_x; bx++) + { + color_rgba pixels[16]; + unpack_block(t.unpack_fmt, out_buf.data() + (by * grid_blocks_x + bx) * bytes_per, pixels, false); + for (uint32_t y = 0; y < 4; y++) + for (uint32_t x = 0; x < 4; x++) + rt.set_clipped(bx * 4 + x, by * 4 + y, pixels[y * 4 + x]); + } + + image_metrics m; + m.calc(ref_img, rt, 0, 3); + const double rgb_psnr = m.m_psnr; + m.calc(ref_img, rt, 0, 4); + fmt_printf(" {}: OK {} bytes, RGB PSNR {}, RGBA PSNR {}\n", + basist::basis_get_format_name(t.fmt), byte_size, rgb_psnr, m.m_psnr); + } + else + { + fmt_printf(" {}: OK {} bytes\n", basist::basis_get_format_name(t.fmt), byte_size); + } + } + + fmt_printf("-test_dds: success\n"); + return true; +} + +// ---- -test_dds_overwrite: transcoder output-buffer SANITY (overrun) test ---- +// This is a SEPARATE test from -test_dds, and a different kind of test: it does NOT check "did we get the right +// pixels?" -- it checks "did dds_transcoder::transcode_image_level() ever write past the buffer I declared, and does +// it correctly REJECT bogus arguments?". For every .dds under the given root (default c:\dev\dds_files), for every +// (level,layer,face) and every supported output format (compressed AND uncompressed), we transcode into a +// basisu::vector that is deliberately LARGER than required and pre-filled with the 0xDEADBEEF byte pattern, +// then verify the bytes at/after the declared buffer size are still pristine (nothing was overwritten). We exercise +// the optional output_row_pitch / output_rows overrides -- expand AND shrink for uncompressed targets (e.g. a 4x4 DDS +// unpacked into a clipped 3x3 region), expand-only for compressed -- plus a battery of must-reject argument errors +// (undersize buffer, shrunken compressed pitch, PVRTC1 non-tight pitch, out-of-range level/layer/face, null output). +// A rejected call must also leave the buffer entirely pristine. Intended to be run against a DEBUG build. +// Usage: basisu -test_dds_overwrite [root_dir_or_file] +static const uint8_t s_deadbeef[4] = { 0xEF, 0xBE, 0xAD, 0xDE }; // little-endian 0xDEADBEEF + +static void fill_deadbeef(uint8_vec& buf) +{ + for (size_t i = 0; i < buf.size(); i++) + buf[i] = s_deadbeef[i & 3]; +} + +struct dds_file_entry { std::string m_path; uint64_t m_size; }; + +// Returns the index of the first byte in [start,end) that is no longer the 0xDEADBEEF pattern, or SIZE_MAX if clean. +static size_t check_deadbeef(const uint8_vec& buf, size_t start, size_t end) +{ + for (size_t i = start; i < end; i++) + if (buf[i] != s_deadbeef[i & 3]) + return i; + return SIZE_MAX; +} + +static bool test_dds_overwrite(int argc, const char* argv[]) +{ + const std::string root = (argc >= 3) ? std::string(argv[2]) : std::string("c:\\dev\\dds_files"); + + // Gather every .dds file under root (recursively) via C++17 std::filesystem; allow a single file too. + std::vector dds_files; + { + std::error_code ec; + const std::filesystem::path root_path(root); + const std::filesystem::file_status st = std::filesystem::status(root_path, ec); + if (ec || !std::filesystem::exists(st)) + { + fmt_error_printf("Root path \"{}\" does not exist\n", root.c_str()); + return false; + } + if (std::filesystem::is_directory(st)) + { + for (std::filesystem::recursive_directory_iterator it(root_path, std::filesystem::directory_options::skip_permission_denied, ec), end; it != end; it.increment(ec)) + { + if (ec) { ec.clear(); continue; } + std::error_code fec; + if (!it->is_regular_file(fec)) + continue; + std::string ext = it->path().extension().string(); + for (char& c : ext) c = (char)tolower((unsigned char)c); + if (ext == ".dds") + { + const uintmax_t sz = it->file_size(fec); + dds_files.push_back({ it->path().string(), fec ? 0ull : (uint64_t)sz }); + } + } + } + else + { + dds_files.push_back({ root, 0 }); // explicit single file: size 0 => never skipped by the cap below + } + } + + if (dds_files.empty()) + { + fmt_error_printf("No .dds files found under \"{}\"\n", root.c_str()); + return false; + } + std::sort(dds_files.begin(), dds_files.end(), [](const dds_file_entry& a, const dds_file_entry& b) { return a.m_path < b.m_path; }); + fmt_printf("-test_dds_overwrite: found {} .dds file(s) under \"{}\"\n", (uint64_t)dds_files.size(), root.c_str()); + + uint64_t total_calls = 0, total_overwrite_failures = 0, total_expectation_mismatches = 0; + uint32_t files_skipped = 0; + + const size_t GUARD_BYTES = 1024; // sentinel margin past the declared buffer to catch overruns + + enum call_kind { KIND_PROBE, KIND_POSITIVE, KIND_MUST_REJECT }; + + // A Debug re-encode of an enormous file (e.g. a multi-hundred-MB BC7 texture array) to every output format x + // pitch/rows combo is impractical; skip files past this cap. Bounds behavior doesn't depend on image size. + const uint64_t MAX_FILE_BYTES = 64ull * 1024 * 1024; + + for (const dds_file_entry& fe : dds_files) + { + const std::string& fn = fe.m_path; + if (fe.m_size > MAX_FILE_BYTES) + { + fmt_printf(" SKIP (too large for Debug test, {} MB): \"{}\"\n", (uint64_t)(fe.m_size / (1024 * 1024)), fn.c_str()); + files_skipped++; + continue; + } + + uint8_vec file_data; + if (!read_file_to_vec(fn.c_str(), file_data)) + { + fmt_printf(" SKIP (read failed): \"{}\"\n", fn.c_str()); + files_skipped++; + continue; + } + + basist::dds_transcoder dds; + if (!dds.init(file_data.data(), file_data.size_u32()) || !dds.start_transcoding()) + { + // The corpus may legitimately contain DDS variants this transcoder doesn't accept -- not a test failure. + fmt_printf(" SKIP (init failed): \"{}\"\n", fn.c_str()); + files_skipped++; + continue; + } + + const uint32_t levels = dds.get_levels(); + const uint32_t eff_layers = dds.get_layers() ? dds.get_layers() : 1; + const uint32_t faces = dds.get_faces(); + + uint64_t file_calls = 0, file_fail = 0; + + // One transcode into a guarded, sentinel-filled buffer. Verifies: (1) nothing written at/after the declared + // size; (2) a rejected call wrote nothing anywhere; (3) the success/failure expectation. Returns the result. + auto run_one = [&](uint32_t lvl, uint32_t lyr, uint32_t fc, + basist::transcoder_texture_format f, uint32_t bytes_per, + uint32_t buf_units, uint32_t P, uint32_t R, + call_kind kind, bool path_active, const char* desc) -> bool + { + total_calls++; file_calls++; + const size_t declared_bytes = (size_t)buf_units * bytes_per; + uint8_vec buf(declared_bytes + GUARD_BYTES); + fill_deadbeef(buf); + + const bool ok = dds.transcode_image_level(lvl, lyr, fc, buf.data(), buf_units, f, 0, P, R); + + // (1) CORE CHECK: the transcoder may never touch a byte at/after the declared buffer size. + const size_t bad_guard = check_deadbeef(buf, declared_bytes, buf.size()); + if (bad_guard != SIZE_MAX) + { + fmt_error_printf(" *** OVERWRITE PAST BUFFER: {} | {} [{}] L{} A{} F{} P={} R={} units={} -> wrote +{} bytes past declared {} ***\n", + fn.c_str(), basist::basis_get_format_name(f), desc, lvl, lyr, fc, P, R, buf_units, + (uint64_t)(bad_guard - declared_bytes), (uint64_t)declared_bytes); + total_overwrite_failures++; file_fail++; + } + + // (2) A rejected transcode must not have written ANYTHING into the buffer. + if (!ok) + { + const size_t bad_any = check_deadbeef(buf, 0, buf.size()); + if (bad_any != SIZE_MAX) + { + fmt_error_printf(" *** REJECTED CALL WROTE TO BUFFER: {} | {} [{}] L{} A{} F{} P={} R={} at byte {} ***\n", + fn.c_str(), basist::basis_get_format_name(f), desc, lvl, lyr, fc, P, R, (uint64_t)bad_any); + total_overwrite_failures++; file_fail++; + } + } + + // (3) Expectation. A must-reject call that succeeds, or a should-succeed call (when the path is active) + // that fails, is a mismatch. The PROBE kind only establishes whether the path is active (no expectation). + if ((kind == KIND_MUST_REJECT) && ok) + { + fmt_error_printf(" *** EXPECTED REJECTION BUT SUCCEEDED: {} | {} [{}] L{} A{} F{} P={} R={} units={} ***\n", + fn.c_str(), basist::basis_get_format_name(f), desc, lvl, lyr, fc, P, R, buf_units); + total_expectation_mismatches++; file_fail++; + } + else if ((kind == KIND_POSITIVE) && path_active && !ok) + { + fmt_error_printf(" *** EXPECTED SUCCESS BUT FAILED: {} | {} [{}] L{} A{} F{} P={} R={} units={} ***\n", + fn.c_str(), basist::basis_get_format_name(f), desc, lvl, lyr, fc, P, R, buf_units); + total_expectation_mismatches++; file_fail++; + } + return ok; + }; + + for (uint32_t lyr = 0; lyr < eff_layers; lyr++) + for (uint32_t fc = 0; fc < faces; fc++) + for (uint32_t lvl = 0; lvl < levels; lvl++) + { + basist::ktx2_image_level_info li; + if (!dds.get_image_level_info(li, lvl, lyr, fc)) + { + fmt_error_printf(" get_image_level_info failed L{} A{} F{} in \"{}\"\n", lvl, lyr, fc, fn.c_str()); + file_fail++; + continue; + } + const uint32_t W = li.m_orig_width, H = li.m_orig_height; + // DDS blocks are always 4x4; the output 4x4-block grid is ceil(dim/4) regardless of source kind. + const uint32_t nbx = (W + 3) / 4, nby = (H + 3) / 4; + + for (int fi = 0; fi < (int)basist::transcoder_texture_format::cTFTotalTextureFormats; fi++) + { + const basist::transcoder_texture_format f = (basist::transcoder_texture_format)fi; + if (!dds.is_transcode_format_supported(f)) + continue; + + const bool uncompressed = basist::basis_transcoder_format_is_uncompressed(f); + const bool is_pvrtc = (f == basist::transcoder_texture_format::cTFPVRTC1_4_RGB) || + (f == basist::transcoder_texture_format::cTFPVRTC1_4_RGBA); + const uint32_t bytes_per = basist::basis_get_bytes_per_block_or_pixel(f); + + // Declared output units for a given (pitch,rows) per the transcode contract: + // uncompressed: (pitch?:width) * (rows?:height) pixels; compressed: (pitch?:num_blocks_x) * num_blocks_y blocks. + auto declared_units = [&](uint32_t P, uint32_t R) -> uint32_t + { + if (uncompressed) + return (P ? P : W) * (R ? R : H); + return (P ? P : nbx) * nby; + }; + + // PROBE the default pitch/rows: is this format's transcode path active for this source+build? + // (A decode->repack target returns false in a build without BASISD_SUPPORT_XUASTC; don't flag that.) + const uint32_t du_default = declared_units(0, 0); + const bool path_active = run_one(lvl, lyr, fc, f, bytes_per, du_default, 0, 0, KIND_PROBE, false, "default"); + + // ---- Positive combos (should succeed whenever the path is active) ---- + struct PR { uint32_t P, R; const char* desc; }; + std::vector pos; + if (uncompressed) + { + pos.push_back({ W + 5, 0, "expand-pitch" }); + pos.push_back({ W + 5, H + 3, "expand-pitch+rows" }); + pos.push_back({ 0, H + 3, "expand-rows" }); + if (W >= 2) { pos.push_back({ W - 1, 0, "shrink-pitch-1" }); pos.push_back({ (W + 1) / 2, 0, "shrink-pitch-half" }); } + if (H >= 2) { pos.push_back({ 0, H - 1, "shrink-rows-1" }); pos.push_back({ 0, (H + 1) / 2, "shrink-rows-half" }); } + if ((W >= 3) && (H >= 3)) pos.push_back({ 3, 3, "clip-to-3x3" }); + } + else if (!is_pvrtc) + { + pos.push_back({ nbx + 2, 0, "expand-pitch" }); + } + for (const PR& c : pos) + run_one(lvl, lyr, fc, f, bytes_per, declared_units(c.P, c.R), c.P, c.R, KIND_POSITIVE, path_active, c.desc); + + // ---- Must-reject combos ---- + if (du_default >= 1) // buffer one unit too small for the default extent + run_one(lvl, lyr, fc, f, bytes_per, du_default - 1, 0, 0, KIND_MUST_REJECT, false, "undersize-buffer"); + if (is_pvrtc) // PVRTC1 is whole-image: any non-tight pitch must be rejected + run_one(lvl, lyr, fc, f, bytes_per, declared_units(0, 0), nbx + 2, 0, KIND_MUST_REJECT, false, "pvrtc-nontight-pitch"); + else if (!uncompressed && (nbx >= 2)) // compressed pitch < num_blocks_x would drop blocks (generous buffer, so only the pitch is bad) + run_one(lvl, lyr, fc, f, bytes_per, nbx * nby, nbx - 1, 0, KIND_MUST_REJECT, false, "compressed-shrink-pitch"); + } // fmt + + // ---- Per-(level,layer,face) must-reject: out-of-range indices & null output (format-independent) ---- + { + const basist::transcoder_texture_format f = basist::transcoder_texture_format::cTFRGBA32; + const uint32_t bytes_per = basist::basis_get_bytes_per_block_or_pixel(f); + const uint32_t du = W * H; + run_one(levels, lyr, fc, f, bytes_per, du, 0, 0, KIND_MUST_REJECT, false, "bad-level"); + run_one(lvl, eff_layers, fc, f, bytes_per, du, 0, 0, KIND_MUST_REJECT, false, "bad-layer"); + run_one(lvl, lyr, faces, f, bytes_per, du, 0, 0, KIND_MUST_REJECT, false, "bad-face"); + + total_calls++; file_calls++; + if (dds.transcode_image_level(lvl, lyr, fc, nullptr, du, f, 0, 0, 0)) + { + fmt_error_printf(" *** EXPECTED REJECTION (null output) BUT SUCCEEDED: {} L{} A{} F{} ***\n", fn.c_str(), lvl, lyr, fc); + total_expectation_mismatches++; file_fail++; + } + } + } // level/layer/face + + if (file_fail) + fmt_error_printf(" {} : {} call(s), {} FAILURE(s)\n", fn.c_str(), (uint64_t)file_calls, (uint64_t)file_fail); + else + fmt_printf(" {} : {} calls OK\n", fn.c_str(), (uint64_t)file_calls); + fflush(stdout); // stream per-file progress (stdout is block-buffered when redirected) + } // file + + fmt_printf("\n-test_dds_overwrite summary: {} call(s) across {} file(s) ({} skipped)\n", + (uint64_t)total_calls, (uint64_t)dds_files.size(), files_skipped); + fmt_printf(" buffer-overwrite failures: {}\n", (uint64_t)total_overwrite_failures); + fmt_printf(" expectation mismatches: {}\n", (uint64_t)total_expectation_mismatches); + + const bool success = (total_overwrite_failures == 0) && (total_expectation_mismatches == 0); + fmt_printf("-test_dds_overwrite: {}\n", success ? "SUCCESS" : "*** FAILED ***"); + return success; +} + +// Maps an -export_dds format string to a transcoder_texture_format. Accepts the +// DirectX BC formats DDS supports plus uncompressed 32-bit RGBA. Case-insensitive. +static bool parse_export_dds_format(const std::string& s, basist::transcoder_texture_format& fmt) +{ + typedef basist::transcoder_texture_format tf; + + if (strcasecmp(s.c_str(), "BC1") == 0) + fmt = tf::cTFBC1_RGB; + else if (strcasecmp(s.c_str(), "BC3") == 0) + fmt = tf::cTFBC3_RGBA; + else if (strcasecmp(s.c_str(), "BC4") == 0) + fmt = tf::cTFBC4_R; + else if (strcasecmp(s.c_str(), "BC5") == 0) + fmt = tf::cTFBC5_RG; + else if (strcasecmp(s.c_str(), "BC6H") == 0) + fmt = tf::cTFBC6H; + else if (strcasecmp(s.c_str(), "BC7") == 0) + fmt = tf::cTFBC7_RGBA; + else if (strcasecmp(s.c_str(), "RGBA32") == 0) + fmt = tf::cTFRGBA32; + else + return false; + + return true; +} + +// Writes a basic DX10 .dds file from source image(s). Uses basis_compressor in process_source_images() +// mode to load/mipmap/lay-out the slices, then packs them to the requested format (see basisu_dds_export.*). +static bool dds_mode(command_line_params& opts) +{ + if (!opts.m_input_filenames.size()) + { + error_printf("-dds: no input image file(s) specified (use -file or list them on the command line)\n"); + return false; + } + + dds_export_params dds_params; + if (!opts.m_dds_format.size()) + { + // No -dds_format given: default to bc7 (a reasonable general-purpose default), but warn. + printf("Warning: -dds: no -dds_format specified; defaulting to BC7\n"); + dds_params.m_format = cDDSFmtBC7; + } + else if (!parse_dds_output_format(opts.m_dds_format.c_str(), dds_params.m_format)) + { + error_printf("-dds: invalid -dds_format \"%s\". Valid: BC1 BC2 BC3 BC4 BC5 BC7 A8R8G8B8 A8B8G8R8 R8 R8G8 R5G6B5 A1R5G5B5 A4R4G4B4\n", opts.m_dds_format.c_str()); + return false; + } + + // Apply optional BC7 packer overrides (only meaningful for -dds_format bc7). + if (opts.m_dds_bc7_encoder != cDDSBC7Encoder_Default) + dds_params.m_bc7_encoder = opts.m_dds_bc7_encoder; + if (opts.m_dds_bc7f_level >= 0) + dds_params.m_bc7f_level = opts.m_dds_bc7f_level; + if (opts.m_dds_bc7e_scalar_level >= 0) + dds_params.m_bc7e_scalar_level = opts.m_dds_bc7e_scalar_level; + + // Only 2D, 2D arrays, and cubemaps/cubemap arrays are supported. Reject volume/video (we always write a + // 2D-dimension DX10 header), rather than silently producing a mislabeled 2D-array file. + const basist::basis_texture_type tex_type = opts.m_comp_params.m_tex_type; + if ((tex_type != basist::cBASISTexType2D) && (tex_type != basist::cBASISTexType2DArray) && (tex_type != basist::cBASISTexTypeCubemapArray)) + { + error_printf("-dds: -tex_type %u is not supported; use 2d, 2darray, or cubemap.\n", (uint32_t)tex_type); + return false; + } + + // -dds is LDR only (we force the XUBC7/LDR prep path below); reject HDR up front rather than silently + // down-converting an HDR request. + if (opts.m_comp_params.m_hdr) + { + error_printf("-dds: HDR is not supported (LDR only).\n"); + return false; + } + + // Thread pool size, same approach as compress_mode(). + uint32_t num_threads = 1; + if (opts.m_comp_params.m_multithreading) + { + num_threads = get_num_hardware_threads(); + if (num_threads < 1) + num_threads = 1; + if (num_threads > opts.m_max_threads) + num_threads = opts.m_max_threads; + } + job_pool jpool(num_threads); + + // Run the compressor in source-images-only mode: it loads the images, generates mipmaps, and lays out the + // cubemap/array slices, but does NOT encode. We inherit the relevant CLI options (mip/cubemap/array, + // sRGB/linear, swizzle, resample, y_flip, ...) from m_comp_params. + basis_compressor_params params = opts.m_comp_params; + params.m_pJob_pool = &jpool; + params.m_read_source_images = true; + params.m_source_filenames = opts.m_input_filenames; + params.m_write_output_basis_or_ktx2_files = false; + + // Spoof the format to XUBC7 so the LDR prep path runs (keeps RGBA together, 4x4 block layout), and enable + // the KTX2 constraint check (all images same resolution + same mip count == the DDS array/cubemap constraints). + params.set_format_mode(basist::basis_tex_format::cXUBC7); + params.m_create_ktx2_file = true; + + basis_compressor comp; + if (!comp.init(params)) + { + error_printf("-dds: basis_compressor::init() failed\n"); + return false; + } + + basis_compressor::error_code ec = comp.process_source_images(); + if (ec != basis_compressor::cECSuccess) + { + error_printf("-dds: failed loading/preparing source images (error code %u)\n", (uint32_t)ec); + return false; + } + + // Output filename: -output_file if given, else .dds (honoring -output_path). + std::string output_filename; + if (opts.m_output_filename.size()) + { + output_filename = opts.m_output_filename; + } + else + { + std::string base; + string_get_filename(opts.m_input_filenames[0].c_str(), base); + string_remove_extension(base); + output_filename = base + ".dds"; + + if (opts.m_output_path.size()) + string_combine_path(output_filename, opts.m_output_path.c_str(), output_filename.c_str()); + } + + uint32_t width = 0, height = 0, levels = 0, layers = 0, faces = 0; + uint8_vec dds_data; + std::string err; + if (!build_dds(dds_data, comp, dds_params, err, &width, &height, &levels, &layers, &faces)) + { + error_printf("-dds: %s\n", err.c_str()); + return false; + } + + if (!write_vec_to_file(output_filename.c_str(), dds_data)) + { + error_printf("-dds: failed writing output file \"%s\"\n", output_filename.c_str()); + return false; + } + + // Report the variant actually written: sRGB only when the format has an sRGB DXGI variant AND it was requested. + // Gated on status output so -quiet / -no_status_output silences it. + const bool wrote_srgb = comp.get_params().m_ktx2_and_basis_srgb_transfer_function && dds_output_format_has_srgb_variant(dds_params.m_format); + if (opts.m_comp_params.m_status_output) + printf("Wrote DDS file \"%s\": %u bytes, format %s %s, %ux%u, %u level(s), %u layer(s), %u face(s)\n", + output_filename.c_str(), dds_data.size_u32(), get_dds_output_format_string(dds_params.m_format), + wrote_srgb ? "(sRGB)" : "(UNORM)", + width, height, levels, layers, faces); + + return true; +} + +// -export_dds : transcodes each input .ktx2 file to FORMAT and writes a +// .DDS file (all mip levels/array layers/cubemap faces). FORMAT is one of +// BC1 BC3 BC4 BC5 BC6H BC7 RGBA32. +static bool export_dds_mode(command_line_params& opts) +{ + if (!opts.m_input_filenames.size()) + { + error_printf("-export_dds: no input .ktx2 file(s) specified (use -file)\n"); + return false; + } + + basist::transcoder_texture_format fmt; + if (!parse_export_dds_format(opts.m_export_dds_format, fmt)) + { + error_printf("-export_dds: invalid format \"%s\". Valid formats: BC1 BC3 BC4 BC5 BC6H BC7 RGBA32\n", opts.m_export_dds_format.c_str()); + return false; + } + + if ((opts.m_output_filename.size()) && (opts.m_input_filenames.size() > 1)) + printf("Warning: -output_file is ignored with multiple inputs; using _.dds names\n"); + + uint32_t num_succeeded = 0; + + // Per-file errors skip-and-continue so one bad file doesn't abandon the batch. + for (size_t file_index = 0; file_index < opts.m_input_filenames.size(); file_index++) + { + const std::string& input_filename = opts.m_input_filenames[file_index]; + + std::string ext(string_get_extension(input_filename)); + if (strcasecmp(ext.c_str(), "ktx2") != 0) + { + error_printf("-export_dds: input file \"%s\" is not a .ktx2 file, skipping\n", input_filename.c_str()); + continue; + } + + uint8_vec ktx2_file_data; + if (!read_file_to_vec(input_filename.c_str(), ktx2_file_data)) + { + error_printf("Failed reading input file \"%s\", skipping\n", input_filename.c_str()); + continue; + } + + basist::ktx2_transcoder dec; + if (!dec.init(ktx2_file_data.data(), ktx2_file_data.size_u32())) + { + error_printf("Failed parsing KTX2 file \"%s\", skipping\n", input_filename.c_str()); + continue; + } + + uint8_vec dds_data; + // -1 = auto sRGB; decode flags from the same options -unpack uses (deblocking, HQ, etc.). + if (!transcode_ktx2_to_dds(dec, fmt, dds_data, -1, get_transcode_flags_from_options(opts))) + { + error_printf("Failed exporting \"%s\" to a %s DDS file, skipping\n", input_filename.c_str(), opts.m_export_dds_format.c_str()); + continue; + } + + // Output name: -output_file (single input only), else _.dds. + // -output_path sets the directory. + std::string output_filename; + if ((opts.m_output_filename.size()) && (opts.m_input_filenames.size() == 1)) + { + output_filename = opts.m_output_filename; + } + else + { + std::string base; + string_get_filename(input_filename.c_str(), base); + string_remove_extension(base); + output_filename = string_format("%s_%s.dds", base.c_str(), opts.m_export_dds_format.c_str()); + + if (opts.m_output_path.size()) + string_combine_path(output_filename, opts.m_output_path.c_str(), output_filename.c_str()); + } + + if (!write_vec_to_file(output_filename.c_str(), dds_data)) + { + error_printf("Failed writing output DDS file \"%s\", skipping\n", output_filename.c_str()); + continue; + } + + printf("Wrote DDS file \"%s\": %u bytes, format %s, %ux%u, %u level(s), %u layer(s), %u face(s)\n", + output_filename.c_str(), dds_data.size_u32(), opts.m_export_dds_format.c_str(), + dec.get_width(), dec.get_height(), dec.get_levels(), maximum(1, dec.get_layers()), dec.get_faces()); + + num_succeeded++; + } + + return num_succeeded != 0; +} + +// Maps an -export_ktx format string to a transcoder_texture_format. Compressed +// formats only (the KTX1 writer doesn't do uncompressed yet). Case-insensitive. +static bool parse_export_ktx_format(const std::string& s, basist::transcoder_texture_format& fmt) +{ + typedef basist::transcoder_texture_format tf; + + static const struct { const char* pName; tf fmt; } s_map[] = + { + { "BC1", tf::cTFBC1_RGB }, { "BC3", tf::cTFBC3_RGBA }, { "BC4", tf::cTFBC4_R }, { "BC5", tf::cTFBC5_RG }, + { "BC6H", tf::cTFBC6H }, { "BC7", tf::cTFBC7_RGBA }, + { "ETC1", tf::cTFETC1_RGB }, { "ETC2", tf::cTFETC2_RGBA }, + { "EAC_R11", tf::cTFETC2_EAC_R11 }, { "EAC_RG11", tf::cTFETC2_EAC_RG11 }, + { "PVRTC1", tf::cTFPVRTC1_4_RGBA }, { "PVRTC1_RGBA", tf::cTFPVRTC1_4_RGBA }, { "PVRTC1_RGB", tf::cTFPVRTC1_4_RGB }, + { "PVRTC2", tf::cTFPVRTC2_4_RGBA }, { "PVRTC2_RGBA", tf::cTFPVRTC2_4_RGBA }, { "PVRTC2_RGB", tf::cTFPVRTC2_4_RGB }, + { "ASTC", tf::cTFASTC_LDR_4x4_RGBA }, { "ASTC_4x4", tf::cTFASTC_LDR_4x4_RGBA }, + { "ASTC_5x4", tf::cTFASTC_LDR_5x4_RGBA }, { "ASTC_5x5", tf::cTFASTC_LDR_5x5_RGBA }, + { "ASTC_6x5", tf::cTFASTC_LDR_6x5_RGBA }, { "ASTC_6x6", tf::cTFASTC_LDR_6x6_RGBA }, + { "ASTC_8x5", tf::cTFASTC_LDR_8x5_RGBA }, { "ASTC_8x6", tf::cTFASTC_LDR_8x6_RGBA }, + { "ASTC_10x5", tf::cTFASTC_LDR_10x5_RGBA }, { "ASTC_10x6", tf::cTFASTC_LDR_10x6_RGBA }, + { "ASTC_8x8", tf::cTFASTC_LDR_8x8_RGBA }, { "ASTC_10x8", tf::cTFASTC_LDR_10x8_RGBA }, + { "ASTC_10x10", tf::cTFASTC_LDR_10x10_RGBA }, { "ASTC_12x10", tf::cTFASTC_LDR_12x10_RGBA }, + { "ASTC_12x12", tf::cTFASTC_LDR_12x12_RGBA }, + { "ASTC_HDR_4x4", tf::cTFASTC_HDR_4x4_RGBA }, { "ASTC_HDR_6x6", tf::cTFASTC_HDR_6x6_RGBA }, + }; + + for (uint32_t i = 0; i < (uint32_t)std::size(s_map); i++) + { + if (strcasecmp(s.c_str(), s_map[i].pName) == 0) + { + fmt = s_map[i].fmt; + return true; + } + } + + return false; +} + +// -export_ktx : transcodes each input .ktx2 file to a compressed KTX1 (.ktx) +// file (all mip levels/array layers/cubemap faces). Compressed formats only. +static bool export_ktx_mode(command_line_params& opts) +{ + if (!opts.m_input_filenames.size()) + { + error_printf("-export_ktx: no input .ktx2 file(s) specified (use -file)\n"); + return false; + } + + basist::transcoder_texture_format fmt; + if (!parse_export_ktx_format(opts.m_export_ktx_format, fmt)) + { + error_printf("-export_ktx: invalid/unsupported format \"%s\". Valid: BC1 BC3 BC4 BC5 BC6H BC7 ETC1 ETC2 EAC_R11 EAC_RG11 PVRTC1[_RGB] PVRTC2[_RGB] ASTC ASTC_ ASTC_HDR_4x4 ASTC_HDR_6x6\n", opts.m_export_ktx_format.c_str()); + return false; + } + + if ((opts.m_output_filename.size()) && (opts.m_input_filenames.size() > 1)) + printf("Warning: -output_file is ignored with multiple inputs; using _.ktx names\n"); + + uint32_t num_succeeded = 0; + + // Per-file errors skip-and-continue so one bad file doesn't abandon the batch. + for (size_t file_index = 0; file_index < opts.m_input_filenames.size(); file_index++) + { + const std::string& input_filename = opts.m_input_filenames[file_index]; + + std::string ext(string_get_extension(input_filename)); + if (strcasecmp(ext.c_str(), "ktx2") != 0) + { + error_printf("-export_ktx: input file \"%s\" is not a .ktx2 file, skipping\n", input_filename.c_str()); + continue; + } + + uint8_vec ktx2_file_data; + if (!read_file_to_vec(input_filename.c_str(), ktx2_file_data)) + { + error_printf("Failed reading input file \"%s\", skipping\n", input_filename.c_str()); + continue; + } + + basist::ktx2_transcoder dec; + if (!dec.init(ktx2_file_data.data(), ktx2_file_data.size_u32())) + { + error_printf("Failed parsing KTX2 file \"%s\", skipping\n", input_filename.c_str()); + continue; + } + + uint8_vec ktx_data; + // -1 = auto sRGB; decode flags from the same options -unpack uses (deblocking, HQ, etc.). + if (!transcode_ktx2_to_ktx(dec, fmt, ktx_data, -1, get_transcode_flags_from_options(opts))) + { + error_printf("Failed exporting \"%s\" to a %s KTX file, skipping\n", input_filename.c_str(), opts.m_export_ktx_format.c_str()); + continue; + } + + // Output name: -output_file (single input only), else _.ktx. + // -output_path sets the directory. + std::string output_filename; + if ((opts.m_output_filename.size()) && (opts.m_input_filenames.size() == 1)) + { + output_filename = opts.m_output_filename; + } + else + { + std::string base; + string_get_filename(input_filename.c_str(), base); + string_remove_extension(base); + output_filename = string_format("%s_%s.ktx", base.c_str(), opts.m_export_ktx_format.c_str()); + + if (opts.m_output_path.size()) + string_combine_path(output_filename, opts.m_output_path.c_str(), output_filename.c_str()); + } + + if (!write_vec_to_file(output_filename.c_str(), ktx_data)) + { + error_printf("Failed writing output KTX file \"%s\", skipping\n", output_filename.c_str()); + continue; + } + + printf("Wrote KTX file \"%s\": %u bytes, format %s, %ux%u, %u level(s), %u layer(s), %u face(s)\n", + output_filename.c_str(), ktx_data.size_u32(), opts.m_export_ktx_format.c_str(), + dec.get_width(), dec.get_height(), dec.get_levels(), maximum(1, dec.get_layers()), dec.get_faces()); + + num_succeeded++; + } + + return num_succeeded != 0; +} + +// --tinydds_info : prints high-level header info for each input .DDS file (via tinydds) +// (header only, no pixel decode) -- a quick sanity check for exported DDS files. +static bool tinydds_info_mode(command_line_params& opts) +{ + if (!opts.m_input_filenames.size()) + { + error_printf("--tinydds_info: no input .DDS file(s) specified (use -file)\n"); + return false; + } + + uint32_t num_ok = 0; + for (size_t i = 0; i < opts.m_input_filenames.size(); i++) + { + if (i) + printf("\n"); + if (print_dds_info(opts.m_input_filenames[i].c_str())) + num_ok++; + } + + return num_ok != 0; +} + +// --ktx_info : prints the KTX1 (.ktx) header fields for each input file +// (header only, no key/value or image data) -- a quick sanity check for exports. +static bool ktx_info_mode(command_line_params& opts) +{ + if (!opts.m_input_filenames.size()) + { + error_printf("--ktx_info: no input .ktx file(s) specified (use -file)\n"); + return false; + } + + uint32_t num_ok = 0; + for (size_t i = 0; i < opts.m_input_filenames.size(); i++) + { + if (i) + printf("\n"); + if (print_ktx_info(opts.m_input_filenames[i].c_str())) + num_ok++; + } + + return num_ok != 0; +} + static int main_internal(int argc, const char** argv) { printf("Basis Universal LDR/HDR GPU Texture Supercompression System v" BASISU_TOOL_VERSION @@ -6068,13 +9643,13 @@ static int main_internal(int argc, const char** argv) " (x64)" #elif defined(__wasi__) " (WASI" - #if BASISU_WASI_THREADS +#if BASISU_WASI_THREADS " Threaded" - #endif +#endif ")" #endif - "\nCopyright (C) 2019-2026 Binomial LLC, All rights reserved\n"); + "\nCopyright (C) 2019-2026 Binomial LLC, All rights reserved\n"); #ifdef FORCE_SAN_FAILURE force_san_failure(); @@ -6082,7 +9657,7 @@ static int main_internal(int argc, const char** argv) //interval_timer tm; //tm.start(); - + // See if OpenCL support has been disabled. We don't want to parse the command line until the lib is initialized bool use_opencl = false; bool opencl_force_serialization = false; @@ -6091,16 +9666,16 @@ static int main_internal(int argc, const char** argv) for (int i = 1; i < argc; i++) { - if ((strcmp(argv[i], "-opencl") == 0) || (strcmp(argv[i], "-clbench") == 0)) + if (opt_match(argv[i], {"-opencl", "-clbench"})) use_opencl = true; - if (strcmp(argv[i], "-opencl_serialize") == 0) + if (opt_match(argv[i], "-opencl_serialize")) opencl_force_serialization = true; - - if ((strcmp(argv[i], "-peek_astc") == 0) || (strcmp(argv[i], "-peek") == 0)) + + if (opt_match(argv[i], {"-peek_astc", "-peek"})) astc_peek_flag = true; - if (strcmp(argv[i], "-dev_astc_fuzz") == 0) + if (opt_match(argv[i], "-dev_astc_fuzz")) astc_fuzz_flag = true; } @@ -6110,9 +9685,74 @@ static int main_internal(int argc, const char** argv) fprintf(stderr, "WARNING: -opencl specified, but OpenCL support was not defined or enabled at compile time! With cmake, use -D BASISU_OPENCL=1. Falling back to CPU compression.\n"); } #endif - + basisu_encoder_init(use_opencl, opencl_force_serialization); + +#if 0 + image a, b; + if (!load_png("d:/dev/test_images/bik/bik23.png", a)) + return EXIT_FAILURE; + if (!load_png("c:/dev/e23_4.png", b)) + return EXIT_FAILURE; + + fmt_printf("Resolution: {}x{}\n", a.get_width(), a.get_height()); + + print_image_metrics(a, b); + + psnr_hvs_metrics hvs_metrics; + if (!psnr_hvs_compute_metrics(a, b, hvs_metrics)) + return EXIT_FAILURE; + psnr_hvs_print_metrics(hvs_metrics); +#if 0 + for (uint32_t y = 0; y < a.get_height(); y++) + { + for (uint32_t x = 0; x < a.get_width(); x++) + { + a(x, y).set(get_601_y(a(x, y).r, a(x, y).g, a(x, y).b)); + b(x, y).set(get_601_y(b(x, y).r, b(x, y).g, b(x, y).b)); + } + } + + image_metrics im; + image_metrics::hvs_metrics hvs[3]; + + double sum_hvs_mseh = 0.0f; + double sum_hvsm_mseh = 0.0f; + + for (uint32_t c = 0; c < 3; c++) + { + if (!im.compute_hvs_chan(a, b, c, hvs[c])) + return EXIT_FAILURE; + + fmt_printf("{c}: PSNR-HVS: {}, PSNR-HVS-M: {}\n", (int)("RGB"[c]), hvs[c].psnr_hvs, hvs[c].psnr_hvsm); + + sum_hvs_mseh += hvs[c].mseh_hvs; + sum_hvsm_mseh += hvs[c].mseh_hvsm; + } + + sum_hvs_mseh /= 3.0f; + sum_hvsm_mseh /= 3.0f; + + fmt_printf("RGB PSNR-HVS: {}\n", psnr_hvs_calc_psnr(sum_hvs_mseh, 1.0f)); + fmt_printf("RGB PSNR-HVS-M: {}\n", psnr_hvs_calc_psnr(sum_hvsm_mseh, 1.0f)); +#endif + + exit(0); +#endif + + // Standalone DDS reader/transcoder test harness (see test_dds()). + if ((argc >= 2) && (strcmp(argv[1], "-test_dds") == 0)) + { + return test_dds(argc, argv) ? EXIT_SUCCESS : EXIT_FAILURE; + } + + // DDS transcoder output-buffer overrun / argument-rejection sanity test (see test_dds_overwrite()). + if ((argc >= 2) && (strcmp(argv[1], "-test_dds_overwrite") == 0)) + { + return test_dds_overwrite(argc, argv) ? EXIT_SUCCESS : EXIT_FAILURE; + } + if (astc_fuzz_flag) { bool status = xuastc_ldr_decoder_fuzz_test(); @@ -6167,13 +9807,19 @@ static int main_internal(int argc, const char** argv) std::string ext(string_get_extension(opts.m_input_filenames[i])); if ((strcasecmp(ext.c_str(), "basis") == 0) || (strcasecmp(ext.c_str(), "ktx") == 0) || (strcasecmp(ext.c_str(), "ktx2") == 0)) { - // If they haven't specified any modes, and they give us a .basis file, then assume they want to unpack it. + // If they haven't specified any modes, and they give us a .basis/.ktx2 file, then assume they want to unpack it. + // NOTE: a bare .dds is intentionally NOT auto-unpacked -- it is a valid compressor SOURCE image + // (read_uncompressed_dds_file). To unpack a .dds use -unpack, or -info for header info. opts.m_mode = cUnpack; break; } } } + // Note: -info/-validate/-unpack all flow through unpack_and_validate_mode(), which sniffs each input's + // magic per-file (KTX2 / DDS / KTX1 / .basis) and routes it to the right handler -- so a mixed-type input + // list (e.g. a .ktx2 and a .dds on one command line) works without any extension-based mode rerouting. + bool status = false; switch (opts.m_mode) @@ -6190,9 +9836,15 @@ static int main_internal(int argc, const char** argv) case cCompare: status = compare_mode(opts); break; + case cCompareHVS: + status = compare_hvs_mode(opts); + break; case cHDRCompare: status = hdr_compare_mode(opts); break; + case cImageDumpStats: + status = image_stats_mode(opts); + break; case cVersion: status = true; // We printed the version at the beginning of main_internal break; @@ -6208,6 +9860,12 @@ static int main_internal(int argc, const char** argv) case cTestXUASTCLDR: status = test_mode_xuastc_ldr(opts); break; + case cTestCodecs: + status = test_codecs_run(opts); + break; + case cTestCodecsGen: + status = test_codecs_generate(opts); + break; case cTestHDR_4x4: status = test_mode_hdr(opts, basist::basis_tex_format::cUASTC_HDR_4x4, std::size(g_hdr_4x4_test_files), g_hdr_4x4_test_files, 0.0f); break; @@ -6232,9 +9890,45 @@ static int main_internal(int argc, const char** argv) case cCombineImages: status = combine_images_mode(opts); break; + case cExtractChannel: + status = extract_channel_mode(opts); + break; + case cExtractSwizzle: + status = extract_swizzle_mode(opts); + break; + case cExtractRegion: + status = extract_region_mode(opts); + break; + case cTextToPng: + status = text_to_png(opts.m_text_image_in.c_str(), opts.m_text_image_out.c_str()); + break; + case cPngToText: + status = png_to_text(opts.m_text_image_in.c_str(), opts.m_text_image_out.c_str()); + break; case cTonemapImage: status = tonemap_image_mode(opts); break; + case cDDS: + status = dds_mode(opts); + break; + case cExportDDS: + status = export_dds_mode(opts); + break; + case cExportKTX: + status = export_ktx_mode(opts); + break; + case cTinyDDSInfo: + status = tinydds_info_mode(opts); + break; + case cKTXInfo: + status = ktx_info_mode(opts); + break; + case cBenchmarkSingle: + status = benchmark_single_mode(opts); + break; + case cBenchmarkSweep: + status = benchmark_sweep_mode(opts); + break; default: assert(0); break; @@ -6345,4 +10039,3 @@ int main(int argc, const char** argv) return status; } - diff --git a/basisu_tool_help.h b/basisu_tool_help.h index 486e166..ee530ed 100644 --- a/basisu_tool_help.h +++ b/basisu_tool_help.h @@ -4,12 +4,59 @@ "The default processing mode is compression of one or more .PNG/.TGA/.JPG/.QOI/\n" ".DDS/.EXR/.HDR files to a LDR or HDR .KTX2 file. Alternate modes:\n" "\n" -" -unpack: Use transcoder to unpack a .basis/.KTX2 file to one or more .KTX, \n" -" .DDS, .PNG, .ASTC, etc. files.\n" +" -unpack: Use transcoder to unpack a .basis/.KTX2/.DDS file to one or more .KTX,\n" +" .DDS, .PNG, .ASTC, etc. files. For a .DDS input, each layer/face/mip image is\n" +" decoded to _unpacked_rgb_/_a_/_rgba_ PNGs and transcoded to every GPU format\n" +" as single-image _transcoded_ files (BC1-7 to .DDS+.KTX, others .KTX only),\n" +" similar to unpacking a .KTX2/.basis (flat per-image output; the exact format\n" +" set differs). (-no_ktx skips the GPU files, -ktx_only skips PNGs.) DDS unpack\n" +" uses a fixed transcode-target set -- -format_only/-etc1_only are ignored for\n" +" .dds. A bare .dds with no mode is a compress SOURCE, not unpacked -- pass\n" +" -unpack to unpack it.\n" "\n" -" -info: Display high-level information about a .basis/.KTX2 file\n" +" -export_dds : Transcode each input .ktx2 to a .DDS file (all mips,\n" +" array layers, cubemap faces). FORMAT: BC1 BC3 BC4 BC5 BC6H BC7 RGBA32. Output\n" +" is _.dds; -output_file/-output_path override the name/dir.\n" +"\n" +" -dds: Write a basic DX10 .DDS file directly from source image(s). Use with\n" +" -dds_format . Honors -mipmap, -cubemap, -tex_type 2darray, -srgb/-linear,\n" +" -swizzle, -resample, etc. (the same source-prep options as compression). All\n" +" inputs form a single .dds (one per layer/face). Output is .dds;\n" +" -output_file/-output_path override the name/dir. LDR only.\n" +" -dds_format : Output format for -dds (defaults to BC7 if omitted). Block\n" +" compressed: BC1 BC2 BC3 BC4 BC5 BC7. Uncompressed: A8R8G8B8 A8B8G8R8 R8 R8G8\n" +" R5G6B5 A1R5G5B5 A4R4G4B4. The sRGB DXGI variant is used for BC1/BC2/BC3/BC7/\n" +" A8R8G8B8/A8B8G8R8 unless -linear is given (other formats are always UNORM). No\n" +" transfer-function conversion is applied to texel data.\n" +" -dds_bc7f / -dds_bc7e_scalar (alias -dds_bc7e): select the BC7 packer for -dds\n" +" -dds_format BC7. bc7f = built-in fast real-time packer (default); bc7e_scalar =\n" +" higher-quality, slower scalar encoder.\n" +" -dds_bc7f_level <0-2>: bc7f quality: 0=analytical, 1=partially analytical\n" +" (default), 2=non analytical (slowest/best).\n" +" -dds_bc7e_scalar_level <0-6>: bc7e_scalar quality: 0=ultrafast..6=slowest\n" +" (default 2); implies -dds_bc7e_scalar.\n" +"\n" +" -export_ktx : Transcode each input .ktx2 to a compressed KTX1 (.ktx)\n" +" file (all mips, array layers, cubemap faces). FORMAT (compressed only): BC1\n" +" BC3 BC4 BC5 BC6H BC7 ETC1 ETC2 EAC_R11 EAC_RG11 PVRTC1 PVRTC2 ASTC ASTC_\n" +" ASTC_HDR_4x4 ASTC_HDR_6x6. Output is _.ktx.\n" +"\n" +" -tinydds_info: Print header info (dimensions, type, mips, array slices, format)\n" +" for each input .DDS file, via the tinydds parser. Header only -- no decode.\n" +" (-info also prints DDS header info, via the dds_transcoder.)\n" +"\n" +" -ktx_info: Print KTX1 (.ktx) header fields (GL type/format/internalFormat,\n" +" dimensions, array elements, faces, mip levels) for each input file. Header\n" +" only -- no key/value or image data parsing.\n" +"\n" +" -info: Display high-level information about a .basis/.KTX2/.DDS/.KTX1 file.\n" +" Each input is detected by content, so mixed file types on one command line\n" +" are fine (a .DDS prints its header info; a .KTX1 prints its KTX header). For\n" +" a .DDS this prints the transcoder's view (closest-matching format); use\n" +" -tinydds_info for the raw DDS header (physical pixel layout).\n" " \n" -" -validate: Validate and display information about a .basis/.KTX2 file\n" +" -validate: Validate and display information about a .basis/.KTX2/.DDS file\n" +" (decodes/transcodes every image to exercise the codecs; writes nothing).\n" " \n" " -compare: Compare two LDR PNG/BMP/TGA/JPG/QOI images specified with -file,\n" " output PSNR and SSIM statistics and RGB/A delta images\n" @@ -17,9 +64,57 @@ " -compare_hdr: Compare two HDR .EXR/.HDR images specified with -file, output\n" " PSNR statistics and RGB delta images\n" "\n" +" -dump_image_stats: Print reference-free per-channel RGBA statistics (min/max/\n" +" mean/variance/std dev/unique count) for each LDR image given with -file, plus\n" +" flags for common issues (constant/dropped channel, all-opaque or all-zero\n" +" alpha, possibly-premultiplied alpha, R==G==B grayscale). LDR/8-bit only for now.\n" +" -dump_image_stats : As above, but restrict the stats to the\n" +" given rectangle (must lie fully inside the image).\n" +"\n" +" -dump_pixel : Print the exact RGBA value of texel (x,y) of each -file\n" +" image (LDR only). Can be combined with -dump_image_stats / -dump_block.\n" +"\n" +" -dump_block : Print the 4x4 texel block at block coords (bx,by) (i.e.\n" +" source texels [bx*4..bx*4+3] x [by*4..by*4+3]) of each -file image (LDR only).\n" +" Can be combined with -dump_image_stats / -dump_pixel.\n" +"\n" " -tonemap: Tonemap an HDR or EXR image to PNG at multiple exposures, use -file\n" " to specify filename\n" "\n" +" -split: Split the -file input image into split_rgb.png plus per-channel\n" +" grayscale split_R/G/B/A.png files\n" +"\n" +" -combine: Combine two -file images: RGB from the first plus the second's\n" +" green channel as alpha, written to combined.png (or -output_file)\n" +"\n" +" -extract_channel [x y w h]: Write one channel (c = r/g/b/a or\n" +" 0..3) of the -file image as a grayscale PNG. Optionally restrict to the given\n" +" rectangle (must lie fully inside the image). LDR only.\n" +"\n" +" -extract_swizzle [x y w h]: Write an RGBA PNG whose R,G,B,A\n" +" come from the source channels named by , a list of 1..4 comma-\n" +" separated channels (each r/g/b/a or 0..3), e.g. \"0,1,2,3\" (identity),\n" +" \"2,1,0\" (swap R/B), \"a,a,a,a\" (broadcast alpha). Output channels not listed\n" +" pass through unchanged. Optional rectangle as with -extract_channel. LDR only.\n" +"\n" +" -extract_region : Write the given rectangle of the\n" +" -file image (full RGBA) to a PNG. The rectangle must lie fully inside the\n" +" image. LDR only.\n" +"\n" +" -extract_to_linear / -extract_to_srgb: Optional transforms applied by the\n" +" -extract_* commands above: convert the output from sRGB to linear, or from\n" +" linear to sRGB. Applied to RGB only by default (alpha passes through).\n" +" -extract_xform_alpha: Also apply the -extract_to_linear/-extract_to_srgb\n" +" transform to the alpha channel (off by default).\n" +"\n" +" -text_to_png : Compose a small PNG from a comma-separated\n" +" pixel text file. Line 1 is the header \"W,H,C\" (W,H in [1,1024], C channels in\n" +" [1,4]); then one pixel per line with C decimal values 0..255 in row-major\n" +" order (1=L, 2=LA, 3=RGB, 4=RGBA). Blank lines and '#' comments are ignored.\n" +" Strict: malformed values / wrong counts are a hard error with the line number.\n" +" -png_to_text : Inverse of -text_to_png. Auto-detects the\n" +" minimal channel count (L/LA/RGB/RGBA) and writes the text format above.\n" +"\n" " -peek_astc: Read an .astc file and calculate statistics (for testing/dev)\n" "\n" " -version or --version: Print version and exit\n" @@ -34,6 +129,8 @@ "unpack .KTX2/.basis files to .PNG, .KTX (v1), .ASTC, or .DDS files using its\n" "single source file transcoder library.\n" "\n" +"Options may be given in either -opt or Unix-style --opt form.\n" +"\n" "The list of supported texture/supercompressed texture codecs:\n" " -LDR: ETC1S (SDR default), RDO UASTC LDR 4x4, ASTC or XUASTC LDR 4x4-12x12\n" " -HDR: UASTC HDR 4x4 (HDR default), RDO ASTC HDR 6x6, UASTC HDR 6x6\n" @@ -66,8 +163,10 @@ "brittle compression (higher distortion on tough image/texture features).\n" "Effort=0 fastest compression, effort=10=extremely slow.\n" "\n" -"Unless an explicit mode is specified, if one or more files have the .basis\n" -"or .KTX2 extension this tool defaults to unpack mode.\n" +"Unless an explicit mode is specified, if one or more files have the .basis,\n" +".KTX2, or .ktx (KTX1) extension this tool defaults to unpack mode. (A .ktx is\n" +"KTX1, which can't be unpacked -- it errors with a pointer to -info/-ktx_info.\n" +"A bare .dds is treated as a compress SOURCE, not unpacked; use -unpack for it.)\n" "\n" "By default, the compressor assumes the input is in the sRGB colorspace (like\n" "typical photos/albedo textures). If the input is NOT sRGB (like a normal map),\n" @@ -78,10 +177,12 @@ "does not support all .EXR compression methods. For unsupported images, you can\n" "use a tool like ImageMagick to convert them to uncompressed .EXR.\n" "\n" -"For .DDS source files: Mipmapped or plain 2D textures (but not cubemaps) are\n" -"supported. Only uncompressed 32-bit RGBA/BGRA, half float RGBA, or float\n" -"RGBA .DDS files are supported. In -tex_array mode, if a .DDS file is specified,\n" -"all source files must be in .DDS format.\n" +"For .DDS COMPRESSION SOURCE files (i.e. compressing a .dds as an input image --\n" +"this does NOT limit -unpack): Mipmapped or plain 2D textures (but not cubemaps)\n" +"are supported, and only uncompressed 32-bit RGBA/BGRA, half float RGBA, or\n" +"float RGBA .DDS files. In -tex_array mode, if a .DDS file is specified, all\n" +"source files must be in .DDS format. (To UNPACK a compressed .dds -- BC1-7,\n" +"cubemaps, arrays, etc. -- use -unpack, which has no such restrictions.)\n" "\n" "Filenames prefixed with a @ symbol are read as filename listing files. Listing\n" "text files specify which actual filenames to process (one filename per line).\n" @@ -130,6 +231,15 @@ " options (-effort, -quality, -xy, -ts, -tl, etc.) below. .89-8 bpp before\n" " ZStd compression.\n" "\n" +" 8. XUBC7: -xubc7. Lossless or lossy supercompressed BC7. Options:\n" +" Use -quality [1,100], -effort [0,10], and -xubc7_rdo_level [0,100]\n" +" (0=no RDO - default, 100=aggressive RDO)\n" +" -xubc7_num_stripes [1,16]: # of encode stripes (default 8; more=faster\n" +" decode/encode parallelism, slightly larger file)\n" +" -xubc7_bc7f / -xubc7_bc7e_scalar: select the BC7 base encoder (default\n" +" bc7f - fast; bc7e_scalar - bc7e.ispc-based: slower, higher quality)\n" +" -xubc7_bc7e_scalar_level [0,6]: use bc7e_scalar+set quality level (def=2)\n" +"\n" "--- Tool Options:\n" "\n" " -ktx2: Write .KTX2 files (the default). By default, UASTC LDR/HDR 4x4 and ASTC\n" @@ -152,12 +262,21 @@ "\n" " -output_path: Output .basis/.KTX2 files to specified directory.\n" "\n" -" -debug or -verbose: Enable codec debug print to stdout (slightly slower).\n" +" -debug: Enable codec debug print to stdout (slightly slower).\n" +"\n" +" -verbose: Shorthand for -debug -stats (debug output plus quality metrics).\n" "\n" " -debug_images: Enable codec debug images (much slower).\n" "\n" " -stats: Compute and display image quality metrics (slightly to much slower).\n" "\n" +" -no_stats: Disable image quality metrics (overrides -verbose/-csv_file).\n" +"\n" +" -validate_output: Validate output texture data after compression.\n" +"\n" +" -csv_file filename.csv: Log image quality metrics to a CSV file (also enables\n" +" -stats computation).\n" +"\n" " -individual: Process input images individually and output\n" " multiple .basis/.KTX2 files (not as a texture array - this is now the default\n" " as of v1.16)\n" @@ -338,6 +457,9 @@ " light. Default is 100 nits. Note: Previous builds used 1 nit. Common\n" " values are 80-100 nits.\n" "\n" +" -hdr_ldr_upconversion_black_bias X: Black bias [0,1] added during SDR->HDR\n" +" upconversion (default 0).\n" +"\n" "--- ASTC LDR/XUASTC LDR specific options (-ldr_4x4 or -ldr_4x4i, up to 12x12):\n" "\n" "Internally both modes (ASTC 4x4-12x12 and XUASTC 4x4-12x12) use the same\n" @@ -373,14 +495,18 @@ " -xp: Force disable RGB dual plane usage in all effort levels (lower quality\n" " but faster compression and faster transcoding to BC7 at certain block sizes)\n" "\n" +" -xsu, -xpu: Inverses of -xs/-xp - force-enable 2-3 subset usage / RGB dual\n" +" plane, respectively.\n" +"\n" " -ts: Use LDR sRGB ASTC decoding profile - the default. This parameter should\n" " match how the developer will decode or sample the ASTC texture data. Inverse\n" " of -tl. Same as -srgb.\n" "\n" " -tl: Use LDR Linear ASTC decoding profile. Inverse of -ts. Same as -linear.\n" "\n" -" -weights X Y Z W: Set unsigned integer channel error weights. Defaults are\n" -" 1,1,1,1. Useful to favor certain channels during compression.\n" +" -weights X Y Z W: Set unsigned integer channel error weights. Default is\n" +" 9,11,1,11 (sRGB); -linear or -normal_map use 1,1,1,1. Useful to favor\n" +" certain channels during compression.\n" "\n" " -ls_min_psnr X, -ls_min_alpha_psnr X, -ls_thresh_psnr X, -ls_thresh_alpha_psnr\n" " X, -ls_thresh_edge_psnr X, -ls_thresh_edge_alpha_psnr X: Windowed/bounded RDO\n" @@ -389,6 +515,52 @@ " -xuastc_blurring: Experimental - enable blurred block candidates (MUCH slower\n" " compression, higher quality).\n" "\n" +" -xuastc_sharpen X: Enable sharpening, set amount [0,10] (0 disables; higher\n" +" = sharper, but more likely to add artifacts). Off by default; default amount\n" +" is 2.0. -xuastc_no_sharpen disables.\n" +"\n" +" -xuastc_heavy_subset_usage, -xuastc_no_heavy_subset_usage: Enable/disable\n" +" heavier 2-3 subset usage (higher quality, slower; disabled by default).\n" +"\n" +"Encode-time deblocking (XUASTC LDR only): control deblocking awareness (SCD)\n" +"and/or the deblocking filter applied during compression. Default is\n" +"-xuastc_deblocking_largest. Each flag also has a -xuastc_ldr_* alias, e.g.\n" +"-xuastc_ldr_deblocking_all.\n" +"\n" +" -xuastc_no_deblocking: Disable both SCD and filtering.\n" +"\n" +" -xuastc_deblocking_largest: Use SCD and filtering only on the largest block\n" +" sizes (the codec default).\n" +"\n" +" -xuastc_deblocking_all: Use SCD and filtering on all block sizes.\n" +"\n" +" -xuastc_deblocking_scd_no_filtering: Use SCD only, no filtering.\n" +"\n" +" -xuastc_deblocking_no_scd_filtering_largest: No SCD, filter largest blocks.\n" +"\n" +" -xuastc_deblocking_no_scd_filtering_all: No SCD, filter all block sizes.\n" +"\n" +" -xuastc_deblocking_num_passes X: Set number of deblocking filter passes\n" +" [2,256].\n" +"\n" +"Internal ASTC encoder selection (advanced/experimental). The astcenc-based\n" +"options (-xuastc_use_astcenc, -xuastc_merge_astcenc, -xuastc_merge_all) require\n" +"building with BASISU_SUPPORT_ASTCENC=1:\n" +"\n" +" -xuastc_use_auto_comp: Auto-pick the ASTC encoder by effort level (default).\n" +"\n" +" -xuastc_use_basisu: Use the built-in basisu ASTC encoder.\n" +"\n" +" -xuastc_use_astcf: Use the astcf ASTC encoder.\n" +"\n" +" -xuastc_use_astcenc: Use the astcenc ASTC encoder.\n" +"\n" +" -xuastc_merge_astcf: Try basisu + astcf, keep the best result per block.\n" +"\n" +" -xuastc_merge_astcenc: Try basisu + astcenc, keep the best result per block.\n" +"\n" +" -xuastc_merge_all: Try all available encoders, keep the best result per block.\n" +"\n" "These modes support all the standard ASTC block sizes. The larger the block\n" "size, the lower the bitrate on disk and in memory, but the more noticeable the\n" "artifacts. Some block sizes (4x4, 6x6, and 8x6) have specially optimized direct\n" @@ -422,6 +594,16 @@ " -test_xuastc: Run an automated XUASTC LDR encoding and transcoding test.\n" " Returns EXIT_FAILURE on any failures.\n" "\n" +" -test_codecs [codec]: Sweep all LDR+HDR codecs across quality 10/25/50/75/100\n" +" and effort 0/3/6, comparing size and RGB/RGBA (LDR) or RGB (HDR) PSNRs vs the\n" +" golden table in basisu_tool_test_codecs.inl. Optional codec name filter, e.g.\n" +" -test_codecs ETC1S | XUASTC | ASTC | XUBC7 | HDR. Defaults test dir to\n" +" \"test_files\". Returns EXIT_FAILURE on any failures.\n" +"\n" +" -test_codecs_gen [outfile.inl] [codec]: (Re)generate the golden table for\n" +" -test_codecs by running the same sweep and writing the measured results.\n" +" Defaults to basisu_tool_test_codecs.inl. Use after a codec changes.\n" +"\n" " -test_dir: Optional directory of test files. Defaults to \"../test_files\".\n" "\n" " -y_flip: Flip input images vertically before compression\n" @@ -429,6 +611,10 @@ " -normal_map: Tunes codec parameters for better quality on normal maps (linear\n" " colorspace metrics, linear mipmap filtering, no selector RDO, no sRGB)\n" "\n" +" -photo: Tune for photographic content (the codec defaults): sRGB metrics,\n" +" endpoint+selector RDO on, sRGB channel weights, sharpening off, deblock\n" +" largest blocks. Opposite of -normal_map/-texture.\n" +"\n" " -no_alpha: Always output non-alpha basis files, even if one or more inputs has\n" " alpha\n" "\n" @@ -451,9 +637,13 @@ " -wasi_threads: Set number of threads to use in WASI threading builds\n" " (default=8, only used in WASI threading builds)\n" "\n" -" -no_ktx: Disable KTX writing when unpacking (faster, less output files)\n" +" -no_ktx (alias -no_dds): When unpacking, skip the transcoded GPU files (the\n" +" .ktx, and .dds for BC1-7) and write only the decoded PNGs (faster, fewer\n" +" files). The two names are identical -- both gate the same .ktx+.dds output.\n" "\n" -" -ktx_only: Only write KTX files when unpacking (faster, less output files)\n" +" -ktx_only (alias -dds_only): When unpacking, write only the transcoded GPU\n" +" files (.ktx, plus .dds for BC1-7) and skip the decoded PNGs. Note not every\n" +" format yields a .dds -- only BC1-7 do; the rest are .ktx-only.\n" "\n" " -write_out: Write 3dfx OUT files when unpacking FXT1 textures\n" "\n" @@ -543,16 +733,14 @@ "\n" " -higher_quality_transcoding: Enable higher quality, but slower, transcoding\n" "\n" -" -no_deblocking: Always disable adaptive deblocking filter on all block sizes\n" -" (XUASTC/ASTC LDR 4x4-12x12 only). By default only block sizes >8x6 are\n" -" deblocked while transcoding. (No deblocking ever occurs when transcoding to\n" -" ASTC: only when re-encoding ASTC to another format, to lower artifacts.)\n" +" -transcode_no_deblocking: Always disable deblocking filter on all block\n" +" sizes (XUASTC/ASTC LDR 4x4-12x12 only). Alias:\n" +" -transcode_disable_deblocking. By default only sizes >= 10x8 are deblocked\n" +" while transcoding. (No CPU deblocking ever occurs when transcoding to ASTC:\n" +" only when transcoding ASTC to another formats.)\n" "\n" -" -force_deblocking: Always use adaptive deblocking filter, even for block sizes\n" -" <= 8x6 (XUASTC/ASTC LDR 4x4-12x12 only)\n" -"\n" -" -stronger_deblocking: Use stronger adaptive deblocking filtering (XUASTC/ASTC\n" -" LDR 4x4-12x12 only)\n" +" -transcode_force_deblocking: Always use deblocking filter, even for block\n" +" sizes smaller than 10x8 (XUASTC/ASTC LDR 4x4-12x12 only).\n" "\n" " -no_etc1s_chroma_filtering: Disable adaptive ETC1S transcode chroma filter,\n" " for faster transcoding to BC7.\n" @@ -594,7 +782,7 @@ " - basisu -basis x.qoi : Compress sRGB image x.qoi to x.basis (supports 24-bit\n" " or 32-bit .QOI files)\n" "\n" -" - basisu x.ktx2 : Unpack x.basis to PNG/KTX files (multiple filenames OK)\n" +" - basisu x.ktx2 : Unpack x.ktx2 to PNG/KTX files (multiple filenames OK)\n" "\n" " - basisu x.basis : Unpack x.basis to PNG/KTX files (multiple filenames OK)\n" "\n" @@ -671,9 +859,9 @@ "\n" "--- Example ASTC/XUASTC LDR 4x4-12x12 command lines:\n" "\n" -" - basisu -ldr_6x6i -quality 75 -xuastc_arith test.png : Compress test.png to XUASTC\n" -" LDR 6x6 using weight grid DCT with setting 75 and the arith profile for\n" -" higher compression.\n" +" - basisu -ldr_6x6i -quality 75 -xuastc_arith test.png : Compress test.png\n" +" to XUASTC LDR 6x6 using weight grid DCT with setting 75 and the arith\n" +" profile for higher compression.\n" "\n" " - basisu -ldr_4x4 -effort 5 test.png : Compress test.png to ASTC LDR 4x4\n" "\n" diff --git a/encoder/3rdparty/android_astc_decomp.cpp b/encoder/3rdparty/android_astc_decomp.cpp index 40f3104..cfd7167 100644 --- a/encoder/3rdparty/android_astc_decomp.cpp +++ b/encoder/3rdparty/android_astc_decomp.cpp @@ -726,7 +726,7 @@ inline void setASTCErrorColorBlock (void* dst, int blockWidth, int blockHeight, } } -DecompressResult decodeVoidExtentBlock (void* dst, const Block128& blockData, int blockWidth, int blockHeight, bool isSRGB, bool isLDRMode) +DecompressResult decodeVoidExtentBlock(void* dst, const Block128& blockData, int blockWidth, int blockHeight, bool isSRGB, bool isLDRMode, bool rejectVoidExtentNaNInf) { // rg: Adding check here to match ARM's decoder and basisu's. // 18.23.Void - Extent Blocks @@ -773,13 +773,16 @@ DecompressResult decodeVoidExtentBlock (void* dst, const Block128& blockData, in if (isHDRBlock) { - for (int c = 0; c < 4; c++) + if (rejectVoidExtentNaNInf) { - if (isFloat16InfOrNan((deFloat16)rgba[c])) + for (int c = 0; c < 4; c++) { - //throw InternalError("Infinity or NaN color component in HDR void extent block in ASTC texture (behavior undefined by ASTC specification)"); - setASTCErrorColorBlock(dst, blockWidth, blockHeight, isSRGB); - return DECOMPRESS_RESULT_ERROR; + if (isFloat16InfOrNan((deFloat16)rgba[c])) + { + //throw InternalError("Infinity or NaN color component in HDR void extent block in ASTC texture (behavior undefined by ASTC specification)"); + setASTCErrorColorBlock(dst, blockWidth, blockHeight, isSRGB); + return DECOMPRESS_RESULT_ERROR; + } } } @@ -1754,7 +1757,7 @@ DecompressResult setTexelColors (void* dst, ColorEndpointPair* colorEndpoints, T return result; } -DecompressResult decompressBlock (void* dst, const Block128& blockData, int blockWidth, int blockHeight, bool isSRGB, bool isLDR) +DecompressResult decompressBlock(void* dst, const Block128& blockData, int blockWidth, int blockHeight, bool isSRGB, bool isLDR, bool rejectVoidExtentNaNInf) { DE_ASSERT(isLDR || !isSRGB); @@ -1770,7 +1773,7 @@ DecompressResult decompressBlock (void* dst, const Block128& blockData, int bloc // Separate path for void-extent. if (blockMode.isVoidExtent) - return decodeVoidExtentBlock(dst, blockData, blockWidth, blockHeight, isSRGB, isLDR); + return decodeVoidExtentBlock(dst, blockData, blockWidth, blockHeight, isSRGB, isLDR, rejectVoidExtentNaNInf); // Compute weight grid values. const int numWeights = computeNumWeights(blockMode); @@ -2022,14 +2025,14 @@ static void convert_from_half_to_float_prec(uint32_t n, float* pVals) } // Assumes the decode_unorm8 extension is active (only upper 8 bits used). -bool decompress_ldr(uint8_t *pDst, const uint8_t * data, bool isSRGB, int blockWidth, int blockHeight) +bool decompress_ldr(uint8_t *pDst, const uint8_t * data, bool isSRGB, int blockWidth, int blockHeight, bool rejectVoidExtentNaNInf) { float linear[MAX_BLOCK_WIDTH * MAX_BLOCK_HEIGHT * 4]; const Block128 blockData(data); // isSRGB is true, this writes uint8_t's. Otherwise it writes floats. - if (decompressBlock(isSRGB ? (void*)pDst : (void*)&linear[0], blockData, blockWidth, blockHeight, isSRGB, true) != DECOMPRESS_RESULT_VALID_BLOCK) + if (decompressBlock(isSRGB ? (void*)pDst : (void*)&linear[0], blockData, blockWidth, blockHeight, isSRGB, true, rejectVoidExtentNaNInf) != DECOMPRESS_RESULT_VALID_BLOCK) { return false; } @@ -2053,11 +2056,11 @@ bool decompress_ldr(uint8_t *pDst, const uint8_t * data, bool isSRGB, int blockW return true; } -bool decompress_hdr(float* pDstRGBA, const uint8_t* data, int blockWidth, int blockHeight) +bool decompress_hdr(float* pDstRGBA, const uint8_t* data, int blockWidth, int blockHeight, bool rejectVoidExtentNaNInf) { const Block128 blockData(data); - if (decompressBlock(pDstRGBA, blockData, blockWidth, blockHeight, false, false) != DECOMPRESS_RESULT_VALID_BLOCK) + if (decompressBlock(pDstRGBA, blockData, blockWidth, blockHeight, false, false, rejectVoidExtentNaNInf) != DECOMPRESS_RESULT_VALID_BLOCK) { return false; } diff --git a/encoder/basisu_astc_hdr_common.h b/encoder/basisu_astc_hdr_common.h index e019992..e9f6089 100644 --- a/encoder/basisu_astc_hdr_common.h +++ b/encoder/basisu_astc_hdr_common.h @@ -402,7 +402,7 @@ namespace basisu uint8_t* pDst_weights); // [wy][wx] void downsample_ise_weights( - uint32_t weight_ise_range, uint32_t quant_weight_ise_range, + uint32_t dequant_weight_ise_range, uint32_t quant_weight_ise_range, uint32_t block_w, uint32_t block_h, uint32_t grid_w, uint32_t grid_h, const uint8_t* pSrc_weights, uint8_t* pDst_weights); diff --git a/encoder/basisu_astc_ldr_common.cpp b/encoder/basisu_astc_ldr_common.cpp index a66969a..97eb01e 100644 --- a/encoder/basisu_astc_ldr_common.cpp +++ b/encoder/basisu_astc_ldr_common.cpp @@ -17,7 +17,8 @@ #include "basisu_astc_hdr_common.h" #include "basisu_astc_ldr_common.h" -#define BASISU_ASTC_LDR_DEBUG_MSGS (1) +#define BASISU_FORCE_EXHAUSTIVE_EVAL (0) +#define BASISU_USE_ORIG_GET_COLORS (0) namespace basisu { @@ -717,10 +718,17 @@ namespace astc_ldr } // Returned in ISE order + // TODO: Investigate alpha handling here vs. latest spec. + // https://raw.githubusercontent.com/KhronosGroup/DataFormat/refs/heads/main/astc.txt + // The safest thing to do may be to assume non-sRGB in the encoder. I don't know yet. + // How should alpha be handled here for lowest divergence from actual ASTC decoding hardware? + // See https://registry.khronos.org/OpenGL/extensions/EXT/EXT_texture_compression_astc_decode_mode.txt + // All channels including alpha >>8. uint32_t get_colors(const color_rgba& l, const color_rgba& h, uint32_t weight_ise_index, color_rgba* pColors, bool decode_mode_srgb) { const uint32_t total_weights = astc_helpers::get_ise_levels(weight_ise_index); +#if BASISU_USE_ORIG_GET_COLORS for (uint32_t i = 0; i < total_weights; i++) { uint32_t w = basisu::g_ise_weight_lerps[weight_ise_index][1 + i]; @@ -728,11 +736,7 @@ namespace astc_ldr for (uint32_t c = 0; c < 4; c++) { int le = l[c], he = h[c]; - - // TODO: Investigate alpha handling here vs. latest spec. - // https://raw.githubusercontent.com/KhronosGroup/DataFormat/refs/heads/main/astc.txt - // The safest thing to do may be to assume non-sRGB in the encoder. I don't know yet. - // How should alpha be handled here for lowest divergence from actual ASTC decoding hardware? + if (decode_mode_srgb) { le = (le << 8) | 0x80; @@ -745,12 +749,80 @@ namespace astc_ldr } uint32_t k = astc_helpers::weight_interpolate(le, he, w); - - // See https://registry.khronos.org/OpenGL/extensions/EXT/EXT_texture_compression_astc_decode_mode.txt - // All channels including alpha >>8. + pColors[i][c] = (uint8_t)(k >> 8); } // c } // i +#else + int le_r = l.r, he_r = h.r, le_g = l.g, he_g = h.g, le_b = l.b, he_b = h.b, le_a = l.a, he_a = h.a; + + const bool rgb_only = (le_a == 255) && (he_a == 255); + + if (decode_mode_srgb) + { + le_r = (le_r << 8) | 0x80; + he_r = (he_r << 8) | 0x80; + + le_g = (le_g << 8) | 0x80; + he_g = (he_g << 8) | 0x80; + + le_b = (le_b << 8) | 0x80; + he_b = (he_b << 8) | 0x80; + + le_a = (le_a << 8) | 0x80; + he_a = (he_a << 8) | 0x80; + } + else + { + le_r = (le_r << 8) | le_r; + he_r = (he_r << 8) | he_r; + + le_g = (le_g << 8) | le_g; + he_g = (he_g << 8) | he_g; + + le_b = (le_b << 8) | le_b; + he_b = (he_b << 8) | he_b; + + le_a = (le_a << 8) | le_a; + he_a = (he_a << 8) | he_a; + } + + if (rgb_only) + { + for (uint32_t i = 0; i < total_weights; i++) + { + const uint32_t w = basisu::g_ise_weight_lerps[weight_ise_index][1 + i]; + + const uint32_t k_r = astc_helpers::weight_interpolate(le_r, he_r, w); + const uint32_t k_g = astc_helpers::weight_interpolate(le_g, he_g, w); + const uint32_t k_b = astc_helpers::weight_interpolate(le_b, he_b, w); + + pColors[i].r = (uint8_t)(k_r >> 8); + pColors[i].g = (uint8_t)(k_g >> 8); + pColors[i].b = (uint8_t)(k_b >> 8); + pColors[i].a = 255; + + } // i + } + else + { + for (uint32_t i = 0; i < total_weights; i++) + { + const uint32_t w = basisu::g_ise_weight_lerps[weight_ise_index][1 + i]; + + const uint32_t k_r = astc_helpers::weight_interpolate(le_r, he_r, w); + const uint32_t k_g = astc_helpers::weight_interpolate(le_g, he_g, w); + const uint32_t k_b = astc_helpers::weight_interpolate(le_b, he_b, w); + const uint32_t k_a = astc_helpers::weight_interpolate(le_a, he_a, w); + + pColors[i].r = (uint8_t)(k_r >> 8); + pColors[i].g = (uint8_t)(k_g >> 8); + pColors[i].b = (uint8_t)(k_b >> 8); + pColors[i].a = (uint8_t)(k_a >> 8); + + } // i + } +#endif return total_weights; } @@ -758,16 +830,13 @@ namespace astc_ldr // Returns 65 colors (NOT just 64 - 0-64 weight levels, so 65). uint32_t get_colors_raw_weights(const color_rgba& l, const color_rgba& h, color_rgba* pColors, bool decode_mode_srgb) { +#if BASISU_USE_ORIG_GET_COLORS for (uint32_t w = 0; w <= 64; w++) { for (uint32_t c = 0; c < 4; c++) { int le = l[c], he = h[c]; - - // TODO: Investigate alpha handling here vs. latest spec. - // https://raw.githubusercontent.com/KhronosGroup/DataFormat/refs/heads/main/astc.txt - // The safest thing to do may be to assume non-sRGB in the encoder. I don't know yet. - // How should alpha be handled here for lowest divergence from actual ASTC decoding hardware? + if (decode_mode_srgb) { le = (le << 8) | 0x80; @@ -781,12 +850,76 @@ namespace astc_ldr uint32_t k = astc_helpers::weight_interpolate(le, he, w); - // See https://registry.khronos.org/OpenGL/extensions/EXT/EXT_texture_compression_astc_decode_mode.txt - // All channels including alpha >>8. pColors[w][c] = (uint8_t)(k >> 8); } // c } // i +#else + int le_r = l.r, he_r = h.r, le_g = l.g, he_g = h.g, le_b = l.b, he_b = h.b, le_a = l.a, he_a = h.a; + + const bool rgb_only = (le_a == 255) && (he_a == 255); + + if (decode_mode_srgb) + { + le_r = (le_r << 8) | 0x80; + he_r = (he_r << 8) | 0x80; + + le_g = (le_g << 8) | 0x80; + he_g = (he_g << 8) | 0x80; + + le_b = (le_b << 8) | 0x80; + he_b = (he_b << 8) | 0x80; + + le_a = (le_a << 8) | 0x80; + he_a = (he_a << 8) | 0x80; + } + else + { + le_r = (le_r << 8) | le_r; + he_r = (he_r << 8) | he_r; + + le_g = (le_g << 8) | le_g; + he_g = (he_g << 8) | he_g; + + le_b = (le_b << 8) | le_b; + he_b = (he_b << 8) | he_b; + + le_a = (le_a << 8) | le_a; + he_a = (he_a << 8) | he_a; + } + + if (rgb_only) + { + for (uint32_t w = 0; w <= 64; w++) + { + const uint32_t k_r = astc_helpers::weight_interpolate(le_r, he_r, w); + const uint32_t k_g = astc_helpers::weight_interpolate(le_g, he_g, w); + const uint32_t k_b = astc_helpers::weight_interpolate(le_b, he_b, w); + + pColors[w].r = (uint8_t)(k_r >> 8); + pColors[w].g = (uint8_t)(k_g >> 8); + pColors[w].b = (uint8_t)(k_b >> 8); + pColors[w].a = 255; + + } // i + } + else + { + for (uint32_t w = 0; w <= 64; w++) + { + const uint32_t k_r = astc_helpers::weight_interpolate(le_r, he_r, w); + const uint32_t k_g = astc_helpers::weight_interpolate(le_g, he_g, w); + const uint32_t k_b = astc_helpers::weight_interpolate(le_b, he_b, w); + const uint32_t k_a = astc_helpers::weight_interpolate(le_a, he_a, w); + + pColors[w].r = (uint8_t)(k_r >> 8); + pColors[w].g = (uint8_t)(k_g >> 8); + pColors[w].b = (uint8_t)(k_b >> 8); + pColors[w].a = (uint8_t)(k_a >> 8); + + } // i + } +#endif return ASTC_LDR_MAX_RAW_WEIGHTS; } @@ -877,8 +1010,7 @@ namespace astc_ldr return mean_axis; } #endif - - // TODO: Try two-step Lanczos iteration/Rayleigh–Ritz approximation in a 2-dimensional Krylov subspace method vs. power method. + static vec4F calc_pca_4D(uint32_t num_pixels, const vec4F* pPixels, const vec4F& mean_f) { float m00 = 0, m01 = 0, m02 = 0, m03 = 0; @@ -1006,7 +1138,7 @@ namespace astc_ldr m_mean_rel_axis3 = calc_pca_3D(m_num_pixels, m_pixels_f, m_mean_f); m_zero_rel_axis3 = calc_pca_3D(m_num_pixels, m_pixels_f, vec4F(0.0f)); - // Mean and zero relative RGBA (4D) PCA axes + // Mean relative RGBA (4D) PCA axes m_mean_rel_axis4 = calc_pca_4D(m_num_pixels, m_pixels_f, m_mean_f); for (uint32_t c = 0; c < 4u; c++) @@ -1022,7 +1154,7 @@ namespace astc_ldr return (uint32_t)(d * d); } - uint64_t eval_solution( + static uint64_t eval_solution( const pixel_stats_t& pixel_stats, uint32_t total_weights, const color_rgba* pWeight_colors, uint8_t* pWeight_vals, uint32_t weight_ise_index, @@ -1031,57 +1163,167 @@ namespace astc_ldr BASISU_NOTE_UNUSED(weight_ise_index); assert((total_weights <= 32) || (total_weights == 65)); + const bool all_one_weights = (params.m_comp_weights[0] == 1) && (params.m_comp_weights[1] == 1) && (params.m_comp_weights[2] == 1) && (params.m_comp_weights[3] == 1); + + bool exhaustive_flag = params.m_use_exhaustive_weight_eval; + if (BASISU_FORCE_EXHAUSTIVE_EVAL) + exhaustive_flag = true; + else if (!all_one_weights) + exhaustive_flag = true; + uint64_t total_err = 0; if (params.m_pForced_weight_vals0) { - for (uint32_t c = 0; c < pixel_stats.m_num_pixels; c++) + if (all_one_weights) { - const color_rgba& px = pixel_stats.m_pixels[c]; + for (uint32_t c = 0; c < pixel_stats.m_num_pixels; c++) + { + const color_rgba& px = pixel_stats.m_pixels[c]; - const uint32_t w = params.m_pForced_weight_vals0[c]; - assert(w < total_weights); + const uint32_t w = params.m_pForced_weight_vals0[c]; + assert(w < total_weights); - uint32_t err = - params.m_comp_weights[0] * square_of_diff(px.r, pWeight_colors[w].r) + - params.m_comp_weights[1] * square_of_diff(px.g, pWeight_colors[w].g) + - params.m_comp_weights[2] * square_of_diff(px.b, pWeight_colors[w].b) + - params.m_comp_weights[3] * square_of_diff(px.a, pWeight_colors[w].a); + uint32_t err = square_of_diff(px.r, pWeight_colors[w].r) + + square_of_diff(px.g, pWeight_colors[w].g) + + square_of_diff(px.b, pWeight_colors[w].b) + + square_of_diff(px.a, pWeight_colors[w].a); - total_err += err; - - pWeight_vals[c] = (uint8_t)w; + total_err += err; + + pWeight_vals[c] = (uint8_t)w; + } + } + else + { + for (uint32_t c = 0; c < pixel_stats.m_num_pixels; c++) + { + const color_rgba& px = pixel_stats.m_pixels[c]; + + const uint32_t w = params.m_pForced_weight_vals0[c]; + assert(w < total_weights); + + uint32_t err = + params.m_comp_weights[0] * square_of_diff(px.r, pWeight_colors[w].r) + + params.m_comp_weights[1] * square_of_diff(px.g, pWeight_colors[w].g) + + params.m_comp_weights[2] * square_of_diff(px.b, pWeight_colors[w].b) + + params.m_comp_weights[3] * square_of_diff(px.a, pWeight_colors[w].a); + + total_err += err; + + pWeight_vals[c] = (uint8_t)w; + } + } + } + else if (exhaustive_flag) + { + if (all_one_weights) + { + for (uint32_t c = 0; c < pixel_stats.m_num_pixels; c++) + { + const color_rgba& px = pixel_stats.m_pixels[c]; + + uint32_t best_err = UINT32_MAX; + uint32_t best_sel = 0; + + for (uint32_t i = 0; i < total_weights; i++) + { + uint32_t err = + square_of_diff(px.r, pWeight_colors[i].r) + + square_of_diff(px.g, pWeight_colors[i].g) + + square_of_diff(px.b, pWeight_colors[i].b) + + square_of_diff(px.a, pWeight_colors[i].a); + + if (err < best_err) + { + best_err = err; + best_sel = i; + } + } + + total_err += best_err; + pWeight_vals[c] = (uint8_t)best_sel; + } + } + else + { + for (uint32_t c = 0; c < pixel_stats.m_num_pixels; c++) + { + const color_rgba& px = pixel_stats.m_pixels[c]; + + uint32_t best_err = UINT32_MAX; + uint32_t best_sel = 0; + + for (uint32_t i = 0; i < total_weights; i++) + { + uint32_t err = + params.m_comp_weights[0] * square_of_diff(px.r, pWeight_colors[i].r) + + params.m_comp_weights[1] * square_of_diff(px.g, pWeight_colors[i].g) + + params.m_comp_weights[2] * square_of_diff(px.b, pWeight_colors[i].b) + + params.m_comp_weights[3] * square_of_diff(px.a, pWeight_colors[i].a); + + if (err < best_err) + { + best_err = err; + best_sel = i; + } + } + + total_err += best_err; + pWeight_vals[c] = (uint8_t)best_sel; + } } } else { + assert(all_one_weights); + + // low endpoint must be the first color (w=0) + const color_rgba& e_l = pWeight_colors[0]; + + // high endpoint must be either the last color or the 2nd depending on the weight BISE range + color_rgba e_h(pWeight_colors[1]); + if ((total_weights == 65) || (g_ise_weight_lerps[weight_ise_index][total_weights] == 64)) + e_h = pWeight_colors[total_weights - 1]; + else + { + assert(g_ise_weight_lerps[weight_ise_index][1 + 1] == 64); + } + + const int dr = e_h[0] - e_l[0], dg = e_h[1] - e_l[1], db = e_h[2] - e_l[2], da = e_h[3] - e_l[3]; + + // computes weights between [0,64] + const float f = 64.0f / (float)(basisu::squarei(dr) + basisu::squarei(dg) + basisu::squarei(db) + basisu::squarei(da) + .00000125f); + + const auto *pTab = (weight_ise_index <= astc_helpers::BISE_32_LEVELS) ? &astc_helpers::g_dequant_tables.get_weight_tab(weight_ise_index).m_val_to_ise : nullptr; + + const int sofs = -(e_l[0] * dr + e_l[1] * dg + e_l[2] * db + e_l[3] * da); + for (uint32_t c = 0; c < pixel_stats.m_num_pixels; c++) { const color_rgba& px = pixel_stats.m_pixels[c]; - uint32_t best_err = UINT32_MAX; - uint32_t best_sel = 0; + int sel = (int)(float(px[0] * dr + px[1] * dg + px[2] * db + px[3] * da + sofs) * f + .5f); - for (uint32_t i = 0; i < total_weights; i++) - { - uint32_t err = - params.m_comp_weights[0] * square_of_diff(px.r, pWeight_colors[i].r) + - params.m_comp_weights[1] * square_of_diff(px.g, pWeight_colors[i].g) + - params.m_comp_weights[2] * square_of_diff(px.b, pWeight_colors[i].b) + - params.m_comp_weights[3] * square_of_diff(px.a, pWeight_colors[i].a); + if (sel < 0) + sel = 0; + else if (sel > 64) + sel = 64; - if (err < best_err) - { - best_err = err; - best_sel = i; - } - } + int best_sel = pTab ? (*pTab)[sel] : sel; + + assert(best_sel < (int)total_weights); + + total_err += + square_of_diff(px.r, pWeight_colors[best_sel].r) + + square_of_diff(px.g, pWeight_colors[best_sel].g) + + square_of_diff(px.b, pWeight_colors[best_sel].b) + + square_of_diff(px.a, pWeight_colors[best_sel].a); - total_err += best_err; pWeight_vals[c] = (uint8_t)best_sel; - } - } // if (params.m_pForced_weight_vals0) - + } // c + } + return total_err; } @@ -1118,7 +1360,7 @@ namespace astc_ldr } // Evaluates against raw weights [0,64], or to ISE quantized weights, depending on weight_ise_index. - uint64_t eval_solution_dp( + static uint64_t eval_solution_dp( uint32_t ccs_index, const pixel_stats_t& pixel_stats, uint32_t total_weights, const color_rgba* pWeight_colors, @@ -1127,9 +1369,17 @@ namespace astc_ldr { BASISU_NOTE_UNUSED(weight_ise_index); - assert((ccs_index >= 0) && (ccs_index <= 3)); + assert(ccs_index <= 3); assert((total_weights <= 32) || (total_weights == 65)); + const bool all_one_weights = (params.m_comp_weights[0] == 1) && (params.m_comp_weights[1] == 1) && (params.m_comp_weights[2] == 1) && (params.m_comp_weights[3] == 1); + + bool exhaustive_flag = params.m_use_exhaustive_weight_eval; + if (BASISU_FORCE_EXHAUSTIVE_EVAL) + exhaustive_flag = true; + else if (!all_one_weights) + exhaustive_flag = true; + uint64_t total_err = 0; if (params.m_pForced_weight_vals0) @@ -1151,7 +1401,7 @@ namespace astc_ldr pWeight_vals0[c] = (uint8_t)w; } } - else + else if (exhaustive_flag) { for (uint32_t c = 0; c < pixel_stats.m_num_pixels; c++) { @@ -1178,6 +1428,61 @@ namespace astc_ldr pWeight_vals0[c] = (uint8_t)best_sel; } } + else + { + assert(all_one_weights); + + // low endpoint must be the first color (w=0) + color_rgba e_l(pWeight_colors[0]); + + // high endpoint must be either the last color or the 2nd depending on the weight BISE range + color_rgba e_h(pWeight_colors[1]); + + if ((total_weights == 65) || (g_ise_weight_lerps[weight_ise_index][total_weights] == 64)) + e_h = pWeight_colors[total_weights - 1]; + else + { + assert(g_ise_weight_lerps[weight_ise_index][1 + 1] == 64); + } + + std::swap(e_l[ccs_index], e_l[3]); + std::swap(e_h[ccs_index], e_h[3]); + + const int dr = e_h[0] - e_l[0], dg = e_h[1] - e_l[1], db = e_h[2] - e_l[2]; + + const float f = 64.0f / (float)(basisu::squarei(dr) + basisu::squarei(dg) + basisu::squarei(db) + .00000125f); + + const int sofs = -(e_l[0] * dr + e_l[1] * dg + e_l[2] * db); + + const auto* pTab = (weight_ise_index <= astc_helpers::BISE_32_LEVELS) ? &astc_helpers::g_dequant_tables.get_weight_tab(weight_ise_index).m_val_to_ise : nullptr; + + for (uint32_t c = 0; c < pixel_stats.m_num_pixels; c++) + { + color_rgba px(pixel_stats.m_pixels[c]); + std::swap(px[ccs_index], px[3]); + + int sel = (int)(float(px[0] * dr + px[1] * dg + px[2] * db + sofs) * f + .5f); + + if (sel < 0) + sel = 0; + else if (sel > 64) + sel = 64; + + int best_sel = pTab ? (*pTab)[sel] : sel; + + assert(best_sel < (int)total_weights); + + color_rgba wc(pWeight_colors[best_sel]); + std::swap(wc[ccs_index], wc[3]); + + total_err += + square_of_diff(px.r, wc.r) + + square_of_diff(px.g, wc.g) + + square_of_diff(px.b, wc.b); + + pWeight_vals0[c] = (uint8_t)best_sel; + } // c + } if (params.m_pForced_weight_vals1) { @@ -1194,7 +1499,7 @@ namespace astc_ldr pWeight_vals1[c] = (uint8_t)w; } } - else + else if (exhaustive_flag) { for (uint32_t c = 0; c < pixel_stats.m_num_pixels; c++) { @@ -1218,6 +1523,53 @@ namespace astc_ldr pWeight_vals1[c] = (uint8_t)best_sel; } } + else + { + assert(all_one_weights); + + // low endpoint must be the first color (w=0) + color_rgba e_l(pWeight_colors[0]); + + // high endpoint must be either the last color or the 2nd depending on the weight BISE range + color_rgba e_h(pWeight_colors[1]); + + if ((total_weights == 65) || (g_ise_weight_lerps[weight_ise_index][total_weights] == 64)) + e_h = pWeight_colors[total_weights - 1]; + else + { + assert(g_ise_weight_lerps[weight_ise_index][1 + 1] == 64); + } + + const int la = e_l[ccs_index]; + const int ha = e_h[ccs_index]; + const int da = ha - la; + + const float f = 64.0f / (float)(da + .00000125f); + + const auto* pTab = (weight_ise_index <= astc_helpers::BISE_32_LEVELS) ? &astc_helpers::g_dequant_tables.get_weight_tab(weight_ise_index).m_val_to_ise : nullptr; + + for (uint32_t c = 0; c < pixel_stats.m_num_pixels; c++) + { + const color_rgba &px = pixel_stats.m_pixels[c]; + + const int pxa = px[ccs_index]; + + int sel = (int)(float(pxa - la) * f + .5f); + + if (sel < 0) + sel = 0; + else if (sel > 64) + sel = 64; + + int best_sel = pTab ? (*pTab)[sel] : sel; + + assert(best_sel < (int)total_weights); + + total_err += square_of_diff(pxa, pWeight_colors[best_sel][ccs_index]); + + pWeight_vals1[c] = (uint8_t)best_sel; + } // c + } return total_err; } @@ -1837,6 +2189,53 @@ namespace astc_ldr return improved_flag; } + static uint32_t compute_endpoint_distance( + const color_rgba &low_color1, const color_rgba &high_color1, + const color_rgba &low_color2, const color_rgba &high_color2) + { + uint32_t ll = color_distance(low_color2, low_color1, true); + uint32_t lh = color_distance(low_color2, high_color1, true); + + uint32_t hl = color_distance(high_color2, low_color1, true); + uint32_t hh = color_distance(high_color2, high_color1, true); + + uint32_t d0 = ll + hh; + uint32_t d1 = lh + hl; + + return minimum(d0, d1); + } + + // true if the direct encoding would be better vs. the current base+ofs encodiing + static bool compare_base_ofs_vs_direct_encoding( + uint32_t cem_index, + const uint8_t *pTrial_endpoint_vals, uint32_t endpoint_ise_range, + const color_rgba &low_color, const color_rgba &high_color, + bool try_blue_contract) + { + assert((cem_index == astc_helpers::CEM_LDR_RGB_BASE_PLUS_OFFSET) || (cem_index == astc_helpers::CEM_LDR_RGBA_BASE_PLUS_OFFSET)); + + color_rgba base_ofs_l, base_ofs_h; + decode_endpoints(cem_index, pTrial_endpoint_vals, endpoint_ise_range, base_ofs_l, base_ofs_h); + + const uint32_t baseofs_best_d = compute_endpoint_distance(base_ofs_l, base_ofs_h, low_color, high_color); + if (!baseofs_best_d) + return false; + + uint8_t direct_endpoint_vals[astc_helpers::NUM_MODE12_ENDPOINTS] = { 0 }; + + const uint32_t direct_cem_index = (cem_index == astc_helpers::CEM_LDR_RGB_BASE_PLUS_OFFSET) ? astc_helpers::CEM_LDR_RGB_DIRECT : astc_helpers::CEM_LDR_RGBA_DIRECT; + + cem_encode_ldr_rgb_or_rgba_direct_result direct_res = cem_encode_ldr_rgb_or_rgba_direct(direct_cem_index, endpoint_ise_range, low_color, high_color, direct_endpoint_vals, try_blue_contract); + BASISU_NOTE_UNUSED(direct_res); // call populates direct_endpoint_vals (used below); its returned struct is unused - silence -Wunused-variable + + color_rgba direct_l, direct_h; + decode_endpoints(direct_cem_index, direct_endpoint_vals, endpoint_ise_range, direct_l, direct_h); + + const uint32_t direct_best_d = compute_endpoint_distance(direct_l, direct_h, low_color, high_color); + + return direct_best_d < baseofs_best_d; + } + // base+offset rgb/rgba, single or dual plane static bool try_cem9_13_sp_or_dp( uint32_t cem_index, int ccs_index, @@ -1844,7 +2243,8 @@ namespace astc_ldr uint32_t endpoint_ise_range, uint32_t weight_ise_range, const vec4F& low_color_f, const vec4F& high_color_f, uint8_t* pTrial_endpoint_vals, uint8_t* pTrial_weight_vals0, uint8_t* pTrial_weight_vals1, uint64_t& trial_blk_error, bool& trial_used_blue_contraction, - bool try_blue_contract, bool& tried_used_blue_contraction, bool &tried_base_ofs_clamped) + bool try_blue_contract, bool& tried_used_blue_contraction, + bool &try_direct_encoding_flag) // try_direct_encoding_flag is now a flag that indicates the base_ofs encoding clamped OR if the rgb(a) direct encoding may be better to try { assert(g_initialized); assert((cem_index == astc_helpers::CEM_LDR_RGB_BASE_PLUS_OFFSET) || (cem_index == astc_helpers::CEM_LDR_RGBA_BASE_PLUS_OFFSET)); @@ -1855,7 +2255,7 @@ namespace astc_ldr assert(pTrial_weight_vals0); assert((ccs_index == -1) || (pTrial_weight_vals1)); - + //const uint32_t num_endpoint_vals = astc_helpers::get_num_cem_values(cem_index); const uint32_t num_comps = (cem_index == astc_helpers::CEM_LDR_RGB_BASE_PLUS_OFFSET) ? 3 : 4; @@ -1873,11 +2273,19 @@ namespace astc_ldr rgb_base_offset_res res = cem_encode_ldr_rgb_or_rgba_base_offset(cem_index, endpoint_ise_range, low_color, high_color, trial_endpoint_vals, try_blue_contract); tried_used_blue_contraction = res.m_used_blue_contraction; - tried_base_ofs_clamped = res.m_delta_clamped; + try_direct_encoding_flag = res.m_delta_clamped; if (res.m_failed_flag) return false; + if (!try_direct_encoding_flag) + { + if (compare_base_ofs_vs_direct_encoding(cem_index, trial_endpoint_vals, endpoint_ise_range, low_color, high_color, try_blue_contract)) + { + try_direct_encoding_flag = true; + } + } + bool improved_flag = false; if (ccs_index == -1) @@ -1952,7 +2360,7 @@ namespace astc_ldr if (res.m_failed_flag) continue; - + if (ccs_index == -1) { uint64_t trial_err = eval_solution( @@ -1970,8 +2378,16 @@ namespace astc_ldr memset(pTrial_weight_vals1, 0, pixel_stats.m_num_pixels); trial_used_blue_contraction = res.m_used_blue_contraction; if (res.m_delta_clamped) - tried_base_ofs_clamped = true; + try_direct_encoding_flag = true; improved_flag = true; + + if (!try_direct_encoding_flag) + { + if (compare_base_ofs_vs_direct_encoding(cem_index, trial_endpoint_vals, endpoint_ise_range, low_color, high_color, try_blue_contract)) + { + try_direct_encoding_flag = true; + } + } } } else @@ -1990,8 +2406,16 @@ namespace astc_ldr memcpy(pTrial_weight_vals1, trial_weight_vals1, pixel_stats.m_num_pixels); trial_used_blue_contraction = res.m_used_blue_contraction; if (res.m_delta_clamped) - tried_base_ofs_clamped = true; + try_direct_encoding_flag = true; improved_flag = true; + + if (!try_direct_encoding_flag) + { + if (compare_base_ofs_vs_direct_encoding(cem_index, trial_endpoint_vals, endpoint_ise_range, low_color, high_color, try_blue_contract)) + { + try_direct_encoding_flag = true; + } + } } } @@ -2029,8 +2453,16 @@ namespace astc_ldr memset(pTrial_weight_vals1, 0, pixel_stats.m_num_pixels); trial_used_blue_contraction = res.m_used_blue_contraction; if (res.m_delta_clamped) - tried_base_ofs_clamped = true; + try_direct_encoding_flag = true; improved_flag = true; + + if (!try_direct_encoding_flag) + { + if (compare_base_ofs_vs_direct_encoding(cem_index, trial_endpoint_vals, endpoint_ise_range, low_color, high_color, try_blue_contract)) + { + try_direct_encoding_flag = true; + } + } } } else @@ -2049,13 +2481,21 @@ namespace astc_ldr memcpy(pTrial_weight_vals1, trial_weight_vals1, pixel_stats.m_num_pixels); trial_used_blue_contraction = res.m_used_blue_contraction; if (res.m_delta_clamped) - tried_base_ofs_clamped = true; + try_direct_encoding_flag = true; improved_flag = true; + + if (!try_direct_encoding_flag) + { + if (compare_base_ofs_vs_direct_encoding(cem_index, trial_endpoint_vals, endpoint_ise_range, low_color, high_color, try_blue_contract)) + { + try_direct_encoding_flag = true; + } + } } } } } - + return improved_flag; } @@ -2582,7 +3022,7 @@ namespace astc_ldr uint32_t cem_index, const pixel_stats_t& pixel_stats, const cem_encode_params& enc_params, uint32_t endpoint_ise_range, uint32_t weight_ise_range, - uint8_t* pEndpoint_vals, uint8_t* pWeight_vals, uint64_t cur_blk_error, bool use_blue_contraction, bool* pBase_ofs_clamped_flag) + uint8_t* pEndpoint_vals, uint8_t* pWeight_vals, uint64_t cur_blk_error, bool use_blue_contraction, bool* pTry_direct_encoding_flag) { assert(g_initialized); assert((cem_index == astc_helpers::CEM_LDR_RGB_DIRECT) || (cem_index == astc_helpers::CEM_LDR_RGBA_DIRECT) || @@ -2592,8 +3032,8 @@ namespace astc_ldr assert((endpoint_ise_range >= astc_helpers::FIRST_VALID_ENDPOINT_ISE_RANGE) && (endpoint_ise_range <= astc_helpers::LAST_VALID_ENDPOINT_ISE_RANGE)); assert(((weight_ise_range >= astc_helpers::FIRST_VALID_WEIGHT_ISE_RANGE) && (weight_ise_range <= astc_helpers::LAST_VALID_WEIGHT_ISE_RANGE)) || (weight_ise_range == astc_helpers::BISE_64_LEVELS)); - if (pBase_ofs_clamped_flag) - *pBase_ofs_clamped_flag = false; + if (pTry_direct_encoding_flag) + *pTry_direct_encoding_flag = false; const bool cem_has_alpha = (cem_index == astc_helpers::CEM_LDR_RGBA_DIRECT) || (cem_index == astc_helpers::CEM_LDR_RGBA_BASE_PLUS_OFFSET); const bool cem_is_base_offset = (cem_index == astc_helpers::CEM_LDR_RGB_BASE_PLUS_OFFSET) || (cem_index == astc_helpers::CEM_LDR_RGBA_BASE_PLUS_OFFSET); @@ -2650,18 +3090,18 @@ namespace astc_ldr if (cem_is_base_offset) { - bool tried_base_ofs_clamped = false; - + bool try_direct_encoding_flag = false; + try_cem9_13_sp_or_dp( cem_index, -1, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, low_color_f, high_color_f, trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, use_blue_contraction, - tried_used_blue_contraction, tried_base_ofs_clamped); - - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + tried_used_blue_contraction, try_direct_encoding_flag); + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; + if (tried_used_blue_contraction) { try_cem9_13_sp_or_dp( @@ -2669,10 +3109,10 @@ namespace astc_ldr endpoint_ise_range, weight_ise_range, low_color_f, high_color_f, trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, false, - tried_used_blue_contraction, tried_base_ofs_clamped); + tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; } } else @@ -2725,16 +3165,16 @@ namespace astc_ldr if (cem_is_base_offset) { - bool tried_base_ofs_clamped = false; + bool try_direct_encoding_flag = false; try_cem9_13_sp_or_dp( cem_index, -1, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, xl, xh, - trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, use_blue_contraction, tried_used_blue_contraction, tried_base_ofs_clamped); + trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, use_blue_contraction, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; if (tried_used_blue_contraction) { @@ -2743,10 +3183,10 @@ namespace astc_ldr cem_index, -1, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, xl, xh, - trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, false, tried_used_blue_contraction, tried_base_ofs_clamped); + trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, false, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; } } else @@ -2806,16 +3246,16 @@ namespace astc_ldr if (cem_is_base_offset) { - bool tried_base_ofs_clamped = false; + bool try_direct_encoding_flag = false; try_cem9_13_sp_or_dp( cem_index, -1, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, xl, xh, - trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, use_blue_contraction, tried_used_blue_contraction, tried_base_ofs_clamped); + trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, use_blue_contraction, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; if (tried_used_blue_contraction) { @@ -2824,10 +3264,10 @@ namespace astc_ldr cem_index, -1, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, xl, xh, - trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, false, tried_used_blue_contraction, tried_base_ofs_clamped); + trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, false, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; } } else @@ -2929,16 +3369,16 @@ namespace astc_ldr if (cem_is_base_offset) { - bool tried_base_ofs_clamped = false; - + bool try_direct_encoding_flag = false; + try_cem9_13_sp_or_dp( cem_index, -1, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, xl, xh, - trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, use_blue_contraction, tried_used_blue_contraction, tried_base_ofs_clamped); + trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, use_blue_contraction, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; if (tried_used_blue_contraction) { @@ -2947,10 +3387,10 @@ namespace astc_ldr cem_index, -1, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, xl, xh, - trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, false, tried_used_blue_contraction, tried_base_ofs_clamped); + trial_blk_endpoints, trial_blk_weights, nullptr, trial_blk_error, trial_used_blue_contraction, false, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; } } else @@ -3041,7 +3481,7 @@ namespace astc_ldr const pixel_stats_t& pixel_stats, const cem_encode_params& enc_params, uint32_t endpoint_ise_range, uint32_t weight_ise_range, uint8_t* pEndpoint_vals, uint8_t* pWeight_vals0, uint8_t* pWeight_vals1, - uint64_t cur_blk_error, bool use_blue_contraction, bool *pBase_ofs_clamped_flag) + uint64_t cur_blk_error, bool use_blue_contraction, bool * pTry_direct_encoding_flag) { assert(g_initialized); assert(ccs_index <= 3); @@ -3049,8 +3489,8 @@ namespace astc_ldr assert((endpoint_ise_range >= astc_helpers::FIRST_VALID_ENDPOINT_ISE_RANGE) && (endpoint_ise_range <= astc_helpers::LAST_VALID_ENDPOINT_ISE_RANGE)); assert(((weight_ise_range >= astc_helpers::FIRST_VALID_WEIGHT_ISE_RANGE) && (weight_ise_range <= astc_helpers::LAST_VALID_WEIGHT_ISE_RANGE)) || (weight_ise_range == astc_helpers::BISE_64_LEVELS)); - if (pBase_ofs_clamped_flag) - *pBase_ofs_clamped_flag = false; + if (pTry_direct_encoding_flag) + *pTry_direct_encoding_flag = false; bool cem_has_alpha = false, cem_is_base_offset = false; switch (cem_index) @@ -3137,17 +3577,17 @@ namespace astc_ldr if (cem_is_base_offset) { - bool tried_base_ofs_clamped = false; + bool try_direct_encoding_flag = false; try_cem9_13_sp_or_dp( cem_index, ccs_index, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, low_color_f, high_color_f, trial_blk_endpoints, trial_blk_weights0, trial_blk_weights1, - trial_blk_error, trial_used_blue_contraction, use_blue_contraction, tried_used_blue_contraction, tried_base_ofs_clamped); + trial_blk_error, trial_used_blue_contraction, use_blue_contraction, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; if (tried_used_blue_contraction) { @@ -3155,10 +3595,10 @@ namespace astc_ldr cem_index, ccs_index, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, low_color_f, high_color_f, - trial_blk_endpoints, trial_blk_weights0, trial_blk_weights1, trial_blk_error, trial_used_blue_contraction, false, tried_used_blue_contraction, tried_base_ofs_clamped); + trial_blk_endpoints, trial_blk_weights0, trial_blk_weights1, trial_blk_error, trial_used_blue_contraction, false, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; } } else @@ -3217,17 +3657,17 @@ namespace astc_ldr if (cem_is_base_offset) { - bool tried_base_ofs_clamped = false; + bool try_direct_encoding_flag = false; try_cem9_13_sp_or_dp( cem_index, ccs_index, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, xl, xh, trial_blk_endpoints, trial_blk_weights0, trial_blk_weights1, trial_blk_error, trial_used_blue_contraction, - use_blue_contraction, tried_used_blue_contraction, tried_base_ofs_clamped); + use_blue_contraction, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; if (tried_used_blue_contraction) { @@ -3237,10 +3677,10 @@ namespace astc_ldr endpoint_ise_range, weight_ise_range, xl, xh, trial_blk_endpoints, trial_blk_weights0, trial_blk_weights1, trial_blk_error, trial_used_blue_contraction, - false, tried_used_blue_contraction, tried_base_ofs_clamped); + false, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; } } else @@ -3314,17 +3754,17 @@ namespace astc_ldr if (cem_is_base_offset) { - bool tried_base_ofs_clamped = false; + bool try_direct_encoding_flag = false; try_cem9_13_sp_or_dp( cem_index, ccs_index, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, vl, vh, trial_blk_endpoints, trial_blk_weights0, trial_blk_weights1, trial_blk_error, trial_used_blue_contraction, - use_blue_contraction, tried_used_blue_contraction, tried_base_ofs_clamped); + use_blue_contraction, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; if (tried_used_blue_contraction) { @@ -3334,10 +3774,10 @@ namespace astc_ldr endpoint_ise_range, weight_ise_range, vl, vh, trial_blk_endpoints, trial_blk_weights0, trial_blk_weights1, trial_blk_error, trial_used_blue_contraction, - false, tried_used_blue_contraction, tried_base_ofs_clamped); + false, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; } } else @@ -3398,17 +3838,17 @@ namespace astc_ldr if (cem_is_base_offset) { - bool tried_base_ofs_clamped = false; + bool try_direct_encoding_flag = false; try_cem9_13_sp_or_dp( cem_index, ccs_index, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, xl, xh, trial_blk_endpoints, trial_blk_weights0, trial_blk_weights1, trial_blk_error, trial_used_blue_contraction, - use_blue_contraction, tried_used_blue_contraction, tried_base_ofs_clamped); + use_blue_contraction, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; if (tried_used_blue_contraction) { @@ -3418,10 +3858,10 @@ namespace astc_ldr endpoint_ise_range, weight_ise_range, xl, xh, trial_blk_endpoints, trial_blk_weights0, trial_blk_weights1, trial_blk_error, trial_used_blue_contraction, - false, tried_used_blue_contraction, tried_base_ofs_clamped); + false, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; } } else @@ -3494,18 +3934,18 @@ namespace astc_ldr if (cem_is_base_offset) { - bool tried_base_ofs_clamped = false; + bool try_direct_encoding_flag = false; did_improve_res = try_cem9_13_sp_or_dp( cem_index, ccs_index, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, vl, vh, trial_blk_endpoints, trial_blk_weights0, trial_blk_weights1, trial_blk_error, trial_used_blue_contraction, - use_blue_contraction, tried_used_blue_contraction, tried_base_ofs_clamped); + use_blue_contraction, tried_used_blue_contraction, try_direct_encoding_flag); BASISU_NOTE_UNUSED(did_improve_res); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; if (tried_used_blue_contraction) { @@ -3515,10 +3955,10 @@ namespace astc_ldr endpoint_ise_range, weight_ise_range, vl, vh, trial_blk_endpoints, trial_blk_weights0, trial_blk_weights1, trial_blk_error, trial_used_blue_contraction, - false, tried_used_blue_contraction, tried_base_ofs_clamped); + false, tried_used_blue_contraction, try_direct_encoding_flag); - if ((pBase_ofs_clamped_flag) && (tried_base_ofs_clamped)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; } } else @@ -4156,7 +4596,7 @@ namespace astc_ldr const pixel_stats_t& pixel_stats, const cem_encode_params& enc_params, uint32_t endpoint_ise_range, uint32_t weight_ise_range, uint8_t* pEndpoint_vals, uint8_t* pWeight_vals0, uint8_t* pWeight_vals1, uint64_t cur_blk_error, - bool use_blue_contraction, bool *pBase_ofs_clamped_flag) + bool use_blue_contraction, bool * pTry_direct_encoding_flag) { assert(g_initialized); assert((ccs_index >= -1) && (ccs_index <= 3)); @@ -4166,8 +4606,8 @@ namespace astc_ldr const bool dual_plane = (ccs_index >= 0); - if (pBase_ofs_clamped_flag) - *pBase_ofs_clamped_flag = false; + if (pTry_direct_encoding_flag) + *pTry_direct_encoding_flag = false; uint64_t blk_error = UINT64_MAX; @@ -4221,7 +4661,7 @@ namespace astc_ldr cem_index, ccs_index, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, - pEndpoint_vals, pWeight_vals0, pWeight_vals1, cur_blk_error, use_blue_contraction, pBase_ofs_clamped_flag); + pEndpoint_vals, pWeight_vals0, pWeight_vals1, cur_blk_error, use_blue_contraction, pTry_direct_encoding_flag); } else { @@ -4229,7 +4669,7 @@ namespace astc_ldr cem_index, pixel_stats, enc_params, endpoint_ise_range, weight_ise_range, - pEndpoint_vals, pWeight_vals0, cur_blk_error, use_blue_contraction, pBase_ofs_clamped_flag); + pEndpoint_vals, pWeight_vals0, cur_blk_error, use_blue_contraction, pTry_direct_encoding_flag); } break; } @@ -4407,14 +4847,13 @@ namespace astc_ldr } return total_err; - } float surrogate_evaluate_rgba_dp(uint32_t ccs_index, const pixel_stats_t& ps, const vec4F& l, const vec4F& h, float* pWeights0, float* pWeights1, uint32_t num_weight_levels, const cem_encode_params& enc_params, uint32_t flags) { assert(g_initialized); - assert((ccs_index >= 0) && (ccs_index <= 3)); + assert(ccs_index <= 3); assert((ps.m_num_pixels) && (ps.m_num_pixels <= ASTC_LDR_MAX_BLOCK_PIXELS)); assert(pWeights0 && pWeights1); @@ -4830,7 +5269,7 @@ namespace astc_ldr uint32_t endpoint_ise_range, uint32_t weight_ise_range, vec4F& low_endpoint, vec4F& high_endpoint, float* pWeights0, float *pWeights1, uint32_t flags) { - assert((ccs_index >= 0) && (ccs_index <= 3)); + assert(ccs_index <= 3); const uint32_t num_endpoint_levels = astc_helpers::get_ise_levels(endpoint_ise_range); // astc_helpers::BISE_64_LEVELS=raw weights ([0,64], NOT [0,63]) @@ -5153,14 +5592,99 @@ namespace astc_ldr return *this; } - // misnamed- just SAD distance, not square + // originally was SAD distance, but that is slower int partition_pattern_vec::get_squared_distance(const partition_pattern_vec& other) const { const uint32_t total_pixels = get_total(); +#if 0 int total_dist = 0; for (uint32_t i = 0; i < total_pixels; i++) - total_dist += iabs((int)m_parts[i] - (int)other.m_parts[i]); + total_dist += squarei((int)m_parts[i] - (int)other.m_parts[i]); + + return total_dist; +#else + int total_dist0 = 0, total_dist1 = 0, total_dist2 = 0, total_dist3 = 0; + + uint32_t idx = 0; + while ((idx + 4) <= total_pixels) + { + total_dist0 += squarei((int)m_parts[idx] - (int)other.m_parts[idx]); + total_dist1 += squarei((int)m_parts[idx + 1] - (int)other.m_parts[idx + 1]); + total_dist2 += squarei((int)m_parts[idx + 2] - (int)other.m_parts[idx + 2]); + total_dist3 += squarei((int)m_parts[idx + 3] - (int)other.m_parts[idx + 3]); + + idx += 4; + } + + while (idx < total_pixels) + { + total_dist0 += squarei((int)m_parts[idx] - (int)other.m_parts[idx]); + ++idx; + } + + int total_dist = total_dist0 + total_dist1 + total_dist2 + total_dist3; + + return total_dist; +#endif + } + + int partition_pattern_vec::get_squared_distance_2subsets(const partition_pattern_vec& other) const + { + const uint32_t total_pixels = get_total(); + + int total_dist0 = 0, total_dist1 = 0, total_dist2 = 0, total_dist3 = 0; + + uint32_t idx = 0; + while ((idx + 4) <= total_pixels) + { + total_dist0 += (m_parts[idx] ^ other.m_parts[idx]); + total_dist1 += (m_parts[idx + 1] ^ other.m_parts[idx + 1]); + total_dist2 += (m_parts[idx + 2] ^ other.m_parts[idx + 2]); + total_dist3 += (m_parts[idx + 3] ^ other.m_parts[idx + 3]); + + idx += 4; + } + + while (idx < total_pixels) + { + total_dist0 += squarei(m_parts[idx] ^ other.m_parts[idx]); + ++idx; + } + + int total_dist = total_dist0 + total_dist1 + total_dist2 + total_dist3; + + return total_dist; + } + + int partition_pattern_vec::get_squared_distance(const partition_pattern_vec& other, uint32_t num_subsets, uint32_t other_perm_index) const + { + assert((num_subsets >= 2) && (num_subsets <= 3)); + + if (num_subsets == 2) + { + assert(other_perm_index < 2); + } + else + { + assert(other_perm_index < NUM_PART3_MAPPINGS); + } + + const uint32_t total_pixels = get_total(); + + int total_dist = 0; + + for (uint32_t i = 0; i < total_pixels; i++) + { + int p = other.m_parts[i]; + + if (num_subsets == 2) + p ^= other_perm_index; + else + p = g_part3_mapping[other_perm_index][p]; + + total_dist += squarei((int)m_parts[i] - p); + } return total_dist; } @@ -5566,7 +6090,177 @@ namespace astc_ldr return std::pair(best_split_pat, best_split_dist); } - void partitions_data::init(uint32_t num_partitions, uint32_t block_width, uint32_t block_height, bool init_vp_tree) + void partition_lsh_map::init(uint32_t num_subsets, uint32_t n, const partition_pattern_vec* pUnique_pats) + { + assert((num_subsets >= 2) && (num_subsets <= 3)); + assert(n && pUnique_pats); + + const uint32_t total_texels = pUnique_pats[0].get_width() * pUnique_pats[0].get_height(); + + m_num_subsets = num_subsets; + m_num_samples = (num_subsets == 2) ? 6 : 5; + //m_num_hash_tabs = (total_texels > 36) ? 12 : 8; + m_num_hash_tabs = 12; + + const uint32_t hash_key_shift = (num_subsets == 2) ? 1 : 2; + + const uint32_t hash_tab_size = 1 << (m_num_samples * hash_key_shift); + + assert(m_num_samples <= cMaxSamples); + assert(m_num_hash_tabs <= cMaxHashTabs); + + basisu::rand rnd; + rnd.seed(33155); + + clear_obj(m_sample_indices); + + for (uint32_t h = 0; h < m_num_hash_tabs; h++) + { + for (uint32_t s = 0; s < m_num_samples; s++) + { + for (uint32_t t = 0; t < 4; t++) + { + const uint8_t rnd_index = (uint8_t)rnd.irand(0, total_texels - 1); + + m_sample_indices[h][s] = rnd_index; + + uint32_t b; + for (b = 0; b < s; b++) + if (rnd_index == m_sample_indices[h][b]) + break; + + if (b == s) + break; + } + } + + m_hash_tabs[h].resize(hash_tab_size); + } // h + + for (uint32_t unique_pat_index = 0; unique_pat_index < n; unique_pat_index++) + { + const partition_pattern_vec* pVec = &pUnique_pats[unique_pat_index]; + + const uint32_t max_perm_index = (num_subsets == 2) ? (uint32_t)partition_pattern_vec::cMaxPermute2Index : (uint32_t)partition_pattern_vec::cMaxPermute3Index; + + for (uint32_t perm_index = 0; perm_index <= max_perm_index; perm_index++) + { + const partition_pattern_vec pv((num_subsets == 2) ? pVec->get_permuted2(perm_index) : pVec->get_permuted3(perm_index)); + + for (uint32_t hash_tab_index = 0; hash_tab_index < m_num_hash_tabs; hash_tab_index++) + { + uint32_t hash_key = 0; + + for (uint32_t sample_index = 0; sample_index < m_num_samples; sample_index++) + { + hash_key <<= hash_key_shift; + + const uint32_t texel_index = m_sample_indices[hash_tab_index][sample_index]; + assert(texel_index < total_texels); + + hash_key |= pv[texel_index]; + } // i + + m_hash_tabs[hash_tab_index][hash_key].push_back(unique_pat_index); + + } // h + + } // perm_index + + } // unique_pat_index + } + + static inline size_t unique_duplicates_only(uint32_t* pResults, size_t total_results) + { + if (total_results < 2) + return 0; + + size_t i = 0, out = 0; + while (i < total_results) + { + size_t j = i + 1; + while ((j < total_results) && (pResults[j] == pResults[i])) + ++j; + + // j - i is the run length; keep this value only if it appeared 2+ times + if ((j - i) >= 2) + pResults[out++] = pResults[i]; + + i = j; + } + return out; + } + + uint32_t partition_lsh_map::find(const partition_pattern_vec& desired_pat, uint32_t* pResults, uint32_t max_results, bool filter_flag) const + { + assert(pResults && max_results); + + uint32_t total_results = 0; + + const uint32_t hash_key_shift = (m_num_subsets == 2) ? 1 : 2; + + for (uint32_t hash_tab_index = 0; hash_tab_index < m_num_hash_tabs; hash_tab_index++) + { + uint32_t hash_key = 0; + + for (uint32_t sample_index = 0; sample_index < m_num_samples; sample_index++) + { + hash_key <<= hash_key_shift; + + const uint32_t texel_index = m_sample_indices[hash_tab_index][sample_index]; + + const uint32_t subset_index = desired_pat[texel_index]; + assert(subset_index < m_num_subsets); + + hash_key |= subset_index; + } // i + + const auto& h_tab = m_hash_tabs[hash_tab_index][hash_key]; + + uint32_t num_to_copy = h_tab.size_u32(); + + uint32_t room_remaining = max_results - total_results; + if (!room_remaining) + break; + + if (num_to_copy > room_remaining) + num_to_copy = room_remaining; + + assert((total_results + num_to_copy) <= max_results); + + // num_to_copy can be 0 for an empty hash bucket, in which case get_ptr() + // returns nullptr -- guard the copy so we don't pass null to memcpy + // (UB / UBSAN nonnull violation even with a 0 length). + if (num_to_copy) + memcpy(pResults + total_results, h_tab.get_ptr(), num_to_copy * sizeof(uint32_t)); + total_results += num_to_copy; + + if (total_results >= max_results) + break; + + } // h + + if (!total_results) + return 0; + + std::sort(pResults, pResults + total_results); + + size_t num_unique; + if (filter_flag) + { + num_unique = unique_duplicates_only(pResults, total_results); + assert((num_unique <= total_results) && (num_unique <= max_results)); + } + else + { + num_unique = std::unique(pResults, pResults + total_results) - pResults; + assert((num_unique >= 1) && (num_unique <= total_results) && (num_unique <= max_results)); + } + + return (uint32_t)num_unique; + } + + void partitions_data::init(uint32_t num_partitions, uint32_t block_width, uint32_t block_height, bool init_vp_tree, bool init_lsh) { assert((num_partitions >= 2) && (num_partitions <= 4)); @@ -5659,7 +6353,14 @@ namespace astc_ldr } // seed_index if (init_vp_tree) + { m_part_vp_tree.init(m_total_unique_patterns, m_partition_pats); + } + + if (init_lsh) + { + m_part_lhs_map.init(num_partitions, m_total_unique_patterns, m_partition_pats); + } } } // namespace astc_ldr diff --git a/encoder/basisu_astc_ldr_common.h b/encoder/basisu_astc_ldr_common.h index 76e7e3f..ff9789f 100644 --- a/encoder/basisu_astc_ldr_common.h +++ b/encoder/basisu_astc_ldr_common.h @@ -32,6 +32,7 @@ namespace astc_ldr uint32_t m_max_ls_passes, m_total_weight_refine_passes; bool m_worst_weight_nudging_flag; bool m_endpoint_refinement_flag; + bool m_use_exhaustive_weight_eval; cem_encode_params() { @@ -54,6 +55,7 @@ namespace astc_ldr m_total_weight_refine_passes = 0; m_worst_weight_nudging_flag = false; m_endpoint_refinement_flag = false; + m_use_exhaustive_weight_eval = false; } float get_total_comp_weights() const @@ -111,12 +113,6 @@ namespace astc_ldr //int apply_delta_to_bise_endpoint_val(uint32_t endpoint_ise_range, int ise_val, int delta); int apply_delta_to_bise_weight_val(uint32_t weight_ise_range, int ise_val, int delta); - uint64_t eval_solution( - const pixel_stats_t& pixel_stats, - uint32_t total_weights, const color_rgba* pWeight_colors, - uint8_t* pWeight_vals, uint32_t weight_ise_index, - const cem_encode_params& params); - uint64_t eval_solution( const pixel_stats_t& pixel_stats, uint32_t cem_index, @@ -124,13 +120,6 @@ namespace astc_ldr uint8_t* pWeight_vals, uint32_t weight_ise_index, const cem_encode_params& params); - uint64_t eval_solution_dp( - uint32_t ccs_index, - const pixel_stats_t& pixel_stats, - uint32_t total_weights, const color_rgba* pWeight_colors, - uint8_t* pWeight_vals0, uint8_t* pWeight_vals1, uint32_t weight_ise_index, - const cem_encode_params& params); - uint64_t eval_solution_dp( const pixel_stats_t& pixel_stats, uint32_t cem_index, uint32_t ccs_index, @@ -147,7 +136,7 @@ namespace astc_ldr const pixel_stats_t& pixel_stats, const cem_encode_params& enc_params, uint32_t endpoint_ise_range, uint32_t weight_ise_range, uint8_t* pEndpoint_vals, uint8_t* pWeight_vals0, uint8_t* pWeight_vals1, uint64_t cur_blk_error, - bool use_blue_contraction, bool* pBase_ofs_clamped_flag); + bool use_blue_contraction, bool* pTry_direct_encoding_flag); // TODO: Rename, confusing vs. std::vector or basisu::vector or vec4F etc. struct partition_pattern_vec @@ -169,19 +158,21 @@ namespace astc_ldr partition_pattern_vec& operator= (const partition_pattern_vec& rhs); - uint32_t get_width() const { return m_width; } - uint32_t get_height() const { return m_height; } - uint32_t get_total() const { return m_width * m_height; } + inline uint32_t get_width() const { return m_width; } + inline uint32_t get_height() const { return m_height; } + inline uint32_t get_total() const { return m_width * m_height; } - uint8_t operator[] (uint32_t i) const { assert(i < get_total()); return m_parts[i]; } - uint8_t& operator[] (uint32_t i) { assert(i < get_total()); return m_parts[i]; } + inline uint8_t operator[] (uint32_t i) const { assert(i < get_total()); return m_parts[i]; } + inline uint8_t& operator[] (uint32_t i) { assert(i < get_total()); return m_parts[i]; } - uint8_t operator() (uint32_t x, uint32_t y) const { assert((x < m_width) && (y < m_height)); return m_parts[x + y * m_width]; } - uint8_t& operator() (uint32_t x, uint32_t y) { assert((x < m_width) && (y < m_height)); return m_parts[x + y * m_width]; } + inline uint8_t operator() (uint32_t x, uint32_t y) const { assert((x < m_width) && (y < m_height)); return m_parts[x + y * m_width]; } + inline uint8_t& operator() (uint32_t x, uint32_t y) { assert((x < m_width) && (y < m_height)); return m_parts[x + y * m_width]; } int get_squared_distance(const partition_pattern_vec& other) const; + int get_squared_distance_2subsets(const partition_pattern_vec& other) const; + int get_squared_distance(const partition_pattern_vec& other, uint32_t num_subsets, uint32_t other_perm_index) const; - float get_distance(const partition_pattern_vec& other) const + inline float get_distance(const partition_pattern_vec& other) const { return sqrtf((float)get_squared_distance(other)); } @@ -381,6 +372,32 @@ namespace astc_ldr void clear() { clear_obj(m_hist); } }; + class partition_lsh_map + { + public: + partition_lsh_map() + { + } + + bool is_valid() const { return m_num_subsets != 0; } + + void init(uint32_t num_subsets, uint32_t n, const partition_pattern_vec* pUnique_pats); + + uint32_t find(const partition_pattern_vec& desired_pat, uint32_t *pResults, uint32_t max_results, bool filter_flag) const; + + private: + uint32_t m_num_subsets = 0; + + uint32_t m_num_samples = 0; + uint32_t m_num_hash_tabs = 0; + + enum { cMaxSamples = 10, cMaxHashTabs = 16 }; + + uint8_t m_sample_indices[cMaxHashTabs][cMaxSamples]; + + basisu::vector m_hash_tabs[cMaxHashTabs]; + }; + struct partitions_data { uint32_t m_width, m_height, m_num_partitions; @@ -397,8 +414,10 @@ namespace astc_ldr // VP tree used to rapidly find nearby/similar patterns. vp_tree m_part_vp_tree; + + partition_lsh_map m_part_lhs_map; - void init(uint32_t num_partitions, uint32_t block_width, uint32_t block_height, bool init_vp_tree = true); + void init(uint32_t num_partitions, uint32_t block_width, uint32_t block_height, bool init_vp_tree = true, bool init_lsh = true); }; float surrogate_quant_endpoint_val(float e, uint32_t num_endpoint_levels, uint32_t flags); @@ -419,27 +438,6 @@ namespace astc_ldr uint32_t endpoint_ise_range, uint32_t weight_ise_range, vec4F& low_endpoint, vec4F& high_endpoint, float& s, float* pWeights0, float* pWeights1, uint32_t flags = 0); -#if 0 - bool requantize_ise_endpoints(uint32_t cem, - uint32_t src_ise_endpoint_range, const uint8_t* pSrc_endpoints, - uint32_t dst_ise_endpoint_range, uint8_t* pDst_endpoints); - - uint32_t get_base_cem_without_alpha(uint32_t cem); - - bool pack_base_offset( - uint32_t cem_index, uint32_t dst_ise_endpoint_range, uint8_t* pPacked_endpoints, - const color_rgba& l, const color_rgba& h, - bool use_blue_contraction, bool auto_disable_blue_contraction_if_clamped, - bool& blue_contraction_clamped_flag, bool& base_ofs_clamped_flag, bool& endpoints_swapped); - - bool convert_endpoints_across_cems( - uint32_t prev_cem, uint32_t prev_endpoint_ise_range, const uint8_t* pPrev_endpoints, - uint32_t dst_cem, uint32_t dst_endpoint_ise_range, uint8_t* pDst_endpoints, - bool always_repack, - bool use_blue_contraction, bool auto_disable_blue_contraction_if_clamped, - bool& blue_contraction_clamped_flag, bool& base_ofs_clamped_flag); -#endif - } // namespace astc_ldr } // namespace basisu diff --git a/encoder/basisu_astc_ldr_encode.cpp b/encoder/basisu_astc_ldr_encode.cpp index 414ee6c..b9603ae 100644 --- a/encoder/basisu_astc_ldr_encode.cpp +++ b/encoder/basisu_astc_ldr_encode.cpp @@ -19,6 +19,8 @@ // pick up BASISD_SUPPORT_KTX2_ZSTD macro (this defines it automatically and sets to 1 if not defined) #include "../transcoder/basisu_transcoder.h" +#include "basisu_astc_ldr_fencode.h" + #include #ifndef BASISD_SUPPORT_KTX2_ZSTD @@ -29,6 +31,17 @@ #include "../zstd/zstd.h" #endif +#ifndef BASISU_SUPPORT_ASTCENC +#define BASISU_SUPPORT_ASTCENC (0) +#endif + +#if BASISU_SUPPORT_ASTCENC +#include "3rdparty/astc-encoder-main/Source/astcenc.h" +#endif + +// Compensate for endpoint adjustment (otherwise we're too pessimestic/underranks 2-3 levels) +#define BASISU_MODIFIED_WEIGHT_QUANT_MSE_ESTIMATE (1) + namespace basisu { namespace astc_ldr { @@ -37,6 +50,21 @@ const bool ASTC_LDR_CONSISTENCY_CHECKING = true; bool g_initialized; +const uint32_t BLUR_ID_GAUSSIAN_ALTERNATE = 1; +const uint32_t BLUR_ID_GRID_DIM_BASE = 32; // experimental +const uint32_t BLUR_ID_DC_LATENT_BASE = 128; +const uint32_t BLUR_ID_AC_LATENT_BASE = 138; +const uint32_t BLUR_ID_BEST_CANDIDATE = 256; // experimental + +[[maybe_unused]] const uint32_t BLUR_ID_ASTCENC = 512; +const uint32_t BLUR_ID_ASTCF = 513; + +const uint32_t BLUR_ID_EXP = 1024; + +// Number of blurred source images used during ASTC LDR encoding. Defined at module scope (not as a function local) +// so the per-block job lambda can reference it directly without capturing it (avoids MSVC C3493 and clang/gcc -Wunused-lambda-capture). +const uint32_t TOTAL_BLURRED_IMAGES = 1 * 3; + const uint32_t EXPECTED_SUPERBUCKET_HASH_SIZE = 8192; const uint32_t EXPECTED_SHORTLIST_HASH_SIZE = 4096; @@ -47,16 +75,219 @@ const uint32_t PART_ESTIMATE_STAGE1_MULTIPLIER = 4; const uint32_t MAX_WIDTH = 65535, MAX_HEIGHT = 65535; -void code_block_weights( +static inline uint32_t apply_kernel(uint32_t a, uint32_t b, uint32_t c) +{ + return (a + b + c + 1u) / 3u; +} + +static inline color_rgba filter_horiz(const image& src, int x, int y) +{ + color_rgba l(src.get_clamped(x - 1, y)); + color_rgba c(src.get_clamped(x, y)); + color_rgba r(src.get_clamped(x + 1, y)); + + color_rgba out; + for (uint32_t ch = 0; ch < 4; ch++) + out[ch] = (uint8_t)apply_kernel(l[ch], c[ch], r[ch]); + return out; +} + +static inline color_rgba filter_vert(const image& src, int x, int y) +{ + color_rgba u(src.get_clamped(x, y - 1)); + color_rgba c(src.get_clamped(x, y)); + color_rgba d(src.get_clamped(x, y + 1)); + + color_rgba out; + for (uint32_t ch = 0; ch < 4; ch++) + out[ch] = (uint8_t)apply_kernel(u[ch], c[ch], d[ch]); + return out; +} + +#define BASISU_CORNER(SX, SY, TX, TY) do { \ + const color_rgba h_l = src_img.get_clamped((SX) - 1, (SY)); \ + const color_rgba h_c = src_img.get_clamped((SX), (SY)); \ + const color_rgba h_r = src_img.get_clamped((SX) + 1, (SY)); \ + const color_rgba v_u = src_img.get_clamped((SX), (SY) - 1); \ + const color_rgba v_d = src_img.get_clamped((SX), (SY) + 1); \ + \ + color_rgba out; \ + out.r = (uint8_t)minimum(255, basisu::fast_roundf_pos_int( \ + (float)(h_l.r + 2 * h_c.r + h_r.r + v_u.r + v_d.r) * (1.0f / 6.0f))); \ + out.g = (uint8_t)minimum(255, basisu::fast_roundf_pos_int( \ + (float)(h_l.g + 2 * h_c.g + h_r.g + v_u.g + v_d.g) * (1.0f / 6.0f))); \ + out.b = (uint8_t)minimum(255, basisu::fast_roundf_pos_int( \ + (float)(h_l.b + 2 * h_c.b + h_r.b + v_u.b + v_d.b) * (1.0f / 6.0f))); \ + out.a = (uint8_t)minimum(255, basisu::fast_roundf_pos_int( \ + (float)(h_l.a + 2 * h_c.a + h_r.a + v_u.a + v_d.a) * (1.0f / 6.0f))); \ + dst_tile.set_clipped((TX), (TY), out); \ +} while (0) + +// bx,by=texel offsets +static void deblock_block_region(int fbw, int fbh, const image& src_img, int bx, int by, image& dst_tile) +{ + assert(&src_img != &dst_tile); + assert((int)dst_tile.get_width() == (fbw + 2)); + assert((int)dst_tile.get_height() == (fbh + 2)); + + for (int ty = 0; ty < (fbh + 2); ty++) + { + const bool on_horiz_edge = (ty <= 1) || (ty >= fbh); + const int sy = by - 1 + ty; + + for (int tx = 0; tx < (fbw + 2); tx++) + { + const bool on_vert_edge = (tx <= 1) || (tx >= fbw); + const int sx = bx - 1 + tx; + + if (on_vert_edge && on_horiz_edge) + { + BASISU_CORNER(sx, sy, tx, ty); + continue; + } + + color_rgba out; + if (on_vert_edge) + out = filter_horiz(src_img, sx, sy); + else if (on_horiz_edge) + out = filter_vert(src_img, sx, sy); + else + out = src_img.get_clamped(sx, sy); + + dst_tile.set_clipped(tx, ty, out); + } + } +} + +static void deblock_block_region_interior(int fbw, int fbh, const image& src_img, int bx, int by, image& dst_tile, int dst_x, int dst_y) +{ + assert((bx >= 0) && (bx < (int)src_img.get_width())); + assert((by >= 0) && (by < (int)src_img.get_height())); + assert(fbw >= 3); + assert(fbh >= 3); + assert(&src_img != &dst_tile); + assert(src_img.get_width() == dst_tile.get_width()); + assert(src_img.get_height() == dst_tile.get_height()); + + const int x_left = bx; + const int x_right = bx + fbw - 1; + const int y_top = by; + const int y_bottom = by + fbh - 1; + + // --- Four corners ------------------------------------------------------- + + BASISU_CORNER(x_left, y_top, dst_x, dst_y); + BASISU_CORNER(x_right, y_top, dst_x + fbw - 1, dst_y); + BASISU_CORNER(x_left, y_bottom, dst_x, dst_y + fbh - 1); + BASISU_CORNER(x_right, y_bottom, dst_x + fbw - 1, dst_y + fbh - 1); + + // --- Top and Bottom edges: rows y_top and y_bottom, columns (x_left, x_right) exclusive. + for (int sy = y_top; sy <= y_bottom; sy += (fbh - 1)) + { + const int ty = dst_y + (sy - by); + for (int tx_offset = 1; tx_offset < fbw - 1; tx_offset++) + { + const int sx = bx + tx_offset; + const color_rgba u = src_img.get_clamped(sx, sy - 1); + const color_rgba c = src_img.get_clamped(sx, sy); + const color_rgba d = src_img.get_clamped(sx, sy + 1); + + color_rgba out; + out.r = (uint8_t)(((uint32_t)u.r + (uint32_t)c.r + (uint32_t)d.r + 1u) / 3u); + out.g = (uint8_t)(((uint32_t)u.g + (uint32_t)c.g + (uint32_t)d.g + 1u) / 3u); + out.b = (uint8_t)(((uint32_t)u.b + (uint32_t)c.b + (uint32_t)d.b + 1u) / 3u); + out.a = (uint8_t)(((uint32_t)u.a + (uint32_t)c.a + (uint32_t)d.a + 1u) / 3u); + + dst_tile.set_clipped(dst_x + tx_offset, ty, out); + } + } + + // --- Left and Right edges: columns x_left and x_right, rows (y_top, y_bottom) exclusive. + for (int sx = x_left; sx <= x_right; sx += (fbw - 1)) + { + const int tx = dst_x + (sx - bx); + for (int ty_offset = 1; ty_offset < fbh - 1; ty_offset++) + { + const int sy = by + ty_offset; + const color_rgba l = src_img.get_clamped(sx - 1, sy); + const color_rgba c = src_img.get_clamped(sx, sy); + const color_rgba r = src_img.get_clamped(sx + 1, sy); + + color_rgba out; + out.r = (uint8_t)(((uint32_t)l.r + (uint32_t)c.r + (uint32_t)r.r + 1u) / 3u); + out.g = (uint8_t)(((uint32_t)l.g + (uint32_t)c.g + (uint32_t)r.g + 1u) / 3u); + out.b = (uint8_t)(((uint32_t)l.b + (uint32_t)c.b + (uint32_t)r.b + 1u) / 3u); + out.a = (uint8_t)(((uint32_t)l.a + (uint32_t)c.a + (uint32_t)r.a + 1u) / 3u); + + dst_tile.set_clipped(tx, dst_y + ty_offset, out); + } + } + + // --- Interior: pass-through copy from source. + // Fast path: interior fully in-bounds -> memcpy each row. + const int interior_last_sx = bx + fbw - 2; + const int interior_last_sy = by + fbh - 2; + const bool interior_in_bounds = + (interior_last_sx < (int)src_img.get_width()) && + (interior_last_sy < (int)src_img.get_height()); + + if (interior_in_bounds) + { + const uint32_t bytes_per_row = (fbw - 2) * (uint32_t)sizeof(color_rgba); + const uint32_t src_pitch = src_img.get_pitch(); + const uint32_t dst_pitch = dst_tile.get_pitch(); + + for (int ty_offset = 1; ty_offset < (fbh - 1); ty_offset++) + { + const color_rgba* pSrc = src_img.get_ptr() + (bx + 1) + (by + ty_offset) * src_pitch; + color_rgba* pDst = dst_tile.get_ptr() + (dst_x + 1) + (dst_y + ty_offset) * dst_pitch; + memcpy(pDst, pSrc, bytes_per_row); + } + } + else + { + for (int ty_offset = 1; ty_offset < (fbh - 1); ty_offset++) + { + const int sy = by + ty_offset; + for (int tx_offset = 1; tx_offset < (fbw - 1); tx_offset++) + { + const int sx = bx + tx_offset; + dst_tile.set_clipped(dst_x + tx_offset, dst_y + ty_offset, src_img.get_clamped(sx, sy)); + } + } + } +} + +static void deblock_image(int fbw, int fbh, const image& src_img, image& dst_img) +{ + assert(&src_img != &dst_img); + dst_img.match_dimensions(src_img); + + const uint32_t num_blocks_x = src_img.get_block_width(fbw); + const uint32_t num_blocks_y = src_img.get_block_height(fbh); + + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + deblock_block_region_interior(fbw, fbh, + src_img, bx * (uint32_t)fbw, by * (uint32_t)fbh, + dst_img, bx * (uint32_t)fbw, by * (uint32_t)fbh); + } // bx + } // by +} + +#undef BASISU_CORNER + +static void code_block_weights( basist::astc_ldr_t::grid_weight_dct &gw_dct, float q, uint32_t plane_index, const astc_helpers::log_astc_block& log_blk, - const basist::astc_ldr_t::astc_block_grid_data* pGrid_data, - basisu::bitwise_coder& c, - basist::astc_ldr_t::dct_syms& syms) + basist::astc_ldr_t::dct_syms& syms, + basist::astc_ldr_t::fvec &dct_work) { assert(q > 0.0f); - + syms.clear(); const uint32_t grid_width = log_blk.m_grid_width, grid_height = log_blk.m_grid_height; @@ -103,12 +334,10 @@ void code_block_weights( const float span_len = gw_dct.get_max_span_len(log_blk, plane_index); float dct_weights[astc_helpers::MAX_BLOCK_PIXELS]; - - // TODO - temp alloc - basist::astc_ldr_t::fvec dct_work; + grid_dim_vals.m_dct.forward(orig_weights, dct_weights, dct_work); - const float level_scale = gw_dct.compute_level_scale(q, span_len, pGrid_data->m_weight_gamma, grid_width, grid_height, log_blk.m_weight_ise_range); + const float level_scale = gw_dct.compute_level_scale(q, span_len, grid_width, grid_height, log_blk.m_weight_ise_range); int dct_quant_tab[astc_helpers::MAX_BLOCK_PIXELS]; gw_dct.compute_quant_table(q, grid_width, grid_height, level_scale, dct_quant_tab); @@ -119,13 +348,11 @@ void code_block_weights( quant_state.init(q, gw_dct.m_block_width, gw_dct.m_block_height, level_scale); #endif - c.put_truncated_binary((int)scaled_mean_weight, (uint32_t)(64.0f * scaled_weight_coding_scale) + 1); - syms.m_dc_sym = (int)scaled_mean_weight; syms.m_num_dc_levels = (uint32_t)(64.0f * scaled_weight_coding_scale) + 1; assert(syms.m_num_dc_levels == gw_dct.get_num_weight_dc_levels(log_blk.m_weight_ise_range)); - int dct_coeffs[astc_helpers::MAX_BLOCK_PIXELS]; + int dct_coeffs[astc_helpers::MAX_BLOCK_PIXELS]; // TODO: max grid size is 8x8 for (uint32_t y = 0; y < grid_height; y++) { @@ -156,6 +383,8 @@ void code_block_weights( const basisu::int_vec& zigzag = grid_dim_vals.m_zigzag; assert(zigzag.size() == total_grid_samples); + + syms.m_coeffs.reserve(65); int total_zeros = 0; for (uint32_t i = 0; i < total_grid_samples; i++) @@ -174,19 +403,20 @@ void code_block_weights( basist::astc_ldr_t::dct_syms::coeff cf; cf.m_num_zeros = basisu::safe_cast_uint16(total_zeros); cf.m_coeff = basisu::safe_cast_int16(coeff); + syms.m_coeffs.push_back(cf); + syms.m_max_coeff_mag = basisu::maximum(syms.m_max_coeff_mag, basisu::iabs(coeff)); syms.m_max_zigzag_index = basisu::maximum(syms.m_max_zigzag_index, i); - c.put_rice(total_zeros, gw_dct.m_zero_run); total_zeros = 0; - - c.put_bits(coeff < 0 ? 1 : 0, 1); - - if (coeff < 0) - coeff = -coeff; - - c.put_rice(coeff, gw_dct.m_coeff); + } + + { + // reduce allocation + basisu::vector temp_coeffs(syms.m_coeffs); + + syms.m_coeffs.swap(temp_coeffs); } if (total_zeros) @@ -195,12 +425,10 @@ void code_block_weights( cf.m_num_zeros = basisu::safe_cast_uint16(total_zeros); cf.m_coeff = INT16_MAX; syms.m_coeffs.push_back(cf); - - c.put_rice(total_zeros, gw_dct.m_zero_run); } } -void astc_ldr_requantize_astc_weights(uint32_t n, const uint8_t* pSrc_ise_vals, uint32_t from_ise_range, uint8_t* pDst_ise_vals, uint32_t to_ise_range) +static void astc_ldr_requantize_astc_weights(uint32_t n, const uint8_t* pSrc_ise_vals, uint32_t from_ise_range, uint8_t* pDst_ise_vals, uint32_t to_ise_range) { if (from_ise_range == to_ise_range) { @@ -237,7 +465,7 @@ void astc_ldr_requantize_astc_weights(uint32_t n, const uint8_t* pSrc_ise_vals, } } -void astc_ldr_downsample_ise_weights( +static void astc_ldr_downsample_ise_weights( uint32_t dequant_weight_ise_range, uint32_t quant_weight_ise_range, uint32_t block_w, uint32_t block_h, uint32_t grid_w, uint32_t grid_h, @@ -340,7 +568,8 @@ void downsample_weight_residual_grid( } } -void downsample_weightsf( +#if 0 +static void downsample_weightsf( const float* pMatrix_weights, uint32_t bx, uint32_t by, // source/from dimension (block size) uint32_t wx, uint32_t wy, // dest/to dimension (grid size) @@ -365,9 +594,11 @@ void downsample_weightsf( } } } +#endif static inline uint32_t weighted_color_error(const color_rgba& a, const color_rgba& b, const astc_ldr::cem_encode_params& p) { +#if 0 uint32_t total_e = 0; for (uint32_t c = 0; c < 4; c++) { @@ -376,11 +607,19 @@ static inline uint32_t weighted_color_error(const color_rgba& a, const color_rgb int ev = av - bv; total_e += (uint32_t)(ev * ev) * p.m_comp_weights[c]; } - + return total_e; +#else + const uint32_t total_e2 = squarei(a[0] - b[0]) * p.m_comp_weights[0] + + squarei(a[1] - b[1]) * p.m_comp_weights[1] + + squarei(a[2] - b[2]) * p.m_comp_weights[2] + + squarei(a[3] - b[3]) * p.m_comp_weights[3]; + + return total_e2; +#endif } -uint64_t eval_error( +static uint64_t eval_error( uint32_t block_width, uint32_t block_height, const astc_helpers::log_astc_block& enc_log_block, const astc_ldr::pixel_stats_t& pixel_stats, @@ -423,7 +662,7 @@ uint64_t eval_error( return total_err; } -uint64_t eval_error( +static uint64_t eval_error( uint32_t block_width, uint32_t block_height, const astc_ldr::pixel_stats_t& pixel_stats, uint32_t cem_index, @@ -433,7 +672,7 @@ uint64_t eval_error( const uint8_t* pEndpoint_vals, const uint8_t* pWeight_grid_vals0, const uint8_t* pWeight_grid_vals1, const astc_ldr::cem_encode_params& params) { - const uint32_t total_block_pixels = block_width * block_height; + //const uint32_t total_block_pixels = block_width * block_height; const uint32_t total_grid_pixels = grid_width * grid_height; astc_helpers::log_astc_block enc_log_block; @@ -468,22 +707,10 @@ uint64_t eval_error( memcpy(enc_log_block.m_weights, pWeight_grid_vals0, total_grid_pixels); } - color_rgba decoded_pixels[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; - bool status = astc_helpers::decode_block(enc_log_block, decoded_pixels, block_width, block_height, params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); - assert(status); - - if (!status) - return UINT64_MAX; - - uint64_t total_err = 0; - - for (uint32_t i = 0; i < total_block_pixels; i++) - total_err += weighted_color_error(pixel_stats.m_pixels[i], decoded_pixels[i], params); - - return total_err; + return eval_error(block_width, block_height, enc_log_block, pixel_stats, params); } -float compute_psnr_from_wsse(uint32_t block_width, uint32_t block_height, uint64_t sse, float total_comp_weights) +static float compute_psnr_from_wsse(uint32_t block_width, uint32_t block_height, uint64_t sse, float total_comp_weights) { const uint32_t total_block_pixels = block_width * block_height; const float wmse = (float)sse / (total_comp_weights * (float)total_block_pixels); @@ -695,8 +922,260 @@ namespace qcd } // namespace qcd +const uint32_t NUM_WEIGHT_POLISH_PASSES = 1; + +#if 0 +// true if improved +static bool polish_block_weights_final_slow( + astc_helpers::log_astc_block& enc_log_block, // assumes there is already a good encoding to improve here + uint8_t* pWeights0, uint8_t* pWeights1, // the latest weights, will be updated if improved + uint32_t block_width, uint32_t block_height, uint32_t grid_width, uint32_t grid_height, + const astc_ldr::pixel_stats_t& pixel_stats, + const astc_ldr::cem_encode_params& params, + uint64_t &cur_err) +{ + const bool dual_plane_flag = enc_log_block.m_dual_plane; + + //const uint32_t endpoint_ise_range = enc_log_block.m_endpoint_ise_range; + const uint32_t weight_ise_range = enc_log_block.m_weight_ise_range; + + //const uint32_t total_block_pixels = block_width * block_height; + const uint32_t total_grid_pixels = grid_width * grid_height; + + bool improved_flag = false; + + for (uint32_t polish_pass = 0; polish_pass < NUM_WEIGHT_POLISH_PASSES; polish_pass++) + { + for (uint32_t y = 0; y < grid_height; y++) + { + for (uint32_t x = 0; x < grid_width; x++) + { + for (uint32_t plane_iter = 0; plane_iter < (dual_plane_flag ? 2u : 1u); plane_iter++) + { + uint8_t base_grid_weights0[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS], base_grid_weights1[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + + memcpy(base_grid_weights0, pWeights0, total_grid_pixels); + if (dual_plane_flag) + memcpy(base_grid_weights1, pWeights1, total_grid_pixels); + + for (int delta = -1; delta <= 1; delta += 2) + { + uint8_t trial_grid_weights0[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS], trial_grid_weights1[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + + memcpy(trial_grid_weights0, base_grid_weights0, total_grid_pixels); + + if (dual_plane_flag) + memcpy(trial_grid_weights1, base_grid_weights1, total_grid_pixels); + + if (plane_iter == 0) + trial_grid_weights0[x + y * grid_width] = (uint8_t)astc_ldr::apply_delta_to_bise_weight_val(weight_ise_range, base_grid_weights0[x + y * grid_width], delta); + else + trial_grid_weights1[x + y * grid_width] = (uint8_t)astc_ldr::apply_delta_to_bise_weight_val(weight_ise_range, base_grid_weights1[x + y * grid_width], delta); + + astc_helpers::log_astc_block trial_log_block(enc_log_block); + + astc_helpers::set_weights(trial_log_block, trial_grid_weights0, 0); + + if (dual_plane_flag) + astc_helpers::set_weights(trial_log_block, trial_grid_weights1, 1); + + uint64_t trial_err = eval_error(block_width, block_height, trial_log_block, pixel_stats, params); + + if (trial_err < cur_err) + { + cur_err = trial_err; + + memcpy(pWeights0, trial_grid_weights0, total_grid_pixels); + + if (dual_plane_flag) + memcpy(pWeights1, trial_grid_weights1, total_grid_pixels); + + improved_flag = true; + } + + } // delta + + } // plane_iter + + } // x + } // y + + } // polish_pass + + return improved_flag; +} +#endif + +#define BASISU_POLISH_DEBUG (0) + +// true if improved +static bool polish_block_weights_final_fast( + astc_helpers::log_astc_block& enc_log_block, // assumes there is already a good encoding to improve here + uint8_t* pWeights0, uint8_t* pWeights1, // the latest weights, will be updated if improved + uint32_t block_width, uint32_t block_height, uint32_t grid_width, uint32_t grid_height, + const astc_ldr::pixel_stats_t& pixel_stats, + const astc_ldr::cem_encode_params& params, + const basist::astc_ldr_t::astc_block_grid_data* pGrid_data, + uint64_t& cur_err) +{ + if (!cur_err) + return false; + + const bool dual_plane_flag = enc_log_block.m_dual_plane; + + //const uint32_t endpoint_ise_range = enc_log_block.m_endpoint_ise_range; + const uint32_t weight_ise_range = enc_log_block.m_weight_ise_range; + + const uint32_t total_block_pixels = block_width * block_height; + BASISU_NOTE_UNUSED(total_block_pixels); + //const uint32_t total_grid_pixels = grid_width * grid_height; + + bool improved_flag = false; + + astc_helpers::log_astc_block cur_log_block(enc_log_block); + + astc_helpers::set_weights(cur_log_block, pWeights0, 0); + if (dual_plane_flag) + astc_helpers::set_weights(cur_log_block, pWeights1, 1); + + astc_helpers::xuastc_ldr_block_decoder block_decoder( + cur_log_block, block_width, block_height, + params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8, + pGrid_data->m_upsample_weights.get_ptr()); + + for (uint32_t polish_pass = 0; polish_pass < NUM_WEIGHT_POLISH_PASSES; polish_pass++) + { + for (uint32_t y = 0; y < grid_height; y++) + { + for (uint32_t x = 0; x < grid_width; x++) + { + const basisu::uint16_vec& influenced_texels = pGrid_data->m_grid_to_texel_influence_list[x + y * grid_width]; + + for (uint32_t plane_iter = 0; plane_iter < (dual_plane_flag ? 2u : 1u); plane_iter++) + { + +#if BASISU_POLISH_DEBUG +#if defined(_DEBUG) || defined(DEBUG) + assert(cur_err == eval_error(block_width, block_height, cur_log_block, pixel_stats, params)); + + { + color_rgba alt_block_pixels[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + bool status = astc_helpers::decode_block_xuastc_ldr(cur_log_block, alt_block_pixels, block_width, block_height, params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); + assert(status); + + uint64_t alt_err = 0; + for (uint32_t k = 0; k < total_block_pixels; k++) + { + const uint32_t texel_x = k % block_width; + const uint32_t texel_y = k / block_width; + + color_rgba dec_c; + block_decoder.decode_texel(texel_x, texel_y, (astc_helpers::color_rgba&)dec_c); + + if ((dec_c.r != alt_block_pixels[k].r) || + (dec_c.g != alt_block_pixels[k].g) || + (dec_c.b != alt_block_pixels[k].b) || + (dec_c.a != alt_block_pixels[k].a)) + { + assert(0); + } + + uint64_t w = weighted_color_error(dec_c, pixel_stats.m_pixels[k], params); + alt_err += w; + } + assert(alt_err == cur_err); + } +#endif +#endif + + const uint8_t base_weight = astc_helpers::get_weight(cur_log_block, plane_iter, x + y * grid_width); + + for (int delta = -1; delta <= 1; delta += 2) + { + const uint8_t new_weight = (uint8_t)astc_ldr::apply_delta_to_bise_weight_val(weight_ise_range, base_weight, delta); + if (new_weight == base_weight) + continue; + + // remove out influence from the current weight + uint64_t trial_err = cur_err; + 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; + + color_rgba dec_c; + block_decoder.decode_texel(texel_x, texel_y, (astc_helpers::color_rgba &)dec_c); + + uint64_t w = weighted_color_error(dec_c, pixel_stats.m_pixels[texel_index], params); + assert(trial_err >= w); + trial_err -= w; + } + + // save weight in case it's worse + const uint8_t saved_weight = astc_helpers::get_weight(cur_log_block, plane_iter, x + y * grid_width); + + // change current weight + astc_helpers::get_weight(cur_log_block, plane_iter, x + y * grid_width) = new_weight; + + // now add in influence from the new weight + 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; + + color_rgba dec_c; + block_decoder.decode_texel(texel_x, texel_y, (astc_helpers::color_rgba&)dec_c); + + trial_err += weighted_color_error(dec_c, pixel_stats.m_pixels[texel_index], params); + } + +#if BASISU_POLISH_DEBUG + assert(trial_err == eval_error(block_width, block_height, cur_log_block, pixel_stats, params)); +#endif + + // see if we've improved the solution by this one weight change + if (trial_err < cur_err) + { + cur_err = trial_err; + + improved_flag = true; + } + else + { + // candidate was worse, so restore the weight + astc_helpers::get_weight(cur_log_block, plane_iter, x + y * grid_width) = saved_weight; + } + + } // delta + +#if BASISU_POLISH_DEBUG + assert(cur_err == eval_error(block_width, block_height, cur_log_block, pixel_stats, params)); +#endif + + } // plane_iter + + } // x + } // y + + } // polish_pass + + if (improved_flag) + { + astc_helpers::extract_weights(cur_log_block, pWeights0, 0); + + if (dual_plane_flag) + astc_helpers::extract_weights(cur_log_block, pWeights1, 1); + } + + return improved_flag; +} + // 1-3 subsets, requires initial weights -bool polish_block_weights( +static bool polish_block_weights( uint32_t block_width, uint32_t block_height, const astc_ldr::pixel_stats_t& pixel_stats, astc_helpers::log_astc_block& enc_log_block, // assumes there is already a good encoding to improve here @@ -723,6 +1202,8 @@ bool polish_block_weights( const auto& dequant_tab = astc_helpers::g_dequant_tables.get_weight_tab(weight_ise_range).m_ISE_to_val; const auto& quant_tab = astc_helpers::g_dequant_tables.get_weight_tab(weight_ise_range).m_val_to_ise; + const basist::astc_ldr_t::astc_block_grid_data *pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, grid_width, grid_height); + //const bool is_downsampling = (grid_width < block_width) || (grid_height < block_height); #if defined(_DEBUG) || defined(DEBUG) @@ -734,9 +1215,7 @@ bool polish_block_weights( } } #endif - - //const astc_block_grid_data* pBlock_grid_data = find_astc_block_grid_data(block_width, block_height, grid_width, grid_height); - + const uint32_t total_block_pixels = block_width * block_height; const uint32_t total_grid_pixels = grid_width * grid_height; @@ -753,16 +1232,12 @@ bool polish_block_weights( const bool global_gradient_desc_enabled = true; const bool global_qcd_enabled = true; const bool global_polish_weights_enabled = true; - - const uint32_t NUM_WEIGHT_POLISH_PASSES = 1; - + // Gradient descent if ((gradient_descent_flag) && (global_gradient_desc_enabled)) { // Downsample the residuals to grid res - vector2D upsample_matrix; - compute_upsample_matrix(upsample_matrix, block_width, block_height, grid_width, grid_height); - + // First compute the block's ideal raw weights given the current endpoints at full block/texel res // TODO: Move to helper uint8_t ideal_block_raw_weights0[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS], ideal_block_raw_weights1[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; @@ -869,13 +1344,8 @@ bool polish_block_weights( float weight_grid_residuals_downsampled0[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS], weight_grid_residuals_downsampled1[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; - basisu::vector unweighted_downsample_matrix; - - // TODO: precompute, store in weight grid data - compute_upsample_matrix_transposed(unweighted_downsample_matrix, block_width, block_height, grid_width, grid_height); - - basisu::vector diag_AtA(total_grid_pixels); - compute_diag_AtA_vector(block_width, block_height, grid_width, grid_height, upsample_matrix, diag_AtA.get_ptr()); + const basisu::vector& unweighted_downsample_matrix = pGrid_data->m_unweighted_downsample_matrix; + const basisu::vector& one_over_diag_AtA = pGrid_data->m_one_over_diag_AtA; downsample_weight_residual_grid( unweighted_downsample_matrix.get_ptr(), @@ -885,7 +1355,7 @@ bool polish_block_weights( weight_grid_residuals_downsampled0); // [wy][wx] for (uint32_t i = 0; i < total_grid_pixels; i++) - weight_grid_residuals_downsampled0[i] /= diag_AtA[i]; + weight_grid_residuals_downsampled0[i] *= one_over_diag_AtA[i]; if (dual_plane_flag) { @@ -897,7 +1367,7 @@ bool polish_block_weights( weight_grid_residuals_downsampled1); // [wy][wx] for (uint32_t i = 0; i < total_grid_pixels; i++) - weight_grid_residuals_downsampled1[i] /= diag_AtA[i]; + weight_grid_residuals_downsampled1[i] *= one_over_diag_AtA[i]; } // Apply the residuals at grid res and quantize @@ -936,7 +1406,7 @@ bool polish_block_weights( astc_helpers::log_astc_block refined_log_block(enc_log_block); - // TODO: This refines both weight planes simultanously, probably not optimal, could do individually. + // TODO: This refines both weight planes simultaneously, probably not optimal, could do individually. astc_helpers::set_weights(refined_log_block, refined_grid_weights0, 0); if (dual_plane_flag) @@ -968,7 +1438,8 @@ bool polish_block_weights( ideal_block_weights1[i] = (float)ideal_block_raw_weights1[i]; } - const float* pUpsample_matrix = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, grid_width, grid_height)->m_upsample_matrix.get_ptr(); + //const float* pUpsample_matrix = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, grid_width, grid_height)->m_upsample_matrix.get_ptr(); + const float* pUpsample_matrix = pGrid_data->m_upsample_matrix.get_ptr(); qcd::qcd_min_solver solver; @@ -1053,113 +1524,112 @@ bool polish_block_weights( if ((polish_weights_flag) && (global_polish_weights_enabled)) { - // Final, expensive, weight polish. Much can be done to improve this, but it's hopefully not ran much in the first place. - // TODO: The dB gain from this is large, must optimize. - for (uint32_t polish_pass = 0; polish_pass < NUM_WEIGHT_POLISH_PASSES; polish_pass++) +#if BASISU_POLISH_DEBUG + uint64_t cur_err_fast = cur_err; + + uint8_t weights0_fast[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + uint8_t weights1_fast[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + + memcpy(weights0_fast, weights0, total_grid_pixels); + if (dual_plane_flag) + memcpy(weights1_fast, weights1, total_grid_pixels); + + bool improved_flag_fast = polish_block_weights_final_fast( + enc_log_block, + weights0_fast, weights1_fast, + block_width, block_height, grid_width, grid_height, + pixel_stats, + params, + pGrid_data, + cur_err_fast); + + if (polish_block_weights_final_slow( + enc_log_block, + weights0, weights1, // the latest weights, will be updated if improved + block_width, block_height, grid_width, grid_height, + pixel_stats, + params, + cur_err)) { - for (uint32_t y = 0; y < grid_height; y++) + assert(improved_flag_fast); + + assert(cur_err == cur_err_fast); + + assert(memcmp(weights0, weights0_fast, total_grid_pixels) == 0); + if (dual_plane_flag) { - for (uint32_t x = 0; x < grid_width; x++) - { - for (uint32_t plane_iter = 0; plane_iter < (dual_plane_flag ? 2u : 1u); plane_iter++) - { - uint8_t base_grid_weights0[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS], base_grid_weights1[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + assert(memcmp(weights1, weights1_fast, total_grid_pixels) == 0); + } - memcpy(base_grid_weights0, weights0, total_grid_pixels); - if (dual_plane_flag) - memcpy(base_grid_weights1, weights1, total_grid_pixels); - - for (int delta = -1; delta <= 1; delta += 2) - { - uint8_t trial_grid_weights0[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS], trial_grid_weights1[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; - - memcpy(trial_grid_weights0, base_grid_weights0, total_grid_pixels); - - if (dual_plane_flag) - memcpy(trial_grid_weights1, base_grid_weights1, total_grid_pixels); - - if (plane_iter == 0) - trial_grid_weights0[x + y * grid_width] = (uint8_t)astc_ldr::apply_delta_to_bise_weight_val(weight_ise_range, base_grid_weights0[x + y * grid_width], delta); - else - trial_grid_weights1[x + y * grid_width] = (uint8_t)astc_ldr::apply_delta_to_bise_weight_val(weight_ise_range, base_grid_weights1[x + y * grid_width], delta); - - astc_helpers::log_astc_block trial_log_block(enc_log_block); - - astc_helpers::set_weights(trial_log_block, trial_grid_weights0, 0); - - if (dual_plane_flag) - astc_helpers::set_weights(trial_log_block, trial_grid_weights1, 1); - - uint64_t trial_err = eval_error(block_width, block_height, trial_log_block, pixel_stats, params); - - if (trial_err < cur_err) - { - cur_err = trial_err; - - memcpy(weights0, trial_grid_weights0, total_grid_pixels); - - if (dual_plane_flag) - memcpy(weights1, trial_grid_weights1, total_grid_pixels); - - improved_flag = true; - } - - } // delta - - } // plane_iter - - } // x - } // y - - } // polish_pass + improved_flag = true; + } + else + { + assert(!improved_flag_fast); + } +#else + if (polish_block_weights_final_fast( + enc_log_block, + weights0, weights1, // the latest weights, will be updated if improved + block_width, block_height, grid_width, grid_height, + pixel_stats, + params, + pGrid_data, + cur_err)) + { + improved_flag = true; + } +#endif } // polish_flag - astc_helpers::log_astc_block new_log_block(enc_log_block); - - astc_helpers::set_weights(new_log_block, weights0, 0); - - if (dual_plane_flag) - astc_helpers::set_weights(new_log_block, weights1, 1); - #if defined(_DEBUG) || defined(DEBUG) - uint64_t new_err = eval_error(block_width, block_height, new_log_block, pixel_stats, params); - - assert(cur_err == new_err); - + // sanity checking if (improved_flag) { uint64_t orig_err = eval_error(block_width, block_height, enc_log_block, pixel_stats, params); - - assert(new_err < orig_err); + assert(cur_err < orig_err); } #endif - enc_log_block = new_log_block; + if (improved_flag) + { + astc_helpers::set_weights(enc_log_block, weights0, 0); + + if (dual_plane_flag) + astc_helpers::set_weights(enc_log_block, weights1, 1); + } + +#if defined(_DEBUG) || defined(DEBUG) + // sanity checking + uint64_t new_err = eval_error(block_width, block_height, enc_log_block, pixel_stats, params); + assert(cur_err == new_err); +#endif return true; } -bool encode_trial_subsets( +static bool encode_trial_subsets( uint32_t block_width, uint32_t block_height, const astc_ldr::pixel_stats_t& pixel_stats, uint32_t cem_index, uint32_t num_parts, uint32_t pat_seed_index, const astc_ldr::partition_pattern_vec* pPat, // seed index is a ASTC partition pattern index uint32_t endpoint_ise_range, uint32_t weight_ise_range, uint32_t grid_width, uint32_t grid_height, + float early_out_thresh, astc_helpers::log_astc_block& enc_log_block, const astc_ldr::cem_encode_params& params, bool refine_only_flag = false, bool gradient_descent_flag = true, bool polish_weights_flag = true, bool qcd_enabled_flag = true, bool use_blue_contraction = true, - bool* pBase_ofs_clamped_flag = nullptr) + bool* pTry_direct_encoding_flag = nullptr) { assert((num_parts >= 2) && (num_parts <= astc_helpers::MAX_PARTITIONS)); assert(pPat); assert(pat_seed_index < astc_helpers::NUM_PARTITION_PATTERNS); - if (pBase_ofs_clamped_flag) - *pBase_ofs_clamped_flag = false; + if (pTry_direct_encoding_flag) + *pTry_direct_encoding_flag = false; const bool is_downsampling = (grid_width < block_width) || (grid_height < block_height); //const uint32_t total_block_pixels = block_width * block_height; @@ -1201,18 +1671,18 @@ bool encode_trial_subsets( if (!refine_only_flag) { - bool base_ofs_clamped_flag = false; + bool try_direct_encoding_flag = false; // Encode at block res, but with quantized weights uint64_t block_err = astc_ldr::cem_encode_pixels(cem_index, -1, part_pixel_stats[part_index], params, endpoint_ise_range, weight_ise_range, - &part_endpoints[part_index][0], &part_weights[part_index][0], nullptr, UINT64_MAX, use_blue_contraction, &base_ofs_clamped_flag); + &part_endpoints[part_index][0], &part_weights[part_index][0], nullptr, UINT64_MAX, use_blue_contraction, &try_direct_encoding_flag); if (block_err == UINT64_MAX) return false; - if ((pBase_ofs_clamped_flag) && (base_ofs_clamped_flag)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; } } // part_index @@ -1271,11 +1741,28 @@ bool encode_trial_subsets( memcpy(enc_log_block.m_endpoints + num_endpoint_vals * p, &part_endpoints[p][0], num_endpoint_vals); } + uint64_t prev_cur_err = UINT64_MAX; + // attempt endpoint refinement given the current weights // TODO: Expose to caller const uint32_t NUM_REFINEMENT_PASSES = 3; for (uint32_t refine_pass = 0; refine_pass < NUM_REFINEMENT_PASSES; refine_pass++) { + uint64_t cur_err = eval_error(block_width, block_height, enc_log_block, pixel_stats, params); + + if (!cur_err) + break; + + if ((early_out_thresh != 0.0f) && (refine_pass) && (prev_cur_err)) + { + double percentage_improvement = (double)(prev_cur_err - cur_err) / (double)prev_cur_err; + + if (percentage_improvement < early_out_thresh) + break; + } + + prev_cur_err = cur_err; + uint8_t dequantized_raw_weights0[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; uint8_t upsampled_weights0[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; // raw weights, NOT ISE @@ -1311,18 +1798,18 @@ bool encode_trial_subsets( uint8_t temp_weights[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; - bool base_ofs_clamped_flag = false; + bool try_direct_encoding_flag = false; // Encode at block res, but with quantized weights uint64_t block_err = astc_ldr::cem_encode_pixels(cem_index, -1, part_pixel_stats[part_index], temp_params, endpoint_ise_range, astc_helpers::BISE_64_LEVELS, - &alt_enc_log_block.m_endpoints[num_endpoint_vals * part_index], temp_weights, nullptr, UINT64_MAX, use_blue_contraction, &base_ofs_clamped_flag); + &alt_enc_log_block.m_endpoints[num_endpoint_vals * part_index], temp_weights, nullptr, UINT64_MAX, use_blue_contraction, &try_direct_encoding_flag); if (block_err == UINT64_MAX) return false; - if ((pBase_ofs_clamped_flag) && (base_ofs_clamped_flag)) - *pBase_ofs_clamped_flag = true; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = true; #if defined(_DEBUG) || defined(DEBUG) for (uint32_t i = 0; i < part_pixel_stats[part_index].m_num_pixels; i++) @@ -1332,8 +1819,7 @@ bool encode_trial_subsets( #endif } // part_index - - uint64_t cur_err = eval_error(block_width, block_height, enc_log_block, pixel_stats, params); + uint64_t ref_err = eval_error(block_width, block_height, alt_enc_log_block, pixel_stats, params); if (ref_err < cur_err) @@ -1365,31 +1851,33 @@ bool encode_trial_subsets( return true; } -bool encode_trial( +static bool encode_trial( uint32_t block_width, uint32_t block_height, const astc_ldr::pixel_stats_t& pixel_stats, uint32_t cem_index, bool dual_plane_flag, int ccs_index, uint32_t endpoint_ise_range, uint32_t weight_ise_range, uint32_t grid_width, uint32_t grid_height, + float early_out_thresh, astc_helpers::log_astc_block& enc_log_block, const astc_ldr::cem_encode_params& params, bool gradient_descent_flag = true, bool polish_weights_flag = true, bool qcd_enabled_flag = true, bool use_blue_contraction = true, - bool* pBase_ofs_clamped_flag = nullptr) + bool* pTry_direct_encoding_flag = nullptr) { assert(dual_plane_flag || (ccs_index == -1)); - if (pBase_ofs_clamped_flag) - *pBase_ofs_clamped_flag = false; + if (pTry_direct_encoding_flag) + *pTry_direct_encoding_flag = false; const bool is_downsampling = (grid_width < block_width) || (grid_height < block_height); - - const basist::astc_ldr_t::astc_block_grid_data* pBlock_grid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, grid_width, grid_height); - + const float* pDownsample_matrix = nullptr; if (is_downsampling) + { + const basist::astc_ldr_t::astc_block_grid_data* pBlock_grid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, grid_width, grid_height); pDownsample_matrix = pBlock_grid_data->m_downsample_matrix.get_ptr(); + } //const uint32_t total_block_pixels = block_width * block_height; const uint32_t total_grid_pixels = grid_width * grid_height; @@ -1424,17 +1912,18 @@ bool encode_trial( if ((grid_width == block_width) && (grid_height == block_height)) { - bool base_ofs_clamped_flag = false; + // No downsampling: a lot easier. + bool try_direct_encoding_flag = false; uint64_t block_err = astc_ldr::cem_encode_pixels(cem_index, ccs_index, pixel_stats, params, endpoint_ise_range, weight_ise_range, - fullres_endpoints, weights0, weights1, UINT64_MAX, use_blue_contraction, &base_ofs_clamped_flag); + fullres_endpoints, weights0, weights1, UINT64_MAX, use_blue_contraction, &try_direct_encoding_flag); if (block_err == UINT64_MAX) return false; - if ((pBase_ofs_clamped_flag) && (base_ofs_clamped_flag)) - *pBase_ofs_clamped_flag = base_ofs_clamped_flag; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = try_direct_encoding_flag; if (dual_plane_flag) { @@ -1459,18 +1948,18 @@ bool encode_trial( uint8_t fullres_raw_weights0[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; uint8_t fullres_raw_weights1[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; - bool base_ofs_clamped_flag = false; + bool try_direct_encoding_flag = false; // Encode at block res, but with quantized weights uint64_t block_err = astc_ldr::cem_encode_pixels(cem_index, ccs_index, pixel_stats, params, endpoint_ise_range, weight_ise_range, - fullres_endpoints, fullres_raw_weights0, fullres_raw_weights1, UINT64_MAX, use_blue_contraction, &base_ofs_clamped_flag); + fullres_endpoints, fullres_raw_weights0, fullres_raw_weights1, UINT64_MAX, use_blue_contraction, &try_direct_encoding_flag); if (block_err == UINT64_MAX) return false; - if ((pBase_ofs_clamped_flag) && (base_ofs_clamped_flag)) - *pBase_ofs_clamped_flag = base_ofs_clamped_flag; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = try_direct_encoding_flag; // Now downsample the weight grid (quantized to quantized) astc_ldr_downsample_ise_weights( @@ -1497,10 +1986,34 @@ bool encode_trial( memcpy(enc_log_block.m_endpoints, fullres_endpoints, astc_helpers::get_num_cem_values(cem_index)); - // TODO: Expose to caller + uint64_t prev_cur_err = UINT64_MAX; + const uint32_t NUM_OUTER_PASSES = 3; for (uint32_t outer_pass = 0; outer_pass < NUM_OUTER_PASSES; outer_pass++) { + uint64_t cur_err = eval_error( + block_width, block_height, + pixel_stats, + cem_index, + dual_plane_flag, ccs_index, + endpoint_ise_range, weight_ise_range, + grid_width, grid_height, + enc_log_block.m_endpoints, weights0, weights1, + params); + + if (!cur_err) + break; + + if ((early_out_thresh != 0.0f) && (outer_pass) && (prev_cur_err)) + { + double percentage_improvement = (double)(prev_cur_err - cur_err) / (double)prev_cur_err; + + if (percentage_improvement < early_out_thresh) + break; + } + + prev_cur_err = cur_err; + // endpoint refinement, given current upsampled weights { astc_helpers::extract_weights(enc_log_block, weights0, 0); @@ -1528,7 +2041,7 @@ bool encode_trial( astc_helpers::upsample_weight_grid(block_width, block_height, grid_width, grid_height, dequantized_raw_weights1, upsampled_weights1); } - // Jam in the weights to the actual raw [0,64] weights the decoder is going to use after upsampling the grid. + // Jam in the actual raw [0,64] weights the decoder is going to use after upsampling the grid. astc_ldr::cem_encode_params refine_params(params); refine_params.m_pForced_weight_vals0 = upsampled_weights0; if (dual_plane_flag) @@ -1540,35 +2053,22 @@ bool encode_trial( uint64_t refined_block_err = astc_ldr::cem_encode_pixels(cem_index, ccs_index, pixel_stats, refine_params, endpoint_ise_range, astc_helpers::BISE_64_LEVELS, - refined_endpoints, refined_weights0, refined_weights1, UINT64_MAX, use_blue_contraction, &base_ofs_clamped_flag); - assert(refined_block_err != UINT64_MAX); - - if ((pBase_ofs_clamped_flag) && (base_ofs_clamped_flag)) - *pBase_ofs_clamped_flag = base_ofs_clamped_flag; - + refined_endpoints, refined_weights0, refined_weights1, UINT64_MAX, use_blue_contraction, &try_direct_encoding_flag); + if (refined_block_err != UINT64_MAX) { - uint64_t cur_err = eval_error( - block_width, block_height, - pixel_stats, - cem_index, - dual_plane_flag, ccs_index, - endpoint_ise_range, weight_ise_range, - grid_width, grid_height, - enc_log_block.m_endpoints, weights0, weights1, - params); - if (refined_block_err < cur_err) { memcpy(enc_log_block.m_endpoints, refined_endpoints, astc_helpers::get_num_cem_values(cem_index)); + + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = try_direct_encoding_flag; } } } - if (outer_pass == (NUM_OUTER_PASSES - 1)) - break; - - if ((!gradient_descent_flag) && (!polish_weights_flag)) + if ( (outer_pass == (NUM_OUTER_PASSES - 1)) || + ((!gradient_descent_flag) && (!polish_weights_flag)) ) break; bool improved_flag = false; @@ -1598,19 +2098,19 @@ bool encode_trial( return true; } -// 1 part only, refines endpoints given current weights -bool encode_trial_refine_only( +// 1 subset only, refines endpoints given current weights +static bool encode_trial_refine_only( uint32_t block_width, uint32_t block_height, const astc_ldr::pixel_stats_t& pixel_stats, astc_helpers::log_astc_block& enc_log_block, const astc_ldr::cem_encode_params& params, bool use_blue_contraction = true, - bool* pBase_ofs_clamped_flag = nullptr) + bool* pTry_direct_encoding_flag = nullptr) { assert(enc_log_block.m_num_partitions == 1); - if (pBase_ofs_clamped_flag) - *pBase_ofs_clamped_flag = false; + if (pTry_direct_encoding_flag) + *pTry_direct_encoding_flag = false; const uint32_t cem_index = enc_log_block.m_color_endpoint_modes[0]; const bool dual_plane_flag = enc_log_block.m_dual_plane; @@ -1668,15 +2168,15 @@ bool encode_trial_refine_only( //bool use_blue_contraction = true; - bool base_ofs_clamped_flag = false; + bool try_direct_encoding_flag = false; uint64_t refined_block_err = astc_ldr::cem_encode_pixels(cem_index, ccs_index, pixel_stats, refine_params, endpoint_ise_range, astc_helpers::BISE_64_LEVELS, - refined_endpoints, refined_weights0, refined_weights1, UINT64_MAX, use_blue_contraction, &base_ofs_clamped_flag); + refined_endpoints, refined_weights0, refined_weights1, UINT64_MAX, use_blue_contraction, &try_direct_encoding_flag); assert(refined_block_err != UINT64_MAX); - if ((pBase_ofs_clamped_flag) && (base_ofs_clamped_flag)) - *pBase_ofs_clamped_flag = base_ofs_clamped_flag; + if ((pTry_direct_encoding_flag) && (try_direct_encoding_flag)) + *pTry_direct_encoding_flag = try_direct_encoding_flag; #if defined(_DEBUG) || defined(DEBUG) for (uint32_t i = 0; i < total_grid_pixels; i++) @@ -1716,6 +2216,7 @@ bool encode_trial_refine_only( struct log_surrogate_astc_blk { + // Important: If not downsampling, grid width/height may match the block width/height and may not be valid ASTC (which has a limit of 64 weight samples). int m_grid_width, m_grid_height; uint32_t m_cem_index; // base+scale or direct variants only @@ -1742,7 +2243,7 @@ struct log_surrogate_astc_blk void decode(uint32_t block_width, uint32_t block_height, vec4F* pPixels, const astc_ldr::partitions_data* pPat_data) const; }; -void upsample_surrogate_weights( +static void upsample_surrogate_weights( const astc_helpers::weighted_sample* pWeighted_samples, const float* pSrc_weights, float* pDst_weights, @@ -1770,7 +2271,7 @@ void upsample_surrogate_weights( const uint32_t sx = pS->m_src_x, sy = pS->m_src_y; float total = 0.0f; - + if (w00) total += pSrc_weights[bounds_check(sx + sy * wx, 0U, total_src_weights)] * (float)w00; if (w01) total += pSrc_weights[bounds_check(sx + 1 + sy * wx, 0U, total_src_weights)] * (float)w01; if (w10) total += pSrc_weights[bounds_check(sx + (sy + 1) * wx, 0U, total_src_weights)] * (float)w10; @@ -1802,16 +2303,15 @@ void log_surrogate_astc_blk::decode(uint32_t block_width, uint32_t block_height, if (needs_upsampling) { - // TODO: Precompute these in tables - astc_helpers::weighted_sample up_weights[astc_helpers::MAX_BLOCK_DIM * astc_helpers::MAX_BLOCK_DIM]; - astc_helpers::compute_upsample_weights(block_width, block_height, m_grid_width, m_grid_height, up_weights); + const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, m_grid_width, m_grid_height); + const astc_helpers::weighted_sample* pUp_weights = pGrid_data->m_upsample_weights.get_ptr(); - upsample_surrogate_weights(up_weights, m_weights0, upsampled_weights0, block_width, block_height, m_grid_width, m_grid_height, m_num_weight_levels); + upsample_surrogate_weights(pUp_weights, m_weights0, upsampled_weights0, block_width, block_height, m_grid_width, m_grid_height, m_num_weight_levels); pWeights0 = upsampled_weights0; if (dual_plane) { - upsample_surrogate_weights(up_weights, m_weights1, upsampled_weights1, block_width, block_height, m_grid_width, m_grid_height, m_num_weight_levels); + upsample_surrogate_weights(pUp_weights, m_weights1, upsampled_weights1, block_width, block_height, m_grid_width, m_grid_height, m_num_weight_levels); pWeights1 = upsampled_weights1; } } @@ -1859,7 +2359,7 @@ void log_surrogate_astc_blk::decode(uint32_t block_width, uint32_t block_height, return decode(block_width, block_height, pPixels, &pPat_data->m_partition_pats[unique_pat_index]); } -void downsample_float_weight_grid( +static void downsample_float_weight_grid( const float* pMatrix_weights, uint32_t bx, uint32_t by, // source/from dimension (block size) uint32_t wx, uint32_t wy, // dest/to dimension (grid size) @@ -1889,7 +2389,7 @@ void downsample_float_weight_grid( } } -float decode_surrogate_and_compute_error( +static float decode_surrogate_and_compute_error( uint32_t block_width, uint32_t block_height, const astc_ldr::pixel_stats_t& pixel_stats, log_surrogate_astc_blk& log_block, @@ -1904,6 +2404,7 @@ float decode_surrogate_and_compute_error( const float wb = (float)params.m_comp_weights[2]; const float wa = (float)params.m_comp_weights[3]; +#if 0 float total_err = 0.0f; for (uint32_t by = 0; by < block_height; by++) { @@ -1921,12 +2422,51 @@ float decode_surrogate_and_compute_error( } // bx } // by +#else + float total_err_alt = 0.0f; + + const uint32_t total_texels = block_width * block_height; + + if ((wr == 1.0f) && (wg == 1.0f) && (wb == 1.0f) && (wa == 1.0f)) + { + for (uint32_t i = 0; i < total_texels; i++) + { + const vec4F& s = pixel_stats.m_pixels_f[i]; + const vec4F& d = dec_pixels[i]; + + float dr = s[0] - d[0]; + float dg = s[1] - d[1]; + float db = s[2] - d[2]; + float da = s[3] - d[3]; + + total_err_alt += (dr * dr) + (dg * dg) + (db * db) + (da * da); + } // i + } + else + { + for (uint32_t i = 0; i < total_texels; i++) + { + const vec4F& s = pixel_stats.m_pixels_f[i]; + const vec4F& d = dec_pixels[i]; + + float dr = s[0] - d[0]; + float dg = s[1] - d[1]; + float db = s[2] - d[2]; + float da = s[3] - d[3]; + + total_err_alt += (wr * dr * dr) + (wg * dg * dg) + (wb * db * db) + (wa * da * da); + } // i + } + + //assert(equal_tol(total_err, total_err_alt, .000125f)); + float total_err = total_err_alt; +#endif return total_err; } // Returns WSSE error -float encode_surrogate_trial( +static float encode_surrogate_trial( uint32_t block_width, uint32_t block_height, const astc_ldr::pixel_stats_t& pixel_stats, uint32_t cem_index, @@ -1939,12 +2479,13 @@ float encode_surrogate_trial( { const bool is_downsampling = (grid_width < block_width) || (grid_height < block_height); const bool dual_plane_flag = (ccs_index >= 0); - - const basist::astc_ldr_t::astc_block_grid_data* pBlock_grid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, grid_width, grid_height); - + const float* pDownsample_matrix = nullptr; if (is_downsampling) + { + const basist::astc_ldr_t::astc_block_grid_data* pBlock_grid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, grid_width, grid_height); pDownsample_matrix = pBlock_grid_data->m_downsample_matrix.get_ptr(); + } //const uint32_t total_block_pixels = block_width * block_height; //const uint32_t total_grid_pixels = grid_width * grid_height; @@ -2006,7 +2547,8 @@ float encode_surrogate_trial( #if defined(_DEBUG) || defined(DEBUG) { float alt_wsse_err = decode_surrogate_and_compute_error(block_width, block_height, pixel_stats, log_block, nullptr, params); - assert(fabs(wsse_err - alt_wsse_err) < .00125f); + //assert(fabs(wsse_err - alt_wsse_err) < .125f); + assert(basisu::equal_rel_tol(wsse_err, alt_wsse_err, .0125f)); } #endif #endif @@ -2015,7 +2557,7 @@ float encode_surrogate_trial( return wsse_err; } -float encode_surrogate_trial_subsets( +static float encode_surrogate_trial_subsets( uint32_t block_width, uint32_t block_height, const astc_ldr::pixel_stats_t& pixel_stats, uint32_t cem_index, @@ -2034,12 +2576,13 @@ float encode_surrogate_trial_subsets( const uint32_t num_weight_levels = astc_helpers::get_ise_levels(weight_ise_range); const uint32_t num_endpoint_levels = astc_helpers::get_ise_levels(endpoint_ise_range); - - const basist::astc_ldr_t::astc_block_grid_data* pBlock_grid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, grid_width, grid_height); - + const float* pDownsample_matrix = nullptr; if (is_downsampling) + { + const basist::astc_ldr_t::astc_block_grid_data* pBlock_grid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, grid_width, grid_height); pDownsample_matrix = pBlock_grid_data->m_downsample_matrix.get_ptr(); + } color_rgba part_pixels[astc_helpers::MAX_PARTITIONS][astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; uint32_t num_part_pixels[astc_helpers::MAX_PARTITIONS] = { 0 }; @@ -2132,7 +2675,8 @@ float encode_surrogate_trial_subsets( { float alt_subset_err = decode_surrogate_and_compute_error(block_width, block_height, pixel_stats, log_block, pPat, params); - assert(fabs(total_subset_err - alt_subset_err) < .00125f); + //assert(fabs(total_subset_err - alt_subset_err) < .00125f); + assert(basisu::equal_rel_tol(total_subset_err, (double)alt_subset_err, (double).0125f)); } #endif #endif @@ -2149,13 +2693,356 @@ static inline vec4F vec4F_norm_approx(vec4F axis) } #endif +// if cov[] wasn't divided by the # of pixels, this is SSE +static inline float estimate_slam_to_line_sse_3D(const float cov[6], float xr, float yr, float zr) +{ + // total var + const float total_var = cov[0] + cov[3] + cov[5]; + + float l = sqrtf(xr * xr + yr * yr + zr * zr); + if (l < basisu::SMALL_FLOAT_VAL) + { + xr = yr = zr = 0.577350269f; + } + else + { + l = 1.0f / l; + xr *= l; yr *= l; zr *= l; + } + + float xr2 = cov[0] * xr + cov[1] * yr + cov[2] * zr; + float xg2 = cov[1] * xr + cov[3] * yr + cov[4] * zr; + float xb2 = cov[2] * xr + cov[4] * yr + cov[5] * zr; + + // Rayleigh quotient/est var of principal axis + const float principal_axis_var = xr2 * xr + xg2 * yr + xb2 * zr; + + // Compute leftover var, this is the var unexplaind by the principal axis + const float ortho_var = basisu::maximum(0.0f, total_var - principal_axis_var); + + return ortho_var; +} + +static inline float estimate_slam_to_line_sse_4D(const float cov[10], float xr, float yr, float zr, float wr) +{ + // total var + const float total_var = cov[0] + cov[4] + cov[7] + cov[9]; + + float l = sqrtf(xr * xr + yr * yr + zr * zr + wr * wr); + if (l < basisu::SMALL_FLOAT_VAL) + { + xr = yr = zr = wr = .5f; + } + else + { + l = 1.0f / l; + xr *= l; yr *= l; zr *= l; wr *= l; + } + + float xr2 = cov[0] * xr + cov[1] * yr + cov[2] * zr + cov[3] * wr; + float xg2 = cov[1] * xr + cov[4] * yr + cov[5] * zr + cov[6] * wr; + float xb2 = cov[2] * xr + cov[5] * yr + cov[7] * zr + cov[8] * wr; + float xa2 = cov[3] * xr + cov[6] * yr + cov[8] * zr + cov[9] * wr; + + // Rayleigh quotient/est var of principal axis + const float principal_axis_var = xr2 * xr + xg2 * yr + xb2 * zr + xa2 * wr; + + // Compute leftover var, this is the var unexplaind by the principal axis + const float ortho_var = basisu::maximum(0.0f, total_var - principal_axis_var); + + return ortho_var; +} + +static float estimate_partition_rgb_sse( + const astc_ldr::pixel_stats_t& pixel_stats, + bool base_scale_flag, + uint32_t block_width, uint32_t block_height, + uint32_t num_subsets, + const astc_ldr::partition_pattern_vec* pPat) +{ + assert((num_subsets >= 2) && (num_subsets <= astc_helpers::MAX_PARTITIONS)); + + color_rgba part_pixels[astc_helpers::MAX_PARTITIONS][astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + uint32_t num_part_pixels[astc_helpers::MAX_PARTITIONS] = { 0 }; + + int part_means[astc_helpers::MAX_PARTITIONS][3]; + clear_obj(part_means); + + const uint32_t total_block_pixels = block_width * block_height; + + // TODO: A moment-based approach could do both RGB direct and base+scale simultaneously. + + if (base_scale_flag) + { + for (uint32_t i = 0; i < total_block_pixels; i++) + { + const color_rgba& px = pixel_stats.m_pixels[i]; + + const uint32_t part_index = (*pPat)[i]; + assert(part_index < num_subsets); + + part_pixels[part_index][num_part_pixels[part_index]] = px; + + num_part_pixels[part_index]++; + + } // i + } + else + { + for (uint32_t i = 0; i < total_block_pixels; i++) + { + const color_rgba& px = pixel_stats.m_pixels[i]; + + const uint32_t part_index = (*pPat)[i]; + assert(part_index < num_subsets); + + part_pixels[part_index][num_part_pixels[part_index]] = px; + + part_means[part_index][0] += px.r; + part_means[part_index][1] += px.g; + part_means[part_index][2] += px.b; + + num_part_pixels[part_index]++; + + } // i + + for (uint32_t i = 0; i < num_subsets; i++) + { + assert(num_part_pixels[i] > 0); + + const int n = num_part_pixels[i]; + const int r = n >> 1; + + part_means[i][0] = (part_means[i][0] + r) / n; + part_means[i][1] = (part_means[i][1] + r) / n; + part_means[i][2] = (part_means[i][2] + r) / n; + } // i + } + + // rr, rg, rb, gg, gb, bb + int part_icov[astc_helpers::MAX_PARTITIONS][6]; + clear_obj(part_icov); + + for (uint32_t p = 0; p < num_subsets; p++) + { + const uint32_t np = num_part_pixels[p]; + + const int mean_r = part_means[p][0]; + const int mean_g = part_means[p][1]; + const int mean_b = part_means[p][2]; + + int* pCov = &part_icov[p][0]; + + for (uint32_t i = 0; i < np; i++) + { + const color_rgba& px = part_pixels[p][i]; + + const int r = (int)px.r - mean_r; + const int g = (int)px.g - mean_g; + const int b = (int)px.b - mean_b; + + pCov[0] += r * r; pCov[1] += r * g; pCov[2] += r * b; + pCov[3] += g * g; pCov[4] += g * b; + pCov[5] += b * b; + + } // i + + } // p + + float slam_to_line_sse_est = 0.0f; + + for (uint32_t p = 0; p < num_subsets; p++) + { + const int block_max_var = basisu::maximum(part_icov[p][0], part_icov[p][3], part_icov[p][5]); + + float cov[6]; + for (uint32_t i = 0; i < 6; i++) + cov[i] = (float)part_icov[p][i]; + + const float sc = block_max_var ? (1.0f / (float)block_max_var) : 0; + const float wx = sc * cov[0], wy = sc * cov[3], wz = sc * cov[5]; + + // estimate principle axis using one iteration of the power method + const float alt_xr = cov[0] * wx + cov[1] * wy + cov[2] * wz; + const float alt_xg = cov[1] * wx + cov[3] * wy + cov[4] * wz; + const float alt_xb = cov[2] * wx + cov[4] * wy + cov[5] * wz; + + slam_to_line_sse_est += estimate_slam_to_line_sse_3D(cov, alt_xr, alt_xg, alt_xb); + + } // p + + return slam_to_line_sse_est; +} + +static float estimate_partition_rgba_sse( + const astc_ldr::pixel_stats_t& pixel_stats, + bool base_scale_flag, + uint32_t block_width, uint32_t block_height, + uint32_t num_subsets, + const astc_ldr::partition_pattern_vec* pPat) +{ + assert((num_subsets >= 2) && (num_subsets <= astc_helpers::MAX_PARTITIONS)); + + color_rgba part_pixels[astc_helpers::MAX_PARTITIONS][astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + uint32_t num_part_pixels[astc_helpers::MAX_PARTITIONS] = { 0 }; + + int part_means[astc_helpers::MAX_PARTITIONS][4]; + clear_obj(part_means); + + const uint32_t total_block_pixels = block_width * block_height; + + if (base_scale_flag) + { + for (uint32_t i = 0; i < total_block_pixels; i++) + { + const color_rgba& px = pixel_stats.m_pixels[i]; + + const uint32_t part_index = (*pPat)[i]; + assert(part_index < num_subsets); + + part_pixels[part_index][num_part_pixels[part_index]] = px; + + num_part_pixels[part_index]++; + + } // i + } + else + { + for (uint32_t i = 0; i < total_block_pixels; i++) + { + const color_rgba& px = pixel_stats.m_pixels[i]; + + const uint32_t part_index = (*pPat)[i]; + assert(part_index < num_subsets); + + part_pixels[part_index][num_part_pixels[part_index]] = px; + + part_means[part_index][0] += px.r; + part_means[part_index][1] += px.g; + part_means[part_index][2] += px.b; + part_means[part_index][3] += px.a; + + num_part_pixels[part_index]++; + + } // i + + for (uint32_t i = 0; i < num_subsets; i++) + { + assert(num_part_pixels[i] > 0); + + const int n = num_part_pixels[i]; + const int r = n >> 1; + + part_means[i][0] = (part_means[i][0] + r) / n; + part_means[i][1] = (part_means[i][1] + r) / n; + part_means[i][2] = (part_means[i][2] + r) / n; + part_means[i][3] = (part_means[i][3] + r) / n; + } // i + } + + int part_icov4[astc_helpers::MAX_PARTITIONS][10]; + clear_obj(part_icov4); + + for (uint32_t p = 0; p < num_subsets; p++) + { + const uint32_t np = num_part_pixels[p]; + + const int mean_r = part_means[p][0]; + const int mean_g = part_means[p][1]; + const int mean_b = part_means[p][2]; + const int mean_a = part_means[p][3]; + + int* pCov = &part_icov4[p][0]; + + for (uint32_t i = 0; i < np; i++) + { + const color_rgba& px = part_pixels[p][i]; + + const int r = (int)px.r - mean_r; + const int g = (int)px.g - mean_g; + const int b = (int)px.b - mean_b; + const int a = (int)px.a - mean_a; + + pCov[0] += r * r; pCov[1] += r * g; pCov[2] += r * b; pCov[3] += r * a; + pCov[4] += g * g; pCov[5] += g * b; pCov[6] += g * a; + pCov[7] += b * b; pCov[8] += b * a; + pCov[9] += a * a; + + } // i + + } // p + + float slam_to_line_sse_est = 0.0f; + + for (uint32_t s = 0; s < num_subsets; s++) + { + const int block_max_var4 = basisu::maximum(part_icov4[s][0], part_icov4[s][4], part_icov4[s][7], part_icov4[s][9]); + + float cov4[10]; + for (uint32_t i = 0; i < 10; i++) + cov4[i] = (float)part_icov4[s][i]; + + const float sc4 = block_max_var4 ? (1.0f / (float)block_max_var4) : 0; + const float wx = sc4 * cov4[0], wy = sc4 * cov4[4], wz = sc4 * cov4[7], wa = sc4 * cov4[9]; + + const float x0 = cov4[0] * wx + cov4[1] * wy + cov4[2] * wz + cov4[3] * wa; + const float y0 = cov4[1] * wx + cov4[4] * wy + cov4[5] * wz + cov4[6] * wa; + const float z0 = cov4[2] * wx + cov4[5] * wy + cov4[7] * wz + cov4[8] * wa; + const float w0 = cov4[3] * wx + cov4[6] * wy + cov4[8] * wz + cov4[9] * wa; + + const float x1 = cov4[0] * x0 + cov4[1] * y0 + cov4[2] * z0 + cov4[3] * w0; + const float y1 = cov4[1] * x0 + cov4[4] * y0 + cov4[5] * z0 + cov4[6] * w0; + const float z1 = cov4[2] * x0 + cov4[5] * y0 + cov4[7] * z0 + cov4[8] * w0; + const float w1 = cov4[3] * x0 + cov4[6] * y0 + cov4[8] * z0 + cov4[9] * w0; + + slam_to_line_sse_est += estimate_slam_to_line_sse_4D(cov4, x1, y1, z1, w1); + + } // s + + return slam_to_line_sse_est; +} + +static inline float estimate_partition_sse(uint32_t cem_index, + const astc_ldr::pixel_stats_t& pixel_stats, + uint32_t block_width, uint32_t block_height, + uint32_t num_subsets, + const astc_ldr::partition_pattern_vec* pPat) +{ + switch (cem_index) + { + // RGB + case astc_helpers::CEM_LDR_RGB_DIRECT: + return estimate_partition_rgb_sse(pixel_stats, false, block_width, block_height, num_subsets, pPat); + case astc_helpers::CEM_LDR_RGB_BASE_SCALE: + return estimate_partition_rgb_sse(pixel_stats, true, block_width, block_height, num_subsets, pPat); + + // RGBA + case astc_helpers::CEM_LDR_RGBA_DIRECT: + return estimate_partition_rgba_sse(pixel_stats, false, block_width, block_height, num_subsets, pPat); + case astc_helpers::CEM_LDR_RGB_BASE_SCALE_PLUS_TWO_A: + return estimate_partition_rgba_sse(pixel_stats, true, block_width, block_height, num_subsets, pPat); + + default: + assert(0); + break; + } + + return 0; +} + +#define BASISU_USE_LSH2 (1) +#define BASISU_USE_LSH3 (1) +#define BASISU_LSH_FILTERING true + static bool estimate_partition2( uint32_t block_width, uint32_t block_height, const astc_ldr::pixel_stats_t& pixels, - int* pBest_parts, uint32_t num_best_parts, // unique indices, not ASTC seeds - const astc_ldr::partitions_data* pPart_data, bool brute_force_flag) + int* pBest_parts, // unique indices, not ASTC seeds + int &num_best_parts, // will be modified with the actual number of results + const astc_ldr::partitions_data* pPart_data, + bool brute_force_flag) { - assert(num_best_parts && (num_best_parts <= pPart_data->m_total_unique_patterns)); + assert(num_best_parts && (num_best_parts <= (int)pPart_data->m_total_unique_patterns)); const uint32_t num_block_pixels = block_width * block_height; @@ -2201,9 +3088,55 @@ static bool estimate_partition2( std::sort(part_similarity, part_similarity + pPart_data->m_total_unique_patterns); - for (uint32_t i = 0; i < num_best_parts; i++) + for (int i = 0; i < num_best_parts; i++) pBest_parts[i] = part_similarity[(pPart_data->m_total_unique_patterns - 1) - i] & 0xFFFF; } + else if (BASISU_USE_LSH2) + { + astc_ldr::partition_pattern_vec desired_part(block_width, block_height); + astc_ldr::partition_pattern_vec desired_part_alt(block_width, block_height); + + for (uint32_t i = 0; i < num_block_pixels; i++) + { + float proj = (pixels.m_pixels_f[i] - pixels.m_mean_f).dot(pixels.m_mean_rel_axis4); + + desired_part.m_parts[i] = proj < 0.0f; + desired_part_alt.m_parts[i] = 1 - desired_part.m_parts[i]; + } + + uint32_t results[astc_helpers::NUM_PARTITION_PATTERNS]; + uint32_t total_results = pPart_data->m_part_lhs_map.find(desired_part, results, astc_helpers::NUM_PARTITION_PATTERNS, BASISU_LSH_FILTERING); + + if (!total_results) + { + num_best_parts = 0; + return false; + } + + uint32_t part_similarity[astc_helpers::NUM_PARTITION_PATTERNS]; + + for (uint32_t res_index = 0; res_index < total_results; res_index++) + { + const uint32_t part_index = results[res_index]; + + const astc_ldr::partition_pattern_vec& pat_vec = pPart_data->m_partition_pats[part_index]; + + const int dist2_a = desired_part.get_squared_distance_2subsets(pat_vec); + const int dist2_b = desired_part_alt.get_squared_distance_2subsets(pat_vec); + + const int dist2 = minimum(dist2_a, dist2_b); + + part_similarity[res_index] = (dist2 << 16) | part_index; + + } // part_index; + + std::sort(part_similarity, part_similarity + total_results); + + num_best_parts = minimum(num_best_parts, total_results); + + for (int i = 0; i < num_best_parts; i++) + pBest_parts[i] = part_similarity[i] & 0xFFFF; + } else { astc_ldr::partition_pattern_vec desired_part(block_width, block_height); @@ -2220,7 +3153,7 @@ static bool estimate_partition2( pPart_data->m_part_vp_tree.find_nearest(2, desired_part, results, num_best_parts); - assert(results.get_size() == num_best_parts); + assert((int)results.get_size() == num_best_parts); const auto& elements = results.get_elements(); @@ -2234,16 +3167,18 @@ static bool estimate_partition2( static bool estimate_partition3( uint32_t block_width, uint32_t block_height, const astc_ldr::pixel_stats_t& pixels, - int* pBest_parts, uint32_t num_best_parts, + int* pBest_parts, + int &num_best_parts, // will be modified with the actual number of results const astc_ldr::partitions_data* pPart_data, bool brute_force_flag) { - assert(num_best_parts && (num_best_parts <= pPart_data->m_total_unique_patterns)); + assert(num_best_parts && (num_best_parts <= (int)pPart_data->m_total_unique_patterns)); vec4F training_vecs[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS], mean(0.0f); const uint32_t num_block_pixels = block_width * block_height, NUM_SUBSETS = 3; - float brightest_inten = 0.0f, darkest_inten = BIG_FLOAT_VAL; + float brightest_inten = -BIG_FLOAT_VAL, darkest_inten = BIG_FLOAT_VAL; + vec4F cluster_centroids[NUM_SUBSETS]; clear_obj(cluster_centroids); @@ -2252,8 +3187,8 @@ static bool estimate_partition3( vec4F& v = training_vecs[i]; v = pixels.m_pixels_f[i]; - - float inten = v.dot(vec4F(1.0f)); + + float inten = (v - pixels.m_mean_f).dot(pixels.m_mean_rel_axis4); if (inten < darkest_inten) { darkest_inten = inten; @@ -2387,7 +3322,49 @@ static bool estimate_partition3( std::sort(part_similarity, part_similarity + pPart_data->m_total_unique_patterns); - for (uint32_t i = 0; i < num_best_parts; i++) + for (int i = 0; i < num_best_parts; i++) + pBest_parts[i] = part_similarity[i] & 0xFFFF; + } + else if (BASISU_USE_LSH3) + { + astc_ldr::partition_pattern_vec desired_parts[astc_ldr::NUM_PART3_MAPPINGS]; + for (uint32_t j = 0; j < astc_ldr::NUM_PART3_MAPPINGS; j++) + desired_parts[j] = desired_part.get_permuted3(j); + + uint32_t results[astc_helpers::NUM_PARTITION_PATTERNS]; + uint32_t total_results = pPart_data->m_part_lhs_map.find(desired_part, results, astc_helpers::NUM_PARTITION_PATTERNS, BASISU_LSH_FILTERING); + + if (!total_results) + { + num_best_parts = 0; + return false; + } + + uint32_t part_similarity[astc_helpers::NUM_PARTITION_PATTERNS]; + + for (uint32_t res_index = 0; res_index < total_results; res_index++) + { + const uint32_t part_index = results[res_index]; + + const astc_ldr::partition_pattern_vec& pat = pPart_data->m_partition_pats[part_index]; + + uint32_t lowest_pat_dist = UINT32_MAX; + for (uint32_t p = 0; p < astc_ldr::NUM_PART3_MAPPINGS; p++) + { + uint32_t dist = pat.get_squared_distance(desired_parts[p]); + if (dist < lowest_pat_dist) + lowest_pat_dist = dist; + } + + part_similarity[res_index] = (lowest_pat_dist << 16) | part_index; + + } // part_index; + + std::sort(part_similarity, part_similarity + total_results); + + num_best_parts = minimum(num_best_parts, total_results); + + for (int i = 0; i < num_best_parts; i++) pBest_parts[i] = part_similarity[i] & 0xFFFF; } else @@ -2397,7 +3374,7 @@ static bool estimate_partition3( pPart_data->m_part_vp_tree.find_nearest(3, desired_part, results, num_best_parts); - assert(results.get_size() == num_best_parts); + assert((int)results.get_size() == num_best_parts); const auto& elements = results.get_elements(); @@ -2424,7 +3401,7 @@ static const float g_sobel_y[3][3] = // [y][x] { 1.0f, 2.0f, 1.0f } }; -void compute_sobel(const image& orig, image& dest, const float* pMatrix_3x3) +static void compute_sobel(const image& orig, image& dest, const float* pMatrix_3x3) { const uint32_t width = orig.get_width(); const uint32_t height = orig.get_height(); @@ -2460,18 +3437,25 @@ void compute_sobel(const image& orig, image& dest, const float* pMatrix_3x3) } // y } -void compute_energy_from_dct(uint32_t block_width, uint32_t block_height, float* pDCT) +// Returns total energy excluding DC +static float compute_ac_energy_from_dct(uint32_t block_width, uint32_t block_height, float* pDCT) { const uint32_t num_texels = block_width * block_height; + float total_energy = 0.0f; for (uint32_t i = 1; i < num_texels; i++) - pDCT[i] = square(pDCT[i]); + { + const float v = square(pDCT[i]); + pDCT[i] = v; + total_energy += v; + } pDCT[0] = 0.0f; + return total_energy; } // Results scaled by # block texels (block-SSE in weight space) -float compute_preserved_dct_energy(uint32_t block_width, uint32_t block_height, const float* pEnergy, uint32_t grid_w, uint32_t grid_h) +static float compute_preserved_dct_energy(uint32_t block_width, uint32_t block_height, const float* pEnergy, uint32_t grid_w, uint32_t grid_h) { float tot = 0.0f; @@ -2488,7 +3472,7 @@ float compute_preserved_dct_energy(uint32_t block_width, uint32_t block_height, } // Results scaled by # block texels (block-SSE in weight space) -inline float compute_lost_dct_energy(uint32_t block_width, uint32_t block_height, const float* pEnergy, uint32_t grid_w, uint32_t grid_h) +[[maybe_unused]] static inline float compute_lost_dct_energy_orig(uint32_t block_width, uint32_t block_height, const float* pEnergy, uint32_t grid_w, uint32_t grid_h) { float tot = 0.0f; @@ -2506,6 +3490,73 @@ inline float compute_lost_dct_energy(uint32_t block_width, uint32_t block_height return tot; } +static inline float compute_lost_dct_energy(uint32_t block_width, uint32_t block_height, const float* pEnergy, uint32_t grid_w, uint32_t grid_h) +{ + float tot0 = 0.0f, tot1 = 0.0f, tot2 = 0.0f, tot3 = 0.0f; + + const float* pSrc_row = pEnergy; + for (uint32_t y = 0; y < block_height; y++, pSrc_row += block_width) + { + uint32_t x = (y < grid_h) ? grid_w : 0; + + while ((x + 4) <= block_width) + { + tot0 += pSrc_row[x + 0]; + tot1 += pSrc_row[x + 1]; + tot2 += pSrc_row[x + 2]; + tot3 += pSrc_row[x + 3]; + x += 4; + } + + while (x < block_width) + { + tot0 += pSrc_row[x]; + ++x; + } + } // y + + return tot0 + tot1 + tot2 + tot3; +} + +// 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. +static inline void prepare_dct_energy_prefix_table(uint32_t block_width, uint32_t block_height, const float* pEnergy, float* pPrefix) +{ + for (uint32_t y = 0; y < block_height; y++) + { + float row_sum = 0.0f; + + for (uint32_t x = 0; x < block_width; x++) + { + row_sum += pEnergy[x + y * block_width]; + + float above = 0.0f; + if (y > 0) + above = pPrefix[x + (y - 1) * block_width]; + + pPrefix[x + y * block_width] = row_sum + above; + } + } +} + +// Sum of the origin-anchored rectangle [0, grid_w) x [0, grid_h). +static inline float query_dct_energy_prefix_sum(uint32_t block_width, uint32_t block_height, const float* pPrefix, uint32_t grid_w, uint32_t grid_h) +{ + (void)block_height; + + assert((grid_w >= 1) && (grid_w <= block_width)); + assert((grid_h >= 1) && (grid_h <= block_height)); + + return pPrefix[(grid_w - 1) + (grid_h - 1) * block_width]; +} + +static inline float compute_lost_dct_energy_prefix_sum(uint32_t block_width, uint32_t block_height, const float* pEnergy_prefix_sum, uint32_t grid_w, uint32_t grid_h, float total_ac_energy) +{ + const float kept_energy = query_dct_energy_prefix_sum(block_width, block_height, pEnergy_prefix_sum, grid_w, grid_h); + + return maximum(total_ac_energy - kept_energy, 0.0f); +} + struct ldr_astc_lowlevel_block_encoder_params { ldr_astc_lowlevel_block_encoder_params() @@ -2552,20 +3603,32 @@ struct ldr_astc_lowlevel_block_encoder_params m_gradient_descent_flag = true; m_polish_weights_flag = true; m_qcd_enabled_flag = true; + m_encode_trial_early_out_thresh = .1f; + m_encode_trial_subsets_early_out_thresh = .1f; m_final_encode_try_base_ofs = true; m_final_encode_always_try_rgb_direct = false; // if true, even if base_ofs succeeds, we try RGB/RGBA direct too +#if 0 m_use_parts_std_dev_thresh = (8.0f / 255.0f); m_use_parts_std_dev_thresh2 = (40.0f / 255.0f); m_sobel_energy_thresh1 = 3200.0f; m_sobel_energy_thresh2 = 30000.0f; m_sobel_energy_thresh3 = 50000.0f; +#else + const float s = .2f; // exp + m_use_parts_std_dev_thresh = (8.0f / 255.0f) * s; + m_use_parts_std_dev_thresh2 = (40.0f / 255.0f) * s; + m_sobel_energy_thresh1 = 3200.0f * s; + m_sobel_energy_thresh2 = 30000.0f * s; + m_sobel_energy_thresh3 = 50000.0f * s; +#endif m_part2_fraction_to_keep = 2; m_part3_fraction_to_keep = 2; m_base_parts2 = 32; m_base_parts3 = 32; + m_use_fast_part_est_stage2 = true; // TODO: Prehaps expose this at a higher level. m_use_blue_contraction = true; @@ -2625,12 +3688,16 @@ struct ldr_astc_lowlevel_block_encoder_params bool m_gradient_descent_flag; bool m_polish_weights_flag; bool m_qcd_enabled_flag; + + float m_encode_trial_early_out_thresh; + float m_encode_trial_subsets_early_out_thresh; bool m_final_encode_try_base_ofs; bool m_final_encode_always_try_rgb_direct; bool m_brute_force_est_parts; bool m_disable_part_est_stage2; // only use single stage partition estimation + bool m_use_fast_part_est_stage2; bool m_use_blue_contraction; // currently global enable/disable @@ -2648,7 +3715,7 @@ struct ldr_astc_lowlevel_block_encoder_params float m_early_stop_wpsnr; float m_early_stop2_wpsnr; - basist::astc_ldr_t::dct2f* m_pDCT2F; // at block size + const basist::astc_ldr_t::dct2f* m_pDCT2F; // at block size }; struct trial_surrogate @@ -2674,7 +3741,7 @@ struct trial_surrogate struct encode_block_output { int16_t m_trial_mode_index; // -1 = solid, no trial mode - uint16_t m_blur_id; // blur index + uint16_t m_blur_id; // blur index, or codec identifier astc_helpers::log_astc_block m_log_blk; @@ -2741,8 +3808,9 @@ struct weight_terms weight_var += squaref(pWeights[i] - m_mean); m_var = weight_var / (float)n; - // drops below 2/3 on smooth blocks and tends to 2/3 when weights are well spread - m_endpoint_factor = (1.0f + 2.0f * m_var + 2.0f * m_mean * m_mean - 2.0f * m_mean) / (2.0f / 3.0f); + // drops below 2/3 on smooth blocks and tends to 2/3 when weights are well spread (so normalized by (2.0f / 3.0f)) + //m_endpoint_factor = (1.0f + 2.0f * m_var + 2.0f * m_mean * m_mean - 2.0f * m_mean) / (2.0f / 3.0f); + m_endpoint_factor = (1.0f + 2.0f * m_var + 2.0f * m_mean * m_mean - 2.0f * m_mean) * (3.0f / 2.0f); m_endpoint_factor = clamp(m_endpoint_factor, .25f, 1.50f); const float UNIFORM_VAR = 1.0f / 12.0f; @@ -2754,13 +3822,18 @@ struct weight_terms }; // weight_gamma is block size/grid size specific factor (0,1] (the amount of MSE quant error remaining taking into account bilinear smoothing) -inline chan_mse_est compute_quantized_channel_mse_estimates(uint32_t num_endpoint_levels, uint32_t num_weight_levels, float span_size, float weight_gamma, const weight_terms* pWeight_terms = nullptr) +static inline chan_mse_est compute_quantized_channel_mse_estimates(uint32_t num_endpoint_levels, uint32_t num_weight_levels, float span_size, float weight_gamma, const weight_terms* pWeight_terms = nullptr) { assert(num_endpoint_levels >= 2); assert(num_weight_levels >= 2); const float Dep = 1.0f / (float)(num_endpoint_levels - 1); // endpoint quant step + +#if BASISU_MODIFIED_WEIGHT_QUANT_MSE_ESTIMATE + const float Dw = 1.0f / (float)(num_weight_levels); // weight quant step +#else const float Dw = 1.0f / (float)(num_weight_levels - 1); // weight quant step +#endif // Endpoint quant MSE estimate is not span dependent float ep_lower = (Dep * Dep) / 12.0f * (2.0f / 3.0f); @@ -2777,14 +3850,14 @@ inline chan_mse_est compute_quantized_channel_mse_estimates(uint32_t num_endpoin return chan_mse_est(ep_lower, wq_lower); } -inline float compute_quantized_channel_endpoint_mse_estimate(uint32_t num_endpoint_levels, const weight_terms* pWeight_terms = nullptr) +static inline float compute_quantized_channel_endpoint_mse_estimate(uint32_t num_endpoint_levels, const weight_terms* pWeight_terms = nullptr) { assert(num_endpoint_levels >= 2); const float Dep = 1.0f / (float)(num_endpoint_levels - 1); // endpoint quant step // Endpoint quant MSE estimate is not span dependent - float ep_lower = (Dep * Dep) / 12.0f * (2.0f / 3.0f); + float ep_lower = (Dep * Dep) * (1.0f / 12.0f) * (2.0f / 3.0f); if (pWeight_terms) ep_lower *= pWeight_terms->m_endpoint_factor; @@ -2792,14 +3865,18 @@ inline float compute_quantized_channel_endpoint_mse_estimate(uint32_t num_endpoi return ep_lower; } -inline float compute_quantized_channel_weight_mse_estimate(uint32_t num_weight_levels, float span_size, float weight_gamma, const weight_terms* pWeight_terms = nullptr) +static inline float compute_quantized_channel_weight_mse_estimate(uint32_t num_weight_levels, float span_size, float weight_gamma, const weight_terms* pWeight_terms = nullptr) { assert(num_weight_levels >= 2); - + +#if BASISU_MODIFIED_WEIGHT_QUANT_MSE_ESTIMATE + const float Dw = 1.0f / (float)(num_weight_levels); // weight quant step +#else const float Dw = 1.0f / (float)(num_weight_levels - 1); // weight quant step +#endif // Weight quant MSE estimate is span dependent - float wq_lower = (Dw * Dw) / 12.0f * weight_gamma * (span_size * span_size); + float wq_lower = (Dw * Dw) * (1.0f / 12.0f) * weight_gamma * (span_size * span_size); if (pWeight_terms) wq_lower *= pWeight_terms->m_weight_spread_scale; @@ -2868,6 +3945,7 @@ typedef basisu::hash_map shortlist_buck struct trial_mode_estimate_superbucket_key { // All member vars from beginning to m_last will be hashed. Be careful of alignment. + // Total size must be sizeof(uint64_t). Unused members MUST be set to 0. uint8_t m_cem_index; int8_t m_ccs_index; uint16_t m_subset_unique_index; @@ -2876,23 +3954,48 @@ struct trial_mode_estimate_superbucket_key uint8_t m_last; uint8_t m_unused[2]; - trial_mode_estimate_superbucket_key() + inline trial_mode_estimate_superbucket_key() { static_assert((sizeof(*this) % 4) == 0, "struct size must be divisible by 4"); } - void clear() + inline void clear() { clear_obj(*this); } - operator size_t() const + inline operator size_t() const { +#if 0 return basist::hash_hsieh((const uint8_t*)this, BASISU_OFFSETOF(trial_mode_estimate_superbucket_key, m_last)); +#elif 1 + static_assert(sizeof(*this) == sizeof(uint64_t), "struct size must be sizeof(uint64_t)"); + + uint64_t x = *reinterpret_cast(this); + + x ^= (x >> 33); + x *= 0xff51afd7ed558ccdULL; // Murmur finalizer constant + x ^= (x >> 33); + + return (size_t)x; +#else + uint64_t x = + (uint64_t)m_cem_index | + (((uint64_t)(uint8_t)m_ccs_index) << 8) | + (((uint64_t)m_subset_unique_index) << 16) | + (((uint64_t)m_num_subsets) << 32); + + x ^= (x >> 33); + x *= 0xff51afd7ed558ccdULL; // Murmur finalizer constant + x ^= (x >> 33); + + return (size_t)x; +#endif } - bool operator== (const trial_mode_estimate_superbucket_key& rhs) const + inline bool operator== (const trial_mode_estimate_superbucket_key& rhs) const { +#if 0 #define COMP(e) if (e != rhs.e) return false; COMP(m_cem_index); COMP(m_ccs_index); @@ -2900,6 +4003,16 @@ struct trial_mode_estimate_superbucket_key COMP(m_num_subsets); #undef COMP return true; +#endif + + static_assert(sizeof(*this) == sizeof(uint64_t), "struct size must be sizeof(uint64_t)"); + + return *reinterpret_cast(this) == *reinterpret_cast(&rhs); + } + + inline bool operator!= (const trial_mode_estimate_superbucket_key& rhs) const + { + return !(*this == rhs); } }; #pragma pack(pop) @@ -3077,7 +4190,7 @@ struct ldr_astc_lowlevel_block_encoder assert(total_parts2 <= (int)std::size(best_parts2_temp)); // Stage 1: kmeans+vptree - const bool has_est_parts2 = estimate_partition2( + bool has_est_parts2 = estimate_partition2( p.m_block_width, p.m_block_height, pixel_stats, best_parts2_temp, total_parts2, @@ -3115,17 +4228,25 @@ struct ldr_astc_lowlevel_block_encoder const astc_ldr::partition_pattern_vec* pPat = &pPart_data->m_partition_pats[unique_seed_index]; log_surrogate_astc_blk surrogate_log_blk; - float sse = encode_surrogate_trial_subsets( - p.m_block_width, p.m_block_height, - pixel_stats, - cem_to_surrogate_encode, 2, part_seed_index, pPat, - astc_helpers::BISE_256_LEVELS, astc_helpers::BISE_64_LEVELS, - p.m_block_width, p.m_block_height, - surrogate_log_blk, - *p.m_pEnc_params, surrogate_encode_flags); - - stats.m_total_surrogate_encodes++; - + float sse; + if (p.m_use_fast_part_est_stage2) + { + sse = estimate_partition_sse(cem_to_surrogate_encode, pixel_stats, p.m_block_width, p.m_block_height, 2, pPat); + } + else + { + sse = encode_surrogate_trial_subsets( + p.m_block_width, p.m_block_height, + pixel_stats, + cem_to_surrogate_encode, 2, part_seed_index, pPat, + astc_helpers::BISE_256_LEVELS, astc_helpers::BISE_64_LEVELS, + p.m_block_width, p.m_block_height, + surrogate_log_blk, + *p.m_pEnc_params, surrogate_encode_flags); + + stats.m_total_surrogate_encodes++; + } + part_sses[i] = sse; } // i @@ -3196,17 +4317,25 @@ struct ldr_astc_lowlevel_block_encoder const astc_ldr::partition_pattern_vec* pPat = &pPart_data->m_partition_pats[unique_seed_index]; log_surrogate_astc_blk surrogate_log_blk; - float sse = encode_surrogate_trial_subsets( - p.m_block_width, p.m_block_height, - pixel_stats, - cem_to_surrogate_encode, 3, part_seed_index, pPat, - astc_helpers::BISE_256_LEVELS, astc_helpers::BISE_64_LEVELS, - p.m_block_width, p.m_block_height, - surrogate_log_blk, - *p.m_pEnc_params, surrogate_encode_flags); - - stats.m_total_surrogate_encodes++; - + float sse; + if (p.m_use_fast_part_est_stage2) + { + sse = estimate_partition_sse(cem_to_surrogate_encode, pixel_stats, p.m_block_width, p.m_block_height, 3, pPat); + } + else + { + sse = encode_surrogate_trial_subsets( + p.m_block_width, p.m_block_height, + pixel_stats, + cem_to_surrogate_encode, 3, part_seed_index, pPat, + astc_helpers::BISE_256_LEVELS, astc_helpers::BISE_64_LEVELS, + p.m_block_width, p.m_block_height, + surrogate_log_blk, + *p.m_pEnc_params, surrogate_encode_flags); + + stats.m_total_surrogate_encodes++; + } + part_sses[i] = sse; } // i @@ -3313,8 +4442,9 @@ struct ldr_astc_lowlevel_block_encoder for (uint32_t grid_size_index = 0; grid_size_index < basist::astc_ldr_t::OTM_NUM_GRID_SIZES; grid_size_index++) { - if (grid_size_index) // if large grid + if (grid_size_index) { + // if "large" grid (gw>=(bw-1)) and (gh>=(bh-1)) - quite conservative filter if (p.m_use_small_grids_only) continue; } @@ -3325,20 +4455,20 @@ struct ldr_astc_lowlevel_block_encoder { if (grid_anisos_index == 1) { - // W>=H + // W_fract >= H_fract if (p.m_filter_horizontally_flag) continue; } else if (grid_anisos_index == 2) { - // Wm_tm_groups[cem_index][subsets_index][ccs_index][grid_size_index][grid_anisos_index]); - + } // grid_aniso_index } // grid_size_index @@ -3416,6 +4546,10 @@ struct ldr_astc_lowlevel_block_encoder // Create superbuckets uint32_t max_superbucket_tm_indices = 0; + + trial_mode_estimate_superbucket_hash::insert_result superbucket_ins_res; + bool superbucket_ins_res_is_valid = false; + for (uint32_t j = 0; j < m_trial_modes_to_estimate.size(); j++) { const uint32_t trial_mode_iter = m_trial_modes_to_estimate[j]; @@ -3431,14 +4565,19 @@ struct ldr_astc_lowlevel_block_encoder if (tm.m_num_parts == 1) { - auto ins_res = m_superbucket_hash.insert(new_key, new_val); - const bool created_flag = ins_res.second; + bool created_flag = false; + if ((!superbucket_ins_res_is_valid) || (new_key != (superbucket_ins_res.first)->first)) + { + superbucket_ins_res = m_superbucket_hash.insert(new_key, new_val); + created_flag = superbucket_ins_res.second; + superbucket_ins_res_is_valid = true; + } - assert(ins_res.first->first.m_cem_index == tm.m_cem); - assert(ins_res.first->first.m_ccs_index == tm.m_ccs_index); - assert(ins_res.first->first.m_num_subsets == tm.m_num_parts); + assert(superbucket_ins_res.first->first.m_cem_index == tm.m_cem); + assert(superbucket_ins_res.first->first.m_ccs_index == tm.m_ccs_index); + assert(superbucket_ins_res.first->first.m_num_subsets == tm.m_num_parts); - trial_mode_estimate_superbucket_value& v = (ins_res.first)->second; + trial_mode_estimate_superbucket_value& v = (superbucket_ins_res.first)->second; if (created_flag) v.m_trial_mode_list.reserve(256); @@ -3460,14 +4599,19 @@ struct ldr_astc_lowlevel_block_encoder new_key.m_subset_unique_index = safe_cast_uint16(part_unique_index); - auto ins_res = m_superbucket_hash.insert(new_key, new_val); - const bool created_flag = ins_res.second; + bool created_flag = false; + if ((!superbucket_ins_res_is_valid) || (new_key != (superbucket_ins_res.first)->first)) + { + superbucket_ins_res = m_superbucket_hash.insert(new_key, new_val); + created_flag = superbucket_ins_res.second; + superbucket_ins_res_is_valid = true; + } - assert(ins_res.first->first.m_cem_index == tm.m_cem); - assert(ins_res.first->first.m_ccs_index == tm.m_ccs_index); - assert(ins_res.first->first.m_num_subsets == tm.m_num_parts); + assert(superbucket_ins_res.first->first.m_cem_index == tm.m_cem); + assert(superbucket_ins_res.first->first.m_ccs_index == tm.m_ccs_index); + assert(superbucket_ins_res.first->first.m_num_subsets == tm.m_num_parts); - trial_mode_estimate_superbucket_value& v = (ins_res.first)->second; + trial_mode_estimate_superbucket_value& v = (superbucket_ins_res.first)->second; if (created_flag) v.m_trial_mode_list.reserve(256); @@ -3491,17 +4635,14 @@ struct ldr_astc_lowlevel_block_encoder const uint32_t max_priority_queue_size = p.m_superbucket_max_to_retain[m_block_complexity_index]; - // purposely downscale lost scale energy relative to the other error sources - // this biased the encoder towards smaller grids const float SLAM_TO_LINE_WEIGHT = 1.5f; // upweight STL relative to other errors to give the estimator more of a signal especially for dual plane - const float QUANT_ERROR_WEIGHT = 1.0f; // quant error is naturally quite pessimistic + const float QUANT_ERROR_WEIGHT = 1.0f; // endpoint/weight quant error const float SCALE_ERROR_WEIGHT = 3.0f; // weight grid downsample (scale) error - + // Discount for blue contraction encoding and base+offset CEM's. const float BLUE_CONTRACTION_ENDPOINT_QUANT_DISCOUNT = .5f; // Iterate over all superbuckets, surrogate encode to compute slam to line error, DCT of weight grid(s) to estimate energy lost during weight grid downsampling. - // TODO: priority queue and aggressive early outs for (auto superbucket_iter = m_superbucket_hash.begin(); superbucket_iter != m_superbucket_hash.end(); ++superbucket_iter) { const trial_mode_estimate_superbucket_key& key = superbucket_iter->first; @@ -3583,21 +4724,30 @@ struct ldr_astc_lowlevel_block_encoder float weight0_energy[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; float weight1_energy[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + float weight0_energy_prefix_sum[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + float weight1_energy_prefix_sum[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + basist::astc_ldr_t::fvec& dct_work = m_dct_work; // Forward DCT in normalized weight (surrogate) space p.m_pDCT2F->forward(log_blk.m_weights0, weight0_energy, dct_work); - compute_energy_from_dct(p.m_block_width, p.m_block_height, weight0_energy); + const float total_weight0_ac_energy = compute_ac_energy_from_dct(p.m_block_width, p.m_block_height, weight0_energy); + prepare_dct_energy_prefix_table(p.m_block_width, p.m_block_height, weight0_energy, weight0_energy_prefix_sum); + + float total_weight1_ac_energy = 0.0f; if (key.m_ccs_index >= 0) { p.m_pDCT2F->forward(log_blk.m_weights1, weight1_energy, dct_work); - compute_energy_from_dct(p.m_block_width, p.m_block_height, weight1_energy); + + total_weight1_ac_energy = compute_ac_energy_from_dct(p.m_block_width, p.m_block_height, weight1_energy); + prepare_dct_energy_prefix_table(p.m_block_width, p.m_block_height, weight1_energy, weight1_energy_prefix_sum); } weight_terms weight0_terms, weight1_terms; weight_terms* pWeight0_terms = &weight0_terms; weight_terms* pWeight1_terms = nullptr; + // TODO: These correction factors are not per-subset, but per-block. weight0_terms.calc(total_block_texels, log_blk.m_weights0); if (key.m_ccs_index >= 0) { @@ -3620,7 +4770,7 @@ struct ldr_astc_lowlevel_block_encoder subset_pixels[subset_index] = total_subset_pixels; } - // Loop through all trial modes in this sueprbucket. TODO: Sort by endpoint levels? + // Loop through all trial modes in this superbucket. TODO: Sort by endpoint levels? for (uint32_t k = 0; k < val.m_trial_mode_list.size(); k++) { const uint32_t trial_mode_index = val.m_trial_mode_list[k]; @@ -3664,6 +4814,7 @@ struct ldr_astc_lowlevel_block_encoder } // chan_index } + // TODO: perhaps we can rapidly predict when blue contraction can actually be applied based off each subset's endpoints. if ((tm.m_cem == astc_helpers::CEM_LDR_RGB_DIRECT) || (tm.m_cem == astc_helpers::CEM_LDR_RGBA_DIRECT)) total_e_quant_wsse *= BLUE_CONTRACTION_ENDPOINT_QUANT_DISCOUNT; @@ -3671,52 +4822,68 @@ struct ldr_astc_lowlevel_block_encoder if (total_wsse_so_far >= worst_wsse_found_so_far) continue; - float lost_weight_energy0 = compute_lost_dct_energy(p.m_block_width, p.m_block_height, weight0_energy, tm.m_grid_width, tm.m_grid_height) * inv_total_block_texels; + //const float lost_weight_energy0 = compute_lost_dct_energy(p.m_block_width, p.m_block_height, weight0_energy, tm.m_grid_width, tm.m_grid_height) * inv_total_block_texels; + //assert(basisu::equal_tol(lost_weight_energy0, compute_lost_dct_energy_orig(p.m_block_width, p.m_block_height, weight0_energy, tm.m_grid_width, tm.m_grid_height) * inv_total_block_texels, .0000125f)); + + const float lost_weight_energy0_prefix_sum = compute_lost_dct_energy_prefix_sum(p.m_block_width, p.m_block_height, weight0_energy_prefix_sum, tm.m_grid_width, tm.m_grid_height, total_weight0_ac_energy) * inv_total_block_texels; + //assert(basisu::equal_tol(lost_weight_energy0, lost_weight_energy0_prefix_sum, .00125f)); + const float lost_weight_energy0 = lost_weight_energy0_prefix_sum; float lost_weight_energy1 = 0; if (key.m_ccs_index >= 0) - lost_weight_energy1 = compute_lost_dct_energy(p.m_block_width, p.m_block_height, weight1_energy, tm.m_grid_width, tm.m_grid_height) * inv_total_block_texels; + { + //lost_weight_energy1 = compute_lost_dct_energy(p.m_block_width, p.m_block_height, weight1_energy, tm.m_grid_width, tm.m_grid_height) * inv_total_block_texels; + //assert(basisu::equal_tol(lost_weight_energy1, compute_lost_dct_energy_orig(p.m_block_width, p.m_block_height, weight1_energy, tm.m_grid_width, tm.m_grid_height) * inv_total_block_texels, .0000125f)); + + const float lost_weight_energy1_prefix_sum = compute_lost_dct_energy_prefix_sum(p.m_block_width, p.m_block_height, weight1_energy_prefix_sum, tm.m_grid_width, tm.m_grid_height, total_weight1_ac_energy) * inv_total_block_texels; + //assert(basisu::equal_tol(lost_weight_energy1, lost_weight_energy1_prefix_sum, .00125f)); + + lost_weight_energy1 = lost_weight_energy1_prefix_sum; + } // Add up: // slam to line error WSSE (weighted sum of squared errors) // weight quant error WSSE // endpoint quant error WSSE // weight grid rescale error WSSE (scaled by span^2) - float total_scale_wsse = 0.0f; - - for (uint32_t subset_index = 0; subset_index < key.m_num_subsets; subset_index++) + if ((lost_weight_energy0 != 0.0f) || (lost_weight_energy1 != 0.0f)) { - const vec4F& subset_chan_spans = subset_spans[subset_index]; - const uint32_t total_subset_pixels = subset_pixels[subset_index]; + float total_scale_wsse = 0.0f; - for (uint32_t c = 0; c < 4; c++) + for (uint32_t subset_index = 0; subset_index < key.m_num_subsets; subset_index++) { - float span_size = fabs(subset_chan_spans[c]); + const vec4F& subset_chan_spans = subset_spans[subset_index]; + const uint32_t total_subset_pixels = subset_pixels[subset_index]; - if ((span_size == 0.0f) && ((log_blk.m_endpoints[subset_index][1][c] == 0.0f) || (log_blk.m_endpoints[subset_index][1][c] == 1.0f))) + for (uint32_t c = 0; c < 4; c++) { - // Won't have any E/W quant err at extremes (0.0 or 1.0 are always perfectly represented), no weight downsample error either. - //chan_mse.m_ep = 0.0f; - //chan_mse.m_wp = 0.0f; - } - else - { - // Scale channel MSE by chan weight and the # of subset pixels to get weighted SSE - const float chan_N = (float)p.m_pEnc_params->m_comp_weights[c] * (float)total_subset_pixels; - - // sum in the plane's lost weight energy, scaled by span_size^2 * chan_weight * num_texels_covered - if (key.m_ccs_index == (int)c) - total_scale_wsse += lost_weight_energy1 * square(span_size) * chan_N; + float span_size = fabs(subset_chan_spans[c]); + + if ((span_size == 0.0f) && ((log_blk.m_endpoints[subset_index][1][c] == 0.0f) || (log_blk.m_endpoints[subset_index][1][c] == 1.0f))) + { + // Won't have any E/W quant err at extremes (0.0 or 1.0 are always perfectly represented), no weight downsample error either. + //chan_mse.m_ep = 0.0f; + //chan_mse.m_wp = 0.0f; + } else - total_scale_wsse += lost_weight_energy0 * square(span_size) * chan_N; - } + { + // Scale channel MSE by chan weight and the # of subset pixels to get weighted SSE + const float chan_N = (float)p.m_pEnc_params->m_comp_weights[c] * (float)total_subset_pixels; - } // chan_index + // sum in the plane's lost weight energy, scaled by span_size^2 * chan_weight * num_texels_covered + if (key.m_ccs_index == (int)c) + total_scale_wsse += lost_weight_energy1 * square(span_size) * chan_N; + else + total_scale_wsse += lost_weight_energy0 * square(span_size) * chan_N; + } + + } // chan_index + } + + total_wsse_so_far += (SCALE_ERROR_WEIGHT * total_scale_wsse); + if (total_wsse_so_far >= worst_wsse_found_so_far) + continue; } - - total_wsse_so_far += (SCALE_ERROR_WEIGHT * total_scale_wsse); - if (total_wsse_so_far >= worst_wsse_found_so_far) - continue; float total_w_quant_wsse = 0.0f; for (uint32_t subset_index = 0; subset_index < key.m_num_subsets; subset_index++) @@ -4428,7 +5595,7 @@ struct ldr_astc_lowlevel_block_encoder } // if (num_modes_in_bucket_to_shortlist < num_modes_in_bucket) - // Surrogate encode the best looking buckets after factoring in estimate SSE errors. + // Surrogate encode the best looking modes in the bucket after factoring in estimate SSE errors. for (uint32_t q = 0; q < num_modes_in_bucket_to_shortlist; q++) { @@ -4525,14 +5692,16 @@ struct ldr_astc_lowlevel_block_encoder bool base_ofs_succeeded_flag = false; - if ((p.m_final_encode_try_base_ofs) && ((tm.m_cem == astc_helpers::CEM_LDR_RGB_DIRECT) || (tm.m_cem == astc_helpers::CEM_LDR_RGBA_DIRECT))) + if ((p.m_final_encode_try_base_ofs) && + ((tm.m_cem == astc_helpers::CEM_LDR_RGB_DIRECT) || (tm.m_cem == astc_helpers::CEM_LDR_RGBA_DIRECT))) + //(tm.m_endpoint_ise_range < astc_helpers::BISE_256_LEVELS)) // although this check makes sense for quality, it may not make sense for rate-distortion performance { // Add RGB/RGBA BASE PLUS OFFSET variant. astc_helpers::log_astc_block log_astc_blk_alt; const uint32_t base_ofs_cem_index = (tm.m_cem == astc_helpers::CEM_LDR_RGB_DIRECT) ? astc_helpers::CEM_LDR_RGB_BASE_PLUS_OFFSET : astc_helpers::CEM_LDR_RGBA_BASE_PLUS_OFFSET; - bool base_ofs_clamped_flag = false; + bool try_direct_encoding_flag = false; bool alt_enc_trial_status; if (tm.m_num_parts > 1) @@ -4547,9 +5716,13 @@ struct ldr_astc_lowlevel_block_encoder p.m_block_width, p.m_block_height, pixel_stats, base_ofs_cem_index, tm.m_num_parts, part_seed_index, pPat, tm.m_endpoint_ise_range, tm.m_weight_ise_range, - tm.m_grid_width, tm.m_grid_height, log_astc_blk_alt, *p.m_pEnc_params, false, + tm.m_grid_width, tm.m_grid_height, + p.m_encode_trial_subsets_early_out_thresh, + log_astc_blk_alt, *p.m_pEnc_params, false, p.m_gradient_descent_flag, p.m_polish_weights_flag, p.m_qcd_enabled_flag, - p.m_use_blue_contraction, &base_ofs_clamped_flag); + p.m_use_blue_contraction, &try_direct_encoding_flag); + + stats.m_total_full_encodes++; } else { @@ -4557,17 +5730,18 @@ struct ldr_astc_lowlevel_block_encoder p.m_block_width, p.m_block_height, pixel_stats, base_ofs_cem_index, tm.m_ccs_index != -1, tm.m_ccs_index, tm.m_endpoint_ise_range, tm.m_weight_ise_range, - tm.m_grid_width, tm.m_grid_height, log_astc_blk_alt, *p.m_pEnc_params, + tm.m_grid_width, tm.m_grid_height, p.m_encode_trial_early_out_thresh, + log_astc_blk_alt, *p.m_pEnc_params, p.m_gradient_descent_flag, p.m_polish_weights_flag, p.m_qcd_enabled_flag, - p.m_use_blue_contraction, &base_ofs_clamped_flag); + p.m_use_blue_contraction, &try_direct_encoding_flag); + + stats.m_total_full_encodes++; } assert(alt_enc_trial_status); if (alt_enc_trial_status) { - stats.m_total_full_encodes++; - encode_block_output* pOut_block2 = out_blocks.enlarge(1); pOut_block2->clear(); pOut_block2->m_trial_mode_index = safe_cast_int16(trial_mode_index); @@ -4589,7 +5763,7 @@ struct ldr_astc_lowlevel_block_encoder } } - base_ofs_succeeded_flag = !base_ofs_clamped_flag; + base_ofs_succeeded_flag = !try_direct_encoding_flag; } } // (p.m_final_encode_try_base_ofs) @@ -4611,9 +5785,13 @@ struct ldr_astc_lowlevel_block_encoder p.m_block_width, p.m_block_height, pixel_stats, tm.m_cem, tm.m_num_parts, part_seed_index, pPat, tm.m_endpoint_ise_range, tm.m_weight_ise_range, - tm.m_grid_width, tm.m_grid_height, log_astc_blk, *p.m_pEnc_params, false, + tm.m_grid_width, tm.m_grid_height, + p.m_encode_trial_subsets_early_out_thresh, + log_astc_blk, *p.m_pEnc_params, false, p.m_gradient_descent_flag, p.m_polish_weights_flag, p.m_qcd_enabled_flag, p.m_use_blue_contraction); + + stats.m_total_full_encodes++; } else { @@ -4621,18 +5799,19 @@ struct ldr_astc_lowlevel_block_encoder p.m_block_width, p.m_block_height, pixel_stats, tm.m_cem, tm.m_ccs_index != -1, tm.m_ccs_index, tm.m_endpoint_ise_range, tm.m_weight_ise_range, - tm.m_grid_width, tm.m_grid_height, log_astc_blk, *p.m_pEnc_params, + tm.m_grid_width, tm.m_grid_height, p.m_encode_trial_early_out_thresh, + log_astc_blk, *p.m_pEnc_params, p.m_gradient_descent_flag, p.m_polish_weights_flag, p.m_qcd_enabled_flag, p.m_use_blue_contraction); + + stats.m_total_full_encodes++; } assert(enc_trial_status); if (!enc_trial_status) return false; - - stats.m_total_full_encodes++; - + { encode_block_output* pOut_block1 = out_blocks.enlarge(1); pOut_block1->clear(); @@ -4831,7 +6010,7 @@ struct trial_mode_desc }; #pragma pack(pop) -static const int s_astc_cem_to_unique_ldr_index[16] = +[[maybe_unused]] static const int s_astc_cem_to_unique_ldr_index[16] = { 0, // CEM_LDR_LUM_DIRECT -1, // CEM_LDR_LUM_BASE_PLUS_OFS @@ -4863,6 +6042,7 @@ static const int s_unique_ldr_index_to_astc_cem[6] = }; #endif +#if 0 static uint32_t pack_tm_desc( uint32_t grid_width, uint32_t grid_height, uint32_t cem_index, uint32_t ccs_index, uint32_t num_subsets, @@ -4906,8 +6086,10 @@ static uint32_t pack_tm_desc( return packed_id; } +#endif -void create_encoder_trial_modes_full_eval(uint32_t block_width, uint32_t block_height, +#if 0 +static void create_encoder_trial_modes_full_eval(uint32_t block_width, uint32_t block_height, basisu::vector& encoder_trial_modes, basist::astc_ldr_t::grouped_trial_modes& grouped_encoder_trial_modes, bool print_debug_info = true, bool print_modes = false) { @@ -5128,6 +6310,7 @@ void create_encoder_trial_modes_full_eval(uint32_t block_width, uint32_t block_h // sanity check assert(encoder_trial_modes.size() < 11000); } +#endif const uint32_t TOTAL_RGBA_CHAN_PAIRS = 6; //const uint32_t TOTAL_RGB_CHAN_PAIRS = 3; @@ -5141,7 +6324,8 @@ static const uint8_t g_rgba_chan_pairs[TOTAL_RGBA_CHAN_PAIRS][2] = { 2, 3 } }; -bool encoder_trial_mode_test() +#if 0 +static bool encoder_trial_mode_test() { for (uint32_t w = 4; w <= 12; w++) { @@ -5198,6 +6382,7 @@ bool encoder_trial_mode_test() fmt_debug_printf("trial mode test succeeded\n"); return true; } +#endif //---------------------------------------------------------------------------------- @@ -5218,9 +6403,10 @@ struct ldr_astc_block_encode_image_high_level_config bool m_subsets_edge_filtering = true; bool m_filter_by_pca_angles_flag = true; - float m_use_direct_angle_thresh = 2.0f; + + float m_use_direct_angle_thresh = .25f; float m_use_base_scale_angle_thresh = 7.0f; - + bool m_force_all_dual_plane_chan_evals = false; // much slower, test on base bool m_disable_rgb_dual_plane = false; // DP can be on alpha only, if block has alpha float m_strong_dp_decorr_thresh_rgb = .998f; @@ -5245,13 +6431,15 @@ struct ldr_astc_block_encode_image_high_level_config float m_early_stop_wpsnr = 0.0f; float m_early_stop2_wpsnr = 0.0f; - bool m_blurring_enabled = false; + bool m_blurring_enabled_p1 = false; bool m_blurring_enabled_p2 = false; bool m_gradient_descent_flag = true; bool m_polish_weights_flag = true; bool m_qcd_enabled_flag = true; // gradient descent must be enabled too bool m_bucket_pruning_passes = true; + float m_encode_trial_early_out_thresh = .1f; + float m_encode_trial_subsets_early_out_thresh = .1f; // 2nd superpass options uint32_t m_base_parts2_p2 = 64; @@ -5263,19 +6451,17 @@ struct ldr_astc_block_encode_image_high_level_config bool m_force_all_dp_chans_p2 = false; bool m_final_encode_always_try_rgb_direct = false; bool m_filter_by_pca_angles_flag_p2 = true; - - // only store the single best result per block - //bool m_save_single_result = false; + + bool m_try_simplified_latent_configs = false; bool m_debug_images = false; bool m_debug_output = false; + bool m_debug_output_image_metrics = true; std::string m_debug_file_prefix; job_pool* m_pJob_pool; - - //saliency_map m_saliency_map; - + astc_ldr::cem_encode_params m_cem_enc_params; }; @@ -5324,8 +6510,9 @@ struct ldr_astc_block_encode_image_output basisu::vector m_out_blocks; - uint32_t m_packed_out_block_index = 0; // index of best out block by WSSE - + uint32_t m_packed_out_block_index = 0; // index of chosen block (typically best out block by WSSE, but not necessarily) + + // TODO: Remove, lossy supercompression uses it, but not all compressors write them. bool m_low_freq_block_flag = false; bool m_super_strong_edges = false; bool m_very_strong_edges = false; @@ -5353,10 +6540,1126 @@ private: constexpr bool selective_blurring = true; -bool ldr_astc_block_encode_image( +struct encoder_config_manager +{ + float m_max_std_dev; + float m_sobel_energy; + bool m_is_lum_only; + basisu::vector m_block_dct_energy; + bool m_filter_horizontally_flag; + bool m_low_freq_block_flag; + uint32_t m_total_active_chans; + basisu::comparative_stats m_cross_chan_stats[TOTAL_RGBA_CHAN_PAIRS]; + float m_chan_pair_correlations[6]; + bool m_active_chan_flags[4]; + float m_min_corr, m_max_corr; + bool m_used_alpha_encoder_modes; + astc_ldr::cem_encode_params m_temp_cem_enc_params; + + encoder_config_manager() + { + clear(); + } + + void clear() + { + m_max_std_dev = 0.0f; + m_sobel_energy = 0.0f; + m_is_lum_only = false; + m_block_dct_energy.clear(); + m_filter_horizontally_flag = false; + m_low_freq_block_flag = false; + m_total_active_chans = 0; + for (uint32_t i = 0; i < TOTAL_RGBA_CHAN_PAIRS; i++) + m_cross_chan_stats[i].clear(); + clear_obj(m_chan_pair_correlations); + clear_obj(m_active_chan_flags); + m_min_corr = 0, m_max_corr = 0; + m_used_alpha_encoder_modes = false; + m_sobel_energy = 0.0f; + } + + void init( + uint32_t bx, uint32_t by, + uint32_t block_width, uint32_t block_height, uint32_t total_block_pixels, + const astc_ldr::pixel_stats_t& pixel_stats, + const basist::astc_ldr_t::dct2f& dct, + const ldr_astc_block_encode_image_high_level_config& enc_cfg, + const image& sobel_xy, + image& vis_dct_low_freq_block, + uint32_t blur_id) + { + m_max_std_dev = 0.0f; + for (uint32_t i = 0; i < 4; i++) + m_max_std_dev = maximum(m_max_std_dev, pixel_stats.m_rgba_stats[i].m_std_dev); + + m_is_lum_only = true; + + for (uint32_t y = 0; y < block_height; y++) + { + for (uint32_t x = 0; x < block_width; x++) + { + const color_rgba& c = pixel_stats.m_pixels[x + y * block_width]; + bool is_lum_texel = (c.r == c.g) && (c.r == c.b); + if (!is_lum_texel) + { + m_is_lum_only = false; + break; + } + } + if (m_is_lum_only) + break; + } + + // TODO: allocation + m_block_dct_energy.resize(total_block_pixels); + m_block_dct_energy.set_all(0); + + m_filter_horizontally_flag = false; + m_low_freq_block_flag = false; + + { + // TODO: allocations + basisu::vector block_floats(total_block_pixels); + basisu::vector block_dct(total_block_pixels); + basist::astc_ldr_t::fvec work; + + for (uint32_t c = 0; c < 4; c++) + { + for (uint32_t i = 0; i < total_block_pixels; i++) + block_floats[i] = pixel_stats.m_pixels_f[i][c]; + + dct.forward(block_floats.data(), block_dct.data(), work); + + for (uint32_t y = 0; y < block_height; y++) + for (uint32_t x = 0; x < block_width; x++) + m_block_dct_energy[x + y * block_width] += (float)enc_cfg.m_cem_enc_params.m_comp_weights[c] * squaref(block_dct[x + y * block_width]); + + } // c + + // Wipe DC + m_block_dct_energy[0] = 0.0f; + + float tot_energy = compute_preserved_dct_energy(block_width, block_height, m_block_dct_energy.get_ptr(), block_width, block_height); + + float h_energy_lost = compute_lost_dct_energy(block_width, block_height, m_block_dct_energy.get_ptr(), block_width / 2, block_height); + float v_energy_lost = compute_lost_dct_energy(block_width, block_height, m_block_dct_energy.get_ptr(), block_width, block_height / 2); + + m_filter_horizontally_flag = h_energy_lost < v_energy_lost; + + float hv2_lost_energy_fract = compute_lost_dct_energy(block_width, block_height, m_block_dct_energy.get_ptr(), 2, 2); + if (tot_energy) + hv2_lost_energy_fract /= tot_energy; + + const float LOW_FREQ_BLOCK_LOST_ENERGY_FRACT_THRESH = .03f; + const float LOW_FREQ_BLOCK_MAX_STD_DEV_TRESH = 1.0f / 255.0f; + // Ultra-smooth block determination: Only look at small grids if the block is mostly low frequency energy OR it has a very low max standard deviation. + if ((hv2_lost_energy_fract < LOW_FREQ_BLOCK_LOST_ENERGY_FRACT_THRESH) || (m_max_std_dev < LOW_FREQ_BLOCK_MAX_STD_DEV_TRESH)) + m_low_freq_block_flag = true; + } + + if ((enc_cfg.m_debug_images) && (blur_id == 0)) + vis_dct_low_freq_block.fill_box(bx * block_width, by * block_height, block_width, block_height, m_low_freq_block_flag ? color_rgba(255, 0, 0, 255) : g_black_color); + + for (uint32_t i = 0; i < 4; i++) + m_active_chan_flags[i] = false; + + // The number of channels with non-zero spans + m_total_active_chans = 0; + + for (uint32_t i = 0; i < 4; i++) + { + if (pixel_stats.m_rgba_stats[i].m_range > 0.0f) + { + assert(pixel_stats.m_max[i] != pixel_stats.m_min[i]); + + m_active_chan_flags[i] = true; + + m_total_active_chans++; + } + else + { + assert(pixel_stats.m_max[i] == pixel_stats.m_min[i]); + } + } + + for (uint32_t i = 0; i < TOTAL_RGBA_CHAN_PAIRS; i++) + { + m_cross_chan_stats[i].clear(); + + // def=max correlation for each channel pair (or 1 if one of the channels is inactive) + m_chan_pair_correlations[i] = 1.0f; + } + + // 0=0, 1 + // 1=0, 2 + // 2=1, 2 + // 3=0, 3 + // 4=1, 3 + // 5=2, 3 + + m_min_corr = 1.0f; + m_max_corr = 0.0f; + + for (uint32_t pair_index = 0; pair_index < TOTAL_RGBA_CHAN_PAIRS; pair_index++) + { + const uint32_t chanA = g_rgba_chan_pairs[pair_index][0]; + const uint32_t chanB = g_rgba_chan_pairs[pair_index][1]; + + // If both channels were active, we've got usable correlation statistics. + if (m_active_chan_flags[chanA] && m_active_chan_flags[chanB]) + { + // TODO: This can be directly derived from the 3D/4D covariance matrix entries. + m_cross_chan_stats[pair_index].calc_pearson(total_block_pixels, + &pixel_stats.m_pixels_f[0][chanA], + &pixel_stats.m_pixels_f[0][chanB], + 4, 4, + &pixel_stats.m_rgba_stats[chanA], + &pixel_stats.m_rgba_stats[chanB]); + + m_chan_pair_correlations[pair_index] = fabsf(m_cross_chan_stats[pair_index].m_pearson); + + const float c = fabsf((float)m_cross_chan_stats[pair_index].m_pearson); + m_min_corr = minimum(m_min_corr, c); + m_max_corr = maximum(m_max_corr, c); + } + } + + // min_cor will be 1.0f if all channels inactive (solid) + + m_used_alpha_encoder_modes = pixel_stats.m_has_alpha; + + m_sobel_energy = 0.0f; + for (uint32_t y = 0; y < block_height; y++) + { + for (uint32_t x = 0; x < block_width; x++) + { + const color_rgba& s = sobel_xy.get_clamped(bx * block_width + x, by * block_height + y); + m_sobel_energy += s[0] * s[0] + s[1] * s[1] + s[2] * s[2] + s[3] * s[3]; + } // x + } // y + + m_sobel_energy /= (float)total_block_pixels; + } + + void select( + ldr_astc_lowlevel_block_encoder_params& enc_blk_params, + uint32_t superpass_index, + uint32_t bx, uint32_t by, + uint32_t block_width, uint32_t block_height, uint32_t total_block_pixels, + const image& orig_img_sobel_xy, + const basisu::vector& encoder_trial_modes, + basist::astc_ldr_t::grouped_trial_modes& grouped_encoder_trial_modes, + astc_ldr::partitions_data* pPart_data_p2, astc_ldr::partitions_data* pPart_data_p3, + const astc_ldr::pixel_stats_t& pixel_stats, + const ldr_astc_block_encode_image_high_level_config& enc_cfg, + const basist::astc_ldr_t::dct2f& dct, + uint32_t blur_id) + { + BASISU_NOTE_UNUSED(blur_id); + + enc_blk_params.m_block_width = block_width; + enc_blk_params.m_block_height = block_height; + enc_blk_params.m_total_block_pixels = total_block_pixels; + enc_blk_params.m_bx = bx; + enc_blk_params.m_by = by; + + enc_blk_params.m_pOrig_img_sobel_xy_t = &orig_img_sobel_xy; + + enc_blk_params.m_num_trial_modes = encoder_trial_modes.size_u32(); + enc_blk_params.m_pTrial_modes = encoder_trial_modes.get_ptr(); + enc_blk_params.m_pGrouped_trial_modes = &grouped_encoder_trial_modes; + + enc_blk_params.m_pPart_data_p2 = pPart_data_p2; + enc_blk_params.m_pPart_data_p3 = pPart_data_p3; + enc_blk_params.m_pEnc_params = &enc_cfg.m_cem_enc_params; + + float ang_dot = saturate(pixel_stats.m_zero_rel_axis3.dot3(pixel_stats.m_mean_rel_axis3)); + const float pca_axis_angles = acosf(ang_dot) * (180.0f / (float)cPiD); + + enc_blk_params.m_use_alpha_or_opaque_modes = m_used_alpha_encoder_modes; + enc_blk_params.m_use_lum_direct_modes = m_is_lum_only; + + const bool filter_by_pca_angles_flag = (superpass_index == 1) ? enc_cfg.m_filter_by_pca_angles_flag_p2 : enc_cfg.m_filter_by_pca_angles_flag; + if (!filter_by_pca_angles_flag) + { + enc_blk_params.m_use_direct_modes = true; + enc_blk_params.m_use_base_scale_modes = true; + } + else + { + // TODO: Make selective based off edge blocks? + enc_blk_params.m_use_direct_modes = (!m_total_active_chans) || (pca_axis_angles > enc_cfg.m_use_direct_angle_thresh); + enc_blk_params.m_use_base_scale_modes = (pca_axis_angles <= enc_cfg.m_use_base_scale_angle_thresh); + } + + enc_blk_params.m_grid_hv_filtering = enc_cfg.m_grid_hv_filtering; + enc_blk_params.m_filter_horizontally_flag = m_filter_horizontally_flag; + + enc_blk_params.m_use_small_grids_only = m_low_freq_block_flag && enc_cfg.m_low_freq_block_filtering; + + enc_blk_params.m_subsets_enabled = enc_cfg.m_subsets_enabled && (!m_low_freq_block_flag || !enc_cfg.m_subsets_edge_filtering); + + enc_blk_params.m_subsets_edge_filtering = enc_cfg.m_subsets_edge_filtering; + + enc_blk_params.m_use_blue_contraction = enc_cfg.m_use_blue_contraction; + enc_blk_params.m_final_encode_try_base_ofs = enc_cfg.m_use_base_ofs; + + memcpy(enc_blk_params.m_superbucket_max_to_retain, enc_cfg.m_superbucket_max_to_retain, sizeof(enc_cfg.m_superbucket_max_to_retain)); + + memcpy(enc_blk_params.m_final_shortlist_fraction, enc_cfg.m_final_shortlist_fraction, sizeof(enc_blk_params.m_final_shortlist_fraction)); + memcpy(enc_blk_params.m_final_shortlist_min_size, enc_cfg.m_final_shortlist_min_size, sizeof(enc_cfg.m_final_shortlist_min_size)); + memcpy(enc_blk_params.m_final_shortlist_max_size, enc_cfg.m_final_shortlist_max_size, sizeof(enc_blk_params.m_final_shortlist_max_size)); + + enc_blk_params.m_part2_fraction_to_keep = enc_cfg.m_part2_fraction_to_keep; + enc_blk_params.m_part3_fraction_to_keep = enc_cfg.m_part3_fraction_to_keep; + enc_blk_params.m_base_parts2 = enc_cfg.m_base_parts2; + enc_blk_params.m_base_parts3 = enc_cfg.m_base_parts3; + enc_blk_params.m_gradient_descent_flag = enc_cfg.m_gradient_descent_flag; + enc_blk_params.m_polish_weights_flag = enc_cfg.m_polish_weights_flag; + enc_blk_params.m_qcd_enabled_flag = enc_cfg.m_qcd_enabled_flag; + enc_blk_params.m_encode_trial_early_out_thresh = enc_cfg.m_encode_trial_early_out_thresh; + enc_blk_params.m_encode_trial_subsets_early_out_thresh = enc_cfg.m_encode_trial_subsets_early_out_thresh; + enc_blk_params.m_bucket_pruning_passes = enc_cfg.m_bucket_pruning_passes; + + enc_blk_params.m_alpha_cems = m_used_alpha_encoder_modes; + + enc_blk_params.m_early_stop_wpsnr = enc_cfg.m_early_stop_wpsnr; + enc_blk_params.m_early_stop2_wpsnr = enc_cfg.m_early_stop2_wpsnr; + + enc_blk_params.m_final_encode_always_try_rgb_direct = enc_cfg.m_final_encode_always_try_rgb_direct; + + enc_blk_params.m_pDCT2F = &dct; + + // Determine DP usage + if (enc_cfg.m_force_all_dual_plane_chan_evals) + { + for (uint32_t i = 0; i < 4; i++) + enc_blk_params.m_dp_active_chans[i] = m_active_chan_flags[i]; + } + else + { + for (uint32_t i = 0; i < 3; i++) + enc_blk_params.m_dp_active_chans[i] = false; + + // Being very conservative with alpha here - always let the analytical evaluator consider it. + enc_blk_params.m_dp_active_chans[3] = pixel_stats.m_has_alpha; + + if (!enc_cfg.m_disable_rgb_dual_plane) + { + const float rg_corr = m_chan_pair_correlations[0]; + const float rb_corr = m_chan_pair_correlations[1]; + const float gb_corr = m_chan_pair_correlations[2]; + + int desired_dp_chan_rgb = -1; + + float min_p = minimum(rg_corr, rb_corr, gb_corr); + + if (min_p < enc_cfg.m_strong_dp_decorr_thresh_rgb) + { + const bool has_r = m_active_chan_flags[0], has_g = m_active_chan_flags[1]; + //const bool has_b = active_chan_flags[2]; + + uint32_t total_active_chans_rgb = 0; + for (uint32_t i = 0; i < 3; i++) + total_active_chans_rgb += m_active_chan_flags[i]; + + if (total_active_chans_rgb == 2) + { + if (!has_r) + desired_dp_chan_rgb = 1; + else if (!has_g) + desired_dp_chan_rgb = 0; + else + desired_dp_chan_rgb = 0; + } + else if (total_active_chans_rgb == 3) + { + // see if rg/rb is weakly correlated vs. gb + if ((rg_corr < gb_corr) && (rb_corr < gb_corr)) + desired_dp_chan_rgb = 0; + // see if gr/gb is weakly correlated vs. rb + else if ((rg_corr < rb_corr) && (gb_corr < rb_corr)) + desired_dp_chan_rgb = 1; + // assume b is weakest + else + desired_dp_chan_rgb = 2; + } + } + + if (desired_dp_chan_rgb != -1) + { + assert(m_active_chan_flags[desired_dp_chan_rgb]); + enc_blk_params.m_dp_active_chans[desired_dp_chan_rgb] = true; + } + } + } + + if (!enc_blk_params.m_dp_active_chans[0] && !enc_blk_params.m_dp_active_chans[1] && !enc_blk_params.m_dp_active_chans[2] && !enc_blk_params.m_dp_active_chans[3]) + { + enc_blk_params.m_use_dual_planes = false; + } + + if (superpass_index == 1) + { + enc_blk_params.m_base_parts2 = enc_cfg.m_base_parts2_p2; + enc_blk_params.m_base_parts3 = enc_cfg.m_base_parts3_p2; + enc_blk_params.m_part2_fraction_to_keep = 1; + enc_blk_params.m_part3_fraction_to_keep = 1; + + memcpy(enc_blk_params.m_superbucket_max_to_retain, enc_cfg.m_superbucket_max_to_retain_p2, sizeof(enc_cfg.m_superbucket_max_to_retain_p2)); + memcpy(enc_blk_params.m_final_shortlist_max_size, enc_cfg.m_final_shortlist_max_size_p2, sizeof(enc_cfg.m_final_shortlist_max_size_p2)); + + if (enc_cfg.m_second_pass_force_subsets_enabled) + enc_blk_params.m_subsets_enabled = true; + enc_blk_params.m_subsets_edge_filtering = false; + + if (enc_cfg.m_force_all_dp_chans_p2) + { + enc_blk_params.m_dp_active_chans[0] = m_active_chan_flags[0]; + enc_blk_params.m_dp_active_chans[1] = m_active_chan_flags[1]; + enc_blk_params.m_dp_active_chans[2] = m_active_chan_flags[2]; + enc_blk_params.m_dp_active_chans[3] = m_active_chan_flags[3]; + enc_blk_params.m_use_dual_planes = true; + + if (!enc_blk_params.m_dp_active_chans[0] && !enc_blk_params.m_dp_active_chans[1] && !enc_blk_params.m_dp_active_chans[2] && !enc_blk_params.m_dp_active_chans[3]) + { + enc_blk_params.m_use_dual_planes = false; + } + } + + enc_blk_params.m_gradient_descent_flag = true; + enc_blk_params.m_polish_weights_flag = true; + + enc_blk_params.m_use_direct_modes = true; + //enc_blk_params.m_use_base_scale_modes = true; // just leaving this alone now from the first pass, it will be disabled for good reasons now + + enc_blk_params.m_early_stop_wpsnr = enc_cfg.m_early_stop_wpsnr + 2.0f; + enc_blk_params.m_early_stop2_wpsnr = enc_cfg.m_early_stop2_wpsnr + 2.0f; + + if (enc_cfg.m_second_pass_total_weight_refine_passes) + { + m_temp_cem_enc_params = enc_cfg.m_cem_enc_params; + enc_blk_params.m_pEnc_params = &m_temp_cem_enc_params; + + m_temp_cem_enc_params.m_total_weight_refine_passes = enc_cfg.m_second_pass_total_weight_refine_passes; + m_temp_cem_enc_params.m_worst_weight_nudging_flag = true; + m_temp_cem_enc_params.m_endpoint_refinement_flag = true; + } + } + } +}; + +static bool apply_weight_grid_dct( + uint32_t block_width, uint32_t block_height, + basist::astc_ldr_t::grid_weight_dct &grid_coder, + encode_block_output& out_block, + const astc_ldr::pixel_stats_t& pixel_stats, bool recalc_block_wsse, + const ldr_astc_block_encode_image_high_level_config& enc_cfg, + astc_ldr::partitions_data* pPart_data_p2, astc_ldr::partitions_data* pPart_data_p3, + bool try_refining_endpoints) +{ + if (out_block.m_trial_mode_index < 0) + return true; + + astc_helpers::log_astc_block& log_astc_blk = out_block.m_log_blk; + + assert(!log_astc_blk.m_solid_color_flag_ldr); + + const uint32_t num_planes = (log_astc_blk.m_dual_plane ? 2 : 1); + const uint32_t total_grid_weights = log_astc_blk.m_grid_width * log_astc_blk.m_grid_height; + + basist::astc_ldr_t::fvec dct_temp; + + for (uint32_t plane_index = 0; plane_index < num_planes; plane_index++) + { + basist::astc_ldr_t::dct_syms& syms = out_block.m_packed_dct_plane_data[plane_index]; + + code_block_weights(grid_coder, enc_cfg.m_base_q, plane_index, log_astc_blk, syms, dct_temp); + + // ensure existing weights get blown away + if (num_planes == 1) + { + memset(log_astc_blk.m_weights, 0, total_grid_weights); + } + else + { + for (uint32_t i = 0; i < total_grid_weights; i++) + log_astc_blk.m_weights[i * num_planes + plane_index] = 0; + } + + // decode the actual post-DCT/quant weights + const bool status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, log_astc_blk, nullptr, nullptr, dct_temp, &syms); + + assert(status); + if (!status) + { + error_printf("grid_coder.decode_block_weights() failed!\n"); + return false; + } + } + + if (!try_refining_endpoints) + { + if (recalc_block_wsse) + { + out_block.m_sse = eval_error(block_width, block_height, log_astc_blk, pixel_stats, enc_cfg.m_cem_enc_params); + } + return true; + } + + uint64_t cur_err = eval_error(block_width, block_height, log_astc_blk, pixel_stats, enc_cfg.m_cem_enc_params); + + astc_helpers::log_astc_block new_log_astc_blk(log_astc_blk); + + bool status; + if (log_astc_blk.m_num_partitions == 1) + { + status = encode_trial_refine_only( + block_width, block_height, + pixel_stats, + new_log_astc_blk, + enc_cfg.m_cem_enc_params); + } + else + { + astc_ldr::partitions_data* pPart_data = (new_log_astc_blk.m_num_partitions == 2) ? pPart_data_p2 : pPart_data_p3; + + const uint32_t part_seed_index = new_log_astc_blk.m_partition_id; + const uint32_t part_unique_index = pPart_data->m_part_seed_to_unique_index[part_seed_index]; + const astc_ldr::partition_pattern_vec* pPat = &pPart_data->m_partition_pats[part_unique_index]; + + status = encode_trial_subsets( + block_width, block_height, + pixel_stats, + new_log_astc_blk.m_color_endpoint_modes[0], new_log_astc_blk.m_num_partitions, + new_log_astc_blk.m_partition_id, pPat, + new_log_astc_blk.m_endpoint_ise_range, new_log_astc_blk.m_weight_ise_range, + new_log_astc_blk.m_grid_width, new_log_astc_blk.m_grid_height, + enc_cfg.m_encode_trial_subsets_early_out_thresh, + new_log_astc_blk, + enc_cfg.m_cem_enc_params, + true, + enc_cfg.m_gradient_descent_flag, enc_cfg.m_polish_weights_flag, enc_cfg.m_qcd_enabled_flag, + enc_cfg.m_use_blue_contraction); + } + + if (status) + { + const uint32_t num_cem_endpoint_vals = astc_helpers::get_num_cem_values(new_log_astc_blk.m_color_endpoint_modes[0]); + const uint32_t total_endpoint_vals = num_cem_endpoint_vals * new_log_astc_blk.m_num_partitions; + + const bool endpoints_differ = memcmp(log_astc_blk.m_endpoints, new_log_astc_blk.m_endpoints, total_endpoint_vals) != 0; + + if (endpoints_differ) + { + for (uint32_t plane_index = 0; plane_index < num_planes; plane_index++) + { + basist::astc_ldr_t::dct_syms& syms = out_block.m_packed_dct_plane_data[plane_index]; + + // decode the actual post-DCT/quant weights + status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, new_log_astc_blk, nullptr, nullptr, dct_temp, &syms); + assert(status); + + if (!status) + { + error_printf("grid_coder.decode_block_weights() failed!\n"); + return false; + } + } + + uint64_t new_err = eval_error(block_width, block_height, new_log_astc_blk, pixel_stats, enc_cfg.m_cem_enc_params); + + if (new_err < cur_err) + { + cur_err = new_err; + log_astc_blk = new_log_astc_blk; + } + } + } + + if (recalc_block_wsse) + out_block.m_sse = cur_err; + + return true; +} + +static void filter_block( + uint32_t block_width, uint32_t block_height, + uint32_t grid_width, uint32_t grid_height, + const astc_ldr::pixel_stats_t& pixel_stats, + color_rgba *pUpsampled_block) +{ + const uint32_t num_block_samples = block_width * block_height; + const uint32_t num_grid_samples = grid_width * grid_height; + + const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, grid_width, grid_height); + + const basisu::vector& d = pGrid_data->m_downsample_matrix; // rows=output num_grid_sampless, cols=input num_block_samples (texels) + const basisu::vector2D& u = pGrid_data->m_upsample_matrix; // rows=output num_block_samples (texels), cols=input num_grid_samples + + for (uint32_t c = 0; c < 4; c++) + { + float downsampled_block[astc_helpers::MAX_BLOCK_PIXELS]; // num_grid_samples + + if ((c == 3) && (!pixel_stats.m_has_alpha)) + { + for (uint32_t i = 0; i < num_block_samples; i++) + pUpsampled_block[i].a = 255; + break; + } + + for (uint32_t g = 0; g < num_grid_samples; g++) + { + float sum = 0; + for (uint32_t i = 0; i < num_block_samples; i++) + sum += (float)pixel_stats.m_pixels[i][c] * d[g * num_block_samples + i]; + + downsampled_block[g] = sum; + } + + for (uint32_t k = 0; k < num_block_samples; k++) + { + float sum = 0; + for (uint32_t i = 0; i < num_grid_samples; i++) + sum += (float)downsampled_block[i] * u.at_row_col(k, i); + + pUpsampled_block[k][c] = (uint8_t)clamp(basisu::fast_roundf_pos_int(sum), 0, 255); + } + } // c +} + +// very slow +[[maybe_unused]] static int find_tm_index(const basisu::vector& encoder_trial_modes, const astc_helpers::log_astc_block& log_blk) +{ + assert(astc_helpers::is_block_xuastc_ldr(log_blk)); + + for (uint32_t i = 0; i < encoder_trial_modes.size(); i++) + { + const basist::astc_ldr_t::trial_mode& tm = encoder_trial_modes[i]; + + if ((tm.m_cem == log_blk.m_color_endpoint_modes[0]) && + (tm.m_grid_width == log_blk.m_grid_width) && (tm.m_grid_height == log_blk.m_grid_height) && + (tm.m_num_parts == log_blk.m_num_partitions) && + (tm.m_weight_ise_range == log_blk.m_weight_ise_range) && + (tm.m_endpoint_ise_range == log_blk.m_endpoint_ise_range)) + { + const bool tm_dp_flag = (tm.m_ccs_index >= 0); + + if (tm_dp_flag != log_blk.m_dual_plane) + continue; + + if (tm_dp_flag) + { + if (tm.m_ccs_index != log_blk.m_color_component_selector) + continue; + } + + return i; + } + } + + return -1; +} + +static bool encode_block_to_dc_latent( + uint32_t block_width, uint32_t block_height, + const astc_ldr::pixel_stats_t& pixel_stats, + basisu::vector& out_blocks, + const ldr_astc_block_encode_image_high_level_config& enc_cfg, + astc_ldr::partitions_data* pPart_data_p2, astc_ldr::partitions_data* pPart_data_p3, + const basisu::vector& encoder_trial_modes, + uint64_vec &best_2subset_seed_ids, // seed ID's in low 10 bits + uint64_vec& best_3subset_seed_ids) // seed ID's in low 10 bits +{ + BASISU_NOTE_UNUSED(encoder_trial_modes); + + best_2subset_seed_ids.resize(0); + best_2subset_seed_ids.reserve(pPart_data_p2->m_total_unique_patterns); + + best_3subset_seed_ids.resize(0); + best_3subset_seed_ids.reserve(pPart_data_p3->m_total_unique_patterns); + + const uint32_t block_size_index = astc_helpers::get_block_size_index(block_width, block_height); + + const uint32_t total_block_pixels = block_width * block_height; + + astc_helpers::log_astc_block log_blk; + log_blk.clear(); + log_blk.m_grid_width = 3; + log_blk.m_grid_height = 2; + log_blk.m_color_endpoint_modes[0] = 6; + log_blk.m_color_endpoint_modes[1] = 6; + log_blk.m_weight_ise_range = astc_helpers::BISE_16_LEVELS; + + for (uint32_t subsets = 2; subsets <= 3; subsets++) + { + encode_block_output& new_output_blk = *out_blocks.enlarge(1); + new_output_blk.clear(); + + astc_helpers::log_astc_block &best_log_blk = new_output_blk.m_log_blk; + uint64_t best_err = UINT64_MAX; + + const astc_ldr::partitions_data* pPat_data = (subsets == 3) ? pPart_data_p3 : pPart_data_p2; + + log_blk.m_num_partitions = (uint8_t)subsets; + log_blk.m_endpoint_ise_range = (uint8_t)((subsets == 3) ? astc_helpers::BISE_64_LEVELS : astc_helpers::BISE_256_LEVELS); + log_blk.m_color_endpoint_modes[2] = (subsets == 3) ? 6 : 0; + + //const auto& weight_quant_tab = astc_helpers::g_dequant_tables.get_weight_tab(log_blk.m_weight_ise_range).m_val_to_ise; + const auto& endpoint_quant_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(log_blk.m_endpoint_ise_range).m_val_to_ise; + + for (uint32_t unique_part_iter = 0; unique_part_iter < pPat_data->m_total_unique_patterns; unique_part_iter++) + { + const uint32_t part_seed = pPat_data->m_unique_index_to_part_seed[unique_part_iter]; + + log_blk.m_partition_id = basisu::safe_cast_uint16(part_seed); + + vec4F sums[3] = { vec4F(0.0f), vec4F(0.0f), vec4F(0.0f) }; + uint32_t num[3] = { 0, 0, 0 }; + + [[maybe_unused]] uint32_t p_hist[3] = { 0, 0, 0 }; + + for (uint32_t y = 0; y < block_height; y++) + { + for (uint32_t x = 0; x < block_width; x++) + { + uint32_t p = astc_helpers::get_precomputed_texel_partition(block_width, block_height, part_seed, x, y, log_blk.m_num_partitions); + + sums[p][0] += pixel_stats.m_pixels_f[x + y * block_width][0]; + sums[p][1] += pixel_stats.m_pixels_f[x + y * block_width][1]; + sums[p][2] += pixel_stats.m_pixels_f[x + y * block_width][2]; + + num[p]++; + } // x + } // y + + for (uint32_t p = 0; p < subsets; p++) + { + assert(num[p]); + + sums[p] /= (float)num[p]; + + for (uint32_t c = 0; c < 3; c++) + log_blk.m_endpoints[p * astc_helpers::NUM_MODE6_ENDPOINTS + c] = endpoint_quant_tab[clamp((int)std::round(255.0f * sums[p][c]), 0, 255)]; + + log_blk.m_endpoints[p * astc_helpers::NUM_MODE6_ENDPOINTS + 3] = endpoint_quant_tab[(subsets == 3) ? 248 : 255]; + } + + uint64_t best_k_err = UINT64_MAX; + + for (uint32_t k = (subsets == 3) ? 13 : 15; k < 16; k++) + { + for (uint32_t i = 0; i < 6; i++) + log_blk.m_weights[i] = (uint8_t)k; + + color_rgba unpacked_pixels_astc[astc_helpers::MAX_BLOCK_PIXELS]; + bool astc_status = astc_helpers::decode_block_xuastc_ldr(log_blk, unpacked_pixels_astc, block_width, block_height, enc_cfg.m_cem_enc_params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); + assert(astc_status); + if (!astc_status) + return false; + + uint64_t total_err = 0; + for (uint32_t i = 0; i < total_block_pixels; i++) + total_err += weighted_color_error(pixel_stats.m_pixels[i], unpacked_pixels_astc[i], enc_cfg.m_cem_enc_params); + + best_k_err = minimum(best_k_err, total_err); + + if (total_err < best_err) + { + best_err = total_err; + best_log_blk = log_blk; + } + } // k + + if (subsets == 2) + { + best_2subset_seed_ids.push_back((best_k_err << 10) | log_blk.m_partition_id); + } + else + { + best_3subset_seed_ids.push_back((best_k_err << 10) | log_blk.m_partition_id); + } + + } // part_seed + + best_2subset_seed_ids.sort(); + best_3subset_seed_ids.sort(); + + // XUASTC LDR trial mode indices + static const uint32_t s_tm_index2[14] = { 675, 675, 974, 974, 1263, 974, 1263, 974, 1263, 1747, 1747, 2131, 2131, 2425 }; + static const uint32_t s_tm_index3[14] = { 679, 679, 978, 978, 1267, 978, 1267, 978, 1267, 1751, 1751, 2135, 2135, 2429 }; + + new_output_blk.m_trial_mode_index = basisu::safe_cast_int16((best_log_blk.m_num_partitions == 2) ? s_tm_index2[block_size_index] : s_tm_index3[block_size_index]); + + assert(new_output_blk.m_trial_mode_index == find_tm_index(encoder_trial_modes, best_log_blk)); + + new_output_blk.m_sse = best_err; + new_output_blk.m_blur_id = BLUR_ID_DC_LATENT_BASE + best_log_blk.m_num_partitions - 2; + + } // subsets + + return true; +} + +static bool encode_block_to_linear_latent( + uint32_t num_patterns_to_try, + uint32_t block_width, uint32_t block_height, + const astc_ldr::pixel_stats_t& pixel_stats, + basisu::vector& out_blocks, + const ldr_astc_block_encode_image_high_level_config& enc_cfg, + astc_ldr::partitions_data* pPart_data_p2, astc_ldr::partitions_data* pPart_data_p3, + const basisu::vector& encoder_trial_modes, + uint64_vec& best_2subset_seed_ids, + uint64_vec& best_3subset_seed_ids) +{ + BASISU_NOTE_UNUSED(encoder_trial_modes); + BASISU_NOTE_UNUSED(pPart_data_p2); + BASISU_NOTE_UNUSED(pPart_data_p3); + + static const uint32_t s_tm2_index_02[14] = { 717, 717, 1016, 1016, 1305, 1016, 1305, 1016, 1305, 1789, 1789, 2173, 2173, 2467 }; + static const uint32_t s_tm2_index_13[14] = { 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215 }; + + static const uint32_t s_tm3_index_02[14] = { 721, 721, 1020, 1020, 1309, 1020, 1309, 1020, 1309, 1793, 1793, 2177, 2177, 2471 }; + static const uint32_t s_tm3_index_13[14] = { 219, 219, 219, 219, 219, 219, 219, 219, 219, 219, 219, 219, 219, 219 }; + + const uint32_t block_size_index = astc_helpers::get_block_size_index(block_width, block_height); + + const uint32_t total_grid_samples = 6; + + const uint32_t weight_ise_range = astc_helpers::BISE_16_LEVELS; + + //const auto& weight_quant_tab = astc_helpers::g_dequant_tables.get_weight_tab(weight_ise_range).m_val_to_ise; + const auto& weight_dequant_tab = astc_helpers::g_dequant_tables.get_weight_tab(weight_ise_range).m_ISE_to_val; + + const uint32_t NUM_WEIGHT_GRIDS = 4 * 4; + + static const uint32_t grid_width[NUM_WEIGHT_GRIDS] = { 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2 }, grid_height[NUM_WEIGHT_GRIDS] = { 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3 }; + + static const uint8_t weight_grids[NUM_WEIGHT_GRIDS][3 * 2] = + { + { + 0, 0, 0, + 15, 15, 15 + }, + + { + 0, 15, + 0, 15, + 0, 15 + }, + + { + 15, 15, 15, + 0, 0, 0 + }, + + { + 15, 0, + 15, 0, + 15, 0 + }, + + // set 2 + { + 0, 0, 0, + 4, 8, 15 + }, + + { + 0, 4, + 0, 8, + 0, 15 + }, + + { + 15, 8, 4, + 0, 0, 0 + }, + + { + 15, 0, + 8, 0, + 4, 0 + }, + + // set 3 + { + 2, 4, 6, + 9, 11, 13 + }, + + { + 2, 9, + 4, 11, + 6, 13 + }, + + { + 9, 11, 13, + 2, 4, 6 + }, + + { + 9, 2, + 11, 4, + 13, 6 + }, + + // set 4 + { + 0, 7, 0, + 7, 15, 7 + }, + + { + 0, 7, + 7, 15, + 0, 7 + }, + + { + 15, 7, 15, + 7, 0, 7 + }, + + { + 7, 0, + 15, 7, + 7, 0 + } + }; + + // TODO + uint8_t dequantized_grid_weights[NUM_WEIGHT_GRIDS][astc_helpers::MAX_BLOCK_PIXELS]; + uint8_t dequantized_block_weights[NUM_WEIGHT_GRIDS][astc_helpers::MAX_BLOCK_PIXELS]; + + for (uint32_t g = 0; g < NUM_WEIGHT_GRIDS; g++) + { + for (uint32_t i = 0; i < total_grid_samples; i++) + dequantized_grid_weights[g][i] = weight_dequant_tab[weight_grids[g][i]]; + + astc_helpers::upsample_weight_grid( + block_width, block_height, // destination/to dimension + grid_width[g], grid_height[g], // source/from dimension + dequantized_grid_weights[g], // these are dequantized [0,64] weights, NOT ISE symbols, [wy][wx] + dequantized_block_weights[g]); // [by][bx] + } + + astc_ldr::cem_encode_params cem_enc_params; + cem_enc_params.init(); + cem_enc_params.m_decode_mode_srgb = enc_cfg.m_cem_enc_params.m_decode_mode_srgb; + cem_enc_params.m_max_ls_passes = 1; + + for (uint32_t subsets = 2; subsets <= 3; subsets++) + //const uint32_t subsets = 2; + { + //const astc_ldr::partitions_data* pPat_data = (subsets == 3) ? pPart_data_p3 : pPart_data_p2; + + for (uint32_t grid_index = 0; grid_index < NUM_WEIGHT_GRIDS; grid_index++) + { + astc_helpers::log_astc_block log_blk; + log_blk.clear(); + + log_blk.m_grid_width = (uint8_t)grid_width[grid_index]; + log_blk.m_grid_height = (uint8_t)grid_height[grid_index]; + log_blk.m_num_partitions = (uint8_t)subsets; + log_blk.m_color_endpoint_modes[0] = 8; + log_blk.m_color_endpoint_modes[1] = 8; + if (subsets == 3) + log_blk.m_color_endpoint_modes[2] = 8; + log_blk.m_endpoint_ise_range = (uint8_t)((subsets == 3) ? astc_helpers::BISE_16_LEVELS : astc_helpers::BISE_64_LEVELS); + log_blk.m_weight_ise_range = (uint8_t)weight_ise_range; + memcpy(log_blk.m_weights, weight_grids[grid_index], total_grid_samples); + + //const auto& endpoint_quant_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(log_blk.m_endpoint_ise_range).m_val_to_ise; + + uint64_t best_err = UINT64_MAX; + astc_helpers::log_astc_block best_log_blk; + best_log_blk.clear(); + + //for (uint32_t unique_part_iter = 0; unique_part_iter < pPat_data->m_total_unique_patterns; unique_part_iter++) + for (uint32_t pat_index = 0; pat_index < num_patterns_to_try; pat_index++) + { + //const uint32_t part_seed = pPat_data->m_unique_index_to_part_seed[unique_part_iter]; + const uint32_t part_seed = ((subsets == 2) ? best_2subset_seed_ids[pat_index] : best_3subset_seed_ids[pat_index]) & 1023; + + log_blk.m_partition_id = basisu::safe_cast_uint16(part_seed); + + color_rgba subset_pixels[3][astc_helpers::MAX_BLOCK_PIXELS]; + uint8_t subset_weights[3][astc_helpers::MAX_BLOCK_PIXELS]; + uint32_t num_subset_pixels[3] = { 0, 0, 0 }; + + astc_ldr::pixel_stats_t stats[3]; + + for (uint32_t y = 0; y < block_height; y++) + { + for (uint32_t x = 0; x < block_width; x++) + { + uint32_t s = astc_helpers::get_precomputed_texel_partition(block_width, block_height, part_seed, x, y, log_blk.m_num_partitions); + + const uint32_t n = num_subset_pixels[s]; + + subset_pixels[s][n] = pixel_stats.m_pixels[y * block_width + x]; + subset_weights[s][n] = dequantized_block_weights[grid_index][y * block_width + x]; + + num_subset_pixels[s] = n + 1; + + } // x + } // y + + uint64_t total_err = 0; + for (uint32_t s = 0; s < subsets; s++) + { + stats[s].init(num_subset_pixels[s], subset_pixels[s]); + + cem_enc_params.m_pForced_weight_vals0 = subset_weights[s]; + + uint8_t temp_weights[astc_helpers::MAX_BLOCK_PIXELS]; + + uint64_t subset_err = astc_ldr::cem_encode_pixels(8, -1, stats[s], cem_enc_params, log_blk.m_endpoint_ise_range, astc_helpers::BISE_64_LEVELS, + &log_blk.m_endpoints[s * astc_helpers::NUM_MODE8_ENDPOINTS], temp_weights, nullptr, UINT64_MAX, true, nullptr); + + if (subset_err == UINT64_MAX) + { + total_err = UINT64_MAX; + break; + } + + total_err += subset_err; + + if (total_err > best_err) + break; + } + + if (total_err < best_err) + { + best_err = total_err; + best_log_blk = log_blk; + } + + } // part_seed + + if (best_err != UINT64_MAX) + { + encode_block_output& new_output_blk = *out_blocks.enlarge(1); + + new_output_blk.m_log_blk = best_log_blk; + + uint32_t tm_index; + if (subsets == 3) + { + if (((grid_index & 3) == 0) || ((grid_index & 3) == 2)) + tm_index = s_tm3_index_02[block_size_index]; + else + tm_index = s_tm3_index_13[block_size_index]; + } + else + { + if (((grid_index & 3) == 0) || ((grid_index & 3) == 2)) + tm_index = s_tm2_index_02[block_size_index]; + else + tm_index = s_tm2_index_13[block_size_index]; + } + + //fmt_printf("block_size_index: {}, grid_index: {}, tm: {}\n", block_size_index, grid_index & 3, find_tm_index(encoder_trial_modes, best_log_blk)); + assert((int)tm_index == find_tm_index(encoder_trial_modes, best_log_blk)); + + new_output_blk.m_trial_mode_index = basisu::safe_cast_int16(tm_index); + new_output_blk.m_sse = best_err; + new_output_blk.m_blur_id = basisu::safe_cast_uint16(BLUR_ID_AC_LATENT_BASE + grid_index + (subsets - 2) * NUM_WEIGHT_GRIDS);// pat_index; + } + + } // grid + + } // subsets + + return true; +} + +static void enforce_max_candidate_limit(ldr_astc_block_encode_image_output::block_info& block_info_out, uint32_t max_candidates) +{ + assert(max_candidates >= 1); + + if (block_info_out.m_out_blocks.size() <= max_candidates) + return; + + uint_vec sorted_indices(block_info_out.m_out_blocks.size()); + for (uint32_t i = 0; i < block_info_out.m_out_blocks.size(); i++) + sorted_indices[i] = i; + + std::partial_sort(sorted_indices.begin(), sorted_indices.begin() + max_candidates, sorted_indices.end(), + [&sorted_indices, &block_info_out](const uint32_t a, const uint32_t b) + { + BASISU_NOTE_UNUSED(sorted_indices); // comparator only reads block_info_out + if (block_info_out.m_out_blocks[a].m_sse < block_info_out.m_out_blocks[b].m_sse) + { + return true; + } + else if (block_info_out.m_out_blocks[a].m_sse == block_info_out.m_out_blocks[b].m_sse) + { + if (a < b) + return true; + } + + return false; + } + ); + + basisu::vector new_blocks; + + int new_packed_out_block_index = -1; + + for (uint32_t i = 0; i < max_candidates; i++) + if (sorted_indices[i] == block_info_out.m_packed_out_block_index) + new_packed_out_block_index = i; + + if (new_packed_out_block_index < 0) + { + new_packed_out_block_index = 0; + + new_blocks.resize(max_candidates + 1); // one more than requested, not the end of the world + + new_blocks[0] = block_info_out.m_out_blocks[block_info_out.m_packed_out_block_index]; + + for (uint32_t i = 0; i < max_candidates; i++) + new_blocks[1 + i] = block_info_out.m_out_blocks[sorted_indices[i]]; + } + else + { + new_blocks.resize(max_candidates); + + for (uint32_t i = 0; i < max_candidates; i++) + new_blocks[i] = block_info_out.m_out_blocks[sorted_indices[i]]; + } + + block_info_out.m_out_blocks.swap(new_blocks); + block_info_out.m_packed_out_block_index = new_packed_out_block_index; +} + +static void display_candidate_statistics(const ldr_astc_block_encode_image_output &enc_out) +{ + const auto& block_info = enc_out.m_image_block_info; + + running_stat rs; + + for (uint32_t by = 0; by < block_info.get_height(); by++) + { + for (uint32_t bx = 0; bx < block_info.get_width(); bx++) + { + rs.push((double)block_info(bx, by).m_out_blocks.size()); + + } // bx + } // by + + fmt_debug_printf("Encoder output candidate stats: total: {}, min: {}, max: {}, avg: {3.3}\n", rs.get_total(), rs.get_min(), rs.get_max(), rs.get_mean()); +} + +static bool ldr_astc_block_encode_image( const image& orig_img, const ldr_astc_block_encode_image_high_level_config& enc_cfg, - ldr_astc_block_encode_image_output& enc_out) + ldr_astc_block_encode_image_output& enc_out, + uint32_t max_candidate_limit) { if (enc_cfg.m_debug_output) fmt_debug_printf("ldr_astc_block_encode_image:\n"); @@ -5371,51 +7674,96 @@ bool ldr_astc_block_encode_image( if (enc_cfg.m_debug_output) { - fmt_debug_printf("ASTC base bitrate: {3.3} bpp\n", 128.0f / (float)(enc_cfg.m_block_width * enc_cfg.m_block_height)); + fmt_debug_printf("\nASTC base bitrate: {3.3} bpp\n", 128.0f / (float)(enc_cfg.m_block_width * enc_cfg.m_block_height)); fmt_debug_printf("ASTC block size: {}x{}\n", enc_cfg.m_block_width, enc_cfg.m_block_height); + + fmt_debug_printf("Image has alpha: {}\n", orig_img.has_alpha()); + + fmt_debug_printf("max_candidate_limit: {}\n", max_candidate_limit);; } - if (enc_cfg.m_debug_output) - fmt_debug_printf("Image has alpha: {}\n", orig_img.has_alpha()); - + // TODO: The transcoder already creates all this stuff for each block size. astc_ldr::partitions_data* pPart_data_p2 = &enc_out.m_part_data_p2; - pPart_data_p2->init(2, enc_cfg.m_block_width, enc_cfg.m_block_height); + pPart_data_p2->init(2, enc_cfg.m_block_width, enc_cfg.m_block_height, BASISU_USE_LSH2 == 0, BASISU_USE_LSH2 != 0); astc_ldr::partitions_data* pPart_data_p3 = &enc_out.m_part_data_p3; - pPart_data_p3->init(3, enc_cfg.m_block_width, enc_cfg.m_block_height); + pPart_data_p3->init(3, enc_cfg.m_block_width, enc_cfg.m_block_height, BASISU_USE_LSH3 == 0, BASISU_USE_LSH3 != 0); - // blurring coefficients - const float bw0 = 1.15f; - const float bw1 = 1.25f, bw1_a = 1.0f; - const float bw2 = 1.25f; - // TODO: Make this optional/tune this, add only 2 level blurring support - image orig_img_blurred2, orig_img_blurred3, orig_img_blurred4, orig_img_blurred5; + // TODO: Make configurable + // (TOTAL_BLURRED_IMAGES is defined at module scope near the top of this file.) + + image orig_img_blurred[TOTAL_BLURRED_IMAGES]; + uint32_t total_blur_encodes_p1 = 0; + uint32_t total_blurred_blocks_p1[2048] = { }; + uint32_t total_blur_encodes_p2 = 0; + uint32_t total_blurred_blocks_p2[2048] = { }; - if ((enc_cfg.m_blurring_enabled) || (enc_cfg.m_blurring_enabled_p2)) + if ((enc_cfg.m_blurring_enabled_p1) || (enc_cfg.m_blurring_enabled_p2)) { - orig_img_blurred2.resize(orig_img.get_width(), orig_img.get_height()); - orig_img_blurred3.resize(orig_img.get_width(), orig_img.get_height()); - orig_img_blurred4.resize(orig_img.get_width(), orig_img.get_height()); - orig_img_blurred5.resize(orig_img.get_width(), orig_img.get_height()); + for (uint32_t i = 0; i < TOTAL_BLURRED_IMAGES; i++) + orig_img_blurred[i].resize(orig_img.get_width(), orig_img.get_height()); - image_resample(orig_img, orig_img_blurred2, true, "gaussian", bw0); - image_resample(orig_img, orig_img_blurred3, true, "gaussian", bw1, false, 0, 4, bw1_a); - image_resample(orig_img, orig_img_blurred4, true, "gaussian", bw1_a, false, 0, 4, bw1); - image_resample(orig_img, orig_img_blurred5, true, "gaussian", bw2, false); + const bool srgb_flag = false; + + //if (TOTAL_BLURRED_IMAGES > 3) + if(0) + { + for (uint32_t k = 0; k < TOTAL_BLURRED_IMAGES; k++) + { + int i = k / 3; + int j = k % 3; + + const float f = (float)i / (float)basisu::maximum(1, ((TOTAL_BLURRED_IMAGES - 1) / 3)); + + float w; + if (TOTAL_BLURRED_IMAGES <= 6) + w = lerp(1.1f, 2.0f, f); + else + w = lerp(1.01f, 2.2f, f); + + float sx = 1.0f, sy = 1.0f; + + switch (j) + { + case 0: + sx = sy = w; break; + case 1: + sx = w; break; + case 2: + default: + sy = w; break; + } + + image_resample(orig_img, orig_img_blurred[k], srgb_flag, "gaussian", sx, false, 0, 4, sy); + } + } + else + { + for (uint32_t i = 0; i < TOTAL_BLURRED_IMAGES; i++) + { + const float f = (float)i / (float)(TOTAL_BLURRED_IMAGES - 1); + + float w; + + w = lerp(1.1f, 2.0f, f); + + float sx = w, sy = w; + + image_resample(orig_img, orig_img_blurred[i], srgb_flag, "gaussian", sx, false, 0, 4, sy); + } + } } if (enc_cfg.m_debug_images) { save_png(enc_cfg.m_debug_file_prefix + "dbg_astc_ldr_orig_img.png", orig_img); - if ((enc_cfg.m_blurring_enabled) || (enc_cfg.m_blurring_enabled_p2)) + if ((enc_cfg.m_blurring_enabled_p1) || (enc_cfg.m_blurring_enabled_p2)) { - save_png(enc_cfg.m_debug_file_prefix + "vis_orig_blurred2.png", orig_img_blurred2); - save_png(enc_cfg.m_debug_file_prefix + "vis_orig_blurred3.png", orig_img_blurred3); - save_png(enc_cfg.m_debug_file_prefix + "vis_orig_blurred4.png", orig_img_blurred4); - save_png(enc_cfg.m_debug_file_prefix + "vis_orig_blurred5.png", orig_img_blurred5); + for (uint32_t i = 0; i < TOTAL_BLURRED_IMAGES; i++) + save_png(enc_cfg.m_debug_file_prefix + fmt_string("vis_orig_blurred{}.png", i), orig_img_blurred[i]); } } @@ -5510,13 +7858,7 @@ bool ldr_astc_block_encode_image( uint32_t total_shortlist_candidates = 0; uint32_t total_full_encodes_pass1 = 0; uint32_t total_full_encodes_pass2 = 0; - - uint32_t total_blur_encodes = 0; - uint32_t total_blurred_blocks1 = 0; - uint32_t total_blurred_blocks2 = 0; - uint32_t total_blurred_blocks3 = 0; - uint32_t total_blurred_blocks4 = 0; - + basist::astc_ldr_t::dct2f dct; dct.init(enc_cfg.m_block_height, enc_cfg.m_block_width); @@ -5594,6 +7936,10 @@ bool ldr_astc_block_encode_image( uint32_t total_blocks_to_recompress = 0; + //uint32_t max_candidates = 64; + //if (total_blocks >= (512 * 512)) + // max_candidates = 16; // save memory + for (uint32_t superpass_index = 0; superpass_index < total_superpasses; superpass_index++) { if (superpass_index == 1) @@ -5619,16 +7965,19 @@ bool ldr_astc_block_encode_image( bx, by, //num_blocks_x, num_blocks_y, total_blocks, block_width, block_height, total_block_pixels, &packed_blocks, &global_mutex, - &orig_img, &orig_img_sobel_xy, &orig_img_blurred2, &orig_img_blurred3, &orig_img_blurred4, &orig_img_blurred5, + &orig_img, &orig_img_sobel_xy, &orig_img_blurred, max_candidate_limit, &enc_cfg, &encoder_failed_flag, pPart_data_p2, pPart_data_p3, &total_blocks_done, &total_superbuckets_created, &total_buckets_created, &total_surrogate_encodes, &total_full_encodes, &total_shortlist_candidates, &encoder_trial_modes, - &total_blur_encodes, &total_blurred_blocks1, + &total_blur_encodes_p1, &total_blurred_blocks_p1, &total_blur_encodes_p2, &total_blurred_blocks_p2, &total_full_encodes_pass1, &total_full_encodes_pass2, &dct, &vis_dct_low_freq_block, &encoder_pool, &grid_coder, &grouped_encoder_trial_modes, &enc_out, &output_block_devel_info, &total_void_extent_blocks_skipped, &superpass2_recompress_block_flags, &total_blocks_to_recompress, &last_printed_progress_val] { + //if ((bx == 0x35) && (by == 0xec)) + // printf("."); + if (encoder_failed_flag) return; @@ -5743,6 +8092,7 @@ bool ldr_astc_block_encode_image( neighbor_log_blk.m_color_endpoint_modes[0], neighbor_log_blk.m_num_partitions, neighbor_log_blk.m_partition_id, pPat, neighbor_log_blk.m_endpoint_ise_range, neighbor_log_blk.m_weight_ise_range, neighbor_log_blk.m_grid_width, neighbor_log_blk.m_grid_height, + enc_cfg.m_encode_trial_subsets_early_out_thresh, new_log_block, enc_cfg.m_cem_enc_params, refine_only_flag, @@ -5758,6 +8108,7 @@ bool ldr_astc_block_encode_image( neighbor_log_blk.m_dual_plane, neighbor_log_blk.m_dual_plane ? neighbor_log_blk.m_color_component_selector : -1, neighbor_log_blk.m_endpoint_ise_range, neighbor_log_blk.m_weight_ise_range, neighbor_log_blk.m_grid_width, neighbor_log_blk.m_grid_height, + enc_cfg.m_encode_trial_early_out_thresh, new_log_block, enc_cfg.m_cem_enc_params, enc_cfg.m_gradient_descent_flag, enc_cfg.m_polish_weights_flag, enc_cfg.m_qcd_enabled_flag, @@ -5780,30 +8131,24 @@ bool ldr_astc_block_encode_image( if (enc_cfg.m_use_dct) { - const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, new_log_block.m_grid_width, new_log_block.m_grid_height); + //const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, new_log_block.m_grid_width, new_log_block.m_grid_height); const uint32_t num_planes = (new_log_block.m_dual_plane ? 2 : 1); + basist::astc_ldr_t::fvec dct_temp; + for (uint32_t plane_index = 0; plane_index < num_planes; plane_index++) { - bitwise_coder c; - basist::astc_ldr_t::dct_syms syms; - code_block_weights(grid_coder, enc_cfg.m_base_q, plane_index, new_log_block, pGrid_data, c, syms); - - new_output_blk.m_packed_dct_plane_data[plane_index] = syms; - - c.flush(); - - basist::bitwise_decoder d; - d.init(c.get_bytes().data(), c.get_bytes().size_u32()); - + basist::astc_ldr_t::dct_syms &syms = new_output_blk.m_packed_dct_plane_data[plane_index]; + + code_block_weights(grid_coder, enc_cfg.m_base_q, plane_index, new_log_block, syms, dct_temp); + // ensure existing weights get blown away for (uint32_t i = 0; i < (uint32_t)(new_log_block.m_grid_width * new_log_block.m_grid_height); i++) new_log_block.m_weights[i * num_planes + plane_index] = 0; - - basist::astc_ldr_t::fvec dct_temp; - bool dec_status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, new_log_block, &d, pGrid_data, nullptr, dct_temp, nullptr); - + + bool dec_status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, new_log_block, nullptr, nullptr, dct_temp, &syms); + assert(dec_status); if (!dec_status) { @@ -5871,29 +8216,23 @@ bool ldr_astc_block_encode_image( if (enc_cfg.m_use_dct) { - const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, new_log_block.m_grid_width, new_log_block.m_grid_height); + //const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, new_log_block.m_grid_width, new_log_block.m_grid_height); const uint32_t num_planes = (new_log_block.m_dual_plane ? 2 : 1); + basist::astc_ldr_t::fvec dct_temp; + for (uint32_t plane_index = 0; plane_index < num_planes; plane_index++) { - bitwise_coder c; - basist::astc_ldr_t::dct_syms syms; - code_block_weights(grid_coder, enc_cfg.m_base_q, plane_index, new_log_block, pGrid_data, c, syms); + basist::astc_ldr_t::dct_syms &syms = new_output_blk.m_packed_dct_plane_data[plane_index]; + + code_block_weights(grid_coder, enc_cfg.m_base_q, plane_index, new_log_block, syms, dct_temp); - new_output_blk.m_packed_dct_plane_data[plane_index] = syms; - - c.flush(); - - basist::bitwise_decoder d; - d.init(c.get_bytes().data(), c.get_bytes().size_u32()); - - // ensure existing weights get blown away + // ensure existing weights get blown away for (uint32_t i = 0; i < (uint32_t)(new_log_block.m_grid_width * new_log_block.m_grid_height); i++) new_log_block.m_weights[i * num_planes + plane_index] = 0; - - basist::astc_ldr_t::fvec dct_temp; - bool dec_status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, new_log_block, &d, pGrid_data, nullptr, dct_temp, nullptr); + + bool dec_status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, new_log_block, nullptr, nullptr, dct_temp, &syms); assert(dec_status); if (!dec_status) @@ -5993,343 +8332,23 @@ bool ldr_astc_block_encode_image( return; } - - float max_std_dev = 0.0f; - for (uint32_t i = 0; i < 4; i++) - max_std_dev = maximum(max_std_dev, pixel_stats.m_rgba_stats[i].m_std_dev); - - bool is_lum_only = true; - - for (uint32_t y = 0; y < block_height; y++) - { - for (uint32_t x = 0; x < block_width; x++) - { - const color_rgba& c = pixel_stats.m_pixels[x + y * block_width]; - bool is_lum_texel = (c.r == c.g) && (c.r == c.b); - if (!is_lum_texel) - { - is_lum_only = false; - break; - } - } - if (is_lum_only) - break; - } - - basisu::vector block_dct_energy(total_block_pixels); - - bool filter_horizontally_flag = false; - bool low_freq_block_flag = 0; - - { - basisu::vector block_floats(total_block_pixels); - basisu::vector block_dct(total_block_pixels); - basist::astc_ldr_t::fvec work; - - for (uint32_t c = 0; c < 4; c++) - { - for (uint32_t i = 0; i < total_block_pixels; i++) - block_floats[i] = pixel_stats.m_pixels_f[i][c]; - - dct.forward(block_floats.data(), block_dct.data(), work); - - for (uint32_t y = 0; y < block_height; y++) - for (uint32_t x = 0; x < block_width; x++) - block_dct_energy[x + y * block_width] += (float)enc_cfg.m_cem_enc_params.m_comp_weights[c] * squaref(block_dct[x + y * block_width]); - - } // c - - // Wipe DC - block_dct_energy[0] = 0.0f; - - float tot_energy = compute_preserved_dct_energy(block_width, block_height, block_dct_energy.get_ptr(), block_width, block_height); - - float h_energy_lost = compute_lost_dct_energy(block_width, block_height, block_dct_energy.get_ptr(), block_width / 2, block_height); - float v_energy_lost = compute_lost_dct_energy(block_width, block_height, block_dct_energy.get_ptr(), block_width, block_height / 2); - - filter_horizontally_flag = h_energy_lost < v_energy_lost; - - float hv2_lost_energy_fract = compute_lost_dct_energy(block_width, block_height, block_dct_energy.get_ptr(), 2, 2); - if (tot_energy) - hv2_lost_energy_fract /= tot_energy; - - if ((hv2_lost_energy_fract < .03f) || (max_std_dev < (1.0f / 255.0f))) - low_freq_block_flag = true; - } - - if (enc_cfg.m_debug_images) - vis_dct_low_freq_block.fill_box(bx * block_width, by * block_height, block_width, block_height, low_freq_block_flag ? color_rgba(255, 0, 0, 255) : g_black_color); - - bool active_chan_flags[4] = { }; - - // The number of channels with non-zero spans - uint32_t total_active_chans = 0; - // The indices of the channels with non-zero spans. - //uint32_t active_chan_list[4] = { 0 }; - - for (uint32_t i = 0; i < 4; i++) - { - if (pixel_stats.m_rgba_stats[i].m_range > 0.0f) - { - assert(pixel_stats.m_max[i] != pixel_stats.m_min[i]); - - active_chan_flags[i] = true; - - //active_chan_list[total_active_chans] = i; - total_active_chans++; - } - else - { - assert(pixel_stats.m_max[i] == pixel_stats.m_min[i]); - } - } - - basisu::comparative_stats cross_chan_stats[TOTAL_RGBA_CHAN_PAIRS]; - - // def=max correlation for each channel pair (or 1 if one of the channels is inactive) - float chan_pair_correlations[6] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; - // 0=0, 1 - // 1=0, 2 - // 2=1, 2 - // 3=0, 3 - // 4=1, 3 - // 5=2, 3 - - float min_corr = 1.0f, max_corr = 0.0f; - - for (uint32_t pair_index = 0; pair_index < TOTAL_RGBA_CHAN_PAIRS; pair_index++) - { - const uint32_t chanA = g_rgba_chan_pairs[pair_index][0]; - const uint32_t chanB = g_rgba_chan_pairs[pair_index][1]; - - // If both channels were active, we've got usable correlation statistics. - if (active_chan_flags[chanA] && active_chan_flags[chanB]) - { - // TODO: This can be directly derived from the 3D/4D covariance matrix entries. - cross_chan_stats[pair_index].calc_pearson(total_block_pixels, - &pixel_stats.m_pixels_f[0][chanA], - &pixel_stats.m_pixels_f[0][chanB], - 4, 4, - &pixel_stats.m_rgba_stats[chanA], - &pixel_stats.m_rgba_stats[chanB]); - - chan_pair_correlations[pair_index] = fabsf(cross_chan_stats[pair_index].m_pearson); - - const float c = fabsf((float)cross_chan_stats[pair_index].m_pearson); - min_corr = minimum(min_corr, c); - max_corr = maximum(max_corr, c); - } - } - - // min_cor will be 1.0f if all channels inactive (solid) - - // Pixel the trial modes the encoder will use: RGB or RGBA (we don't currently support trying both) - - const bool used_alpha_encoder_modes = pixel_stats.m_has_alpha; - - float sobel_energy = 0.0f; - for (uint32_t y = 0; y < block_height; y++) - { - for (uint32_t x = 0; x < block_width; x++) - { - const color_rgba& s = orig_img_sobel_xy.get_clamped(bx * block_width + x, by * block_height + y); - sobel_energy += s[0] * s[0] + s[1] * s[1] + s[2] * s[2] + s[3] * s[3]; - } // x - } // y - - sobel_energy /= (float)total_block_pixels; - + // Configure low-level block encoder. ldr_astc_lowlevel_block_encoder_params enc_blk_params; - enc_blk_params.m_block_width = block_width; - enc_blk_params.m_block_height = block_height; - enc_blk_params.m_total_block_pixels = total_block_pixels; - enc_blk_params.m_bx = bx; - enc_blk_params.m_by = by; - - enc_blk_params.m_pOrig_img_sobel_xy_t = &orig_img_sobel_xy; - - enc_blk_params.m_num_trial_modes = encoder_trial_modes.size_u32(); - enc_blk_params.m_pTrial_modes = encoder_trial_modes.get_ptr(); - enc_blk_params.m_pGrouped_trial_modes = &grouped_encoder_trial_modes; - - enc_blk_params.m_pPart_data_p2 = pPart_data_p2; - enc_blk_params.m_pPart_data_p3 = pPart_data_p3; - enc_blk_params.m_pEnc_params = &enc_cfg.m_cem_enc_params; - - float ang_dot = saturate(pixel_stats.m_zero_rel_axis3.dot3(pixel_stats.m_mean_rel_axis3)); - const float pca_axis_angles = acosf(ang_dot) * (180.0f / (float)cPiD); - - enc_blk_params.m_use_alpha_or_opaque_modes = used_alpha_encoder_modes; - enc_blk_params.m_use_lum_direct_modes = is_lum_only; - - const bool filter_by_pca_angles_flag = (superpass_index == 1) ? enc_cfg.m_filter_by_pca_angles_flag_p2 : enc_cfg.m_filter_by_pca_angles_flag; - if (!filter_by_pca_angles_flag) - { - enc_blk_params.m_use_direct_modes = true; - enc_blk_params.m_use_base_scale_modes = true; - } - else - { - // TODO: Make selective based off edge blocks? - enc_blk_params.m_use_direct_modes = (!total_active_chans) || (pca_axis_angles > enc_cfg.m_use_direct_angle_thresh); - enc_blk_params.m_use_base_scale_modes = (pca_axis_angles <= enc_cfg.m_use_base_scale_angle_thresh); - } - - enc_blk_params.m_grid_hv_filtering = enc_cfg.m_grid_hv_filtering; - enc_blk_params.m_filter_horizontally_flag = filter_horizontally_flag; - - enc_blk_params.m_use_small_grids_only = low_freq_block_flag && enc_cfg.m_low_freq_block_filtering; - - enc_blk_params.m_subsets_enabled = enc_cfg.m_subsets_enabled && (!low_freq_block_flag || !enc_cfg.m_subsets_edge_filtering); - - enc_blk_params.m_subsets_edge_filtering = enc_cfg.m_subsets_edge_filtering; - - enc_blk_params.m_use_blue_contraction = enc_cfg.m_use_blue_contraction; - enc_blk_params.m_final_encode_try_base_ofs = enc_cfg.m_use_base_ofs; - - memcpy(enc_blk_params.m_superbucket_max_to_retain, enc_cfg.m_superbucket_max_to_retain, sizeof(enc_cfg.m_superbucket_max_to_retain)); - - memcpy(enc_blk_params.m_final_shortlist_fraction, enc_cfg.m_final_shortlist_fraction, sizeof(enc_blk_params.m_final_shortlist_fraction)); - memcpy(enc_blk_params.m_final_shortlist_min_size, enc_cfg.m_final_shortlist_min_size, sizeof(enc_cfg.m_final_shortlist_min_size)); - memcpy(enc_blk_params.m_final_shortlist_max_size, enc_cfg.m_final_shortlist_max_size, sizeof(enc_blk_params.m_final_shortlist_max_size)); - - enc_blk_params.m_part2_fraction_to_keep = enc_cfg.m_part2_fraction_to_keep; - enc_blk_params.m_part3_fraction_to_keep = enc_cfg.m_part3_fraction_to_keep; - enc_blk_params.m_base_parts2 = enc_cfg.m_base_parts2; - enc_blk_params.m_base_parts3 = enc_cfg.m_base_parts3; - enc_blk_params.m_gradient_descent_flag = enc_cfg.m_gradient_descent_flag; - enc_blk_params.m_polish_weights_flag = enc_cfg.m_polish_weights_flag; - enc_blk_params.m_qcd_enabled_flag = enc_cfg.m_qcd_enabled_flag; - enc_blk_params.m_bucket_pruning_passes = enc_cfg.m_bucket_pruning_passes; - - enc_blk_params.m_alpha_cems = used_alpha_encoder_modes; - - enc_blk_params.m_early_stop_wpsnr = enc_cfg.m_early_stop_wpsnr; - enc_blk_params.m_early_stop2_wpsnr = enc_cfg.m_early_stop2_wpsnr; - - enc_blk_params.m_final_encode_always_try_rgb_direct = enc_cfg.m_final_encode_always_try_rgb_direct; - - enc_blk_params.m_pDCT2F = &dct; - - // Determine DP usage - if (enc_cfg.m_force_all_dual_plane_chan_evals) - { - for (uint32_t i = 0; i < 4; i++) - enc_blk_params.m_dp_active_chans[i] = active_chan_flags[i]; - } - else - { - for (uint32_t i = 0; i < 3; i++) - enc_blk_params.m_dp_active_chans[i] = false; - - // Being very conservative with alpha here - always let the analytical evaluator consider it. - enc_blk_params.m_dp_active_chans[3] = pixel_stats.m_has_alpha; - - if (!enc_cfg.m_disable_rgb_dual_plane) - { - const float rg_corr = chan_pair_correlations[0]; - const float rb_corr = chan_pair_correlations[1]; - const float gb_corr = chan_pair_correlations[2]; - - int desired_dp_chan_rgb = -1; - - float min_p = minimum(rg_corr, rb_corr, gb_corr); - - if (min_p < enc_cfg.m_strong_dp_decorr_thresh_rgb) - { - const bool has_r = active_chan_flags[0], has_g = active_chan_flags[1]; - //const bool has_b = active_chan_flags[2]; - - uint32_t total_active_chans_rgb = 0; - for (uint32_t i = 0; i < 3; i++) - total_active_chans_rgb += active_chan_flags[i]; - - if (total_active_chans_rgb == 2) - { - if (!has_r) - desired_dp_chan_rgb = 1; - else if (!has_g) - desired_dp_chan_rgb = 0; - else - desired_dp_chan_rgb = 0; - } - else if (total_active_chans_rgb == 3) - { - // see if rg/rb is weakly correlated vs. gb - if ((rg_corr < gb_corr) && (rb_corr < gb_corr)) - desired_dp_chan_rgb = 0; - // see if gr/gb is weakly correlated vs. rb - else if ((rg_corr < rb_corr) && (gb_corr < rb_corr)) - desired_dp_chan_rgb = 1; - // assume b is weakest - else - desired_dp_chan_rgb = 2; - } - } - - if (desired_dp_chan_rgb != -1) - { - assert(active_chan_flags[desired_dp_chan_rgb]); - enc_blk_params.m_dp_active_chans[desired_dp_chan_rgb] = true; - } - } - } - - if (!enc_blk_params.m_dp_active_chans[0] && !enc_blk_params.m_dp_active_chans[1] && !enc_blk_params.m_dp_active_chans[2] && !enc_blk_params.m_dp_active_chans[3]) - { - enc_blk_params.m_use_dual_planes = false; - } - - astc_ldr::cem_encode_params temp_cem_enc_params; - if (superpass_index == 1) - { - enc_blk_params.m_base_parts2 = enc_cfg.m_base_parts2_p2; - enc_blk_params.m_base_parts3 = enc_cfg.m_base_parts3_p2; - enc_blk_params.m_part2_fraction_to_keep = 1; - enc_blk_params.m_part3_fraction_to_keep = 1; - - memcpy(enc_blk_params.m_superbucket_max_to_retain, enc_cfg.m_superbucket_max_to_retain_p2, sizeof(enc_cfg.m_superbucket_max_to_retain_p2)); - memcpy(enc_blk_params.m_final_shortlist_max_size, enc_cfg.m_final_shortlist_max_size_p2, sizeof(enc_cfg.m_final_shortlist_max_size_p2)); - - if (enc_cfg.m_second_pass_force_subsets_enabled) - enc_blk_params.m_subsets_enabled = true; - enc_blk_params.m_subsets_edge_filtering = false; - - if (enc_cfg.m_force_all_dp_chans_p2) - { - enc_blk_params.m_dp_active_chans[0] = active_chan_flags[0]; - enc_blk_params.m_dp_active_chans[1] = active_chan_flags[1]; - enc_blk_params.m_dp_active_chans[2] = active_chan_flags[2]; - enc_blk_params.m_dp_active_chans[3] = active_chan_flags[3]; - enc_blk_params.m_use_dual_planes = true; - - if (!enc_blk_params.m_dp_active_chans[0] && !enc_blk_params.m_dp_active_chans[1] && !enc_blk_params.m_dp_active_chans[2] && !enc_blk_params.m_dp_active_chans[3]) - { - enc_blk_params.m_use_dual_planes = false; - } - } - - enc_blk_params.m_gradient_descent_flag = true; - enc_blk_params.m_polish_weights_flag = true; - - enc_blk_params.m_use_direct_modes = true; - enc_blk_params.m_use_base_scale_modes = true; - - enc_blk_params.m_early_stop_wpsnr = enc_cfg.m_early_stop_wpsnr + 2.0f; - enc_blk_params.m_early_stop2_wpsnr = enc_cfg.m_early_stop2_wpsnr + 2.0f; - - if (enc_cfg.m_second_pass_total_weight_refine_passes) - { - temp_cem_enc_params = enc_cfg.m_cem_enc_params; - enc_blk_params.m_pEnc_params = &temp_cem_enc_params; - - temp_cem_enc_params.m_total_weight_refine_passes = enc_cfg.m_second_pass_total_weight_refine_passes; - temp_cem_enc_params.m_worst_weight_nudging_flag = true; - temp_cem_enc_params.m_endpoint_refinement_flag = true; - } - } + encoder_config_manager cfg_man; + cfg_man.init(bx, by, block_width, block_height, total_block_pixels, + pixel_stats, + dct, enc_cfg, orig_img_sobel_xy, vis_dct_low_freq_block, + 0); + + cfg_man.select(enc_blk_params, superpass_index, + bx, by, block_width, block_height, total_block_pixels, + orig_img_sobel_xy, encoder_trial_modes, grouped_encoder_trial_modes, + pPart_data_p2, pPart_data_p3, + pixel_stats, enc_cfg, + dct, + 0); scoped_ldr_astc_lowlevel_block_encoder scoped_block_encoder(encoder_pool); if (scoped_block_encoder.get_ptr() == nullptr) @@ -6339,7 +8358,7 @@ bool ldr_astc_block_encode_image( return; } - // solid color + // solid color - must be first { encode_block_output* pOut = out_blocks.enlarge(1); pOut->clear(); @@ -6360,164 +8379,184 @@ bool ldr_astc_block_encode_image( } encode_block_stats enc_block_stats; - - bool enc_status = scoped_block_encoder.get_ptr()->full_encode(enc_blk_params, pixel_stats, out_blocks, 0, enc_block_stats); - if (!enc_status) + bool enc_status; + { - encoder_failed_flag.store(true); - return; + enc_status = scoped_block_encoder.get_ptr()->full_encode(enc_blk_params, pixel_stats, out_blocks, 0, enc_block_stats); + if (!enc_status) + { + encoder_failed_flag.store(true); + return; + } } -#if 1 - // --------------------- BLOCK BLURRING - // TODO - very slow, needs more configuration and tuning, experimental - const float BLUR_STD_DEV_THRESH = (15.0f / 255.0f); - const float BLUR_SOBEL_ENERGY_THRESH = 15000.0f; - - const bool use_blurs = (enc_cfg.m_blurring_enabled && (!selective_blurring || ((max_std_dev > BLUR_STD_DEV_THRESH) && (sobel_energy > BLUR_SOBEL_ENERGY_THRESH)))) || - (enc_cfg.m_blurring_enabled_p2 && (superpass_index == 1)); - - if (use_blurs) +#if 0 + if (enc_cfg.m_debug_images) { - { - assert(orig_img_blurred2.get_width()); - - color_rgba block_pixels_blurred2[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; - orig_img_blurred2.extract_block_clamped(block_pixels_blurred2, bx * block_width, by * block_height, block_width, block_height); - - astc_ldr::pixel_stats_t pixel_stats_blurred2; - pixel_stats_blurred2.init(total_block_pixels, block_pixels_blurred2); - - enc_status = scoped_block_encoder.get_ptr()->full_encode(enc_blk_params, pixel_stats_blurred2, out_blocks, 1, enc_block_stats); - if (!enc_status) - { - encoder_failed_flag.store(true); - return; - } - } - - { - assert(orig_img_blurred3.get_width()); - - color_rgba block_pixels_blurred3[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; - orig_img_blurred3.extract_block_clamped(block_pixels_blurred3, bx * block_width, by * block_height, block_width, block_height); - - astc_ldr::pixel_stats_t pixel_stats_blurred3; - pixel_stats_blurred3.init(total_block_pixels, block_pixels_blurred3); - - enc_status = scoped_block_encoder.get_ptr()->full_encode(enc_blk_params, pixel_stats_blurred3, out_blocks, 2, enc_block_stats); - if (!enc_status) - { - encoder_failed_flag.store(true); - return; - } - } - - { - assert(orig_img_blurred4.get_width()); - - color_rgba block_pixels_blurred4[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; - orig_img_blurred4.extract_block_clamped(block_pixels_blurred4, bx * block_width, by * block_height, block_width, block_height); - - astc_ldr::pixel_stats_t pixel_stats_blurred4; - pixel_stats_blurred4.init(total_block_pixels, block_pixels_blurred4); - - enc_status = scoped_block_encoder.get_ptr()->full_encode(enc_blk_params, pixel_stats_blurred4, out_blocks, 3, enc_block_stats); - if (!enc_status) - { - encoder_failed_flag.store(true); - return; - } - } - - { - assert(orig_img_blurred5.get_width()); - - color_rgba block_pixels_blurred5[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; - orig_img_blurred5.extract_block_clamped(block_pixels_blurred5, bx * block_width, by * block_height, block_width, block_height); - - astc_ldr::pixel_stats_t pixel_stats_blurred5; - pixel_stats_blurred5.init(total_block_pixels, block_pixels_blurred5); - - enc_status = scoped_block_encoder.get_ptr()->full_encode(enc_blk_params, pixel_stats_blurred5, out_blocks, 4, enc_block_stats); - if (!enc_status) - { - encoder_failed_flag.store(true); - return; - } - } + const bool vis_flag = (scoped_block_encoder.get_ptr()->m_block_complexity_index == 0) && (max_std_dev < (6.0f / 255.0f)); + vis_dct_low_freq_block.fill_box(bx * block_width, by * block_height, block_width, block_height, vis_flag ? color_rgba(255, 0, 255, 255) : g_black_color); } #endif + // --------------------- BLOCK BLURRING + //const float BLUR_STD_DEV_THRESH = (15.0f / 255.0f); + //const float BLUR_SOBEL_ENERGY_THRESH = 15000.0f; + + //const float BLUR_STD_DEV_THRESH = (5.0f / 255.0f); + //const float BLUR_SOBEL_ENERGY_THRESH = 5000.0f; + + const float BLUR_STD_DEV_THRESH = (10.0f / 255.0f); + const float BLUR_SOBEL_ENERGY_THRESH = 10000.0f; + const bool use_blurs = (enc_cfg.m_blurring_enabled_p1 && (!selective_blurring || ((cfg_man.m_max_std_dev > BLUR_STD_DEV_THRESH) && (cfg_man.m_sobel_energy > BLUR_SOBEL_ENERGY_THRESH)))) || + (enc_cfg.m_blurring_enabled_p2 && (superpass_index == 1)); + if (use_blurs) + { + for (uint32_t i = 0; i < TOTAL_BLURRED_IMAGES; i++) + { + const uint32_t blur_id = BLUR_ID_GAUSSIAN_ALTERNATE + i; + const image& blurred_img = orig_img_blurred[i]; + assert(blurred_img.get_width()); + + color_rgba block_pixels_blurred[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + blurred_img.extract_block_clamped(block_pixels_blurred, bx * block_width, by * block_height, block_width, block_height); + + astc_ldr::pixel_stats_t pixel_stats_blurred; + pixel_stats_blurred.init(total_block_pixels, block_pixels_blurred); + + cfg_man.init(bx, by, block_width, block_height, total_block_pixels, + pixel_stats_blurred, + dct, enc_cfg, orig_img_sobel_xy, vis_dct_low_freq_block, blur_id); + + ldr_astc_lowlevel_block_encoder_params enc_blk_params_blurred; + + cfg_man.select(enc_blk_params_blurred, + superpass_index, + bx, by, block_width, block_height, total_block_pixels, + orig_img_sobel_xy, encoder_trial_modes, grouped_encoder_trial_modes, + pPart_data_p2, pPart_data_p3, + pixel_stats_blurred, enc_cfg, + dct, + blur_id); + + enc_status = scoped_block_encoder.get_ptr()->full_encode(enc_blk_params_blurred, pixel_stats_blurred, out_blocks, blur_id, enc_block_stats); + if (!enc_status) + { + encoder_failed_flag.store(true); + return; + } + } + } + + // Grid dimension prefiltering (experimental) + const bool BLOCK_PREFILTER_BY_GRID_DIM = false; + if (BLOCK_PREFILTER_BY_GRID_DIM && (enc_cfg.m_blurring_enabled_p1 || enc_cfg.m_blurring_enabled_p2)) + { + uint_vec grid_dims; + + for (uint32_t out_block_iter = 0; out_block_iter < out_blocks.size_u32(); out_block_iter++) + { + if (out_blocks[out_block_iter].m_trial_mode_index < 0) + continue; + + astc_helpers::log_astc_block& log_astc_blk = out_blocks[out_block_iter].m_log_blk; + if (log_astc_blk.m_solid_color_flag_ldr) + continue; + + if ((log_astc_blk.m_grid_width == block_width) && (log_astc_blk.m_grid_height == block_height)) + continue; + + grid_dims.push_back( + ((uint32_t)log_astc_blk.m_grid_width << 8) | ((uint32_t)log_astc_blk.m_grid_height)); + } + + if (grid_dims.size()) + { + grid_dims.unique(); + const uint32_t total_unique = grid_dims.size_u32(); + + for (uint32_t b = 0; b < total_unique; b++) + { + const uint32_t grid_width = grid_dims[b] >> 8; + const uint32_t grid_height = grid_dims[b] & 0xFF; + + color_rgba upsampled_block[astc_helpers::MAX_BLOCK_PIXELS]; // num_block_samples + + filter_block(block_width, block_height, grid_width, grid_height, pixel_stats, upsampled_block); + + const uint32_t blur_id = BLUR_ID_GRID_DIM_BASE + b; + + astc_ldr::pixel_stats_t pixel_stats_blurred; + pixel_stats_blurred.init(total_block_pixels, upsampled_block); + + cfg_man.init(bx, by, block_width, block_height, total_block_pixels, + pixel_stats_blurred, + dct, enc_cfg, orig_img_sobel_xy, vis_dct_low_freq_block, blur_id); + + ldr_astc_lowlevel_block_encoder_params enc_blk_params_blurred; + + cfg_man.select(enc_blk_params_blurred, + superpass_index, + bx, by, block_width, block_height, total_block_pixels, + orig_img_sobel_xy, encoder_trial_modes, grouped_encoder_trial_modes, + pPart_data_p2, pPart_data_p3, + pixel_stats_blurred, enc_cfg, + dct, + blur_id); + + enc_status = scoped_block_encoder.get_ptr()->full_encode(enc_blk_params_blurred, pixel_stats_blurred, out_blocks, blur_id, enc_block_stats); + if (!enc_status) + { + encoder_failed_flag.store(true); + return; + } + + } // b + } + } + + // --------------------- EXPERIMENTAL - DC/LINEAR LATENT CODING + if ((enc_cfg.m_try_simplified_latent_configs) && + (!pixel_stats.m_has_alpha) && + (cfg_man.m_max_std_dev > (2.0f / 255.0f))) + { + uint64_vec best_2subset_seed_ids, best_3subset_seed_ids; + + encode_block_to_dc_latent( + block_width, block_height, + pixel_stats, + out_blocks, + enc_cfg, + pPart_data_p2, pPart_data_p3, encoder_trial_modes, + best_2subset_seed_ids, best_3subset_seed_ids); + + encode_block_to_linear_latent(8, + block_width, block_height, + pixel_stats, + out_blocks, + enc_cfg, + pPart_data_p2, pPart_data_p3, encoder_trial_modes, + best_2subset_seed_ids, best_3subset_seed_ids); + } + // --------------------- WEIGHT GRID DCT CODING if (enc_cfg.m_use_dct) { // apply DCT to weights for (uint32_t out_block_iter = 0; out_block_iter < out_blocks.size_u32(); out_block_iter++) { - if (out_blocks[out_block_iter].m_trial_mode_index < 0) - continue; - - astc_helpers::log_astc_block& log_astc_blk = out_blocks[out_block_iter].m_log_blk; - - const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, log_astc_blk.m_grid_width, log_astc_blk.m_grid_height); - - const uint32_t num_planes = (log_astc_blk.m_dual_plane ? 2 : 1); - for (uint32_t plane_index = 0; plane_index < num_planes; plane_index++) + if (!apply_weight_grid_dct(block_width, block_height, + grid_coder, + out_blocks[out_block_iter], + pixel_stats, true, + enc_cfg, pPart_data_p2, pPart_data_p3, true)) { - bitwise_coder c; - basist::astc_ldr_t::dct_syms syms; - code_block_weights(grid_coder, enc_cfg.m_base_q, plane_index, log_astc_blk, pGrid_data, c, syms); - - out_blocks[out_block_iter].m_packed_dct_plane_data[plane_index] = syms; - - c.flush(); - - basist::bitwise_decoder d; - d.init(c.get_bytes().data(), c.get_bytes().size_u32()); - - // ensure existing weights get blown away - for (uint32_t i = 0; i < (uint32_t)(log_astc_blk.m_grid_width * log_astc_blk.m_grid_height); i++) - log_astc_blk.m_weights[i * num_planes + plane_index] = 0; - - basist::astc_ldr_t::fvec dct_temp; - bool status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, log_astc_blk, &d, pGrid_data, nullptr, dct_temp, nullptr); - - assert(status); - if (!status) - { - error_printf("grid_coder.decode_block_weights() failed!\n"); - - encoder_failed_flag.store(true); - return; - } - -#if 0 - { - astc_helpers::log_astc_block alt_log_astc_blk(log_astc_blk); - - for (uint32_t i = 0; i < (uint32_t)(log_astc_blk.m_grid_width * log_astc_blk.m_grid_height); i++) - alt_log_astc_blk.m_weights[i * num_planes + plane_index] = 0; - - status = grid_coder.decode_block_weights(q, plane_index, alt_log_astc_blk, nullptr, pGrid_data, &out_block_dct_stats[out_block_iter], &syms); - assert(status); - - for (uint32_t i = 0; i < (uint32_t)(log_astc_blk.m_grid_width * log_astc_blk.m_grid_height); i++) - { - assert(log_astc_blk.m_weights[i * num_planes + plane_index] == alt_log_astc_blk.m_weights[i * num_planes + plane_index]); - } - - } -#endif - // TODO: in theory, endpoints can be refined if they don't change the DCT span. + encoder_failed_flag.store(true); + return; } - - out_blocks[out_block_iter].m_sse = eval_error(block_width, block_height, log_astc_blk, pixel_stats, enc_cfg.m_cem_enc_params); - } // for } // use_dct - + // Find best output block uint64_t best_out_blocks_err = UINT64_MAX; uint32_t best_out_blocks_index = 0; @@ -6527,8 +8566,8 @@ bool ldr_astc_block_encode_image( { const astc_helpers::log_astc_block& log_astc_blk = out_blocks[out_block_iter].m_log_blk; - color_rgba dec_pixels[astc_helpers::MAX_BLOCK_DIM * astc_helpers::MAX_BLOCK_DIM]; - bool dec_status = astc_helpers::decode_block(log_astc_blk, dec_pixels, block_width, block_height, enc_cfg.m_cem_enc_params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); + color_rgba dec_pixels[astc_helpers::MAX_BLOCK_PIXELS]; + const bool dec_status = astc_helpers::decode_block_xuastc_ldr(log_astc_blk, dec_pixels, block_width, block_height, enc_cfg.m_cem_enc_params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); assert(dec_status); if (!dec_status) @@ -6564,6 +8603,99 @@ bool ldr_astc_block_encode_image( } } // out_block_iter + // try blurring best candidate found so far (very experimental) + const bool BLOCK_POSTFILTERING = false; + if (BLOCK_POSTFILTERING && + ( ((enc_cfg.m_blurring_enabled_p1) || (enc_cfg.m_blurring_enabled_p2)) && (cfg_man.m_max_std_dev > (5.0f / 255.0f)) ) + ) + { + encode_block_output &best_blk = out_blocks[best_out_blocks_index]; + + const astc_helpers::log_astc_block& best_log_blk = best_blk.m_log_blk; + + if ((!best_log_blk.m_solid_color_flag_ldr) && + ((best_log_blk.m_grid_width < block_width) || (best_log_blk.m_grid_height < block_height))) + { + color_rgba upsampled_block[astc_helpers::MAX_BLOCK_PIXELS]; // num_block_samples + + filter_block(block_width, block_height, best_log_blk.m_grid_width, best_log_blk.m_grid_height, pixel_stats, upsampled_block); + + const uint32_t blur_id = BLUR_ID_BEST_CANDIDATE; + + astc_ldr::pixel_stats_t pixel_stats_blurred; + pixel_stats_blurred.init(total_block_pixels, upsampled_block); + + cfg_man.init(bx, by, block_width, block_height, total_block_pixels, + pixel_stats_blurred, + dct, enc_cfg, orig_img_sobel_xy, vis_dct_low_freq_block, blur_id); + + ldr_astc_lowlevel_block_encoder_params enc_blk_params_blurred; + + cfg_man.select(enc_blk_params_blurred, + superpass_index, + bx, by, block_width, block_height, total_block_pixels, + orig_img_sobel_xy, encoder_trial_modes, grouped_encoder_trial_modes, + pPart_data_p2, pPart_data_p3, + pixel_stats_blurred, enc_cfg, + dct, + blur_id); + + const uint32_t cur_num_out_blocks = out_blocks.size_u32(); + + enc_status = scoped_block_encoder.get_ptr()->full_encode(enc_blk_params_blurred, pixel_stats_blurred, out_blocks, blur_id, enc_block_stats); + if (!enc_status) + { + encoder_failed_flag.store(true); + return; + } + + for (uint32_t out_block_iter = cur_num_out_blocks; out_block_iter < out_blocks.size_u32(); out_block_iter++) + { + if (!apply_weight_grid_dct(block_width, block_height, + grid_coder, + out_blocks[out_block_iter], + pixel_stats, true, + enc_cfg, pPart_data_p2, pPart_data_p3, true)) + { + encoder_failed_flag.store(true); + return; + } + } + + for (uint32_t out_block_iter = cur_num_out_blocks; out_block_iter < out_blocks.size_u32(); out_block_iter++) + { + const astc_helpers::log_astc_block& log_astc_blk = out_blocks[out_block_iter].m_log_blk; + + color_rgba dec_pixels[astc_helpers::MAX_BLOCK_PIXELS]; + bool dec_status = astc_helpers::decode_block(log_astc_blk, dec_pixels, block_width, block_height, enc_cfg.m_cem_enc_params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); + + assert(dec_status); + if (!dec_status) + { + encoder_failed_flag.store(true); + return; + } + + uint64_t total_err = 0; + for (uint32_t i = 0; i < total_block_pixels; i++) + total_err += weighted_color_error(block_pixels[i], dec_pixels[i], enc_cfg.m_cem_enc_params); + + assert(out_blocks[out_block_iter].m_blur_id != 0); + + // Replace m_sse with the actual WSSE vs. the original source block (in case it was blurred) + out_blocks[out_block_iter].m_sse = total_err; + + if (total_err < best_out_blocks_err) + { + best_out_blocks_err = total_err; + best_out_blocks_log_astc_blk = log_astc_blk; + best_out_blocks_index = out_block_iter; + } + } // out_block_iter + + } + } + #if 0 // TODO: Save memory, only minimally tested if (enc_cfg.m_save_single_result) @@ -6576,15 +8708,26 @@ bool ldr_astc_block_encode_image( best_out_blocks_index = 0; } #endif - + // TODO: limit max candidates to save memory here + ldr_astc_block_encode_image_output::block_info& block_info_out = enc_out.m_image_block_info(bx, by); - block_info_out.m_low_freq_block_flag = low_freq_block_flag; + block_info_out.m_low_freq_block_flag = cfg_man.m_low_freq_block_flag; block_info_out.m_super_strong_edges = scoped_block_encoder.get_ptr()->m_super_strong_edges; block_info_out.m_very_strong_edges = scoped_block_encoder.get_ptr()->m_very_strong_edges; block_info_out.m_strong_edges = scoped_block_encoder.get_ptr()->m_strong_edges; + block_info_out.m_packed_out_block_index = best_out_blocks_index; + const uint64_t check_wsse = out_blocks[best_out_blocks_index].m_sse; + + enforce_max_candidate_limit(block_info_out, max_candidate_limit); + + best_out_blocks_index = block_info_out.m_packed_out_block_index; // may have changed + + BASISU_NOTE_UNUSED(check_wsse); + assert(check_wsse == out_blocks[best_out_blocks_index].m_sse); + // Create packed ASTC block astc_helpers::astc_block& best_phys_block = packed_blocks(bx, by); bool pack_success = astc_helpers::pack_astc_block(best_phys_block, best_out_blocks_log_astc_blk); @@ -6595,7 +8738,7 @@ bool ldr_astc_block_encode_image( } output_block_devel_desc& out_devel_desc = output_block_devel_info(bx, by); - out_devel_desc.m_low_freq_block_flag = low_freq_block_flag; + out_devel_desc.m_low_freq_block_flag = cfg_man.m_low_freq_block_flag; out_devel_desc.m_super_strong_edges = scoped_block_encoder.get_ptr()->m_super_strong_edges; out_devel_desc.m_very_strong_edges = scoped_block_encoder.get_ptr()->m_very_strong_edges; out_devel_desc.m_strong_edges = scoped_block_encoder.get_ptr()->m_strong_edges; @@ -6603,12 +8746,31 @@ bool ldr_astc_block_encode_image( // Critical Section { std::lock_guard g(global_mutex); + + if (superpass_index == 0) + { + if (use_blurs) + total_blur_encodes_p1++; - if (use_blurs) - total_blur_encodes++; + if (out_blocks[best_out_blocks_index].m_blur_id >= 1) + { + const int blur_id = out_blocks[best_out_blocks_index].m_blur_id - 1; + if (blur_id < (int)std::size(total_blurred_blocks_p1)) + total_blurred_blocks_p1[blur_id]++; + } + } + else if (superpass_index == 1) + { + if (use_blurs) + total_blur_encodes_p2++; - if (out_blocks[best_out_blocks_index].m_blur_id) - total_blurred_blocks1++; + if (out_blocks[best_out_blocks_index].m_blur_id >= 1) + { + const int blur_id = out_blocks[best_out_blocks_index].m_blur_id - 1; + if (blur_id < (int)std::size(total_blurred_blocks_p2)) + total_blurred_blocks_p2[blur_id]++; + } + } if (superpass_index == 0) { @@ -6822,6 +8984,8 @@ bool ldr_astc_block_encode_image( } // j + enforce_max_candidate_limit(out_block_info, max_candidate_limit); + } // bx } // by @@ -6845,6 +9009,13 @@ bool ldr_astc_block_encode_image( { const ldr_astc_block_encode_image_output::block_info& out_block_info = enc_out.m_image_block_info(bx, by); + if (out_block_info.m_packed_out_block_index >= out_block_info.m_out_blocks.size()) + { + fmt_error_printf("consistency check failed\n"); + assert(0); + return false; + } + #if BU_CHECK_NEIGHBOR_BEST uint64_t best_sse = UINT64_MAX; uint32_t best_out_block_index = 0; @@ -6901,11 +9072,13 @@ bool ldr_astc_block_encode_image( return false; } - 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_log_blk.m_grid_width, best_log_blk.m_grid_height); + //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_log_blk.m_grid_width, best_log_blk.m_grid_height); const uint32_t total_planes = best_log_blk.m_num_partitions ? (best_log_blk.m_dual_plane ? 2 : 1) : 0; astc_helpers::log_astc_block verify_log_blk(best_log_blk); + basist::astc_ldr_t::fvec dct_temp; + for (uint32_t plane_index = 0; plane_index < total_planes; plane_index++) { if (!out_block_info.m_out_blocks[out_block_info.m_packed_out_block_index].m_packed_dct_plane_data[plane_index].m_coeffs.size()) @@ -6914,9 +9087,8 @@ bool ldr_astc_block_encode_image( assert(0); return false; } - - basist::astc_ldr_t::fvec dct_temp; - bool dec_status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, verify_log_blk, nullptr, pGrid_data, nullptr, dct_temp, + + bool dec_status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, verify_log_blk, nullptr, nullptr, dct_temp, &out_block_info.m_out_blocks[out_block_info.m_packed_out_block_index].m_packed_dct_plane_data[plane_index]); if (!dec_status) @@ -7174,6 +9346,8 @@ bool ldr_astc_block_encode_image( if (enc_cfg.m_debug_output) { + display_candidate_statistics(enc_out); + uint32_t cem_used_hist[16] = { 0 }; uint32_t cem_used_bc[16] = { 0 }; uint32_t cem_used_subsets[16] = { 0 }; @@ -7195,10 +9369,15 @@ bool ldr_astc_block_encode_image( uint32_t total_used_bc = 0; + uint32_t total_candidates = 0; + for (uint32_t by = 0; by < num_blocks_y; by++) { for (uint32_t bx = 0; bx < num_blocks_x; bx++) { + const ldr_astc_block_encode_image_output::block_info& out_block_info = enc_out.m_image_block_info(bx, by); + total_candidates += out_block_info.m_out_blocks.size_u32(); + const output_block_devel_desc& desc = output_block_devel_info(bx, by); const astc_helpers::astc_block* pPhys_block = &packed_blocks(bx, by); @@ -7289,6 +9468,8 @@ bool ldr_astc_block_encode_image( } // bx } // by + fmt_debug_printf("Avg candidates per block: {}\n", (float)total_candidates / (float)total_blocks); + uint32_t total_used_modes = 0; fmt_debug_printf("--------------------- Trial Modes:\n"); @@ -7347,15 +9528,37 @@ bool ldr_astc_block_encode_image( fmt_debug_printf("\nEncoder stats:\n"); fmt_debug_printf("Total utilized encoder trial modes: {} {3.2}%\n", total_used_modes, (float)total_used_modes * 100.0f / (float)encoder_trial_modes.size()); - const uint32_t total_blurred_blocks = total_blurred_blocks1 + total_blurred_blocks2 + total_blurred_blocks3 + total_blurred_blocks4; + uint32_t overall_blurred_blocks_p1 = 0, overall_blurred_blocks_p2 = 0; + for (uint32_t i = 0; i < std::size(total_blurred_blocks_p1); i++) + { + overall_blurred_blocks_p1 += total_blurred_blocks_p1[i]; + overall_blurred_blocks_p2 += total_blurred_blocks_p2[i]; + } - fmt_debug_printf("\nTotal blur encodes: {} ({3.2}%)\n", total_blur_encodes, (float)total_blur_encodes * 100.0f / (float)total_blocks); - fmt_debug_printf("Total blurred blocks: {} ({3.2}%)\n", total_blurred_blocks, (float)total_blurred_blocks * 100.0f / (float)total_blocks); - fmt_debug_printf("Total blurred1 blocks: {} ({3.2}%)\n", total_blurred_blocks1, (float)total_blurred_blocks1 * 100.0f / (float)total_blocks); - fmt_debug_printf("Total blurred2 blocks: {} ({3.2}%)\n", total_blurred_blocks2, (float)total_blurred_blocks2 * 100.0f / (float)total_blocks); - fmt_debug_printf("Total blurred3 blocks: {} ({3.2}%)\n", total_blurred_blocks3, (float)total_blurred_blocks3 * 100.0f / (float)total_blocks); - fmt_debug_printf("Total blurred4 blocks: {} ({3.2}%)\n", total_blurred_blocks4, (float)total_blurred_blocks4 * 100.0f / (float)total_blocks); + fmt_debug_printf("\nTotal blur encodes p1: {} ({3.2}%)\n", total_blur_encodes_p1, (float)total_blur_encodes_p1 * 100.0f / (float)total_blocks); + fmt_debug_printf("Total blurred blocks p1: {} ({3.2}%)\n", overall_blurred_blocks_p1, (float)overall_blurred_blocks_p1 * 100.0f / (float)total_blocks); + + if (overall_blurred_blocks_p1) + { + for (uint32_t i = 0; i < std::size(total_blurred_blocks_p1); i++) + { + if (total_blurred_blocks_p1[i]) + fmt_debug_printf("Total blurred{} blocks p1: {} ({3.2}%)\n", i, total_blurred_blocks_p1[i], (float)total_blurred_blocks_p1[i] * 100.0f / (float)total_blocks); + } + } + fmt_debug_printf("\nTotal blur encodes p2: {} ({3.2}%)\n", total_blur_encodes_p2, (float)total_blur_encodes_p2 * 100.0f / (float)total_blocks); + fmt_debug_printf("Total blurred blocks p2: {} ({3.2}%)\n", overall_blurred_blocks_p2, (float)overall_blurred_blocks_p2 * 100.0f / (float)total_blocks); + + if (overall_blurred_blocks_p2) + { + for (uint32_t i = 0; i < std::size(total_blurred_blocks_p2); i++) + { + if (total_blurred_blocks_p2[i]) + fmt_debug_printf("Total blurred{} blocks p2: {} ({3.2}%)\n", i, total_blurred_blocks_p2[i], (float)total_blurred_blocks_p2[i] * 100.0f / (float)total_blocks); + } + } + fmt_debug_printf("\nTotal superbuckets created: {} ({4.1} per block)\n", total_superbuckets_created, (float)total_superbuckets_created / (float)total_blocks); fmt_debug_printf("Total shortlist buckets created: {} ({4.1} per block)\n", total_buckets_created, (float)total_buckets_created / (float)total_blocks); fmt_debug_printf("Total surrogate encodes: {} ({4.1} per block)\n", total_surrogate_encodes, (float)total_surrogate_encodes / (float)total_blocks); @@ -7403,8 +9606,11 @@ bool ldr_astc_block_encode_image( } debug_printf("\n"); - fmt_debug_printf("orig vs. ASTC compressed:\n"); - print_image_metrics(orig_img, unpacked_img); + if (enc_cfg.m_debug_output_image_metrics) + { + fmt_debug_printf("orig vs. ASTC compressed:\n"); + print_image_metrics(orig_img, unpacked_img); + } fmt_debug_printf("Total encode time: {.3} secs, {.3} ms per block, {.1} blocks/sec\n", total_enc_time, total_enc_time * 1000.0f / total_blocks, total_blocks / total_enc_time); @@ -7414,7 +9620,1365 @@ bool ldr_astc_block_encode_image( return true; } -//const uint32_t rice_zero_run_m = 3, rice_dct_coeff_m = 2; +bool ldr_astc_block_encode_image_fast_4x4( + const image& orig_img, + const ldr_astc_block_encode_image_high_level_config& enc_cfg, + const astc_ldr_encode_config& global_cfg, + ldr_astc_block_encode_image_output& enc_out, + uint32_t max_candidate_limit) +{ + BASISU_NOTE_UNUSED(max_candidate_limit); + + if (enc_cfg.m_debug_output) + fmt_debug_printf("ldr_astc_block_encode_image_fast_4x4:\n"); + + const uint32_t block_width = enc_cfg.m_block_width, block_height = enc_cfg.m_block_height; + if ((block_width != 4) || (block_height != 4)) + { + assert(0); + return false; + } + + //const uint32_t width = orig_img.get_width(), height = orig_img.get_height(); + //const uint32_t total_pixels = width * height; + const uint32_t total_block_pixels = enc_cfg.m_block_width * enc_cfg.m_block_height; + const uint32_t num_blocks_x = orig_img.get_block_width(enc_cfg.m_block_width); + const uint32_t num_blocks_y = orig_img.get_block_height(enc_cfg.m_block_height); + const uint32_t total_blocks = num_blocks_x * num_blocks_y; + + if (enc_cfg.m_debug_output) + { + fmt_debug_printf("\nASTC base bitrate: {3.3} bpp\n", 128.0f / (float)(enc_cfg.m_block_width * enc_cfg.m_block_height)); + + fmt_debug_printf("ASTC block size: {}x{}\n", enc_cfg.m_block_width, enc_cfg.m_block_height); + + fmt_debug_printf("Image has alpha: {}\n", orig_img.has_alpha()); + + fmt_debug_printf("max_candidate_limit: {}\n", max_candidate_limit);; + } + + if ((enc_cfg.m_cem_enc_params.m_comp_weights[0] != 1) || (enc_cfg.m_cem_enc_params.m_comp_weights[1] != 1) || + (enc_cfg.m_cem_enc_params.m_comp_weights[2] != 1) || (enc_cfg.m_cem_enc_params.m_comp_weights[3] != 1)) + { + printf("WARNING: Fast (effort 0) ASTC/XUASTC LDR 4x4 compressor doesn't support non-default channel weights\n"); + } + + // We don't use this here, but the supercompressors use these tables. + // TODO: The transcoder already creates all this stuff for each block size. + astc_ldr::partitions_data* pPart_data_p2 = &enc_out.m_part_data_p2; + pPart_data_p2->init(2, enc_cfg.m_block_width, enc_cfg.m_block_height, BASISU_USE_LSH2 == 0, BASISU_USE_LSH2 != 0); + + astc_ldr::partitions_data* pPart_data_p3 = &enc_out.m_part_data_p3; + pPart_data_p3->init(3, enc_cfg.m_block_width, enc_cfg.m_block_height, BASISU_USE_LSH3 == 0, BASISU_USE_LSH3 != 0); + + basisu::vector& encoder_trial_modes = enc_out.m_encoder_trial_modes; + encoder_trial_modes.reserve(4096); + + basist::astc_ldr_t::grouped_trial_modes& grouped_encoder_trial_modes = enc_out.m_grouped_encoder_trial_modes; + basist::astc_ldr_t::create_encoder_trial_modes_table(block_width, block_height, encoder_trial_modes, grouped_encoder_trial_modes, enc_cfg.m_debug_output, false); + + uint32_t bc7f_override_flags = basist::bc7f::cPackBC7FlagPBitOpt | basist::bc7f::cPackBC7FlagPBitOptMode6 | basist::bc7f::cPackBC7FlagUseTrivialMode6 | + basist::bc7f::cPackBC7FlagUse2SubsetsRGB | basist::bc7f::cPackBC7FlagASTCCompatible | + basist::bc7f::cPackBC7FlagUseDualPlaneRGB | basist::bc7f::cPackBC7FlagUseDualPlaneRGBA | + basist::bc7f::cPackBC7FlagUse3SubsetsRGB | basist::bc7f::cPackBC7FlagUse2SubsetsRGBA; + + if (global_cfg.m_force_disable_subsets) + { + bc7f_override_flags &= ~(basist::bc7f::cPackBC7FlagUse2SubsetsRGB | basist::bc7f::cPackBC7FlagUse3SubsetsRGB | basist::bc7f::cPackBC7FlagUse2SubsetsRGBA); + } + + if (global_cfg.m_force_disable_rgb_dual_plane) + { + bc7f_override_flags &= ~basist::bc7f::cPackBC7FlagUseDualPlaneRGB; + bc7f_override_flags |= basist::bc7f::cPackBC7FlagDisableRGBDualPlane; + } + + enc_out.m_image_block_info.resize(0, 0); + enc_out.m_image_block_info.resize(num_blocks_x, num_blocks_y); + + enc_out.m_packed_phys_blocks.resize(num_blocks_x, num_blocks_y); + + basist::astc_ldr_t::grid_weight_dct grid_coder; + if (enc_cfg.m_use_dct) + grid_coder.init(block_width, block_height); + + assert(enc_cfg.m_pJob_pool); + job_pool& job_pool = *enc_cfg.m_pJob_pool; + + const uint32_t num_threads = (uint32_t)job_pool.get_total_threads(); + assert(num_threads); + + std::atomic cur_row; + cur_row.store(0); + + std::atomic encoder_failed_flag; + encoder_failed_flag.store(false); + + for (uint32_t job_index = 0; job_index < num_threads; job_index++) + { + job_pool.add_job([job_index, num_threads, num_blocks_x, num_blocks_y, block_width, block_height, total_blocks, total_block_pixels, bc7f_override_flags, + &cur_row, &encoder_failed_flag, + &orig_img, &enc_cfg, &encoder_trial_modes, &grid_coder, &grouped_encoder_trial_modes, &enc_out] + { + BASISU_NOTE_UNUSED(job_index); BASISU_NOTE_UNUSED(num_threads); BASISU_NOTE_UNUSED(total_blocks); BASISU_NOTE_UNUSED(total_block_pixels); BASISU_NOTE_UNUSED(encoder_trial_modes); BASISU_NOTE_UNUSED(grouped_encoder_trial_modes); + basist::astc_ldr_t::fvec dct_temp; + + for ( ; ; ) + { + if (encoder_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++) + { + if (encoder_failed_flag) + return; + + color_rgba block_pixels[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + orig_img.extract_block_clamped(block_pixels, bx * block_width, by * block_height, block_width, block_height); + + ldr_astc_block_encode_image_output::block_info& blk_info = enc_out.m_image_block_info(bx, by); + + encode_block_output* pEnc_block_output = blk_info.m_out_blocks.enlarge(1); + pEnc_block_output->clear(); + + astc_helpers::log_astc_block& log_blk = pEnc_block_output->m_log_blk; + + if (!basist::bc7f::fast_pack_astc(log_blk, (basist::color_rgba*)block_pixels, bc7f_override_flags)) + { + assert(0); + encoder_failed_flag.store(true); + return; + } + + if (log_blk.m_solid_color_flag_ldr) + { + // weighted SSE should be zero, the block should always be solid here + pEnc_block_output->m_trial_mode_index = -1; + pEnc_block_output->m_sse = 0; + } + else + { + uint32_t cem_to_find = log_blk.m_color_endpoint_modes[0]; + if (cem_to_find == astc_helpers::CEM_LDR_RGB_BASE_PLUS_OFFSET) + cem_to_find = astc_helpers::CEM_LDR_RGB_DIRECT; + else if (cem_to_find == astc_helpers::CEM_LDR_RGBA_BASE_PLUS_OFFSET) + cem_to_find = astc_helpers::CEM_LDR_RGBA_DIRECT; + + const int ccs_to_find = log_blk.m_dual_plane ? log_blk.m_color_component_selector : -1; + BASISU_NOTE_UNUSED(ccs_to_find); + + const basisu::uint_vec& tms = basist::astc_ldr_t::get_tm_candidates(basist::astc_ldr_t::g_grouped_encoder_trial_modes[0], + cem_to_find, log_blk.m_num_partitions - 1, log_blk.m_dual_plane ? log_blk.m_color_component_selector + 1 : 0, + 1, basist::astc_ldr_t::calc_grid_aniso_val(log_blk.m_grid_width, log_blk.m_grid_height, 4, 4)); + + uint32_t tm_index = 0, tms_index = 0; + for (tms_index = 0; tms_index < tms.size(); tms_index++) + { + tm_index = tms[tms_index]; + + const auto& tm = basist::astc_ldr_t::g_encoder_trial_modes[0][tm_index]; + + assert(tm.m_cem == cem_to_find); + assert(tm.m_num_parts == log_blk.m_num_partitions); + assert(tm.m_ccs_index == ccs_to_find); + + if ((tm.m_endpoint_ise_range == log_blk.m_endpoint_ise_range) && (tm.m_weight_ise_range == log_blk.m_weight_ise_range)) + { + if ((tm.m_grid_width == log_blk.m_grid_width) && (tm.m_grid_height == log_blk.m_grid_height)) + { + break; + } + } + } // tms_index + + if (tms_index == tms.size()) + { + assert(0); + encoder_failed_flag.store(true); + return; + } + + pEnc_block_output->m_trial_mode_index = basisu::safe_cast_int16(tm_index); + + if (enc_cfg.m_use_dct) + { + //const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, new_log_block.m_grid_width, new_log_block.m_grid_height); + + const uint32_t num_planes = (log_blk.m_dual_plane ? 2 : 1); + + uint32_t total_empty_planes = 0; + + for (uint32_t plane_index = 0; plane_index < num_planes; plane_index++) + { + basist::astc_ldr_t::dct_syms& syms = pEnc_block_output->m_packed_dct_plane_data[plane_index]; + + code_block_weights(grid_coder, enc_cfg.m_base_q, plane_index, log_blk, syms, dct_temp); + + // ensure existing weights get blown away + for (uint32_t i = 0; i < (uint32_t)(log_blk.m_grid_width * log_blk.m_grid_height); i++) + log_blk.m_weights[i * num_planes + plane_index] = 0; + + bool dec_status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, log_blk, nullptr, nullptr, dct_temp, &syms); + + assert(dec_status); + if (!dec_status) + { + error_printf("grid_coder.decode_block_weights() failed!\n"); + encoder_failed_flag.store(true); + return; + } + + // check for all-zero AC's + if (syms.m_coeffs.size() == 1) + { + if ((1 + syms.m_coeffs[0].m_num_zeros) == (log_blk.m_grid_width * log_blk.m_grid_height)) + { + total_empty_planes++; + } + } + } + + if ((log_blk.m_num_partitions == 1) && (total_empty_planes == num_planes)) + { + // entire block post-quantization is DC only (no non-zero AC), switch to void-extent + uint32_t sum_r = 0, sum_g = 0, sum_b = 0, sum_a = 0; + for (uint32_t i = 0; i < 16; i++) + { + sum_r += block_pixels[i].r; + sum_g += block_pixels[i].g; + sum_b += block_pixels[i].b; + sum_a += block_pixels[i].a; + } + + sum_r = (sum_r + 8) >> 4; + sum_g = (sum_g + 8) >> 4; + sum_b = (sum_b + 8) >> 4; + sum_a = (sum_a + 8) >> 4; + + astc_helpers::set_ldr_solid_block(log_blk, sum_r, sum_g, sum_b, sum_a); + + pEnc_block_output->m_trial_mode_index = -1; + } + + } // if (enc_cfg.m_use_dct) + + { + color_rgba dec_block_pixels[16]; + + bool dec_status = astc_helpers::decode_block_xuastc_ldr(log_blk, dec_block_pixels, 4, 4, + enc_cfg.m_cem_enc_params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); + + if (!dec_status) + { + // Shouldn't ever happen + assert(0); + encoder_failed_flag.store(true); + return; + } + + uint64_t total_err = 0; + + for (uint32_t i = 0; i < 16; i++) + total_err += weighted_color_error(dec_block_pixels[i], block_pixels[i], enc_cfg.m_cem_enc_params); + + pEnc_block_output->m_sse = total_err; + } + } + + bool pack_status = astc_helpers::pack_astc_block(enc_out.m_packed_phys_blocks(bx, by), log_blk); + if (!pack_status) + { + assert(0); + encoder_failed_flag.store(true); + return; + } + + } // bx + + } // for + + } ); + + } // job_index + + job_pool.wait_for_all(); + + if (encoder_failed_flag) + { + fmt_error_printf("ldr_astc_block_encode_image_fast_4x4: Main compressor block loop failed!\n"); + return false; + } + + if (enc_cfg.m_debug_output) + { + display_candidate_statistics(enc_out); + + fmt_debug_printf("ldr_astc_block_encode_image_fast_4x4: done\n"); + } + + return true; +} + +#if BASISU_SUPPORT_ASTCENC +static astcenc_context* comp_astc_init_ldr( + uint32_t block_w, + uint32_t block_h, + bool srgb, + float quality, // ASTCENC_PRE_MEDIUM etc. + uint32_t thread_count, + uint32_t max_partitions, + bool xuastc_ldr_flag, + const uint32_t chan_weights[4], + bool disable_rgb_dual_planes, + uint32_t candidate_row_pitch, std::vector< std::vector > *pCandidates) +{ + astcenc_config config{}; + + const astcenc_profile profile = srgb ? ASTCENC_PRF_LDR_SRGB : ASTCENC_PRF_LDR; + + uint32_t flags = ASTCENC_FLG_USE_DECODE_UNORM8; + + // flags = 0 for ordinary LDR color compression + astcenc_error status = astcenc_config_init( + profile, + block_w, + block_h, + 1, + quality, + flags, + &config); + + if (status != ASTCENC_SUCCESS) + { + std::printf("astcenc_config_init() failed\n"); + return nullptr; + } + + config.cw_r_weight = (float)chan_weights[0]; + config.cw_g_weight = (float)chan_weights[1]; + config.cw_b_weight = (float)chan_weights[2]; + config.cw_a_weight = (float)chan_weights[3]; + + config.m_xuastc_ldr_flag = xuastc_ldr_flag; + + config.tune_partition_count_limit = maximum(1u, max_partitions); + config.m_disable_rgb_dual_planes = disable_rgb_dual_planes; + + if (pCandidates) + { + config.candidate_row_pitch = candidate_row_pitch; + config.m_pCandidates = pCandidates; + } + + astcenc_context* pContext = nullptr; + + status = astcenc_context_alloc(&config, thread_count, &pContext); + + if (status != ASTCENC_SUCCESS) + { + std::printf("astcenc_context_alloc() failed\n"); + return nullptr; + } + + return pContext; +} + +static void comp_astc_deinit(astcenc_context* pContext) +{ + astcenc_context_free(pContext); +} + +static uint32_t get_astc_block_count(uint32_t dim, uint32_t block_dim) +{ + return (dim + block_dim - 1) / block_dim; +} + +// Compress from tightly packed RGBA8 input. +// src_rgba must point to w * h * 4 bytes. +static bool comp_astc_image_u8( + astcenc_context* pContext, + const uint8_t* src_rgba, + uint32_t w, + uint32_t h, + uint32_t block_w, + uint32_t block_h, + basisu::vector2D& out_blocks, + job_pool &job_pool) +{ + static const astcenc_swizzle swizzle{ + ASTCENC_SWZ_R, ASTCENC_SWZ_G, ASTCENC_SWZ_B, ASTCENC_SWZ_A + }; + + astcenc_image image{}; + image.dim_x = w; + image.dim_y = h; + image.dim_z = 1; + image.data_type = ASTCENC_TYPE_U8; + + // astcenc_image::data is an array of pointers to 2D slices. + // For a 2D image we provide one slice pointer. + void* slices[1]; + slices[0] = const_cast(src_rgba); + image.data = slices; + + out_blocks.resize(get_astc_block_count(w, block_w), get_astc_block_count(h, block_h)); + + const uint32_t num_threads = (uint32_t)job_pool.get_total_threads(); + + if (num_threads == 1) + { + astcenc_error status = astcenc_compress_image( + pContext, + &image, + &swizzle, + reinterpret_cast(out_blocks.get_ptr()), + out_blocks.size_in_bytes(), + 0); + + return (status == ASTCENC_SUCCESS); + } + + std::atomic any_failures; + any_failures.store(false); + + for (uint32_t thread_index = 0; thread_index < num_threads; thread_index++) + { + job_pool.add_job([thread_index, pContext, &image, &out_blocks, &any_failures] + { + if (any_failures) + return; + + astcenc_error status = astcenc_compress_image( + pContext, + &image, + &swizzle, + reinterpret_cast(out_blocks.get_ptr()), + out_blocks.size_in_bytes(), + thread_index); + + if (status != ASTCENC_SUCCESS) + { + any_failures.store(true); + } + } + ); + } + + job_pool.wait_for_all(); + + if (any_failures) + return false; + + astcenc_compress_reset(pContext); + + return true; +} + +static bool ldr_astc_block_encode_image_astcenc( + const image& orig_img, + const ldr_astc_block_encode_image_high_level_config& enc_cfg, + const astc_ldr_encode_config& global_cfg, + ldr_astc_block_encode_image_output& enc_out, + uint32_t max_candidate_limit) +{ + if (enc_cfg.m_debug_output) + fmt_debug_printf("ldr_astc_block_encode_image_astcenc:\n"); + + const uint32_t block_width = enc_cfg.m_block_width, block_height = enc_cfg.m_block_height; + + //const uint32_t width = orig_img.get_width(), height = orig_img.get_height(); + //const uint32_t total_pixels = width * height; + const uint32_t total_block_pixels = enc_cfg.m_block_width * enc_cfg.m_block_height; + const uint32_t num_blocks_x = orig_img.get_block_width(enc_cfg.m_block_width); + const uint32_t num_blocks_y = orig_img.get_block_height(enc_cfg.m_block_height); + const uint32_t total_blocks = num_blocks_x * num_blocks_y; + + if (enc_cfg.m_debug_output) + { + fmt_debug_printf("\nASTC base bitrate: {3.3} bpp\n", 128.0f / (float)(enc_cfg.m_block_width * enc_cfg.m_block_height)); + + fmt_debug_printf("ASTC block size: {}x{}\n", enc_cfg.m_block_width, enc_cfg.m_block_height); + + fmt_debug_printf("Image has alpha: {}\n", orig_img.has_alpha()); + + fmt_debug_printf("max_candidate_limit: {}\n", max_candidate_limit); + } + + // We don't use this here, but the supercompressors use these tables. + // TODO: The transcoder already creates all this stuff for each block size. + astc_ldr::partitions_data* pPart_data_p2 = &enc_out.m_part_data_p2; + pPart_data_p2->init(2, enc_cfg.m_block_width, enc_cfg.m_block_height, BASISU_USE_LSH2 == 0, BASISU_USE_LSH2 != 0); + + astc_ldr::partitions_data* pPart_data_p3 = &enc_out.m_part_data_p3; + pPart_data_p3->init(3, enc_cfg.m_block_width, enc_cfg.m_block_height, BASISU_USE_LSH3 == 0, BASISU_USE_LSH3 != 0); + + basisu::vector& encoder_trial_modes = enc_out.m_encoder_trial_modes; + encoder_trial_modes.reserve(4096); + + basist::astc_ldr_t::grouped_trial_modes& grouped_encoder_trial_modes = enc_out.m_grouped_encoder_trial_modes; + basist::astc_ldr_t::create_encoder_trial_modes_table(block_width, block_height, encoder_trial_modes, grouped_encoder_trial_modes, enc_cfg.m_debug_output, false); + + const uint32_t block_size_index = astc_helpers::get_block_size_index(block_width, block_height); + + assert(enc_cfg.m_pJob_pool); + job_pool& job_pool = *enc_cfg.m_pJob_pool; + const uint32_t num_threads = (uint32_t)job_pool.get_total_threads(); + + float astcenc_quality = ASTCENC_PRE_FASTEST; + + switch (global_cfg.m_effort_level) + { + case 0: + astcenc_quality = ASTCENC_PRE_FASTEST; + break; + case 1: + astcenc_quality = lerp(ASTCENC_PRE_FASTEST, ASTCENC_PRE_FAST, 1.0f / 3.0f); + break; + case 2: + astcenc_quality = lerp(ASTCENC_PRE_FASTEST, ASTCENC_PRE_FAST, 2.0f / 3.0f); + break; + case 3: + astcenc_quality = ASTCENC_PRE_FAST; + break; + case 4: + astcenc_quality = (ASTCENC_PRE_FAST + ASTCENC_PRE_MEDIUM) * .5f; + break; + case 5: + astcenc_quality = ASTCENC_PRE_MEDIUM; + break; + case 6: + astcenc_quality = (ASTCENC_PRE_MEDIUM + ASTCENC_PRE_THOROUGH) * .5f; + break; + case 7: + astcenc_quality = ASTCENC_PRE_THOROUGH; + break; + case 8: + astcenc_quality = (ASTCENC_PRE_THOROUGH + ASTCENC_PRE_VERYTHOROUGH) * .5f; + break; + case 9: + astcenc_quality = ASTCENC_PRE_VERYTHOROUGH; + break; + case 10: + default: + astcenc_quality = ASTCENC_PRE_EXHAUSTIVE; + break; + } + + std::vector< std::vector > block_candidates(total_blocks); + + astcenc_context* pContext = comp_astc_init_ldr( + block_width, + block_height, + enc_cfg.m_cem_enc_params.m_decode_mode_srgb, + astcenc_quality, + num_threads, + global_cfg.m_force_disable_subsets ? 1 : 3, + true, enc_cfg.m_cem_enc_params.m_comp_weights, + global_cfg.m_force_disable_rgb_dual_plane, + num_blocks_x, &block_candidates); + + if (!pContext) + { + assert(0); + return false; + } + + if (enc_cfg.m_debug_output) + fmt_debug_printf("Compressing with astcenc\n"); + + interval_timer itm; + itm.start(); + + const bool comp_status = comp_astc_image_u8( + pContext, + (const uint8_t*)orig_img.get_ptr(), + orig_img.get_width(), + orig_img.get_height(), + block_width, + block_height, + enc_out.m_packed_phys_blocks, + job_pool); + + if (enc_cfg.m_debug_output) + fmt_debug_printf("Total time: {} seconds\n", itm.get_elapsed_secs()); + + comp_astc_deinit(pContext); + pContext = nullptr; + + if (!comp_status) + { + fmt_error_printf("comp_astc_image_u8() failed!\n"); + + assert(0); + return false; + } + + if (enc_cfg.m_debug_output) + fmt_debug_printf("Compressing with astcenc: OK\n"); + + enc_out.m_image_block_info.resize(0, 0); + enc_out.m_image_block_info.resize(num_blocks_x, num_blocks_y); + + basist::astc_ldr_t::grid_weight_dct grid_coder; + if (enc_cfg.m_use_dct) + grid_coder.init(block_width, block_height); + + basist::astc_ldr_t::fvec dct_temp; + + uint32_t total_suboptimal_cem_encodings = 0; + + uint32_t min_candidates = UINT32_MAX, max_candidates = 0, total_candidates = 0; + + uint32_t total_bad_candidates = 0; + + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + color_rgba block_pixels[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + orig_img.extract_block_clamped(block_pixels, bx * block_width, by * block_height, block_width, block_height); + + const std::vector& cand_blocks = block_candidates[bx + by * num_blocks_x]; + + if (!cand_blocks.size()) + { + assert(0); + fmt_error_printf("astcenc gave us no candidates for a block.\n"); + return false; + } + + min_candidates = minimum((uint32_t)cand_blocks.size(), min_candidates); + max_candidates = maximum((uint32_t)cand_blocks.size(), max_candidates); + total_candidates += (uint32_t)cand_blocks.size(); + + uint64_t best_sse = UINT64_MAX; + uint32_t best_cand_index = 0; + + ldr_astc_block_encode_image_output::block_info& blk_info = enc_out.m_image_block_info(bx, by); + + for (uint32_t cand_index = 0; cand_index < cand_blocks.size(); cand_index++) + { + astc_helpers::astc_block* pOrig_packed_block = (astc_helpers::astc_block * )cand_blocks[cand_index].m_bytes; + + const uint32_t cur_out_block_index = blk_info.m_out_blocks.size_u32(); + + encode_block_output* pEnc_block_output = blk_info.m_out_blocks.enlarge(1); + pEnc_block_output->clear(); + + pEnc_block_output->m_blur_id = BLUR_ID_ASTCENC; + + astc_helpers::log_astc_block& log_blk = pEnc_block_output->m_log_blk; + + if (!astc_helpers::unpack_block(pOrig_packed_block, log_blk, block_width, block_height, true)) + { + fmt_error_printf("astcenc produced an invalid physical ASTC block!\n"); + + assert(0); + return false; + } + + if (!astc_helpers::is_block_xuastc_ldr(log_blk)) + { + // Ideally this doesn't happen if we've modified astcenc correctly to ignore non-XUASTC configs. + if (!total_bad_candidates) + fmt_printf("WARNING: astcenc produced a non-XUASTC LDR compliant candidate! (1). Ignoring it.\n"); + + blk_info.m_out_blocks.pop_back(); + + total_bad_candidates++; + continue; + } + + // We can slam this to disabled because if its otherwise valid XUASTC LDR it doesn't matter. (astcenc does use them, but most of the time they don't change the BISE endpoint/weight levels.) + // Because we map to valid XUASTC LDR trial mode configs, if slamming this disabled results in an invalid non-suboptimal CEM config, we'll not be able to find the config. + if (log_blk.m_uses_suboptimal_cem_encoding) + { + total_suboptimal_cem_encodings++; + log_blk.m_uses_suboptimal_cem_encoding = false; + } + + if (log_blk.m_solid_color_flag_ldr) + { + pEnc_block_output->m_trial_mode_index = -1; + } + else + { + uint32_t cem_to_find = log_blk.m_color_endpoint_modes[0]; + if (cem_to_find == astc_helpers::CEM_LDR_RGB_BASE_PLUS_OFFSET) + cem_to_find = astc_helpers::CEM_LDR_RGB_DIRECT; + else if (cem_to_find == astc_helpers::CEM_LDR_RGBA_BASE_PLUS_OFFSET) + cem_to_find = astc_helpers::CEM_LDR_RGBA_DIRECT; + + const int ccs_to_find = log_blk.m_dual_plane ? log_blk.m_color_component_selector : -1; + BASISU_NOTE_UNUSED(ccs_to_find); + + const basisu::uint_vec& tms = basist::astc_ldr_t::get_tm_candidates( + basist::astc_ldr_t::g_grouped_encoder_trial_modes[block_size_index], + cem_to_find, log_blk.m_num_partitions - 1, log_blk.m_dual_plane ? log_blk.m_color_component_selector + 1 : 0, + basist::astc_ldr_t::calc_grid_size_val(log_blk.m_grid_width, log_blk.m_grid_height, block_width, block_height), + basist::astc_ldr_t::calc_grid_aniso_val(log_blk.m_grid_width, log_blk.m_grid_height, block_width, block_height)); + + uint32_t tm_index = 0, tms_index = 0; + for (tms_index = 0; tms_index < tms.size(); tms_index++) + { + tm_index = tms[tms_index]; + + const auto& tm = basist::astc_ldr_t::g_encoder_trial_modes[block_size_index][tm_index]; + + assert(tm.m_cem == cem_to_find); + assert(tm.m_num_parts == log_blk.m_num_partitions); + assert(tm.m_ccs_index == ccs_to_find); + + if ((tm.m_endpoint_ise_range == log_blk.m_endpoint_ise_range) && (tm.m_weight_ise_range == log_blk.m_weight_ise_range)) + { + if ((tm.m_grid_width == log_blk.m_grid_width) && (tm.m_grid_height == log_blk.m_grid_height)) + { + break; + } + } + } // tms_index + + if (tms_index == tms.size()) + { + // Shouldn't happen if we've properly modified astcenc correctly to use the XUASTC LDR compliant configs. + if (!total_bad_candidates) + fmt_printf("WARNING: astcenc produced a non-XUASTC LDR compliant candidate - but we couldn't find this block's config! (2) Ignoring it.\n"); + + blk_info.m_out_blocks.pop_back(); + + total_bad_candidates++; + continue; + } + + pEnc_block_output->m_trial_mode_index = basisu::safe_cast_int16(tm_index); + + if (enc_cfg.m_use_dct) + { + //const basist::astc_ldr_t::astc_block_grid_data* pGrid_data = basist::astc_ldr_t::find_astc_block_grid_data(block_width, block_height, new_log_block.m_grid_width, new_log_block.m_grid_height); + + const uint32_t num_planes = (log_blk.m_dual_plane ? 2 : 1); + + uint32_t total_empty_planes = 0; + + for (uint32_t plane_index = 0; plane_index < num_planes; plane_index++) + { + basist::astc_ldr_t::dct_syms& syms = pEnc_block_output->m_packed_dct_plane_data[plane_index]; + + code_block_weights(grid_coder, enc_cfg.m_base_q, plane_index, log_blk, syms, dct_temp); + + // ensure existing weights get blown away + for (uint32_t i = 0; i < (uint32_t)(log_blk.m_grid_width * log_blk.m_grid_height); i++) + log_blk.m_weights[i * num_planes + plane_index] = 0; + + bool dec_status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, log_blk, nullptr, nullptr, dct_temp, &syms); + + assert(dec_status); + if (!dec_status) + { + error_printf("grid_coder.decode_block_weights() failed!\n"); + return false; + } + + // check for all-zero AC's + if (syms.m_coeffs.size() == 1) + { + if ((1 + syms.m_coeffs[0].m_num_zeros) == (log_blk.m_grid_width * log_blk.m_grid_height)) + { + total_empty_planes++; + } + } + } + + if ((log_blk.m_num_partitions == 1) && (total_empty_planes == num_planes)) + { + // entire block post-quantization is DC only (no non-zero AC), switch to void-extent + uint32_t sum_r = 0, sum_g = 0, sum_b = 0, sum_a = 0; + for (uint32_t i = 0; i < total_block_pixels; i++) + { + sum_r += block_pixels[i].r; + sum_g += block_pixels[i].g; + sum_b += block_pixels[i].b; + sum_a += block_pixels[i].a; + } + + sum_r = (sum_r + (total_block_pixels >> 1)) / total_block_pixels; + sum_g = (sum_g + (total_block_pixels >> 1)) / total_block_pixels; + sum_b = (sum_b + (total_block_pixels >> 1)) / total_block_pixels; + sum_a = (sum_a + (total_block_pixels >> 1)) / total_block_pixels; + + astc_helpers::set_ldr_solid_block(log_blk, sum_r, sum_g, sum_b, sum_a); + + pEnc_block_output->m_trial_mode_index = -1; + } + } + } + + uint64_t total_err = 0; + + { + color_rgba dec_block_pixels[astc_ldr::ASTC_LDR_MAX_BLOCK_PIXELS]; + + bool dec_status = astc_helpers::decode_block_xuastc_ldr(log_blk, dec_block_pixels, block_width, block_height, + enc_cfg.m_cem_enc_params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); + + if (!dec_status) + { + // Shouldn't ever happen + assert(0); + return false; + } + + for (uint32_t i = 0; i < total_block_pixels; i++) + total_err += weighted_color_error(dec_block_pixels[i], block_pixels[i], enc_cfg.m_cem_enc_params); + + pEnc_block_output->m_sse = total_err; + } + + if (total_err < best_sse) + { + best_sse = total_err; + best_cand_index = cur_out_block_index; + } + + } // cand_index + + if (!blk_info.m_out_blocks.size()) + { + //assert(0); + error_printf("Couldn't find any compliant astcenc candidates at block {}x{}\n", bx, by); + return false; + } + + astc_helpers::astc_block* pFinal_packed_block = &enc_out.m_packed_phys_blocks(bx, by); + + bool pack_status = astc_helpers::pack_astc_block(*pFinal_packed_block, blk_info.m_out_blocks[best_cand_index].m_log_blk); + if (!pack_status) + { + //assert(0); + error_printf("Failed packing final physical ASTC block at {}x{}\n", bx, by); + return false; + } + + blk_info.m_packed_out_block_index = best_cand_index; + + const uint64_t check_wsse = blk_info.m_out_blocks[best_cand_index].m_sse; + BASISU_NOTE_UNUSED(check_wsse); + + enforce_max_candidate_limit(blk_info, max_candidate_limit); + + best_cand_index = blk_info.m_packed_out_block_index; // may have changed + + assert(check_wsse == blk_info.m_out_blocks[best_cand_index].m_sse); + + } // bx + + } // by + + if (total_bad_candidates) + fmt_printf("WARNING: ldr_astc_block_encode_image_astcenc ignored {} invalid (non-XUASTC) candidates.\n", total_bad_candidates); + + if (enc_cfg.m_debug_output) + { + display_candidate_statistics(enc_out); + + fmt_debug_printf("Total candidates: {}, Avg candidates per block: {}, Min candidates: {}, Max candidates: {}\n", + total_candidates, (float)total_candidates / (float)total_blocks, min_candidates, max_candidates); + + if (total_suboptimal_cem_encodings) + fmt_debug_printf("Total blocks using suboptimal CEM encodings: {}\n", total_suboptimal_cem_encodings); + + if (enc_cfg.m_debug_output) + fmt_debug_printf("ldr_astc_block_encode_image_astcenc: done\n"); + } + + return true; +} +#endif // BASISU_SUPPORT_ASTCENC + +// -1 for solid, -2 for error, >= 0 if found +static int find_tm_index(uint32_t block_width, uint32_t block_height, uint32_t block_size_index, const astc_helpers::log_astc_block &log_blk) +{ + assert(astc_helpers::is_block_xuastc_ldr(log_blk)); + + if (log_blk.m_solid_color_flag_ldr) + { + // weighted SSE should be zero, the block should always be solid here + return -1; + } + + uint32_t cem_to_find = log_blk.m_color_endpoint_modes[0]; + if (cem_to_find == astc_helpers::CEM_LDR_RGB_BASE_PLUS_OFFSET) + cem_to_find = astc_helpers::CEM_LDR_RGB_DIRECT; + else if (cem_to_find == astc_helpers::CEM_LDR_RGBA_BASE_PLUS_OFFSET) + cem_to_find = astc_helpers::CEM_LDR_RGBA_DIRECT; + + const int ccs_to_find = log_blk.m_dual_plane ? log_blk.m_color_component_selector : -1; + BASISU_NOTE_UNUSED(ccs_to_find); + + const basisu::uint_vec& tms = basist::astc_ldr_t::get_tm_candidates( + basist::astc_ldr_t::g_grouped_encoder_trial_modes[block_size_index], + cem_to_find, log_blk.m_num_partitions - 1, log_blk.m_dual_plane ? log_blk.m_color_component_selector + 1 : 0, + basist::astc_ldr_t::calc_grid_size_val(log_blk.m_grid_width, log_blk.m_grid_height, block_width, block_height), + basist::astc_ldr_t::calc_grid_aniso_val(log_blk.m_grid_width, log_blk.m_grid_height, block_width, block_height)); + + uint32_t tm_index = 0, tms_index = 0; + for (tms_index = 0; tms_index < tms.size(); tms_index++) + { + tm_index = tms[tms_index]; + + const auto& tm = basist::astc_ldr_t::g_encoder_trial_modes[block_size_index][tm_index]; + + assert(tm.m_cem == cem_to_find); + assert(tm.m_num_parts == log_blk.m_num_partitions); + assert(tm.m_ccs_index == ccs_to_find); + + if ((tm.m_endpoint_ise_range == log_blk.m_endpoint_ise_range) && (tm.m_weight_ise_range == log_blk.m_weight_ise_range)) + { + if ((tm.m_grid_width == log_blk.m_grid_width) && (tm.m_grid_height == log_blk.m_grid_height)) + { + break; + } + } + } // tms_index + + if (tms_index == tms.size()) + { + assert(0); + return -2; + } + + return tm_index; +} + +static bool ldr_astc_block_encode_image_astcf( + const image& orig_img, + const ldr_astc_block_encode_image_high_level_config& enc_cfg, + const astc_ldr_encode_config& global_cfg, + ldr_astc_block_encode_image_output& enc_out, + uint32_t max_candidate_limit) +{ + if (enc_cfg.m_debug_output) + fmt_debug_printf("ldr_astc_block_encode_image_astcf:\n"); + + const uint32_t block_width = enc_cfg.m_block_width, block_height = enc_cfg.m_block_height; + + const int block_dim_index = astc_helpers::find_astc_block_size_index(block_width, block_height); + assert((block_dim_index >= 0) && (block_dim_index < (int)astc_helpers::NUM_ASTC_BLOCK_SIZES)); + + const uint32_t width = orig_img.get_width(), height = orig_img.get_height(); + const uint32_t total_pixels = width * height; + const uint32_t total_block_pixels = enc_cfg.m_block_width * enc_cfg.m_block_height; + const uint32_t num_blocks_x = orig_img.get_block_width(enc_cfg.m_block_width); + const uint32_t num_blocks_y = orig_img.get_block_height(enc_cfg.m_block_height); + const uint32_t total_blocks = num_blocks_x * num_blocks_y; + const bool has_alpha = orig_img.has_alpha(); + + if (enc_cfg.m_debug_output) + { + fmt_debug_printf("\nASTC base bitrate: {3.3} bpp\n", 128.0f / (float)(enc_cfg.m_block_width * enc_cfg.m_block_height)); + + fmt_debug_printf("ASTC block size: {}x{}\n", enc_cfg.m_block_width, enc_cfg.m_block_height); + + fmt_debug_printf("Image has alpha: {}\n", has_alpha); + + fmt_debug_printf("max_candidate_limit: {}\n", max_candidate_limit);; + } + + // We don't use this here, but the supercompressors use these tables. + // TODO: The transcoder already creates all this stuff for each block size. + astc_ldr::partitions_data* pPart_data_p2 = &enc_out.m_part_data_p2; + pPart_data_p2->init(2, enc_cfg.m_block_width, enc_cfg.m_block_height, BASISU_USE_LSH2 == 0, BASISU_USE_LSH2 != 0); + + astc_ldr::partitions_data* pPart_data_p3 = &enc_out.m_part_data_p3; + pPart_data_p3->init(3, enc_cfg.m_block_width, enc_cfg.m_block_height, BASISU_USE_LSH3 == 0, BASISU_USE_LSH3 != 0); + + basisu::vector& encoder_trial_modes = enc_out.m_encoder_trial_modes; + encoder_trial_modes.reserve(4096); + + basist::astc_ldr_t::grouped_trial_modes& grouped_encoder_trial_modes = enc_out.m_grouped_encoder_trial_modes; + basist::astc_ldr_t::create_encoder_trial_modes_table(block_width, block_height, encoder_trial_modes, grouped_encoder_trial_modes, enc_cfg.m_debug_output, false); + + vector2D& packed_blocks = enc_out.m_packed_phys_blocks; + packed_blocks.resize(num_blocks_x, num_blocks_y); + memset(packed_blocks.get_ptr(), 0, packed_blocks.size_in_bytes()); + + enc_out.m_image_block_info.resize(0, 0); + enc_out.m_image_block_info.resize(num_blocks_x, num_blocks_y); + + uint32_t max_subsets = 1; + bool use_subsets = false; + float var_thresh_2subsets = squaref(6.0f); + float var_thresh_3subsets = squaref(6.0f); + uint32_t num_subset_carriers = 1, num_subset_pats = 1; + uint32_t dot_thresh_fract_index_2subsets = 0; + + bool weight_polishing = (global_cfg.m_effort_level >= 2); + uint32_t num_candidates = 1; + + const float feffort_level = global_cfg.m_effort_level * (1.0f / 10.0f); + +#if 0 + if (block_width * block_height <= 25) + { + // tiny blocks make the DCT related candidate eval quite slow + if (has_alpha) + num_candidates = clamp((int)std::round(lerp(8.0f, 24.0f, feffort_level)), 1, 64); + else + num_candidates = clamp((int)std::round(lerp(1.0f, 16.0f, feffort_level)), 1, 64); + } + else +#endif + { + if (has_alpha) + num_candidates = clamp((int)std::round(lerp(16.0f, 48.0f, feffort_level)), 1, 64); + else + num_candidates = clamp((int)std::round(lerp(1.0f, 48.0f, feffort_level)), 1, 64); + } + + if (global_cfg.m_effort_level) + { + if (block_width * block_height <= 25) + { + num_subset_carriers = clamp((int)std::round(lerp(1.0f, 2.0f, feffort_level)), 1, 3); + num_subset_pats = clamp((int)std::round(lerp(1.0f, 6.0f, feffort_level)), 1, 16); + } + else + { + num_subset_carriers = clamp((int)std::round(lerp(1.0f, 3.0f, feffort_level)), 1, 3); + num_subset_pats = clamp((int)std::round(lerp(1.0f, 16.0f, feffort_level)), 1, 16); + } + + if (global_cfg.m_effort_level == 8) + dot_thresh_fract_index_2subsets = 1; + else if (global_cfg.m_effort_level == 9) + dot_thresh_fract_index_2subsets = 2; + else if (global_cfg.m_effort_level == 10) + dot_thresh_fract_index_2subsets = 3; + + use_subsets = (num_subset_carriers > 0) && (num_subset_pats > 0); + if (use_subsets) + { + max_subsets = (global_cfg.m_effort_level > 1) ? 3 : 2; + } + } + + if (global_cfg.m_force_disable_subsets) + use_subsets = false; + + basist::astc_ldr_t::grid_weight_dct grid_coder; + if (enc_cfg.m_use_dct) + grid_coder.init(block_width, block_height); + + assert(enc_cfg.m_pJob_pool); + job_pool& job_pool = *enc_cfg.m_pJob_pool; + const uint32_t num_threads = (uint32_t)job_pool.get_total_threads(); + + std::atomic cur_row; + cur_row.store(0); + + std::atomic encoder_failed_flag; + encoder_failed_flag.store(false); + + astc_ldrf::subset_enc_context ctx; + + bool status = astc_ldrf::init_single_subset_context( + ctx, + block_width, block_height, + enc_cfg.m_cem_enc_params.m_decode_mode_srgb ? astc_helpers::decode_mode::cDecodeModeSRGB8 : astc_helpers::decode_mode::cDecodeModeLDR8, + enc_cfg.m_cem_enc_params.m_comp_weights, + num_candidates, 2, global_cfg.m_force_disable_rgb_dual_plane, has_alpha, weight_polishing); + + if (global_cfg.m_effort_level <= 2) + { + // faster/weaker subset encoding + ctx.m_use_method2 = false; + } + + if (global_cfg.m_effort_level <= 6) + { + // both very rarely worth the effort + ctx.m_higher_effort_bc = false; + ctx.m_try_base_ofs = false; + } + + if ((status) && (use_subsets)) + { + status = astc_ldrf::init_multi_subset_context(ctx, max_subsets, num_subset_carriers, num_subset_pats, + var_thresh_2subsets, dot_thresh_fract_index_2subsets, var_thresh_3subsets, + pPart_data_p2, pPart_data_p3); + } + + if (!status) + { + fmt_error_printf("astc_ldrf::init_single_subset_context() failed!\n"); + return false; + } + + if (enc_cfg.m_debug_output) + { + fmt_debug_printf("num_candidates: {}, use subsets: {}, num_2subset_carriers: {}, num_2subset_pats: {}, 2 subsets threshold var: {}, dot_thresh_fract_index: {}, 3 subsets threshold var: {}, use subsets method1: {}, use subsets method2: {}, higher effort BC: {}, try base ofs: {}, max subsets: {}:\n", + num_candidates, use_subsets, num_subset_carriers, num_subset_pats, + var_thresh_2subsets, dot_thresh_fract_index_2subsets, var_thresh_3subsets, + ctx.m_use_method1, ctx.m_use_method2, ctx.m_higher_effort_bc, ctx.m_try_base_ofs, ctx.m_max_subsets); + } + + for (uint32_t job_index = 0; job_index < num_threads; job_index++) + { + job_pool.add_job([job_index, num_threads, has_alpha, width, height, total_pixels, num_blocks_x, num_blocks_y, block_width, block_height, block_dim_index, total_blocks, total_block_pixels, + num_candidates, use_subsets, weight_polishing, + &cur_row, &encoder_failed_flag, &ctx, max_candidate_limit, + &orig_img, &enc_cfg, &encoder_trial_modes, &grid_coder, &grouped_encoder_trial_modes, &enc_out] + { + BASISU_NOTE_UNUSED(job_index); BASISU_NOTE_UNUSED(num_threads); BASISU_NOTE_UNUSED(has_alpha); BASISU_NOTE_UNUSED(width); BASISU_NOTE_UNUSED(height); BASISU_NOTE_UNUSED(total_pixels); BASISU_NOTE_UNUSED(total_blocks); BASISU_NOTE_UNUSED(weight_polishing); BASISU_NOTE_UNUSED(encoder_trial_modes); BASISU_NOTE_UNUSED(grouped_encoder_trial_modes); + if (encoder_failed_flag) + return; + + basist::astc_ldr_t::fvec dct_temp; + + astc_ldrf::astc_lblock_vec all_candidates; + all_candidates.reserve(num_candidates); + + astc_ldrf::subset_enc_thread_context thread_ctx; + + uint_vec sorted_indices; + sorted_indices.reserve(max_candidate_limit); + + for (; ; ) + { + if (encoder_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++) + { + if (encoder_failed_flag) + return; + + ldr_astc_block_encode_image_output::block_info& blk_info = enc_out.m_image_block_info(bx, by); + + color_rgba block_pixels[astc_helpers::MAX_BLOCK_PIXELS]; + orig_img.extract_block_clamped(block_pixels, bx * block_width, by * block_height, block_width, block_height); + + all_candidates.resize(0); + + astc_helpers::log_astc_block best_lblock; + if (use_subsets) + { + astc_ldrf::compress_block_subsets(ctx, thread_ctx, (const uint8_t*)block_pixels, best_lblock, &all_candidates); + } + else + { + astc_ldrf::compress_single_subset(ctx, (const uint8_t*)block_pixels, best_lblock, &all_candidates, false); + } + + if (encoder_failed_flag) + return; + + if (!all_candidates.size()) + { + fmt_error_printf("compress_block: returned no candidates!\n"); + + encoder_failed_flag.store(true); + return; + } + + uint64_t best_cand_err = UINT64_MAX; + uint32_t best_cand_index = 0; + + for (uint32_t cand_index = 0; cand_index < all_candidates.size(); cand_index++) + { + const astc_helpers::log_astc_block& candidate_log_blk = all_candidates[cand_index]; + + encode_block_output* pEnc_block_output = blk_info.m_out_blocks.enlarge(1); + pEnc_block_output->clear(); + + pEnc_block_output->m_blur_id = BLUR_ID_ASTCF; + + astc_helpers::log_astc_block& log_blk = pEnc_block_output->m_log_blk; + + log_blk = candidate_log_blk; + astc_ldrf::convert_rank_lblock_to_ise(log_blk); + + int tm_index = find_tm_index(block_width, block_height, block_dim_index, log_blk); + if (tm_index == -2) + { + fmt_error_printf("compress_block: invalid candidate!\n"); + + encoder_failed_flag.store(true); + return; + } + + if ((tm_index >= 0) && (enc_cfg.m_use_dct)) + { + const uint32_t num_planes = (log_blk.m_dual_plane ? 2 : 1); + + uint32_t total_empty_planes = 0; + + for (uint32_t plane_index = 0; plane_index < num_planes; plane_index++) + { + basist::astc_ldr_t::dct_syms& syms = pEnc_block_output->m_packed_dct_plane_data[plane_index]; + + code_block_weights(grid_coder, enc_cfg.m_base_q, plane_index, log_blk, syms, dct_temp); + + // ensure existing weights get blown away + for (uint32_t i = 0; i < (uint32_t)(log_blk.m_grid_width * log_blk.m_grid_height); i++) + log_blk.m_weights[i * num_planes + plane_index] = 0; + + bool dec_status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, log_blk, nullptr, nullptr, dct_temp, &syms); + + assert(dec_status); + if (!dec_status) + { + error_printf("grid_coder.decode_block_weights() failed!\n"); + encoder_failed_flag.store(true); + return; + } + + // check for all-zero AC's + if (syms.m_coeffs.size() == 1) + { + if ((1 + syms.m_coeffs[0].m_num_zeros) == (log_blk.m_grid_width * log_blk.m_grid_height)) + { + total_empty_planes++; + } + } + } + + if ((log_blk.m_num_partitions == 1) && (total_empty_planes == num_planes)) + { + // entire block post-quantization is DC only (no non-zero AC), switch to void-extent + uint32_t sum_r = 0, sum_g = 0, sum_b = 0, sum_a = 0; + for (uint32_t i = 0; i < total_block_pixels; i++) + { + sum_r += block_pixels[i].r; + sum_g += block_pixels[i].g; + sum_b += block_pixels[i].b; + sum_a += block_pixels[i].a; + } + + const uint32_t round = total_block_pixels >> 1; + sum_r = (sum_r + round) / total_block_pixels; + sum_g = (sum_g + round) / total_block_pixels; + sum_b = (sum_b + round) / total_block_pixels; + sum_a = (sum_a + round) / total_block_pixels; + + astc_helpers::set_ldr_solid_block(log_blk, sum_r, sum_g, sum_b, sum_a); + + tm_index = -1; + } + + } // if (enc_cfg.m_use_dct) + + pEnc_block_output->m_trial_mode_index = basisu::safe_cast_int16(tm_index); + + // unpack the block and compute actual WSSE + { + color_rgba dec_block_pixels[astc_helpers::MAX_BLOCK_PIXELS]; + + bool dec_status = astc_helpers::decode_block_xuastc_ldr(log_blk, dec_block_pixels, block_width, block_height, + enc_cfg.m_cem_enc_params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); + + if (!dec_status) + { + // Shouldn't ever happen + assert(0); + + error_printf("decode_block_xuastc_ldr() failed!\n"); + + encoder_failed_flag.store(true); + return; + } + + uint64_t total_err = 0; + + for (uint32_t i = 0; i < total_block_pixels; i++) + total_err += weighted_color_error(dec_block_pixels[i], block_pixels[i], enc_cfg.m_cem_enc_params); + + pEnc_block_output->m_sse = total_err; + + if (total_err < best_cand_err) + { + best_cand_err = total_err; + best_cand_index = cand_index; + } + } + + } // cand_index + + if (blk_info.m_out_blocks.size() > max_candidate_limit) + { + const uint64_t check_wsse = blk_info.m_out_blocks[best_cand_index].m_sse; + BASISU_NOTE_UNUSED(check_wsse); + + // There were just too many candidates returned - we'll risk running out of RAM in WASM. So sort them and just keep the top X. + sorted_indices.resize(blk_info.m_out_blocks.size_u32()); + + for (uint32_t i = 0; i < sorted_indices.size(); i++) + sorted_indices[i] = i; + + std::sort(sorted_indices.begin(), sorted_indices.end(), + [&blk_info](const uint32_t a, const uint32_t b) + { + if (blk_info.m_out_blocks[a].m_sse < blk_info.m_out_blocks[b].m_sse) + return true; + return false; + } + ); + + basisu::vector shrunk_out_blocks(max_candidate_limit); + for (uint32_t i = 0; i < max_candidate_limit; i++) + shrunk_out_blocks[i] = blk_info.m_out_blocks[sorted_indices[i]]; + + blk_info.m_out_blocks.swap(shrunk_out_blocks); + best_cand_index = 0; + + assert(check_wsse == blk_info.m_out_blocks[best_cand_index].m_sse); + } + + blk_info.m_packed_out_block_index = best_cand_index; + + const astc_helpers::log_astc_block& best_log_blk = blk_info.m_out_blocks[best_cand_index].m_log_blk; + + bool pack_status = astc_helpers::pack_astc_block(enc_out.m_packed_phys_blocks(bx, by), best_log_blk); + if (!pack_status) + { + assert(0); + encoder_failed_flag.store(true); + return; + } + } // bx + + } // for ( ; ; ) + + } // lambda function + ); + + } // job_index + + job_pool.wait_for_all(); + + if (encoder_failed_flag) + { + fmt_error_printf("ldr_astc_block_encode_image_astcf: Main compressor block loop failed!\n"); + return false; + } + + if (enc_cfg.m_debug_output) + { + display_candidate_statistics(enc_out); + + fmt_debug_printf("ldr_astc_block_encode_image_astcf: OK\n"); + } + + return true; +} const uint_vec& separate_tm_index(uint32_t block_width, uint32_t block_height, const basist::astc_ldr_t::grouped_trial_modes& grouped_enc_trial_modes, const basist::astc_ldr_t::trial_mode& tm, uint32_t& cem_index, uint32_t& subset_index, uint32_t& ccs_index, uint32_t& grid_size, uint32_t& grid_aniso) @@ -7503,7 +11067,7 @@ static bool compare_log_blocks_for_equality(const astc_helpers::log_astc_block& return false; } -void configure_encoder_effort_level(int level, ldr_astc_block_encode_image_high_level_config& cfg) +static void configure_encoder_effort_level(int level, ldr_astc_block_encode_image_high_level_config& cfg) { switch (level) { @@ -7516,6 +11080,9 @@ void configure_encoder_effort_level(int level, ldr_astc_block_encode_image_high_ cfg.m_use_blue_contraction = true; cfg.m_use_base_ofs = true; + cfg.m_encode_trial_early_out_thresh = 0.01f; + cfg.m_encode_trial_subsets_early_out_thresh = 0.01f; + cfg.m_force_all_dual_plane_chan_evals = true; cfg.m_filter_by_pca_angles_flag = false; @@ -7556,6 +11123,8 @@ void configure_encoder_effort_level(int level, ldr_astc_block_encode_image_high_ cfg.m_grid_hv_filtering = false; cfg.m_low_freq_block_filtering = false; + cfg.m_cem_enc_params.m_use_exhaustive_weight_eval = true; + break; } case 9: @@ -7567,12 +11136,15 @@ void configure_encoder_effort_level(int level, ldr_astc_block_encode_image_high_ cfg.m_use_blue_contraction = true; cfg.m_use_base_ofs = true; + cfg.m_encode_trial_early_out_thresh = 0.01f; + cfg.m_encode_trial_subsets_early_out_thresh = 0.01f; + cfg.m_force_all_dual_plane_chan_evals = false; cfg.m_filter_by_pca_angles_flag = true; - cfg.m_superbucket_max_to_retain[0] = 8; - cfg.m_superbucket_max_to_retain[1] = 16; - cfg.m_superbucket_max_to_retain[2] = 32; + cfg.m_superbucket_max_to_retain[0] = 16; + cfg.m_superbucket_max_to_retain[1] = 32; + cfg.m_superbucket_max_to_retain[2] = 64; cfg.m_base_parts2 = 32; cfg.m_base_parts3 = 32; @@ -7583,18 +11155,18 @@ void configure_encoder_effort_level(int level, ldr_astc_block_encode_image_high_ cfg.m_final_shortlist_fraction[1] = 1.0f; cfg.m_final_shortlist_fraction[2] = 1.0f; - cfg.m_final_shortlist_max_size[0] = 4; - cfg.m_final_shortlist_max_size[1] = 12; - cfg.m_final_shortlist_max_size[2] = 24; + cfg.m_final_shortlist_max_size[0] = 16; + cfg.m_final_shortlist_max_size[1] = 32; + cfg.m_final_shortlist_max_size[2] = 64; // Second superpass - cfg.m_second_superpass_fract_to_recompress = .075f; + cfg.m_second_superpass_fract_to_recompress = .15f; cfg.m_superbucket_max_to_retain_p2[0] = 16; cfg.m_superbucket_max_to_retain_p2[1] = 64; cfg.m_superbucket_max_to_retain_p2[2] = 256; - cfg.m_final_shortlist_max_size_p2[0] = 8; - cfg.m_final_shortlist_max_size_p2[1] = 16; - cfg.m_final_shortlist_max_size_p2[2] = 32; + cfg.m_final_shortlist_max_size_p2[0] = 32; + cfg.m_final_shortlist_max_size_p2[1] = 64; + cfg.m_final_shortlist_max_size_p2[2] = 128; cfg.m_base_parts2_p2 = 64; cfg.m_base_parts3_p2 = 64; cfg.m_force_all_dp_chans_p2 = false; @@ -7605,10 +11177,13 @@ void configure_encoder_effort_level(int level, ldr_astc_block_encode_image_high_ cfg.m_early_stop_wpsnr = 75.0f; cfg.m_early_stop2_wpsnr = 70.0f; + cfg.m_cem_enc_params.m_use_exhaustive_weight_eval = true; + break; } case 8: { +#if 0 cfg.m_second_superpass_refinement = true; cfg.m_third_superpass_try_neighbors = true; @@ -7653,6 +11228,61 @@ void configure_encoder_effort_level(int level, ldr_astc_block_encode_image_high_ cfg.m_early_stop_wpsnr = 75.0f; cfg.m_early_stop2_wpsnr = 70.0f; +#endif + + // old 9 + cfg.m_second_superpass_refinement = true; + cfg.m_third_superpass_try_neighbors = true; + + cfg.m_subsets_enabled = true; + cfg.m_use_blue_contraction = true; + cfg.m_use_base_ofs = true; + + cfg.m_encode_trial_early_out_thresh = 0.01f; + cfg.m_encode_trial_subsets_early_out_thresh = 0.01f; + + cfg.m_force_all_dual_plane_chan_evals = false; + cfg.m_filter_by_pca_angles_flag = true; + + cfg.m_superbucket_max_to_retain[0] = 8; + cfg.m_superbucket_max_to_retain[1] = 16; + cfg.m_superbucket_max_to_retain[2] = 32; + + cfg.m_base_parts2 = 32; + cfg.m_base_parts3 = 32; + cfg.m_part2_fraction_to_keep = 2; + cfg.m_part3_fraction_to_keep = 2; + + cfg.m_final_shortlist_fraction[0] = 1.0f; + cfg.m_final_shortlist_fraction[1] = 1.0f; + cfg.m_final_shortlist_fraction[2] = 1.0f; + + cfg.m_final_shortlist_max_size[0] = 4; + cfg.m_final_shortlist_max_size[1] = 12; + cfg.m_final_shortlist_max_size[2] = 24; + + // Second superpass + cfg.m_second_superpass_fract_to_recompress = .15f; + cfg.m_superbucket_max_to_retain_p2[0] = 16; + cfg.m_superbucket_max_to_retain_p2[1] = 64; + cfg.m_superbucket_max_to_retain_p2[2] = 256; + cfg.m_final_shortlist_max_size_p2[0] = 8; + cfg.m_final_shortlist_max_size_p2[1] = 16; + cfg.m_final_shortlist_max_size_p2[2] = 32; + cfg.m_base_parts2_p2 = 64; + cfg.m_base_parts3_p2 = 64; + cfg.m_force_all_dp_chans_p2 = false; + cfg.m_filter_by_pca_angles_flag_p2 = false; + + cfg.m_final_encode_always_try_rgb_direct = false; + + cfg.m_early_stop_wpsnr = 75.0f; + cfg.m_early_stop2_wpsnr = 70.0f; + + cfg.m_cem_enc_params.m_use_exhaustive_weight_eval = true; + + //cfg.m_second_pass_total_weight_refine_passes = 0; + break; } case 7: @@ -8298,6 +11928,16 @@ static bool compress_image_full_zstd( for (uint32_t y = 0; y < num_blocks_y; y++) for (uint32_t x = 0; x < num_blocks_x; x++) coded_blocks(x, y).clear(); + + vector2D input_blocks; + if (global_cfg.m_debug_images) + { + input_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++) + input_blocks(x, y).clear(); + } vector2D prev_block_states(num_blocks_x, num_blocks_y); @@ -8372,6 +12012,11 @@ static bool compress_image_full_zstd( uint32_t best_packed_out_block_index = blk_info.m_packed_out_block_index; + if (global_cfg.m_debug_images) + { + input_blocks(bx, by) = blk_info.m_out_blocks[best_packed_out_block_index].m_log_blk; + } + // check for run if ((use_run_commands_global_enable) && (bx || by)) { @@ -8858,14 +12503,14 @@ static bool compress_image_full_zstd( for (part_iter = 0; part_iter < tm.m_num_parts; part_iter++) { const bool always_repack_flag = false; - bool blue_contraction_clamped_flag = false, base_ofs_clamped_flag = false; + bool blue_contraction_clamped_flag = false, try_direct_encoding_flag = false; bool conv_status = basist::astc_ldr_t::convert_endpoints_across_cems( pTrial_log_blk->m_color_endpoint_modes[0], pTrial_log_blk->m_endpoint_ise_range, pTrial_log_blk->m_endpoints, cur_actual_cem, cur_log_blk.m_endpoint_ise_range, trial_predicted_endpoints[part_iter], always_repack_flag, endpoints_use_bc[part_iter], false, - blue_contraction_clamped_flag, base_ofs_clamped_flag); + blue_contraction_clamped_flag, try_direct_encoding_flag); if (!conv_status) break; @@ -8939,14 +12584,14 @@ static bool compress_image_full_zstd( for (uint32_t part_iter = 0; part_iter < tm.m_num_parts; part_iter++) { const bool always_repack_flag = false; - bool blue_contraction_clamped_flag = false, base_ofs_clamped_flag = false; + bool blue_contraction_clamped_flag = false, try_direct_encoding_flag = false; bool conv_status = basist::astc_ldr_t::convert_endpoints_across_cems( pEndpoint_pred_log_blk->m_color_endpoint_modes[0], pEndpoint_pred_log_blk->m_endpoint_ise_range, pEndpoint_pred_log_blk->m_endpoints, cur_actual_cem, cur_log_blk.m_endpoint_ise_range, predicted_endpoints[part_iter], always_repack_flag, endpoints_use_bc[part_iter], false, - blue_contraction_clamped_flag, base_ofs_clamped_flag); + blue_contraction_clamped_flag, try_direct_encoding_flag); if (!conv_status) { @@ -9030,6 +12675,13 @@ static bool compress_image_full_zstd( for (uint32_t plane_iter = 0; plane_iter < total_planes; plane_iter++) { const basist::astc_ldr_t::dct_syms& syms = blk_out.m_packed_dct_plane_data[plane_iter]; + + if (!syms.m_coeffs.size()) + { + fmt_error_printf("compress_image_full_zstd: internal error - no DCT coeffs\n"); + return false; + } + if (syms.m_max_coeff_mag > basist::astc_ldr_t::DCT_MAX_ARITH_COEFF_MAG) { use_dct = false; @@ -9153,8 +12805,9 @@ static bool compress_image_full_zstd( weight3_bits.flush(); weight4_bits.flush(); - const uint32_t zstd_level = 9; - + // TODO: Make this configurable + const uint32_t zstd_level = (global_cfg.m_effort_level >= 3) ? 19 : 9; + uint8_vec comp_mode, comp_solid_dpcm, comp_endpoint_dpcm_reuse_indices; uint8_vec comp_use_bc_bits, comp_endpoint_dpcm_3bit, comp_endpoint_dpcm_4bit, comp_endpoint_dpcm_5bit, comp_endpoint_dpcm_6bit, comp_endpoint_dpcm_7bit, comp_endpoint_dpcm_8bit; @@ -9245,6 +12898,7 @@ static bool compress_image_full_zstd( if ((global_cfg.m_debug_images) || (global_cfg.m_debug_output)) { + image input_img(width, height); image coded_img(width, height); vector2D phys_blocks(num_blocks_x, num_blocks_y); @@ -9260,7 +12914,7 @@ static bool compress_image_full_zstd( bool status = astc_helpers::decode_block(log_blk, block_pixels, block_width, block_height, enc_cfg.m_cem_enc_params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); if (!status) { - fmt_error_printf("astc_helpers::decode_block() failed\n"); + fmt_error_printf("astc_helpers::decode_block() failed (1)\n"); return false; } @@ -9280,18 +12934,41 @@ static bool compress_image_full_zstd( } coded_img.set_block_clipped(block_pixels, bx * block_width, by * block_height, block_width, block_height); + + if (global_cfg.m_debug_images) + { + // input image + + status = astc_helpers::decode_block(input_blocks(bx, by), block_pixels, block_width, block_height, enc_cfg.m_cem_enc_params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); + if (!status) + { + fmt_error_printf("astc_helpers::decode_block() failed (2)\n"); + return false; + } + + input_img.set_block_clipped(block_pixels, bx * block_width, by * block_height, block_width, block_height); + } } // bx } //by if (global_cfg.m_debug_images) - save_png(global_cfg.m_debug_file_prefix + "coded_img.png", coded_img); + { + save_png(global_cfg.m_debug_file_prefix + "input_img.png", input_img); + debug_printf("Wrote input_img.png\n"); - if (global_cfg.m_debug_output) + save_png(global_cfg.m_debug_file_prefix + "coded_img.png", coded_img); + debug_printf("Wrote coded_img.png\n"); + } + + if ((global_cfg.m_debug_output) && (global_cfg.m_debug_output_image_metrics)) { debug_printf("Orig image vs. coded img:\n"); print_image_metrics(orig_img, coded_img); + + debug_printf("display_astc_statistics:\n"); + display_astc_statistics(coded_blocks, block_width, block_height, orig_img.get_width(), orig_img.get_height(), false); } } @@ -9342,13 +13019,1877 @@ static bool compress_image_full_zstd( } #endif -bool compress_image( - const image& orig_img, uint8_vec& comp_data, vector2D& coded_blocks, +static uint64_t calc_block_wsse( + const image& orig_img, int sx, int sy, int block_width, int block_height, + const image& tile_img, int tx, int ty, + const astc_ldr::cem_encode_params& p) +{ + assert(((int)tile_img.get_width() == block_width) && ((int)tile_img.get_height() == block_height)); + + uint64_t total_err = 0; + + for (int y = 0; y < block_height; y++) + { + const int oy = sy + y; + if ((oy < 0) || (oy >= (int)orig_img.get_height())) + continue; + + for (int x = 0; x < block_width; x++) + { + const int ox = sx + x; + if ((ox < 0) || (ox >= (int)orig_img.get_width())) + continue; + + total_err += weighted_color_error(orig_img(ox, oy), tile_img(tx + x, ty + y), p); + } // x + + } // y + + return total_err; +} + +static inline vec4F calc_color_delta(const color_rgba& a, const color_rgba& b) +{ + return vec4F( + (float)(a.r - b.r), + (float)(a.g - b.g), + (float)(a.b - b.b), + (float)(a.a - b.a)); +} + +static inline double calc_penalty(const vec4F& orig, const vec4F& cand, const vec4F &comp_weights) +{ + vec4F delta(orig - cand); + delta = vec4F::component_mul(delta, delta); + delta = vec4F::component_mul(delta, comp_weights); + return delta[0] + delta[1] + delta[2] + delta[3]; +} + +static convar g_astc_refine_cross_block_penalty_weight("astc_refine_cross_block_penalty_weight", 2.5f, 0.0f, 1000.0f); + +static uint64_t calc_cross_block_boundary_delta_mismatch(const image& orig_img, const image& candidate_img, uint32_t block_width, uint32_t block_height, uint32_t bx, uint32_t by, const astc_ldr::cem_encode_params& p) +{ + const int ofs_x = block_width * bx, ofs_y = block_height * by; + + const vec4F comp_weights((float)p.m_comp_weights[0], (float)p.m_comp_weights[1], (float)p.m_comp_weights[2], (float)p.m_comp_weights[3]); + + double penalty = 0.0f; + + // TODO: Compute in integer + for (int x = 0; x < (int)block_width; x++) + { + vec4F orig_delta_top(calc_color_delta(orig_img.get_clamped(ofs_x + x, ofs_y), orig_img.get_clamped(ofs_x + x, ofs_y - 1))); + vec4F cand_delta_top(calc_color_delta(candidate_img.get_clamped(ofs_x + x, ofs_y), candidate_img.get_clamped(ofs_x + x, ofs_y - 1))); + penalty += calc_penalty(orig_delta_top, cand_delta_top, comp_weights); + + vec4F orig_delta_bot = calc_color_delta(orig_img.get_clamped(ofs_x + x, ofs_y + block_height - 1), orig_img.get_clamped(ofs_x + x, ofs_y + block_height - 1 + 1)); + vec4F cand_delta_bot = calc_color_delta(candidate_img.get_clamped(ofs_x + x, ofs_y + block_height - 1), candidate_img.get_clamped(ofs_x + x, ofs_y + block_height - 1 + 1)); + penalty += calc_penalty(orig_delta_bot, cand_delta_bot, comp_weights); + } // x + + for (int y = 0; y < (int)block_height; y++) + { + vec4F orig_delta_left(calc_color_delta(orig_img.get_clamped(ofs_x, ofs_y + y), orig_img.get_clamped(ofs_x - 1, ofs_y + y))); + vec4F cand_delta_left(calc_color_delta(candidate_img.get_clamped(ofs_x, ofs_y + y), candidate_img.get_clamped(ofs_x - 1, ofs_y + y))); + penalty += calc_penalty(orig_delta_left, cand_delta_left, comp_weights); + + vec4F orig_delta_right(calc_color_delta(orig_img.get_clamped(ofs_x + block_width - 1, ofs_y + y), orig_img.get_clamped(ofs_x + block_width - 1 + 1, ofs_y + y))); + vec4F cand_delta_right(calc_color_delta(candidate_img.get_clamped(ofs_x + block_width - 1, ofs_y + y), candidate_img.get_clamped(ofs_x + block_width - 1 + 1, ofs_y + y))); + penalty += calc_penalty(orig_delta_right, cand_delta_right, comp_weights); + } // x + + //const float PENALTY_WEIGHT = 2.5f; + const float PENALTY_WEIGHT = g_astc_refine_cross_block_penalty_weight.get_float(); + return (uint64_t)std::round(penalty * PENALTY_WEIGHT); +} + +static inline vec3F calc_ycbcr(const vec3F& c) +{ + const float r = c[0], g = c[1], b = c[2]; + + float Y = r * 0.212600f + g * 0.715200f + b * 0.072200f; + float Cb = r * -0.114572f + g * -0.385428f + b * 0.500000f; + float Cr = r * 0.500000f + g * -0.454153f + b * -0.045847f; + + return vec3F(Y, Cb, Cr); +} + +static uint64_t calc_chroma_loss_penalty(const image& orig_img, const image& candidate_img, uint32_t block_width, uint32_t block_height, uint32_t bx, uint32_t by, const astc_ldr::cem_encode_params& p) +{ + color_rgba orig_block[astc_helpers::MAX_BLOCK_PIXELS]; + color_rgba cand_block[astc_helpers::MAX_BLOCK_PIXELS]; + + orig_img.extract_block_clamped(orig_block, bx * block_width, by * block_height, block_width, block_height); + candidate_img.extract_block_clamped(cand_block, bx * block_width, by * block_height, block_width, block_height); + + const uint32_t total_block_pixels = block_width * block_height; + + vec4F avg_orig(0.0f), avg_cand(0.0f); + for (uint32_t i = 0; i < total_block_pixels; i++) + { + avg_orig += orig_block[i].get_vec4F(); + avg_cand += cand_block[i].get_vec4F(); + } + + avg_orig *= (1.0f / (float)total_block_pixels); + avg_cand *= (1.0f / (float)total_block_pixels); + + vec3F o(calc_ycbcr(avg_orig)); + vec3F c(calc_ycbcr(avg_cand)); + + vec3F delta(o - c); + + float penalty = (delta[1] * delta[1]) + (delta[2] * delta[2]); + + const float PENALTY_WEIGHT = ((float)total_block_pixels * .25f) * (float)p.m_comp_weights[1] * (14.0f * 14.0f); + penalty *= PENALTY_WEIGHT; + + return (uint64_t)std::round(penalty); +} + +static uint64_t calc_block_sse( + uint32_t block_width, uint32_t block_height, + const color_rgba *pBlock_pixels, + const astc_helpers::log_astc_block& log_astc_blk, + const ldr_astc_block_encode_image_high_level_config& enc_cfg) +{ + color_rgba dec_pixels[astc_helpers::MAX_BLOCK_PIXELS]; + bool dec_status = astc_helpers::decode_block(log_astc_blk, dec_pixels, block_width, block_height, enc_cfg.m_cem_enc_params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); + + assert(dec_status); + if (!dec_status) + return UINT64_MAX; + + const uint32_t total_block_pixels = block_width * block_height; + + uint64_t total_err = 0; + for (uint32_t i = 0; i < total_block_pixels; i++) + total_err += weighted_color_error(pBlock_pixels[i], dec_pixels[i], enc_cfg.m_cem_enc_params); + + return total_err; +} + +struct deblocking_thread_state +{ + image m_deblock_orig; // bw*3 x by*3 + image m_deblock_staging; // bw*3 x by*3 + image m_deblock_temp; // (bw+2) x (by+2) +}; + +// true on success +static bool deblocking_find_best_candidate( + uint32_t block_width, uint32_t block_height, uint32_t num_blocks_x, uint32_t num_blocks_y, + uint32_t pass, uint32_t bx, uint32_t by, + deblocking_thread_state &thread_state, + const image& candidate_img, + const image& orig_img, const astc_ldr_encode_config& global_cfg, + const ldr_astc_block_encode_image_high_level_config& enc_cfg, + ldr_astc_block_encode_image_output& enc_out, + uint32_t &new_best_packed_block_index, uint64_t &best_err) +{ + BASISU_NOTE_UNUSED(pass); + + best_err = UINT64_MAX; + + const astc_helpers::decode_mode dec_mode = global_cfg.m_astc_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8; + + ldr_astc_block_encode_image_output::block_info& blk_info = enc_out.m_image_block_info(bx, by); + + const basisu::vector& out_blocks = blk_info.m_out_blocks; + + new_best_packed_block_index = blk_info.m_packed_out_block_index; + + const uint64_t cur_sse = out_blocks[blk_info.m_packed_out_block_index].m_sse; + if (!cur_sse) + { + best_err = 0; + return true; + } + + // if a candidate has a WSSE worse than this, ignore it + //const uint64_t skip_sse_thresh = (cur_sse * 4) / 3; + const uint64_t skip_sse_thresh = UINT64_MAX; + + uint32_t best_cand_index = 0; + + // always 3x3 blocks centered around our current block + image& deblock_orig_img = thread_state.m_deblock_orig; + image& deblock_staging_img = thread_state.m_deblock_staging; + image& deblock_temp_img = thread_state.m_deblock_temp; + + // grab 3x3 block region around block from the original image and the current output, place into thread local temporary buffer + // this makes candidate evaluation simpler + deblock_orig_img.blit_clamped( + orig_img, + ((int)bx - 1) * (int)block_width, ((int)by - 1) * (int)block_height, + block_width * 3, block_height * 3, + 0, 0); + + deblock_staging_img.blit_clamped( + candidate_img, + ((int)bx - 1) * (int)block_width, ((int)by - 1) * (int)block_height, + block_width * 3, block_height * 3, + 0, 0); + + uint32_t total_neighbors_base_ofs = 0, total_neighbors_examined = 0; + for (int k = 0; k < 4; k++) + { + int nbx = 0, nby = 0; + + if (k < 2) + nbx = (int)bx + ((k & 1) ? -1 : 1); + else + nby = (int)by + (((k - 2) & 1) ? -1 : 1); + + if ((nbx < 0) || (nbx >= (int)num_blocks_x)) + continue; + if ((nby < 0) || (nby >= (int)num_blocks_y)) + continue; + + ldr_astc_block_encode_image_output::block_info& neighbor_blk_info = enc_out.m_image_block_info(nbx, nby); + + const astc_helpers::log_astc_block& neighbor_block = neighbor_blk_info.m_out_blocks[neighbor_blk_info.m_packed_out_block_index].m_log_blk; + + if (neighbor_block.m_solid_color_flag_ldr) + continue; + + if (astc_helpers::cem_is_ldr_base_scale(neighbor_block.m_color_endpoint_modes[0])) + total_neighbors_base_ofs++; + + total_neighbors_examined++; + } + + bool penalize_isolated_base_ofs_blocks = false; + if (total_neighbors_examined) + { + const uint32_t neighbors_base_ofs_fract = (total_neighbors_base_ofs << 4) / total_neighbors_examined; + + const uint32_t PENALIZE_ISOLATED_BASE_OFS_FRACT = 8; // div 16 + + penalize_isolated_base_ofs_blocks = neighbors_base_ofs_fract < PENALIZE_ISOLATED_BASE_OFS_FRACT; + } + + [[maybe_unused]] bool applied_base_ofs_penalty = false; + + color_rgba block_pixels[astc_helpers::MAX_BLOCK_PIXELS]; + + for (uint32_t cand_index = 0; cand_index < out_blocks.size(); cand_index++) + { + const encode_block_output& out_blk = out_blocks[cand_index]; + + const uint64_t cand_sse = out_blocks[cand_index].m_sse; + + uint64_t block_wsse = UINT64_MAX, block_artifact_penalty_wsse = 0, chroma_loss_penalty_wsse = 0; + uint64_t cand_err = UINT64_MAX; + + // skip if the pre-deblock error would just increase too much, likely not worth it + if (cand_sse > skip_sse_thresh) + continue; + + if (!astc_helpers::decode_block_xuastc_ldr(out_blk.m_log_blk, block_pixels, block_width, block_height, dec_mode)) + { + assert(0); + return false; + } + + // insert candidate block into the center of our 3x3 block staging image + deblock_staging_img.set_block_clipped(block_pixels, 1 * block_width, 1 * block_height, block_width, block_height); + + if (global_cfg.m_scd_will_postfilter) + { + // deblocks a region of (block_width+2)x(block_height+2), with source pixel offset of (-1,-1), output into deblock_temp_img + deblock_block_region(block_width, block_height, deblock_staging_img, 1 * block_width, 1 * block_height, deblock_temp_img); + + block_wsse = calc_block_wsse( + deblock_orig_img, 1 * block_width - 1, 1 * block_height - 1, block_width + 2, block_height + 2, + deblock_temp_img, 0, 0, + enc_cfg.m_cem_enc_params); + } + else + { + block_wsse = 0; + + for (int y = 0; y < (int)block_height; y++) + for (int x = 0; x < (int)block_width; x++) + block_wsse += weighted_color_error(deblock_orig_img.get_clamped(1 * block_width + x, 1 * block_height + y), block_pixels[x + y * block_width], enc_cfg.m_cem_enc_params); + + // CPU/GPU postfilter is NOT going to be applied, but they want to deblock anyway. No impact to texels around this block. + const uint32_t num_filtered_texels = (block_width + 2) * (block_height + 2); + const uint32_t num_unfiltered_texels = block_width * block_height; + + // boost this wsse so the other penalties (tuned with filtering enabled) are roughly relative to it + block_wsse = (block_wsse * num_filtered_texels) / num_unfiltered_texels; + } + + block_artifact_penalty_wsse = calc_cross_block_boundary_delta_mismatch(deblock_orig_img, deblock_staging_img, block_width, block_height, 1, 1, enc_cfg.m_cem_enc_params); + + chroma_loss_penalty_wsse = global_cfg.m_scd_preserve_chroma ? calc_chroma_loss_penalty(deblock_orig_img, deblock_staging_img, block_width, block_height, 1, 1, enc_cfg.m_cem_enc_params) : 0; + + cand_err = block_wsse; + + if (penalize_isolated_base_ofs_blocks) + { + if (!out_blk.m_log_blk.m_solid_color_flag_ldr && astc_helpers::cem_is_ldr_base_scale(out_blk.m_log_blk.m_color_endpoint_modes[0])) + { + // penalize base-ofs candidates if there are not enough base-ofs neighbors, as they will likely be more visible + cand_err = (cand_err * 5) / 4; + //total_base_ofs_penalties++; + applied_base_ofs_penalty = true; + } + } + + cand_err += block_artifact_penalty_wsse + chroma_loss_penalty_wsse; + + if ((cand_index != blk_info.m_packed_out_block_index) && (out_blk.m_log_blk.m_solid_color_flag_ldr)) + { + // don't switch to solid unless it REALLY seems to help + cand_err = cand_err * 8; + } + + if (cand_err < best_err) + { + best_err = cand_err; + best_cand_index = cand_index; + } + + } // cand_index + + new_best_packed_block_index = best_cand_index; + + return true; +} + +// ------ + +// true on success +static bool deblocking_compute_error( + uint32_t block_width, uint32_t block_height, uint32_t num_blocks_x, uint32_t num_blocks_y, + uint32_t bx, uint32_t by, + image& candidate_img, + const image& orig_img, + const astc_ldr_encode_config& global_cfg, + const ldr_astc_block_encode_image_high_level_config& enc_cfg, + const ldr_astc_block_encode_image_output& enc_out, + uint64_t& cur_err, + image &deblock_orig, image &deblock_temp) +{ + assert(deblock_orig.get_width() == block_width); + assert(deblock_orig.get_height() == block_height); + + assert(deblock_temp.get_width() == (block_width + 2)); + assert(deblock_temp.get_height() == (block_height + 2)); + + const astc_helpers::decode_mode dec_mode = global_cfg.m_astc_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8; + + cur_err = UINT64_MAX; + + const ldr_astc_block_encode_image_output::block_info& blk_info = enc_out.m_image_block_info(bx, by); + + const basisu::vector& out_blocks = blk_info.m_out_blocks; + + deblock_orig.blit(candidate_img, + bx * block_width, by * block_height, block_width, block_height, + 0, 0, + true); + + color_rgba block_pixels[astc_helpers::MAX_BLOCK_PIXELS]; + + uint32_t total_neighbors_base_ofs = 0, total_neighbors_examined = 0; + for (int k = 0; k < 4; k++) + { + int nbx = 0, nby = 0; + + if (k < 2) + nbx = (int)bx + ((k & 1) ? -1 : 1); + else + nby = (int)by + (((k - 2) & 1) ? -1 : 1); + + if ((nbx < 0) || (nbx >= (int)num_blocks_x)) + continue; + if ((nby < 0) || (nby >= (int)num_blocks_y)) + continue; + + const ldr_astc_block_encode_image_output::block_info& neighbor_blk_info = enc_out.m_image_block_info(nbx, nby); + + const astc_helpers::log_astc_block& neighbor_block = neighbor_blk_info.m_out_blocks[neighbor_blk_info.m_packed_out_block_index].m_log_blk; + + if (neighbor_block.m_solid_color_flag_ldr) + continue; + + if (astc_helpers::cem_is_ldr_base_scale(neighbor_block.m_color_endpoint_modes[0])) + total_neighbors_base_ofs++; + + total_neighbors_examined++; + } + + bool penalize_isolated_base_ofs_blocks = false; + if (total_neighbors_examined) + { + const uint32_t neighbors_base_ofs_fract = (total_neighbors_base_ofs << 4) / total_neighbors_examined; + + const uint32_t PENALIZE_ISOLATED_BASE_OFS_FRACT = 8; // div 16 + + penalize_isolated_base_ofs_blocks = neighbors_base_ofs_fract < PENALIZE_ISOLATED_BASE_OFS_FRACT; + } + + const uint32_t cand_index = blk_info.m_packed_out_block_index; + + { + const encode_block_output& out_blk = out_blocks[cand_index]; + + //const uint64_t cand_sse = out_blocks[cand_index].m_sse; + + uint64_t block_wsse = 0, block_artifact_penalty_wsse = 0, chroma_loss_penalty_wsse = 0; + + // skip if the pre-deblock error would just increase too much, likely not worth it + + if (!astc_helpers::decode_block_xuastc_ldr(out_blk.m_log_blk, block_pixels, block_width, block_height, dec_mode)) + { + assert(0); + return false; + } + + candidate_img.set_block_clipped(block_pixels, bx * block_width, by * block_height, block_width, block_height); + + if (global_cfg.m_scd_will_postfilter) + { + deblock_block_region(block_width, block_height, candidate_img, bx * block_width, by * block_height, deblock_temp); + + block_wsse = calc_block_wsse(orig_img, bx * block_width - 1, by * block_height - 1, block_width + 2, block_height + 2, deblock_temp, 0, 0, enc_cfg.m_cem_enc_params); + } + else + { + block_wsse = 0; + + const int sx = bx * block_width, sy = by * block_height; + for (int y = 0; y < (int)block_height; y++) + for (int x = 0; x < (int)block_width; x++) + block_wsse += weighted_color_error(orig_img.get_clamped(sx + x, sy + y), block_pixels[x + y * block_width], enc_cfg.m_cem_enc_params); + + // CPU/GPU postfilter is NOT going to be applied, but they want to deblock anyway. No impact to texels around this block. + const uint32_t num_filtered_texels = (block_width + 2) * (block_height + 2); + const uint32_t num_unfiltered_texels = block_width * block_height; + + // boost this wsse so the other penalties (tuned with filtering enabled) are roughly relative to it + block_wsse = (block_wsse * num_filtered_texels) / num_unfiltered_texels; + } + + block_artifact_penalty_wsse = calc_cross_block_boundary_delta_mismatch(orig_img, candidate_img, block_width, block_height, bx, by, enc_cfg.m_cem_enc_params); + + chroma_loss_penalty_wsse = global_cfg.m_scd_preserve_chroma ? calc_chroma_loss_penalty(orig_img, candidate_img, block_width, block_height, bx, by, enc_cfg.m_cem_enc_params) : 0; + + uint64_t cand_err = block_wsse; + + if (penalize_isolated_base_ofs_blocks) + { + if (!out_blk.m_log_blk.m_solid_color_flag_ldr && astc_helpers::cem_is_ldr_base_scale(out_blk.m_log_blk.m_color_endpoint_modes[0])) + { + // penalize base-ofs candidates if there are not enough base-ofs neighbors, as they will likely be more visible + cand_err = (cand_err * 5) / 4; + //total_base_ofs_penalties++; + } + } + + cand_err += block_artifact_penalty_wsse + chroma_loss_penalty_wsse; + + cur_err = cand_err; + } + + candidate_img.blit(deblock_orig, + 0, 0, block_width, block_height, + bx * block_width, by * block_height, + true); + + return true; +} + +static uint64_t deblocking_compute_overall_error( + uint32_t block_width, uint32_t block_height, uint32_t num_blocks_x, uint32_t num_blocks_y, + const image& orig_img, + const astc_ldr_encode_config& global_cfg, + const ldr_astc_block_encode_image_high_level_config& enc_cfg, + const ldr_astc_block_encode_image_output& enc_out) +{ + const uint32_t width = orig_img.get_width(), height = orig_img.get_height(); + + image candidate_img; + candidate_img.resize(width, height); + + const astc_helpers::decode_mode dec_mode = global_cfg.m_astc_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8; + + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + const ldr_astc_block_encode_image_output::block_info& blk_info = enc_out.m_image_block_info(bx, by); + const encode_block_output& best_blk = blk_info.m_out_blocks[blk_info.m_packed_out_block_index]; + + const astc_helpers::log_astc_block& log_blk = best_blk.m_log_blk; + + color_rgba block_pixels[astc_helpers::MAX_BLOCK_PIXELS]; + + if (!astc_helpers::decode_block_xuastc_ldr(log_blk, block_pixels, block_width, block_height, dec_mode)) + return false; + + candidate_img.set_block_clipped(block_pixels, bx * block_width, by * block_height, block_width, block_height); + + } // bx + + } // by + + const image orig_candidate_img(candidate_img); + + image deblock_orig(block_width, block_height); + image deblock_temp(block_width + 2, block_height + 2); + + uint64_t overall_err = 0; + + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + uint64_t cur_err = 0; + + if (!deblocking_compute_error( + block_width, block_height, num_blocks_x, num_blocks_y, + bx, by, + candidate_img, + orig_img, + global_cfg, + enc_cfg, + enc_out, + cur_err, + deblock_orig, deblock_temp)) + { + return UINT64_MAX; + } + + overall_err += cur_err; + + } // bx + } // by + + fmt_printf("Overall image error: {}, {3.3} per block\n", overall_err, (double)overall_err / (double)(num_blocks_x * num_blocks_y)); + + uint32_t total_cand_differences = 0; + + for (uint32_t y = 0; y < height; y++) + { + for (uint32_t x = 0; x < width; x++) + { + if (candidate_img(x, y) != orig_candidate_img(x, y)) + total_cand_differences++; + } + } + + fmt_printf("Total cand differences: {}\n", total_cand_differences); + return overall_err; +} + +// ------ + +static convar g_astc_refine_max_new_blocks("astc_refine_max_new_blocks", 16, 1, 256); + +static convar g_astc_refine_try_nonprimary_candidates("astc_refine_try_nonprimary_candidates", 1, 0, 1); +static convar g_astc_refine_mutate_part_id_prob("astc_refine_mutate_part_id_prob", 10, 0, 100); +static convar g_astc_refine_mutate_endpoint_edge("astc_refine_mutate_endpoint_edge", 1, 0, 1); +static convar g_astc_refine_mutate_dct("astc_refine_mutate_dct", 1, 0, 1); +static convar g_astc_refine_mutate_endpoints("astc_refine_mutate_endpoints", 1, 0, 1); +static convar g_astc_refine_base_seed("astc_refine_base_seed", 0, INT_MIN, INT_MAX); + +static bool mutate_candidates( + uint32_t pass, + uint32_t block_width, uint32_t block_height, uint32_t num_blocks_x, uint32_t num_blocks_y, uint32_t total_blocks, + const uint32_t max_new_blocks, const uint_vec* const pJob_block_list, const vector2D &block_std_dev, + const uint32_t NUM_SIMILAR_PATS, const vector2D similar_pats[2], const vector2D similar_pat_perm_index[2], + const image& orig_img, const image& candidate_img, + uint32_t num_threads, job_pool &jp, + const astc_ldr_encode_config& global_cfg, const ldr_astc_block_encode_image_high_level_config& enc_cfg, ldr_astc_block_encode_image_output& enc_out) +{ + assert(num_threads); + + basist::astc_ldr_t::grid_weight_dct grid_coder; + grid_coder.init(block_width, block_height); + + float rnd_mag = 1.7f; + + if (pass >= 16) + rnd_mag = .9f; + else if (pass >= 10) + rnd_mag = 1.5f; + else if (pass >= 6) + rnd_mag = 1.7f; + else if (pass >= 4) + rnd_mag = 2.5f; + + std::atomic cur_block_index; + cur_block_index.store(0); + + std::atomic encoder_failed_flag; + encoder_failed_flag.store(false); + + for (uint32_t job_index = 0; job_index < num_threads; job_index++) + { + jp.add_job([job_index, num_threads, + num_blocks_x, num_blocks_y, block_width, block_height, total_blocks, &grid_coder, + pJob_block_list, &block_std_dev, max_new_blocks, rnd_mag, + pass, + &cur_block_index, &encoder_failed_flag, + &candidate_img, &similar_pats, &similar_pat_perm_index, NUM_SIMILAR_PATS, + &orig_img, + &global_cfg, &enc_cfg, &enc_out] + { + BASISU_NOTE_UNUSED(job_index); BASISU_NOTE_UNUSED(num_threads); BASISU_NOTE_UNUSED(num_blocks_y); BASISU_NOTE_UNUSED(pJob_block_list); + // Thread locals + basist::astc_ldr_t::fvec dct_temp; + + const uint32_t block_size_index = astc_helpers::find_astc_block_size_index(block_width, block_height); + [[maybe_unused]] const uint32_t num_unique_subset_pats[2] = { basist::astc_ldr_t::get_total_unique_patterns(block_size_index, 2), basist::astc_ldr_t::get_total_unique_patterns(block_size_index, 3) }; + + uint64_vec sorted_candidates; + sorted_candidates.reserve(1024); + + for (; ;) + { + // Asynchronously fetch the next block index to process. + const uint32_t block_index = cur_block_index.fetch_add(1); + if (block_index >= total_blocks) + break; + + const uint32_t bx = block_index % num_blocks_x; + const uint32_t by = block_index / num_blocks_x; + + sorted_candidates.resize(0); + + // first retire old mutated candidates (older than 4 passes) + { + ldr_astc_block_encode_image_output::block_info& blk_info = enc_out.m_image_block_info(bx, by); + + basisu::vector& cur_out_blocks = blk_info.m_out_blocks; + + basisu::vector new_out_blocks; + new_out_blocks.reserve(1 + (cur_out_blocks.size_u32() / 2)); + + int new_packed_out_block_index = -1; + + for (uint32_t i = 0; i < cur_out_blocks.size(); i++) + { + const encode_block_output& blk = cur_out_blocks[i]; + + bool should_remove = false; + + if (i == blk_info.m_packed_out_block_index) + { + new_packed_out_block_index = new_out_blocks.size_u32(); + } + else + { + if (blk.m_blur_id >= BLUR_ID_EXP) + { + int blk_pass = blk.m_blur_id - BLUR_ID_EXP; + + if (blk_pass <= (int)(pass - 4)) + should_remove = true; + } + } + + if (!should_remove) + { + sorted_candidates.push_back((blk.m_sse << 16u) | new_out_blocks.size()); + + new_out_blocks.push_back(blk); + } + + } // i + + assert(new_packed_out_block_index != -1); + + blk_info.m_packed_out_block_index = new_packed_out_block_index; + blk_info.m_out_blocks.swap(new_out_blocks); + } + + sorted_candidates.sort(); + + // generate new candidates via mutation + { + basisu::rand rnd; + + const uint64_t h = 1ull + pass * (4096ull * 4096ull) + block_index; + rnd.seed(g_astc_refine_base_seed.get_int() + basist::hash_hsieh(reinterpret_cast(&h), sizeof(h))); // endianness (not a big deal here) + + ldr_astc_block_encode_image_output::block_info& blk_info = enc_out.m_image_block_info(bx, by); + + basisu::vector& out_blocks = blk_info.m_out_blocks; + + { + const encode_block_output& best_blk = out_blocks[blk_info.m_packed_out_block_index]; + const astc_helpers::log_astc_block& log_blk = best_blk.m_log_blk; + if (log_blk.m_solid_color_flag_ldr) + continue; + + //uint32_t cem_index = log_blk.m_color_endpoint_modes[0]; + // TODO + //if ((cem_index != 6) && (cem_index != 8) && (cem_index != 9)) + // continue; + } + + color_rgba orig_block[astc_helpers::MAX_BLOCK_PIXELS]; + orig_img.extract_block_clamped(orig_block, bx * block_width, by * block_height, block_width, block_height); + + const uint32_t num_rand_blocks = clamp((int)std::round(block_std_dev(bx, by) * .25f), 1, max_new_blocks) + 2; + + for (uint32_t q = 0; q < num_rand_blocks; q++) + { + const encode_block_output* pBest_blk = &out_blocks[blk_info.m_packed_out_block_index]; + + bool choose_nonprimary_cand_flag = false; + + if (g_astc_refine_try_nonprimary_candidates.get_bool()) + { + if ((q >= (num_rand_blocks - 2)) && (sorted_candidates.size_u32() >= 2)) + { + uint32_t ix = rnd.irand(0, minimum(10u, sorted_candidates.size_u32()) - 1); + + uint32_t rnd_cand_index = sorted_candidates[ix] & UINT16_MAX; + pBest_blk = &out_blocks[rnd_cand_index]; + + if (pBest_blk->m_log_blk.m_solid_color_flag_ldr) + { + rnd_cand_index = sorted_candidates[(ix + 1) % sorted_candidates.size_u32()] & UINT16_MAX; + pBest_blk = &out_blocks[rnd_cand_index]; + } + + choose_nonprimary_cand_flag = true; + } + } + + const encode_block_output& best_blk = *pBest_blk; + + if (best_blk.m_log_blk.m_solid_color_flag_ldr) + continue; + + const astc_helpers::log_astc_block& log_blk = best_blk.m_log_blk; + + const uint32_t cem_index = log_blk.m_color_endpoint_modes[0]; + const uint32_t num_cem_vals = astc_helpers::get_num_cem_values(cem_index); + + astc_helpers::log_astc_block new_log_blk(log_blk); + + const auto& endpoint_tab = astc_helpers::g_dequant_tables.get_endpoint_tab(log_blk.m_endpoint_ise_range); + + bool any_changed = false; + + basist::astc_ldr_t::dct_syms new_packed_dct_plane_data[2]; + bool has_new_packed_dct_plane_data = false; + + if (choose_nonprimary_cand_flag) + new_log_blk.m_user_mode |= 8; + + if (g_astc_refine_mutate_part_id_prob.get_int() && (new_log_blk.m_num_partitions >= 2) && rnd.iprob(g_astc_refine_mutate_part_id_prob.get_int())) + { + // partition pattern mutation + const uint32_t r = rnd.irand(0, NUM_SIMILAR_PATS - 1); + + const uint32_t k = similar_pats[new_log_blk.m_num_partitions - 2](new_log_blk.m_partition_id, r); + + if (k != UINT16_MAX) + { + new_log_blk.m_partition_id = safe_cast_uint16(k); + + const uint32_t perm_index = similar_pat_perm_index[new_log_blk.m_num_partitions - 2](new_log_blk.m_partition_id, r); + + // now permute the endpoints if necessary so they roughly match + if (perm_index) + { + //const uint32_t num_cem_vals = astc_helpers::get_num_cem_values(new_log_blk.m_color_endpoint_modes[0]); + + uint8_t cur_endpoints[astc_helpers::MAX_ENDPOINT_VALS]; + memcpy(cur_endpoints, new_log_blk.m_endpoints, num_cem_vals * new_log_blk.m_num_partitions); + + for (uint32_t src_index = 0; src_index < new_log_blk.m_num_partitions; src_index++) + { + uint32_t dst_index; + if (new_log_blk.m_num_partitions == 2) + dst_index = src_index ^ 1; + else + dst_index = g_part3_mapping[perm_index][src_index]; + + memcpy(new_log_blk.m_endpoints + dst_index * num_cem_vals, cur_endpoints + src_index * num_cem_vals, num_cem_vals); + } + } + + new_log_blk.m_user_mode |= 1; + + any_changed = true; + } + } + + if (!any_changed && g_astc_refine_mutate_endpoint_edge.get_bool() && rnd.iprob(10)) + { + // lerp endpoint towards adjacent block edge mutation + const uint32_t subset_index = (new_log_blk.m_num_partitions == 1) ? 0 : rnd.irand(0, new_log_blk.m_num_partitions - 1); + + vec4F avg_edge(0.0f); + const uint32_t edge_index = rnd.irand(0, 3); + + if ((edge_index == 0) || (edge_index == 2)) + { + // top (0) or bottom (2) + for (uint32_t x = 0; x < block_width; x++) + { + const int src_x = bx * block_width + x; + const int src_y = ((int)by * (int)block_height) + ((edge_index == 0) ? -1 : (int)block_height); + avg_edge += candidate_img.get_clamped(src_x, src_y).get_vec4F(); + } + avg_edge *= (1.0f / (float)block_width); + } + else + { + // right (1) or left (3) + for (uint32_t y = 0; y < block_height; y++) + { + const int src_x = ((int)bx * (int)block_width) + ((edge_index == 1) ? (int)block_width : -1); + const int src_y = by * block_height + y; + avg_edge += candidate_img.get_clamped(src_x, src_y).get_vec4F(); + } + avg_edge *= (1.0f / (float)block_height); + } + + uint8_t* pDst_endpoints = new_log_blk.m_endpoints + num_cem_vals * subset_index; + + color_rgba le, he; + decode_endpoints(cem_index, pDst_endpoints, new_log_blk.m_endpoint_ise_range, le, he, nullptr); + + const float lrp = rnd.frand(0.0f, .3f); + const uint32_t ei = rnd.irand(0, 2); + + for (uint32_t i = 0; i < 4; i++) + { + if ((ei == 0) || (ei == 2)) + le[i] = (uint8_t)clamp(basisu::fast_roundf_int(lerp((float)le[i], avg_edge[i], lrp)), 0, 255); + + if ((ei == 1) || (ei == 2)) + he[i] = (uint8_t)clamp(basisu::fast_roundf_int(lerp((float)he[i], avg_edge[i], lrp)), 0, 255); + } // i + + const float fendpoints[8] = { + (float)le[0], (float)he[0], + (float)le[1], (float)he[1], + (float)le[2], (float)he[2], + (float)le[3], (float)he[3] }; + + uint8_t new_endpoints[8] = { }; + + astc_ldrf::cem_encode(cem_index, fendpoints, new_log_blk.m_endpoint_ise_range, new_endpoints, true, false); + + if (memcmp(pDst_endpoints, new_endpoints, num_cem_vals) != 0) + { + memcpy(pDst_endpoints, new_endpoints, num_cem_vals); + new_log_blk.m_user_mode |= 2; + any_changed = true; + } + } + + if (!any_changed && global_cfg.m_use_dct && g_astc_refine_mutate_dct.get_bool() && (rnd.iprob(10))) + { + // DCT coefficient mutation + if (rnd.iprob(10)) + { + // DC + const uint32_t plane_to_change = new_log_blk.m_dual_plane ? rnd.irand(0, 1) : 0; + + for (uint32_t plane_index = 0; plane_index < (new_log_blk.m_dual_plane ? 2u : 1u); plane_index++) + { + auto& new_coeffs = new_packed_dct_plane_data[plane_index]; + + new_coeffs = best_blk.m_packed_dct_plane_data[plane_index]; + + if (plane_to_change == plane_index) + { + int new_dc_sym = new_coeffs.m_dc_sym; + + new_dc_sym += rnd.bit() ? -1 : 1; //basisu::fast_roundf_int((float)new_dc_sym + rnd.gaussian(0.0f, 1.0f)); + + new_dc_sym = clamp(new_dc_sym, 0, new_coeffs.m_num_dc_levels - 1); + + if (new_dc_sym != (int)new_coeffs.m_dc_sym) + { + new_coeffs.m_dc_sym = new_dc_sym; + + any_changed = true; + has_new_packed_dct_plane_data = true; + new_log_blk.m_user_mode |= 16; + } + } + } + } + + if (!any_changed) + { + // AC + for (uint32_t plane_index = 0; plane_index < (new_log_blk.m_dual_plane ? 2u : 1u); plane_index++) + { + auto& new_coeffs = new_packed_dct_plane_data[plane_index]; + + new_coeffs = best_blk.m_packed_dct_plane_data[plane_index]; + + uint32_t total_coeffs = new_coeffs.m_coeffs.size_u32(); + + if ((total_coeffs) && (new_coeffs.m_coeffs[total_coeffs - 1].m_coeff == INT16_MAX)) + total_coeffs--; + + if (!total_coeffs) + continue; + + int rnd_coeff_index = rnd.irand(0, total_coeffs - 1); + + int cur_coeff = new_coeffs.m_coeffs[rnd_coeff_index].m_coeff; + + if (rnd.iprob(10)) + cur_coeff = -cur_coeff; + else if (rnd.bit()) + cur_coeff++; + else + cur_coeff--; + + if ((!cur_coeff) || (iabs(cur_coeff) > basist::astc_ldr_t::DCT_MAX_ARITH_COEFF_MAG)) + continue; + + new_coeffs.m_coeffs[rnd_coeff_index].m_coeff = safe_cast_int16(cur_coeff); + new_coeffs.m_max_coeff_mag = basisu::maximum(new_coeffs.m_max_coeff_mag, iabs(cur_coeff)); + + any_changed = true; + has_new_packed_dct_plane_data = true; + new_log_blk.m_user_mode |= 4; + + } // plane_index + } // if (!any_changed) + } + + if ((!any_changed) && g_astc_refine_mutate_endpoints.get_bool()) + { + // gaussian endpoint CEM value mutation + const uint32_t subset_index = (log_blk.m_num_partitions == 1) ? 0 : rnd.irand(0, log_blk.m_num_partitions - 1); + const uint32_t e = rnd.bit(); + + const bool cem_has_alpha = astc_helpers::does_cem_have_alpha(cem_index); + + bool mutate_rgb = true, mutate_alpha = false; + + if (cem_has_alpha) + { + uint32_t c = rnd.irand(0, 2); + mutate_rgb = (c == 0) || (c == 2); + mutate_alpha = (c == 1) || (c == 2); + } + + if (mutate_rgb) + { + uint32_t e_ofs = subset_index * num_cem_vals, e_stride = 0, nc = 0; + + switch (cem_index) + { + case astc_helpers::CEM_LDR_LUM_DIRECT: + case astc_helpers::CEM_LDR_LUM_ALPHA_DIRECT: + { + e_ofs += e; + e_stride = 2; + nc = 1; + break; + } + case astc_helpers::CEM_LDR_RGB_BASE_SCALE: + case astc_helpers::CEM_LDR_RGB_BASE_SCALE_PLUS_TWO_A: + { + // e=0 -> scale, e=1 -> base rgb + e_ofs += (e ? 0 : 3); + e_stride = 1; + nc = e ? 3 : 1; + break; + } + case astc_helpers::CEM_LDR_RGB_DIRECT: + case astc_helpers::CEM_LDR_RGB_BASE_PLUS_OFFSET: + case astc_helpers::CEM_LDR_RGBA_DIRECT: + case astc_helpers::CEM_LDR_RGBA_BASE_PLUS_OFFSET: + { + e_ofs += e; + e_stride = 2; + nc = 3; + break; + } + default: + { + assert(0); + break; + } + } + + for (uint32_t c = 0; c < nc; c++) + { + assert((e_ofs + c * e_stride) < (num_cem_vals * new_log_blk.m_num_partitions)); + const int cur_rank = endpoint_tab.m_ISE_to_rank[new_log_blk.m_endpoints[e_ofs + c * e_stride]]; + + int new_rank = clamp((int)std::round(cur_rank + rnd.gaussian(0.0f, rnd_mag)), 0, astc_helpers::get_ise_levels(log_blk.m_endpoint_ise_range) - 1); + + if (new_rank != cur_rank) + any_changed = true; + + new_log_blk.m_endpoints[e_ofs + c * e_stride] = endpoint_tab.m_rank_to_ISE[new_rank]; + } + + } // mutate_rgb + + if (mutate_alpha) + { + uint32_t e_ofs = subset_index * num_cem_vals; + + switch (cem_index) + { + case astc_helpers::CEM_LDR_LUM_ALPHA_DIRECT: + { + e_ofs += 2 + e; + break; + } + case astc_helpers::CEM_LDR_RGB_BASE_SCALE_PLUS_TWO_A: + { + e_ofs += 4 + e; + break; + } + case astc_helpers::CEM_LDR_RGBA_DIRECT: + case astc_helpers::CEM_LDR_RGBA_BASE_PLUS_OFFSET: + { + e_ofs += 6 + e; + break; + } + default: + { + assert(0); + break; + } + } + + { + assert(e_ofs < num_cem_vals * new_log_blk.m_num_partitions); + const int cur_rank = endpoint_tab.m_ISE_to_rank[new_log_blk.m_endpoints[e_ofs]]; + + int new_rank = clamp((int)std::round(cur_rank + rnd.gaussian(0.0f, rnd_mag)), 0, astc_helpers::get_ise_levels(log_blk.m_endpoint_ise_range) - 1); + + if (new_rank != cur_rank) + any_changed = true; + + new_log_blk.m_endpoints[e_ofs] = endpoint_tab.m_rank_to_ISE[new_rank]; + } + + } // mutate_alpha + + } + + if (!any_changed) + continue; + + encode_block_output new_out_block; + new_out_block.m_trial_mode_index = best_blk.m_trial_mode_index; + new_out_block.m_blur_id = safe_cast_uint16(BLUR_ID_EXP + pass); + + if (enc_cfg.m_use_dct) + { + if (has_new_packed_dct_plane_data) + { + new_out_block.m_packed_dct_plane_data[0] = new_packed_dct_plane_data[0]; + new_out_block.m_packed_dct_plane_data[1] = new_packed_dct_plane_data[1]; + } + else + { + new_out_block.m_packed_dct_plane_data[0] = best_blk.m_packed_dct_plane_data[0]; + new_out_block.m_packed_dct_plane_data[1] = best_blk.m_packed_dct_plane_data[1]; + } + + for (uint32_t plane_index = 0; plane_index < (new_log_blk.m_dual_plane ? 2u : 1u); plane_index++) + { + basist::astc_ldr_t::dct_syms& syms = new_out_block.m_packed_dct_plane_data[plane_index]; + + bool status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, new_log_blk, nullptr, nullptr, dct_temp, &syms); + assert(status); + + if (!status) + { + encoder_failed_flag.store(true); + return; + } + } + } + + new_out_block.m_log_blk = new_log_blk; + + new_out_block.m_sse = calc_block_sse(block_width, block_height, orig_block, new_log_blk, enc_cfg); + + out_blocks.push_back(new_out_block); + } // q + } + + } // for (; ;) + + } + ); + + } // job_index + + jp.wait_for_all(); + + if (encoder_failed_flag) + { + fmt_error_printf("refine_output_for_deblocking: Threaded deblocking pass failed! (2)\n"); + return false; + } + + return true; +} + +static bool refine_output_for_deblocking( + const image& orig_img, + const astc_ldr_encode_config& global_cfg, + const ldr_astc_block_encode_image_high_level_config& enc_cfg, + ldr_astc_block_encode_image_output& enc_out) +{ + if (global_cfg.m_debug_output) + fmt_debug_printf("------------------- refine_output_for_deblocking:\n"); + + const uint32_t width = orig_img.get_width(); + const uint32_t height = orig_img.get_height(); + const uint32_t block_width = global_cfg.m_astc_block_width; + const uint32_t block_height = global_cfg.m_astc_block_height; + const uint32_t total_block_pixels = block_width * block_height; + + //const uint32_t total_pixels = width * height; + 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; + + uint64_t initial_deblocked_err = 0; + if (global_cfg.m_debug_output) + { + initial_deblocked_err = deblocking_compute_overall_error( + block_width, block_height, num_blocks_x, num_blocks_y, + orig_img, + global_cfg, + enc_cfg, + enc_out); + } + + image candidate_img; + candidate_img.resize(width, height); + + const astc_helpers::decode_mode dec_mode = global_cfg.m_astc_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8; + + uint32_t num_min_candidates = UINT32_MAX, num_max_candidates = 1; + uint32_t total_overall_candidates = 0; + + vector2D block_std_dev(num_blocks_x, num_blocks_y); + + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + ldr_astc_block_encode_image_output::block_info& blk_info = enc_out.m_image_block_info(bx, by); + const encode_block_output& best_blk = blk_info.m_out_blocks[blk_info.m_packed_out_block_index]; + + const astc_helpers::log_astc_block& log_blk = best_blk.m_log_blk; + + color_rgba block_pixels[astc_helpers::MAX_BLOCK_PIXELS]; + + if (!astc_helpers::decode_block_xuastc_ldr(log_blk, block_pixels, block_width, block_height, dec_mode)) + return false; + + vec4F sum(0.0f), sum2(0.0f); + for (uint32_t i = 0; i < total_block_pixels; i++) + { + vec4F p(block_pixels[i].get_vec4F()); + sum += p; + sum2 += vec4F::component_mul(p, p); + } + + const float oo_total_texels = 1.0f / (float)total_block_pixels; + + const vec4F var = vec4F::component_max(sum2 - vec4F::component_mul(sum, sum) * oo_total_texels, vec4F(0.0f)) * oo_total_texels; + const vec4F std_dev = vec4F::component_sqrt(var); + + const float block_stddev = basisu::maximum(std_dev[0], std_dev[1], std_dev[2], std_dev[3]); + for (int dy = -1; dy <= 1; dy++) + { + const int y = by + dy; + if ((y < 0) || (y >= (int)num_blocks_y)) + continue; + for (int dx = -1; dx <= 1; dx++) + { + const int x = bx + dx; + if ((x < 0) || (x >= (int)num_blocks_x)) + continue; + block_std_dev(x, y) = basisu::maximum(block_std_dev(x, y), block_stddev); + } // dx + } // dy + + candidate_img.set_block_clipped(block_pixels, bx * block_width, by * block_height, block_width, block_height); + + num_min_candidates = minimum(num_min_candidates, blk_info.m_out_blocks.size_u32()); + num_max_candidates = maximum(num_max_candidates, blk_info.m_out_blocks.size_u32()); + + total_overall_candidates += blk_info.m_out_blocks.size_u32(); + } // bx + + } // by + + if (global_cfg.m_debug_images) + { + image std_dev_vis(width, height); + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + const float std_dev = block_std_dev(bx, by); + + const float STD_DEV_SCALE = 4.0f; + std_dev_vis.fill_box(bx * block_width, by * block_height, block_width, block_height, color_rgba((uint8_t)std::min(255.0f, std_dev * STD_DEV_SCALE), 255)); + } + } + + save_png("deblock_std_dev_vis.png", std_dev_vis); + fmt_printf("Wrote deblock_std_dev_vis.png\n"); + } + + if (global_cfg.m_debug_output) + { + fmt_debug_printf("Total candidates: {}, Avg candidates per block: {}, Min candidates: {}, Max candidates: {}\n", + total_overall_candidates, + (float)total_overall_candidates / (float)total_blocks, + num_min_candidates, num_max_candidates); + } + + { + image candidate_img_deblocked; + deblock_image(block_width, block_height, candidate_img, candidate_img_deblocked); + + if (global_cfg.m_debug_images) + { + save_png(global_cfg.m_debug_file_prefix + "deblock_initial_candidate_img.png", candidate_img); + save_png(global_cfg.m_debug_file_prefix + "deblock_initial_candidate_img_deblocked.png", candidate_img_deblocked); + } + + if ((global_cfg.m_debug_output) && (global_cfg.m_debug_output_image_metrics)) + { + fmt_debug_printf("orig vs. initial candidate image:\n"); + print_image_metrics(orig_img, candidate_img); + + fmt_debug_printf("\norig vs. initial candidate image deblocked:\n"); + print_image_metrics(orig_img, candidate_img_deblocked); + } + } + + if (num_max_candidates == 1) + { + if (global_cfg.m_debug_output) + fmt_debug_printf("No candidates to try for any block, exiting.\n"); + return true; + } + + image deblock_orig(block_width * 3, block_height * 3); + image deblock_temp(block_width + 2, block_height + 2); + +#if 0 + image debug_block_image; + uint32_t debug_block_cur_x = 0, debug_block_cur_y = 0; + const int DEBUG_IMG_COL_WIDTH = 680; + if (global_cfg.m_debug_block_x != -1) + { + debug_block_image.resize(3072, 2048); + + debug_block_image.blit(orig_img, + global_cfg.m_debug_block_x * block_width, global_cfg.m_debug_block_y * block_height, block_width, block_height, + debug_block_cur_x, debug_block_cur_y, + true); + + debug_block_cur_y += block_height + 2; + + debug_printf("Debug block coordinate {}x{}\n", global_cfg.m_debug_block_x, global_cfg.m_debug_block_y); + } +#endif + + uint_vec white_list, black_list; + white_list.reserve(total_blocks); + black_list.reserve(total_blocks); + for (uint32_t y = 0; y < num_blocks_y; y++) + { + for (uint32_t x = 0; x < num_blocks_x; x++) + { + const uint32_t color_index = (x ^ y) & 1; + (color_index ? black_list : white_list).push_back(x + y * num_blocks_x); + } // x + } // y + + vector2D block_best_err(num_blocks_x, num_blocks_y); + block_best_err.set_all(UINT64_MAX); + + const uint32_t max_new_blocks = ((width * height) >= (2048 * 2048)) ? ((g_astc_refine_max_new_blocks.get_int() + 1) / 2) : g_astc_refine_max_new_blocks.get_int(); + + assert(enc_cfg.m_pJob_pool); + job_pool& job_pool = *enc_cfg.m_pJob_pool; + + const uint32_t num_threads = (uint32_t)job_pool.get_total_threads(); + assert(num_threads); + + const uint32_t num_passes = clamp(global_cfg.m_num_scd_passes, 2, 256); + + // TODO + const bool mutation_enabled = true; + + vector2D orig_best_block_indices(num_blocks_x, num_blocks_y); + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + ldr_astc_block_encode_image_output::block_info& blk_info = enc_out.m_image_block_info(bx, by); + + orig_best_block_indices(bx, by) = blk_info.m_packed_out_block_index; + } + } + +#if 1 + // TODO: Move to init, this is per-mipmap + const uint32_t NUM_SIMILAR_PATS = 16; + vector2D similar_pats[2]; + vector2D similar_pat_perm_index[2]; + for (uint32_t p = 0; p < 2; p++) + { + similar_pats[p].resize(1024, NUM_SIMILAR_PATS); + similar_pat_perm_index[p].resize(1024, NUM_SIMILAR_PATS); + + similar_pats[p].set_all(UINT16_MAX); + } + + const uint32_t block_size_index = astc_helpers::find_astc_block_size_index(block_width, block_height); + + for (uint32_t num_subsets = 2; num_subsets <= 3; num_subsets++) + { + const uint32_t num_unique_subset_pats = basist::astc_ldr_t::get_total_unique_patterns(block_size_index, num_subsets); + + const partitions_data& pat_data = (num_subsets == 2) ? enc_out.m_part_data_p2 : enc_out.m_part_data_p3; + + assert(num_unique_subset_pats == pat_data.m_total_unique_patterns); + + for (uint32_t unique_pat_index = 0; unique_pat_index < num_unique_subset_pats; unique_pat_index++) + { + const uint32_t seed_index = pat_data.m_unique_index_to_part_seed[unique_pat_index]; + + const partition_pattern_vec& desired_pat_vec = pat_data.m_partition_pats[unique_pat_index]; + + uint32_t results[NUM_SIMILAR_PATS + 1]; + + uint32_t num_results = pat_data.m_part_lhs_map.find(desired_pat_vec, results, NUM_SIMILAR_PATS + 1, false); + + uint32_t dst_index = 0; + + for (uint32_t i = 0; i < num_results; i++) + { + assert(results[i] < num_unique_subset_pats); + if (results[i] == unique_pat_index) + continue; + + const uint32_t similar_unique_pat_index = results[i]; + + similar_pats[num_subsets - 2](seed_index, dst_index) = safe_cast_uint16(pat_data.m_unique_index_to_part_seed[similar_unique_pat_index]); + + const uint32_t total_perms = (num_subsets == 2) ? 2 : NUM_PART3_MAPPINGS; + uint32_t best_dist = UINT32_MAX, best_perm_index = 0; + + const partition_pattern_vec& similar_pat_vec = pat_data.m_partition_pats[similar_unique_pat_index]; + + for (uint32_t perm_index = 0; perm_index < total_perms; perm_index++) + { + partition_pattern_vec desired_pat_vec_permuted = (num_subsets == 2) ? desired_pat_vec.get_permuted2(perm_index) : desired_pat_vec.get_permuted3(perm_index); + + uint32_t dist = desired_pat_vec_permuted.get_squared_distance(similar_pat_vec); + if (dist < best_dist) + { + best_dist = dist; + best_perm_index = perm_index; + } + } // p + + similar_pat_perm_index[num_subsets - 2](seed_index, dst_index) = safe_cast_uint8(best_perm_index); + + dst_index++; + if (dst_index >= NUM_SIMILAR_PATS) + break; + } // i + + } // unique_pat_index + + } // num_subsets +#endif + + for (uint32_t pass = 0; pass < num_passes; pass++) + { + const bool final_pass_flag = (pass == (num_passes - 1)); + + if (global_cfg.m_debug_output) + fmt_debug_printf("Pass: {}\n", pass); + + uint_vec* const pJob_block_list = (pass & 1) ? &black_list : &white_list; + + vector2D new_best_packed_block_indices(num_blocks_x, num_blocks_y); + new_best_packed_block_indices.set_all(-1); + + std::atomic total_err; + total_err.store(0); + + if (pJob_block_list->size()) + { + std::atomic cur_block_list_index; + cur_block_list_index.store(0); + + std::atomic encoder_failed_flag; + encoder_failed_flag.store(false); + + for (uint32_t job_index = 0; job_index < num_threads; job_index++) + { + job_pool.add_job([job_index, num_threads, + num_blocks_x, num_blocks_y, block_width, block_height, total_blocks, + pJob_block_list, + pass, &block_best_err, &new_best_packed_block_indices, &total_err, + &cur_block_list_index, &encoder_failed_flag, + &candidate_img, + &orig_img, + &global_cfg, &enc_cfg, &enc_out] + { + BASISU_NOTE_UNUSED(job_index); BASISU_NOTE_UNUSED(num_threads); BASISU_NOTE_UNUSED(total_blocks); + deblocking_thread_state thread_state; + thread_state.m_deblock_orig.resize(block_width * 3, block_height * 3); + thread_state.m_deblock_staging.resize(block_width * 3, block_height * 3); + thread_state.m_deblock_temp.resize(block_width + 2, block_height + 2); + + for (; ; ) + { + if (encoder_failed_flag) + return; + + const uint32_t block_list_index = cur_block_list_index.fetch_add(1); + if (block_list_index >= pJob_block_list->size_u32()) + break; + + const uint32_t block_index = (*pJob_block_list)[block_list_index]; + const uint32_t bx = block_index % num_blocks_x; + const uint32_t by = block_index / num_blocks_x; + + uint32_t new_best_packed_block_index = 0; + uint64_t best_err = 0; + + if (!deblocking_find_best_candidate( + block_width, block_height, num_blocks_x, num_blocks_y, + pass, bx, by, + thread_state, + candidate_img, + orig_img, + global_cfg, + enc_cfg, + enc_out, + new_best_packed_block_index, best_err)) + { + encoder_failed_flag.store(true); + return; + } + + total_err.fetch_add(best_err, std::memory_order_relaxed); + + new_best_packed_block_indices(bx, by) = new_best_packed_block_index; + + block_best_err(bx, by) = best_err; + + } // for + + }); + + } // job_index + + job_pool.wait_for_all(); + + if (encoder_failed_flag) + { + fmt_error_printf("refine_output_for_deblocking: Threaded deblocking pass failed!\n"); + return false; + } + } + + uint32_t total_blocks_changed = 0; + + // now commit changed blocks to candidate_img + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + const int new_best_packed_block_index = new_best_packed_block_indices(bx, by); + + if (new_best_packed_block_index < 0) + continue; + + ldr_astc_block_encode_image_output::block_info& blk_info = enc_out.m_image_block_info(bx, by); + + if ((int)blk_info.m_packed_out_block_index == new_best_packed_block_index) + continue; + + const basisu::vector& out_blocks = blk_info.m_out_blocks; + + total_blocks_changed++; + + blk_info.m_packed_out_block_index = new_best_packed_block_index; + + const encode_block_output& out_blk = out_blocks[new_best_packed_block_index]; + + color_rgba block_pixels[astc_helpers::MAX_BLOCK_PIXELS]; + + if (!astc_helpers::decode_block_xuastc_ldr(out_blk.m_log_blk, block_pixels, block_width, block_height, dec_mode)) + { + assert(0); + return false; + } + + candidate_img.set_block_clipped(block_pixels, bx * block_width, by * block_height, block_width, block_height); + + } // bx + } // by + +#if 0 + if ((pass == 0) && (global_cfg.m_debug_block_x != -1)) + { + save_png(global_cfg.m_debug_file_prefix + "debug_block_image.png", debug_block_image); + } +#endif + + if (global_cfg.m_debug_output) + { + fmt_debug_printf("Pass total error: {}\n", total_err); + fmt_debug_printf("Total blocks changed: {} {3.2}% (rel to all blocks)\n", total_blocks_changed, (float)total_blocks_changed * 100.0f / total_blocks); + } + + if ((pass & 1) && (global_cfg.m_debug_output)) + { + uint64_t img_total_err = 0; + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + uint64_t block_err = block_best_err(bx, by); + assert(block_err != UINT64_MAX); + img_total_err += block_err; + } // bx + } // by + + fmt_printf("Total image error: {}\n", img_total_err); + } + + image final_candidate_img_deblocked; + if ((global_cfg.m_debug_images) || + ((global_cfg.m_debug_output) && (global_cfg.m_debug_output_image_metrics))) + { + deblock_image(block_width, block_height, candidate_img, final_candidate_img_deblocked); + + if ((final_pass_flag) && (global_cfg.m_debug_images)) + { + save_png(global_cfg.m_debug_file_prefix + "deblock_final_candidate_img.png", candidate_img); + save_png(global_cfg.m_debug_file_prefix + "deblock_final_candidate_img_deblocked.png", final_candidate_img_deblocked); + } + } + + if ((global_cfg.m_debug_output) && (global_cfg.m_debug_output_image_metrics)) + { + fmt_debug_printf("\norig vs. final candidate image:\n"); + print_image_metrics(orig_img, candidate_img); + + if (final_candidate_img_deblocked.get_total_pixels()) + { + fmt_debug_printf("\norig vs. final candidate image deblocked:\n"); + print_image_metrics(orig_img, final_candidate_img_deblocked); + } + } + + if ((mutation_enabled) && (((int)pass >= 4) && (pass < (num_passes - 2)))) + { + if (!mutate_candidates( + pass, + block_width, block_height, num_blocks_x, num_blocks_y, total_blocks, + max_new_blocks, pJob_block_list, block_std_dev, + NUM_SIMILAR_PATS, similar_pats, similar_pat_perm_index, + orig_img, + candidate_img, + num_threads, + job_pool, + global_cfg, + enc_cfg, + enc_out)) + { + return false; + } + } + + } // pass + + if (global_cfg.m_debug_output) + { + uint32_t total_changed_blocks = 0, total_mutated_blocks = 0, total_part_id_blocks = 0, total_endpoint_lerp_blocks = 0, total_mutated_dct_ac_blocks = 0, total_mutated_dct_dc_blocks = 0, total_nonprimary_candidate_blocks = 0; + + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + ldr_astc_block_encode_image_output::block_info& blk_info = enc_out.m_image_block_info(bx, by); + + if (orig_best_block_indices(bx, by) != blk_info.m_packed_out_block_index) + total_changed_blocks++; + + const encode_block_output& best_blk = blk_info.m_out_blocks[blk_info.m_packed_out_block_index]; + if (best_blk.m_blur_id >= BLUR_ID_EXP) + total_mutated_blocks++; + + if (best_blk.m_log_blk.m_user_mode & 1) + total_part_id_blocks++; + + if (best_blk.m_log_blk.m_user_mode & 2) + total_endpoint_lerp_blocks++; + + if (best_blk.m_log_blk.m_user_mode & 4) + total_mutated_dct_ac_blocks++; + + if (best_blk.m_log_blk.m_user_mode & 8) + total_nonprimary_candidate_blocks++; + + if (best_blk.m_log_blk.m_user_mode & 16) + total_mutated_dct_dc_blocks++; + } + } + + fmt_printf("------ SCD complete:\n"); + fmt_printf("Total candidate ID changed blocks: {} {3.2}%\n", total_changed_blocks, (total_changed_blocks * 100.0f) / (float)total_blocks); + fmt_printf("Total mutated blocks: {} {3.2}%\n", total_mutated_blocks, (total_mutated_blocks * 100.0f) / (float)total_blocks); + fmt_printf("Total mutated part id blocks: {} {3.2}%\n", total_part_id_blocks, (total_part_id_blocks * 100.0f) / (float)total_blocks); + fmt_printf("Total mutated endpoint lerp blocks: {} {3.2}%\n", total_endpoint_lerp_blocks, (total_endpoint_lerp_blocks * 100.0f) / (float)total_blocks); + fmt_printf("Total mutated DCT AC blocks: {} {3.2}%\n", total_mutated_dct_ac_blocks, (total_mutated_dct_ac_blocks * 100.0f) / (float)total_blocks); + fmt_printf("Total mutated DCT DC blocks: {} {3.2}%\n", total_mutated_dct_dc_blocks, (total_mutated_dct_dc_blocks * 100.0f) / (float)total_blocks); + fmt_printf("Total non-primary candidate blocks: {} {3.2}%\n", total_nonprimary_candidate_blocks, (total_nonprimary_candidate_blocks * 100.0f) / (float)total_blocks); + + const uint64_t final_deblocked_err = deblocking_compute_overall_error( + block_width, block_height, num_blocks_x, num_blocks_y, + orig_img, + global_cfg, + enc_cfg, + enc_out); + + fmt_printf("Cross check: Initial error: {} {3.3} per block, final error: {} {3.3} per block, Avg. change per block: {3.3}\n", + initial_deblocked_err, (double)initial_deblocked_err / (double)total_blocks, + final_deblocked_err, (double)final_deblocked_err / (double)total_blocks, + ((double)final_deblocked_err - (double)initial_deblocked_err) / (double)total_blocks); + + fmt_debug_printf("------------------- refine_output_for_deblocking: OK\n"); + } + + return true; +} + +static bool sharpen_image(const image& in, image& out, const astc_ldr_encode_config& global_cfg) +{ + const float amount = global_cfg.m_sharpen_amount; + + if ((in.get_width() <= 4) && (in.get_height() <= 4)) + { + out = in; + return true; + } + + if (global_cfg.m_debug_output) + fmt_debug_printf("sharpen_image: DoG amount {} (note this modifies the original image and impacts downstream PSNR's)\n", amount); + + image blur1, blur2; + blur1.match_dimensions(in); + blur2.match_dimensions(in); + + if (!image_resample(in, blur1, false, "gaussian", 1.05f)) + return false; + if (!image_resample(in, blur2, false, "gaussian", 1.3f)) + return false; + + out.match_dimensions(in); + + for (uint32_t y = 0; y < in.get_height(); y++) + { + for (uint32_t x = 0; x < in.get_width(); x++) + { + const color_rgba& i = in(x, y); + const color_rgba& b1 = blur1(x, y); + const color_rgba& b2 = blur2(x, y); + + color_rgba o(0, 0, 0, i.a); + + for (int c = 0; c < 3; c++) + { + int k = (int)std::round((float)i[c] + amount * ((float)b1[c] - (float)b2[c])); + o[c] = (uint8_t)clamp(k, 0, 255); + } + + out(x, y) = o; + } // x + } // y + + return true; +} + +// merges output candidates (with no de-dup) from enc_out2 to enc_out, returns # of better blocks found in enc_out2 vs. enc_out +static uint32_t merge_output_candidates(ldr_astc_block_encode_image_output& enc_out, const ldr_astc_block_encode_image_output& enc_out2) +{ + uint32_t total_better_blocks = 0; + + // Merge the two outputs now + for (uint32_t by = 0; by < enc_out2.m_image_block_info.get_height(); by++) + { + for (uint32_t bx = 0; bx < enc_out2.m_image_block_info.get_width(); bx++) + { + ldr_astc_block_encode_image_output::block_info& blk_info1 = enc_out.m_image_block_info(bx, by); + const ldr_astc_block_encode_image_output::block_info& blk_info2 = enc_out2.m_image_block_info(bx, by); + + const uint32_t orig_out_block_size = blk_info1.m_out_blocks.size_u32(); + + blk_info1.m_out_blocks.append(blk_info2.m_out_blocks); + +#if 0 + for (uint32_t i = 0; i < blk_info2.m_out_blocks.size(); i++) + { + blk_info1.m_out_blocks[orig_out_block_size + i].m_blur_id = 768; + } +#endif + + const uint64_t best_err1 = blk_info1.m_out_blocks[blk_info1.m_packed_out_block_index].m_sse; + const uint64_t best_err2 = blk_info2.m_out_blocks[blk_info2.m_packed_out_block_index].m_sse; + + if (best_err2 < best_err1) + { + total_better_blocks++; + + blk_info1.m_packed_out_block_index = orig_out_block_size + blk_info2.m_packed_out_block_index; + + enc_out.m_packed_phys_blocks(bx, by) = enc_out2.m_packed_phys_blocks(bx, by); + } + + } // bx + } // by + + return total_better_blocks; +} + +static bool cross_check_dct( + uint32_t block_width, uint32_t block_height, uint32_t num_blocks_x, uint32_t num_blocks_y, + const ldr_astc_block_encode_image_output &enc_out, + const astc_ldr_encode_config& global_cfg, + const ldr_astc_block_encode_image_high_level_config& enc_cfg) +{ + if (global_cfg.m_debug_output) + fmt_debug_printf("cross_check_dct:\n"); + + basist::astc_ldr_t::grid_weight_dct grid_coder; + grid_coder.init(block_width, block_height); + + basist::astc_ldr_t::fvec dct_temp; + + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + const ldr_astc_block_encode_image_output::block_info& blk_info = enc_out.m_image_block_info(bx, by); + + const uint32_t best_packed_out_block_index = blk_info.m_packed_out_block_index; + + if (best_packed_out_block_index >= blk_info.m_out_blocks.size()) + { + fmt_error_printf("astc_ldr::cross_check_dct(): best_packed_out_block_index is invalid\n"); + return false; + } + + const auto& out_blk = blk_info.m_out_blocks[best_packed_out_block_index]; + + const auto& log_blk = out_blk.m_log_blk; + if (log_blk.m_solid_color_flag_ldr) + continue; + + astc_helpers::log_astc_block temp_log_blk(log_blk); + + for (uint32_t plane_index = 0; plane_index < (log_blk.m_dual_plane ? 2u : 1u); plane_index++) + { + const basist::astc_ldr_t::dct_syms& syms = out_blk.m_packed_dct_plane_data[plane_index]; + + bool status = grid_coder.decode_block_weights(enc_cfg.m_base_q, plane_index, temp_log_blk, nullptr, nullptr, dct_temp, &syms); + assert(status); + + if (!status) + { + fmt_error_printf("astc_ldr::cross_check_dct(): failed decoding weight grids\n"); + return false; + } + + int max_coeff = 0; + + for (uint32_t i = 0; i < syms.m_coeffs.size(); i++) + { + if (syms.m_coeffs[i].m_coeff != INT16_MAX) + max_coeff = basisu::maximum(max_coeff, iabs(syms.m_coeffs[i].m_coeff)); + } + + // TODO: the DCT mutator can only boost the max not reduce it, so it's not entirely accurate now but safe enough for supercompression purposes + if (max_coeff > (int)syms.m_max_coeff_mag) + { + fmt_error_printf("astc_ldr::cross_check_dct(): m_max_coeff_mag is invalid\n"); + return false; + } + } + + // Ensure the decoded weight grid matches the stored weight grids, or there's a serious problem. + const uint32_t total_grid_weights = astc_helpers::get_total_weights(temp_log_blk); + if (memcmp(log_blk.m_weights, temp_log_blk.m_weights, total_grid_weights) != 0) + { + fmt_error_printf("astc_ldr::cross_check_dct(): decoded weight grid mismatch\n"); + return false; + } + + } // bx + + } // by + + if (global_cfg.m_debug_output) + fmt_debug_printf("Weight grid DCT decode cross-check OK\n"); + + return true; +} + +bool compress_image( + const image& actual_orig_img, uint8_vec& comp_data, vector2D& coded_blocks, + const astc_ldr_encode_config& orig_global_cfg, job_pool& job_pool) { assert(g_initialized); + astc_ldr_encode_config global_cfg(orig_global_cfg); + + //global_cfg.m_debug_block_x = 17; + //global_cfg.m_debug_block_y = 26; + +#if !BASISU_SUPPORT_ASTCENC + if (global_cfg.m_use_astcenc) + { + fmt_error_printf("Can't use astcenc as it hasn't been enabled at compilation time (BASISU_SUPPORT_ASTCENC is 0)\n"); + global_cfg.m_use_astcenc = false; + } +#endif + + if (global_cfg.m_scd_enabled) + { + fmt_debug_printf("Disabling lossy supercompression: SCD is incompatible with it\n"); + global_cfg.m_lossy_supercompression = false; + } + if (global_cfg.m_debug_output) { fmt_debug_printf("\n------------------- astc_ldr::compress_image\n"); @@ -9363,6 +14904,19 @@ bool compress_image( if (!g_initialized) return false; + image sharpened_img; + const image *pOrig_img = &actual_orig_img; + + if ((global_cfg.m_sharpen_flag) && (global_cfg.m_sharpen_amount > 0.0f)) + { + if (!sharpen_image(actual_orig_img, sharpened_img, global_cfg)) + return false; + + pOrig_img = &sharpened_img; + } + + const image& orig_img = *pOrig_img; + const uint32_t width = orig_img.get_width(), height = orig_img.get_height(); if (!is_in_range(width, 1, (int)MAX_WIDTH) || !is_in_range(height, 1, (int)MAX_HEIGHT)) @@ -9382,7 +14936,10 @@ bool compress_image( const bool has_alpha = orig_img.has_alpha(); if (global_cfg.m_debug_output) + { + fmt_debug_printf("Alternative compressors active: astcenc: {}, astcf: {}\n", global_cfg.m_use_astcenc, global_cfg.m_use_astcf); fmt_debug_printf("Encoding image dimensions {}x{}, has alpha: {}\n", orig_img.get_width(), orig_img.get_height(), has_alpha); + } ldr_astc_block_encode_image_high_level_config enc_cfg; @@ -9405,15 +14962,20 @@ bool compress_image( const float psnr_trial_diff_thresh = has_alpha ? global_cfg.m_psnr_trial_diff_thresh_alpha : global_cfg.m_psnr_trial_diff_thresh; const float psnr_trial_diff_thresh_edge = has_alpha ? global_cfg.m_psnr_trial_diff_thresh_edge_alpha : global_cfg.m_psnr_trial_diff_thresh_edge; - enc_cfg.m_blurring_enabled = global_cfg.m_block_blurring_p1; + enc_cfg.m_blurring_enabled_p1 = global_cfg.m_block_blurring_p1; enc_cfg.m_blurring_enabled_p2 = global_cfg.m_block_blurring_p2; + enc_cfg.m_try_simplified_latent_configs = global_cfg.m_try_simplified_latent_configs; + for (uint32_t i = 0; i < 4; i++) { enc_cfg.m_cem_enc_params.m_comp_weights[i] = global_cfg.m_comp_weights[i]; if (!is_in_range(global_cfg.m_comp_weights[i], 1, 256)) + { + fmt_error_printf("astc_ldr::compress_image: m_comp_weights[] are out of range\n"); return false; + } } int cfg_effort_level = global_cfg.m_effort_level; @@ -9421,7 +14983,7 @@ bool compress_image( fmt_debug_printf("Using cfg effort level: {}\n", cfg_effort_level); configure_encoder_effort_level(cfg_effort_level, enc_cfg); - + if (global_cfg.m_force_disable_subsets) { enc_cfg.m_subsets_enabled = false; @@ -9437,19 +14999,166 @@ bool compress_image( enc_cfg.m_cem_enc_params.m_decode_mode_srgb = global_cfg.m_astc_decode_mode_srgb; enc_cfg.m_debug_output = global_cfg.m_debug_output; + enc_cfg.m_debug_output_image_metrics = global_cfg.m_debug_output_image_metrics; enc_cfg.m_debug_images = global_cfg.m_debug_images; enc_cfg.m_debug_file_prefix = global_cfg.m_debug_file_prefix; - ldr_astc_block_encode_image_output enc_out; - - const bool enc_status = ldr_astc_block_encode_image(orig_img, enc_cfg, enc_out); - - if (global_cfg.m_debug_output) - fmt_debug_printf("ldr_astc_block_encode_image: {}\n", enc_status); + //ldr_astc_block_encode_image_output enc_out; + + std::unique_ptr pEnc_out = std::make_unique(); + ldr_astc_block_encode_image_output& enc_out = *pEnc_out; + + bool enc_status = false; + + bool encoded_flag = false; // has the input been encoded yet + + // Candidates are expensive memory wise, so try to place sane limits on them (especially important for WASM). + // This is a per-encoder limit (not total after merging). + uint32_t max_candidate_limit = 16; + + if (global_cfg.m_scd_enabled) + { + if (total_blocks >= (512 * 512)) + { + if (global_cfg.m_merge_basisu_into_output) + max_candidate_limit = 8; + else + max_candidate_limit = 16; + } + else if (total_blocks >= (256 * 256)) + { + if (global_cfg.m_merge_basisu_into_output) + max_candidate_limit = 16; + else + max_candidate_limit = 32; + } + else + { + if (global_cfg.m_merge_basisu_into_output) + max_candidate_limit = 32; + else + max_candidate_limit = 64; + } + } + +#if BASISU_SUPPORT_ASTCENC + if (global_cfg.m_use_astcenc) + { + // TODO: If this somehow fails, we could fall back to our encoder. + enc_status = ldr_astc_block_encode_image_astcenc(orig_img, enc_cfg, global_cfg, enc_out, max_candidate_limit); + if (!enc_status) + { + fmt_error_printf("ldr_astc_block_encode_image_astcenc() failed!\n"); + return false; + } + + if (global_cfg.m_use_astcf) + { + std::unique_ptr pEnc_out_astcf = std::make_unique(); + + enc_status = ldr_astc_block_encode_image_astcf(orig_img, enc_cfg, global_cfg, *pEnc_out_astcf, max_candidate_limit); + if (!enc_status) + { + fmt_error_printf("ldr_astc_block_encode_image_astcf() failed!\n"); + return false; + } + + if (global_cfg.m_debug_output) + fmt_debug_printf("Merging outputs from astcenc and astcf encoders\n"); + + const uint32_t total_better_blocks_astcf = merge_output_candidates(enc_out, *pEnc_out_astcf); + + if (global_cfg.m_debug_output) + fmt_debug_printf("Total astcf blocks better than astcenc's: {} {3.2}%, out of {} total blocks\n", total_better_blocks_astcf, ((float)total_better_blocks_astcf * 100.0f) / (float)total_blocks, total_blocks); + } + + encoded_flag = true; + } +#endif + + if ((!encoded_flag) && (global_cfg.m_use_astcf)) + { + enc_status = ldr_astc_block_encode_image_astcf(orig_img, enc_cfg, global_cfg, enc_out, max_candidate_limit); + if (!enc_status) + { + fmt_error_printf("ldr_astc_block_encode_image_astcf() failed!\n"); + return false; + } + + encoded_flag = true; + } + + // combine the outputs now + if ((encoded_flag) && (global_cfg.m_merge_basisu_into_output)) + { + std::unique_ptr pEnc_out_basisu = std::make_unique(); + + if ((global_cfg.m_effort_level == 0) && (global_cfg.m_astc_block_width == 4) && (global_cfg.m_astc_block_height == 4)) + enc_status = ldr_astc_block_encode_image_fast_4x4(orig_img, enc_cfg, global_cfg, *pEnc_out_basisu, max_candidate_limit); + else + enc_status = ldr_astc_block_encode_image(orig_img, enc_cfg, *pEnc_out_basisu, max_candidate_limit); + + if (global_cfg.m_debug_output) + fmt_debug_printf("ldr_astc_block_encode_image: {}\n", enc_status); + + if (!enc_status) + { + fmt_error_printf("ldr_astc_block_encode_image_fast_4x4() or ldr_astc_block_encode_image() failed!\n"); + return false; + } + + if (global_cfg.m_debug_output) + fmt_debug_printf("Merging basisu output\n"); + + const uint32_t total_better_blocks_basisu = merge_output_candidates(enc_out, *pEnc_out_basisu); + + if (global_cfg.m_debug_output) + fmt_debug_printf("Total better basisu blocks: {} {3.2}%, out of {} total blocks\n", total_better_blocks_basisu, ((float)total_better_blocks_basisu * 100.0f) / (float)total_blocks, total_blocks); + } + else if (!encoded_flag) + { + if ((global_cfg.m_effort_level == 0) && (global_cfg.m_astc_block_width == 4) && (global_cfg.m_astc_block_height == 4)) + { + enc_status = ldr_astc_block_encode_image_fast_4x4(orig_img, enc_cfg, global_cfg, enc_out, max_candidate_limit); + } + else + { + enc_status = ldr_astc_block_encode_image(orig_img, enc_cfg, enc_out, max_candidate_limit); + } + + if (global_cfg.m_debug_output) + fmt_debug_printf("ldr_astc_block_encode_image: {}\n", enc_status); + + if (!enc_status) + { + fmt_error_printf("ldr_astc_block_encode_image_fast_4x4() or ldr_astc_block_encode_image() failed!\n"); + return false; + } + + encoded_flag = true; + } + + assert(encoded_flag); + + if (global_cfg.m_debug_output) + display_candidate_statistics(enc_out); + + if (global_cfg.m_scd_enabled) + { + if (!refine_output_for_deblocking(orig_img, global_cfg, enc_cfg, enc_out)) + return false; + } + + // sanity check decoded weight grids + if (global_cfg.m_use_dct) + { + if (!cross_check_dct(block_width, block_height, num_blocks_x, num_blocks_y, enc_out, global_cfg, enc_cfg)) + return false; + } + + if (global_cfg.m_debug_output) + display_candidate_statistics(enc_out); - if (!enc_status) - return false; - basist::astc_ldr_t::xuastc_ldr_syntax syntax = global_cfg.m_compressed_syntax; if (syntax >= basist::astc_ldr_t::xuastc_ldr_syntax::cTotal) @@ -9598,6 +15307,16 @@ bool compress_image( for (uint32_t x = 0; x < num_blocks_x; x++) coded_blocks(x, y).clear(); + vector2D input_blocks; + if (global_cfg.m_debug_images) + { + input_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++) + input_blocks(x, y).clear(); + } + const bool endpoint_dpcm_global_enable = true; uint32_t total_used_endpoint_dpcm = 0, total_used_endpoint_raw = 0; @@ -9673,6 +15392,11 @@ bool compress_image( uint32_t best_packed_out_block_index = blk_info.m_packed_out_block_index; + if (global_cfg.m_debug_images) + { + input_blocks(bx, by) = blk_info.m_out_blocks[best_packed_out_block_index].m_log_blk; + } + // check for run if ((use_run_commands) && (bx || by)) { @@ -9727,6 +15451,7 @@ bool compress_image( if ((global_cfg.m_lossy_supercompression) && (ref_wpsnr >= replacement_min_psnr) && (!blk_info.m_out_blocks[blk_info.m_packed_out_block_index].m_log_blk.m_solid_color_flag_ldr)) { + // TODO: recompute m_strong_edges? Not all encoders set it. const float psnr_thresh = blk_info.m_strong_edges ? psnr_trial_diff_thresh_edge : psnr_trial_diff_thresh; float best_alt_wpsnr = 0.0f; @@ -10297,14 +16022,14 @@ bool compress_image( for (part_iter = 0; part_iter < tm.m_num_parts; part_iter++) { const bool always_repack_flag = false; - bool blue_contraction_clamped_flag = false, base_ofs_clamped_flag = false; + bool blue_contraction_clamped_flag = false, try_direct_encoding_flag = false; bool conv_status = basist::astc_ldr_t::convert_endpoints_across_cems( pTrial_log_blk->m_color_endpoint_modes[0], pTrial_log_blk->m_endpoint_ise_range, pTrial_log_blk->m_endpoints, cur_actual_cem, cur_log_blk.m_endpoint_ise_range, trial_predicted_endpoints[part_iter], always_repack_flag, endpoints_use_bc[part_iter], false, - blue_contraction_clamped_flag, base_ofs_clamped_flag); + blue_contraction_clamped_flag, try_direct_encoding_flag); if (!conv_status) break; @@ -10381,14 +16106,14 @@ bool compress_image( for (uint32_t part_iter = 0; part_iter < tm.m_num_parts; part_iter++) { const bool always_repack_flag = false; - bool blue_contraction_clamped_flag = false, base_ofs_clamped_flag = false; + bool blue_contraction_clamped_flag = false, try_direct_encoding_flag = false; bool conv_status = basist::astc_ldr_t::convert_endpoints_across_cems( pEndpoint_pred_log_blk->m_color_endpoint_modes[0], pEndpoint_pred_log_blk->m_endpoint_ise_range, pEndpoint_pred_log_blk->m_endpoints, cur_actual_cem, cur_log_blk.m_endpoint_ise_range, predicted_endpoints[part_iter], always_repack_flag, endpoints_use_bc[part_iter], false, - blue_contraction_clamped_flag, base_ofs_clamped_flag); + blue_contraction_clamped_flag, try_direct_encoding_flag); if (!conv_status) { @@ -10495,6 +16220,13 @@ bool compress_image( for (uint32_t plane_iter = 0; plane_iter < total_planes; plane_iter++) { const basist::astc_ldr_t::dct_syms& syms = blk_out.m_packed_dct_plane_data[plane_iter]; + + if (!syms.m_coeffs.size()) + { + fmt_error_printf("compress_image: internal error - no DCT coeffs\n"); + return false; + } + if (syms.m_max_coeff_mag > basist::astc_ldr_t::DCT_MAX_ARITH_COEFF_MAG) { use_dct = false; @@ -10769,7 +16501,7 @@ bool compress_image( if (global_cfg.m_debug_output) { - fmt_debug_printf("Encoding time: {} secs\n", itm.get_elapsed_secs()); + fmt_debug_printf("Supercompression (backend) encoding time: {} secs\n", itm.get_elapsed_secs()); } if (global_cfg.m_debug_images) @@ -10779,6 +16511,7 @@ bool compress_image( if ((global_cfg.m_debug_images) || (global_cfg.m_debug_output)) { + image input_img(width, height); image coded_img(width, height); vector2D phys_blocks(num_blocks_x, num_blocks_y); @@ -10794,7 +16527,7 @@ bool compress_image( bool status = astc_helpers::decode_block(log_blk, block_pixels, block_width, block_height, enc_cfg.m_cem_enc_params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); if (!status) { - fmt_error_printf("astc_helpers::decode_block() failed\n"); + fmt_error_printf("astc_helpers::decode_block() failed (3)\n"); return false; } @@ -10815,17 +16548,44 @@ bool compress_image( coded_img.set_block_clipped(block_pixels, bx * block_width, by * block_height, block_width, block_height); + if (global_cfg.m_debug_images) + { + // input image + + status = astc_helpers::decode_block(input_blocks(bx, by), block_pixels, block_width, block_height, enc_cfg.m_cem_enc_params.m_decode_mode_srgb ? astc_helpers::cDecodeModeSRGB8 : astc_helpers::cDecodeModeLDR8); + if (!status) + { + fmt_error_printf("astc_helpers::decode_block() failed (4)\n"); + return false; + } + + input_img.set_block_clipped(block_pixels, bx * block_width, by * block_height, block_width, block_height); + } + } // bx - } //by + } // by if (global_cfg.m_debug_images) - save_png(global_cfg.m_debug_file_prefix + "coded_img.png", coded_img); - - if (global_cfg.m_debug_output) { - debug_printf("Orig image vs. coded img:\n"); + save_png(global_cfg.m_debug_file_prefix + "input_img.png", input_img); + debug_printf("Wrote input_img.png\n"); + + save_png(global_cfg.m_debug_file_prefix + "coded_img.png", coded_img); + debug_printf("Wrote coded_img.png\n"); + } + + if ((global_cfg.m_debug_output) && (global_cfg.m_debug_output_image_metrics)) + { + if ((global_cfg.m_sharpen_flag) && (global_cfg.m_sharpen_amount > 0.0f)) + debug_printf("Sharepened orig image vs. coded img:\n"); + else + debug_printf("Orig image vs. coded img:\n"); + print_image_metrics(orig_img, coded_img); + + debug_printf("display_astc_statistics:\n"); + display_astc_statistics(coded_blocks, block_width, block_height, orig_img.get_width(), orig_img.get_height(), false); } } @@ -10992,8 +16752,61 @@ void encoder_init() return; g_initialized = true; + + for (uint32_t h_iter = 0; h_iter < basist::astc_ldr_t::astc_block_grid_data_hash_t::LUT_SIZE; ++h_iter) + { + const uint32_t hash_index = basist::astc_ldr_t::g_astc_block_grid_data_hash.m_hash[h_iter]; + if (!hash_index) + continue; + + uint32_t block_width, block_height, grid_width, grid_height; + basist::astc_ldr_t::astc_block_grid_data_hash_t::astc_cfg_from_index(h_iter, block_width, block_height, grid_width, grid_height); + + basist::astc_ldr_t::astc_block_grid_data& grid_data = basist::astc_ldr_t::g_astc_block_grid_data_hash.m_grid_data[hash_index - 1]; + +#if defined(DEBUG) || defined(_DEBUG) + assert(grid_data.m_bw == block_width); + assert(grid_data.m_bh == block_height); + assert(grid_data.m_gw == grid_width); + assert(grid_data.m_gh == grid_height); +#endif + + const uint32_t num_block_samples = block_width * block_height; + const uint32_t num_grid_samples = grid_width * grid_height; + + grid_data.m_upsample_weights.resize(num_block_samples); + astc_helpers::compute_upsample_weights(block_width, block_height, grid_width, grid_height, grid_data.m_upsample_weights.get_ptr()); + + grid_data.m_grid_to_texel_influence_list.resize(num_grid_samples); + for (uint32_t grid_sample = 0; grid_sample < num_grid_samples; grid_sample++) + { + for (uint32_t block_sample = 0; block_sample < num_block_samples; block_sample++) + { + const float weight = grid_data.m_downsample_matrix[grid_sample * num_block_samples + block_sample]; + if (weight == 0.0f) + continue; + + const uint32_t texel_x = block_sample % block_width; + const uint32_t texel_y = block_sample / block_width; + + // Create a list of texels that influence each grid sample, for fast lookup during encoding. + grid_data.m_grid_to_texel_influence_list[grid_sample].push_back(basisu::safe_cast_uint16(texel_x | (texel_y << 8))); + } + + } // grid_sample + + // used for gradient descent + compute_upsample_matrix_transposed(grid_data.m_unweighted_downsample_matrix, block_width, block_height, grid_width, grid_height); + + grid_data.m_one_over_diag_AtA.resize(num_grid_samples); + compute_diag_AtA_vector(block_width, block_height, grid_width, grid_height, grid_data.m_upsample_matrix, grid_data.m_one_over_diag_AtA.get_ptr()); + for (uint32_t i = 0; i < num_grid_samples; i++) + grid_data.m_one_over_diag_AtA[i] = 1.0f / grid_data.m_one_over_diag_AtA[i]; + + } // it } +#if 0 void deblock_filter(uint32_t filter_block_width, uint32_t filter_block_height, const image& src_img, image& dst_img, bool stronger_filtering, int SKIP_THRESH) { image temp_img(src_img); @@ -11096,6 +16909,8 @@ void deblock_filter(uint32_t filter_block_width, uint32_t filter_block_height, c } // y } +#endif } // namespace astc_ldr } // namespace basisu + diff --git a/encoder/basisu_astc_ldr_encode.h b/encoder/basisu_astc_ldr_encode.h index c9e0e83..4f57a68 100644 --- a/encoder/basisu_astc_ldr_encode.h +++ b/encoder/basisu_astc_ldr_encode.h @@ -68,15 +68,34 @@ namespace astc_ldr { // If true, try encoding blurred blocks, in addition to unblurred, for superpass 1 and 2. // Higher quality, but massively slower and not yet tuned/refined. bool m_block_blurring_p1 = false, m_block_blurring_p2 = false; - + // If true, no matter what effort level subset usage will be disabled. bool m_force_disable_subsets = false; // If true, no matter what effort level RGB dual plane usage will be disabled. bool m_force_disable_rgb_dual_plane = false; + + bool m_use_astcenc = false; + bool m_use_astcf = false; + bool m_merge_basisu_into_output = false; + + // If true, the encoder's deblocking passes (SCD - stochastic coordinate descent) are enabled. + bool m_scd_enabled = false; + // When m_scd_enabled is true: whether or not a CPU/GPU postfilter will be applied to the output. + bool m_scd_will_postfilter = true; + bool m_scd_preserve_chroma = true; // if true, the SCD passes will add in chroma (CbCr) error into the composite metric + uint32_t m_num_scd_passes = 12; // [4,256] (clamped automatically), higher=better quality, but slower. + + bool m_sharpen_flag = false; + float m_sharpen_amount = 4.0f; + + bool m_try_simplified_latent_configs = false; + + int m_debug_block_x = -1, m_debug_block_y = -1; bool m_debug_images = false; bool m_debug_output = false; + bool m_debug_output_image_metrics = true; std::string m_debug_file_prefix; @@ -100,6 +119,22 @@ namespace astc_ldr { fmt_debug_printf("m_replacement_min_psnr_alpha: {}\n", m_replacement_min_psnr_alpha); fmt_debug_printf("m_psnr_trial_diff_thresh_alpha: {}\n", m_psnr_trial_diff_thresh_alpha); fmt_debug_printf("m_psnr_trial_diff_thresh_edge_alpha: {}\n", m_psnr_trial_diff_thresh_edge_alpha); + + fmt_debug_printf("m_use_astcenc: {}\n", m_use_astcenc); + fmt_debug_printf("m_use_astcf: {}\n", m_use_astcf); + fmt_debug_printf("m_merge_basisu_into_output: {}\n", m_merge_basisu_into_output); + + fmt_debug_printf("m_scd_enabled: {}\n", m_scd_enabled); + fmt_debug_printf("m_scd_will_postfilter: {}\n", m_scd_will_postfilter); + fmt_debug_printf("m_num_scd_passes: {}\n", m_num_scd_passes); + fmt_debug_printf("m_scd_preserve_chroma: {}\n", m_scd_preserve_chroma); + + fmt_debug_printf("m_sharpen_flag: {}\n", m_sharpen_flag); + fmt_debug_printf("m_sharpen_amount: {}\n", m_sharpen_amount); + + fmt_debug_printf("m_try_simplified_latent_configs: {}\n", m_try_simplified_latent_configs); + + fmt_debug_printf("m_debug_block_x/m_debug_block_y: {}x{}\n", m_debug_block_x, m_debug_block_y); fmt_debug_printf("m_debug_images: {}\n", m_debug_images); } @@ -110,7 +145,12 @@ namespace astc_ldr { const astc_ldr_encode_config& global_cfg, job_pool& job_pool); - void deblock_filter(uint32_t filter_block_width, uint32_t filter_block_height, const image& src_img, image& dst_img, bool stronger_filtering = false, int SKIP_THRESH = 24); + void downsample_weight_residual_grid( + const float* pMatrix_weights, + uint32_t bx, uint32_t by, // source/from dimension (block size) + uint32_t wx, uint32_t wy, // dest/to dimension (grid size) + const int* pSrc_weights, // these are dequantized weights, NOT ISE symbols, [by][bx] + float* pDst_weights); // [wy][wx] } // namespace astc_ldr } // namespace basisu diff --git a/encoder/basisu_basis_file.cpp b/encoder/basisu_basis_file.cpp index 19b398c..e5f852c 100644 --- a/encoder/basisu_basis_file.cpp +++ b/encoder/basisu_basis_file.cpp @@ -20,7 +20,13 @@ namespace basisu { - void basisu_file::create_header(const basisu_backend_output &encoder_output, basist::basis_texture_type tex_type, uint32_t userdata0, uint32_t userdata1, bool y_flipped, uint32_t us_per_frame) + void basisu_file::create_header( + const basisu_backend_output& encoder_output, + basist::basis_texture_type tex_type, + uint32_t userdata0, + uint32_t userdata1, + bool y_flipped, + uint32_t us_per_frame) { m_header.m_header_size = sizeof(basist::basis_file_header); @@ -93,11 +99,17 @@ namespace basisu m_header.m_tables_file_size = (uint32_t)encoder_output.m_slice_image_tables.size(); m_header.m_slice_desc_file_ofs = m_slice_descs_file_ofs; + + if (m_kv_data_file_size) + { + m_header.m_extended_file_ofs = m_kv_data_file_ofs; + m_header.m_extended_file_size = m_kv_data_file_size; + } } - bool basisu_file::create_image_descs(const basisu_backend_output &encoder_output) + bool basisu_file::create_image_descs(const basisu_backend_output& encoder_output) { - const basisu_backend_slice_desc_vec &slice_descs = encoder_output.m_slice_desc; + const basisu_backend_slice_desc_vec& slice_descs = encoder_output.m_slice_desc; m_images_descs.resize(slice_descs.size()); @@ -143,11 +155,14 @@ namespace basisu return true; } - void basisu_file::create_comp_data(const basisu_backend_output &encoder_output) + void basisu_file::create_comp_data(const basisu_backend_output& encoder_output, const uint8_vec& kv_data) { - const basisu_backend_slice_desc_vec &slice_descs = encoder_output.m_slice_desc; + const basisu_backend_slice_desc_vec& slice_descs = encoder_output.m_slice_desc; - append_vector(m_comp_data, reinterpret_cast(&m_header), sizeof(m_header)); + append_vector(m_comp_data, reinterpret_cast(&m_header), sizeof(m_header)); + + assert(m_comp_data.size() == m_kv_data_file_ofs); + append_vector(m_comp_data, reinterpret_cast(&kv_data[0]), kv_data.size()); assert(m_comp_data.size() == m_slice_descs_file_ofs); append_vector(m_comp_data, reinterpret_cast(&m_images_descs[0]), m_images_descs.size() * sizeof(m_images_descs[0])); @@ -182,7 +197,7 @@ namespace basisu void basisu_file::fixup_crcs() { - basist::basis_file_header *pHeader = reinterpret_cast(&m_comp_data[0]); + basist::basis_file_header* pHeader = reinterpret_cast(&m_comp_data[0]); pHeader->m_data_size = m_total_file_size - sizeof(basist::basis_file_header); pHeader->m_data_crc16 = basist::crc16(&m_comp_data[0] + sizeof(basist::basis_file_header), m_total_file_size - sizeof(basist::basis_file_header), 0); @@ -193,24 +208,119 @@ namespace basisu pHeader->m_ver = BASIS_FILE_VERSION;// basist::basis_file_header::cBASISFirstVersion; } - bool basisu_file::init(const basisu_backend_output &encoder_output, basist::basis_texture_type tex_type, uint32_t userdata0, uint32_t userdata1, bool y_flipped, uint32_t us_per_frame) + bool basisu_file::check_key(const uint8_vec& k) + { + if (!k.size()) + return false; + + if (k.back() != '\0') + return false; + + for (uint32_t i = 0; i < k.size(); i++) + { + const uint8_t c = k[i]; + + if (!c) + { + if (i != (k.size() - 1)) + return false; + } + } + + // String len (not including null terminator) must be [1,255] bytes. + if ((k.size() - 1) > 255) + return false; + + return true; + } + + bool basisu_file::create_key_value_data(uint8_vec& kv_data, const basist::key_value_vec& key_values) + { + kv_data.resize(0); + + if (!key_values.size()) + return true; + + if (key_values.size() > UINT32_MAX) + return false; + + kv_data.reserve(4096); + + basist::basis_key_value_data_header kv_header; + clear_obj(kv_header); + + kv_header.m_num = (uint32_t)key_values.size(); + + for (uint32_t i = 0; i < key_values.size(); i++) + { + const basist::key_value& kv = key_values[i]; + + // The key must be a null terminated C-style string of [1,255] characters. + if (!check_key(kv.m_key)) + return false; + + assert(kv.m_key.size()); + assert((kv.m_key.size() - 1) <= UINT8_MAX); + + kv_data.push_back((uint8_t)(kv.m_key.size() - 1)); + + // The value is assumed binary, but it could be a null terminated string. We just pass it through unchanged. + if (kv.m_value.size() > UINT32_MAX) + return false; + + kv_data.push_back(kv.m_value.size() & 0xFF); + kv_data.push_back((kv.m_value.size() >> 8) & 0xFF); + kv_data.push_back((kv.m_value.size() >> 16) & 0xFF); + kv_data.push_back((kv.m_value.size() >> 24) & 0xFF); + + // remove key's null terminator + uint8_vec temp_key(kv.m_key); + assert(temp_key.back() == '\0'); + temp_key.pop_back(); + kv_data.append(temp_key); + + kv_data.append(kv.m_value); + } + + assert(kv_data.size()); + + if (kv_data.size() > UINT32_MAX) + return false; + + kv_header.m_crc16 = basist::crc16(kv_data.get_ptr(), kv_data.size(), 0); + kv_header.m_sig = basist::cBASISKVDataSig; + + kv_data.insert(0, (const uint8_t *)&kv_header, sizeof(kv_header)); + + if (kv_data.size() > UINT32_MAX) + return false; + + return true; + } + + bool basisu_file::init(const basisu_backend_output &encoder_output, basist::basis_texture_type tex_type, uint32_t userdata0, uint32_t userdata1, bool y_flipped, uint32_t us_per_frame, const basist::key_value_vec& key_values) { clear(); + uint8_vec kv_data; + if (!create_key_value_data(kv_data, key_values)) + { + error_printf("basisu_file::init: Failed creating key value data!\n"); + return false; + } + const basisu_backend_slice_desc_vec &slice_descs = encoder_output.m_slice_desc; // The Basis file uses 32-bit fields for lots of stuff, so make sure it's not too large. - uint64_t check_size = 0; + uint64_t check_size = (uint64_t)sizeof(basist::basis_file_header) + kv_data.size() + (uint64_t)sizeof(basist::basis_slice_desc) * slice_descs.size(); + if (!encoder_output.m_uses_global_codebooks) { - check_size = (uint64_t)sizeof(basist::basis_file_header) + (uint64_t)sizeof(basist::basis_slice_desc) * slice_descs.size() + - (uint64_t)encoder_output.m_endpoint_palette.size() + (uint64_t)encoder_output.m_selector_palette.size() + (uint64_t)encoder_output.m_slice_image_tables.size(); - } - else - { - check_size = (uint64_t)sizeof(basist::basis_file_header) + (uint64_t)sizeof(basist::basis_slice_desc) * slice_descs.size() + - (uint64_t)encoder_output.m_slice_image_tables.size(); + check_size += (uint64_t)encoder_output.m_endpoint_palette.size() + (uint64_t)encoder_output.m_selector_palette.size(); } + + check_size += (uint64_t)encoder_output.m_slice_image_tables.size(); + if (check_size >= 0xFFFF0000ULL) { error_printf("basisu_file::init: File is too large!\n"); @@ -218,7 +328,13 @@ namespace basisu } m_header_file_ofs = 0; - m_slice_descs_file_ofs = sizeof(basist::basis_file_header); + + m_kv_data_file_ofs = sizeof(basist::basis_file_header); // key-value data goes right after the header + assert(kv_data.size() <= UINT32_MAX); + m_kv_data_file_size = (uint32_t)kv_data.size(); + + m_slice_descs_file_ofs = m_kv_data_file_ofs + (uint32_t)kv_data.size(); + if (encoder_output.m_tex_format == basist::basis_tex_format::cETC1S) { if (encoder_output.m_uses_global_codebooks) @@ -243,9 +359,12 @@ namespace basisu m_first_image_file_ofs = m_slice_descs_file_ofs + sizeof(basist::basis_slice_desc) * (uint32_t)slice_descs.size(); } + assert(check_size == m_first_image_file_ofs); + uint64_t total_file_size = m_first_image_file_ofs; for (uint32_t i = 0; i < encoder_output.m_slice_image_data.size(); i++) total_file_size += encoder_output.m_slice_image_data[i].size(); + if (total_file_size >= 0xFFFF0000ULL) { error_printf("basisu_file::init: File is too large!\n"); @@ -259,7 +378,7 @@ namespace basisu if (!create_image_descs(encoder_output)) return false; - create_comp_data(encoder_output); + create_comp_data(encoder_output, kv_data); fixup_crcs(); diff --git a/encoder/basisu_basis_file.h b/encoder/basisu_basis_file.h index 57448bc..b237ed5 100644 --- a/encoder/basisu_basis_file.h +++ b/encoder/basisu_basis_file.h @@ -36,6 +36,8 @@ namespace basisu m_header_file_ofs = 0; m_slice_descs_file_ofs = 0; + m_kv_data_file_ofs = 0; + m_kv_data_file_size = 0; m_endpoint_cb_file_ofs = 0; m_selector_cb_file_ofs = 0; m_tables_file_ofs = 0; @@ -43,7 +45,11 @@ namespace basisu m_total_file_size = 0; } - bool init(const basisu_backend_output& encoder_output, basist::basis_texture_type tex_type, uint32_t userdata0, uint32_t userdata1, bool y_flipped, uint32_t us_per_frame); + bool init(const basisu_backend_output& encoder_output, + basist::basis_texture_type tex_type, + uint32_t userdata0, uint32_t userdata1, + bool y_flipped, uint32_t us_per_frame, + const basist::key_value_vec &key_values); const uint8_vec &get_compressed_data() const { return m_comp_data; } @@ -53,18 +59,25 @@ namespace basisu uint8_vec m_comp_data; - uint32_t m_header_file_ofs; - uint32_t m_slice_descs_file_ofs; - uint32_t m_endpoint_cb_file_ofs; - uint32_t m_selector_cb_file_ofs; - uint32_t m_tables_file_ofs; - uint32_t m_first_image_file_ofs; - uint32_t m_total_file_size; + uint32_t m_header_file_ofs; // Header file ofs + uint32_t m_slice_descs_file_ofs; // Slice descs file ofs + + uint32_t m_kv_data_file_ofs; // Key-value file ofs + uint32_t m_kv_data_file_size; // Key-value file size + + uint32_t m_endpoint_cb_file_ofs; // Endpoint CB file ofs + uint32_t m_selector_cb_file_ofs; // Selector CB file ofs + uint32_t m_tables_file_ofs; // Huffman tables file ofs + + uint32_t m_first_image_file_ofs; // First image data file ofs + uint32_t m_total_file_size; // Total file size file ofs void create_header(const basisu_backend_output& encoder_output, basist::basis_texture_type tex_type, uint32_t userdata0, uint32_t userdata1, bool y_flipped, uint32_t us_per_frame); bool create_image_descs(const basisu_backend_output& encoder_output); - void create_comp_data(const basisu_backend_output& encoder_output); + void create_comp_data(const basisu_backend_output& encoder_output, const uint8_vec& kv_data); void fixup_crcs(); + bool check_key(const uint8_vec& k); + bool create_key_value_data(uint8_vec& kv_data, const basist::key_value_vec& key_values); }; } // namespace basisu diff --git a/encoder/basisu_comp.cpp b/encoder/basisu_comp.cpp index dee91aa..6d972d1 100644 --- a/encoder/basisu_comp.cpp +++ b/encoder/basisu_comp.cpp @@ -137,6 +137,15 @@ namespace basisu fmt_debug_printf("Low-level XUASTC LDR effort level (0-10): {}\n", m_xuastc_ldr_effort_level); } + else if (mode == basist::basis_tex_format::cXUBC7) + { + if (effort >= 0) + m_xubc7_effort_level = clamp(effort, 0, 10); + else if (set_defaults) + m_xubc7_effort_level = xbc7::DEFAULT_EFFORT_LEVEL; + + fmt_debug_printf("Low-level XUBC7 effort level (0-10): {}\n", m_xubc7_effort_level); + } else { assert(0); @@ -179,6 +188,13 @@ namespace basisu // Attempt to derive a reasonable lambda from quality m_rdo_uastc_ldr_4x4_quality_scalar = uastc_ldr_4x4_lambda_from_quality(fquality); } + else if (quality == 100) + { + // Disable RDO postprocessing + m_rdo_uastc_ldr_4x4 = false; + + m_rdo_uastc_ldr_4x4_quality_scalar = 1.0f; + } else if (set_defaults) { m_rdo_uastc_ldr_4x4 = false; @@ -224,6 +240,13 @@ namespace basisu m_xuastc_ldr_use_dct = true; m_xuastc_ldr_use_lossy_supercompression = true; } + else if (quality == 100) + { + // Disable DCT + lossy supercompression + m_quality_level = -1; + m_xuastc_ldr_use_dct = false; + m_xuastc_ldr_use_lossy_supercompression = false; + } else if (set_defaults) { m_quality_level = -1; @@ -233,6 +256,19 @@ namespace basisu fmt_debug_printf("Low-level XUASTC quality level (0-100): {}, Use DCT: {}, Use lossy supercompression: {}\n", m_quality_level, m_xuastc_ldr_use_dct, m_xuastc_ldr_use_lossy_supercompression); } + else if (mode == basist::basis_tex_format::cXUBC7) + { + if ((quality >= 0) && (quality <= 100)) + { + m_quality_level = clamp(quality, 1, 100); + } + else if (set_defaults) + { + m_quality_level = -1; + } + + fmt_debug_printf("Low-level XUBC7 quality level (0-100): {}\n", m_quality_level); + } else { assert(0); @@ -257,7 +293,8 @@ namespace basisu m_ldr_to_hdr_upconversion_nit_multiplier(1.0f), m_upconverted_any_ldr_images(false), m_any_source_image_has_alpha(false), - m_opencl_failed(false) + m_opencl_failed(false), + m_has_been_processed(false) { debug_printf("basis_compressor::basis_compressor\n"); @@ -414,6 +451,8 @@ namespace basisu m_ldr_to_hdr_upconversion_nit_multiplier = 1.0f; m_upconverted_any_ldr_images = false; + m_has_been_processed = false; + m_total_slice_orig_texels = 0; m_basis_file_size = 0; m_basis_bits_per_texel = 0.0f; @@ -501,6 +540,7 @@ namespace basisu PRINT_BOOL_VALUE(m_read_source_images); PRINT_BOOL_VALUE(m_write_output_basis_or_ktx2_files); PRINT_BOOL_VALUE(m_compute_stats); + PRINT_BOOL_VALUE(m_psnr_hvs_m_stats); PRINT_BOOL_VALUE(m_check_for_alpha); PRINT_BOOL_VALUE(m_force_alpha); debug_printf("swizzle: %d,%d,%d,%d\n", @@ -560,47 +600,68 @@ namespace basisu debug_printf("KTX2 UASTC supercompression: %u\n", m_params.m_ktx2_uastc_supercompression); debug_printf("KTX2 Zstd supercompression level: %i\n", (int)m_params.m_ktx2_zstd_supercompression_level); debug_printf("KTX2/basis sRGB transfer function: %u\n", (int)m_params.m_ktx2_and_basis_srgb_transfer_function); - debug_printf("Total KTX2 key values: %u\n", m_params.m_ktx2_key_values.size()); - for (uint32_t i = 0; i < m_params.m_ktx2_key_values.size(); i++) + + debug_printf("Total key values: %u\n", m_params.m_key_values.size()); + for (uint32_t i = 0; i < m_params.m_key_values.size(); i++) { - debug_printf("Key: \"%s\"\n", m_params.m_ktx2_key_values[i].m_key.data()); - debug_printf("Value size: %u\n", m_params.m_ktx2_key_values[i].m_value.size()); + debug_printf(" Key: \"%s\"\n", m_params.m_key_values[i].m_key.data()); + debug_printf(" Value size: %u\n", m_params.m_key_values[i].m_value.size()); } PRINT_BOOL_VALUE(m_validate_output_data); PRINT_UINT_VALUE(m_transcode_flags); PRINT_BOOL_VALUE(m_ldr_hdr_upconversion_srgb_to_linear); PRINT_FLOAT_VALUE(m_ldr_hdr_upconversion_nit_multiplier); + PRINT_FLOAT_VALUE(m_ldr_hdr_upconversion_black_bias); debug_printf("Allow UASTC HDR 4x4 uber mode: %u\n", m_params.m_uastc_hdr_4x4_options.m_allow_uber_mode); debug_printf("UASTC HDR 4x4 ultra quant: %u\n", m_params.m_uastc_hdr_4x4_options.m_ultra_quant); PRINT_BOOL_VALUE(m_hdr_favor_astc); PRINT_INT_VALUE(m_xuastc_ldr_effort_level); PRINT_BOOL_VALUE(m_xuastc_ldr_blurring); + PRINT_INT_VALUE(m_xuastc_ldr_astc_comp_selection); + PRINT_INT_VALUE(m_xuastc_ldr_sharpen_mode); + PRINT_FLOAT_VALUE(m_xuastc_ldr_sharpen_amount); + PRINT_INT_VALUE(m_xuastc_ldr_deblocking_mode); + PRINT_UINT_VALUE(m_xuastc_ldr_num_deblocking_passes); + PRINT_BOOL_VALUE(m_xuastc_ldr_heavy_subset_usage); PRINT_BOOL_VALUE(m_xuastc_ldr_use_dct); PRINT_BOOL_VALUE(m_xuastc_ldr_use_lossy_supercompression); PRINT_BOOL_VALUE(m_xuastc_ldr_force_disable_subsets); PRINT_BOOL_VALUE(m_xuastc_ldr_force_disable_rgb_dual_plane); PRINT_INT_VALUE(m_xuastc_ldr_syntax); - + debug_printf("XUASTC LDR channel weights: "); for (uint32_t i = 0; i < 4; i++) fmt_debug_printf("{} ", m_params.m_xuastc_ldr_channel_weights[i]); debug_printf("\n"); - + PRINT_FLOAT_VALUE(m_ls_min_psnr); PRINT_FLOAT_VALUE(m_ls_thresh_psnr); PRINT_FLOAT_VALUE(m_ls_thresh_edge_psnr); PRINT_FLOAT_VALUE(m_ls_min_alpha_psnr); PRINT_FLOAT_VALUE(m_ls_thresh_alpha_psnr); PRINT_FLOAT_VALUE(m_ls_thresh_edge_alpha_psnr); - + + PRINT_INT_VALUE(m_xuastc_ldr_debug_block_x); + PRINT_INT_VALUE(m_xuastc_ldr_debug_block_y); + + PRINT_INT_VALUE(m_xubc7_effort_level); + PRINT_INT_VALUE(m_xubc7_rdo_level); + PRINT_INT_VALUE(m_xubc7_num_stripes); + PRINT_INT_VALUE(m_xubc7_encoder); + PRINT_INT_VALUE(m_xubc7_bc7e_scalar_level); + + PRINT_BOOL_VALUE(m_status_output); + PRINT_BOOL_VALUE(m_print_stats); + #undef PRINT_BOOL_VALUE #undef PRINT_INT_VALUE #undef PRINT_UINT_VALUE #undef PRINT_FLOAT_VALUE fmt_printf("m_format_mode: {}\n", (uint32_t)m_params.get_format_mode()); + fmt_printf("job pool total threads: {}\n", m_params.m_pJob_pool ? (uint32_t)m_params.m_pJob_pool->get_total_threads() : 0); fmt_printf("\n"); } @@ -619,11 +680,26 @@ namespace basisu bool basis_compressor::pick_format_mode() { - // Unfortunately due to the legacy of this code and backwards API compatibility this is more complex than I would like. - m_fmt_mode = basist::basis_tex_format::cETC1S; m_fmt_mode_block_width = 4; m_fmt_mode_block_height = 4; + if ((uint32_t)m_params.m_format_mode == (uint32_t)basist::basis_tex_format::cXUBC7) + { + // XUBC7 - must be set using the new format API. + + m_fmt_mode = basist::basis_tex_format::cXUBC7; + + m_params.m_xuastc_or_astc_ldr_basis_tex_format = -1; + m_params.m_hdr = false; + m_params.m_uastc = true; + m_params.m_hdr_mode = hdr_modes::cUASTC_HDR_4X4; // doesn't matter + return true; + } + + // Unfortunately due to the legacy of this code and backwards API compatibility this is more complex than I would like. + // First assume ETC1S unless we're being overridden. + m_fmt_mode = basist::basis_tex_format::cETC1S; + if (m_params.m_hdr) { assert(m_params.m_uastc); @@ -734,6 +810,12 @@ namespace basisu break; } + case basist::basis_tex_format::cXUBC7: + { + fmt_debug_printf("Format Mode: cXUBC7\n"); + break; + } + default: assert(0); break; @@ -747,6 +829,13 @@ namespace basisu { debug_printf("basis_compressor::process\n"); + if (m_has_been_processed) + { + error_printf("basis_compressor::process: process()/process_source_images() may only be called once per init().\n"); + return cECFailedInitializing; + } + m_has_been_processed = true; + if (!read_dds_source_images()) return cECFailedReadingSourceImages; @@ -811,8 +900,19 @@ namespace basisu else if (m_params.m_uastc) { error_code ec = cECFailedEncodeUASTC; - - if (basis_tex_format_is_xuastc_ldr(m_fmt_mode) || basis_tex_format_is_astc_ldr(m_fmt_mode)) + + if (m_fmt_mode == basist::basis_tex_format::cXUBC7) + { + // XUBC7 + if (m_params.m_status_output) + { + fmt_printf("Mode: XUBC7, Effort Level (0-10): {}, Quality level (1-100): {}, RDO level (0-100): {}\n", + m_params.m_xubc7_effort_level, m_params.m_quality_level, m_params.m_xubc7_rdo_level); + } + + ec = encode_slices_to_xubc7(); + } + else if (basis_tex_format_is_xuastc_ldr(m_fmt_mode) || basis_tex_format_is_astc_ldr(m_fmt_mode)) { // XUASTC LDR 4x4-12x12 or ASTC LDR 4x4-12x12 if (m_params.m_status_output) @@ -822,7 +922,7 @@ namespace basisu if (basis_tex_format_is_xuastc_ldr(m_fmt_mode)) { - fmt_printf("Mode: XUASTC LDR {}x{}, Effort Level (0-10): {}, Disable Subsets: {}, Disable RGB Dual Plane: {}\nWeight grid DCT: {}, DCT quality level (1-100): {}, Lossy supercompression: {}, sRGB8 ASTC decode profile: {}, Syntax: {}, Channel weights: {} {} {} {}\n", + fmt_printf("Mode: XUASTC LDR {}x{}, Effort Level (0-10): {}, Disable Subsets: {}, Disable RGB Dual Plane: {}\nWeight grid DCT: {}, DCT quality level (1-100): {}, Lossy supercompression: {}\nsRGB8 ASTC decode profile: {}, Syntax: {}, Channel weights: {} {} {} {}\n", block_width, block_height, (int)m_params.m_xuastc_ldr_effort_level, (bool)m_params.m_xuastc_ldr_force_disable_subsets, (bool)m_params.m_xuastc_ldr_force_disable_rgb_dual_plane, (bool)m_params.m_xuastc_ldr_use_dct, (bool)m_params.m_xuastc_ldr_use_dct ? m_params.m_quality_level : 0, @@ -833,7 +933,7 @@ namespace basisu } else { - fmt_printf("Mode: ASTC LDR {}x{}, Effort Level (0-10): {}, Disable Subsets: {}, Disable RGB Dual Plane: {}, sRGB8 ASTC decode profile: {}, Syntax: {}, Channel weights: {} {} {} {}\n", + fmt_printf("Mode: ASTC LDR {}x{}, Effort Level (0-10): {}, Disable Subsets: {}, Disable RGB Dual Plane: {}\nsRGB8 ASTC decode profile: {}, Syntax: {}, Channel weights: {} {} {} {}\n", block_width, block_height, (int)m_params.m_xuastc_ldr_effort_level, (bool)m_params.m_xuastc_ldr_force_disable_subsets, (bool)m_params.m_xuastc_ldr_force_disable_rgb_dual_plane, (bool)m_params.m_ktx2_and_basis_srgb_transfer_function, @@ -863,6 +963,8 @@ namespace basisu } else { + assert(m_fmt_mode == basist::basis_tex_format::cETC1S); + // ETC1S if (m_params.m_status_output) printf("Mode: ETC1S Quality (0-255): %i, Comp Level (Effort, 0-6): %i\n", m_params.m_quality_level, (int)m_params.m_etc1s_compression_level); @@ -889,6 +991,48 @@ namespace basisu if (!write_output_files_and_compute_stats()) return cECFailedWritingOutput; + if (m_params.m_status_output) + fmt_printf("basis_compressor::process: Success\n"); + + return cECSuccess; + } + + // Special-purpose alternative to process(): runs ONLY the source image loading/preparation prefix (identical to the + // first stage of process()), then STOPS before block extraction, encoding, or file output. After success the caller + // inspects the prepared slices/metadata via the get_slice_*()/get_params()/etc. accessors. See the header for usage. + basis_compressor::error_code basis_compressor::process_source_images() + { + debug_printf("basis_compressor::process_source_images\n"); + + if (m_has_been_processed) + { + error_printf("basis_compressor::process_source_images: process()/process_source_images() may only be called once per init().\n"); + return cECFailedInitializing; + } + m_has_been_processed = true; + + if (!read_dds_source_images()) + return cECFailedReadingSourceImages; + + // Note: After here m_params.m_hdr, m_params.m_uastc and m_fmt_mode, m_fmt_mode_block_width/height cannot be changed. + if (!pick_format_mode()) + return cECFailedInvalidParameters; + + if (!read_source_images()) + return cECFailedReadingSourceImages; + + if (!validate_texture_type_constraints()) + return cECFailedValidating; + + if (m_params.m_create_ktx2_file) + { + if (!validate_ktx2_constraints()) + { + error_printf("Inputs do not satisfy .KTX2 texture constraints: all source images must be the same resolution and have the same number of mipmap levels.\n"); + return cECFailedValidating; + } + } + return cECSuccess; } @@ -930,6 +1074,9 @@ namespace basisu for (uint32_t slice_index = 0; slice_index < m_slice_descs.size(); slice_index++) { + if (m_params.m_status_output) + fmt_printf("Encoding slice {}\n", slice_index); + gpu_image& dst_tex = m_uastc_slice_textures[slice_index]; uint8_vec &dst_buf = m_uastc_backend_output.m_slice_image_data[slice_index]; @@ -1075,6 +1222,9 @@ namespace basisu for (uint32_t slice_index = 0; slice_index < m_slice_descs.size(); slice_index++) { + if (m_params.m_status_output) + fmt_printf("Encoding slice {}\n", slice_index); + gpu_image& tex = m_uastc_slice_textures[slice_index]; basisu_backend_slice_desc& slice_desc = m_slice_descs[slice_index]; (void)slice_desc; @@ -1320,6 +1470,8 @@ namespace basisu block_width = basist::basis_get_block_width(transcoder_tex_fmt); block_height = basist::basis_get_block_height(transcoder_tex_fmt); + const bool very_large_blocks_flag = (block_width * block_height) >= basist::BASISU_DEBLOCKING_BLOCK_SIZE_THRESHOLD; + #if defined(_DEBUG) || defined(DEBUG) // sanity checking { @@ -1343,13 +1495,117 @@ namespace basisu astc_ldr::astc_ldr_encode_config cfg; cfg.m_astc_block_width = block_width; cfg.m_astc_block_height = block_height; - cfg.m_block_blurring_p1 = m_params.m_xuastc_ldr_blurring; // experimental, not recommended, very slow - cfg.m_block_blurring_p2 = m_params.m_xuastc_ldr_blurring; // experimental, not recommended, very slow + cfg.m_block_blurring_p1 = m_params.m_xuastc_ldr_blurring; + cfg.m_block_blurring_p2 = m_params.m_xuastc_ldr_blurring; cfg.m_effort_level = clamp(m_params.m_xuastc_ldr_effort_level, astc_ldr::EFFORT_LEVEL_MIN, astc_ldr::EFFORT_LEVEL_MAX); cfg.m_force_disable_subsets = m_params.m_xuastc_ldr_force_disable_subsets; cfg.m_force_disable_rgb_dual_plane = m_params.m_xuastc_ldr_force_disable_rgb_dual_plane; cfg.m_astc_decode_mode_srgb = m_params.m_ktx2_and_basis_srgb_transfer_function; + cfg.m_use_astcenc = false; + cfg.m_use_astcf = false; + cfg.m_merge_basisu_into_output = false; + + switch (m_params.m_xuastc_ldr_astc_comp_selection) + { + case (int)xuastc_ldr_astc_comp_selection::cAuto: + { + switch (cfg.m_effort_level) + { + case 0: + { + if ((block_width == 4) && (block_height == 4)) + { + // special case: switches to bc7f->ASTC LDR 4x4 transcoding + } + else + { + cfg.m_use_astcf = true; + } + + break; + } + case 1: + case 2: + { + cfg.m_use_astcf = true; + break; + } + case 3: + { + if (((block_width * block_height) > 64) || (m_any_source_image_has_alpha)) + cfg.m_merge_basisu_into_output = true; + + cfg.m_use_astcf = true; + break; + } + case 4: + { + cfg.m_merge_basisu_into_output = true; + cfg.m_use_astcf = true; + break; + } + default: + { + cfg.m_use_astcf = true; + cfg.m_merge_basisu_into_output = true; + break; + } + } + + break; + } + case (int)xuastc_ldr_astc_comp_selection::cBasisU: + { + break; + } + case (int)xuastc_ldr_astc_comp_selection::cASTCENC: + { + cfg.m_use_astcenc = true; + break; + } + case (int)xuastc_ldr_astc_comp_selection::cASTCF: + { + cfg.m_use_astcf = true; + break; + } + case (int)xuastc_ldr_astc_comp_selection::cBasisU_and_ASTCENC: + { + cfg.m_use_astcenc = true; + cfg.m_merge_basisu_into_output = true; + break; + } + case (int)xuastc_ldr_astc_comp_selection::cBasisU_and_ASTCF: + { + cfg.m_use_astcf = true; + cfg.m_merge_basisu_into_output = true; + break; + } + case (int)xuastc_ldr_astc_comp_selection::cUseAll: + { + cfg.m_use_astcenc = true; + cfg.m_use_astcf = true; + cfg.m_merge_basisu_into_output = true; + break; + } + default: + { + fmt_error_printf("Invalid m_xuastc_ldr_astc_comp_selection\n"); + return cECFailedInvalidParameters; + } + } + + cfg.m_sharpen_flag = false; + if (m_params.m_xuastc_ldr_sharpen_mode == (int)xuastc_ldr_sharpen_mode::cAllBlockSizes) + cfg.m_sharpen_flag = true; + else if (m_params.m_xuastc_ldr_sharpen_mode == (int)xuastc_ldr_sharpen_mode::cOnlyLargestBlocks) + { + // only 10x8 or larger + cfg.m_sharpen_flag = very_large_blocks_flag; + } + + cfg.m_sharpen_amount = m_params.m_xuastc_ldr_sharpen_amount; + cfg.m_compressed_syntax = (basist::astc_ldr_t::xuastc_ldr_syntax)(int)m_params.m_xuastc_ldr_syntax; if (cfg.m_compressed_syntax >= basist::astc_ldr_t::xuastc_ldr_syntax::cTotal) { @@ -1359,7 +1615,7 @@ namespace basisu if (basist::basis_tex_format_is_xuastc_ldr(m_fmt_mode)) { - if (m_params.m_quality_level >= 0) + if ((m_params.m_quality_level >= astc_ldr::DCT_QUALITY_MIN) && (m_params.m_quality_level < astc_ldr::DCT_QUALITY_MAX)) { // Enable weight grid DCT cfg.m_dct_quality = static_cast(clamp(m_params.m_quality_level, astc_ldr::DCT_QUALITY_MIN, astc_ldr::DCT_QUALITY_MAX)); @@ -1367,10 +1623,10 @@ namespace basisu } else { - // No DCT quality level specified, but they wanted DCT - display warning + // <= 0 or >= 100 DCT quality level specified, but they wanted DCT - display warning if (m_params.m_xuastc_ldr_use_dct) { - printf("Warning: m_xuastc_ldr_use_dct enabled, but m_quality_level was -1 (not set). Not using DCT. Quality level must range from 1-100.\n"); + printf("Warning: m_xuastc_ldr_use_dct enabled, but quality level must range from [1,100).\n"); } } } @@ -1387,12 +1643,109 @@ namespace basisu cfg.m_replacement_min_psnr_alpha = m_params.m_ls_min_alpha_psnr; cfg.m_psnr_trial_diff_thresh_alpha = m_params.m_ls_thresh_alpha_psnr; cfg.m_psnr_trial_diff_thresh_edge_alpha = m_params.m_ls_thresh_edge_alpha_psnr; + + cfg.m_debug_block_x = m_params.m_xuastc_ldr_debug_block_x; + cfg.m_debug_block_y = m_params.m_xuastc_ldr_debug_block_y; cfg.m_debug_output = m_params.m_debug; cfg.m_debug_images = m_params.m_debug_images; + cfg.m_debug_output_image_metrics = m_params.m_compute_stats && m_params.m_print_stats; + + // By default: No SCD, no deblocking post filtering + cfg.m_scd_enabled = false; + cfg.m_scd_will_postfilter = false; + cfg.m_num_scd_passes = 0; + + // SCD passes will factor in a chroma mismatch penality into the composite scores if m_params.m_perceptual is true (the default). + cfg.m_scd_preserve_chroma = m_params.m_perceptual; + + // apply deblocking mode + if (((very_large_blocks_flag) && (m_params.m_xuastc_ldr_deblocking_mode == (int)xuastc_ldr_deblocking_mode::cUseSCDAndFilteringOnlyLargestBlocks)) || + (m_params.m_xuastc_ldr_deblocking_mode == (int)xuastc_ldr_deblocking_mode::cUseSCDAndFilteringAllBlockSizes)) + { + cfg.m_scd_enabled = true; + + cfg.m_scd_will_postfilter = true; + } + else if (m_params.m_xuastc_ldr_deblocking_mode == (int)xuastc_ldr_deblocking_mode::cUseSCDNoFiltering) + { + cfg.m_scd_enabled = true; + } + else if ( (m_params.m_xuastc_ldr_deblocking_mode == (int)xuastc_ldr_deblocking_mode::cNoSCDButEnableFilteringOnAllBlocks) || + ((very_large_blocks_flag) && (m_params.m_xuastc_ldr_deblocking_mode == (int)xuastc_ldr_deblocking_mode::cNoSCDButEnableFilteringOnLargestBlocks)) ) + { + cfg.m_scd_will_postfilter = true; + } + + if (cfg.m_scd_will_postfilter) + { + // Write the key-value field with the deblocking filter ID (there's only one defined currently) + const uint32_t DEBLOCK_FILTER_ID = 1; + basist::add_key_value(m_params.m_key_values, BASISU_DEBLOCK_FILTER_ID_NAME, fmt_string("{}", DEBLOCK_FILTER_ID)); + } + + if (m_params.m_xuastc_ldr_num_deblocking_passes >= 256) + { + switch (cfg.m_effort_level) + { + case 0: + case 1: + cfg.m_num_scd_passes = 0; + break; + case 2: + cfg.m_num_scd_passes = 2; + break; + case 3: + cfg.m_num_scd_passes = 8; + break; + case 4: + cfg.m_num_scd_passes = 10; + break; + case 5: + cfg.m_num_scd_passes = 12; + break; + case 6: + cfg.m_num_scd_passes = 14; + break; + case 7: + cfg.m_num_scd_passes = 16; + break; + case 8: + case 9: + cfg.m_num_scd_passes = 20; + break; + case 10: + default: + cfg.m_num_scd_passes = 32; + break; + } + } + else if (cfg.m_scd_enabled) + { + cfg.m_num_scd_passes = m_params.m_xuastc_ldr_num_deblocking_passes; + } + + if (!cfg.m_num_scd_passes) + { + cfg.m_scd_enabled = false; + } + + cfg.m_try_simplified_latent_configs = m_params.m_xuastc_ldr_heavy_subset_usage; + + if (m_params.m_status_output) + { + fmt_printf("Prefiltering/blurring: {}, Sharpening: {} (Amount: {}), SCD/Deblocking Passes: {} (Factor in CPU/GPU Post-Filtering: {}), Heavy subset usage: {}, Compressor: {}\n", + cfg.m_block_blurring_p1 || cfg.m_block_blurring_p2, + cfg.m_sharpen_flag, cfg.m_sharpen_amount, cfg.m_scd_enabled, cfg.m_scd_will_postfilter, + cfg.m_try_simplified_latent_configs, + m_params.m_xuastc_ldr_astc_comp_selection); + } for (uint32_t slice_index = 0; slice_index < m_slice_descs.size(); slice_index++) { + if (m_params.m_status_output) + fmt_printf("Encoding slice {}\n", slice_index); + gpu_image& dst_tex = m_uastc_slice_textures[slice_index]; basisu_backend_slice_desc& slice_desc = m_slice_descs[slice_index]; @@ -1471,6 +1824,151 @@ namespace basisu return cECSuccess; } + basis_compressor::error_code basis_compressor::encode_slices_to_xubc7() + { + if (m_params.m_debug) + debug_printf("basis_compressor::encode_slices_to_xubc7\n"); + + m_uastc_slice_textures.resize(m_slice_descs.size()); + + const texture_format tex_fmt = texture_format::cBC7; + //const basist::transcoder_texture_format transcoder_tex_fmt = basist::transcoder_texture_format::cTFBC7_RGBA; + //BASISU_NOTE_UNUSED(transcoder_tex_fmt); + + //const uint32_t block_width = 4, block_height = 4; + + for (uint32_t slice_index = 0; slice_index < m_slice_descs.size(); slice_index++) + m_uastc_slice_textures[slice_index].init(tex_fmt, m_slice_descs[slice_index].m_orig_width, m_slice_descs[slice_index].m_orig_height); + + m_uastc_backend_output.m_tex_format = m_fmt_mode; + + m_uastc_backend_output.m_etc1s = false; + m_uastc_backend_output.m_srgb = m_params.m_ktx2_and_basis_srgb_transfer_function; + m_uastc_backend_output.m_slice_desc = m_slice_descs; + m_uastc_backend_output.m_slice_image_data.resize(m_slice_descs.size()); + m_uastc_backend_output.m_slice_image_crcs.resize(m_slice_descs.size()); + + xbc7::pack_options options; + options.m_dct_q = (m_params.m_quality_level < 0) ? 100 : clamp(m_params.m_quality_level, 1, 100); + options.set_rdo_level(m_params.m_xubc7_rdo_level); + + options.m_weights[0] = m_params.m_xuastc_ldr_channel_weights[0]; + options.m_weights[1] = m_params.m_xuastc_ldr_channel_weights[1]; + options.m_weights[2] = m_params.m_xuastc_ldr_channel_weights[2]; + options.m_weights[3] = m_params.m_xuastc_ldr_channel_weights[3]; + + if ((options.m_dct_q >= 90) && (m_params.m_xubc7_effort_level >= 3)) + { + options.m_optimize_weights_after_bc7f = true; + } + + if (m_params.m_xubc7_effort_level <= 1) + { + options.m_bc7_pack_flags = basist::bc7f::cPackBC7FlagDefault; + } + else if (m_params.m_xubc7_effort_level == 10) + { + options.m_bc7_pack_flags = basist::bc7f::cPackBC7FlagDefaultNonAnalytical; + } + + options.m_effort_level = clamp(m_params.m_xubc7_effort_level, 0, 10); + + options.m_bc7_encoder = (xbc7::bc7_encoder_type)(int)m_params.m_xubc7_encoder; + options.m_bc7e_scalar_level = clamp(m_params.m_xubc7_bc7e_scalar_level, xbc7::BC7E_SCALAR_MIN_LEVEL, xbc7::BC7E_SCALAR_MAX_LEVEL); + options.m_perceptual = m_params.m_perceptual; + + options.m_debug_output = m_params.m_debug; + options.m_print_stats = m_params.m_compute_stats && m_params.m_print_stats; + options.m_debug_images = m_params.m_debug_images; + + options.m_pJob_pool = m_params.m_pJob_pool; + + const uint32_t desired_num_stripes = m_params.m_xubc7_num_stripes; // [1,16]; set_num_stripes_for_image() re-clamps per image + + //options.m_debug_file_prefix = fmt_string("slice_{}_", slice_index); + + if (m_params.m_status_output) + { + fmt_printf("XUBC7 encoder: {} (bc7e_scalar level: {}), effort: {}, RDO level [0,100]: {}, Desired stripes [1,16]: {}\n", + (options.m_bc7_encoder == xbc7::bc7_encoder_type::cBC7E_Scalar) ? "bc7e_scalar" : "bc7f", options.m_bc7e_scalar_level, + m_params.m_xubc7_effort_level, m_params.m_xubc7_rdo_level, desired_num_stripes); + } + + for (uint32_t slice_index = 0; slice_index < m_slice_descs.size(); slice_index++) + { + if (m_params.m_status_output) + fmt_printf("Encoding slice {}\n", slice_index); + + gpu_image& dst_tex = m_uastc_slice_textures[slice_index]; + + basisu_backend_slice_desc& slice_desc = m_slice_descs[slice_index]; + (void)slice_desc; + + const image& slice_source_image = m_slice_images[slice_index]; + const image* pSource_image = &slice_source_image; + + image temp_image; + if ((slice_source_image.get_width() != slice_desc.m_orig_width) || (slice_source_image.get_height() != slice_desc.m_orig_height)) + { + // Copy to actual/original dimensions so PSNR statistics are calculated correctly. (There's no need to pad the image to multiples of the block dimensions.) + temp_image = slice_source_image; + temp_image.crop(slice_desc.m_orig_width, slice_desc.m_orig_height); + pSource_image = &temp_image; + } + + options.m_debug_file_prefix = fmt_string("slice_{}_", slice_index); + + options.set_num_stripes_for_image(*pSource_image, desired_num_stripes); + + if (m_params.m_debug) + fmt_debug_printf("----------------------------------------------------------------------------\n"); + + uint8_vec intermediate_tex_data; + vector2D coded_log_blocks; + + bool comp_status = xbc7::pack_image(*pSource_image, options, intermediate_tex_data, coded_log_blocks); + if (!comp_status) + return cECFailedEncodeUASTC; + + if (m_params.m_debug) + fmt_debug_printf("----------------------------------------------------------------------------\n"); + + const uint32_t num_blocks_x = dst_tex.get_blocks_x(); + const uint32_t num_blocks_y = dst_tex.get_blocks_y(); + + assert(coded_log_blocks.get_width() == num_blocks_x); + assert(coded_log_blocks.get_height() == num_blocks_y); + + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + const basist::bc7u::log_bc7_block& log_blk = coded_log_blocks(bx, by); + + bool pack_status = basist::bc7u::pack_bc7(log_blk, static_cast(dst_tex.get_block_ptr(bx, by))); + if (!pack_status) + { + error_printf("basis_compressor::encode_slices_to_xubc7: basist::bc7u::pack_bc7() failed!\n"); + return cECFailedEncodeUASTC; + } + + } // bx + } // by + + uint8_vec& dst_buf = m_uastc_backend_output.m_slice_image_data[slice_index]; + + assert(intermediate_tex_data.size_in_bytes()); + + dst_buf.resize(intermediate_tex_data.size_in_bytes()); + memcpy(&dst_buf[0], intermediate_tex_data.get_ptr(), intermediate_tex_data.size_in_bytes()); + + m_uastc_backend_output.m_slice_image_crcs[slice_index] = basist::crc16(dst_buf.get_ptr(), dst_buf.size_in_bytes(), 0); + + } // slice_index + + return cECSuccess; + } + basis_compressor::error_code basis_compressor::encode_slices_to_uastc_4x4_ldr() { debug_printf("basis_compressor::encode_slices_to_uastc_4x4_ldr\n"); @@ -1487,6 +1985,9 @@ namespace basisu for (uint32_t slice_index = 0; slice_index < m_slice_descs.size(); slice_index++) { + if (m_params.m_status_output) + fmt_printf("Encoding slice {}\n", slice_index); + gpu_image& tex = m_uastc_slice_textures[slice_index]; basisu_backend_slice_desc& slice_desc = m_slice_descs[slice_index]; (void)slice_desc; @@ -2710,8 +3211,8 @@ namespace basisu if ((m_fmt_mode == basist::basis_tex_format::cASTC_HDR_6x6) || (m_fmt_mode == basist::basis_tex_format::cUASTC_HDR_6x6_INTERMEDIATE)) return true; - // No need to extract blocks in XUASTC/ASTC LDR mode either. - if (basis_tex_format_is_xuastc_ldr(m_fmt_mode) || basis_tex_format_is_astc_ldr(m_fmt_mode)) + // No need to extract blocks in XUASTC/ASTC LDR/XUBC7 modes either. + if (basis_tex_format_is_xuastc_ldr(m_fmt_mode) || basis_tex_format_is_astc_ldr(m_fmt_mode) || basis_tex_format_is_xubc7(m_fmt_mode)) return true; if (m_params.m_hdr) @@ -2984,6 +3485,9 @@ namespace basisu for (uint32_t i = 0; i < m_slice_descs.size(); i++) { + if (m_params.m_status_output) + fmt_printf("Processing slice {}\n", i); + const basisu_backend_slice_desc &slice_desc = m_slice_descs[i]; const uint32_t num_blocks_x = slice_desc.m_num_blocks_x; @@ -3055,9 +3559,53 @@ namespace basisu { debug_printf("basis_compressor::create_basis_file_and_transcode\n"); + basist::key_value_vec key_values(m_params.m_key_values); + + if (find_key_value(key_values, BASISU_LIB_VERSION_KEY_NAME) != nullptr) + { + error_printf("basis_compressor::create_basis_file_and_transcode: Key already exists\n"); + return false; + } + + basist::add_key_value(key_values, BASISU_LIB_VERSION_KEY_NAME, fmt_string("{}", BASISU_LIB_VERSION_STRING)); + + if (m_params.m_hdr) + { + if (m_upconverted_any_ldr_images) + { + if (find_key_value(key_values, BASISU_LDR_UPCONVERSION_SCALE_KEY_NAME) == nullptr) + basist::add_key_value(key_values, BASISU_LDR_UPCONVERSION_SCALE_KEY_NAME, fmt_string("{g}", m_ldr_to_hdr_upconversion_nit_multiplier)); + + if (m_params.m_ldr_hdr_upconversion_srgb_to_linear) + { + if (find_key_value(key_values, BASISU_LDR_UPCONVERSION_SRGB_TO_LIN_KEY_NAME) == nullptr) + basist::add_key_value(key_values, BASISU_LDR_UPCONVERSION_SRGB_TO_LIN_KEY_NAME, "1"); + } + } + + if (find_key_value(key_values, BASISU_HDR_MAP_RANGE_KEY_NAME) == nullptr) + { + // add HDR map range key value + basist::basisu_map_range val; + val.m_scale = *reinterpret_cast(&m_hdr_image_scale); + val.m_offset = 0; + + auto* pNew_key = key_values.enlarge(1); + + const char* pKey_name = BASISU_HDR_MAP_RANGE_KEY_NAME; + size_t key_name_len = strlen(pKey_name) + 1; + + pNew_key->m_key.resize(key_name_len); + memcpy(pNew_key->m_key.data(), pKey_name, key_name_len); + + pNew_key->m_value.resize(sizeof(val)); + memcpy(pNew_key->m_value.data(), &val, sizeof(val)); + } + } + const basisu_backend_output& encoded_output = m_params.m_uastc ? m_uastc_backend_output : m_backend.get_output(); - if (!m_basis_file.init(encoded_output, m_params.m_tex_type, m_params.m_userdata0, m_params.m_userdata1, m_params.m_y_flip, m_params.m_us_per_frame)) + if (!m_basis_file.init(encoded_output, m_params.m_tex_type, m_params.m_userdata0, m_params.m_userdata1, m_params.m_y_flip, m_params.m_us_per_frame, key_values)) { error_printf("basis_compressor::create_basis_file_and_transcode: basisu_backend:init() failed!\n"); return false; @@ -3142,6 +3690,7 @@ namespace basisu basisu::texture_format tex_format; basist::block_format blk_format; + // Chose first transcode format if (m_params.m_hdr) { // HDR @@ -3162,6 +3711,12 @@ namespace basisu tex_format = basist::basis_get_texture_format_from_xuastc_or_astc_ldr_basis_tex_format(m_fmt_mode); blk_format = basist::xuastc_get_block_format(transcoder_fmt); } + else if (basis_tex_format_is_xubc7(m_fmt_mode)) + { + // Choose ASTC - we'll also transcode to BC7 too as the alternate. + tex_format = texture_format::cASTC_LDR_4x4; + blk_format = basist::block_format::cASTC_LDR_4x4; + } else { // ETC1S @@ -3169,6 +3724,9 @@ namespace basisu blk_format = basist::block_format::cETC1; } + if (m_params.m_print_stats) + fmt_printf("Transcoding to texture format: {}\n", get_texture_format_name(tex_format)); + for (uint32_t slice_iter = 0; slice_iter < m_slice_descs.size(); slice_iter++) { gpu_image decoded_texture; @@ -3216,6 +3774,7 @@ namespace basisu { if (is_hdr_6x6) { + // Transcode to ASTC HDR 6x6 assert(basist::basis_is_format_supported(basist::transcoder_texture_format::cTFASTC_HDR_6x6_RGBA, basist::basis_tex_format::cASTC_HDR_6x6)); assert(basist::basis_is_format_supported(basist::transcoder_texture_format::cTFASTC_HDR_6x6_RGBA, basist::basis_tex_format::cUASTC_HDR_6x6_INTERMEDIATE)); @@ -3236,6 +3795,7 @@ namespace basisu } else { + // Transcode to ASTC HDR 4x4 assert(basist::basis_is_format_supported(basist::transcoder_texture_format::cTFASTC_HDR_4x4_RGBA, basist::basis_tex_format::cUASTC_HDR_4x4)); for (uint32_t i = 0; i < m_slice_descs.size(); i++) @@ -3256,8 +3816,10 @@ namespace basisu } else { + // Transcode to BC7 if (basist::basis_is_format_supported(basist::transcoder_texture_format::cTFBC7_RGBA, basist::basis_tex_format::cUASTC_LDR_4x4) && - basist::basis_is_format_supported(basist::transcoder_texture_format::cTFBC7_RGBA, basist::basis_tex_format::cETC1S)) + basist::basis_is_format_supported(basist::transcoder_texture_format::cTFBC7_RGBA, basist::basis_tex_format::cETC1S) && + basist::basis_is_format_supported(basist::transcoder_texture_format::cTFBC7_RGBA, basist::basis_tex_format::cXUBC7)) { for (uint32_t i = 0; i < m_slice_descs.size(); i++) { @@ -3281,6 +3843,7 @@ namespace basisu total_alt_transcode_time = tm.get_elapsed_secs(); + // Unpack transcoded GPU textures to raw images for (uint32_t i = 0; i < m_slice_descs.size(); i++) { if (m_params.m_hdr) @@ -3478,6 +4041,9 @@ namespace basisu { image_stats& s = m_stats[slice_index]; + // The m_basis_* stats are computed against the transcoded ASTC HDR texture (same format printed below). + s.m_tex_format = m_decoded_output_textures_astc_hdr[slice_index].get_format(); + if (m_params.m_print_stats) { printf("Slice: %u\n", slice_index); @@ -3487,6 +4053,7 @@ namespace basisu if (m_params.m_print_stats) { + fmt_printf("Quality stats vs. transcoded {} (and {} alternate) textures:\n", get_texture_format_name(m_decoded_output_textures_astc_hdr[slice_index].get_format()), get_texture_format_name(m_decoded_output_textures[slice_index].get_format())); printf("\nASTC channels:\n"); for (uint32_t i = 0; i < 3; i++) { @@ -3532,7 +4099,7 @@ namespace basisu } im.calc(m_slice_images_hdr[slice_index], m_decoded_output_textures_astc_hdr_unpacked[slice_index], 0, 3, true, true); - s.m_basis_rgb_avg_log2_psnr = (float)im.m_psnr; + s.m_basis_rgb_avg_astc_hdr_log2_psnr = (float)im.m_psnr; if (m_params.m_print_stats) { @@ -3634,10 +4201,14 @@ namespace basisu printf("Slice: %u\n", slice_index); image_stats& s = m_stats[slice_index]; - + + // The m_basis_* stats are computed against the transcoded LDR texture (same format printed below). + s.m_tex_format = m_decoded_output_textures[slice_index].get_format(); + image_metrics em; - // ---- .basis stats + if (m_params.m_print_stats) + fmt_printf("Quality stats vs. transcoded {} texture:\n", get_texture_format_name(m_decoded_output_textures[slice_index].get_format())); em.calc(m_slice_images[slice_index], m_decoded_output_textures_unpacked[slice_index], 0, 3); if (m_params.m_print_stats) em.print("RGB Avg: "); @@ -3690,9 +4261,24 @@ namespace basisu } } + if (m_params.m_psnr_hvs_m_stats) + { + bool hvs_status = psnr_hvs_compute_metrics(m_slice_images[slice_index], m_decoded_output_textures_unpacked[slice_index], s.m_hvs_metrics); + if (!hvs_status) + error_printf("psnr_hvs_compute_metrics() failed!\n"); + + if (m_params.m_print_stats) + { + fmt_printf("PSNR-HVS and PSNR-HVS-M metrics:\n"); + psnr_hvs_print_metrics(s.m_hvs_metrics); + } + } + if (m_decoded_output_textures_unpacked_bc7[slice_index].get_width()) { // ---- BC7 stats + if (m_params.m_print_stats) + fmt_printf("Quality stats vs. transcoded {} texture:\n", get_texture_format_name(m_decoded_output_textures_bc7[slice_index].get_format())); em.calc(m_slice_images[slice_index], m_decoded_output_textures_unpacked_bc7[slice_index], 0, 3); if (m_params.m_print_stats) em.print("BC7 RGB Avg: "); @@ -3734,6 +4320,19 @@ namespace basisu if (m_params.m_print_stats) em.print("BC7 601 Luma: "); s.m_bc7_luma_601_psnr = static_cast(em.m_psnr); + + if (m_params.m_psnr_hvs_m_stats) + { + bool hvs_status2 = psnr_hvs_compute_metrics(m_slice_images[slice_index], m_decoded_output_textures_unpacked_bc7[slice_index], s.m_hvs_metrics_bc7); + if (!hvs_status2) + error_printf("psnr_hvs_compute_metrics() failed!\n"); + + if (m_params.m_print_stats) + { + fmt_printf("PSNR-HVS and PSNR-HVS-M metrics (BC7):\n"); + psnr_hvs_print_metrics(s.m_hvs_metrics_bc7); + } + } } if (!m_params.m_uastc) @@ -4015,6 +4614,23 @@ namespace basisu 0x0,0x0,0x0,0x0, // 9 sampleLower (0.0) 0xFF,0xFF,0xFF,0xFF // 10 sampleHigher (0xFF) }; + + // colorModel=KTX2_KDF_DF_MODEL_XUBC7 (0xAA) + // Custom supercompressed intermediate format, decodes directly to standard ASTC LDR 4x4-12x12. + static uint8_t g_ktx2_xubc7_dfd[44] = + { + 0x2C,0x0,0x0,0x0, // 0 totalSize + 0x0,0x0,0x0,0x0, // 1 descriptorType/vendorId + 0x2,0x0,0x28,0x0, // 2 descriptorBlockSize/versionNumber + (uint8_t)basist::KTX2_KDF_DF_MODEL_XUBC7,0x1,0x1,0x0, // 3 flags, transferFunction, colorPrimaries, colorModel + 0x3,0x3,0x0,0x0, // 4 texelBlockDimension0-texelBlockDimension3 + 0x10,0x0,0x0,0x0, // 5 bytesPlane0-bytesPlane3 + 0x0,0x0,0x0,0x0, // 6 bytesPlane4-bytesPlane7 + 0x0,0x0,0x7F,0x00, // 7 bitOffset/bitLength/channelType and Qualifer flags (KHR_DF_SAMPLE_DATATYPE_FLOAT etc.) + 0x0,0x0,0x0,0x0, // 8 samplePosition0-samplePosition3 + 0x0,0x0,0x0,0x0, // 9 sampleLower (0) + 0xFF,0xFF,0xFF,0xFF // 10 sampleHigher (0xFF) + }; bool basis_compressor::get_dfd(uint8_vec &dfd, const basist::ktx2_header &header) { @@ -4025,6 +4641,7 @@ namespace basisu const bool is_xuastc_ldr = basis_tex_format_is_xuastc_ldr(m_fmt_mode); const bool is_astc_ldr = basis_tex_format_is_astc_ldr(m_fmt_mode); + const bool is_xubc7 = basis_tex_format_is_xubc7(m_fmt_mode); // TODO: This was writen before m_fmt_mode existed, refactor to use that exclusively instead. @@ -4044,6 +4661,8 @@ namespace basisu { if (m_params.m_hdr) { + assert(!is_xubc7); + switch (m_params.m_hdr_mode) { case hdr_modes::cUASTC_HDR_4X4: @@ -4077,7 +4696,13 @@ namespace basisu } } } - // Must be LDR UASTC 4x4 + else if (is_xubc7) + { + // XUBC7 + pDFD = g_ktx2_xubc7_dfd; + dfd_len = sizeof(g_ktx2_xubc7_dfd); + } + // Fallback: Must be LDR UASTC 4x4 else if (m_any_source_image_has_alpha) { assert(m_fmt_mode == basist::basis_tex_format::cUASTC_LDR_4x4); @@ -4098,7 +4723,7 @@ namespace basisu // Must be ETC1S. assert(!m_params.m_hdr); assert(m_fmt_mode == basist::basis_tex_format::cETC1S); - + if (m_any_source_image_has_alpha) { pDFD = g_ktx2_etc1s_alpha_dfd; @@ -4174,7 +4799,7 @@ namespace basisu // TODO: Allow the caller to override this. Derive from swizzle? // Only do this for UASTC LDR 4x4 or XUASTC LDR 4x4-12x12 - and now also ASTC LDR 4x4-12x12, which isn't quite standard, but we need some way of determining if the ASTC data has alpha by examining the KTX2 DFD. if ((m_any_source_image_has_alpha) && - ((m_fmt_mode == basist::basis_tex_format::cUASTC_LDR_4x4) || basist::basis_tex_format_is_xuastc_ldr(m_fmt_mode) || basis_tex_format_is_astc_ldr(m_fmt_mode))) + ((m_fmt_mode == basist::basis_tex_format::cUASTC_LDR_4x4) || basis_tex_format_is_xuastc_ldr(m_fmt_mode) || basis_tex_format_is_astc_ldr(m_fmt_mode) || basis_tex_format_is_xubc7(m_fmt_mode))) { dfd_chan0 |= (basist::KTX2_DF_CHANNEL_UASTC_RGBA << 24); } @@ -4205,9 +4830,11 @@ namespace basisu { //bool needs_global_data = false; bool can_use_zstd = false; + bool is_xuastc_ldr = false; bool is_astc_ldr = false; bool is_hdr_6x6i = false; + bool is_xubc7 = false; switch (m_fmt_mode) { @@ -4252,7 +4879,7 @@ namespace basisu case basist::basis_tex_format::cXUASTC_LDR_12x10: case basist::basis_tex_format::cXUASTC_LDR_12x12: { - // has built-in compression, no need for Zstd + // has built-in compression, no need for Zstd, global supercompressed data has seek tables is_xuastc_ldr = true; break; } @@ -4276,6 +4903,12 @@ namespace basisu can_use_zstd = true; break; } + case basist::basis_tex_format::cXUBC7: + { + // has built-in compression, global supercompressed data has seek tables + is_xubc7 = true; + break; + } default: assert(0); //fmt_debug_printf("HERE 1\n"); @@ -4344,7 +4977,7 @@ namespace basisu else { // Either ETC1S, UASTC LDR 4x4, or XUASTC/ASTC LDR 4x4-12x12. - assert((m_fmt_mode == basist::basis_tex_format::cETC1S) || (m_fmt_mode == basist::basis_tex_format::cUASTC_LDR_4x4) || is_xuastc_ldr || is_astc_ldr); + assert((m_fmt_mode == basist::basis_tex_format::cETC1S) || (m_fmt_mode == basist::basis_tex_format::cUASTC_LDR_4x4) || is_xuastc_ldr || is_astc_ldr || is_xubc7); if (is_astc_ldr) { @@ -4523,9 +5156,9 @@ namespace basisu header.m_supercompression_scheme = basist::KTX2_SS_BASISLZ; } - else if ((is_hdr_6x6i) || (is_xuastc_ldr)) + else if ((is_hdr_6x6i) || (is_xuastc_ldr) || (is_xubc7)) { - // The global data for UASTC HDR 6x6 INTERMEDIATE and XUASTC LDR is an array of ktx2_slice_offset_len_desc_std's, which the transcoder needs to locate the variable length compressed slice data. + // The global data for UASTC HDR 6x6 INTERMEDIATE/XUASTC LDR/XUBC7 is an array of ktx2_slice_offset_len_desc_std's, which the transcoder needs to locate the variable length compressed slice data. // Note: The original v2.0 release used ktx2_slice_offset_len_desc_orig's basisu::vector slice_offset_len_descs(total_levels * total_layers * total_faces); memset((void *)slice_offset_len_descs.data(), 0, slice_offset_len_descs.size_in_bytes()); @@ -4560,15 +5193,27 @@ namespace basisu profile = m_uastc_backend_output.m_slice_image_data[slice_index][0] | (m_uastc_backend_output.m_slice_image_data[slice_index][1] << 8); } } + else if (is_xubc7) + { + assert(m_uastc_backend_output.m_slice_image_data[slice_index].size() >= 1); + + if (m_uastc_backend_output.m_slice_image_data[slice_index].size() >= 1) + { + // First 8-bits is the marker/profile version + // TODO high byte is the XUBC7 codec variant index, currently hardcoded to 1 until we have an internal query/introspection API for this + profile = m_uastc_backend_output.m_slice_image_data[slice_index][0] | (0x01 << 8); + } + } else { - assert(is_xuastc_ldr); + assert(is_xuastc_ldr ); assert(m_uastc_backend_output.m_slice_image_data[slice_index].size() >= 1); if (m_uastc_backend_output.m_slice_image_data[slice_index].size() >= 1) { // First byte is always the profile index (Zstd, hybrid, arithmetic etc.) - profile = m_uastc_backend_output.m_slice_image_data[slice_index][0] | (0x01 << 8); // TODO high byte is the XUASTC LDR codec variant index, currently hardcoded to 1 until we have an internal query/introspection API for this + // TODO high byte is the XUASTC LDR codec variant index, currently hardcoded to 1 until we have an internal query/introspection API for this + profile = m_uastc_backend_output.m_slice_image_data[slice_index][0] | (0x01 << 8); } } @@ -4581,22 +5226,22 @@ namespace basisu // Note v2.0 would always write BASISLZ for the supercompression scheme. KTX-Software changes this, and we need to be compatible. //header.m_supercompression_scheme = basist::KTX2_SS_BASISLZ; - header.m_supercompression_scheme = is_hdr_6x6i ? basist::KTX2_SS_UASTC_HDR_6x6I : basist::KTX2_SS_XUASTC_LDR; + header.m_supercompression_scheme = is_hdr_6x6i ? basist::KTX2_SS_UASTC_HDR_6x6I : (is_xubc7 ? basist::KTX2_SS_XUBC7 : basist::KTX2_SS_XUASTC_LDR); } // Key values - basist::ktx2_transcoder::key_value_vec key_values(m_params.m_ktx2_key_values); + basist::key_value_vec key_values(m_params.m_key_values); - basist::ktx2_add_key_value(key_values, "KTXwriter", fmt_string("Basis Universal {}", BASISU_LIB_VERSION_STRING)); + basist::add_key_value(key_values, "KTXwriter", fmt_string("Basis Universal {}", BASISU_LIB_VERSION_STRING)); if (m_params.m_hdr) { if (m_upconverted_any_ldr_images) { - basist::ktx2_add_key_value(key_values, "LDRUpconversionMultiplier", fmt_string("{}", m_ldr_to_hdr_upconversion_nit_multiplier)); + basist::add_key_value(key_values, "LDRUpconversionMultiplier", fmt_string("{}", m_ldr_to_hdr_upconversion_nit_multiplier)); if (m_params.m_ldr_hdr_upconversion_srgb_to_linear) - basist::ktx2_add_key_value(key_values, "LDRUpconversionSRGBToLinear", "1"); + basist::add_key_value(key_values, "LDRUpconversionSRGBToLinear", "1"); } // Always write the scale to simplify testing. @@ -5052,8 +5697,8 @@ namespace basisu comp_params.m_write_output_basis_or_ktx2_files = false; - // sRGB handling - set parameters consistently - // sRGB here controls the error metrics, KTX2/.basis transfer function fields, and mipmap filtering + // sRGB handling - set parameters consistently. "sRGB" here also applies perceptual metrics, or the opposite of linear RGB(A) metrics. + // sRGB here controls the error metrics, KTX2/.basis transfer function fields, mipmap filtering, and ASTC/XUASTC channel weights. const bool srgb_flag = (flags_and_quality & cFlagSRGB) != 0; // Use sRGB colorspace metrics, channel weights @@ -5065,6 +5710,9 @@ namespace basisu // Correct for sRGB transfer function during mipmapping comp_params.m_mip_srgb = srgb_flag; + // Set linear or Rec 709 weights (we assume Rec 709 weights if content is sRGB) + comp_params.set_xuastc_ldr_srgb_channel_weights(srgb_flag); + comp_params.m_mip_gen = (flags_and_quality & (cFlagGenMipsWrap | cFlagGenMipsClamp)) != 0; comp_params.m_mip_wrapping = (flags_and_quality & cFlagGenMipsWrap) != 0; @@ -5096,6 +5744,7 @@ namespace basisu // Valid XUASTC LDR weight grid DCT quality levels are 1-100. if (basist::basis_tex_format_is_xuastc_ldr(mode) && (uastc_rdo_or_dct_quality != 0.0f)) { + // [1,99]=DCT, 100=no DCT if ((uastc_rdo_or_dct_quality >= (float)BASISU_XUASTC_QUALITY_MIN) && (uastc_rdo_or_dct_quality <= (float)BASISU_XUASTC_QUALITY_MAX)) { if (uastc_rdo_or_dct_quality < (float)BASISU_XUASTC_QUALITY_MAX) @@ -5103,7 +5752,7 @@ namespace basisu // Enable weight grid DCT usage, set quality level. comp_params.m_xuastc_ldr_use_dct = true; comp_params.m_quality_level = (int)uastc_rdo_or_dct_quality; - + // Also enable bounded lossy distortion mode in the normally lossless supercompressor for extra savings. comp_params.m_xuastc_ldr_use_lossy_supercompression = true; } @@ -5127,6 +5776,30 @@ namespace basisu } } } + else if (basist::basis_tex_format_is_xubc7(mode)) + { + // XUBC7: configure effort and quality level + comp_params.m_xubc7_effort_level = clamp(flags_and_quality & 255, 0, 10); + + if (uastc_rdo_or_dct_quality != 0.0f) + { + int quality = clamp((int)std::round(uastc_rdo_or_dct_quality), 1, 100); + comp_params.m_quality_level = quality; + } + + // BC7 base encoder selection (packed 3-bit field in the high flag bits): + // 0 = bc7f (the default fast packer); 1-7 = bc7e_scalar at level 0-6. + const uint32_t xubc7_base_enc = (flags_and_quality >> cFlagXUBC7BaseEncoderShift) & cFlagXUBC7BaseEncoderMask; + if (xubc7_base_enc == 0) + { + comp_params.m_xubc7_encoder = (int)xbc7::bc7_encoder_type::cBC7F; + } + else + { + comp_params.m_xubc7_encoder = (int)xbc7::bc7_encoder_type::cBC7E_Scalar; + comp_params.m_xubc7_bc7e_scalar_level = clamp((int)xubc7_base_enc - 1, (int)xbc7::BC7E_SCALAR_MIN_LEVEL, (int)xbc7::BC7E_SCALAR_MAX_LEVEL); + } + } comp_params.m_create_ktx2_file = (flags_and_quality & cFlagKTX2) != 0; @@ -5138,6 +5811,9 @@ namespace basisu } comp_params.m_compute_stats = (pStats != nullptr); + if (comp_params.m_compute_stats) + comp_params.m_psnr_hvs_m_stats = true; // TODO: should put this behind a flag, it's not very fast + comp_params.m_print_stats = (flags_and_quality & cFlagPrintStats) != 0; comp_params.m_status_output = (flags_and_quality & cFlagPrintStatus) != 0; @@ -5160,6 +5836,16 @@ namespace basisu comp_params.m_astc_hdr_6x6_options.m_rec2020_bt2100_color_gamut = (flags_and_quality & cFlagREC2020) != 0; comp_params.m_validate_output_data = (flags_and_quality & cFlagValidateOutput) != 0; + + // The defaults are to use deblocking/sharpening on larger block sizes, unless the user disables them. + // More finer grain control could be useful but would need more flags/config. + if (flags_and_quality & cFlagDisableDeblocking) + comp_params.m_xuastc_ldr_deblocking_mode = (int)xuastc_ldr_deblocking_mode::cDisabled; + else if (flags_and_quality & cFlagForceDeblocking) + comp_params.m_xuastc_ldr_deblocking_mode = (int)xuastc_ldr_deblocking_mode::cUseSCDAndFilteringAllBlockSizes; + + //if (flags_and_quality & cFlagDisableSharpening) + // comp_params.m_xuastc_ldr_sharpen_mode = (int)xuastc_ldr_sharpen_mode::cDisabled; // Now set the unified quality/effort level, if they've specified it. // This will override some of the lower-level options set above, or leave them alone if -1. @@ -5167,7 +5853,7 @@ namespace basisu { comp_params.set_format_mode_and_quality_effort(mode, quality_level, effort_level, false); } - + // Create the compressor, initialize it, and process the input basis_compressor comp; if (!comp.init(comp_params)) diff --git a/encoder/basisu_comp.h b/encoder/basisu_comp.h index fd0de37..221dd73 100644 --- a/encoder/basisu_comp.h +++ b/encoder/basisu_comp.h @@ -21,9 +21,10 @@ #include "basisu_uastc_hdr_4x4_enc.h" #include "basisu_astc_hdr_6x6_enc.h" #include "basisu_astc_ldr_encode.h" +#include "basisu_xbc7_encode.h" -#define BASISU_LIB_VERSION 210 -#define BASISU_LIB_VERSION_STRING "2.10" +#define BASISU_LIB_VERSION 250 +#define BASISU_LIB_VERSION_STRING "2.50" #ifndef BASISD_SUPPORT_KTX2 #error BASISD_SUPPORT_KTX2 is undefined @@ -69,6 +70,8 @@ namespace basisu const int BASISU_RDO_UASTC_DICT_SIZE_MIN = 64; const int BASISU_RDO_UASTC_DICT_SIZE_MAX = 65536; + const float BASISU_XUASTC_LDR_DEFAULT_SHARPEN_AMOUNT = 1.1f; + struct image_stats { image_stats() @@ -81,16 +84,17 @@ namespace basisu m_filename.clear(); m_width = 0; m_height = 0; - - m_basis_rgb_avg_psnr = 0.0f; - m_basis_rgb_avg_log2_psnr = 0.0f; + m_tex_format = texture_format::cInvalidTextureFormat; + + m_basis_rgb_avg_psnr = 0.0f; m_basis_rgba_avg_psnr = 0.0f; m_basis_a_avg_psnr = 0.0f; m_basis_luma_709_psnr = 0.0f; m_basis_luma_601_psnr = 0.0f; m_basis_luma_709_ssim = 0.0f; + m_basis_rgb_avg_astc_hdr_log2_psnr = 0.0f; m_basis_rgb_avg_bc6h_psnr = 0.0f; m_basis_rgb_avg_bc6h_log2_psnr = 0.0f; @@ -106,6 +110,9 @@ namespace basisu m_best_etc1s_luma_601_psnr = 0.0f; m_best_etc1s_luma_709_ssim = 0.0f; + m_hvs_metrics.clear(); + m_hvs_metrics_bc7.clear(); + m_opencl_failed = false; } @@ -113,34 +120,46 @@ namespace basisu uint32_t m_width; uint32_t m_height; - // .basis/.ktx2 compressed (LDR: ETC1S or UASTC statistics, HDR: transcoded BC6H statistics) - float m_basis_rgb_avg_psnr; - float m_basis_rgb_avg_log2_psnr; + // .basis/.ktx2 metrics + + // The texture_format the m_basis_* stats below were computed against: the transcoded ASTC HDR format for HDR, + // or the transcoded ETC1S/UASTC/ASTC LDR format for LDR. (For XUBC7 this is the ASTC LDR transcode format, not + // native BC7 - see the m_bc7_* stats for native BC7.) cInvalidTextureFormat if stats weren't computed. + texture_format m_tex_format; - float m_basis_rgba_avg_psnr; - float m_basis_a_avg_psnr; - float m_basis_luma_709_psnr; - float m_basis_luma_601_psnr; - float m_basis_luma_709_ssim; + // LDR formats: Transcoded ETC1S, UASTC LDR 4x4, or ASTC LDR 4x4-12x12 statistics + // HDR formats: Transcoded ASTC HDR statistics + // All ASTC based - so for XUBC7, these are transcoded statistics (not native BC7, see below) + float m_basis_rgb_avg_psnr; // LDR/HDR + float m_basis_rgba_avg_psnr; // not valid in HDR + float m_basis_a_avg_psnr; // not valid in HDR + float m_basis_luma_709_psnr; // not valid in HDR + float m_basis_luma_601_psnr; // not valid in HDR + float m_basis_luma_709_ssim; // not valid in HDR + + // HDR formats only: Transcoded ASTC HDR and BC6H statistics + float m_basis_rgb_avg_astc_hdr_log2_psnr; + float m_basis_rgb_avg_bc6h_psnr; + float m_basis_rgb_avg_bc6h_log2_psnr; - // UASTC HDR only. - float m_basis_rgb_avg_bc6h_psnr; - float m_basis_rgb_avg_bc6h_log2_psnr; - - // LDR: BC7 statistics + // LDR formats: Transcoded BC7 statistics (native BC7 for XUBC7) float m_bc7_rgb_avg_psnr; float m_bc7_rgba_avg_psnr; float m_bc7_a_avg_psnr; float m_bc7_luma_709_psnr; float m_bc7_luma_601_psnr; float m_bc7_luma_709_ssim; + + psnr_hvs_metrics m_hvs_metrics; + psnr_hvs_metrics m_hvs_metrics_bc7; - // LDR: Highest achievable quality ETC1S statistics, for development/comparison + // ETC1S only: Highest achievable quality ETC1S statistics, for development float m_best_etc1s_rgb_avg_psnr; float m_best_etc1s_luma_709_psnr; float m_best_etc1s_luma_601_psnr; float m_best_etc1s_luma_709_ssim; + // true if OpenCL failed during compression bool m_opencl_failed; }; @@ -155,6 +174,44 @@ namespace basisu cTotal }; + enum class xuastc_ldr_sharpen_mode + { + cDisabled = 0, // never sharpen mipmap levels before compression + cOnlyLargestBlocks = 1, // only sharpen 10x8 or larger block sizes + cAllBlockSizes = 2, // sharpen all block sizes + + cTotal + }; + + enum class xuastc_ldr_deblocking_mode + { + cDisabled = 0, // no SCD, no deblocking filter, write no deblocking filter ID to ktx2/.basis file + + cUseSCDAndFilteringOnlyLargestBlocks = 1, // SCD+deblocking filter on 10x8 or larger block sizes (default) + + cUseSCDAndFilteringAllBlockSizes = 2, // SCD+deblocking filter on all block sizes (not recommended, will overblur smaller blocks) + + cUseSCDNoFiltering = 3, // SCD enabled, but no filtering, all block sizes + + cNoSCDButEnableFilteringOnLargestBlocks = 4, // SCD disabled, but filtering enabled on larger block sizes, write a deblocking filter ID to ktx2/.basis file + + cNoSCDButEnableFilteringOnAllBlocks = 5, // SCD disabled, but filtering enabled on all block sizes, write a deblocking filter ID to ktx2/.basis file + + cTotal + }; + + enum class xuastc_ldr_astc_comp_selection + { + cAuto = 0, // automatically select between cASTCF or merging cASTCF with cBasisU depending on the effort level + cBasisU, // always available + cASTCENC, // only if compiled in + cASTCF, // new ASTC encoder + cBasisU_and_ASTCENC, // merge basisu+ASTCENC when enabled + cBasisU_and_ASTCF, // merge basisu+astcf when possible/enabled + cUseAll, // merge all available encoders whenever possible/enabled + cTotal + }; + template struct bool_param { @@ -240,6 +297,8 @@ namespace basisu // Also see basis_compress() below for a simplified C-style interface. struct basis_compressor_params { + friend class basis_compressor; + basis_compressor_params() : m_xuastc_or_astc_ldr_basis_tex_format(-1, -1, INT_MAX), // Note the ETC1S default compression/effort level is 2, not the command line default of 1. @@ -268,9 +327,20 @@ namespace basisu m_ldr_hdr_upconversion_black_bias(0.0f, 0.0f, 1.0f), m_xuastc_ldr_effort_level(astc_ldr::EFFORT_LEVEL_DEF, astc_ldr::EFFORT_LEVEL_MIN, astc_ldr::EFFORT_LEVEL_MAX), m_xuastc_ldr_syntax((int)basist::astc_ldr_t::xuastc_ldr_syntax::cFullZStd, (int)basist::astc_ldr_t::xuastc_ldr_syntax::cFullArith, (int)basist::astc_ldr_t::xuastc_ldr_syntax::cFullZStd), + m_xuastc_ldr_deblocking_mode((int)xuastc_ldr_deblocking_mode::cUseSCDAndFilteringOnlyLargestBlocks, (int)xuastc_ldr_deblocking_mode::cDisabled, (int)xuastc_ldr_deblocking_mode::cTotal - 1), + m_xuastc_ldr_num_deblocking_passes(256, 2, 256), // 256=automatic depending on XUASTC LDR effort level + m_xuastc_ldr_sharpen_mode((int)xuastc_ldr_sharpen_mode::cDisabled, (int)xuastc_ldr_sharpen_mode::cDisabled, (int)xuastc_ldr_sharpen_mode::cAllBlockSizes), + m_xuastc_ldr_sharpen_amount(BASISU_XUASTC_LDR_DEFAULT_SHARPEN_AMOUNT, 0.0f, 10.0f), m_ls_min_psnr(35.0f, 0.0f, 100.0f), m_ls_min_alpha_psnr(38.0f, 0.0f, 100.0f), m_ls_thresh_psnr(1.5f, 0.0f, 100.0f), m_ls_thresh_alpha_psnr(0.75f, 0.0f, 100.0f), m_ls_thresh_edge_psnr(1.0f, 0.0f, 100.00f), m_ls_thresh_edge_alpha_psnr(0.5f, 0.0f, 100.00f), + m_xuastc_ldr_debug_block_x(-1, INT_MIN, INT_MAX), m_xuastc_ldr_debug_block_y(-1, INT_MIN, INT_MAX), + m_xuastc_ldr_astc_comp_selection((int)xuastc_ldr_astc_comp_selection::cAuto, (int)xuastc_ldr_astc_comp_selection::cAuto, (int)xuastc_ldr_astc_comp_selection::cTotal - 1), + m_xubc7_effort_level(xbc7::DEFAULT_EFFORT_LEVEL, 0, 10), + m_xubc7_rdo_level(0, 0, 100), + m_xubc7_num_stripes(8, 1, 16), + m_xubc7_encoder((int)xbc7::bc7_encoder_type::cBC7F, (int)xbc7::bc7_encoder_type::cBC7F, (int)xbc7::bc7_encoder_type::cBC7E_Scalar), + m_xubc7_bc7e_scalar_level(xbc7::DEFAULT_BC7E_SCALAR_LEVEL, xbc7::BC7E_SCALAR_MIN_LEVEL, xbc7::BC7E_SCALAR_MAX_LEVEL), m_pJob_pool(nullptr) { clear(); @@ -307,6 +377,7 @@ namespace basisu m_write_output_basis_or_ktx2_files.clear(); m_etc1s_compression_level.clear(); m_compute_stats.clear(); + m_psnr_hvs_m_stats.clear(); m_print_stats.clear(); m_check_for_alpha.clear(); m_force_alpha.clear(); @@ -359,7 +430,7 @@ namespace basisu m_create_ktx2_file.clear(); m_ktx2_uastc_supercompression = basist::KTX2_SS_NONE; - m_ktx2_key_values.clear(); + m_key_values.clear(); m_ktx2_zstd_supercompression_level.clear(); m_ktx2_and_basis_srgb_transfer_function.clear(); @@ -389,15 +460,33 @@ namespace basisu m_ls_thresh_alpha_psnr.clear(); m_ls_thresh_edge_psnr.clear(); m_ls_thresh_edge_alpha_psnr.clear(); - for (uint32_t i = 0; i < 4; i++) - m_xuastc_ldr_channel_weights[i] = 1; + + m_xuastc_ldr_debug_block_x.clear(); + m_xuastc_ldr_debug_block_y.clear(); + + // 4/26/2026: The default ASTC/XUASTC LDR channel weights are now 9,11,1 - this looks a lot better for photos vs. 1,1,1. + set_xuastc_ldr_srgb_channel_weights(true); + m_xuastc_ldr_blurring.clear(); - + m_xuastc_ldr_astc_comp_selection.clear(); + m_xuastc_ldr_sharpen_mode.clear(); + m_xuastc_ldr_sharpen_amount.clear(); + + m_xuastc_ldr_deblocking_mode.clear(); + m_xuastc_ldr_num_deblocking_passes.clear(); + m_xuastc_ldr_heavy_subset_usage.clear(); + + m_xubc7_effort_level.clear(); + m_xubc7_rdo_level.clear(); + m_xubc7_num_stripes.clear(); + m_xubc7_encoder.clear(); + m_xubc7_bc7e_scalar_level.clear(); + m_pJob_pool = nullptr; } // Configures the compressor's mode by setting the proper parameters (which were preserved for backwards compatibility with old code). - // This is by far the preferred way of controlling which codec mode the compressor will select. + // This is the preferred way of controlling which codec mode the compressor will select. void set_format_mode(basist::basis_tex_format mode) { m_format_mode = mode; @@ -485,6 +574,15 @@ namespace basisu m_hdr_mode = hdr_modes::cUASTC_HDR_4X4; // doesn't matter break; } + case basist::basis_tex_format::cXUBC7: + { + // XUBC7 + m_xuastc_or_astc_ldr_basis_tex_format = -1; + m_hdr = false; + m_uastc = true; + m_hdr_mode = hdr_modes::cUASTC_HDR_4X4; // doesn't matter + break; + } default: assert(0); break; @@ -498,6 +596,8 @@ namespace basisu bool set_format_mode_and_quality_effort(basist::basis_tex_format mode, int quality = -1, int effort = -1, bool set_defaults = true); // Sets all the sRGB-related options (m_perceptual, m_mip_srgb, m_ktx2_and_basis_srgb_transfer_function) to the specified value. + // If m_perceptual is true, the encoder assumes the input is sRGB photographic-like data and optimizes for perceptual quality. If false, the encoder assumes the input is linear data and optimizes for PSNR. + // For ASTC/XUASTC LDR, also see the channel weights below: m_xuastc_ldr_channel_weights. They default to 9,11,1,11. void set_srgb_options(bool srgb_flag) { m_perceptual = srgb_flag; @@ -520,10 +620,16 @@ namespace basisu { return m_uastc && m_hdr && (m_hdr_mode == hdr_modes::cUASTC_HDR_4X4); } - + + bool is_xubc7() const + { + return m_uastc && (m_format_mode == basist::basis_tex_format::cXUBC7); + } + // By default we generate LDR ETC1S data. // Ideally call set_format_mode() above instead of directly manipulating the below fields. These individual parameters are for backwards API compatibility. // - If m_uastc is false you get ETC1S (the default). + // - If m_format_mode==cXUBC7, we generate XUBC7 data (lossy or lossless supercompressed 8bpp) // - If m_uastc is true, and m_hdr is not true, and m_xuastc_or_astc_ldr_basis_tex_format==-1, we generate UASTC 4x4 LDR data (8bpp with or without RDO). // - If m_uastc is true, and m_hdr is not true, and m_xuastc_or_astc_ldr_basis_tex_format!=-1, we generate XUASTC 4x4-12x12 or ASTC 4x4-12x12 LDR data, controlled by m_xuastc_or_astc_ldr_basis_tex_format. // - If m_uastc is true and m_hdr is true, we generate 4x4 or 6x6 HDR data, controlled by m_hdr_mode. @@ -596,7 +702,7 @@ namespace basisu // Use perceptual sRGB colorspace metrics instead of linear. // Note: You probably also want to set m_ktx2_srgb_transfer_func to match. - // Note: This member variable was previously called "m_perceptual". + // Prefer calling set_srgb_options() instead, so all the sRGB-related/perceptual encoding related options are set together and consistently. bool_param m_perceptual; // Disable selector RDO, for faster compression but larger files @@ -614,11 +720,12 @@ namespace basisu // Compute and display image metrics bool_param m_compute_stats; + bool_param m_psnr_hvs_m_stats; // Print stats to stdout, if m_compute_stats is true. bool_param m_print_stats; - // Check to see if any input image has an alpha channel, if so then the output basis/ktx2 file will have alpha channels + // Check to see if any input image has an alpha channel, if so then the output basis/ktx2 file will have alpha channels. If false: all image source alpha is slammed to 255. bool_param m_check_for_alpha; // Always put alpha slices in the output basis/ktx2 file, even when the input doesn't have alpha @@ -642,8 +749,8 @@ namespace basisu bool_param m_mip_gen; param m_mip_scale; std::string m_mip_filter; - bool_param m_mip_srgb; - bool_param m_mip_premultiplied; // not currently supported + bool_param m_mip_srgb; // 4/26/2026: defaulting this to true to match the defaults for m_perceptual and m_ktx2_and_basis_srgb_transfer_function + bool_param m_mip_premultiplied; // not currently supported bool_param m_mip_renormalize; bool_param m_mip_wrapping; bool_param m_mip_fast; @@ -658,6 +765,7 @@ namespace basisu // Quality level (bitrate vs. distortion tradeoff) control for ETC1S or XUASTC LDR 4x4-12x12. // ETC1S: Must set to [1,255] or [BASISU_QUALITY_MIN, BASISU_QUALITY_MAX] to control quality vs. bitrate. If -1 (the default!), quality is controlled by m_etc1s_max_endpoint_clusters and m_etc1s_max_selector_clusters directly. // XUASTC LDR: Must not be -1 for DCT. + // XUBC7: -1 or 100=no DCT, [1,99]=DCT int m_quality_level; // m_tex_type, m_userdata0, m_userdata1, m_framerate - These fields go directly into the .basis file header. @@ -687,11 +795,13 @@ namespace basisu // ETC1S global codebook control const basist::basisu_lowlevel_etc1s_transcoder *m_pGlobal_codebooks; + // .basis or .KTX2 key value fields. + basist::key_value_vec m_key_values; + // KTX2 specific parameters. // Internally, the compressor always creates a .basis file then it converts that losslessly to KTX2. bool_param m_create_ktx2_file; basist::ktx2_supercompression m_ktx2_uastc_supercompression; - basist::ktx2_transcoder::key_value_vec m_ktx2_key_values; param m_ktx2_zstd_supercompression_level; // Note: The default for this parameter (which used to be "m_ktx2_srgb_transfer_func") used to be false, now setting this to true and renaming to m_ktx2_and_basis_srgb_transfer_function. @@ -731,20 +841,88 @@ namespace basisu bool_param m_hdr_favor_astc; // XUASTC LDR 4x4-12x12 specific options + + // XUASTC LDR specific effort level. Prefer calling set_format_mode_and_effort() or set_format_mode_and_quality_effort() instead of setting this directly. param m_xuastc_ldr_effort_level; - bool_param m_xuastc_ldr_use_dct; // set the DCT quality above using m_quality_level, [1,100] - bool_param m_xuastc_ldr_use_lossy_supercompression; // allows the compressor to introduce a bounded amount of distortion if doing so would make smaller files (actually ASTC or XUASTC) - bool_param m_xuastc_ldr_force_disable_subsets; // disable 2-3 subset usage in all effort levels, faster encoding, faster transcoding to BC7, but lower quality) - bool_param m_xuastc_ldr_force_disable_rgb_dual_plane; // disable RGB dual plane usage (still can use dual plane on alpha blocks), for faster transcoding to BC7 but lower quality - param m_xuastc_ldr_syntax; // favor faster decompression over ratio, default is basist::astc_ldr_t::xuastc_ldr_syntax::cFullZstd (fastest transcoding but lower ratio) - uint32_t m_xuastc_ldr_channel_weights[4]; - bool_param m_xuastc_ldr_blurring; // experimental, not recommended, very slow + + // Enable or disable Weight Grid DCT. Set the DCT quality above using m_quality_level: [1,100] + // Prefer calling set_format_mode_and_effort() or set_format_mode_and_quality_effort() instead of setting this directly. + bool_param m_xuastc_ldr_use_dct; + + // Allow the compressor to introduce a bounded amount of distortion if doing so would make smaller files (actually ASTC or XUASTC). + // Prefer calling set_format_mode_and_effort() or set_format_mode_and_quality_effort() instead of setting this directly. + bool_param m_xuastc_ldr_use_lossy_supercompression; + + // Disable 2-3 subset usage in all effort levels, faster encoding, faster transcoding to BC7, but lower quality). + bool_param m_xuastc_ldr_force_disable_subsets; + + // Disable RGB dual plane usage (still can use dual plane on alpha blocks), for faster transcoding to BC7 but lower quality. + bool_param m_xuastc_ldr_force_disable_rgb_dual_plane; - // XUASTC Lossy supercompression PSNR threshold parameters + // Entropy coding syntax: Default is basist::astc_ldr_t::xuastc_ldr_syntax::cFullZstd (fastest transcoding but lower ratio). + param m_xuastc_ldr_syntax; + + // XUASTC/XUBC7 weights (TODO: rename to make it clear these are shared now) + // Each component channel weight must be >= 1 (no 0 weights allowed). + // Important: Default channel weights are 9,11,1,11. + // For best photo quality, especially on the largest block sizes, the RGB weights should be set to roughly 9,11,1, and alpha set to ~G, so 11. + // For non-photographic, non-sRGB or linear (normal map) content, this should be set to 1,1,1,1. + uint32_t m_xuastc_ldr_channel_weights[4]; + + // Set ASTC/XUASTC LDR linear or Rec 709-like channel weights. On larger ASTC/XUASTC LDR block sizes, 709-like weights make a noticeable difference in quality. + void set_xuastc_ldr_srgb_channel_weights(bool srgb_flag) + { + if (srgb_flag) + { + // 9,11,1,11 + m_xuastc_ldr_channel_weights[0] = 9; + m_xuastc_ldr_channel_weights[1] = 11; + m_xuastc_ldr_channel_weights[2] = 1; + m_xuastc_ldr_channel_weights[3] = 11; + } + else + { + m_xuastc_ldr_channel_weights[0] = 1; + m_xuastc_ldr_channel_weights[1] = 1; + m_xuastc_ldr_channel_weights[2] = 1; + m_xuastc_ldr_channel_weights[3] = 1; + } + } + + // Enable prefiltering (slight H and/or V axis blurring) during encoding: much slower, but higher quality especially on larger block sizes with DCT enabled. + bool_param m_xuastc_ldr_blurring; + + // See xuastc_ldr_deblocking_mode - controls SCD (stochastic coordinate descent) and whether or not a deblocking filter is applied during encoding + param m_xuastc_ldr_deblocking_mode; + + // Controls the # of SCD passes [2,256], 256=automatic depending on effort (the default) + param m_xuastc_ldr_num_deblocking_passes; + + // If true, low DCT quality factor quality is greatly improved - at the cost of slower encoding and higher bitrate. Experimental. + bool_param m_xuastc_ldr_heavy_subset_usage; + + // Sharpening mode and amount. Sharpening uses a Difference of Gaussians based unsharp masking approach to ALL levels including mip 0 before any compression. All PSNR's will be against the post-sharpened images. + // By default sharpening is disabled. + param m_xuastc_ldr_sharpen_mode; // enum class xuastc_ldr_sharpen_mode, default is cDisabled + param m_xuastc_ldr_sharpen_amount; // defaults to BASISU_XUASTC_LDR_DEFAULT_SHARPEN_AMOUNT, or 2.0 - the higher, the more sharpening, but also the more likely to introduce artifacts. + + // XUASTC LDR: Lossy supercompression PSNR threshold parameters param m_ls_min_psnr, m_ls_min_alpha_psnr; param m_ls_thresh_psnr, m_ls_thresh_alpha_psnr; param m_ls_thresh_edge_psnr, m_ls_thresh_edge_alpha_psnr; - + + param m_xuastc_ldr_debug_block_x, m_xuastc_ldr_debug_block_y; + + // Use astcenc vs. our ASTC LDR encoder: highly experimental/development. Not usable unless lib is compiled in/enabled via BASISU_SUPPORT_ASTCENC. Has known quality issues with alpha blocks as of 4/24/2026. + param m_xuastc_ldr_astc_comp_selection; // enum class xuastc_ldr_astc_comp_selection + + // XUBC7 + param m_xubc7_effort_level; // [0,10] + param m_xubc7_rdo_level; // [0,100], 0=no RDO + param m_xubc7_num_stripes; // [1,16], desired # of encode stripes (decode parallelism vs size) + param m_xubc7_encoder; // xbc7::bc7_encoder_type: 0=bc7f (default), 1=bc7e_scalar + param m_xubc7_bc7e_scalar_level; // bc7e_scalar quality level, clamped to [BC7E_SCALAR_MIN_LEVEL, BC7E_SCALAR_MAX_LEVEL] + // Job pool, MUST not be nullptr; job_pool *m_pJob_pool; @@ -788,6 +966,19 @@ namespace basisu error_code process(); + // Special-purpose alternative to process(): runs ONLY the source image loading/preparation prefix of the + // pipeline (read DDS sources, pick the format mode, read/prepare source images incl. mipmap generation and + // texture array/cubemap layout, then validate), and STOPS before any block extraction, encoding, or file output. + // init() MUST have been called first (identical contract to process()). To control LDR vs HDR preparation, set + // the format mode in the params BEFORE init() via set_format_mode(): use set_format_mode(cXUBC7) for the LDR path + // or set_format_mode(cUASTC_HDR_4x4) for the HDR path, so the existing format-driven loading/preparation code + // does something sane on this alternative path. After success, inspect the prepared slices/metadata via the + // getters below (get_slice_images()/get_slice_images_hdr()/get_slice_descs()/get_params()/etc.). + // NOTE: get_params() reflects the params AFTER pick_format_mode() has resolved/normalized them (e.g. m_hdr, + // m_uastc, the picked format), not necessarily what was passed to init(); use get_fmt_mode() for the resolved format. + // Like process(), this may only be called once per init() (see m_has_been_processed); to retry after a failure, call init() again. + error_code process_source_images(); + // The output .basis file will always be valid of process() succeeded. const uint8_vec &get_output_basis_file() const { return m_output_basis_file; } @@ -808,7 +999,29 @@ namespace basisu bool get_any_source_image_has_alpha() const { return m_any_source_image_has_alpha; } bool get_opencl_failed() const { return m_opencl_failed; } - + + // Accessors for the prepared source image data. These are valid after a successful process_source_images() + // (or process()) call, and remain valid for the lifetime of this object. The slice images are expanded if + // necessary (duplicating cols/rows) to account for block dimensions. + const basisu::vector &get_slice_images() const { return m_slice_images; } + const basisu::vector &get_slice_images_hdr() const { return m_slice_images_hdr; } + const basisu_backend_slice_desc_vec &get_slice_descs() const { return m_slice_descs; } + const basis_compressor_params &get_params() const { return m_params; } + // Finalized during HDR source image reading (only changed from the default 1.0f when the HDR input had to be + // scaled down to fit into half floats); stays 1.0f on the LDR path. + float get_hdr_image_scale() const { return m_hdr_image_scale; } + float get_ldr_to_hdr_upconversion_nit_multiplier() const { return m_ldr_to_hdr_upconversion_nit_multiplier; } + bool get_upconverted_any_ldr_images() const { return m_upconverted_any_ldr_images; } + + // The resolved output format mode and its block dimensions, as chosen by pick_format_mode() (these reflect the + // actual codec/format that the source images were prepared for, which may differ from get_params().get_format_mode()). + basist::basis_tex_format get_fmt_mode() const { return m_fmt_mode; } + uint32_t get_fmt_mode_block_width() const { return m_fmt_mode_block_width; } + uint32_t get_fmt_mode_block_height() const { return m_fmt_mode_block_height; } + + // Total number of (block-dimensioned) blocks across all prepared slices. + uint32_t get_total_blocks() const { return m_total_blocks; } + private: basis_compressor_params m_params; @@ -884,6 +1097,11 @@ namespace basisu bool m_opencl_failed; + // True once process() or process_source_images() has been called for the current init(). Guards against + // calling either more than once per init() (which is unsupported and would re-run/corrupt the front pipeline). + // Reset by init(). + bool m_has_been_processed; + void check_for_hdr_inputs(); bool sanity_check_input_params(); void clean_hdr_image(imagef& src_img); @@ -898,6 +1116,7 @@ namespace basisu bool write_output_files_and_compute_stats(); error_code encode_slices_to_astc_6x6_hdr(); error_code encode_slices_to_uastc_4x4_hdr(); + error_code encode_slices_to_xubc7(); error_code encode_slices_to_uastc_4x4_ldr(); error_code encode_slices_to_xuastc_or_astc_ldr(); bool generate_mipmaps(const imagef& img, basisu::vector& mips, bool has_alpha); @@ -928,12 +1147,12 @@ namespace basisu // In UASTC LDR 4x4 mode, the lower 8-bits are the UASTC LDR/HDR pack or effort level (see cPackUASTCLevelFastest to cPackUASTCLevelVerySlow). Fastest/lowest quality is 0, so be sure to set it correctly. Valid values are [0,4] for both LDR/HDR. // In UASTC HDR 4x4 mode, the lower 8-bits are the codec's effort level. Valid range is [uastc_hdr_4x4_codec_options::cMinLevel, uastc_hdr_4x4_codec_options::cMaxLevel]. Higher=better quality, but slower. // In RDO ASTC HDR 6x6/UASTC HDR 6x6 mode, the lower 8-bits are the codec's effort level. Valid range is [0,astc_6x6_hdr::ASTC_HDR_6X6_MAX_USER_COMP_LEVEL]. Higher levels=better quality, but slower. - // In XUASTC/ASTC LDR 4x4-12x12 mode, the lower 8-bits are the compressor's effort level from [0,10] (astc_ldr_t::EFFORT_LEVEL_MIN, astc_ldr_t::EFFORT_LEVEL_MAX). + // In XUASTC/ASTC LDR 4x4-12x12/XUBC7 mode, the lower 8-bits are the compressor's effort level from [0,10] (astc_ldr_t::EFFORT_LEVEL_MIN, astc_ldr_t::EFFORT_LEVEL_MAX). If you don't set it, you'll get lowest effort (lowest quality, worst ratio). // // float uastc_rdo_or_dct_quality: // UASTC LDR 4x4 RDO quality level: RDO lambda setting - 0=no change/highest quality. Higher values lower quality but increase compressibility, initially try .5-1.5. // RDO ASTC 6x6 HDR/UASTC 6x6 HDR: RDO lambda setting. 0=no change/highest quality. Higher values lower quality but increase compressibility, initially try 250-2000 (HDR) or 1000-10000 (LDR/SDR inputs upconverted to HDR). - // In XUASTC/ASTC LDR 4x4-12x12 mode, this is the [1,100] weight grid DCT quality level. + // In XUASTC/ASTC/XUBC7 LDR 4x4-12x12 mode, this is the [1,100] weight grid DCT quality level. // // pSize: Returns the output data's compressed size in bytes // @@ -947,7 +1166,7 @@ namespace basisu cFlagKTX2 = 1 << 11, // generate a KTX2 file cFlagKTX2UASTCSuperCompression = 1 << 12, // use KTX2 Zstd supercompression on non-supercompressed formats that support it. - cFlagSRGB = 1 << 13, // input texture is sRGB, use perceptual colorspace metrics, also use sRGB filtering during mipmap gen, and also sets KTX2/.basis output transfer func to sRGB + cFlagSRGB = 1 << 13, // input texture is sRGB, use perceptual colorspace metrics, also use sRGB filtering during mipmap gen, set 9,11,1,11 channel ASTC/XUASTC weights, and also sets KTX2/.basis output transfer func to sRGB. Otherwise assume linear input. cFlagGenMipsClamp = 1 << 14, // generate mipmaps with clamp addressing cFlagGenMipsWrap = 1 << 15, // generate mipmaps with wrap addressing @@ -981,6 +1200,17 @@ namespace basisu cFlagTextureTypeShift = 25, cFlagTextureTypeMask = 3, + + cFlagDisableDeblocking = 1 << 27, // ASTC/XUASTC LDR: by default 10x8 block sizes or larger (>= 80 texels/block) get deblocked/deblocking aware encoding. The deblocking ID will be written to the output file. This disables all deblocking related features. + cFlagForceDeblocking = 1 << 28, // ASTC/XUASTC LDR: force deblocking aware encoding on all block sizes, write deblocking ID to the output file + + // XUBC7 only: selects the BC7 base encoder and (for bc7e_scalar) its quality level, packed into a 3-bit field + // in the (otherwise unused) high bits. Field value 0 = bc7f (the default fast real-time packer; no level). + // Values 1-7 = bc7e_scalar (slower, higher quality) at level 0-6 (i.e. bc7e_scalar level = field value - 1). + // Packed as a single field because only 3 flag bits remain and bc7f needs no level; build the bc7e_scalar + // value as ((level + 1) << cFlagXUBC7BaseEncoderShift). Adding new bits here doesn't change the API signature. + cFlagXUBC7BaseEncoderShift = 29, + cFlagXUBC7BaseEncoderMask = 7 }; void* basis_compress_internal( diff --git a/encoder/basisu_enc.cpp b/encoder/basisu_enc.cpp index e92b4b8..0e4fc3b 100644 --- a/encoder/basisu_enc.cpp +++ b/encoder/basisu_enc.cpp @@ -53,9 +53,7 @@ namespace basisu uint64_t interval_timer::g_init_ticks, interval_timer::g_freq; double interval_timer::g_timer_freq; -#if BASISU_SUPPORT_SSE - bool g_cpu_supports_sse41; -#endif + bool g_cpu_supports_sse41 = false; fast_linear_to_srgb g_fast_linear_to_srgb; @@ -182,7 +180,7 @@ namespace basisu }; float g_srgb_to_linear_table[256]; - + void init_srgb_to_linear_table() { for (int i = 0; i < 256; ++i) @@ -465,6 +463,20 @@ namespace basisu return true; } + + bool load_qoi(const uint8_t* pBuf, size_t buf_size, image& img) + { + qoi_desc desc; + clear_obj(desc); + + void* p = qoi_decode(pBuf, (size_t)buf_size, &desc, 4); + if (!p) + return false; + + img.grant_ownership(static_cast(p), desc.width, desc.height); + + return true; + } bool load_qoi(const char* pFilename, image& img) { @@ -630,7 +642,7 @@ namespace basisu { case hdr_image_type::cHITRGBAHalfFloat: { - if (mem_size != width * height * sizeof(basist::half_float) * 4) + if (mem_size != (uint64_t)width * height * sizeof(basist::half_float) * 4) { assert(0); return false; @@ -665,7 +677,7 @@ namespace basisu } case hdr_image_type::cHITRGBAFloat: { - if (mem_size != width * height * sizeof(float) * 4) + if (mem_size != (uint64_t)width * height * sizeof(float) * 4) { assert(0); return false; @@ -700,6 +712,30 @@ namespace basisu convert_ldr_to_hdr_image(img, ldr_img, ldr_srgb_to_linear, linear_nit_multiplier, ldr_black_bias); break; } + case hdr_image_type::cHITQOIImage: + { + image ldr_img; + if (!load_qoi(static_cast(pMem), mem_size, ldr_img)) + return false; + + convert_ldr_to_hdr_image(img, ldr_img, ldr_srgb_to_linear, linear_nit_multiplier, ldr_black_bias); + break; + } + case hdr_image_type::cHITRGBA8Image: + { + if (!width || !height) + return false; + + const uint64_t expected_size = (uint64_t)width * height * sizeof(uint32_t); + if (mem_size != expected_size) + return false; + + image ldr_img(static_cast(pMem), width, height, 4); + + convert_ldr_to_hdr_image(img, ldr_img, ldr_srgb_to_linear, linear_nit_multiplier, ldr_black_bias); + + break; + } case hdr_image_type::cHITEXRImage: { if (!read_exr(pMem, mem_size, img)) @@ -2217,6 +2253,303 @@ namespace basisu im.print("Y 601 "); } + // PSNR-HVS and PSNR-HVS-M references: + // https://www.researchgate.net/profile/Vladimir_Lukin2/publication/251229783_A_NEW_FULL-REFERENCE_QUALITY_METRICS_BASED_ON_HVS/links/0046351f669a9c1869000000.pdf + // https://www.ponomarenko.info/psnrhvsm.htm + // https://github.com/lyckantropen/psnr_hvsm + // Note: to Match the Python implementation, convert to 8-bit REC 601 like this and compute only on Y. + // static inline uint8_t get_601_y(int r, int g, int b) { return (uint8_t)std::round(16.0f + 65.481f * (float)r / 255.0f + 128.553f * (float)g / 255.0f + 24.966f * (float)b / 255.0f); } + // For testing (image must be divisible by 8 pixels on each dimension, can't be grayscale): python3.12 -m psnr_hvsm image_a.png image_b.png + // Our 8-bit 601 Y metrics should very closely (within ~.001 dB) match the Python implementation (as of 4/1/2026) on RGB images divisible by 8 pixels on each dimension, otherwise there's something wrong. + + static const float g_csf[64] = + { + 1.608443f, 2.339554f, 2.573509f, 1.608443f, 1.072295f, 0.643377f, 0.504610f, 0.421887f, + 2.144591f, 2.144591f, 1.838221f, 1.354478f, 0.989811f, 0.443708f, 0.428918f, 0.467911f, + 1.838221f, 1.979622f, 1.608443f, 1.072295f, 0.643377f, 0.451493f, 0.372972f, 0.459555f, + 1.838221f, 1.513829f, 1.169777f, 0.887417f, 0.504610f, 0.295806f, 0.321689f, 0.415082f, + 1.429727f, 1.169777f, 0.695543f, 0.459555f, 0.378457f, 0.236102f, 0.249855f, 0.334222f, + 1.072295f, 0.735288f, 0.467911f, 0.402111f, 0.317717f, 0.247453f, 0.227744f, 0.279729f, + 0.525206f, 0.402111f, 0.329937f, 0.295806f, 0.249855f, 0.212687f, 0.214459f, 0.254803f, + 0.357432f, 0.279729f, 0.270896f, 0.262603f, 0.229778f, 0.257351f, 0.249855f, 0.259950f + }; + + static const float g_mask[64] = + { + 0.390625f, 0.826446f, 1.000000f, 0.390625f, 0.173611f, 0.062500f, 0.038447f, 0.026874f, + 0.694444f, 0.694444f, 0.510204f, 0.277008f, 0.147929f, 0.029727f, 0.027778f, 0.033058f, + 0.510204f, 0.591716f, 0.390625f, 0.173611f, 0.062500f, 0.030779f, 0.021004f, 0.031888f, + 0.510204f, 0.346021f, 0.206612f, 0.118906f, 0.038447f, 0.013212f, 0.015625f, 0.026015f, + 0.308642f, 0.206612f, 0.073046f, 0.031888f, 0.021626f, 0.008417f, 0.009426f, 0.016866f, + 0.173611f, 0.081633f, 0.033058f, 0.024414f, 0.015242f, 0.009246f, 0.007831f, 0.011815f, + 0.041649f, 0.024414f, 0.016437f, 0.013212f, 0.009426f, 0.006830f, 0.006944f, 0.009803f, + 0.019290f, 0.011815f, 0.011080f, 0.010412f, 0.007972f, 0.010000f, 0.009426f, 0.010203f + }; + + static float vari_ddof1_times_n(const float* s, uint32_t n) + { + assert(n); + + if (n <= 1) + return 0.0f; + + float mean = 0.0f; + for (uint32_t i = 0; i < n; ++i) + mean += s[i]; + mean /= static_cast(n); + + float sum_sq = 0.0f; + for (uint32_t i = 0; i < n; ++i) + { + const float d = s[i] - mean; + sum_sq += d * d; + } + + // sample variance * N = sum_sq/(N-1) * N (to match the Python implementation) + return sum_sq * (static_cast(n) / static_cast(n - 1)); + } + + static float vari_8x8_ddof1_times_n(const float block[64]) + { + return vari_ddof1_times_n(block, 64); + } + + static float vari_4x4_ddof1_times_n(const float block[64], uint32_t x0, uint32_t y0) + { + float tmp[16]; + uint32_t k = 0; + for (uint32_t y = 0; y < 4; ++y) + for (uint32_t x = 0; x < 4; ++x) + tmp[k++] = block[(y0 + y) * 8 + (x0 + x)]; + return vari_ddof1_times_n(tmp, 16); + } + + static float compute_mask_strength(const float block[64], const float dct[64]) + { + float mask = 0.0f; + for (uint32_t i = 1; i < 64; ++i) + mask += (dct[i] * dct[i]) * g_mask[i]; + + float pop = vari_8x8_ddof1_times_n(block); + if (pop != 0.0f) + { + const float qsum = vari_4x4_ddof1_times_n(block, 0, 0) + vari_4x4_ddof1_times_n(block, 4, 0) + vari_4x4_ddof1_times_n(block, 0, 4) + vari_4x4_ddof1_times_n(block, 4, 4); + pop = qsum / pop; + } + + return std::sqrt(mask * pop / 16.0f / 64.0f); + } + + bool psnr_hvs_compute_chan(const image& a, const image& b, int chan, psnr_hvs_chan_metrics&res) + { + clear_obj(res); + + // we allow the inputs to differ due to block size padding (which we assume has been done with clamping beyond the valid edges) + const uint32_t width = minimum(a.get_width(), b.get_width()); + const uint32_t height = minimum(a.get_height(), b.get_height()); + + if (!width || !height) + { + assert(0); + return false; + } + + const uint32_t num_blocks_x = (width + 7) / 8; + const uint32_t num_blocks_y = (height + 7) / 8; + + basist::astc_ldr_t::dct2f dct2d; + + bool status = dct2d.init(8, 8); + assert(status); + + if (!status) + return false; + + basist::astc_ldr_t::fvec dct_work; + + double sum_hvs = 0.0f, sum_hvsm = 0.0f; + + // Note: Python/Matlab variants only process full blocks, we process ALL blocks with clamping as needed. + for (uint32_t by = 0; by < num_blocks_y; by++) + { + for (uint32_t bx = 0; bx < num_blocks_x; bx++) + { + color_rgba a_block_rgba[64]; + a.extract_block_clamped(a_block_rgba, bx * 8, by * 8, 8, 8); + + color_rgba b_block_rgba[64]; + b.extract_block_clamped(b_block_rgba, bx * 8, by * 8, 8, 8); + + float a_block[64], b_block[64]; + if (chan < 0) + { + if ((psnr_hvs_channel_use)chan == psnr_hvs_channel_use::cUse601Y8Bit) + { + // convert to BT.601 Y 8-bit to match the Python implementation for testing/verification + for (uint32_t i = 0; i < 64; i++) + { + a_block[i] = (float)get_psnr_hvs_601_y(a_block_rgba[i]) * (1.0f / 255.0f); + b_block[i] = (float)get_psnr_hvs_601_y(b_block_rgba[i]) * (1.0f / 255.0f); + } + } + else + { + assert((psnr_hvs_channel_use)chan == psnr_hvs_channel_use::cUse601YFloat); + + // convert to BT.601 Y float for more precision (but doesn't match the Python implementation, resulting in significantly different output) + for (uint32_t i = 0; i < 64; i++) + { + a_block[i] = get_psnr_hvs_601_yf(a_block_rgba[i]); + b_block[i] = get_psnr_hvs_601_yf(b_block_rgba[i]); + } + } + } + else + { + for (uint32_t i = 0; i < 64; i++) + { + a_block[i] = (float)(a_block_rgba[i])[chan] * (1.0f / 255.0f); + b_block[i] = (float)(b_block_rgba[i])[chan] * (1.0f / 255.0f); + } + } + + float a_dct[64], b_dct[64]; + dct2d.forward(a_block, a_dct, dct_work); + dct2d.forward(b_block, b_dct, dct_work); + + float mask_a = compute_mask_strength(a_block, a_dct); + float mask_b = compute_mask_strength(b_block, b_dct); + if (mask_b > mask_a) + mask_a = mask_b; + + for (uint32_t i = 0; i < 64; i++) + { + float u = std::fabs(a_dct[i] - b_dct[i]); + + // PSNR-HVS + { + const float weighted = u * g_csf[i]; + sum_hvs += static_cast(weighted * weighted); + } + + // PSNR-HVS-M + if (i != 0) + { + const float threshold = mask_a / g_mask[i]; + if (u < threshold) + u = 0.0f; + else + u = u - threshold; + } + + { + const float weighted = u * g_csf[i]; + sum_hvsm += static_cast(weighted * weighted); + } + + } // j + + } // bx + } // by + + const uint32_t total_blocks = num_blocks_x * num_blocks_y; + const uint32_t total_samples = total_blocks * 64; + + res.m_mseh_hvs = sum_hvs / double(total_samples); + res.m_mseh_hvsm = sum_hvsm / double(total_samples); + + res.m_psnr_hvs = psnr_hvs_calc_psnr(res.m_mseh_hvs, 1.0f); + res.m_psnr_hvsm = psnr_hvs_calc_psnr(res.m_mseh_hvsm, 1.0f); + + return true; + } + + bool psnr_hvs_compute_metrics(const image& a, const image& b, psnr_hvs_metrics& metrics) + { + metrics.clear(); + + // This should closely match the psnr_hvsm project's output, but see Issue #9: https://github.com/lyckantropen/psnr_hvsm/issues/9 + bool status = psnr_hvs_compute_chan(a, b, (int)psnr_hvs_channel_use::cUse601Y8Bit, metrics.m_y_601_8bit); + if (!status) + return false; + + // Now compute as 601 float - noticeably more precise, but doesn't match psnr_hvsm (python) + status = psnr_hvs_compute_chan(a, b, (int)psnr_hvs_channel_use::cUse601YFloat, metrics.m_y_601_float); + if (!status) + return false; + + double sum_hvs_rgb = 0, sum_hvsm_rgb = 0; + double sum_hvs_rgba = 0, sum_hvsm_rgba = 0; + for (uint32_t c = 0; c < 4; c++) + { + auto& chan_metrics = metrics.m_chan[c]; + + bool status2 = psnr_hvs_compute_chan(a, b, c, chan_metrics); + if (!status2) + return false; + + if (c < 3) + { + sum_hvs_rgb += chan_metrics.m_mseh_hvs; + sum_hvsm_rgb += chan_metrics.m_mseh_hvsm; + } + sum_hvs_rgba += chan_metrics.m_mseh_hvs; + sum_hvsm_rgba += chan_metrics.m_mseh_hvsm; + } + + sum_hvs_rgb /= 3.0f; + sum_hvsm_rgb /= 3.0f; + + sum_hvs_rgba /= 4.0f; + sum_hvsm_rgba /= 4.0f; + + metrics.m_rgb.m_mseh_hvs = sum_hvs_rgb; + metrics.m_rgb.m_mseh_hvsm = sum_hvsm_rgb; + metrics.m_rgb.m_psnr_hvs = psnr_hvs_calc_psnr(sum_hvs_rgb, 1.0f); + metrics.m_rgb.m_psnr_hvsm = psnr_hvs_calc_psnr(sum_hvsm_rgb, 1.0f); + + metrics.m_rgba.m_mseh_hvs = sum_hvs_rgba; + metrics.m_rgba.m_mseh_hvsm = sum_hvsm_rgba; + metrics.m_rgba.m_psnr_hvs = psnr_hvs_calc_psnr(sum_hvs_rgba, 1.0f); + metrics.m_rgba.m_psnr_hvsm = psnr_hvs_calc_psnr(sum_hvsm_rgba, 1.0f); + + metrics.m_valid = true; + + return true; + } + + void psnr_hvs_print_metrics(const psnr_hvs_metrics& metrics) + { + if (!metrics.m_valid) + { + fmt_printf(" PSNR-HVS metrics are not valid.\n"); + return; + } + + fmt_printf(" Float Y 601 PSNR-HVS: {1.3} dB, PSNR-HVS-M: {1.3} dB\n", metrics.m_y_601_float.m_psnr_hvs, metrics.m_y_601_float.m_psnr_hvsm); + fmt_printf(" 8-Bit Y 601 PSNR-HVS: {1.3} dB, PSNR-HVS-M: {1.3} dB\n", metrics.m_y_601_8bit.m_psnr_hvs, metrics.m_y_601_8bit.m_psnr_hvsm); + + fmt_printf(" RGB Avg. PSNR-HVS: {1.3} dB, PSNR-HVS-M: {1.3} dB\n", metrics.m_rgb.m_psnr_hvs, metrics.m_rgb.m_psnr_hvsm); + fmt_printf(" RGBA Avg. PSNR-HVS: {1.3} dB, PSNR-HVS-M: {1.3} dB\n", metrics.m_rgba.m_psnr_hvs, metrics.m_rgba.m_psnr_hvsm); + + for (uint32_t c = 0; c < 4; c++) + { + fmt_printf(" {c} PSNR-HVS: {1.3} dB, PSNR-HVS-M: {1.3} dB\n", "RGBA"[c], metrics.m_chan[c].m_psnr_hvs, metrics.m_chan[c].m_psnr_hvsm); + } + } + + void print_psnr_hvs_image_metrics(const image& a, const image& b) + { + psnr_hvs_metrics metrics; + if (!psnr_hvs_compute_metrics(a, b, metrics)) + { + fmt_error_printf("print_psnr_hvs_image_metrics: psnr_hvs_compute_metrics() failed!\n"); + return; + } + + psnr_hvs_print_metrics(metrics); + } + void fill_buffer_with_random_bytes(void *pBuf, size_t size, uint32_t seed) { rand r(seed); @@ -2237,6 +2570,156 @@ namespace basisu } } + std::vector bounded_samples( + size_t num_samples, + float half_span, + float sigma, + uint32_t seed, + float min_tail_prob, + float tail_amp_cap, + bool fix_tail_count, + bool recenter) + { + assert(num_samples > 0); + assert(half_span > 0.0f); + + if (sigma < 0.0f) + sigma = 0.0f; + if (sigma > half_span) + sigma = half_span; + tail_amp_cap = clamp(tail_amp_cap, 0.0f, 1.0f); + + std::mt19937 rng(seed ? seed : std::random_device{}()); + std::uniform_real_distribution uniform_dist(-half_span, half_span); + std::bernoulli_distribution coin(0.5); + auto unit_rand = [&]() { return std::generate_canonical(rng); }; + + const float H = half_span; + const float H2 = H * H; + const float V = sigma * sigma; // target variance + const float varU = H2 / 3.0f; // variance of Uniform[-H, H] + const float Amax = tail_amp_cap; + + float p = 0.0f; // tail probability + float A = 0.0f; // tail amplitude fraction actually used (<= Amax) + float k2 = 0.0f; // bulk scale^2 for the k*Uniform component + + if (V <= varU + 1e-12f) + { + // Small target sigma: a scaled uniform already covers the variance, unless occasional tails are requested. + if (min_tail_prob <= 0.0f) + { + p = 0.0f; + k2 = (varU > 0.0f) ? (V / varU) : 0.0f; // support +/-kH + } + else + { + // Enforce a small tail probability, shrinking the amplitude so p*A^2*H^2 <= V. + p = clamp(min_tail_prob, 0.0f, 1.0f); + + // Max feasible amplitude for this p under the variance budget: A_needed <= sqrt(V/(p*H^2)). + float A_needed = (p > 0.0f) ? std::sqrt(maximum(0.0f, V) / (p * H2)) : 0.0f; + A = clamp(A_needed, 0.0f, Amax); + const float varT = (A * A) * H2; + + // Remaining variance goes to the scaled uniform. + const float denom = (1.0f - p) * varU; + k2 = (denom > 0.0f) ? ((V - p * varT) / denom) : 0.0f; + k2 = clamp(k2, 0.0f, 1.0f); + } + } + else + { + // Large target sigma: need enough tail mass to exceed the uniform's variance. + // Use the maximum tail amplitude, then solve for the required tail probability. + A = Amax; + const float varT = (A * A) * H2; + const float denom = (varT - varU); + float p_needed = (denom > 0.0f) ? ((V - varU) / denom) : 1.0f; + p_needed = clamp(p_needed, 0.0f, 1.0f); + + // Take at least p_needed, but also honor any requested baseline tail probability. + p = maximum(p_needed, clamp(min_tail_prob, 0.0f, 1.0f)); + if (p >= 1.0f - 1e-12f) + { + p = 1.0f; + k2 = 0.0f; + } + else + { + const float denom2 = (1.0f - p) * varU; + k2 = (denom2 > 0.0f) ? ((V - p * varT) / denom2) : 0.0f; + k2 = clamp(k2, 0.0f, 1.0f); + } + } + + const float k = std::sqrt(k2); + const float edge = A * H; + + std::vector out; + out.reserve(num_samples); + + if (p == 0.0f) + { + // Scaled uniform only (support +/-kH). + for (size_t i = 0; i < num_samples; ++i) + out.push_back(k * uniform_dist(rng)); + } + else if (fix_tail_count) + { + size_t num_tail = static_cast(std::round(p * num_samples)); + if (num_tail > num_samples) + num_tail = num_samples; + if (num_tail & 1) + { + // Keep the tail count even so the +edge / -edge halves stay symmetric. + if (num_tail < num_samples) + ++num_tail; + else + --num_tail; + } + + for (size_t i = 0; i < num_tail / 2; ++i) + out.push_back(+edge); + for (size_t i = 0; i < num_tail / 2; ++i) + out.push_back(-edge); + for (size_t i = out.size(); i < num_samples; ++i) + out.push_back(k * uniform_dist(rng)); + } + else + { + for (size_t i = 0; i < num_samples; ++i) + { + if (unit_rand() < p) + out.push_back(coin(rng) ? +edge : -edge); + else + out.push_back(k * uniform_dist(rng)); + } + } + + if (recenter) + { + float sum = 0.0f; + for (float v : out) + sum += v; + + const float mu = sum / static_cast(num_samples); + if (std::abs(mu) > 0.0f) + { + for (float& v : out) + { + v -= mu; + if (v > H) + v = H; + else if (v < -H) + v = -H; + } + } + } + + return out; + } + job_pool::job_pool(uint32_t num_threads) : m_num_active_jobs(0) { @@ -2269,11 +2752,21 @@ namespace basisu m_has_work.notify_all(); #ifdef __EMSCRIPTEN__ + // Without this wait the join()'s aren't reliable, and I have no idea why. WASM threading in the browser is sometimes mysterious. + const uint32_t max_iterations = 90; + + uint32_t iteration_index = 0; for ( ; ; ) { if (m_num_active_workers.load() <= 0) break; std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + if (++iteration_index > max_iterations) + { + debug_printf("job_pool::~job_pool: wait timed out!\n"); + break; + } } // At this point all worker threads should be exiting or exited. @@ -2283,6 +2776,8 @@ namespace basisu // Wait for all worker threads to exit. for (uint32_t i = 0; i < m_threads.size(); i++) m_threads[i].join(); + + debug_printf("job_pool::~job_pool: joined OK\n"); } void job_pool::add_job(const std::function& job) @@ -4620,7 +5115,7 @@ namespace basisu if (pFilename_prefix) { - std::string mode_vis_filename(std::string(pFilename_prefix) + "bc7_mode_vis.png"); + std::string mode_vis_filename(std::string(pFilename_prefix) + "_bc7_mode_vis.png"); save_png(mode_vis_filename, bc7_mode_vis); fmt_debug_printf("Wrote BC7 mode visualization to PNG file {}\n", mode_vis_filename); @@ -4723,6 +5218,120 @@ namespace basisu } // x } // y } + + static inline float eval_rotated_gaussian(float x, float y, float cos_t, float sin_t, float sigma_par, float sigma_perp) + { + // Project (x,y) onto the rotated axes + const float u = cos_t * x + sin_t * y; // parallel to blur direction + const float v = -sin_t * x + cos_t * y; // perpendicular + + const float inv_par2 = 1.0f / (2.0f * sigma_par * sigma_par); + const float inv_perp2 = 1.0f / (2.0f * sigma_perp * sigma_perp); + + return std::exp(-(u * u * inv_par2 + v * v * inv_perp2)); + } + + DirectionalKernel make_directional_kernel(float angle_deg, float sigma_par, float sigma_perp) + { + assert(sigma_par > 0.0f && "sigma_par must be positive"); + assert(sigma_perp > 0.0f && "sigma_perp must be positive"); + + // Convert angle to radians + const float angle_rad = angle_deg * (3.14159265358979323846f / 180.0f); + const float cos_t = std::cos(angle_rad); + const float sin_t = std::sin(angle_rad); + + // Determine kernel half-size and total size + const float sigma_x = std::sqrt(sigma_par * sigma_par * cos_t * cos_t + sigma_perp * sigma_perp * sin_t * sin_t); + const float sigma_y = std::sqrt(sigma_par * sigma_par * sin_t * sin_t + sigma_perp * sigma_perp * cos_t * cos_t); + const float reach = 3.0f * basisu::maximum(sigma_x, sigma_y) + 0.5f; + int half = maximum(1, static_cast(std::ceil(reach))); + + const int size = 2 * half + 1; // always odd + assert(size >= 3 && "kernel must be at least 3x3"); + + DirectionalKernel kernel; + kernel.m_size = size; + kernel.m_data.resize(size * size, 0.0f); + + // Fill with un-normalised Gaussian values, accumulate sum for normalisation + float sum = 0.0f; + for (int row = 0; row < size; ++row) + { + // y offset from kernel centre (positive = downward in image space) + const float y = static_cast(row - half); + + for (int col = 0; col < size; ++col) + { + const float x = static_cast(col - half); + const float w = eval_rotated_gaussian(x, y, cos_t, sin_t, sigma_par, sigma_perp); + kernel.m_data[row * size + col] = w; + sum += w; + } + } + + assert(sum > 0.0f && "kernel sum is zero - sigma values too small?"); + const float inv_sum = 1.0f / sum; + for (float& v : kernel.m_data) + v *= inv_sum; + +#if defined(DEBUG) || defined(_DEBUG) + { + float check = 0.0f; + for (float v : kernel.m_data) check += v; + assert(std::abs(check - 1.0f) < 1e-4f && "kernel normalisation failed"); + } +#endif + + return kernel; + } + + void directional_gaussian_blur(const image& src_img, image& dst_img, float angle_deg, float sigma_par, float sigma_perp) + { + assert((sigma_par > 0.0f) && (sigma_par > 0.0f)); + + dst_img.match_dimensions(src_img); + + const int width = src_img.get_width(); + const int height = src_img.get_height(); + + DirectionalKernel kernel(make_directional_kernel(angle_deg, sigma_par, sigma_perp)); + const int kernel_size = kernel.m_size; + const int half_kernel_size = kernel_size >> 1; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + float sum_r = 0, sum_g = 0, sum_b = 0, sum_a = 0; + + for (int ky = 0; ky < kernel_size; ky++) + { + const int sy = clamp(y + (ky - half_kernel_size), 0, height - 1); + + for (int kx = 0; kx < kernel_size; kx++) + { + const int sx = clamp(x + (kx - half_kernel_size), 0, width - 1); + + float weight = kernel.m_data[kx + ky * kernel_size]; + + const color_rgba& pixel = src_img(sx, sy); + + sum_r += (float)pixel.r * weight; + sum_g += (float)pixel.g * weight; + sum_b += (float)pixel.b * weight; + sum_a += (float)pixel.a * weight; + + } // kx + + } // ky + + color_rgba& dst_pixel = dst_img(x, y); + dst_pixel.set(basisu::fast_roundf_pos_int(sum_r), basisu::fast_roundf_pos_int(sum_g), basisu::fast_roundf_pos_int(sum_b), basisu::fast_roundf_pos_int(sum_a)); + + } // x + } // y + } // macro sent by CMakeLists.txt file when (TARGET_WASM AND WASM_THREADING) #if BASISU_WASI_THREADS @@ -4747,5 +5356,528 @@ namespace basisu return num_threads; } - + + bool display_astc_statistics( + const vector2D& blocks, + uint32_t block_width, uint32_t block_height, uint32_t image_width, uint32_t image_height, bool verbose) + { + const uint32_t total_block_pixels = block_width * block_height; + + fmt_printf("------- display_astc_statistics:\n"); + fmt_printf("Image dimensions in pixels: {}x{}, blocks: {}x{}\n", image_width, image_height, blocks.get_width(), blocks.get_height()); + fmt_printf("Block dimensions in pixels: {}x{}, {} total pixels\n", block_width, block_height, total_block_pixels); + + fmt_printf("Extra cols/rows to pad image to ASTC block dimensions: {}x{}\n", + blocks.get_width() * block_width - image_width, + blocks.get_height() * block_height - image_height); + + image dec_image_srgb(image_width, image_height); + image dec_image_linear(image_width, image_height); + imagef dec_image_float(image_width, image_height); + + uint32_t cem_hist[16] = { }; + uint32_t cem_dp_hist[16] = { }; + uint32_t cem_used_bc_hist[16] = { }; + uint32_t total_dp = 0; + uint32_t cem_ccs_hist[16][4] = { }; + uint32_t cem_part_hist[16][4] = { }; // 1-4 subsets + + uint32_t total_solid_blocks_ldr = 0; + uint32_t total_solid_blocks_hdr = 0; + uint32_t total_normal_blocks = 0; + + uint32_t part_hist[4] = { }; + uint32_t used_endpoint_levels_hist[astc_helpers::LAST_VALID_ENDPOINT_ISE_RANGE - astc_helpers::FIRST_VALID_ENDPOINT_ISE_RANGE + 1] = { }; + uint32_t used_weight_levels_hist[astc_helpers::LAST_VALID_WEIGHT_ISE_RANGE - astc_helpers::FIRST_VALID_WEIGHT_ISE_RANGE + 1] = { }; + + uint32_t total_unequal_cem_blocks = 0; + uint32_t total_unequal_cem_blocks_2subsets = 0; + uint32_t total_unequal_cem_blocks_3subsets = 0; + uint32_t total_unequal_cem_blocks_4subsets = 0; + + uint32_t highest_part_seed = 0; + + uint32_t total_suboptimal_cem_blocks = 0; + uint32_t total_unnecessary_suboptimal_cem_blocks = 0; + uint32_t total_useful_suboptimal_cem_blocks = 0; + + int min_weight_grid_width = INT_MAX, min_weight_grid_height = INT_MAX; + int max_weight_grid_width = 0, max_weight_grid_height = 0; + + uint32_t total_ldr_blocks = 0, total_hdr_blocks = 0; + + basisu::hash_map weight_grid_histogram; + + basisu::hash_map part_seed_hash; + + struct log_astc_block_config_cmp_t + { + bool operator()(const astc_helpers::log_astc_block& a, + const astc_helpers::log_astc_block& b) const + { + // This only compares the ASTC configuration for equality, NOT the contents. + if (a.m_error_flag != b.m_error_flag) + return false; + if (a.m_error_flag) + return true; + + if (a.m_grid_width != b.m_grid_width) + return false; + if (a.m_grid_height != b.m_grid_height) + return false; + + if (a.m_solid_color_flag_ldr != b.m_solid_color_flag_ldr) + return false; + if (a.m_solid_color_flag_hdr != b.m_solid_color_flag_hdr) + return false; + + if (a.m_solid_color_flag_ldr || a.m_solid_color_flag_hdr) + return true; + + if (a.m_dual_plane != b.m_dual_plane) + return false; + if (a.m_color_component_selector != b.m_color_component_selector) + return false; + + if (a.m_num_partitions != b.m_num_partitions) + return false; + if (a.m_uses_suboptimal_cem_encoding != b.m_uses_suboptimal_cem_encoding) + return false; + + if (a.m_endpoint_ise_range != b.m_endpoint_ise_range) + return false; + if (a.m_weight_ise_range != b.m_weight_ise_range) + return false; + + for (uint32_t i = 0; i < a.m_num_partitions; i++) + if (a.m_color_endpoint_modes[i] != b.m_color_endpoint_modes[i]) + return false; + + return true; + } + }; + + basisu::hash_map, log_astc_block_config_cmp_t > unique_config_histogram; + + uint32_t total_subsets = 0; + + for (uint32_t by = 0; by < blocks.get_height(); by++) + { + for (uint32_t bx = 0; bx < blocks.get_width(); bx++) + { + astc_helpers::log_astc_block log_blk; + + if (!astc_helpers::unpack_block(&blocks(bx, by), log_blk, block_width, block_height)) + { + fmt_error_printf("astc_helpers::unpack_block() failed on block {}x{}\n", bx, by); + return false; + } + + if (log_blk.m_error_flag) + { + fmt_error_printf("astc_helpers::unpack_block() returned an error flag on block {}x{}\n", bx, by); + return false; + } + + if (log_blk.m_uses_suboptimal_cem_encoding) + { + total_suboptimal_cem_blocks++; + + astc_helpers::log_astc_block temp_log_blk(log_blk); + temp_log_blk.m_uses_suboptimal_cem_encoding = false; + + astc_helpers::astc_block temp_phys_block; + + int expected_endpoint_range = -1; + + bool pack_status = astc_helpers::pack_astc_block(temp_phys_block, temp_log_blk, &expected_endpoint_range); + + // If the packing succeeded without the suboptimal CEM encoding, it means the BISE endpoint range didn't change, and it was unnecessary to use the suboptimal CEM encoding in the first place. + if (pack_status) + { + total_unnecessary_suboptimal_cem_blocks++; + } + else + { + // the endpoint range should have changed, and be valid + assert(expected_endpoint_range != -1); + assert(expected_endpoint_range != log_blk.m_endpoint_ise_range); + total_useful_suboptimal_cem_blocks++; + } + } + + { + astc_helpers::log_astc_block scrubbed_log_blk; + memset(&scrubbed_log_blk, 0, sizeof(scrubbed_log_blk)); + + // just record the config, not the contents, so only the config hashes + scrubbed_log_blk.m_solid_color_flag_ldr = log_blk.m_solid_color_flag_ldr; + scrubbed_log_blk.m_solid_color_flag_hdr = log_blk.m_solid_color_flag_hdr; + scrubbed_log_blk.m_dual_plane = log_blk.m_dual_plane; + scrubbed_log_blk.m_color_component_selector = log_blk.m_color_component_selector; + scrubbed_log_blk.m_grid_width = log_blk.m_grid_width; + scrubbed_log_blk.m_grid_height = log_blk.m_grid_height; + scrubbed_log_blk.m_num_partitions = log_blk.m_num_partitions; + scrubbed_log_blk.m_uses_suboptimal_cem_encoding = log_blk.m_uses_suboptimal_cem_encoding; + scrubbed_log_blk.m_color_endpoint_modes[0] = log_blk.m_color_endpoint_modes[0]; + scrubbed_log_blk.m_color_endpoint_modes[1] = log_blk.m_color_endpoint_modes[1]; + scrubbed_log_blk.m_color_endpoint_modes[2] = log_blk.m_color_endpoint_modes[2]; + scrubbed_log_blk.m_color_endpoint_modes[3] = log_blk.m_color_endpoint_modes[3]; + scrubbed_log_blk.m_weight_ise_range = log_blk.m_weight_ise_range; + scrubbed_log_blk.m_endpoint_ise_range = log_blk.m_endpoint_ise_range; + + auto ins_res(unique_config_histogram.insert(scrubbed_log_blk, 0)); + (ins_res.first)->second = (ins_res.first)->second + 1; + } + + bool is_hdr = log_blk.m_solid_color_flag_hdr; + + if (log_blk.m_solid_color_flag_ldr) + { + total_solid_blocks_ldr++; + total_ldr_blocks++; + } + else if (log_blk.m_solid_color_flag_hdr) + { + total_solid_blocks_hdr++; + total_hdr_blocks++; + } + else + { + total_normal_blocks++; + + min_weight_grid_width = minimum(min_weight_grid_width, log_blk.m_grid_width); + min_weight_grid_height = minimum(min_weight_grid_height, log_blk.m_grid_height); + + max_weight_grid_width = maximum(max_weight_grid_width, log_blk.m_grid_width); + max_weight_grid_height = maximum(max_weight_grid_height, log_blk.m_grid_height); + + { + uint32_t weight_grid_hash_key = log_blk.m_grid_width | (log_blk.m_grid_height << 8); + auto ins_res(weight_grid_histogram.insert(weight_grid_hash_key, 0)); + (ins_res.first)->second = (ins_res.first)->second + 1; + } + + if (log_blk.m_dual_plane) + { + total_dp++; + cem_ccs_hist[log_blk.m_color_endpoint_modes[0]][log_blk.m_color_component_selector]++; + } + + cem_part_hist[log_blk.m_color_endpoint_modes[0]][log_blk.m_num_partitions - 1]++; + + part_hist[log_blk.m_num_partitions - 1]++; + + // For debugging seed packing bugs + highest_part_seed = basisu::maximum(highest_part_seed, log_blk.m_partition_id); + + if (log_blk.m_num_partitions > 1) + { + auto ins_it = part_seed_hash.insert(log_blk.m_partition_id, 0); + (ins_it.first)->second = (ins_it.first)->second + 1; + } + + uint32_t cur_endpoint_ofs = 0; + bool has_unequal_cems = false; + + total_subsets += log_blk.m_num_partitions; + + for (uint32_t p = 0; p < log_blk.m_num_partitions; p++) + { + if (astc_helpers::is_cem_hdr(log_blk.m_color_endpoint_modes[p])) + is_hdr = true; + + cem_hist[log_blk.m_color_endpoint_modes[p]]++; + + if (log_blk.m_dual_plane) + cem_dp_hist[log_blk.m_color_endpoint_modes[p]]++; + + if ((p) && (log_blk.m_color_endpoint_modes[p] != log_blk.m_color_endpoint_modes[0])) + { + has_unequal_cems = true; + } + + if (astc_helpers::is_cem_ldr(log_blk.m_color_endpoint_modes[p])) + { + bool uses_bc = astc_helpers::used_blue_contraction(log_blk.m_color_endpoint_modes[p], log_blk.m_endpoints + cur_endpoint_ofs, log_blk.m_endpoint_ise_range); + + cem_used_bc_hist[log_blk.m_color_endpoint_modes[p]] += uses_bc; + } + + cur_endpoint_ofs += astc_helpers::get_num_cem_values(log_blk.m_color_endpoint_modes[p]); + } + + if (log_blk.m_num_partitions >= 2) + { + total_unequal_cem_blocks += has_unequal_cems; + + if (log_blk.m_num_partitions == 2) + total_unequal_cem_blocks_2subsets += has_unequal_cems; + else if (log_blk.m_num_partitions == 3) + total_unequal_cem_blocks_3subsets += has_unequal_cems; + else if (log_blk.m_num_partitions == 4) + total_unequal_cem_blocks_4subsets += has_unequal_cems; + } + + used_weight_levels_hist[open_range_check(log_blk.m_weight_ise_range - astc_helpers::FIRST_VALID_WEIGHT_ISE_RANGE, std::size(used_weight_levels_hist))]++; + used_endpoint_levels_hist[open_range_check(log_blk.m_endpoint_ise_range - astc_helpers::FIRST_VALID_ENDPOINT_ISE_RANGE, std::size(used_endpoint_levels_hist))]++; + } + + if (is_hdr) + { + total_hdr_blocks++; + } + else + { + total_ldr_blocks++; + + color_rgba block_pixels[astc_helpers::MAX_BLOCK_PIXELS]; + + // sRGB8 decode profile unpack + bool status = astc_helpers::decode_block(log_blk, block_pixels, block_width, block_height, astc_helpers::cDecodeModeSRGB8); + if (!status) + { + fmt_error_printf("astc_helpers::decode_block() failed on block {}x{}\n", bx, by); + return false; + } + + dec_image_srgb.set_block_clipped(block_pixels, bx * block_width, by * block_height, block_width, block_height); + + // linear8 decode profile unpack + status = astc_helpers::decode_block(log_blk, block_pixels, block_width, block_height, astc_helpers::cDecodeModeLDR8); + if (!status) + { + fmt_error_printf("astc_helpers::decode_block() failed on block {}x{}\n", bx, by); + return false; + } + + dec_image_linear.set_block_clipped(block_pixels, bx * block_width, by * block_height, block_width, block_height); + } + + // half float unpack + { + basist::half_float block_pixels_half[astc_helpers::MAX_BLOCK_PIXELS][4]; + + bool status = astc_helpers::decode_block(log_blk, block_pixels_half, block_width, block_height, astc_helpers::cDecodeModeHDR16); + if (!status) + { + fmt_error_printf("astc_helpers::decode_block() failed on block {}x{}\n", bx, by); + return false; + } + + vec4F block_pixels_float[astc_helpers::MAX_BLOCK_PIXELS]; + for (uint32_t i = 0; i < total_block_pixels; i++) + for (uint32_t j = 0; j < 4; j++) + block_pixels_float[i][j] = basist::half_to_float(block_pixels_half[i][j]); + + dec_image_float.set_block_clipped(block_pixels_float, bx * block_width, by * block_height, block_width, block_height); + } + + } // bx + + } //by + + fmt_printf("Total LDR blocks: {}, total HDR blocks: {}\n", total_ldr_blocks, total_hdr_blocks); + + if (verbose) + { + save_png("astc_decoded_srgb8_ldr.png", dec_image_srgb); + fmt_printf("Wrote astc_decoded_srgb8_ldr.png\n"); + + save_png("astc_decoded_linear8_ldr.png", dec_image_linear); + fmt_printf("Wrote astc_decoded_linear8_ldr.png\n"); + + write_exr("astc_decoded_half.exr", dec_image_float, 4, 0); + fmt_printf("Wrote astc_decoded_half.exr\n"); + } + + fmt_printf("\nASTC file statistics:\n"); + + const uint32_t total_blocks = (uint32_t)blocks.size(); + + fmt_printf("Total blocks: {}, total void extent LDR: {}, total void extent HDR: {}, total normal: {}\n", total_blocks, total_solid_blocks_ldr, total_solid_blocks_hdr, total_normal_blocks); + fmt_printf("Total dual plane: {} {3.2}%\n", total_dp, total_dp * 100.0f / (float)total_blocks); + + fmt_printf("Total blocks using suboptimal CEM encodings: {} {3.2}%\n", total_suboptimal_cem_blocks, total_suboptimal_cem_blocks * 100.0f / (float)total_blocks); + fmt_printf("Total blocks using unnecessary suboptimal CEM encodings: {} {3.2}%\n", total_unnecessary_suboptimal_cem_blocks, total_unnecessary_suboptimal_cem_blocks * 100.0f / (float)total_blocks); + fmt_printf("Total blocks using useful suboptimal CEM encodings: {} {3.2}%\n", total_useful_suboptimal_cem_blocks, total_useful_suboptimal_cem_blocks * 100.0f / (float)total_blocks); + + fmt_printf("Total subsets across all blocks: {}, Avg. subsets per block: {}\n", total_subsets, (float)total_subsets / (float)total_blocks); + + fmt_printf("Min weight grid usage bounds: {}x{}\n", min_weight_grid_width, min_weight_grid_height); + fmt_printf("Max weight grid usage bounds: {}x{}\n", max_weight_grid_width, max_weight_grid_height); + + fmt_printf("\nPartition usage histogram:\n"); + for (uint32_t i = 0; i < 4; i++) + fmt_printf("{}: {} {3.2}%\n", i + 1, part_hist[i], (float)part_hist[i] * 100.0f / (float)total_blocks); + + fmt_printf("\nCEM usage histogram (percentages relative to total overall subsets used in texture):\n"); + for (uint32_t i = 0; i < 15; i++) + { + fmt_printf("{}: {} {3.2}%, total BC: {} {3.2}%, total DP: {} {3.2}% (R:{} G:{} B:{} A:{}), parts: {} {} {} {})\n", i, + cem_hist[i], (float)cem_hist[i] * 100.0f / (float)total_subsets, + cem_used_bc_hist[i], (float)cem_used_bc_hist[i] * 100.0f / (float)total_subsets, + cem_dp_hist[i], (float)cem_dp_hist[i] * 100.0f / (float)total_subsets, + cem_ccs_hist[i][0], cem_ccs_hist[i][1], cem_ccs_hist[i][2], cem_ccs_hist[i][3], + cem_part_hist[i][0], cem_part_hist[i][1], cem_part_hist[i][2], cem_part_hist[i][3]); + } + + fmt_printf("\nUsed endpoint ISE levels:\n"); + for (uint32_t i = 0; i < std::size(used_endpoint_levels_hist); i++) + fmt_printf("{} levels: {}\n", astc_helpers::get_ise_levels(astc_helpers::FIRST_VALID_ENDPOINT_ISE_RANGE + i), used_endpoint_levels_hist[i]); + + fmt_printf("\nUsed weight ISE levels:\n"); + for (uint32_t i = 0; i < std::size(used_weight_levels_hist); i++) + fmt_printf("{} levels: {}\n", astc_helpers::get_ise_levels(astc_helpers::FIRST_VALID_WEIGHT_ISE_RANGE + i), used_weight_levels_hist[i]); + + fmt_printf("\nTotal 2+ subset blocks using unequal CEM's: {} {3.2}%\n", total_unequal_cem_blocks, (float)total_unequal_cem_blocks * 100.0f / (float)total_blocks); + fmt_printf("Total 2 subset blocks using unequal CEM's: {} {3.2}%\n", total_unequal_cem_blocks_2subsets, (float)total_unequal_cem_blocks_2subsets * 100.0f / (float)total_blocks); + fmt_printf("Total 3 subset blocks using unequal CEM's: {} {3.2}%\n", total_unequal_cem_blocks_3subsets, (float)total_unequal_cem_blocks_3subsets * 100.0f / (float)total_blocks); + fmt_printf("Total 4 subset blocks using unequal CEM's: {} {3.2}%\n", total_unequal_cem_blocks_4subsets, (float)total_unequal_cem_blocks_4subsets * 100.0f / (float)total_blocks); + + fmt_printf("\nHighest part ID seed: {}, 0x{0x}\n", highest_part_seed, highest_part_seed); + + fmt_printf("Total used partition seed ID's: {}\n", part_seed_hash.size_u32()); + if (verbose) + { + for (auto it = part_seed_hash.begin(); it != part_seed_hash.end(); ++it) + fmt_printf(" Seed ID {} used {} times\n", it->first, it->second); + } + + fmt_printf("\nWeight grid usage histogram:\n"); + + uint64_vec v; + for (auto it = weight_grid_histogram.begin(); it != weight_grid_histogram.end(); ++it) + v.push_back(((uint64_t)it->first << 32) | it->second); + + v.sort(); + + for (uint32_t i = 0; i < v.size(); i++) + fmt_printf(" {}x{}: total blocks {}\n", (v[i] >> 32) & 0xFF, (v[i] >> 40) & 0xFF, v[i] & UINT32_MAX); + + fmt_printf("\nTotal unique ASTC configurations: {}\n", unique_config_histogram.size_u32()); + + if (verbose) + { + uint32_t config_idx = 0; + for (auto it = unique_config_histogram.begin(); it != unique_config_histogram.end(); ++it) + { + const auto& l = it->first; + const uint32_t total = it->second; + + fmt_printf(" {}. Used {} {3.2}% times: Solid LDR: {} HDR: {}, Grid: {}x{}, Dual Plane: {}, CCS: {}, NumParts: {}, SuboptimalCEM: {}, CEMS: {} {} {} {}, WeightISERange: {} ({} levels), EndpointISERange: {} ({} levels)\n", + config_idx, total, float(total) * 100.0f / total_blocks, + l.m_solid_color_flag_ldr, l.m_solid_color_flag_hdr, + l.m_grid_width, l.m_grid_height, + l.m_dual_plane, l.m_color_component_selector, + l.m_num_partitions, l.m_uses_suboptimal_cem_encoding, + l.m_color_endpoint_modes[0], l.m_color_endpoint_modes[1], l.m_color_endpoint_modes[2], l.m_color_endpoint_modes[3], + l.m_weight_ise_range, astc_helpers::get_ise_levels(l.m_weight_ise_range), + l.m_endpoint_ise_range, astc_helpers::get_ise_levels(l.m_endpoint_ise_range)); + + config_idx++; + } + } + + fmt_printf("------- display_astc_statistics: OK\n"); + return true; + } + + bool display_astc_statistics( + const vector2D& blocks, + uint32_t block_width, uint32_t block_height, uint32_t image_width, uint32_t image_height, bool verbose) + { + vector2D phys_blocks(blocks.get_width(), blocks.get_height()); + + // ugh, but it's just for development/testing + for (uint32_t y = 0; y < blocks.get_height(); y++) + for (uint32_t x = 0; x < blocks.get_width(); x++) + if (!astc_helpers::pack_astc_block(phys_blocks(x, y), blocks(x, y))) + return false; + + return display_astc_statistics( + phys_blocks, + block_width, block_height, image_width, image_height, verbose); + } + + basisu::vector& get_convars() + { + static basisu::vector s_convars; + return s_convars; + } + + void list_convars() + { + fmt_printf("{} convars:\n", get_convars().size_u32()); + + for (size_t i = 0; i < get_convars().size(); i++) + { + const convar* p = get_convars()[i]; + + fmt_printf("convar: {} type: {} value: {}\n", p->get_name(), get_convar_type_string(p->get_type()), p->get_val_as_string()); + } + } + + static convar* find_convar(const std::string& name) + { + for (size_t i = 0; i < get_convars().size(); i++) + if (name == get_convars()[i]->get_name()) + return get_convars()[i]; + + return nullptr; + } + + void print_convar(const std::string& name) + { + convar* p = find_convar(name); + if (!p) + { + fmt_printf("error: convar \"{}\" not found\n", name); + return; + } + + fmt_printf("convar: {} type: {} value: {}\n", name, get_convar_type_string(p->get_type()), p->get_val_as_string()); + } + + void reset_convar(const std::string& name) + { + convar* p = find_convar(name); + if (!p) + { + fmt_printf("error: convar \"{}\" not found\n", name); + return; + } + + p->reset(); + + fmt_printf("OK\n"); + } + + void set_convar(const std::string& name, const std::string& val) + { + convar* p = find_convar(name); + if (!p) + { + fmt_printf("error: convar \"{}\" not found\n", name); + return; + } + + if (!val.size()) + { + fmt_printf("error: empty value for convar \"{}\"\n", name); + return; + } + + if (p->get_type() == cConvarFloat) + { + p->set((float)atof(val.c_str())); + } + else + { + p->set(atoi(val.c_str())); + } + + fmt_printf("OK\n"); + } + } // namespace basisu diff --git a/encoder/basisu_enc.h b/encoder/basisu_enc.h index ebf5473..fbaf4f7 100644 --- a/encoder/basisu_enc.h +++ b/encoder/basisu_enc.h @@ -52,14 +52,10 @@ namespace basisu void basisu_encoder_deinit(); // basisu_kernels_sse.cpp - will be a no-op and g_cpu_supports_sse41 will always be false unless compiled with BASISU_SUPPORT_SSE=1 - extern void detect_sse41(); - -#if BASISU_SUPPORT_SSE extern bool g_cpu_supports_sse41; -#else - const bool g_cpu_supports_sse41 = false; -#endif + extern void detect_sse41(); + void error_vprintf(const char* pFmt, va_list args); void error_printf(const char *pFmt, ...); @@ -75,7 +71,6 @@ namespace basisu void platform_sleep(uint32_t ms); // Helpers - inline uint8_t clamp255(int32_t i) { return (uint8_t)((i & 0xFFFFFF00U) ? (~(i >> 31)) : i); @@ -114,6 +109,7 @@ namespace basisu return (int)(x + 0.5f); } + // basic float to int (float_to_int) with rounding without std::roundf overhead inline int fast_roundf_int(float x) { return (x >= 0.0f) ? (int)(x + 0.5f) : (int)(x - 0.5f); @@ -125,6 +121,12 @@ namespace basisu return ((x < 0.0f) && (x != (float)xi)) ? (xi - 1) : xi; } + inline uint32_t extract_bits(uint32_t val, uint32_t ofs, uint32_t len) + { + assert((len) && (len < 32) && ((ofs + len) <= 32)); + return (val >> ofs) & ((1u << len) - 1u); + } + inline uint64_t read_bits(const uint8_t* pBuf, uint32_t& bit_offset, uint32_t codesize) { assert(codesize <= 64); @@ -179,18 +181,28 @@ namespace basisu inline int bounds_check_incl(int v, int l, int h) { (void)v; (void)l; (void)h; assert(v >= l && v <= h); return v; } inline uint32_t bounds_check_incl(uint32_t v, uint32_t l, uint32_t h) { (void)v; (void)l; (void)h; assert(v >= l && v <= h); return v; } + inline bool equal_abs_tol(float a, float b, float rel_tol) + { + return fabsf(a - b) <= rel_tol; + } + + inline bool equal_abs_tol(double a, double b, double rel_tol) + { + return fabs(a - b) <= rel_tol; + } + inline bool equal_rel_tol(float a, float b, float rel_tol) { - float diff = std::fabs(a - b); - float max_abs = std::max(std::fabs(a), std::fabs(b)); - return diff <= (max_abs * rel_tol); + const float diff = fabsf(a - b); + const float max_abs = basisu::maximum(fabsf(a), fabsf(b)); + return diff <= (rel_tol * basisu::maximum(1.0f, max_abs)); // near 0 use abs tol } inline bool equal_rel_tol(double a, double b, double rel_tol) { - double diff = std::fabs(a - b); - double max_abs = std::max(std::fabs(a), std::fabs(b)); - return diff <= (max_abs * rel_tol); + const double diff = fabs(a - b); + const double max_abs = basisu::maximum(fabs(a), fabs(b)); + return diff <= (rel_tol * basisu::maximum(1.0f, max_abs)); // near 0 use abs tol } inline uint32_t clz(uint32_t x) @@ -508,6 +520,14 @@ namespace basisu return res; } + static vec component_sqrt(const vec& a) + { + vec res; + for (uint32_t i = 0; i < N; i++) + res[i] = sqrt(a[i]); + return res; + } + static vec lerp(const vec& a, const vec& b, float s) { vec res; @@ -1035,6 +1055,22 @@ namespace basisu inline uint32_t get_bgra_uint32() const { return b | (g << 8) | (r << 16) | (a << 24); } inline uint32_t get_rgba_uint32() const { return r | (g << 8) | (b << 16) | (a << 24); } + inline vec3F get_vec3F() const { return vec3F((float)r, (float)g, (float)b); } + inline vec4F get_vec4F() const { return vec4F((float)r, (float)g, (float)b, (float)a); } + + inline uint32_t get_dist2(const color_rgba& p) const + { + return squarei(m_comps[0] - p[0]) + squarei(m_comps[1] - p[1]) + squarei(m_comps[2] - p[2]) + squarei(m_comps[3] - p[3]); + } + + inline uint32_t get_weighted_dist2(const color_rgba& p, const uint32_t weights[4]) const + { + return squarei(m_comps[0] - p[0]) * (int)weights[0] + + squarei(m_comps[1] - p[1]) * (int)weights[1] + + squarei(m_comps[2] - p[2]) * (int)weights[2] + + squarei(m_comps[3] - p[3]) * (int)weights[3]; + } + inline basist::color32 get_color32() const { return basist::color32(r, g, b, a); @@ -1371,26 +1407,49 @@ namespace basisu class rand { std::mt19937 m_mt; + std::normal_distribution m_norm; public: rand() { } - rand(uint32_t s) { seed(s); } - void seed(uint32_t s) { m_mt.seed(s); } + inline explicit rand(uint32_t s) { seed(s); } + + inline void seed(uint32_t s) { m_mt.seed(s); m_norm.reset(); } // between [l,h] - int irand(int l, int h) { std::uniform_int_distribution d(l, h); return d(m_mt); } + inline int irand(int l, int h) { std::uniform_int_distribution d(l, h); return d(m_mt); } - uint32_t urand32() { return static_cast(irand(INT32_MIN, INT32_MAX)); } + // p=[0,100] + inline bool iprob(int p) { assert((p >= 0) && (p <= 100)); return irand(0, 99) < p; } + + inline uint32_t urand32() { return std::uniform_int_distribution(0, std::numeric_limits::max())(m_mt); } - bool bit() { return irand(0, 1) == 1; } + inline bool bit() { return irand(0, 1) == 1; } - uint8_t byte() { return static_cast(urand32()); } + inline uint8_t byte() { return static_cast(urand32()); } // between [l,h) - float frand(float l, float h) { std::uniform_real_distribution d(l, h); return d(m_mt); } + float frand(float l, float h) + { + assert(l <= h); - float gaussian(float mean, float stddev) { std::normal_distribution d(mean, stddev); return d(m_mt); } + if (l >= h) + return l; + + std::uniform_real_distribution d(l, h); + float x = d(m_mt); + + // paranoia for buggy implementations + if (x < l) + x = l; + else if (x >= h) + x = std::nextafter(h, l); + + return x; + } + + // use standard N(0,1), scale manually + inline float gaussian(float mean, float stddev) { assert(stddev >= 0.0f); return mean + stddev * m_norm(m_mt); } }; class priority_queue @@ -3053,31 +3112,59 @@ namespace basisu } // Very straightforward blit with full clipping. Not fast, but it works. - image &blit(const image &src, int src_x, int src_y, int src_w, int src_h, int dst_x, int dst_y) + image &blit(const image &src, int src_x, int src_y, int src_w, int src_h, int dst_x, int dst_y, bool set_clipped_to_black = false) { - for (int y = 0; y < src_h; y++) + if (set_clipped_to_black) { - const int sy = src_y + y; - if (sy < 0) - continue; - else if (sy >= (int)src.get_height()) - break; - - for (int x = 0; x < src_w; x++) + for (int y = 0; y < src_h; y++) { - const int sx = src_x + x; - if (sx < 0) + const int sy = src_y + y; + + for (int x = 0; x < src_w; x++) + { + const int sx = src_x + x; + + const bool clipped_flag = (sx < 0) || (sx >= (int)src.get_width()) || (sy < 0) || (sy >= (int)src.get_height()); + + set_clipped(dst_x + x, dst_y + y, clipped_flag ? g_black_color : src(sx, sy)); + } + } + } + else + { + for (int y = 0; y < src_h; y++) + { + const int sy = src_y + y; + if (sy < 0) continue; - else if (sx >= (int)src.get_width()) + else if (sy >= (int)src.get_height()) break; - set_clipped(dst_x + x, dst_y + y, src(sx, sy)); + for (int x = 0; x < src_w; x++) + { + const int sx = src_x + x; + if (sx < 0) + continue; + else if (sx >= (int)src.get_width()) + break; + + set_clipped(dst_x + x, dst_y + y, src(sx, sy)); + } } } return *this; } + image& blit_clamped(const image& src, int src_x, int src_y, int src_w, int src_h, int dst_x, int dst_y) + { + for (int y = 0; y < src_h; y++) + for (int x = 0; x < src_w; x++) + set_clipped(dst_x + x, dst_y + y, src.get_clamped(src_x + x, src_y + y)); + + return *this; + } + const image &extract_block_clamped(color_rgba *pDst, uint32_t src_x, uint32_t src_y, uint32_t w, uint32_t h) const { if (((src_x + w) > m_width) || ((src_y + h) > m_height)) @@ -3755,13 +3842,12 @@ namespace basisu }; extern fast_linear_to_srgb g_fast_linear_to_srgb; - + // Image metrics - + class image_metrics { public: - // TODO: Add ssim uint32_t m_width, m_height; double m_max, m_mean, m_mean_squared, m_rms, m_psnr, m_ssim; bool m_has_neg, m_hf_mag_overflow, m_any_abnormal; @@ -3810,6 +3896,70 @@ namespace basisu }; void print_image_metrics(const image& a, const image& b); + + const float PSNR_HVS_LOSSLESS_DB = 100000.0f; + + inline double psnr_hvs_calc_psnr(double mseh, double peak) + { + if (mseh <= 0.0f) + return PSNR_HVS_LOSSLESS_DB; + + return 10.0f * std::log10((peak * peak) / mseh); + } + + // To match the PSNR-HVS calcs in the Python code, for testing + // computes Y of BT.601 "studio swing" / "narrow range" YCbCr + inline uint8_t get_psnr_hvs_601_y(const color_rgba& c) + { + int r = c[0], g = c[1], b = c[2]; + return (uint8_t)std::round(16.0f + 65.481f * (float)r * (1.0f / 255.0f) + 128.553f * (float)g * (1.0f / 255.0f) + 24.966f * (float)b * (1.0f / 255.0f)); + } + + inline float get_psnr_hvs_601_yf(const color_rgba& c) + { + float r = (float)c[0], g = (float)c[1], b = (float)c[2]; + return (16.0f + (65.481f / 255.0f) * r + (128.553f / 255.0f) * g + (24.966f / 255.0f) * b) * (1.0f / 255.0f); + } + + struct psnr_hvs_chan_metrics + { + double m_mseh_hvs; + double m_mseh_hvsm; + double m_psnr_hvs; + double m_psnr_hvsm; + }; + + // if chan<0, computes in narrow range BT.601 Y + enum class psnr_hvs_channel_use : int + { + cUse601Y8Bit = -1, + cUse601YFloat = -2 + }; + bool psnr_hvs_compute_chan(const image& a, const image& b, int chan, psnr_hvs_chan_metrics& res); + + struct psnr_hvs_metrics + { + psnr_hvs_chan_metrics m_y_601_8bit; + psnr_hvs_chan_metrics m_y_601_float; + + psnr_hvs_chan_metrics m_chan[4]; + + psnr_hvs_chan_metrics m_rgb; + psnr_hvs_chan_metrics m_rgba; + + bool m_valid; + + psnr_hvs_metrics() { clear(); } + + void clear() + { + clear_obj(*this); + } + }; + + bool psnr_hvs_compute_metrics(const image& a, const image& b, psnr_hvs_metrics &metrics); + void psnr_hvs_print_metrics(const psnr_hvs_metrics& metrics); + void print_psnr_hvs_image_metrics(const image& a, const image& b); // Image saving/loading/resampling @@ -3820,6 +3970,7 @@ namespace basisu bool load_tga(const char* pFilename, image& img); inline bool load_tga(const std::string &filename, image &img) { return load_tga(filename.c_str(), img); } + bool load_qoi(const uint8_t *pBuf, size_t buf_size, image &img); bool load_qoi(const char* pFilename, image& img); bool load_jpg(const char *pFilename, image& img); @@ -3849,7 +4000,9 @@ namespace basisu cHITPNGImage = 2, cHITEXRImage = 3, cHITHDRImage = 4, - cHITJPGImage = 5 + cHITJPGImage = 5, + cHITQOIImage = 6, + cHITRGBA8Image = 7 // plain LDR/SDR image to be upconverted }; bool load_image_hdr(const void* pMem, size_t mem_size, imagef& img, uint32_t width, uint32_t height, hdr_image_type img_type, bool ldr_srgb_to_linear, float linear_nit_multiplier = 1.0f, float ldr_black_bias = 0.0f); @@ -3971,6 +4124,31 @@ namespace basisu void fill_buffer_with_random_bytes(void *pBuf, size_t size, uint32_t seed = 1); + // Monte Carlo helper: returns num_samples random floats, every one inside the hard bound + // [-half_span, half_span], whose distribution targets a standard deviation of sigma. + // + // The distribution is a mixture of a scaled uniform "bulk" (k*Uniform[-H,H]) and a pair of + // symmetric "tails" placed at +/-(A*H). For small sigma the bulk alone carries the variance; + // for large sigma (above the uniform's own variance H^2/3) the tails are added so the target + // standard deviation can still be reached without ever exceeding the bound. + // + // seed 0 => nondeterministic seed (std::random_device), otherwise deterministic. + // min_tail_prob minimum probability mass placed in the +/-edge tails (in [0,1]). + // tail_amp_cap max tail amplitude as a fraction of half_span (in [0,1]). + // fix_tail_count if true, place exactly round(p*num_samples) tail samples instead of + // drawing each sample's tail/bulk choice randomly (lower-variance count). + // recenter if true, subtract the realized sample mean afterwards, then re-clamp to + // [-half_span, half_span]. + std::vector bounded_samples( + size_t num_samples, + float half_span, // H: hard bound, all returned samples lie in [-H, H] + float sigma, // target standard deviation (clamped to [0, H]) + uint32_t seed = 0, + float min_tail_prob = 0.0f, + float tail_amp_cap = 1.0f, + bool fix_tail_count = false, + bool recenter = false); + const uint32_t cPixelBlockWidth = 4; const uint32_t cPixelBlockHeight = 4; const uint32_t cPixelBlockTotalPixels = cPixelBlockWidth * cPixelBlockHeight; @@ -4392,9 +4570,198 @@ namespace basisu // simple non-perspective correct triangle rasterizer with texture mapping, useful for generating randomized test data void draw_tri2(image& dst, const image* pTex, const tri2& tri, bool alpha_blend); + struct DirectionalKernel + { + basisu::vector m_data; // row-major, size*size elements + int m_size; // always odd, e.g. 7 means a 7x7 kernel + }; + + DirectionalKernel make_directional_kernel(float angle_deg, float sigma_par, float sigma_perp = 0.5f); + + void directional_gaussian_blur(const image& src_img, image& dst_img, float angle_deg, float sigma_par, float sigma_perp = 0.5f); + void set_num_wasi_threads(uint32_t num_threads); int get_num_hardware_threads(); - + + bool display_astc_statistics( + const vector2D& blocks, + uint32_t block_width, uint32_t block_height, uint32_t image_width, uint32_t image_height, bool verbose); + + bool display_astc_statistics( + const vector2D& blocks, + uint32_t block_width, uint32_t block_height, uint32_t image_width, uint32_t image_height, bool verbose); + + enum convar_type + { + cConvarFloat, + cConvarInt + }; + + static inline const char* get_convar_type_string(convar_type t) + { + switch (t) + { + case cConvarFloat: return "float"; + case cConvarInt: return "int"; + default: assert(0); return "invalid"; + } + } + + // intended for development only + class convar; + + extern basisu::vector& get_convars(); + + // instances MUST be at global scope, and last forever + class convar + { + std::string m_name; + convar_type m_type; + + struct int_t + { + int m_cur, m_def, m_min, m_max; + }; + + struct float_t + { + float m_cur, m_def, m_min, m_max; + }; + + union + { + int_t m_int; + float_t m_float; + }; + + public: + convar(const char* pName, int def, int min_val, int max_val) : + m_name(pName), + m_type(cConvarInt) + { + m_int.m_cur = def; + m_int.m_def = def; + m_int.m_min = min_val; + m_int.m_max = max_val; + + size_t cur_idx; + if (!get_convars().find(this, cur_idx)) + get_convars().push_back(this); + } + + convar(const char* pName, float def, float min_val, float max_val) : + m_name(pName), + m_type(cConvarFloat) + { + m_float.m_cur = def; + m_float.m_def = def; + m_float.m_min = min_val; + m_float.m_max = max_val; + + size_t cur_idx; + if (!get_convars().find(this, cur_idx)) + get_convars().push_back(this); + } + + ~convar() + { + // do nothing, we assume global convars + } + + const std::string& get_name() const { return m_name; } + + std::string get_val_as_string() const + { + std::string res; + + if (m_type == cConvarFloat) + { + res = fmt_string("{}", m_float.m_cur); + } + else if (m_type == cConvarInt) + { + res = fmt_string("{}", m_int.m_cur); + } + else + { + assert(0); + } + + return res; + } + + convar_type get_type() const { return m_type; } + + void set(float val) + { + if (m_type != cConvarFloat) + { + assert(0); + return; + } + m_float.m_cur = clamp(val, m_float.m_min, m_float.m_max); + } + + void set(int val) + { + if (m_type != cConvarInt) + { + assert(0); + return; + } + m_int.m_cur = clamp(val, m_int.m_min, m_int.m_max); + } + + float get_float() const + { + if (m_type != cConvarFloat) + { + assert(0); + return 0.0f; + } + return m_float.m_cur; + } + + int get_int() const + { + if (m_type != cConvarInt) + { + assert(0); + return 0; + } + return m_int.m_cur; + } + + void reset() + { + if (m_type == cConvarInt) + m_int.m_cur = m_int.m_def; + else + { + assert(m_type == cConvarFloat); + m_float.m_cur = m_float.m_def; + } + } + + bool get_bool() const + { + if (m_type == cConvarInt) + { + return m_int.m_cur != 0; + } + else + { + assert(m_type == cConvarFloat); + return m_float.m_cur != 0.0f; + } + } + }; + + void list_convars(); + void print_convar(const std::string& name); + void reset_convar(const std::string& name); + void set_convar(const std::string& name, const std::string &val); + } // namespace basisu #include "basisu_math.h" diff --git a/encoder/basisu_gpu_texture.cpp b/encoder/basisu_gpu_texture.cpp index 89bb2fe..23ef016 100644 --- a/encoder/basisu_gpu_texture.cpp +++ b/encoder/basisu_gpu_texture.cpp @@ -17,6 +17,7 @@ #include "basisu_pvrtc1_4.h" #include "basisu_bc7enc.h" #include "../transcoder/basisu_astc_hdr_core.h" +#include "../transcoder/basisu_transcoder.h" #define TINYDDS_IMPLEMENTATION #include "3rdparty/tinydds.h" @@ -25,6 +26,95 @@ namespace basisu { + const char* get_dds_format_string(basist::dds_format fmt) + { + switch (fmt) + { + case basist::dds_format::cBC1: return "BC1 (DXT1)"; + case basist::dds_format::cBC2: return "BC2 (DXT2/DXT3)"; + case basist::dds_format::cBC3: return "BC3 (DXT4/DXT5)"; + case basist::dds_format::cBC4: return "BC4 (ATI1/BC4U)"; + case basist::dds_format::cBC5: return "BC5 (ATI2/DXN)"; + case basist::dds_format::cBC7: return "BC7"; + case basist::dds_format::cR5G6B5: return "R5G6B5 (16bpp)"; + case basist::dds_format::cA1R5G5B5: return "A1R5G5B5 (16bpp)"; + case basist::dds_format::cX1R5G5B5: return "X1R5G5B5 (16bpp)"; + case basist::dds_format::cA4R4G4B4: return "A4R4G4B4 (16bpp)"; + case basist::dds_format::cX4R4G4B4: return "X4R4G4B4 (16bpp)"; + case basist::dds_format::cR8G8B8: return "R8G8B8 (24bpp, BGR in memory)"; + case basist::dds_format::cB8G8R8: return "B8G8R8 (24bpp, RGB in memory)"; + case basist::dds_format::cA8R8G8B8: return "A8R8G8B8 (32bpp, BGRA in memory)"; + case basist::dds_format::cX8R8G8B8: return "X8R8G8B8 (32bpp, BGRX in memory)"; + case basist::dds_format::cA8B8G8R8: return "A8B8G8R8 (32bpp, RGBA in memory)"; + case basist::dds_format::cX8B8G8R8: return "X8B8G8R8 (32bpp, RGBX in memory)"; + case basist::dds_format::cR8: return "R8 (8bpp)"; + case basist::dds_format::cR8G8: return "R8G8 (16bpp)"; + case basist::dds_format::cA8: return "A8 (8bpp)"; + case basist::dds_format::cL8: return "L8 (8bpp luminance)"; + case basist::dds_format::cA8L8: return "A8L8 (16bpp luminance+alpha)"; + case basist::dds_format::cInvalid: + default: return "Invalid"; + } + } + + const char* get_texture_format_name(texture_format fmt) + { + switch (fmt) + { + case texture_format::cInvalidTextureFormat: return "Invalid"; + case texture_format::cETC1: return "ETC1"; + case texture_format::cETC1S: return "ETC1S"; + case texture_format::cETC2_RGB: return "ETC2_RGB"; + case texture_format::cETC2_RGBA: return "ETC2_RGBA"; + case texture_format::cETC2_ALPHA: return "ETC2_ALPHA"; + case texture_format::cBC1: return "BC1"; + case texture_format::cBC3: return "BC3"; + case texture_format::cBC4: return "BC4"; + case texture_format::cBC5: return "BC5"; + case texture_format::cBC6HSigned: return "BC6H_Signed"; + case texture_format::cBC6HUnsigned: return "BC6H_Unsigned"; + case texture_format::cBC7: return "BC7"; + case texture_format::cASTC_LDR_4x4: return "ASTC_LDR_4x4"; + case texture_format::cASTC_HDR_4x4: return "ASTC_HDR_4x4"; + case texture_format::cASTC_HDR_6x6: return "ASTC_HDR_6x6"; + case texture_format::cPVRTC1_4_RGB: return "PVRTC1_4_RGB"; + case texture_format::cPVRTC1_4_RGBA: return "PVRTC1_4_RGBA"; + case texture_format::cATC_RGB: return "ATC_RGB"; + case texture_format::cATC_RGBA_INTERPOLATED_ALPHA: return "ATC_RGBA_INTERPOLATED_ALPHA"; + case texture_format::cFXT1_RGB: return "FXT1_RGB"; + case texture_format::cPVRTC2_4_RGBA: return "PVRTC2_4_RGBA"; + case texture_format::cETC2_R11_EAC: return "ETC2_R11_EAC"; + case texture_format::cETC2_RG11_EAC: return "ETC2_RG11_EAC"; + case texture_format::cUASTC4x4: return "UASTC4x4"; + case texture_format::cUASTC_HDR_4x4: return "UASTC_HDR_4x4"; + case texture_format::cBC1_NV: return "BC1_NV"; + case texture_format::cBC1_AMD: return "BC1_AMD"; + case texture_format::cRGBA32: return "RGBA32"; + case texture_format::cRGB565: return "RGB565"; + case texture_format::cBGR565: return "BGR565"; + case texture_format::cRGBA4444: return "RGBA4444"; + case texture_format::cABGR4444: return "ABGR4444"; + case texture_format::cRGBA_HALF: return "RGBA_HALF"; + case texture_format::cRGB_HALF: return "RGB_HALF"; + case texture_format::cRGB_9E5: return "RGB_9E5"; + case texture_format::cASTC_LDR_5x4: return "ASTC_LDR_5x4"; + case texture_format::cASTC_LDR_5x5: return "ASTC_LDR_5x5"; + case texture_format::cASTC_LDR_6x5: return "ASTC_LDR_6x5"; + case texture_format::cASTC_LDR_6x6: return "ASTC_LDR_6x6"; + case texture_format::cASTC_LDR_8x5: return "ASTC_LDR_8x5"; + case texture_format::cASTC_LDR_8x6: return "ASTC_LDR_8x6"; + case texture_format::cASTC_LDR_10x5: return "ASTC_LDR_10x5"; + case texture_format::cASTC_LDR_10x6: return "ASTC_LDR_10x6"; + case texture_format::cASTC_LDR_8x8: return "ASTC_LDR_8x8"; + case texture_format::cASTC_LDR_10x8: return "ASTC_LDR_10x8"; + case texture_format::cASTC_LDR_10x10: return "ASTC_LDR_10x10"; + case texture_format::cASTC_LDR_12x10: return "ASTC_LDR_12x10"; + case texture_format::cASTC_LDR_12x12: return "ASTC_LDR_12x12"; + default: break; + } + return "?"; + } + //------------------------------------------------------------------------------------------------ // ETC2 EAC @@ -64,6 +154,8 @@ namespace basisu //------------------------------------------------------------------------------------------------ // BC1 + +#if 0 struct bc1_block { enum { cTotalEndpointBytes = 2, cTotalSelectorBytes = 4 }; @@ -88,69 +180,13 @@ namespace basisu inline uint32_t get_selector(uint32_t x, uint32_t y) const { assert((x < 4U) && (y < 4U)); return (m_selectors[y] >> (x * 2)) & 3; } }; - - // Returns true if the block uses 3 color punchthrough alpha mode. - bool unpack_bc1(const void* pBlock_bits, color_rgba* pPixels, bool set_alpha) - { - static_assert(sizeof(bc1_block) == 8, "sizeof(bc1_block) == 8"); - - const bc1_block* pBlock = static_cast(pBlock_bits); - - const uint32_t l = pBlock->get_low_color(); - const uint32_t h = pBlock->get_high_color(); - - color_rgba c[4]; - - uint32_t r0, g0, b0, r1, g1, b1; - bc1_block::unpack_color(l, r0, g0, b0); - bc1_block::unpack_color(h, r1, g1, b1); - - c[0].set_noclamp_rgba(r0, g0, b0, 255); - c[1].set_noclamp_rgba(r1, g1, b1, 255); - - bool used_punchthrough = false; - - if (l > h) - { - c[2].set_noclamp_rgba((r0 * 2 + r1) / 3, (g0 * 2 + g1) / 3, (b0 * 2 + b1) / 3, 255); - c[3].set_noclamp_rgba((r1 * 2 + r0) / 3, (g1 * 2 + g0) / 3, (b1 * 2 + b0) / 3, 255); - } - else - { - c[2].set_noclamp_rgba((r0 + r1) / 2, (g0 + g1) / 2, (b0 + b1) / 2, 255); - c[3].set_noclamp_rgba(0, 0, 0, 0); - used_punchthrough = true; - } - - if (set_alpha) - { - for (uint32_t y = 0; y < 4; y++, pPixels += 4) - { - pPixels[0] = c[pBlock->get_selector(0, y)]; - pPixels[1] = c[pBlock->get_selector(1, y)]; - pPixels[2] = c[pBlock->get_selector(2, y)]; - pPixels[3] = c[pBlock->get_selector(3, y)]; - } - } - else - { - for (uint32_t y = 0; y < 4; y++, pPixels += 4) - { - pPixels[0].set_rgb(c[pBlock->get_selector(0, y)]); - pPixels[1].set_rgb(c[pBlock->get_selector(1, y)]); - pPixels[2].set_rgb(c[pBlock->get_selector(2, y)]); - pPixels[3].set_rgb(c[pBlock->get_selector(3, y)]); - } - } - - return used_punchthrough; - } +#endif bool unpack_bc1_nv(const void* pBlock_bits, color_rgba* pPixels, bool set_alpha) { - static_assert(sizeof(bc1_block) == 8, "sizeof(bc1_block) == 8"); + static_assert(sizeof(basist::bc1_block) == 8, "sizeof(bc1_block) == 8"); - const bc1_block* pBlock = static_cast(pBlock_bits); + const basist::bc1_block* pBlock = static_cast(pBlock_bits); const uint32_t l = pBlock->get_low_color(); const uint32_t h = pBlock->get_high_color(); @@ -230,7 +266,7 @@ namespace basisu bool unpack_bc1_amd(const void* pBlock_bits, color_rgba* pPixels, bool set_alpha) { - const bc1_block* pBlock = static_cast(pBlock_bits); + const basist::bc1_block* pBlock = static_cast(pBlock_bits); const uint32_t l = pBlock->get_low_color(); const uint32_t h = pBlock->get_high_color(); @@ -238,8 +274,8 @@ namespace basisu color_rgba c[4]; uint32_t r0, g0, b0, r1, g1, b1; - bc1_block::unpack_color(l, r0, g0, b0); - bc1_block::unpack_color(h, r1, g1, b1); + basist::bc1_block::unpack_color(l, r0, g0, b0); + basist::bc1_block::unpack_color(h, r1, g1, b1); c[0].set_noclamp_rgba(r0, g0, b0, 255); c[1].set_noclamp_rgba(r1, g1, b1, 255); @@ -282,108 +318,6 @@ namespace basisu return used_punchthrough; } - //------------------------------------------------------------------------------------------------ - // BC3-5 - - struct bc4_block - { - enum { cBC4SelectorBits = 3, cTotalSelectorBytes = 6, cMaxSelectorValues = 8 }; - uint8_t m_endpoints[2]; - - uint8_t m_selectors[cTotalSelectorBytes]; - - inline uint32_t get_low_alpha() const { return m_endpoints[0]; } - inline uint32_t get_high_alpha() const { return m_endpoints[1]; } - inline bool is_alpha6_block() const { return get_low_alpha() <= get_high_alpha(); } - - inline uint64_t get_selector_bits() const - { - return ((uint64_t)((uint32_t)m_selectors[0] | ((uint32_t)m_selectors[1] << 8U) | ((uint32_t)m_selectors[2] << 16U) | ((uint32_t)m_selectors[3] << 24U))) | - (((uint64_t)m_selectors[4]) << 32U) | - (((uint64_t)m_selectors[5]) << 40U); - } - - inline uint32_t get_selector(uint32_t x, uint32_t y, uint64_t selector_bits) const - { - assert((x < 4U) && (y < 4U)); - return (selector_bits >> (((y * 4) + x) * cBC4SelectorBits)) & (cMaxSelectorValues - 1); - } - - static inline uint32_t get_block_values6(uint8_t* pDst, uint32_t l, uint32_t h) - { - pDst[0] = static_cast(l); - pDst[1] = static_cast(h); - pDst[2] = static_cast((l * 4 + h) / 5); - pDst[3] = static_cast((l * 3 + h * 2) / 5); - pDst[4] = static_cast((l * 2 + h * 3) / 5); - pDst[5] = static_cast((l + h * 4) / 5); - pDst[6] = 0; - pDst[7] = 255; - return 6; - } - - static inline uint32_t get_block_values8(uint8_t* pDst, uint32_t l, uint32_t h) - { - pDst[0] = static_cast(l); - pDst[1] = static_cast(h); - pDst[2] = static_cast((l * 6 + h) / 7); - pDst[3] = static_cast((l * 5 + h * 2) / 7); - pDst[4] = static_cast((l * 4 + h * 3) / 7); - pDst[5] = static_cast((l * 3 + h * 4) / 7); - pDst[6] = static_cast((l * 2 + h * 5) / 7); - pDst[7] = static_cast((l + h * 6) / 7); - return 8; - } - - static inline uint32_t get_block_values(uint8_t* pDst, uint32_t l, uint32_t h) - { - if (l > h) - return get_block_values8(pDst, l, h); - else - return get_block_values6(pDst, l, h); - } - }; - - void unpack_bc4(const void* pBlock_bits, uint8_t* pPixels, uint32_t stride) - { - static_assert(sizeof(bc4_block) == 8, "sizeof(bc4_block) == 8"); - - const bc4_block* pBlock = static_cast(pBlock_bits); - - uint8_t sel_values[8]; - bc4_block::get_block_values(sel_values, pBlock->get_low_alpha(), pBlock->get_high_alpha()); - - const uint64_t selector_bits = pBlock->get_selector_bits(); - - for (uint32_t y = 0; y < 4; y++, pPixels += (stride * 4U)) - { - pPixels[0] = sel_values[pBlock->get_selector(0, y, selector_bits)]; - pPixels[stride * 1] = sel_values[pBlock->get_selector(1, y, selector_bits)]; - pPixels[stride * 2] = sel_values[pBlock->get_selector(2, y, selector_bits)]; - pPixels[stride * 3] = sel_values[pBlock->get_selector(3, y, selector_bits)]; - } - } - - // Returns false if the block uses 3-color punchthrough alpha mode, which isn't supported on some GPU's for BC3. - bool unpack_bc3(const void* pBlock_bits, color_rgba* pPixels) - { - bool success = true; - - if (unpack_bc1((const uint8_t*)pBlock_bits + sizeof(bc4_block), pPixels, true)) - success = false; - - unpack_bc4(pBlock_bits, &pPixels[0].a, sizeof(color_rgba)); - - return success; - } - - // writes RG - void unpack_bc5(const void* pBlock_bits, color_rgba* pPixels) - { - unpack_bc4(pBlock_bits, &pPixels[0].r, sizeof(color_rgba)); - unpack_bc4((const uint8_t*)pBlock_bits + sizeof(bc4_block), &pPixels[0].g, sizeof(color_rgba)); - } - //------------------------------------------------------------------------------------------------ // ATC isn't officially documented, so I'm assuming these references: // http://www.guildsoftware.com/papers/2012.Converting.DXTC.to.ATC.pdf @@ -1053,7 +987,7 @@ namespace basisu { case texture_format::cBC1: { - unpack_bc1(pBlock, pPixels, true); + basist::bcu::unpack_bc1(pBlock, reinterpret_cast(pPixels), true); break; } case texture_format::cBC1_NV: @@ -1068,17 +1002,19 @@ namespace basisu } case texture_format::cBC3: { - return unpack_bc3(pBlock, pPixels); + // unpack_bc3 is void (always succeeds); fall through to the function's success return. + basist::bcu::unpack_bc3(pBlock, reinterpret_cast(pPixels)); + break; } case texture_format::cBC4: { // Unpack to R - unpack_bc4(pBlock, &pPixels[0].r, sizeof(color_rgba)); + basist::bcu::unpack_bc4(pBlock, &pPixels[0].r, sizeof(color_rgba)); break; } case texture_format::cBC5: { - unpack_bc5(pBlock, pPixels); + basist::bcu::unpack_bc5(pBlock, reinterpret_cast(pPixels)); break; } case texture_format::cBC7: @@ -1152,7 +1088,7 @@ namespace basisu case texture_format::cATC_RGBA_INTERPOLATED_ALPHA: { unpack_atc(static_cast(pBlock) + 8, pPixels); - unpack_bc4(pBlock, &pPixels[0].a, sizeof(color_rgba)); + basist::bcu::unpack_bc4(pBlock, &pPixels[0].a, sizeof(color_rgba)); break; } case texture_format::cFXT1_RGB: @@ -1304,7 +1240,10 @@ namespace basisu } assert((m_block_width <= cMaxBlockSize) && (m_block_height <= cMaxBlockSize)); + color_rgba pixels[cMaxBlockSize * cMaxBlockSize]; + + // Notice here we default all texels to (0,0,0,255) - some block unpackers like BC4/5 only write certain channels. for (uint32_t i = 0; i < cMaxBlockSize * cMaxBlockSize; i++) pixels[i] = g_black_color; @@ -1388,6 +1327,16 @@ namespace basisu KTX_COMPRESSED_SRGB_ALPHA_BPTC_UNORM = 0x8E8D, KTX_COMPRESSED_RGB_BPTC_SIGNED_FLOAT = 0x8E8E, KTX_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT = 0x8E8F, + + // sRGB GL enums, emitted when exporting an sRGB source (EXT_texture_sRGB / ETC2 / BPTC). + // BC7's sRGB enum is KTX_COMPRESSED_SRGB_ALPHA_BPTC_UNORM = 0x8E8D, already defined above. + KTX_COMPRESSED_SRGB_S3TC_DXT1_EXT = 0x8C4C, // BC1 RGB, sRGB + KTX_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT = 0x8C4F, // BC3, sRGB + KTX_COMPRESSED_SRGB8_ETC2 = 0x9275, // ETC2 RGB, sRGB + KTX_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279, // ETC2 RGBA, sRGB + KTX_COMPRESSED_SRGB_PVRTC_4BPPV1_EXT = 0x8A55, // PVRTC1 4bpp RGB, sRGB (EXT_pvrtc_sRGB) + KTX_COMPRESSED_SRGB_ALPHA_PVRTC_4BPPV1_EXT = 0x8A57, // PVRTC1 4bpp RGBA, sRGB (EXT_pvrtc_sRGB) + KTX_COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00, KTX_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02, @@ -1535,12 +1484,12 @@ namespace basisu case texture_format::cBC1_NV: case texture_format::cBC1_AMD: { - internal_fmt = KTX_COMPRESSED_RGB_S3TC_DXT1_EXT; + internal_fmt = !astc_srgb_flag ? KTX_COMPRESSED_RGB_S3TC_DXT1_EXT : KTX_COMPRESSED_SRGB_S3TC_DXT1_EXT; break; } case texture_format::cBC3: { - internal_fmt = KTX_COMPRESSED_RGBA_S3TC_DXT5_EXT; + internal_fmt = !astc_srgb_flag ? KTX_COMPRESSED_RGBA_S3TC_DXT5_EXT : KTX_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT; base_internal_fmt = KTX_RGBA; break; } @@ -1559,17 +1508,19 @@ namespace basisu case texture_format::cETC1: case texture_format::cETC1S: { + // ETC1 has no sRGB GL enum; it is always written as linear ETC1, never + // relabeled as ETC2. internal_fmt = KTX_ETC1_RGB8_OES; break; } case texture_format::cETC2_RGB: { - internal_fmt = KTX_COMPRESSED_RGB8_ETC2; + internal_fmt = !astc_srgb_flag ? KTX_COMPRESSED_RGB8_ETC2 : KTX_COMPRESSED_SRGB8_ETC2; break; } case texture_format::cETC2_RGBA: { - internal_fmt = KTX_COMPRESSED_RGBA8_ETC2_EAC; + internal_fmt = !astc_srgb_flag ? KTX_COMPRESSED_RGBA8_ETC2_EAC : KTX_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC; base_internal_fmt = KTX_RGBA; break; } @@ -1587,18 +1538,18 @@ namespace basisu } case texture_format::cBC7: { - internal_fmt = KTX_COMPRESSED_RGBA_BPTC_UNORM; + internal_fmt = !astc_srgb_flag ? KTX_COMPRESSED_RGBA_BPTC_UNORM : KTX_COMPRESSED_SRGB_ALPHA_BPTC_UNORM; base_internal_fmt = KTX_RGBA; break; } case texture_format::cPVRTC1_4_RGB: { - internal_fmt = KTX_COMPRESSED_RGB_PVRTC_4BPPV1_IMG; + internal_fmt = !astc_srgb_flag ? KTX_COMPRESSED_RGB_PVRTC_4BPPV1_IMG : KTX_COMPRESSED_SRGB_PVRTC_4BPPV1_EXT; break; } case texture_format::cPVRTC1_4_RGBA: { - internal_fmt = KTX_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG; + internal_fmt = !astc_srgb_flag ? KTX_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG : KTX_COMPRESSED_SRGB_ALPHA_PVRTC_4BPPV1_EXT; base_internal_fmt = KTX_RGBA; break; } @@ -2457,6 +2408,681 @@ namespace basisu return write_vec_to_file(pFilename, file_data); } - + + bool write_uncompressed_rgba32_dds(uint8_vec& dds_data, const basisu::vector& images, bool cubemap_flag, bool use_srgb_format) + { + dds_data.resize(0); + + if (!images.size()) + { + assert(0); + return false; + } + + // Determine the number of array slices (cubemap arrays pack 6 faces per slice). + uint32_t slices = 1; + if (cubemap_flag) + { + if ((images.size() % 6) != 0) + { + fprintf(stderr, "write_uncompressed_rgba32_dds: cubemap face count (%u) must be a multiple of 6\n", images.size_u32()); + return false; + } + slices = images.size_u32() / 6; + } + else + { + slices = images.size_u32(); + } + + // Validate consistent dimensions and a well-formed mip chain across all slices. + uint32_t width = 0, height = 0, total_levels = 0; + for (uint32_t slice_index = 0; slice_index < images.size(); slice_index++) + { + const image_vec& levels = images[slice_index]; + + if (!levels.size()) + { + fprintf(stderr, "write_uncompressed_rgba32_dds: slice %u has an empty mip chain\n", slice_index); + return false; + } + + if (!slice_index) + { + width = levels[0].get_width(); + height = levels[0].get_height(); + total_levels = levels.size_u32(); + + if (!width || !height) + { + fprintf(stderr, "write_uncompressed_rgba32_dds: zero-sized base image\n"); + return false; + } + } + else if ((width != levels[0].get_width()) || (height != levels[0].get_height()) || (total_levels != levels.size())) + { + fprintf(stderr, "write_uncompressed_rgba32_dds: all slices must share the same dimensions and mip count\n"); + return false; + } + + for (uint32_t level_index = 0; level_index < levels.size(); level_index++) + { + const uint32_t expected_w = maximum(1, width >> level_index); + const uint32_t expected_h = maximum(1, height >> level_index); + if ((levels[level_index].get_width() != expected_w) || (levels[level_index].get_height() != expected_h)) + { + fprintf(stderr, "write_uncompressed_rgba32_dds: malformed mip chain at slice %u, level %u\n", slice_index, level_index); + return false; + } + } + } + + if ((!total_levels) || (total_levels >= 32)) + { + fprintf(stderr, "write_uncompressed_rgba32_dds: invalid mip level count (%u)\n", total_levels); + return false; + } + + // Concatenate every (slice, level) image into a single contiguous, tightly + // packed buffer in face-then-mip order, and hand it to tinydds as "level 0". + // This mirrors the workaround in write_dds_file(): tinydds.h's writer doesn't + // lay out the per-level array/cubemap data correctly when given separate mip + // pointers, but the resulting on-disk DDS byte order is identical to this. + uint8_vec mipmaps[32]; + for (uint32_t slice_index = 0; slice_index < images.size(); slice_index++) + { + const image_vec& levels = images[slice_index]; + + for (uint32_t level_index = 0; level_index < levels.size(); level_index++) + { + const image& img = levels[level_index]; + const uint32_t w = img.get_width(), h = img.get_height(); + + // Copy row by row to honor the image's pitch, emitting tight w*4-byte rows. + for (uint32_t y = 0; y < h; y++) + append_vector(mipmaps[0], (const uint8_t*)(img.get_ptr() + (size_t)y * img.get_pitch()), w * sizeof(color_rgba)); + } + } + + // Write the DDS using tinydds (same invocation pattern as write_dds_file()). + TinyDDS_WriteCallbacks cbs; + cbs.error = [](void* user, char const* msg) { BASISU_NOTE_UNUSED(user); fprintf(stderr, "tinydds: %s\n", msg); }; + cbs.alloc = [](void* user, size_t size) -> void* { BASISU_NOTE_UNUSED(user); return malloc(size); }; + cbs.free = [](void* user, void* memory) { BASISU_NOTE_UNUSED(user); free(memory); }; + cbs.write = [](void* user, void const* buffer, size_t byteCount) { BASISU_NOTE_UNUSED(user); uint8_vec* pVec = (uint8_vec*)user; append_vector(*pVec, (const uint8_t*)buffer, byteCount); }; + + uint32_t mipmap_sizes[32]; + const void* mipmap_ptrs[32]; + + clear_obj(mipmap_sizes); + clear_obj(mipmap_ptrs); + + for (uint32_t i = 0; i < total_levels; i++) + { + mipmap_sizes[i] = mipmaps[i].size_in_bytes_u32(); + mipmap_ptrs[i] = mipmaps[i].get_ptr(); + } + + const uint32_t tinydds_fmt = use_srgb_format ? TDDS_R8G8B8A8_SRGB : TDDS_R8G8B8A8_UNORM; + + fmt_debug_printf("write_uncompressed_rgba32_dds: {}x{}, slices: {}, mipLevels: {}, cubemap_flag: {}, use_srgb_format: {}\n", + width, height, slices, total_levels, cubemap_flag, use_srgb_format); + + bool status = TinyDDS_WriteImage(&cbs, + &dds_data, + width, + height, + 1, + slices, + total_levels, + (TinyDDS_Format)tinydds_fmt, + cubemap_flag, + true, + mipmap_sizes, + mipmap_ptrs); + + if (!status) + { + fprintf(stderr, "write_uncompressed_rgba32_dds: TinyDDS_WriteImage failed\n"); + dds_data.resize(0); + return false; + } + + return true; + } + + // Shared by transcode_ktx2_to_dds()/transcode_ktx2_to_ktx(): transcodes every + // (array layer, cubemap face, mip level) of an already-start_transcoding()'d + // ktx2_transcoder to the block GPU format (fmt, with its basisu::texture_format + // equivalent bfmt) and builds the [slice][mip] gpu_image array the DDS/KTX + // writers consume. Slices are ordered layer-major then cubemap face. Returns + // false on any transcode failure. + static bool transcode_ktx2_to_block_image_slices(basist::ktx2_transcoder& transcoder, basist::transcoder_texture_format fmt, texture_format bfmt, basisu::vector& slices, uint32_t decode_flags) + { + using namespace basist; + + const uint32_t levels = transcoder.get_levels(); + const uint32_t faces = transcoder.get_faces(); + const uint32_t layers = maximum(1, transcoder.get_layers()); + + slices.resize(0); + slices.reserve(layers * faces); + + for (uint32_t layer = 0; layer < layers; layer++) + { + for (uint32_t face = 0; face < faces; face++) + { + gpu_image_vec mips; + mips.resize(levels); + + for (uint32_t level = 0; level < levels; level++) + { + ktx2_image_level_info li; + if (!transcoder.get_image_level_info(li, level, layer, face)) + { + error_printf("transcode_ktx2_to_block_image_slices: get_image_level_info(L%u, layer %u, face %u) failed\n", level, layer, face); + return false; + } + + gpu_image& gi = mips[level]; + gi.init(bfmt, li.m_orig_width, li.m_orig_height); + + if (!transcoder.transcode_image_level(level, layer, face, gi.get_ptr(), gi.get_total_blocks(), fmt, decode_flags)) + { + error_printf("transcode_ktx2_to_block_image_slices: transcode_image_level(L%u, layer %u, face %u) failed\n", level, layer, face); + return false; + } + } + + slices.push_back(mips); + } + } + + return true; + } + + bool transcode_ktx2_to_dds(basist::ktx2_transcoder& transcoder, basist::transcoder_texture_format fmt, uint8_vec& dds_data, int srgb_mode, uint32_t decode_flags) + { + using namespace basist; + + dds_data.resize(0); + + // Validate the requested output format: BC1/BC3/BC4/BC5/BC6H/BC7 or RGBA32 only. + const bool is_rgba32 = (fmt == transcoder_texture_format::cTFRGBA32); + const texture_format bfmt = basis_get_basisu_texture_format(fmt); + if ((!is_rgba32) && (!does_dds_support_format(bfmt))) + { + error_printf("transcode_ktx2_to_dds: output format \"%s\" can't be written to DDS (must be BC1/BC3/BC4/BC5/BC6H/BC7 or RGBA32)\n", basis_get_format_name(fmt)); + return false; + } + + if (!basis_is_format_supported(fmt, transcoder.get_basis_tex_format())) + { + error_printf("transcode_ktx2_to_dds: output format \"%s\" isn't a supported transcode target for this input texture\n", basis_get_format_name(fmt)); + return false; + } + + if (!transcoder.start_transcoding()) + { + error_printf("transcode_ktx2_to_dds: start_transcoding() failed\n"); + return false; + } + + const uint32_t levels = transcoder.get_levels(); + const uint32_t faces = transcoder.get_faces(); + const uint32_t layers = maximum(1, transcoder.get_layers()); + const bool cubemap_flag = (faces == 6); + // srgb_mode: -1 = auto (use the KTX2's transfer function), 0 = force linear, 1 = force sRGB. + const bool srgb = (srgb_mode < 0) ? transcoder.is_srgb() : (srgb_mode != 0); + + if ((!levels) || (!faces)) + { + error_printf("transcode_ktx2_to_dds: degenerate texture (levels=%u, faces=%u)\n", levels, faces); + return false; + } + + // Build the slice list ordered layer-major then face -- the layout + // write_dds_file() / write_uncompressed_rgba32_dds() expect for 2D, 2D arrays, + // cubemaps, and cubemap arrays (with or without mips). + if (is_rgba32) + { + basisu::vector slices; + slices.reserve(layers * faces); + + for (uint32_t layer = 0; layer < layers; layer++) + { + for (uint32_t face = 0; face < faces; face++) + { + image_vec mips; + mips.resize(levels); + + for (uint32_t level = 0; level < levels; level++) + { + ktx2_image_level_info li; + if (!transcoder.get_image_level_info(li, level, layer, face)) + { + error_printf("transcode_ktx2_to_dds: get_image_level_info(L%u, layer %u, face %u) failed\n", level, layer, face); + return false; + } + + image& img = mips[level]; + img.resize(li.m_orig_width, li.m_orig_height); + + // Pass the image's pitch explicitly so the result is correct regardless of row padding. + if (!transcoder.transcode_image_level(level, layer, face, img.get_ptr(), img.get_pitch() * img.get_height(), fmt, decode_flags, img.get_pitch(), img.get_height())) + { + error_printf("transcode_ktx2_to_dds: transcode_image_level(L%u, layer %u, face %u) to RGBA32 failed\n", level, layer, face); + return false; + } + } + + slices.push_back(mips); + } + } + + if (!write_uncompressed_rgba32_dds(dds_data, slices, cubemap_flag, srgb)) + { + error_printf("transcode_ktx2_to_dds: write_uncompressed_rgba32_dds() failed\n"); + return false; + } + } + else + { + basisu::vector slices; + if (!transcode_ktx2_to_block_image_slices(transcoder, fmt, bfmt, slices, decode_flags)) + return false; + + if (!write_dds_file(dds_data, slices, cubemap_flag, srgb)) + { + error_printf("transcode_ktx2_to_dds: write_dds_file() failed\n"); + return false; + } + } + + return true; + } + + bool does_ktx_support_format(texture_format fmt) + { + switch (fmt) + { + case texture_format::cBC1: + case texture_format::cBC1_NV: + case texture_format::cBC1_AMD: + case texture_format::cBC3: + case texture_format::cBC4: + case texture_format::cBC5: + case texture_format::cBC6HSigned: + case texture_format::cBC6HUnsigned: + case texture_format::cBC7: + case texture_format::cETC1: + case texture_format::cETC1S: + case texture_format::cETC2_RGB: + case texture_format::cETC2_RGBA: + case texture_format::cETC2_R11_EAC: + case texture_format::cETC2_RG11_EAC: + case texture_format::cPVRTC1_4_RGB: + case texture_format::cPVRTC1_4_RGBA: + case texture_format::cPVRTC2_4_RGBA: + case texture_format::cUASTC4x4: + case texture_format::cASTC_LDR_4x4: + case texture_format::cASTC_LDR_5x4: + case texture_format::cASTC_LDR_5x5: + case texture_format::cASTC_LDR_6x5: + case texture_format::cASTC_LDR_6x6: + case texture_format::cASTC_LDR_8x5: + case texture_format::cASTC_LDR_8x6: + case texture_format::cASTC_LDR_10x5: + case texture_format::cASTC_LDR_10x6: + case texture_format::cASTC_LDR_8x8: + case texture_format::cASTC_LDR_10x8: + case texture_format::cASTC_LDR_10x10: + case texture_format::cASTC_LDR_12x10: + case texture_format::cASTC_LDR_12x12: + case texture_format::cASTC_HDR_4x4: + case texture_format::cUASTC_HDR_4x4: + case texture_format::cASTC_HDR_6x6: + return true; + default: + break; + } + return false; + } + + bool transcode_ktx2_to_ktx(basist::ktx2_transcoder& transcoder, basist::transcoder_texture_format fmt, uint8_vec& ktx_data, int srgb_mode, uint32_t decode_flags) + { + using namespace basist; + + ktx_data.resize(0); + + // Validate: must be a compressed format the KTX1 writer supports. Uncompressed + // (RGBA32/half/float/9e5) is intentionally not supported here yet. + const texture_format bfmt = basis_get_basisu_texture_format(fmt); + if (!does_ktx_support_format(bfmt)) + { + error_printf("transcode_ktx2_to_ktx: output format \"%s\" can't be written to a compressed KTX file\n", basis_get_format_name(fmt)); + return false; + } + + if (!basis_is_format_supported(fmt, transcoder.get_basis_tex_format())) + { + error_printf("transcode_ktx2_to_ktx: output format \"%s\" isn't a supported transcode target for this input texture\n", basis_get_format_name(fmt)); + return false; + } + + if (!transcoder.start_transcoding()) + { + error_printf("transcode_ktx2_to_ktx: start_transcoding() failed\n"); + return false; + } + + const uint32_t levels = transcoder.get_levels(); + const uint32_t faces = transcoder.get_faces(); + const bool cubemap_flag = (faces == 6); + // srgb_mode: -1 = auto (KTX2's transfer function), 0 = force linear, 1 = force sRGB. + // Selects the sRGB GL enum variants in create_ktx_texture_file() for the formats + // that have them (BC1/BC3/BC7, ETC2, PVRTC1, ASTC LDR); ignored for formats with + // no sRGB variant (BC4/BC5/BC6H/ETC1/PVRTC2). + const bool srgb = (srgb_mode < 0) ? transcoder.is_srgb() : (srgb_mode != 0); + + if ((!levels) || (!faces)) + { + error_printf("transcode_ktx2_to_ktx: degenerate texture (levels=%u, faces=%u)\n", levels, faces); + return false; + } + + basisu::vector slices; + if (!transcode_ktx2_to_block_image_slices(transcoder, fmt, bfmt, slices, decode_flags)) + return false; + + if (!create_ktx_texture_file(ktx_data, slices, cubemap_flag, srgb)) + { + error_printf("transcode_ktx2_to_ktx: create_ktx_texture_file() failed\n"); + return false; + } + + return true; + } + + // Returns a friendly name for the common DDS formats we care about (BC1-7 and + // the usual LDR/HDR uncompressed variants); returns nullptr for anything else so + // the caller can fall back to printing the raw hex value. For --tinydds_info. + static const char* tinydds_format_to_string(uint32_t fmt) + { + switch (fmt) + { + case TDDS_BC1_RGBA_UNORM_BLOCK: return "BC1 (UNORM)"; + case TDDS_BC1_RGBA_SRGB_BLOCK: return "BC1 (sRGB)"; + case TDDS_BC2_UNORM_BLOCK: return "BC2 (UNORM)"; + case TDDS_BC2_SRGB_BLOCK: return "BC2 (sRGB)"; + case TDDS_BC3_UNORM_BLOCK: return "BC3 (UNORM)"; + case TDDS_BC3_SRGB_BLOCK: return "BC3 (sRGB)"; + case TDDS_BC4_UNORM_BLOCK: return "BC4 (UNORM)"; + case TDDS_BC4_SNORM_BLOCK: return "BC4 (SNORM)"; + case TDDS_BC5_UNORM_BLOCK: return "BC5 (UNORM)"; + case TDDS_BC5_SNORM_BLOCK: return "BC5 (SNORM)"; + case TDDS_BC6H_UFLOAT_BLOCK: return "BC6H (UF16, unsigned half)"; + case TDDS_BC6H_SFLOAT_BLOCK: return "BC6H (SF16, signed half)"; + case TDDS_BC7_UNORM_BLOCK: return "BC7 (UNORM)"; + case TDDS_BC7_SRGB_BLOCK: return "BC7 (sRGB)"; + case TDDS_R8G8B8A8_UNORM: return "RGBA32 (R8G8B8A8 UNORM)"; + case TDDS_R8G8B8A8_SRGB: return "RGBA32 (R8G8B8A8 sRGB)"; + case TDDS_B8G8R8A8_UNORM: return "BGRA32 (B8G8R8A8 UNORM)"; + case TDDS_B8G8R8A8_SRGB: return "BGRA32 (B8G8R8A8 sRGB)"; + case TDDS_R16G16B16A16_UNORM: return "RGBA64 (R16G16B16A16 UNORM)"; + case TDDS_R16G16B16A16_SFLOAT: return "RGBA HALF (R16G16B16A16 float)"; + case TDDS_R32G32B32A32_SFLOAT: return "RGBA FLOAT (R32G32B32A32 float)"; + case TDDS_R9G9B9E5_UFLOAT: return "RGB9E5 (shared exponent)"; + default: return nullptr; + } + } + + bool print_dds_info(const char* pFilename) + { + TinyDDS_Callbacks cbs; + cbs.errorFn = [](void* user, char const* msg) { BASISU_NOTE_UNUSED(user); fprintf(stderr, "tinydds: %s\n", msg); }; + cbs.allocFn = [](void* user, size_t size) -> void* { BASISU_NOTE_UNUSED(user); return malloc(size); }; + cbs.freeFn = [](void* user, void* memory) { BASISU_NOTE_UNUSED(user); free(memory); }; + cbs.readFn = [](void* user, void* buffer, size_t byteCount) -> size_t { return (size_t)fread(buffer, 1, byteCount, (FILE*)user); }; +#ifdef _MSC_VER + cbs.seekFn = [](void* user, int64_t ofs) -> bool { return _fseeki64((FILE*)user, ofs, SEEK_SET) == 0; }; + cbs.tellFn = [](void* user) -> int64_t { return _ftelli64((FILE*)user); }; +#else + cbs.seekFn = [](void* user, int64_t ofs) -> bool { return fseek((FILE*)user, (long)ofs, SEEK_SET) == 0; }; + cbs.tellFn = [](void* user) -> int64_t { return (int64_t)ftell((FILE*)user); }; +#endif + + FILE* pFile = fopen_safe(pFilename, "rb"); + if (!pFile) + { + error_printf("Can't open .DDS file \"%s\"\n", pFilename); + return false; + } + + TinyDDS_ContextHandle ctx = TinyDDS_CreateContext(&cbs, pFile); + if (!ctx) + { + error_printf("Failed creating tinydds context for \"%s\"\n", pFilename); + fclose(pFile); + return false; + } + + if (!TinyDDS_ReadHeader(ctx)) + { + error_printf("Failed parsing DDS header in file \"%s\"\n", pFilename); + TinyDDS_DestroyContext(ctx); + fclose(pFile); + return false; + } + + const uint32_t w = TinyDDS_Width(ctx); + const uint32_t h = TinyDDS_Height(ctx); + const uint32_t depth = TinyDDS_Depth(ctx); + const uint32_t slices = TinyDDS_ArraySlices(ctx); + const uint32_t mips = TinyDDS_NumberOfMipmaps(ctx); + const bool is_cube = TinyDDS_IsCubemap(ctx); + const bool is_array = TinyDDS_IsArray(ctx); + const bool is_3d = TinyDDS_Is3D(ctx); + const bool is_1d = TinyDDS_Is1D(ctx); + const uint32_t fmt = (uint32_t)TinyDDS_GetFormat(ctx); + + const char* pType = + is_cube ? ((slices > 1) ? "Cubemap array" : "Cubemap") : + is_3d ? "3D (volume)" : + is_1d ? ((slices > 1) ? "1D array" : "1D") : + ((slices > 1) ? "2D array" : "2D"); + + const char* pFmtStr = tinydds_format_to_string(fmt); + + printf("DDS file: %s\n", pFilename); + printf(" Texture type : %s\n", pType); + if (is_3d) + printf(" Dimensions : %ux%ux%u (WxHxDepth)\n", w, h, depth); + else + printf(" Dimensions : %ux%u\n", w, h); + printf(" Mip levels : %u\n", mips); + // tinydds reports 0 array slices for a non-array texture; show 1 for clarity. + const uint32_t num_slices = maximum(1, slices); + if (is_cube) + printf(" Array slices : %u cube(s) x 6 faces = %u image(s) per mip\n", num_slices, num_slices * 6); + else + printf(" Array slices : %u\n", num_slices); + printf(" Cubemap : %s\n", is_cube ? "yes" : "no"); + // "Array" reflects a genuine multi-element array. tinydds's IsArray() is true for ANY DX10 file + // (arraySize>=1), so without care a plain single 2D texture or a single cubemap would falsely read + // "yes". For a cubemap, array-ness means more than one cube (slices > 1). For a 2D texture, annotate + // the DX10 1-element quirk ("yes (1 element)") rather than contradict "Texture type : 2D". + if (is_cube) + printf(" Array : %s\n", (num_slices > 1) ? "yes" : "no"); + else if (is_array && (num_slices == 1)) + printf(" Array : yes (1 element)\n"); + else + printf(" Array : %s\n", is_array ? "yes" : "no"); + if (pFmtStr) + printf(" Format : %s [tinydds %u / 0x%X]\n", pFmtStr, fmt, fmt); + else + printf(" Format : 0x%X [tinydds %u]\n", fmt, fmt); + + TinyDDS_DestroyContext(ctx); + fclose(pFile); + + return true; + } + + // Friendly name for the KTX1 glInternalFormat GL enums we care about (the set + // create_ktx_texture_file() writes, plus a few common extras). nullptr otherwise + // so the caller prints the raw hex. For --ktx_info. + static const char* ktx_gl_internal_format_name(uint32_t fmt) + { + switch (fmt) + { + case KTX_RGB: return "RGB (uncompressed)"; + case KTX_RGBA: return "RGBA (uncompressed)"; + case KTX_RED: return "RED (uncompressed)"; + case KTX_RG: return "RG (uncompressed)"; + case KTX_ETC1_RGB8_OES: return "ETC1 RGB8"; + case KTX_COMPRESSED_RGB_S3TC_DXT1_EXT: return "BC1 (DXT1 RGB)"; + case KTX_COMPRESSED_SRGB_S3TC_DXT1_EXT: return "BC1 (DXT1 RGB sRGB)"; + case KTX_COMPRESSED_RGBA_S3TC_DXT5_EXT: return "BC3 (DXT5 RGBA)"; + case KTX_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT: return "BC3 (DXT5 RGBA sRGB)"; + case KTX_COMPRESSED_RED_RGTC1_EXT: return "BC4 (RGTC1 R)"; + case KTX_COMPRESSED_RED_GREEN_RGTC2_EXT: return "BC5 (RGTC2 RG)"; + case KTX_COMPRESSED_RGB8_ETC2: return "ETC2 RGB8"; + case KTX_COMPRESSED_SRGB8_ETC2: return "ETC2 RGB8 sRGB"; + case KTX_COMPRESSED_RGBA8_ETC2_EAC: return "ETC2 RGBA8 EAC"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC: return "ETC2 RGBA8 EAC sRGB"; + case KTX_COMPRESSED_RGBA_BPTC_UNORM: return "BC7 (BPTC UNORM)"; + case KTX_COMPRESSED_SRGB_ALPHA_BPTC_UNORM: return "BC7 (BPTC sRGB)"; + case KTX_COMPRESSED_RGB_BPTC_SIGNED_FLOAT: return "BC6H (signed float)"; + case KTX_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT: return "BC6H (unsigned float)"; + case KTX_COMPRESSED_RGB_PVRTC_4BPPV1_IMG: return "PVRTC1 4bpp RGB"; + case KTX_COMPRESSED_SRGB_PVRTC_4BPPV1_EXT: return "PVRTC1 4bpp RGB sRGB"; + case KTX_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG: return "PVRTC1 4bpp RGBA"; + case KTX_COMPRESSED_SRGB_ALPHA_PVRTC_4BPPV1_EXT: return "PVRTC1 4bpp RGBA sRGB"; + case KTX_COMPRESSED_RGBA_PVRTC_4BPPV2_IMG: return "PVRTC2 4bpp RGBA"; + case KTX_COMPRESSED_R11_EAC: return "EAC R11"; + case KTX_COMPRESSED_RG11_EAC: return "EAC RG11"; + case KTX_COMPRESSED_RGBA_UASTC_4x4_KHR: return "UASTC 4x4"; + case KTX_ATC_RGB_AMD: return "ATC RGB"; + case KTX_ATC_RGBA_INTERPOLATED_ALPHA_AMD: return "ATC RGBA (interpolated alpha)"; + case KTX_COMPRESSED_RGB_FXT1_3DFX: return "FXT1 RGB"; + case KTX_COMPRESSED_RGBA_FXT1_3DFX: return "FXT1 RGBA"; + case KTX_COMPRESSED_RGBA_ASTC_4x4_KHR: return "ASTC 4x4"; + case KTX_COMPRESSED_RGBA_ASTC_5x4_KHR: return "ASTC 5x4"; + case KTX_COMPRESSED_RGBA_ASTC_5x5_KHR: return "ASTC 5x5"; + case KTX_COMPRESSED_RGBA_ASTC_6x5_KHR: return "ASTC 6x5"; + case KTX_COMPRESSED_RGBA_ASTC_6x6_KHR: return "ASTC 6x6"; + case KTX_COMPRESSED_RGBA_ASTC_8x5_KHR: return "ASTC 8x5"; + case KTX_COMPRESSED_RGBA_ASTC_8x6_KHR: return "ASTC 8x6"; + case KTX_COMPRESSED_RGBA_ASTC_8x8_KHR: return "ASTC 8x8"; + case KTX_COMPRESSED_RGBA_ASTC_10x5_KHR: return "ASTC 10x5"; + case KTX_COMPRESSED_RGBA_ASTC_10x6_KHR: return "ASTC 10x6"; + case KTX_COMPRESSED_RGBA_ASTC_10x8_KHR: return "ASTC 10x8"; + case KTX_COMPRESSED_RGBA_ASTC_10x10_KHR: return "ASTC 10x10"; + case KTX_COMPRESSED_RGBA_ASTC_12x10_KHR: return "ASTC 12x10"; + case KTX_COMPRESSED_RGBA_ASTC_12x12_KHR: return "ASTC 12x12"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR: return "ASTC 4x4 sRGB"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR: return "ASTC 5x4 sRGB"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR: return "ASTC 5x5 sRGB"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR: return "ASTC 6x5 sRGB"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR: return "ASTC 6x6 sRGB"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR: return "ASTC 8x5 sRGB"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR: return "ASTC 8x6 sRGB"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR: return "ASTC 8x8 sRGB"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR: return "ASTC 10x5 sRGB"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR: return "ASTC 10x6 sRGB"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR: return "ASTC 10x8 sRGB"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR: return "ASTC 10x10 sRGB"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR: return "ASTC 12x10 sRGB"; + case KTX_COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR: return "ASTC 12x12 sRGB"; + default: return nullptr; + } + } + + bool print_ktx_info(const char* pFilename) + { + FILE* pFile = fopen_safe(pFilename, "rb"); + if (!pFile) + { + error_printf("Can't open .ktx file \"%s\"\n", pFilename); + return false; + } + + ktx_header hdr; + hdr.clear(); + const size_t n = fread(&hdr, 1, sizeof(hdr), pFile); + fclose(pFile); + + if (n != sizeof(hdr)) + { + error_printf("Failed reading KTX header from \"%s\" (file too small)\n", pFilename); + return false; + } + + if (memcmp(hdr.m_identifier, g_ktx_file_id, sizeof(g_ktx_file_id)) != 0) + { + error_printf("File \"%s\" is not a KTX1 file (bad identifier)\n", pFilename); + return false; + } + + // The endianness field is 0x04030201 stored in the file's byte order. If we + // read back KTX_ENDIAN the file matches our (little-endian) byte order; if we + // read KTX_OPPOSITE_ENDIAN every uint32 field must be byte-swapped. + const uint32_t endianness = hdr.m_endianness; + bool swap; + if (endianness == KTX_ENDIAN) + swap = false; + else if (endianness == KTX_OPPOSITE_ENDIAN) + swap = true; + else + { + error_printf("File \"%s\" has an invalid KTX endianness field (0x%08X)\n", pFilename, endianness); + return false; + } + + // packed_uint<4> yields a little-endian uint32; swap it for an opposite-endian file. + auto rd = [swap](uint32_t v) -> uint32_t + { + if (!swap) + return v; + return ((v >> 24) & 0xFFu) | ((v >> 8) & 0xFF00u) | ((v << 8) & 0xFF0000u) | ((v << 24) & 0xFF000000u); + }; + + const uint32_t gl_type = rd(hdr.m_glType); + const uint32_t gl_type_size = rd(hdr.m_glTypeSize); + const uint32_t gl_format = rd(hdr.m_glFormat); + const uint32_t gl_internal_format = rd(hdr.m_glInternalFormat); + const uint32_t gl_base_internal_format = rd(hdr.m_glBaseInternalFormat); + const uint32_t pixel_width = rd(hdr.m_pixelWidth); + const uint32_t pixel_height = rd(hdr.m_pixelHeight); + const uint32_t pixel_depth = rd(hdr.m_pixelDepth); + const uint32_t array_elements = rd(hdr.m_numberOfArrayElements); + const uint32_t faces = rd(hdr.m_numberOfFaces); + const uint32_t mip_levels = rd(hdr.m_numberOfMipmapLevels); + const uint32_t kvd_bytes = rd(hdr.m_bytesOfKeyValueData); + + const char* pName = ktx_gl_internal_format_name(gl_internal_format); + + printf("KTX1 file: %s\n", pFilename); + printf(" Endianness : 0x%08X (%s)\n", endianness, swap ? "opposite -- byte-swapped" : "native"); + printf(" glType : 0x%X (%u)%s\n", gl_type, gl_type, gl_type ? "" : " (0 = compressed)"); + printf(" glTypeSize : %u\n", gl_type_size); + printf(" glFormat : 0x%X (%u)%s\n", gl_format, gl_format, gl_format ? "" : " (0 = compressed)"); + if (pName) + printf(" glInternalFormat : 0x%X (%u) %s\n", gl_internal_format, gl_internal_format, pName); + else + printf(" glInternalFormat : 0x%X (%u)\n", gl_internal_format, gl_internal_format); + printf(" glBaseInternalFormat : 0x%X (%u)\n", gl_base_internal_format, gl_base_internal_format); + printf(" pixelWidth : %u\n", pixel_width); + printf(" pixelHeight : %u%s\n", pixel_height, pixel_height ? "" : " (1D texture)"); + printf(" pixelDepth : %u%s\n", pixel_depth, pixel_depth ? "" : " (non-3D)"); + printf(" numberOfArrayElements : %u%s\n", array_elements, array_elements ? "" : " (not an array)"); + printf(" numberOfFaces : %u%s\n", faces, (faces == 6) ? " (cubemap)" : ""); + printf(" numberOfMipmapLevels : %u\n", mip_levels); + printf(" bytesOfKeyValueData : %u\n", kvd_bytes); + + return true; + } + } // basisu diff --git a/encoder/basisu_gpu_texture.h b/encoder/basisu_gpu_texture.h index bcfc9cb..495d596 100644 --- a/encoder/basisu_gpu_texture.h +++ b/encoder/basisu_gpu_texture.h @@ -17,6 +17,17 @@ #include "../transcoder/basisu_astc_helpers.h" #include "basisu_etc.h" +// Forward declarations for transcode_ktx2_to_dds() below. The full transcoder +// header is only included by basisu_gpu_texture.cpp, keeping this header light. +// (transcoder_texture_format is a plain int-backed enum class, so an opaque +// forward declaration is valid here.) +namespace basist +{ + class ktx2_transcoder; + enum class transcoder_texture_format; + enum class dds_format; +} + namespace basisu { // GPU texture "image" @@ -125,17 +136,82 @@ namespace basisu typedef basisu::vector gpu_image_vec; + // A mip chain (or single image) of uncompressed RGBA images. Used by + // write_uncompressed_rgba32_dds() as one array slice / cubemap face. + typedef basisu::vector image_vec; + // KTX1 file writing - compatible with ARM's astcenc tool, and some other tools. // Note astc_linear_flag used to be always effectively true in older code. It's ignored for ASTC HDR formats. bool create_ktx_texture_file(uint8_vec &ktx_data, const basisu::vector& gpu_images, bool cubemap_flag, bool astc_srgb_flag); bool does_dds_support_format(texture_format fmt); + + // Returns true if create_ktx_texture_file() can write this (compressed) texture + // format. Mirrors that writer's supported set, minus the formats we don't expose + // for KTX export right now (ATC, FXT1, and all uncompressed formats). + bool does_ktx_support_format(texture_format fmt); bool write_dds_file(uint8_vec& dds_data, const basisu::vector& gpu_images, bool cubemap_flag, bool use_srgb_format); bool write_dds_file(const char* pFilename, const basisu::vector& gpu_images, bool cubemap_flag, bool use_srgb_format); + // Writes uncompressed 32-bit RGBA image data to an in-memory .DDS blob. + // images is indexed [array-slice][mip level]. For cubemaps and cubemap arrays + // the outer slice count must be a multiple of 6, ordered layer-major then the + // six faces, and cubemap_flag must be true; for 2D textures and 2D arrays + // cubemap_flag is false and the outer count is the array size (1 for a plain + // 2D texture). Every slice must have the same mip count and the same level-0 + // dimensions, and each mip level must be floor(prev/2) (min 1). Supports 2D, + // 2D arrays, cubemaps, and cubemap arrays, with or without a mip chain. + // Validates all of the above; returns false (and clears dds_data) on any + // inconsistency or write failure. Reusable on its own (a sibling to + // write_dds_file for the uncompressed RGBA case). + bool write_uncompressed_rgba32_dds(uint8_vec& dds_data, const basisu::vector& images, bool cubemap_flag, bool use_srgb_format); + + // Transcodes the entire contents of an init()'d ktx2_transcoder (every mip + // level, array layer, and cubemap face) to fmt and serializes a Microsoft + // .DDS (DirectDraw Surface) blob into dds_data. fmt MUST be one of the DirectX + // BC formats writable to DDS (cTFBC1_RGB, cTFBC3_RGBA, cTFBC4_R, cTFBC5_RG, + // cTFBC6H, cTFBC7_RGBA) or uncompressed cTFRGBA32; any other format fails. + // Supports 2D, 2D arrays, cubemaps, and cubemap arrays (with or without mips). + // The transcoder need only have been init()'d -- this calls start_transcoding() + // itself. Returns false (and clears dds_data) on any error. + // srgb_mode selects whether the sRGB DDS format variants are used: -1 = auto + // (follow the KTX2's transfer function via transcoder.is_srgb()), 0 = force + // linear/UNORM, 1 = force sRGB. Ignored for formats that have no sRGB variant + // (BC4/BC5/BC6H/etc.). decode_flags is passed straight to + // ktx2_transcoder::transcode_image_level() (a cDecodeFlags* bitmask), letting the + // caller control the transcode -- e.g. force/disable deblocking, high quality, etc. + bool transcode_ktx2_to_dds(basist::ktx2_transcoder& transcoder, basist::transcoder_texture_format fmt, uint8_vec& dds_data, int srgb_mode = -1, uint32_t decode_flags = 0); + + // Like transcode_ktx2_to_dds(), but serializes a KTX1 (.ktx) file instead, via + // create_ktx_texture_file(). COMPRESSED formats only -- BC1-7, ETC1/ETC2, + // ETC2 EAC R11/RG11, PVRTC1, PVRTC2 (RGBA), ASTC LDR/HDR, UASTC; uncompressed + // (RGBA32/half/float) is not supported here yet. fmt must pass + // does_ktx_support_format(). Supports 2D, 2D arrays, cubemaps, and cubemap + // arrays (with or without mips). srgb_mode is as above; it selects the sRGB GL + // enum variants for the formats that have them (BC1/BC3/BC7, ETC2, PVRTC1, ASTC LDR) + // and is ignored for formats with no sRGB variant (BC4/BC5/BC6H/ETC1/PVRTC2). decode_flags is + // passed straight to transcode_image_level() (a cDecodeFlags* bitmask). The + // transcoder need only have been init()'d. Returns false (and clears ktx_data) + // on any error. + bool transcode_ktx2_to_ktx(basist::ktx2_transcoder& transcoder, basist::transcoder_texture_format fmt, uint8_vec& ktx_data, int srgb_mode = -1, uint32_t decode_flags = 0); + // Currently reads 2D 32bpp RGBA, 16-bit HALF RGBA, or 32-bit FLOAT RGBA, with or without mipmaps. No tex arrays or cubemaps, yet. bool read_uncompressed_dds_file(const char* pFilename, basisu::vector& ldr_mips, basisu::vector& hdr_mips); + // Cracks open a .DDS file with tinydds (header only -- no pixel decode) and + // prints its high-level info to stdout: texture type (2D / 2D array / cubemap / + // cubemap array / 3D), dimensions, mip level count, array slice count, and the + // format (a friendly name for BC1-7 and the common LDR/HDR uncompressed formats, + // otherwise the raw hex value). Intended as a quick development sanity check. + bool print_dds_info(const char* pFilename); + + // Reads a KTX1 (.ktx) file's 64-byte header (endian-swapping the fields if the + // file's endianness marker says so) and prints them to stdout: GL type/format/ + // internalFormat (+ a friendly name for the common compressed formats), + // dimensions, array elements, faces, mip levels, and key-value-data size. Header + // only -- it does not parse the key/value data or image data. Development aid. + bool print_ktx_info(const char* pFilename); + // Supports DDS and KTX bool write_compressed_texture_file(const char *pFilename, const basisu::vector& g, bool cubemap_flag, bool use_srgb_format); bool write_compressed_texture_file(const char* pFilename, const gpu_image_vec& g, bool use_srgb_format); @@ -143,21 +219,15 @@ namespace basisu bool write_3dfx_out_file(const char* pFilename, const gpu_image& gi); + // Returns the ASCII name of a texture_format enum value, e.g. "BC7", "ASTC_LDR_4x4", "ETC1". + const char* get_texture_format_name(texture_format fmt); + + // Human-readable name for the exact physical format stored in a .DDS file (basist::dds_transcoder::get_dds_format()). + const char* get_dds_format_string(basist::dds_format fmt); + // GPU texture block unpacking // For ETC1, use in basisu_etc.h: bool unpack_etc1(const etc_block& block, color_rgba *pDst, bool preserve_alpha) void unpack_etc2_eac(const void *pBlock_bits, color_rgba *pPixels); - bool unpack_bc1(const void *pBlock_bits, color_rgba *pPixels, bool set_alpha); - void unpack_bc4(const void *pBlock_bits, uint8_t *pPixels, uint32_t stride); - bool unpack_bc3(const void *pBlock_bits, color_rgba *pPixels); - void unpack_bc5(const void *pBlock_bits, color_rgba *pPixels); - -#if 0 - bool unpack_bc7_mode6(const void *pBlock_bits, color_rgba *pPixels); - int determine_bc7_mode(const void* pBlock); - int determine_bc7_mode_4_index_mode(const void* pBlock); - int determine_bc7_mode_4_or_5_rotation(const void* pBlock); - bool unpack_bc7(const void* pBlock_bits, color_rgba* pPixels); // full format -#endif bool unpack_bc6h(const void* pSrc_block, void* pDst_block, bool is_signed, uint32_t dest_pitch_in_halfs = 4 * 3); // full format, outputs HALF values, RGB texels only (not RGBA) void unpack_atc(const void* pBlock_bits, color_rgba* pPixels); diff --git a/encoder/basisu_math.h b/encoder/basisu_math.h index 24b8385..9128ab6 100644 --- a/encoder/basisu_math.h +++ b/encoder/basisu_math.h @@ -1,6 +1,16 @@ // File: basisu_math.h #pragma once +// heavily uses static_assert, may not want this in regular builds +#define BASISU_FIXED_TESTS + +// The fixed-point helpers were moved to namespace basist (in the transcoder). +// The self-test at the bottom of this file still exercises them, so pull them +// in -- but only when the test is actually compiled. +#ifdef BASISU_FIXED_TESTS +#include "../transcoder/basisu_transcoder_internal.h" +#endif + // TODO: Would prefer this in the basisu namespace, but to avoid collisions with the existing vec/matrix classes I'm placing this in "bu_math". namespace bu_math { @@ -2470,6 +2480,1057 @@ namespace bu_math return result; } + // Dynamic (heap-allocated) vector and matrix classes, templated on scalar type only. + // Row-major matrix storage, compatible with the static bu_math::vec/matrix classes. + // ----------------------------------------------------------------------- + // dynamic_vec - dynamically-sized vector using basisu::vector + // ----------------------------------------------------------------------- + template + class dynamic_vec + { + static_assert(std::is_floating_point::value, "dynamic_vec only supports float and double"); + + public: + typedef T scalar_type; + + inline dynamic_vec() + { + } + + inline explicit dynamic_vec(uint32_t n) : m_v(n) + { + clear(); + } + + inline dynamic_vec(uint32_t n, const T* pValues) : m_v(n) + { + for (uint32_t i = 0; i < n; i++) + m_v[i] = pValues[i]; + } + + static inline dynamic_vec make_filled(uint32_t n, T fill_val) + { + dynamic_vec result(n); + result.set_all(fill_val); + return result; + } + + // Conversion from static vec + template + inline explicit dynamic_vec(const vec& other) : m_v(N) + { + for (uint32_t i = 0; i < N; i++) + m_v[i] = static_cast(other[i]); + } + + // Assign from static vec + template + inline dynamic_vec& operator=(const vec& other) + { + m_v.resize(N); + for (uint32_t i = 0; i < N; i++) + m_v[i] = static_cast(other[i]); + return *this; + } + + // Convert to static vec (truncates or zero-pads as needed) + template + inline vec to_vec() const + { + vec result(basisu::cClear); + const uint32_t m = basisu::minimum(size(), N); + for (uint32_t i = 0; i < m; i++) + result[i] = static_cast(m_v[i]); + return result; + } + + inline void resize(uint32_t n) + { + m_v.resize(n); + } + + inline uint32_t size() const { return (uint32_t)m_v.size(); } + inline bool is_empty() const { return m_v.empty(); } + + inline dynamic_vec& clear() + { + for (uint32_t i = 0; i < size(); i++) + m_v[i] = static_cast(0); + return *this; + } + + inline dynamic_vec& set_all(T val) + { + for (uint32_t i = 0; i < size(); i++) + m_v[i] = val; + return *this; + } + + inline T operator[](uint32_t i) const + { + assert(i < size()); + return m_v[i]; + } + + inline T& operator[](uint32_t i) + { + assert(i < size()); + return m_v[i]; + } + + inline const T* get_ptr() const { return m_v.empty() ? nullptr : &m_v[0]; } + inline T* get_ptr() { return m_v.empty() ? nullptr : &m_v[0]; } + + inline bool operator==(const dynamic_vec& rhs) const + { + if (size() != rhs.size()) + return false; + for (uint32_t i = 0; i < size(); i++) + if (!(m_v[i] == rhs.m_v[i])) + return false; + return true; + } + + inline bool operator!=(const dynamic_vec& rhs) const + { + return !(*this == rhs); + } + + // Unary + inline dynamic_vec operator-() const + { + const uint32_t n = size(); + dynamic_vec result(n); + for (uint32_t i = 0; i < n; i++) + result.m_v[i] = -m_v[i]; + return result; + } + + inline dynamic_vec operator+() const + { + return *this; + } + + // Compound assignment: vec op vec + inline dynamic_vec& operator+=(const dynamic_vec& rhs) + { + assert(size() == rhs.size()); + for (uint32_t i = 0; i < size(); i++) + m_v[i] += rhs.m_v[i]; + return *this; + } + + inline dynamic_vec& operator-=(const dynamic_vec& rhs) + { + assert(size() == rhs.size()); + for (uint32_t i = 0; i < size(); i++) + m_v[i] -= rhs.m_v[i]; + return *this; + } + + inline dynamic_vec& operator*=(const dynamic_vec& rhs) + { + assert(size() == rhs.size()); + for (uint32_t i = 0; i < size(); i++) + m_v[i] *= rhs.m_v[i]; + return *this; + } + + inline dynamic_vec& operator/=(const dynamic_vec& rhs) + { + assert(size() == rhs.size()); + for (uint32_t i = 0; i < size(); i++) + m_v[i] /= rhs.m_v[i]; + return *this; + } + + // Compound assignment: vec op scalar + inline dynamic_vec& operator*=(T s) + { + for (uint32_t i = 0; i < size(); i++) + m_v[i] *= s; + return *this; + } + + inline dynamic_vec& operator/=(T s) + { + for (uint32_t i = 0; i < size(); i++) + m_v[i] /= s; + return *this; + } + + // Binary: vec op vec + friend inline dynamic_vec operator+(const dynamic_vec& a, const dynamic_vec& b) + { + assert(a.size() == b.size()); + const uint32_t n = a.size(); + dynamic_vec result(n); + for (uint32_t i = 0; i < n; i++) + result.m_v[i] = a.m_v[i] + b.m_v[i]; + return result; + } + + friend inline dynamic_vec operator-(const dynamic_vec& a, const dynamic_vec& b) + { + assert(a.size() == b.size()); + const uint32_t n = a.size(); + dynamic_vec result(n); + for (uint32_t i = 0; i < n; i++) + result.m_v[i] = a.m_v[i] - b.m_v[i]; + return result; + } + + // Binary: vec op scalar, scalar op vec + friend inline dynamic_vec operator*(const dynamic_vec& a, T s) + { + const uint32_t n = a.size(); + dynamic_vec result(n); + for (uint32_t i = 0; i < n; i++) + result.m_v[i] = a.m_v[i] * s; + return result; + } + + friend inline dynamic_vec operator*(T s, const dynamic_vec& a) + { + return a * s; + } + + friend inline dynamic_vec operator/(const dynamic_vec& a, T s) + { + const uint32_t n = a.size(); + dynamic_vec result(n); + for (uint32_t i = 0; i < n; i++) + result.m_v[i] = a.m_v[i] / s; + return result; + } + + // Dot product + static inline T dot(const dynamic_vec& a, const dynamic_vec& b) + { + assert(a.size() == b.size()); + T result = 0; + for (uint32_t i = 0; i < a.size(); i++) + result += a.m_v[i] * b.m_v[i]; + return result; + } + + inline T dot(const dynamic_vec& rhs) const + { + return dot(*this, rhs); + } + + // Squared length (norm) + inline T norm() const + { + T sum = 0; + for (uint32_t i = 0; i < size(); i++) + sum += m_v[i] * m_v[i]; + return sum; + } + + inline T length() const + { + return sqrt(norm()); + } + + // Returns squared length. If zero length, optionally sets to default. + inline double normalize(const dynamic_vec* pDefault = nullptr) + { + double n = 0; + for (uint32_t i = 0; i < size(); i++) + n += (double)m_v[i] * (double)m_v[i]; + + if (n != 0.0) + *this *= static_cast(1.0 / sqrt(n)); + else if (pDefault) + *this = *pDefault; + return n; + } + + inline dynamic_vec& normalize_in_place(const dynamic_vec* pDefault = nullptr) + { + normalize(pDefault); + return *this; + } + + inline dynamic_vec get_normalized(const dynamic_vec* pDefault = nullptr) const + { + dynamic_vec result(*this); + result.normalize(pDefault); + return result; + } + + inline T squared_distance(const dynamic_vec& rhs) const + { + assert(size() == rhs.size()); + T dist2 = 0; + for (uint32_t i = 0; i < size(); i++) + { + T d = m_v[i] - rhs.m_v[i]; + dist2 += d * d; + } + return dist2; + } + + inline T distance(const dynamic_vec& rhs) const + { + return sqrt(squared_distance(rhs)); + } + + inline dynamic_vec& clamp(T lo, T hi) + { + for (uint32_t i = 0; i < size(); i++) + m_v[i] = basisu::clamp(m_v[i], lo, hi); + return *this; + } + + inline dynamic_vec& saturate() + { + return clamp(static_cast(0), static_cast(1)); + } + + static inline dynamic_vec component_min(const dynamic_vec& a, const dynamic_vec& b) + { + assert(a.size() == b.size()); + const uint32_t n = a.size(); + dynamic_vec result(n); + for (uint32_t i = 0; i < n; i++) + result.m_v[i] = basisu::minimum(a.m_v[i], b.m_v[i]); + return result; + } + + static inline dynamic_vec component_max(const dynamic_vec& a, const dynamic_vec& b) + { + assert(a.size() == b.size()); + const uint32_t n = a.size(); + dynamic_vec result(n); + for (uint32_t i = 0; i < n; i++) + result.m_v[i] = basisu::maximum(a.m_v[i], b.m_v[i]); + return result; + } + + static inline dynamic_vec lerp(const dynamic_vec& a, const dynamic_vec& b, T t) + { + assert(a.size() == b.size()); + const uint32_t n = a.size(); + dynamic_vec result(n); + for (uint32_t i = 0; i < n; i++) + result.m_v[i] = a.m_v[i] + (b.m_v[i] - a.m_v[i]) * t; + return result; + } + + inline bool equal_tol(const dynamic_vec& b, T t) const + { + if (size() != b.size()) + return false; + for (uint32_t i = 0; i < size(); i++) + if (!basisu::equal_tol(m_v[i], b.m_v[i], t)) + return false; + return true; + } + + void print() const + { + printf("[ "); + for (uint32_t i = 0; i < size(); i++) + printf("%3.3f ", (double)m_v[i]); + printf("]\n"); + } + + inline void swap(dynamic_vec& other) + { + m_v.swap(other.m_v); + } + + private: + basisu::vector m_v; + }; + + // ADL swap for dynamic_vec + template + inline void swap(dynamic_vec& a, dynamic_vec& b) + { + a.swap(b); + } + + typedef dynamic_vec dynamic_vecF; + typedef dynamic_vec dynamic_vecD; + + // ----------------------------------------------------------------------- + // dynamic_matrix - dynamically-sized row-major matrix using basisu::vector + // ----------------------------------------------------------------------- + template + class dynamic_matrix + { + static_assert(std::is_floating_point::value, "dynamic_matrix only supports float and double"); + + public: + typedef T scalar_type; + + inline dynamic_matrix() : m_rows(0), m_cols(0) + { + } + + inline dynamic_matrix(uint32_t rows, uint32_t cols) : m_rows(rows), m_cols(cols), m_v(rows * cols) + { + clear(); + } + + inline dynamic_matrix(uint32_t rows, uint32_t cols, T fill_val) : m_rows(rows), m_cols(cols), m_v(rows * cols) + { + set_all(fill_val); + } + + inline dynamic_matrix(uint32_t rows, uint32_t cols, const T *pInit) : m_rows(0), m_cols(0) + { + resize(rows, cols, pInit); + } + + // Conversion from static matrix + template + inline explicit dynamic_matrix(const matrix& other) : m_rows(R), m_cols(C), m_v(R * C) + { + for (uint32_t r = 0; r < R; r++) + for (uint32_t c = 0; c < C; c++) + (*this)(r, c) = static_cast(other(r, c)); + } + + // Assign from static matrix + template + inline dynamic_matrix& operator=(const matrix& other) + { + resize(R, C); + for (uint32_t r = 0; r < R; r++) + for (uint32_t c = 0; c < C; c++) + (*this)(r, c) = static_cast(other(r, c)); + return *this; + } + + // Convert to static matrix (truncates or zero-pads) + template + inline matrix to_matrix() const + { + matrix result(basisu::cClear); + const uint32_t mr = basisu::minimum(m_rows, R); + const uint32_t mc = basisu::minimum(m_cols, C); + for (uint32_t r = 0; r < mr; r++) + for (uint32_t c = 0; c < mc; c++) + result(r, c) = static_cast((*this)(r, c)); + return result; + } + + inline void resize(uint32_t rows, uint32_t cols) + { + if ((rows == m_rows) && (cols == m_cols)) + return; + + if (cols == m_cols) + { + // Same column count - flat data stays aligned, just grow/shrink rows + m_v.resize(rows * cols); + m_rows = rows; + } + else + { + // Column count changed - need to re-layout row by row + basisu::vector new_v(rows * cols); + + const uint32_t copy_rows = basisu::minimum(m_rows, rows); + const uint32_t copy_cols = basisu::minimum(m_cols, cols); + for (uint32_t r = 0; r < copy_rows; r++) + for (uint32_t c = 0; c < copy_cols; c++) + new_v[r * cols + c] = m_v[r * m_cols + c]; + + m_v = new_v; + m_rows = rows; + m_cols = cols; + } + } + + inline void resize(uint32_t rows, uint32_t cols, const T* pInit) + { + const uint32_t n = rows * cols; + assert(pInit && n); + + if (n != m_v.size()) + m_v.resize(n); + + m_rows = rows; + m_cols = cols; + + if ((pInit) && (n)) + { + memcpy(m_v.data(), pInit, sizeof(T) * n); + } + } + + inline uint32_t get_num_rows() const { return m_rows; } + inline uint32_t get_num_cols() const { return m_cols; } + inline uint32_t get_total_elements() const { return m_rows * m_cols; } + inline bool is_empty() const { return (m_rows == 0) || (m_cols == 0); } + inline bool is_square() const { return m_rows == m_cols; } + + inline dynamic_matrix& clear() + { + //const uint32_t total = m_rows * m_cols; + //for (uint32_t i = 0; i < total; i++) + // m_v[i] = static_cast(0); + memset(m_v.data(), 0, m_v.size_in_bytes()); + + return *this; + } + + inline dynamic_matrix& set_all(T val) + { + const uint32_t total = m_rows * m_cols; + for (uint32_t i = 0; i < total; i++) + m_v[i] = val; + return *this; + } + + // Element access - row-major: element(r, c) = m_v[r * m_cols + c] + inline T operator()(uint32_t r, uint32_t c) const + { + assert((r < m_rows) && (c < m_cols)); + return m_v[r * m_cols + c]; + } + + inline T& operator()(uint32_t r, uint32_t c) + { + assert((r < m_rows) && (c < m_cols)); + return m_v[r * m_cols + c]; + } + + // Row access - returns pointer to start of row + inline const T* get_row_ptr(uint32_t r) const + { + assert(r < m_rows); + return &m_v[r * m_cols]; + } + + inline T* get_row_ptr(uint32_t r) + { + assert(r < m_rows); + return &m_v[r * m_cols]; + } + + inline const T* get_ptr() const { return m_v.empty() ? nullptr : &m_v[0]; } + inline T* get_ptr() { return m_v.empty() ? nullptr : &m_v[0]; } + + // Copy a row into a dynamic_vec + inline dynamic_vec get_row(uint32_t r) const + { + assert(r < m_rows); + return dynamic_vec(m_cols, get_row_ptr(r)); + } + + // Copy a column into a dynamic_vec + inline dynamic_vec get_col(uint32_t c) const + { + assert(c < m_cols); + dynamic_vec result(m_rows); + for (uint32_t r = 0; r < m_rows; r++) + result[r] = (*this)(r, c); + return result; + } + + inline dynamic_matrix& set_row(uint32_t r, const dynamic_vec& v) + { + assert(r < m_rows); + assert(v.size() == m_cols); + T* pDst = get_row_ptr(r); + for (uint32_t c = 0; c < m_cols; c++) + pDst[c] = v[c]; + return *this; + } + + inline dynamic_matrix& set_col(uint32_t c, const dynamic_vec& v) + { + assert(c < m_cols); + assert(v.size() == m_rows); + for (uint32_t r = 0; r < m_rows; r++) + (*this)(r, c) = v[r]; + return *this; + } + + // ---- Identity / Scale ---- + + inline dynamic_matrix& set_identity() + { + clear(); + const uint32_t m = basisu::minimum(m_rows, m_cols); + for (uint32_t i = 0; i < m; i++) + (*this)(i, i) = static_cast(1); + return *this; + } + + static inline dynamic_matrix make_identity(uint32_t n) + { + dynamic_matrix result(n, n); + result.set_identity(); + return result; + } + + inline dynamic_matrix& set_scale(T s) + { + clear(); + assert(m_rows == m_cols); + assert(m_rows >= 1); + for (uint32_t i = 0; i + 1 < m_rows; i++) + (*this)(i, i) = s; + (*this)(m_rows - 1, m_cols - 1) = static_cast(1); + return *this; + } + + inline dynamic_matrix& set_scale(const dynamic_vec& s) + { + clear(); + const uint32_t m = basisu::minimum(m_rows, basisu::minimum(m_cols, s.size())); + for (uint32_t i = 0; i < m; i++) + (*this)(i, i) = s[i]; + return *this; + } + + static inline dynamic_matrix make_scale(uint32_t n, T s) + { + dynamic_matrix result(n, n); + result.set_scale(s); + return result; + } + + // ---- Arithmetic ---- + + inline dynamic_matrix& operator+=(const dynamic_matrix& rhs) + { + assert((m_rows == rhs.m_rows) && (m_cols == rhs.m_cols)); + for (uint32_t i = 0; i < m_rows * m_cols; i++) + m_v[i] += rhs.m_v[i]; + return *this; + } + + inline dynamic_matrix& operator-=(const dynamic_matrix& rhs) + { + assert((m_rows == rhs.m_rows) && (m_cols == rhs.m_cols)); + for (uint32_t i = 0; i < m_rows * m_cols; i++) + m_v[i] -= rhs.m_v[i]; + return *this; + } + + inline dynamic_matrix& operator*=(T s) + { + for (uint32_t i = 0; i < m_rows * m_cols; i++) + m_v[i] *= s; + return *this; + } + + inline dynamic_matrix& operator/=(T s) + { + for (uint32_t i = 0; i < m_rows * m_cols; i++) + m_v[i] /= s; + return *this; + } + + inline dynamic_matrix operator+() const + { + return *this; + } + + inline dynamic_matrix operator-() const + { + dynamic_matrix result(m_rows, m_cols); + for (uint32_t i = 0; i < m_rows * m_cols; i++) + result.m_v[i] = -m_v[i]; + return result; + } + + friend inline dynamic_matrix operator+(const dynamic_matrix& a, const dynamic_matrix& b) + { + assert((a.m_rows == b.m_rows) && (a.m_cols == b.m_cols)); + dynamic_matrix result(a.m_rows, a.m_cols); + for (uint32_t i = 0; i < a.m_rows * a.m_cols; i++) + result.m_v[i] = a.m_v[i] + b.m_v[i]; + return result; + } + + friend inline dynamic_matrix operator-(const dynamic_matrix& a, const dynamic_matrix& b) + { + assert((a.m_rows == b.m_rows) && (a.m_cols == b.m_cols)); + dynamic_matrix result(a.m_rows, a.m_cols); + for (uint32_t i = 0; i < a.m_rows * a.m_cols; i++) + result.m_v[i] = a.m_v[i] - b.m_v[i]; + return result; + } + + friend inline dynamic_matrix operator*(const dynamic_matrix& a, T s) + { + dynamic_matrix result(a.m_rows, a.m_cols); + for (uint32_t i = 0; i < a.m_rows * a.m_cols; i++) + result.m_v[i] = a.m_v[i] * s; + return result; + } + + friend inline dynamic_matrix operator*(T s, const dynamic_matrix& a) + { + return a * s; + } + + friend inline dynamic_matrix operator/(const dynamic_matrix& a, T s) + { + dynamic_matrix result(a.m_rows, a.m_cols); + for (uint32_t i = 0; i < a.m_rows * a.m_cols; i++) + result.m_v[i] = a.m_v[i] / s; + return result; + } + + // ---- Matrix * Matrix ---- + // result = lhs[R0 x C0] * rhs[R1 x C1], requires C0 == R1, result is [R0 x C1] + static inline dynamic_matrix multiply(const dynamic_matrix& lhs, const dynamic_matrix& rhs) + { + assert(lhs.m_cols == rhs.m_rows); + assert(lhs.m_cols > 0); + + const uint32_t result_rows = lhs.m_rows; + const uint32_t result_cols = rhs.m_cols; + const uint32_t inner = lhs.m_cols; + + dynamic_matrix result(result_rows, result_cols); + + for (uint32_t r = 0; r < result_rows; r++) + { + const T* pLhsRow = lhs.get_row_ptr(r); + T* pDstRow = result.get_row_ptr(r); + + for (uint32_t c = 0; c < result_cols; c++) + { + T s = pLhsRow[0] * rhs(0, c); + for (uint32_t i = 1; i < inner; i++) + s += pLhsRow[i] * rhs(i, c); + pDstRow[c] = s; + } + } + + return result; + } + + friend inline dynamic_matrix operator*(const dynamic_matrix& lhs, const dynamic_matrix& rhs) + { + return multiply(lhs, rhs); + } + + inline dynamic_matrix& operator*=(const dynamic_matrix& rhs) + { + *this = multiply(*this, rhs); + return *this; + } + + // ---- Vector * Matrix (D3D-style: row vector on left) ---- + // v[1 x R] * M[R x C] = result[1 x C] + // vec size must equal num_rows + static inline dynamic_vec transform(const dynamic_vec& v, const dynamic_matrix& m) + { + assert(v.size() == m.m_rows); + + dynamic_vec result(m.m_cols); + + for (uint32_t r = 0; r < m.m_rows; r++) + { + const T s = v[r]; + const T* pRow = m.get_row_ptr(r); + for (uint32_t c = 0; c < m.m_cols; c++) + result[c] += pRow[c] * s; + } + + return result; + } + + // vec * matrix convenience operator + friend inline dynamic_vec operator*(const dynamic_vec& v, const dynamic_matrix& m) + { + return transform(v, m); + } + + // ---- Matrix * Vector (OGL-style: column vector on right) ---- + // M[R x C] * v[C x 1] = result[R x 1] + // vec size must equal num_cols + static inline dynamic_vec transform(const dynamic_matrix& m, const dynamic_vec& v) + { + assert(v.size() == m.m_cols); + assert(m.m_cols > 0); + + dynamic_vec result(m.m_rows); + + for (uint32_t r = 0; r < m.m_rows; r++) + { + const T* pRow = m.get_row_ptr(r); + T s = pRow[0] * v[0]; + for (uint32_t c = 1; c < m.m_cols; c++) + s += pRow[c] * v[c]; + result[r] = s; + } + + return result; + } + + // matrix * vec convenience operator + friend inline dynamic_vec operator*(const dynamic_matrix& m, const dynamic_vec& v) + { + return transform(m, v); + } + + // ---- D3D-style point/vector transforms ---- + // Last component of vec is assumed to be 1 (point) or 0 (vector). + // Input vec size is num_rows - 1, output vec size is num_cols - 1. + static inline dynamic_vec transform_point(const dynamic_vec& v, const dynamic_matrix& m) + { + assert(m.m_rows >= 1); + assert(v.size() == m.m_rows - 1); + + const uint32_t sub_rows = m.m_rows - 1; + const uint32_t sub_cols = m.m_cols > 0 ? m.m_cols - 1 : 0; + + dynamic_vec result(sub_cols); + + for (uint32_t r = 0; r < sub_rows; r++) + { + const T s = v[r]; + const T* pRow = m.get_row_ptr(r); + for (uint32_t c = 0; c < sub_cols; c++) + result[c] += pRow[c] * s; + } + + // Add last row (w=1) + const T* pLastRow = m.get_row_ptr(m.m_rows - 1); + for (uint32_t c = 0; c < sub_cols; c++) + result[c] += pLastRow[c]; + + return result; + } + + static inline dynamic_vec transform_vector(const dynamic_vec& v, const dynamic_matrix& m) + { + assert(m.m_rows >= 1); + assert(v.size() == m.m_rows - 1); + + const uint32_t sub_rows = m.m_rows - 1; + const uint32_t sub_cols = m.m_cols > 0 ? m.m_cols - 1 : 0; + + dynamic_vec result(sub_cols); + + for (uint32_t r = 0; r < sub_rows; r++) + { + const T s = v[r]; + const T* pRow = m.get_row_ptr(r); + for (uint32_t c = 0; c < sub_cols; c++) + result[c] += pRow[c] * s; + } + + return result; + } + + // ---- Outer Product ---- + // result[i][j] = a[i] * b[j], result is a.size() x b.size() + // If result is already sized correctly it won't be reallocated. + static inline dynamic_matrix& outer_product(dynamic_matrix& result, const dynamic_vec& a, const dynamic_vec& b) + { + const uint32_t rows = a.size(); + const uint32_t cols = b.size(); + + if ((result.m_rows != rows) || (result.m_cols != cols)) + result.resize(rows, cols); + + for (uint32_t r = 0; r < rows; r++) + { + const T s = a[r]; + T* pRow = result.get_row_ptr(r); + for (uint32_t c = 0; c < cols; c++) + pRow[c] = s * b[c]; + } + + return result; + } + + // ---- Transpose ---- + + inline dynamic_matrix get_transposed() const + { + dynamic_matrix result(m_cols, m_rows); + for (uint32_t r = 0; r < m_rows; r++) + { + const T* pRow = get_row_ptr(r); + for (uint32_t c = 0; c < m_cols; c++) + result(c, r) = pRow[c]; + } + return result; + } + + inline dynamic_matrix& transpose_in_place() + { + *this = get_transposed(); + return *this; + } + + // ---- Frobenius Norm ---- + + inline T get_norm() const + { + T result = 0; + for (uint32_t i = 0; i < m_rows * m_cols; i++) + result += m_v[i] * m_v[i]; + return static_cast(sqrt(result)); + } + + // ---- Trace ---- + + inline T get_trace() const + { + assert(m_rows == m_cols); + T total = 0; + for (uint32_t i = 0; i < m_rows; i++) + total += (*this)(i, i); + return total; + } + + // ---- Comparison ---- + + inline bool operator==(const dynamic_matrix& rhs) const + { + if ((m_rows != rhs.m_rows) || (m_cols != rhs.m_cols)) + return false; + for (uint32_t i = 0; i < m_rows * m_cols; i++) + if (!(m_v[i] == rhs.m_v[i])) + return false; + return true; + } + + inline bool operator!=(const dynamic_matrix& rhs) const + { + return !(*this == rhs); + } + + inline bool equal_tol(const dynamic_matrix& b, T tol) const + { + if ((m_rows != b.m_rows) || (m_cols != b.m_cols)) + return false; + for (uint32_t i = 0; i < m_rows * m_cols; i++) + if (!basisu::equal_tol(m_v[i], b.m_v[i], tol)) + return false; + return true; + } + + // ---- Inversion (Gauss-Jordan, square matrices only) ---- + + bool invert(dynamic_matrix& result) const + { + assert(m_rows == m_cols); + const uint32_t n = m_rows; + + dynamic_matrix mat(*this); + result = make_identity(n); + + for (uint32_t c = 0; c < n; c++) + { + // Partial pivot + uint32_t max_r = c; + for (uint32_t r = c + 1; r < n; r++) + if (fabs(mat(r, c)) > fabs(mat(max_r, c))) + max_r = r; + + if (mat(max_r, c) == 0.0f) + { + result = make_identity(n); + return false; + } + + if (max_r != c) + { + // Swap rows + for (uint32_t j = 0; j < n; j++) + { + T tmp; + tmp = mat(c, j); mat(c, j) = mat(max_r, j); mat(max_r, j) = tmp; + tmp = result(c, j); result(c, j) = result(max_r, j); result(max_r, j) = tmp; + } + } + + const T pivot = mat(c, c); + + // Scale pivot row + for (uint32_t j = 0; j < n; j++) + { + result(c, j) /= pivot; + mat(c, j) /= pivot; + } + + // Eliminate column + for (uint32_t row = 0; row < n; row++) + { + if (row != c) + { + const T factor = mat(row, c); + for (uint32_t j = 0; j < n; j++) + { + mat(row, j) -= mat(c, j) * factor; + result(row, j) -= result(c, j) * factor; + } + } + } + } + + return true; + } + + inline dynamic_matrix get_inverse() const + { + dynamic_matrix result; + invert(result); + return result; + } + + inline dynamic_matrix& invert_in_place() + { + dynamic_matrix result; + invert(result); + *this = result; + return *this; + } + + // ---- Debug print ---- + + void print() const + { + for (uint32_t r = 0; r < m_rows; r++) + { + for (uint32_t c = 0; c < m_cols; c++) + printf("%3.7f ", (double)(*this)(r, c)); + printf("\n"); + } + } + + inline void swap(dynamic_matrix& other) + { + m_v.swap(other.m_v); + + uint32_t tmp; + tmp = m_rows; m_rows = other.m_rows; other.m_rows = tmp; + tmp = m_cols; m_cols = other.m_cols; other.m_cols = tmp; + } + + private: + uint32_t m_rows; + uint32_t m_cols; + basisu::vector m_v; + }; + + // ADL swap for dynamic_matrix + template + inline void swap(dynamic_matrix& a, dynamic_matrix& b) + { + a.swap(b); + } + + typedef dynamic_matrix dynamic_matrixF; + typedef dynamic_matrix dynamic_matrixD; + } // namespace bu_math namespace basisu @@ -2556,7 +3617,7 @@ namespace basisu FloatType m_total, m_total_sq; // total, total of squares values FloatType m_avg, m_avg_sq; // mean, mean of the squared values FloatType m_rms; // sqrt(m_avg_sq) - FloatType m_std_dev, m_var; // population standard deviation and variance + FloatType m_std_dev, m_var; // population standard deviation and variance (normalized) FloatType m_mad; // mean absolute deviation FloatType m_min, m_max, m_range; // min and max values, and max-min FloatType m_len; // length of values as a vector (Euclidean norm or L2 norm) @@ -3247,7 +4308,550 @@ namespace basisu return res; } - - + +} // namespace bu_math + +// std::swap specializations +namespace std +{ + template + inline void swap(bu_math::dynamic_vec& a, bu_math::dynamic_vec& b) + { + a.swap(b); + } + + template + inline void swap(bu_math::dynamic_matrix& a, bu_math::dynamic_matrix& b) + { + a.swap(b); + } +} + +namespace basisu +{ + const float SMALL_EPS = .001f, TINY_EPS = .00001f; + + static inline void vec3_zero(float pDst[3]) { pDst[0] = 0; pDst[1] = 0; pDst[2] = 0; } + static inline void vec4_zero(float pDst[4]) { pDst[0] = 0; pDst[1] = 0; pDst[2] = 0; pDst[3] = 0; } + + static inline void vec_zero(float* pDst, uint32_t num_comps) { assert(num_comps); memset(pDst, 0, num_comps * sizeof(float)); } + + static inline void vec3_set(float pDst[3], float s) { pDst[0] = s; pDst[1] = s; pDst[2] = s; } + static inline void vec4_set(float pDst[4], float s) { pDst[0] = s; pDst[1] = s; pDst[2] = s; pDst[3] = s; } + + static inline void vec3_set(float pDst[3], float x, float y, float z) { pDst[0] = x; pDst[1] = y; pDst[2] = z; } + static inline void vec4_set(float pDst[4], float x, float y, float z, float w) { pDst[0] = x; pDst[1] = y; pDst[2] = z; pDst[3] = w; } + + static inline void vec3_add(float pDst[3], const float pB[3]) { pDst[0] += pB[0]; pDst[1] += pB[1]; pDst[2] += pB[2]; } + static inline void vec4_add(float pDst[4], const float pB[4]) { pDst[0] += pB[0]; pDst[1] += pB[1]; pDst[2] += pB[2]; pDst[3] += pB[3]; } + + static inline void vec3_add(float pDst[3], const float pA[3], const float pB[3]) { pDst[0] = pA[0] + pB[0]; pDst[1] = pA[1] + pB[1]; pDst[2] = pA[2] + pB[2]; } + static inline void vec4_add(float pDst[4], const float pA[4], const float pB[4]) { pDst[0] = pA[0] + pB[0]; pDst[1] = pA[1] + pB[1]; pDst[2] = pA[2] + pB[2]; pDst[3] = pA[3] + pB[3]; } + + static inline void vec3_sub(float pDst[3], const float pB[3]) { pDst[0] -= pB[0]; pDst[1] -= pB[1]; pDst[2] -= pB[2]; } + static inline void vec4_sub(float pDst[4], const float pB[4]) { pDst[0] -= pB[0]; pDst[1] -= pB[1]; pDst[2] -= pB[2]; pDst[3] -= pB[3]; } + + static inline void vec3_sub(float pDst[3], const float pA[3], const float pB[3]) { pDst[0] = pA[0] - pB[0]; pDst[1] = pA[1] - pB[1]; pDst[2] = pA[2] - pB[2]; } + static inline void vec4_sub(float pDst[4], const float pA[4], const float pB[4]) { pDst[0] = pA[0] - pB[0]; pDst[1] = pA[1] - pB[1]; pDst[2] = pA[2] - pB[2]; pDst[3] = pA[3] - pB[3]; } + + static inline void vec3_mul(float pDst[3], const float pA[3], const float pB[3]) { pDst[0] = pA[0] * pB[0]; pDst[1] = pA[1] * pB[1]; pDst[2] = pA[2] * pB[2]; } + static inline void vec4_mul(float pDst[4], const float pA[4], const float pB[4]) { pDst[0] = pA[0] * pB[0]; pDst[1] = pA[1] * pB[1]; pDst[2] = pA[2] * pB[2]; pDst[3] = pA[3] * pB[3]; } + + static inline void vec3_madd(float pDst[3], const float pA[3], const float pM[3], const float pB[3]) { pDst[0] = pA[0] * pM[0] + pB[0]; pDst[1] = pA[1] * pM[1] + pB[1]; pDst[2] = pA[2] * pM[2] + pB[2]; } + static inline void vec4_madd(float pDst[4], const float pA[4], const float pM[4], const float pB[4]) { pDst[0] = pA[0] * pM[0] + pB[0]; pDst[1] = pA[1] * pM[1] + pB[1]; pDst[2] = pA[2] * pM[2] + pB[2]; pDst[3] = pA[3] * pM[3] + pB[3]; } + + static inline void vec3_scale_add(float pDst[3], const float pA[3], float s, const float pB[3]) { pDst[0] = pA[0] * s + pB[0]; pDst[1] = pA[1] * s + pB[1]; pDst[2] = pA[2] * s + pB[2]; } + static inline void vec4_scale_add(float pDst[4], const float pA[4], float s, const float pB[4]) { pDst[0] = pA[0] * s + pB[0]; pDst[1] = pA[1] * s + pB[1]; pDst[2] = pA[2] * s + pB[2]; pDst[3] = pA[3] * s + pB[3]; } + + static inline void vec3_scale(float pDst[3], float s) { pDst[0] *= s; pDst[1] *= s; pDst[2] *= s; } + static inline void vec4_scale(float pDst[4], float s) { pDst[0] *= s; pDst[1] *= s; pDst[2] *= s; pDst[3] *= s; } + + static inline void vec3_clamp(float pDst[3], float l, float h) { pDst[0] = clamp(pDst[0], l, h); pDst[1] = clamp(pDst[1], l, h); pDst[2] = clamp(pDst[2], l, h); } + static inline void vec4_clamp(float pDst[4], float l, float h) { pDst[0] = clamp(pDst[0], l, h); pDst[1] = clamp(pDst[1], l, h); pDst[2] = clamp(pDst[2], l, h); pDst[3] = clamp(pDst[3], l, h); } + + static inline void vec3_scale(float pDst[3], const float pVec[3], float s) { pDst[0] = pVec[0] * s; pDst[1] = pVec[1] * s; pDst[2] = pVec[2] * s; } + static inline void vec4_scale(float pDst[4], const float pVec[4], float s) { pDst[0] = pVec[0] * s; pDst[1] = pVec[1] * s; pDst[2] = pVec[2] * s; pDst[3] = pVec[3] * s; } + + static inline void vec3_div(float pDst[3], float s) { assert(s != 0.0f); pDst[0] /= s; pDst[1] /= s; pDst[2] /= s; } + static inline void vec4_div(float pDst[4], float s) { assert(s != 0.0f); pDst[0] /= s; pDst[1] /= s; pDst[2] /= s; pDst[3] /= s; } + + static inline void vec3_div(float pDst[3], const float pVec[3], float s) { assert(s != 0.0f); pDst[0] = pVec[0] / s; pDst[1] = pVec[1] / s; pDst[2] = pVec[2] / s; } + static inline void vec4_div(float pDst[4], const float pVec[4], float s) { assert(s != 0.0f); pDst[0] = pVec[0] / s; pDst[1] = pVec[1] / s; pDst[2] = pVec[2] / s; pDst[3] = pVec[3] / s; } + + static inline void vec_scale(float* pDst, float scale, uint32_t num_comps) { assert(num_comps); for (uint32_t i = 0; i < num_comps; i++) pDst[i] *= scale; } + static inline void vec_scale(float* pDst, const float* pVec, float scale, uint32_t num_comps) { assert(num_comps); for (uint32_t i = 0; i < num_comps; i++) pDst[i] = pVec[i] * scale; } + + static inline float* vec_copy(float* pDst, const float* pSrc, uint32_t num_comps) { assert(num_comps); memcpy(pDst, pSrc, sizeof(float) * num_comps); return pDst; } + static inline float* vec3_copy(float* pDst, const float* pSrc) { memcpy(pDst, pSrc, sizeof(float) * 3); return pDst; } + static inline float* vec4_copy(float* pDst, const float* pSrc) { memcpy(pDst, pSrc, sizeof(float) * 4); return pDst; } + + static inline float vec3_dot(const float pA[3], const float pB[3]) { return (pA[0] * pB[0]) + (pA[1] * pB[1]) + (pA[2] * pB[2]); } + static inline float vec4_dot(const float pA[4], const float pB[4]) { return (pA[0] * pB[0]) + (pA[1] * pB[1]) + (pA[2] * pB[2]) + (pA[3] * pB[3]); } + static inline float vec_dot(const float* pA, const float* pB, uint32_t num_comps) { assert(num_comps); float d = 0.0f; for (uint32_t i = 0; i < num_comps; i++) d += pA[i] * pB[i]; return d; } + + static inline float vec_get_squared_len(const float* pVec, uint32_t num_comps) { assert(num_comps); float l = 0.0f; for (uint32_t i = 0; i < num_comps; i++) l += pVec[i] * pVec[i]; return l; } + + static inline bool vec3_compare(const float pA[3], const float pB[3]) { return memcmp(pA, pB, sizeof(float) * 3) == 0; } + static inline bool vec4_compare(const float pA[4], const float pB[4]) { return memcmp(pA, pB, sizeof(float) * 4) == 0; } + + static inline void vec3_load_u8(float pDst[3], const uint8_t* pSrc) { pDst[0] = (float)pSrc[0]; pDst[1] = (float)pSrc[1]; pDst[2] = (float)pSrc[2]; } + static inline void vec4_load_u8(float pDst[4], const uint8_t* pSrc) { pDst[0] = (float)pSrc[0]; pDst[1] = (float)pSrc[1]; pDst[2] = (float)pSrc[2]; pDst[3] = (float)pSrc[3]; } + + static inline float vec_normalize(float* pVec, uint32_t num_comps) + { + assert(num_comps); + float len2 = 0.0f; + for (uint32_t i = 0; i < num_comps; i++) + len2 += pVec[i] * pVec[i]; + const float scale = (len2 > TINY_EPS) ? (1.0f / sqrtf(len2)) : 0.0f; + for (uint32_t i = 0; i < num_comps; i++) + pVec[i] *= scale; + return len2; + } + + static inline float vec_normalize(float* pDst, const float* pVec, uint32_t num_comps) + { + assert(num_comps); + float len2 = 0.0f; + for (uint32_t i = 0; i < num_comps; i++) + len2 += pVec[i] * pVec[i]; + const float scale = (len2 > TINY_EPS) ? (1.0f / sqrtf(len2)) : 0.0f; + for (uint32_t i = 0; i < num_comps; i++) + pDst[i] = pVec[i] * scale; + return len2; + } + + static inline float vec3_squared_dist(const float pA[4], const float pB[4]) + { + return square(pA[0] - pB[0]) + square(pA[1] - pB[1]) + square(pA[2] - pB[2]); + } + + static inline float vec4_squared_dist(const float pA[4], const float pB[4]) + { + return square(pA[0] - pB[0]) + square(pA[1] - pB[1]) + square(pA[2] - pB[2]) + square(pA[3] - pB[3]); + } + + static inline void vec3_set_comp(float pDst[3], uint32_t comp_index, float val) + { + assert(comp_index < 3); + pDst[comp_index] = val; + } + + static inline void vec4_set_comp(float pDst[4], uint32_t comp_index, float val) + { + assert(comp_index < 4); + pDst[comp_index] = val; + } + + static inline float clamp_flag(float v, float l, float h, bool& did_clamp) + { + if (v < l) + { + did_clamp = true; + return l; + } + else if (v > h) + { + did_clamp = true; + return h; + } + return v; + } + + static inline void vec3_clamp_flag(float pDst[3], float l, float h, bool& did_clamp) { for (uint32_t i = 0; i < 3; i++) pDst[i] = clamp_flag(pDst[i], l, h, did_clamp); } + static inline void vec4_clamp_flag(float pDst[4], float l, float h, bool& did_clamp) { for (uint32_t i = 0; i < 4; i++) pDst[i] = clamp_flag(pDst[i], l, h, did_clamp); } + } // namespace basisu +namespace bu_math +{ + +#ifdef BASISU_FIXED_TESTS + namespace fixed_test + { + // The fixed-point helpers (fixed<>, fixed_detail::) now live in basist. + using namespace basist; + + // --------------------------------------------------------------------------- + // tiny test framework + // NOTE: header-safe form. Everything is `inline` (functions) or C++17 + // `inline` variables (state), so this can live in bu_math.h included + // from many translation units without duplicate-symbol link errors, + // and all TUs share ONE copy of the counters (required for ODR + // correctness: an inline function must reference the same entities + // in every TU, which `static` file-scope variables violate). + // --------------------------------------------------------------------------- + inline long g_fails = 0; + inline long g_checks = 0; + inline long g_printed = 0; + +#define BASISU_FIXED_CHECK(cond, ...) \ +do { \ + g_checks++; \ + if (!(cond)) { \ + g_fails++; \ + if (g_printed++ < 25) { \ + std::printf("FAIL %s:%d: ", __FILE__, __LINE__); \ + std::printf(__VA_ARGS__); \ + std::printf("\n"); \ + } \ + } \ +} while (0) + + inline void section(const char* name) + { + static long last = 0; // function-local statics in an + static const char* prev = nullptr; // inline function are shared - OK + if (prev) std::printf(" %-44s %s\n", prev, g_fails == last ? "OK" : "** FAILED **"); + prev = name; last = g_fails; + if (name) std::fflush(stdout); + } + + // --------------------------------------------------------------------------- + // independent references (int64 quotient/remainder -- no __int128, no shifts) + // --------------------------------------------------------------------------- + // round half away from zero of num/den; |num|, |den| must fit int64 safely + inline int64_t ref_round_div(int64_t num, int64_t den) + { + const bool neg = (num < 0) != (den < 0); + const uint64_t n = num < 0 ? uint64_t(0) - uint64_t(num) : uint64_t(num); + const uint64_t d = den < 0 ? uint64_t(0) - uint64_t(den) : uint64_t(den); + uint64_t q = n / d, r = n % d; + if (2 * r >= d) q++; // tie -> away from zero + return neg ? -int64_t(q) : int64_t(q); + } + + inline int clz64_ref(uint64_t x) // naive, obviously-correct + { + int n = 0; + for (uint64_t b = uint64_t(1) << 63; b && !(x & b); b >>= 1) n++; + return n; + } + + // --------------------------------------------------------------------------- + // compile-time battery: failures here do not even build + // --------------------------------------------------------------------------- + namespace ct { + using fx = fixed<16>; + static_assert(fx::ONE == 65536, ""); + static_assert(fx::from_int(3).v == 3 * 65536, ""); + static_assert(fx::from_int(-3).v == -3 * 65536, ""); + static_assert(fx::from_float(0.5f).v == 32768, ""); + static_assert(fx::from_double(-2.75).v == -(2 * 65536 + 49152), ""); + static_assert(fx::from_float(3.14159265f).v == 205887, ""); + static_assert((fx::from_int(1) + fx::from_int(2)) == fx::from_int(3), ""); + static_assert((fx::from_int(1) - fx::from_int(2)) == fx::from_int(-1), ""); + static_assert((fx::from_int(2)* fx::from_double(0.5)) == fx::from_int(1), ""); + static_assert((fx::from_int(1) / fx::from_int(2)) == fx::from_double(0.5), ""); + static_assert((fx::from_int(7) / 2) == fx::from_double(3.5), ""); + static_assert((3 * fx::from_int(2)) == fx::from_int(6), ""); + static_assert(fx::from_float(2.5f).round_to_int() == 3, ""); + static_assert(fx::from_float(-2.5f).round_to_int() == -3, ""); + static_assert(fx::from_double(-2.75).floor() == fx::from_int(-3), ""); + static_assert(fx::from_double(-2.75).ceil() == fx::from_int(-2), ""); + static_assert(fx::from_double(-2.75).abs() == fx::from_double(2.75), ""); + static_assert(fx::from_double(-2.75).frac() == fx::from_double(0.25), ""); + static_assert(fx::from_int(4).sqrt() == fx::from_int(2), ""); + static_assert(fx::from_double(0.25).sqrt() == fx::from_double(0.5), ""); + static_assert(fx().sqrt() == fx(), ""); + static_assert(fx().sqrt_fast() == fx(), ""); + static_assert(fx::from_int(3).mul_fast(fx::from_double(0.5)) == fx::from_double(1.5), ""); + static_assert(fx::from_int(-7).div_fast(fx::from_int(2)) == fx::from_double(-3.5), ""); + static_assert((fx::from_int(-3) << 2) == fx::from_int(-12), ""); + static_assert(fx::from_int(2) < fx::from_int(3) && fx::from_int(3) >= fx::from_int(3), ""); + static_assert(fixed_detail::clz64(1) == 63, ""); + static_assert(fixed_detail::clz64(~uint64_t(0)) == 0, ""); + // sqrt_fast(4) within tolerance of 2.0 (approximate by design) + constexpr int32_t sf4 = fx::from_int(4).sqrt_fast().v; + static_assert(sf4 > 2 * 65536 - 32 && sf4 < 2 * 65536 + 32, ""); + } + + // --------------------------------------------------------------------------- + template + inline void test_basics() + { + using fx = fixed; + // int round trips over the full representable integer range + const int32_t imax = INT32_MAX / fx::ONE; + for (int32_t i = -imax; i <= imax; i += (imax / 500) + 1) { + BASISU_FIXED_CHECK(fx::from_int(i).round_to_int() == i, "F=%d from_int/round %d", F, (int)i); + BASISU_FIXED_CHECK(fx::from_int(i).to_int() == i, "F=%d from_int/to_int %d", F, (int)i); + } + // float/double round trips: raw -> double -> raw must be exact + std::mt19937 rng(101); + std::uniform_int_distribution d(INT32_MIN, INT32_MAX); + for (int k = 0; k < 100000; k++) { + int32_t raw = d(rng); + BASISU_FIXED_CHECK(fx::from_double(fx::from_raw(raw).to_double()).v == raw, + "F=%d double round trip raw=%d", F, (int)raw); + } + } + + // --------------------------------------------------------------------------- + inline void test_rounded_ops_random() + { + using fx = fixed<16>; + std::mt19937 rng(202); + // range keeps products and quotients inside int32 (no asserts, no skips) + std::uniform_int_distribution d(-181 * fx::ONE, 181 * fx::ONE); + for (int k = 0; k < 1000000; k++) { + const int32_t a = d(rng), b = d(rng); + BASISU_FIXED_CHECK((fx::from_raw(a) + fx::from_raw(b)).v == a + b, "add %d %d", (int)a, (int)b); + BASISU_FIXED_CHECK((fx::from_raw(a) - fx::from_raw(b)).v == a - b, "sub %d %d", (int)a, (int)b); + const int64_t wm = ref_round_div(int64_t(a) * b, fx::ONE); + BASISU_FIXED_CHECK((fx::from_raw(a) * fx::from_raw(b)).v == int32_t(wm), + "mul %d %d", (int)a, (int)b); + if (b != 0) { + const int64_t wd = ref_round_div(int64_t(a) * fx::ONE, b); + if (wd >= INT32_MIN && wd <= INT32_MAX) + BASISU_FIXED_CHECK((fx::from_raw(a) / fx::from_raw(b)).v == int32_t(wd), + "div %d %d", (int)a, (int)b); + } + const int32_t s = int32_t(rng() % 2001) - 1000; + if (s != 0) + BASISU_FIXED_CHECK((fx::from_raw(a) / s).v == int32_t(ref_round_div(a, s)), + "idiv %d %d", (int)a, (int)s); + BASISU_FIXED_CHECK(fx::from_raw(a).round_to_int() == int32_t(ref_round_div(a, fx::ONE)), + "round %d", (int)a); + } + } + + // --------------------------------------------------------------------------- + // exhaustive small-format sweep: rounding behaviour is periodic in ONE, so + // fixed<4> over a modest grid hits every remainder/tie pattern, all signs + inline void test_exhaustive_fixed4() + { + using f4 = fixed<4>; + for (int32_t a = -640; a <= 640; a++) + for (int32_t b = -640; b <= 640; b++) { + BASISU_FIXED_CHECK((f4::from_raw(a) * f4::from_raw(b)).v + == int32_t(ref_round_div(int64_t(a) * b, f4::ONE)), + "f4 mul %d %d", (int)a, (int)b); + if (b != 0) + BASISU_FIXED_CHECK((f4::from_raw(a) / f4::from_raw(b)).v + == int32_t(ref_round_div(int64_t(a) * f4::ONE, b)), + "f4 div %d %d", (int)a, (int)b); + } + } + + // --------------------------------------------------------------------------- + inline void test_fast_variants() + { + using fx = fixed<16>; + std::mt19937 rng(303); + std::uniform_int_distribution d(-181 * fx::ONE, 181 * fx::ONE); + for (int k = 0; k < 1000000; k++) { + const int32_t a = d(rng), b = d(rng); + // mul_fast truncates toward -inf: floor((a*b)/ONE) + const int64_t p = int64_t(a) * b; + const int64_t wm = (p - (p < 0 ? fx::ONE - 1 : 0)) / fx::ONE; // floor div + BASISU_FIXED_CHECK(fx::from_raw(a).mul_fast(fx::from_raw(b)).v == int32_t(wm), + "mul_fast %d %d", (int)a, (int)b); + BASISU_FIXED_CHECK(std::llabs((fx::from_raw(a) * fx::from_raw(b)).v - wm) <= 1, + "mul_fast >1ulp from rounded %d %d", (int)a, (int)b); + if (b != 0) { + // div_fast truncates toward zero (C++ division) + const int64_t wd = (int64_t(a) * fx::ONE) / b; + if (wd >= INT32_MIN && wd <= INT32_MAX) { + BASISU_FIXED_CHECK(fx::from_raw(a).div_fast(fx::from_raw(b)).v == int32_t(wd), + "div_fast %d %d", (int)a, (int)b); + BASISU_FIXED_CHECK(std::llabs((fx::from_raw(a) / fx::from_raw(b)).v - wd) <= 1, + "div_fast >1ulp from rounded %d %d", (int)a, (int)b); + } + } + const int32_t s = int32_t(rng() % 2001) - 1000; + if (s != 0) + BASISU_FIXED_CHECK(fx::from_raw(a).div_fast(s).v == a / s, "div_fast int %d %d", (int)a, (int)s); + } + } + + // --------------------------------------------------------------------------- + inline void test_clz() + { + std::mt19937_64 rng(404); + for (int bit = 0; bit < 64; bit++) { + const uint64_t p = uint64_t(1) << bit; + BASISU_FIXED_CHECK(fixed_detail::clz64(p) == clz64_ref(p), "clz 2^%d", bit); + const uint64_t f = (bit == 63) ? ~uint64_t(0) : (p | (p - 1)); + BASISU_FIXED_CHECK(fixed_detail::clz64(f) == clz64_ref(f), "clz fill %d", bit); + for (int k = 0; k < 2000; k++) { + const uint64_t x = p | (rng() & (p - 1)); + BASISU_FIXED_CHECK(fixed_detail::clz64(x) == clz64_ref(x), "clz rand bit %d", bit); + } + } + } + + // --------------------------------------------------------------------------- + // exact sqrt: pure-integer round-to-nearest certificate, no floating point. + // r = round(sqrt(x)) iff (2r-1)^2 <= 4x <= (2r+1)^2 (ties impossible) + template + inline void check_sqrt_exact(int32_t raw) + { + using fx = fixed; + const uint64_t x = uint64_t(uint32_t(raw)) << F; + const uint64_t r = uint64_t(uint32_t(fx::from_raw(raw).sqrt().v)); + if (x == 0) { BASISU_FIXED_CHECK(r == 0, "sqrt(0) F=%d", F); return; } + BASISU_FIXED_CHECK(r > 0, "sqrt=0 for x>0, F=%d raw=%d", F, (int)raw); + const uint64_t lo = (2 * r - 1) * (2 * r - 1); + const uint64_t hi = (2 * r + 1) * (2 * r + 1); + BASISU_FIXED_CHECK(lo <= 4 * x && 4 * x <= hi, "sqrt not nearest F=%d raw=%d r=%llu", + F, (int)raw, (unsigned long long)r); + } + + inline void test_sqrt() + { + using fx = fixed<16>; + for (int32_t raw = 0; raw <= 300000; raw++) check_sqrt_exact<16>(raw); // exhaustive small + std::mt19937 rng(505); + std::uniform_int_distribution d(0, INT32_MAX); + for (int k = 0; k < 500000; k++) check_sqrt_exact<16>(d(rng)); // random full range + for (int k = 0; k < 50000; k++) { // other formats + check_sqrt_exact<8>(d(rng)); + check_sqrt_exact<24>(d(rng)); + check_sqrt_exact<30>(d(rng)); + } + // monotonic on a coarse sweep (regression guard for table/normalize bugs) + int32_t prev = -1; + for (int64_t raw = 0; raw <= INT32_MAX; raw += 4093) { + const int32_t r = fx::from_raw(int32_t(raw)).sqrt().v; + BASISU_FIXED_CHECK(r >= prev, "sqrt not monotonic at raw=%lld", (long long)raw); + prev = r; + } + } + + // bound: within max(2 ulp, 6e-5 relative) of the EXACT integer sqrt. + // (measured: <= ~3.1e-5 relative for v >= 1; tiny inputs are + // quantization-dominated, covered by the 2-ulp floor) + // Plain function rather than a local lambda: MSVC had trouble + // compiling the lambda form, and this is equivalent. + inline void check_sqrt_fast_one(int32_t raw) + { + using fx = fixed<16>; + const int64_t e = fx::from_raw(raw).sqrt().v; + const int64_t f = fx::from_raw(raw).sqrt_fast().v; + const double tol = basisu::maximum(2.0, double(e) * 6e-5); + BASISU_FIXED_CHECK(std::llabs(f - e) <= (int64_t)tol, + "sqrt_fast raw=%d exact=%lld fast=%lld", (int)raw, (long long)e, (long long)f); + } + + inline void test_sqrt_fast() + { + for (int32_t raw = 0; raw <= 200000; raw++) + check_sqrt_fast_one(raw); + std::mt19937 rng(606); + std::uniform_int_distribution d(0, INT32_MAX); + for (int k = 0; k < 500000; k++) + check_sqrt_fast_one(d(rng)); + // every LUT index gets exercised at both interval edges + for (uint64_t i = 32; i < 128; i++) { + for (int t = 16; t <= 44; t += 2) { // various magnitudes, F=16 + const uint64_t X = i << 57; + const uint64_t x = X >> t; + if (x >> 16 <= uint64_t(INT32_MAX) && (x >> 16) << 16 == ((X >> t) >> 16) << 16) { + const int32_t raw = int32_t(x >> 16); + if (raw > 0) + check_sqrt_fast_one(raw); + } + } + } + } + + // --------------------------------------------------------------------------- + inline void test_helpers_and_compound() + { + using fx = fixed<16>; + std::mt19937 rng(707); + std::uniform_int_distribution d(-30000 * fx::ONE / 2, 30000 * fx::ONE / 2); + for (int k = 0; k < 200000; k++) { + const int32_t a = d(rng), b = d(rng); + const fx A = fx::from_raw(a), B = fx::from_raw(b); + // floor/ceil/frac identities (pure integer) + BASISU_FIXED_CHECK(A.floor().v % fx::ONE == 0 && A.floor().v <= a && a - A.floor().v < fx::ONE, + "floor %d", (int)a); + BASISU_FIXED_CHECK(A.ceil().v % fx::ONE == 0 && A.ceil().v >= a && A.ceil().v - a < fx::ONE, + "ceil %d", (int)a); + BASISU_FIXED_CHECK(A.floor().v + A.frac().v == a, "floor+frac %d", (int)a); + BASISU_FIXED_CHECK(A.abs().v == (a < 0 ? -a : a), "abs %d", (int)a); + BASISU_FIXED_CHECK((-A).v == -a, "neg %d", (int)a); + // comparisons agree with raw + BASISU_FIXED_CHECK((A < B) == (a < b) && (A == B) == (a == b) && (A >= B) == (a >= b), + "cmp %d %d", (int)a, (int)b); + // compound forms equal their binary counterparts + fx c = A; c += B; BASISU_FIXED_CHECK(c == A + B, "+= %d %d", (int)a, (int)b); + c = A; c -= B; BASISU_FIXED_CHECK(c == A - B, "-= %d %d", (int)a, (int)b); + c = A; c *= fx::from_double(0.25); BASISU_FIXED_CHECK(c == A * fx::from_double(0.25), "*="); + if (b != 0) { + // stay inside the legal domain: skip quotients that won't fit int32 + const int64_t w = (int64_t(a) * fx::ONE) / b; + if (w > INT32_MIN / 2 && w < INT32_MAX / 2) { + c = A; c /= B; BASISU_FIXED_CHECK(c == A / B, "/="); + } + } + // shift identities on safe range + const fx S = fx::from_raw(a / 16); + BASISU_FIXED_CHECK((S << 2) == S * 4, "<< %d", (int)a); + BASISU_FIXED_CHECK(((S << 3) >> 3) == S, "<<>> %d", (int)a); + } + // wide-accumulate pattern + lerp + std::uniform_int_distribution dw(-60 * fx::ONE, 60 * fx::ONE); // 60*60*8 fits + for (int k = 0; k < 100000; k++) { + int64_t acc = 0, ref = 0; + for (int i = 0; i < 8; i++) { + const fx va = fx::from_raw(dw(rng)), vb = fx::from_raw(dw(rng)); + acc += va.mul_wide(vb); + ref += int64_t(va.v) * vb.v; + } + BASISU_FIXED_CHECK(fx::from_sum(acc).v == int32_t(ref_round_div(ref, fx::ONE)), "dot %d", k); + const int32_t la = dw(rng), lb = dw(rng), lt = int32_t(uint32_t(rng()) % (fx::ONE + 1)); + const int64_t want = la + ref_round_div((int64_t(lb) - la) * lt, fx::ONE); + BASISU_FIXED_CHECK(fx::lerp(fx::from_raw(la), fx::from_raw(lb), fx::from_raw(lt)).v == int32_t(want), + "lerp %d %d %d", (int)la, (int)lb, (int)lt); + BASISU_FIXED_CHECK(fx::lerp(fx::from_raw(la), fx::from_raw(lb), fx()).v == la, "lerp t=0"); + BASISU_FIXED_CHECK(fx::lerp(fx::from_raw(la), fx::from_raw(lb), fx::from_int(1)).v == lb, "lerp t=1"); + } + // boundary values that are legal and must not misbehave + BASISU_FIXED_CHECK(fx::from_raw(INT32_MIN).round_to_int() == -32768, "round INT32_MIN"); + BASISU_FIXED_CHECK(fx::from_raw(INT32_MAX).round_to_int() == 32768, "round INT32_MAX"); + BASISU_FIXED_CHECK(fx::from_raw(INT32_MAX).sqrt().v > 0, "sqrt max"); + BASISU_FIXED_CHECK(fx::from_raw(0).sqrt_fast().v == 0, "sqrt_fast 0"); + BASISU_FIXED_CHECK(fx::from_raw(1).sqrt().v == 256, "sqrt 1 ulp"); // sqrt(2^-16)=2^-8 + } + + // --------------------------------------------------------------------------- + // Call this from your test runner / main(). Returns true on success, + // false on any failure. + inline bool run_tests() + { + g_fails = 0; g_checks = 0; g_printed = 0; // re-runnable + + std::printf("fixed.hpp validation harness (C++17, %s build)\n", +#ifdef NDEBUG + "NDEBUG" +#else + "assert-enabled" +#endif + ); + + section("basics/round-trips fixed<16>"); test_basics<16>(); + section("basics/round-trips fixed<8>"); test_basics<8>(); + section("basics/round-trips fixed<24>"); test_basics<24>(); + section("rounded ops, 1M random vs reference"); test_rounded_ops_random(); + section("exhaustive fixed<4> mul/div (all tie patterns)"); test_exhaustive_fixed4(); + section("fast variants semantics, 1M random"); test_fast_variants(); + section("clz64 vs naive reference, all bit positions"); test_clz(); + section("sqrt exact: integer nearest-certificate"); test_sqrt(); + section("sqrt_fast: error bounds + LUT coverage"); test_sqrt_fast(); + section("helpers, compounds, boundaries"); test_helpers_and_compound(); + section(nullptr); // flush last section + + std::printf("\n%ld checks, %ld failures -> %s\n", + g_checks, g_fails, g_fails ? "FAILED" : "ALL PASSED"); + return g_fails == 0; + } + } // namespace fixed_test + +#undef BASISU_FIXED_CHECK + +#endif // BASISU_FIXED_TESTS + +} // namespace bu_math diff --git a/encoder/basisu_tinyexr.cpp b/encoder/basisu_tinyexr.cpp index 6d3828a..5666da6 100644 --- a/encoder/basisu_tinyexr.cpp +++ b/encoder/basisu_tinyexr.cpp @@ -27,14 +27,22 @@ typedef Byte Bytef; #define compress buminiz::mz_compress #define uncompress buminiz::mz_uncompress +// tinyexr.h is third-party code we use unmodified, so silence its warnings here with SPECIFIC, low-risk +// disables only. +// These affect only this translation unit, which contains nothing but the tinyexr implementation include. #ifdef _MSC_VER -#pragma warning (disable: 4060) -#pragma warning (disable: 4100) -#pragma warning (disable: 4245) -#pragma warning (disable: 4505) -#pragma warning (disable: 4702) +#pragma warning (disable: 4060) // warning C4060: switch statement contains no 'case' or 'default' labels +#pragma warning (disable: 4100) // warning C4100: unreferenced formal parameter +#pragma warning (disable: 4245) // warning C4245: conversion from 'type1' to 'type2', signed/unsigned mismatch +#pragma warning (disable: 4505) // warning C4505: unreferenced function with internal linkage has been removed +#pragma warning (disable: 4702) // warning C4702: unreachable code #pragma warning (disable: 4530) // warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify /EHsc #endif +#if defined(__GNUC__) // covers both gcc and clang +#pragma GCC diagnostic ignored "-Wunused-parameter" // tinyexr: unused params in its (de)compress helpers (DecompressPxr24, etc.) +#pragma GCC diagnostic ignored "-Wunused-function" // tinyexr: unused static helpers (CompressPxr24, CompressB44, AddIntAttribute) +#endif + #define TINYEXR_IMPLEMENTATION #include "3rdparty/tinyexr.h" diff --git a/encoder/basisu_wasm_api_common.h b/encoder/basisu_wasm_api_common.h index d3fe1ae..598ecd8 100644 --- a/encoder/basisu_wasm_api_common.h +++ b/encoder/basisu_wasm_api_common.h @@ -19,17 +19,17 @@ typedef uint32_t wasm_bool_t; // Compression constants -#define BU_QUALITY_MIN 0 +#define BU_QUALITY_MIN 1 #define BU_QUALITY_MAX 100 #define BU_EFFORT_MIN 0 #define BU_EFFORT_MAX 10 -#define BU_EFFORT_SUPER_FAST = 0 -#define BU_EFFORT_FAST = 2 -#define BU_EFFORT_NORMAL = 5 -#define BU_EFFORT_DEFAULT = 2 -#define BU_EFFORT_SLOW = 8 -#define BU_EFFORT_VERY_SLOW = 10 +#define BU_EFFORT_SUPER_FAST 0 +#define BU_EFFORT_FAST 2 +#define BU_EFFORT_NORMAL 5 +#define BU_EFFORT_DEFAULT 2 +#define BU_EFFORT_SLOW 8 +#define BU_EFFORT_VERY_SLOW 10 #define BU_COMP_FLAGS_NONE (0) #define BU_COMP_FLAGS_USE_OPENCL (1 << 8 ) @@ -60,6 +60,19 @@ typedef uint32_t wasm_bool_t; #define BU_COMP_FLAGS_TEXTURE_TYPE_SHIFT (25) #define BU_COMP_FLAGS_TEXTURE_TYPE_MASK (3) +// ASTC/XUASTC LDR deblocking control. By default 10x8 or larger block sizes (>= 80 texels/block) get deblocking-aware +// encoding and the deblocking ID is written to the output file. +#define BU_COMP_FLAGS_DISABLE_DEBLOCKING (1 << 27) // disable all deblocking related features +#define BU_COMP_FLAGS_FORCE_DEBLOCKING (1 << 28) // force deblocking-aware encoding on all block sizes + +// XUBC7 only: selects the BC7 base encoder (and, for bc7e_scalar, its quality level), packed into a 3-bit field. +// Field value 0 = bc7f (the default fast real-time packer; no level). Values 1-7 = bc7e_scalar (slower, higher quality) +// at level 0-6 (i.e. bc7e_scalar level = field value - 1). Build a bc7e_scalar value as: +// ((level + 1) << BU_COMP_FLAGS_XUBC7_BASE_ENCODER_SHIFT) +#define BU_COMP_FLAGS_XUBC7_BASE_ENCODER_BC7F (0 << 29) +#define BU_COMP_FLAGS_XUBC7_BASE_ENCODER_SHIFT (29) +#define BU_COMP_FLAGS_XUBC7_BASE_ENCODER_MASK (7) + #define BU_COMP_FLAGS_VERBOSE (BU_COMP_FLAGS_DEBUG_OUTPUT | BU_COMP_FLAGS_PRINT_STATS | BU_COMP_FLAGS_PRINT_STATUS) // basist::basis_tex_format: the supported .ktx2 (and .basis) file format types @@ -96,7 +109,8 @@ typedef uint32_t wasm_bool_t; #define BTF_ASTC_LDR_10X10 30 #define BTF_ASTC_LDR_12X10 31 #define BTF_ASTC_LDR_12X12 32 -#define BTF_TOTAL_FORMATS 33 +#define BTF_XUBC7 33 +#define BTF_TOTAL_FORMATS 34 // Transcoding constants @@ -151,6 +165,5 @@ typedef uint32_t wasm_bool_t; #define DECODE_FLAGS_HIGH_QUALITY 32 #define DECODE_FLAGS_NO_ETC1S_CHROMA_FILTERING 64 #define DECODE_FLAGS_NO_DEBLOCK_FILTERING 128 -#define DECODE_FLAGS_STRONGER_DEBLOCK_FILTERING 256 #define DECODE_FLAGS_FORCE_DEBLOCK_FILTERING 512 #define DECODE_FLAGS_XUASTC_LDR_DISABLE_FAST_BC7_TRANSCODING 1024 diff --git a/encoder/basisu_wasm_transcoder_api.cpp b/encoder/basisu_wasm_transcoder_api.cpp index ab46525..280ac19 100644 --- a/encoder/basisu_wasm_transcoder_api.cpp +++ b/encoder/basisu_wasm_transcoder_api.cpp @@ -41,7 +41,7 @@ void bt_init() basisu_transcoder_init(); } -// Memory alloc/free — stubs +// Memory alloc/free stubs BU_WASM_EXPORT("bt_alloc") uint64_t bt_alloc(uint64_t size) { @@ -77,6 +77,16 @@ wasm_bool_t bt_basis_tex_format_is_astc_ldr(uint32_t basis_tex_fmt_u32) return basis_tex_format_is_astc_ldr(tex_fmt); } +BU_WASM_EXPORT("bt_basis_tex_format_is_xubc7") +wasm_bool_t bt_basis_tex_format_is_xubc7(uint32_t basis_tex_fmt_u32) +{ + assert(basis_tex_fmt_u32 < (uint32_t)basis_tex_format::cTotalFormats); + + basis_tex_format tex_fmt = static_cast(basis_tex_fmt_u32); + + return basis_tex_format_is_xubc7(tex_fmt); +} + BU_WASM_EXPORT("bt_basis_tex_format_get_block_width") uint32_t bt_basis_tex_format_get_block_width(uint32_t basis_tex_fmt_u32) { @@ -535,6 +545,24 @@ wasm_bool_t bt_ktx2_is_xuastc_ldr(uint64_t handle) return pHandle->m_transcoder.is_xuastc_ldr(); } +BU_WASM_EXPORT("bt_ktx2_is_xubc7") +wasm_bool_t bt_ktx2_is_xubc7(uint64_t handle) +{ + if (!handle) + { + assert(0); + return false; + } + + ktx2_handle_t* pHandle = reinterpret_cast(wasm_ptr(handle)); + + assert(pHandle->m_magic == KTX2_HANDLE_MAGIC); + if (pHandle->m_magic != KTX2_HANDLE_MAGIC) + return false; + + return pHandle->m_transcoder.is_xubc7(); +} + BU_WASM_EXPORT("bt_ktx2_get_block_width") uint32_t bt_ktx2_get_block_width(uint64_t handle) { @@ -571,6 +599,24 @@ uint32_t bt_ktx2_get_block_height(uint64_t handle) return pHandle->m_transcoder.get_block_height(); } +BU_WASM_EXPORT("bt_ktx2_get_deblocking_filter_index") +uint32_t bt_ktx2_get_deblocking_filter_index(uint64_t handle) +{ + if (!handle) + { + assert(0); + return 0; + } + + ktx2_handle_t* pHandle = reinterpret_cast(wasm_ptr(handle)); + + assert(pHandle->m_magic == KTX2_HANDLE_MAGIC); + if (pHandle->m_magic != KTX2_HANDLE_MAGIC) + return 0; + + return pHandle->m_transcoder.get_deblocking_filter_index(); +} + BU_WASM_EXPORT("bt_ktx2_has_alpha") wasm_bool_t bt_ktx2_has_alpha(uint64_t handle) { diff --git a/encoder/basisu_wasm_transcoder_api.h b/encoder/basisu_wasm_transcoder_api.h index a7389ac..470b97c 100644 --- a/encoder/basisu_wasm_transcoder_api.h +++ b/encoder/basisu_wasm_transcoder_api.h @@ -1,4 +1,5 @@ // File: basisu_wasm_transcoder_api.h - Transcoding API support for WASM WASI modules and Python native support. +// TODO: Add .DDS transcoding support (it's supported in C++ native and in the JS emscripten C++ wrappers, but not in the pure C wrappers yet, or Python). #pragma once #include "basisu_wasm_api_common.h" @@ -27,6 +28,9 @@ wasm_bool_t bt_basis_tex_format_is_xuastc_ldr(uint32_t basis_tex_fmt_u32); BU_WASM_EXPORT("bt_basis_tex_format_is_astc_ldr") wasm_bool_t bt_basis_tex_format_is_astc_ldr(uint32_t basis_tex_fmt_u32); +BU_WASM_EXPORT("bt_basis_tex_format_is_xubc7") +wasm_bool_t bt_basis_tex_format_is_xubc7(uint32_t basis_tex_fmt_u32); + BU_WASM_EXPORT("bt_basis_tex_format_get_block_width") uint32_t bt_basis_tex_format_get_block_width(uint32_t basis_tex_fmt_u32); @@ -126,12 +130,18 @@ wasm_bool_t bt_ktx2_is_astc_ldr(uint64_t handle); BU_WASM_EXPORT("bt_ktx2_is_xuastc_ldr") wasm_bool_t bt_ktx2_is_xuastc_ldr(uint64_t handle); +BU_WASM_EXPORT("bt_ktx2_is_xubc7") +wasm_bool_t bt_ktx2_is_xubc7(uint64_t handle); + BU_WASM_EXPORT("bt_ktx2_get_block_width") uint32_t bt_ktx2_get_block_width(uint64_t handle); BU_WASM_EXPORT("bt_ktx2_get_block_height") uint32_t bt_ktx2_get_block_height(uint64_t handle); +BU_WASM_EXPORT("bt_ktx2_get_deblocking_filter_index") +uint32_t bt_ktx2_get_deblocking_filter_index(uint64_t handle); + BU_WASM_EXPORT("bt_ktx2_has_alpha") wasm_bool_t bt_ktx2_has_alpha(uint64_t handle); diff --git a/encoder/cppspmd_math.h b/encoder/cppspmd_math.h index 4040324..022884d 100644 --- a/encoder/cppspmd_math.h +++ b/encoder/cppspmd_math.h @@ -679,28 +679,28 @@ CPPSPMD_FORCE_INLINE vint cmple_epi16(const vint &a, const vint &b) return cmpge_epi16(b, a); } -void spmd_kernel::print_vint(vint v) +inline void spmd_kernel::print_vint(vint v) { for (uint32_t i = 0; i < PROGRAM_COUNT; i++) printf("%i ", extract(v, i)); printf("\n"); } -void spmd_kernel::print_vbool(vbool v) +inline void spmd_kernel::print_vbool(vbool v) { for (uint32_t i = 0; i < PROGRAM_COUNT; i++) printf("%i ", extract(v, i) ? 1 : 0); printf("\n"); } -void spmd_kernel::print_vint_hex(vint v) +inline void spmd_kernel::print_vint_hex(vint v) { for (uint32_t i = 0; i < PROGRAM_COUNT; i++) printf("0x%X ", extract(v, i)); printf("\n"); } -void spmd_kernel::print_active_lanes(const char *pPrefix) +inline void spmd_kernel::print_active_lanes(const char *pPrefix) { CPPSPMD_DECL(int, flags[PROGRAM_COUNT]); memset(flags, 0, sizeof(flags)); @@ -717,7 +717,7 @@ void spmd_kernel::print_active_lanes(const char *pPrefix) printf("\n"); } -void spmd_kernel::print_vfloat(vfloat v) +inline void spmd_kernel::print_vfloat(vfloat v) { for (uint32_t i = 0; i < PROGRAM_COUNT; i++) printf("%f ", extract(v, i)); diff --git a/encoder/pvpngreader.cpp b/encoder/pvpngreader.cpp index e81d1d6..35024aa 100644 --- a/encoder/pvpngreader.cpp +++ b/encoder/pvpngreader.cpp @@ -472,6 +472,10 @@ public: bool m_scanned_flag; + // Set once png_decode_start() runs; guards against a second call (which would re-alloc the line buffers and + // re-init the inflator without freeing the first set). Debug-only assert; the public API never re-enters. + bool m_decode_started_flag; + int m_terminate_status; #define TEMP_BUF_SIZE (384) @@ -551,7 +555,11 @@ void* png_decoder::png_malloc(uint32_t len) break; if (j == PNG_MAX_ALLOC_BLOCKS) + { + // Tracking table full: free the block we just allocated rather than leaking it. + free(p); return nullptr; + } m_pMalloc_blocks[j] = p; @@ -666,7 +674,7 @@ int64_t png_decoder::fetch_next_chunk_dword() return (int)status; if (status != 4) - terminate(PNG_BAD_CHUNK_SIZE); + return terminate(PNG_BAD_CHUNK_SIZE); uint32_t v = buf[3] + ((uint32_t)buf[2] << 8) + ((uint32_t)buf[1] << 16) + ((uint32_t)buf[0] << 24); return (int64_t)v; @@ -694,6 +702,7 @@ int png_decoder::fetch_next_chunk_init() int64_t status = fetch_next_chunk_data(m_chunk_name, 4); if (status < 0) return (int)status; + if (status != 4) return terminate(PNG_BAD_CHUNK_SIZE); @@ -732,7 +741,6 @@ int png_decoder::unchunk_data(uint8_t* buf, uint32_t bytes, uint32_t* ptr_bytes_ int64_t res = fetch_next_chunk_data(buf + bytes_read, bytes - bytes_read); if (res < 0) return (int)res; - assert(res <= UINT32_MAX); bytes_read += (uint32_t)res; @@ -1159,7 +1167,7 @@ int png_decoder::adam7_pass_size(int size, int start, int step) int png_decoder::decompress_line(uint32_t* bytes_decoded) { int status; - uint32_t temp, src_bytes_left, dst_bytes_left; + uint32_t temp = 0, src_bytes_left, dst_bytes_left; m_inflate_dst_buf_ofs = 0; @@ -1502,6 +1510,11 @@ void png_decoder::png_decode_end() int png_decoder::png_decode_start() { + // Must only be called once per decode: it allocates the line buffers and inits the inflator fresh, so a + // second call would leak the first set. The public API always uses a one-shot fresh decoder. + assert(!m_decode_started_flag); + m_decode_started_flag = true; + int status; if (m_img_supported_flag != TRUE) @@ -1649,7 +1662,14 @@ int png_decoder::png_decode_start() m_adam7_pass_size_y[5] = adam7_pass_size(m_ihdr.m_height, 0, 2); m_adam7_pass_size_y[6] = adam7_pass_size(m_ihdr.m_height, 1, 2); - m_adam7_image_buf.resize(m_dst_bytes_per_line * m_ihdr.m_height); + // Compute the de-interlace buffer size in 64-bit and bail if it's unreasonably large, to avoid a 32-bit + // multiply overflow (m_dst_bytes_per_line * m_height) that could wrap to a too-small allocation followed + // by out-of-bounds writes during de-interlacing. Cap at 16384*16384*4 bytes (1 GiB). + const uint64_t adam7_buf_size = (uint64_t)m_dst_bytes_per_line * (uint64_t)m_ihdr.m_height; + if (adam7_buf_size > (uint64_t)16384 * 16384 * 4) + return terminate(PNG_UNS_RESOLUTION); + + m_adam7_image_buf.resize((size_t)adam7_buf_size); m_adam7_pass_num = -1; @@ -2162,6 +2182,7 @@ void png_decoder::clear() m_adam7_decoded_flag = FALSE; m_scanned_flag = false; + m_decode_started_flag = false; m_terminate_status = 0; }