// File: crunch.cpp - Command line tool for DDS/CRN texture compression/decompression. // This tool exposes all of crnlib's functionality. It also uses a bunch of internal crlib // classes that aren't directly exposed in the main crnlib.h header. The actual tool is // implemented as a single class "crunch" which in theory is reusable. Most of the heavy // lifting is actually done by functions in the crnlib::texture_conversion namespace, // which are mostly wrappers over the public crnlib.h functions. // See Copyright Notice and license at the end of inc/crnlib.h // // Important: If compiling with gcc, be sure strict aliasing is disabled: -fno-strict-aliasing #include "crn_core.h" #include "crn_console.h" #include "crn_colorized_console.h" #include "crn_find_files.h" #include "crn_file_utils.h" #include "crn_command_line_params.h" #include "crn_dxt.h" #include "crn_cfile_stream.h" #include "crn_texture_conversion.h" #define CRND_HEADER_FILE_ONLY #include "crn_decomp.h" #include "corpus_gen.h" #include "corpus_test.h" using namespace crnlib; const int cDefaultCRNQualityLevel = 128; class crunch { CRNLIB_NO_COPY_OR_ASSIGNMENT_OP(crunch); cfile_stream m_log_stream; uint32 m_num_processed; uint32 m_num_failed; uint32 m_num_succeeded; uint32 m_num_skipped; public: crunch() : m_num_processed(0), m_num_failed(0), m_num_succeeded(0), m_num_skipped(0) { } ~crunch() { } enum convert_status { cCSFailed, cCSSucceeded, cCSSkipped, cCSBadParam, }; inline uint32 get_num_processed() const { return m_num_processed; } inline uint32 get_num_failed() const { return m_num_failed; } inline uint32 get_num_succeeded() const { return m_num_succeeded; } inline uint32 get_num_skipped() const { return m_num_skipped; } static void print_usage() { // ------------------------------------------------------------------------------- console::message("\nCommand line usage:"); console::printf("crunch [options] -file filename"); console::printf("-file filename - Required input filename, wildcards, multiple -file params OK."); console::printf("-file @list.txt - List of files to convert."); console::printf("Supported source file formats: dds,ktx,crn,tga,bmp,png,jpg/jpeg,psd"); console::printf("Note: Some file format variants are unsupported."); console::printf("See the docs for stb_image.c: http://www.nothings.org/stb_image.c"); console::printf("Progressive JPEG files are supported, see: http://code.google.com/p/jpeg-compressor/"); console::message("\nPath/file related parameters:"); console::printf("-out filename - Output filename"); console::printf("-outdir dir - Output directory"); console::printf("-outsamedir - Write output file to input directory"); console::printf("-deep - Recurse subdirectories, default=false"); console::printf("-nooverwrite - Don't overwrite existing files"); console::printf("-timestamp - Update only changed files"); console::printf("-forcewrite - Overwrite read-only files"); console::printf("-recreate - Recreate directory structure"); console::printf("-fileformat [dds,ktx,crn,tga,bmp,png] - Output file format, default=crn or dds"); console::message("\nModes:"); console::printf("-compare - Compare input and output files (no output files are written)."); console::printf("-info - Only display input file statistics (no output files are written)."); console::message("\nMisc. options:"); console::printf("-helperThreads # - Set number of helper threads, 0-16, default=(# of CPU's)-1"); console::printf("-noprogress - Disable progress output"); console::printf("-quiet - Disable all console output"); console::printf("-ignoreerrors - Continue processing files after errors. Note: The default"); console::printf(" behavior is to immediately exit whenever an error occurs."); console::printf("-logfile filename - Append output to log file"); console::printf("-pause - Wait for keypress on error"); console::printf("-window - Crop window before processing"); console::printf("-clamp - Crop image if larger than width/height"); console::printf("-clampscale - Scale image if larger than width/height"); console::printf("-nostats - Disable all output file statistics (faster)"); console::printf("-imagestats - Print various image qualilty statistics"); console::printf("-mipstats - Print statistics for each mipmap, not just the top mip"); console::printf("-lzmastats - Print size of output file compressed with LZMA codec"); console::printf("-split - Write faces/mip levels to multiple separate output PNG files"); console::printf("-yflip - Always flip texture on Y axis before processing"); console::printf("-unflip - Unflip texture if read from source file as flipped"); console::message("\nImage rescaling (mutually exclusive options)"); console::printf("-rescale - Rescale image to specified resolution"); console::printf("-relscale - Rescale image to specified relative resolution"); console::printf("-rescalemode - Auto-rescale non-power of two images"); console::printf(" nearest - Use nearest power of 2, hi - Use next, lo - Use previous"); console::message("\nDDS/CRN compression quality control:"); console::printf("-quality # (or /q #) - Set Clustered DDS/CRN quality factor [0-255] 255=best"); console::printf(" DDS default quality is best possible."); console::printf(" CRN default quality is %u.", cDefaultCRNQualityLevel); console::printf("-bitrate # - Set the desired output bitrate of DDS or CRN output files."); console::printf(" This option causes crunch to find the quality factor"); console::printf(" closest to the desired bitrate using a binary search."); console::message("\nLow-level CRN specific options:"); console::printf("-c # - Color endpoint palette size, 32-8192, default=3072"); console::printf("-s # - Color selector palette size, 32-8192, default=3072"); console::printf("-ca # - Alpha endpoint palette size, 32-8192, default=3072"); console::printf("-sa # - Alpha selector palette size, 32-8192, default=3072"); // ------------------------------------------------------------------------------- console::message("\nMipmap filtering options:"); console::printf("-mipMode [UseSourceOrGenerate,UseSource,Generate,None]"); console::printf(" Default mipMode is UseSourceOrGenerate"); console::printf(" UseSourceOrGenerate: Use source mipmaps if possible, or create new mipmaps."); console::printf(" UseSource: Always use source mipmaps, if any (never generate new mipmaps)"); console::printf(" Generate: Always generate a new mipmap chain (ignore source mipmaps)"); console::printf(" None: Do not output any mipmaps"); console::printf("-mipFilter [box,tent,lanczos4,mitchell,kaiser], default=kaiser"); console::printf("-gamma # - Mipmap gamma correction value, default=2.2, use 1.0 for linear"); console::printf("-blurriness # - Scale filter kernel, >1=blur, <1=sharpen, .01-8, default=.9"); console::printf("-wrap - Assume texture is tiled when filtering, default=clamping"); console::printf("-renormalize - Renormalize filtered normal map texels, default=disabled"); console::printf("-maxmips # - Limit number of generated texture mipmap levels, 1-16, default=16"); console::printf("-minmipsize # - Smallest allowable mipmap resolution, default=1"); console::message("\nCompression options:"); console::printf("-alphaThreshold # - Set DXT1A alpha threshold, 0-255, default=128"); console::printf(" Note: -alphaThreshold also changes the compressor's behavior to"); console::printf(" prefer DXT1A over DXT5 for images with alpha channels (.DDS only)."); console::printf("-uniformMetrics - Use uniform color metrics, default=use perceptual metrics"); console::printf("-noAdaptiveBlocks - Disable adaptive block sizes (i.e. disable macroblocks)."); #ifdef CRNLIB_SUPPORT_ATI_COMPRESS console::printf("-compressor [CRN,CRNF,RYG,ATI] - Set DXTn compressor, default=CRN"); #else console::printf("-compressor [CRN,CRNF,RYG] - Set DXTn compressor, default=CRN"); #endif console::printf("-dxtQuality [superfast,fast,normal,better,uber] - Endpoint optimizer speed."); console::printf(" Sets endpoint optimizer's max iteration depth. Default=uber."); console::printf("-noendpointcaching - Don't try reusing previous DXT endpoint solutions."); console::printf("-grayscalsampling - Assume shader will convert fetched results to luma (Y)."); console::printf("-forceprimaryencoding - Only use DXT1 color4 and DXT5 alpha8 block encodings."); console::printf("-usetransparentindicesforblack - Try DXT1 transparent indices for dark pixels."); console::message("\nOuptut pixel format options:"); console::printf("-usesourceformat - Use input file's format for output format (when possible)."); console::message("\nAll supported texture formats (Note: .CRN only supports DXTn pixel formats):"); for (uint32 i = 0; i < pixel_format_helpers::get_num_formats(); i++) { pixel_format fmt = pixel_format_helpers::get_pixel_format_by_index(i); console::printf("-%s", pixel_format_helpers::get_pixel_format_string(fmt)); } console::printf("\nFor bugs, support, or feedback: info@binomial.info"); } bool convert(const char* pCommand_line) { m_num_processed = 0; m_num_failed = 0; m_num_succeeded = 0; m_num_skipped = 0; command_line_params::param_desc std_params[] = { { "file", 1, true }, { "out", 1, false }, { "outdir", 1, false }, { "outsamedir", 0, false }, { "deep", 0, false }, { "fileformat", 1, false }, { "helperThreads", 1, false }, { "noprogress", 0, false }, { "quiet", 0, false }, { "ignoreerrors", 0, false }, { "logfile", 1, false }, { "q", 1, false }, { "quality", 1, false }, { "c", 1, false }, { "s", 1, false }, { "ca", 1, false }, { "sa", 1, false }, { "mipMode", 1, false }, { "mipFilter", 1, false }, { "gamma", 1, false }, { "blurriness", 1, false }, { "wrap", 0, false }, { "renormalize", 0, false }, { "noprogress", 0, false }, { "paramdebug", 0, false }, { "debug", 0, false }, { "quick", 0, false }, { "imagestats", 0, false }, { "nostats", 0, false }, { "mipstats", 0, false }, { "alphaThreshold", 1, false }, { "uniformMetrics", 0, false }, { "noAdaptiveBlocks", 0, false }, { "compressor", 1, false }, { "dxtQuality", 1, false }, { "noendpointcaching", 0, false }, { "grayscalesampling", 0, false }, { "converttoluma", 0, false }, { "setalphatoluma", 0, false }, { "pause", 0, false }, { "timestamp", 0, false }, { "nooverwrite", 0, false }, { "forcewrite", 0, false }, { "recreate", 0, false }, { "compare", 0, false }, { "info", 0, false }, { "forceprimaryencoding", 0, false }, { "usetransparentindicesforblack", 0, false }, { "usesourceformat", 0, false }, { "rescalemode", 1, false }, { "rescale", 2, false }, { "relrescale", 2, false }, { "clamp", 2, false }, { "clampScale", 2, false }, { "window", 4, false }, { "maxmips", 1, false }, { "minmipsize", 1, false }, { "bitrate", 1, false }, { "lzmastats", 0, false }, { "split", 0, false }, { "csvfile", 1, false }, { "yflip", 0, false }, { "unflip", 0, false }, }; crnlib::vector params; params.append(std_params, sizeof(std_params) / sizeof(std_params[0])); for (uint32 i = 0; i < pixel_format_helpers::get_num_formats(); i++) { pixel_format fmt = pixel_format_helpers::get_pixel_format_by_index(i); command_line_params::param_desc desc; desc.m_pName = pixel_format_helpers::get_pixel_format_string(fmt); desc.m_num_values = 0; desc.m_support_listing_file = false; params.push_back(desc); } if (!m_params.parse(pCommand_line, params.size(), params.get_ptr(), true)) { return false; } if (!m_params.get_num_params()) { console::error("No command line parameters specified!"); print_usage(); return false; } #if 0 if (m_params.get_count("")) { console::error("Unrecognized command line parameter: \"%s\"", m_params.get_value_as_string_or_empty("", 0).get_ptr()); return false; } #endif if (m_params.get_value_as_bool("debug")) { console::debug("Command line parameters:"); for (command_line_params::param_map_const_iterator it = m_params.begin(); it != m_params.end(); ++it) { console::disable_crlf(); console::debug("Key:\"%s\" Values (%u): ", it->first.get_ptr(), it->second.m_values.size()); for (uint32 i = 0; i < it->second.m_values.size(); i++) console::debug("\"%s\" ", it->second.m_values[i].get_ptr()); console::debug("\n"); console::enable_crlf(); } } dynamic_string log_filename; if (m_params.get_value_as_string("logfile", 0, log_filename)) { if (!m_log_stream.open(log_filename.get_ptr(), cDataStreamWritable | cDataStreamSeekable, true)) { console::error("Unable to open log file: \"%s\"", log_filename.get_ptr()); return false; } console::printf("Appending output to log file \"%s\"", log_filename.get_ptr()); console::set_log_stream(&m_log_stream); } bool status = convert(); if (m_log_stream.is_opened()) { console::set_log_stream(NULL); m_log_stream.close(); } return status; } private: command_line_params m_params; bool convert() { find_files::file_desc_vec files; uint32 total_input_specs = 0; for (uint32 phase = 0; phase < 2; phase++) { command_line_params::param_map_const_iterator begin, end; m_params.find(phase ? "" : "file", begin, end); for (command_line_params::param_map_const_iterator it = begin; it != end; ++it) { total_input_specs++; const dynamic_string_array& strings = it->second.m_values; for (uint32 i = 0; i < strings.size(); i++) { if (!process_input_spec(files, strings[i])) { if (!m_params.get_value_as_bool("ignoreerrors")) return false; } } } } if (!total_input_specs) { console::error("No input files specified!"); return false; } if (files.empty()) { console::error("No files found to process!"); return false; } std::sort(files.begin(), files.end()); files.resize((uint32)(std::unique(files.begin(), files.end()) - files.begin())); timer tm; tm.start(); if (!process_files(files)) { if (!m_params.get_value_as_bool("ignoreerrors")) return false; } double total_time = tm.get_elapsed_secs(); console::printf("Total time: %3.3fs", total_time); console::printf( ((m_num_skipped) || (m_num_failed)) ? cWarningConsoleMessage : cInfoConsoleMessage, "%u total file(s) successfully processed, %u file(s) skipped, %u file(s) failed.", m_num_succeeded, m_num_skipped, m_num_failed); return true; } bool process_input_spec(find_files::file_desc_vec& files, const dynamic_string& input_spec) { dynamic_string find_name(input_spec); if ((find_name.get_len()) && (file_utils::does_dir_exist(find_name.get_ptr()))) { file_utils::combine_path(find_name, find_name.get_ptr(), "*"); } if ((find_name.is_empty()) || (!file_utils::full_path(find_name))) { console::error("Invalid input filename: %s", find_name.get_ptr()); return false; } const bool deep_flag = m_params.get_value_as_bool("deep"); dynamic_string find_drive, find_path, find_fname, find_ext; file_utils::split_path(find_name.get_ptr(), &find_drive, &find_path, &find_fname, &find_ext); dynamic_string find_pathname; file_utils::combine_path(find_pathname, find_drive.get_ptr(), find_path.get_ptr()); dynamic_string find_filename; find_filename = find_fname + find_ext; find_files file_finder; bool success = file_finder.find(find_pathname.get_ptr(), find_filename.get_ptr(), find_files::cFlagAllowFiles | (deep_flag ? find_files::cFlagRecursive : 0)); if (!success) { console::error("Failed finding files: %s", find_name.get_ptr()); return false; } if (file_finder.get_files().empty()) { console::warning("No files found: %s", find_name.get_ptr()); return true; } files.append(file_finder.get_files()); return true; } bool read_only_file_check(const char* pDst_filename) { if (!file_utils::is_read_only(pDst_filename)) return true; if (m_params.get_value_as_bool("forcewrite")) { if (file_utils::disable_read_only(pDst_filename)) { console::warning("Setting read-only file \"%s\" to writable", pDst_filename); return true; } else { console::error("Failed setting read-only file \"%s\" to writable!", pDst_filename); return false; } } console::error("Output file \"%s\" is read-only!", pDst_filename); return false; } bool process_files(find_files::file_desc_vec& files) { const bool compare_mode = m_params.get_value_as_bool("compare"); const bool info_mode = m_params.get_value_as_bool("info"); for (uint32 file_index = 0; file_index < files.size(); file_index++) { const find_files::file_desc& file_desc = files[file_index]; const dynamic_string& in_filename = file_desc.m_fullname; dynamic_string in_drive, in_path, in_fname, in_ext; file_utils::split_path(in_filename.get_ptr(), &in_drive, &in_path, &in_fname, &in_ext); texture_file_types::format out_file_type = texture_file_types::cFormatCRN; dynamic_string fmt; if (m_params.get_value_as_string("fileformat", 0, fmt)) { if (fmt == "tga") out_file_type = texture_file_types::cFormatTGA; else if (fmt == "bmp") out_file_type = texture_file_types::cFormatBMP; else if (fmt == "dds") out_file_type = texture_file_types::cFormatDDS; else if (fmt == "ktx") out_file_type = texture_file_types::cFormatKTX; else if (fmt == "crn") out_file_type = texture_file_types::cFormatCRN; else if (fmt == "png") out_file_type = texture_file_types::cFormatPNG; else { console::error("Unsupported output file type: %s", fmt.get_ptr()); return false; } } // No explicit output format has been specified - try to determine something doable. if (!m_params.has_key("fileformat")) { if (m_params.has_key("split")) { out_file_type = texture_file_types::cFormatPNG; } else { texture_file_types::format input_file_type = texture_file_types::determine_file_format(in_filename.get_ptr()); if (input_file_type == texture_file_types::cFormatCRN) { // Automatically transcode CRN->DXTc and write to DDS files, unless the user specifies either the /fileformat or /split options. out_file_type = texture_file_types::cFormatDDS; } else if (input_file_type == texture_file_types::cFormatKTX) { // Default to converting KTX files to PNG out_file_type = texture_file_types::cFormatPNG; } } } dynamic_string out_filename; if (m_params.get_value_as_bool("outsamedir")) out_filename.format("%s%s%s.%s", in_drive.get_ptr(), in_path.get_ptr(), in_fname.get_ptr(), texture_file_types::get_extension(out_file_type)); else if (m_params.has_key("out")) { out_filename = m_params.get_value_as_string_or_empty("out"); if (files.size() > 1) { dynamic_string out_drive, out_dir, out_name, out_ext; file_utils::split_path(out_filename.get_ptr(), &out_drive, &out_dir, &out_name, &out_ext); out_name.format("%s_%u", out_name.get_ptr(), file_index); out_filename.format("%s%s%s%s", out_drive.get_ptr(), out_dir.get_ptr(), out_name.get_ptr(), out_ext.get_ptr()); } if (!m_params.has_key("fileformat")) out_file_type = texture_file_types::determine_file_format(out_filename.get_ptr()); } else { dynamic_string out_dir(m_params.get_value_as_string_or_empty("outdir")); if (m_params.get_value_as_bool("recreate") && file_desc.m_rel.get_len()) { file_utils::combine_path(out_dir, out_dir.get_ptr(), file_desc.m_rel.get_ptr()); } if (out_dir.get_len()) { if (file_utils::is_path_separator(out_dir.back())) out_filename.format("%s%s.%s", out_dir.get_ptr(), in_fname.get_ptr(), texture_file_types::get_extension(out_file_type)); else out_filename.format("%s\\%s.%s", out_dir.get_ptr(), in_fname.get_ptr(), texture_file_types::get_extension(out_file_type)); } else { out_filename.format("%s.%s", in_fname.get_ptr(), texture_file_types::get_extension(out_file_type)); } if (m_params.get_value_as_bool("recreate")) { if (file_utils::full_path(out_filename)) { if ((!compare_mode) && (!info_mode)) { dynamic_string out_drive, out_path; file_utils::split_path(out_filename.get_ptr(), &out_drive, &out_path, NULL, NULL); out_drive += out_path; file_utils::create_path(out_drive.get_ptr()); } } } } if ((!compare_mode) && (!info_mode)) { if (file_utils::does_file_exist(out_filename.get_ptr())) { if (m_params.get_value_as_bool("nooverwrite")) { console::warning("Skipping already existing file: %s\n", out_filename.get_ptr()); m_num_skipped++; continue; } if (m_params.get_value_as_bool("timestamp")) { if (file_utils::is_older_than(in_filename.get_ptr(), out_filename.get_ptr())) { console::warning("Skipping up to date file: %s\n", out_filename.get_ptr()); m_num_skipped++; continue; } } } } convert_status status = cCSFailed; if (info_mode) status = display_file_info(file_index, files.size(), in_filename.get_ptr()); else if (compare_mode) status = compare_file(file_index, files.size(), in_filename.get_ptr(), out_filename.get_ptr(), out_file_type); else if (read_only_file_check(out_filename.get_ptr())) status = convert_file(file_index, files.size(), in_filename.get_ptr(), out_filename.get_ptr(), out_file_type); m_num_processed++; switch (status) { case cCSSucceeded: { console::info(""); m_num_succeeded++; break; } case cCSSkipped: { console::info("Skipping file.\n"); m_num_skipped++; break; } case cCSBadParam: { return false; } default: { if (!m_params.get_value_as_bool("ignoreerrors")) return false; console::info(""); m_num_failed++; break; } } } return true; } void print_texture_info(const char* pTex_desc, texture_conversion::convert_params& params, mipmapped_texture& tex) { console::info("%s: %ux%u, Levels: %u, Faces: %u, Format: %s", pTex_desc, tex.get_width(), tex.get_height(), tex.get_num_levels(), tex.get_num_faces(), pixel_format_helpers::get_pixel_format_string(tex.get_format())); console::disable_crlf(); console::info("Apparent type: %s, ", get_texture_type_desc(params.m_texture_type)); console::info("Flags: "); if (tex.get_comp_flags() & pixel_format_helpers::cCompFlagRValid) console::info("R "); if (tex.get_comp_flags() & pixel_format_helpers::cCompFlagGValid) console::info("G "); if (tex.get_comp_flags() & pixel_format_helpers::cCompFlagBValid) console::info("B "); if (tex.get_comp_flags() & pixel_format_helpers::cCompFlagAValid) console::info("A "); if (tex.get_comp_flags() & pixel_format_helpers::cCompFlagGrayscale) console::info("Grayscale "); if (tex.get_comp_flags() & pixel_format_helpers::cCompFlagNormalMap) console::info("NormalMap "); if (tex.get_comp_flags() & pixel_format_helpers::cCompFlagLumaChroma) console::info("LumaChroma "); if (tex.is_flipped()) console::info("Flipped "); else console::info("Non-Flipped "); console::info("\n"); console::enable_crlf(); } static bool progress_callback_func(uint32 percentage_complete, void* pUser_data_ptr) { pUser_data_ptr; console::disable_crlf(); char buf[8]; for (uint32 i = 0; i < 7; i++) buf[i] = 8; buf[7] = '\0'; for (uint32 i = 0; i < 130/8; i++) console::progress(buf); console::progress("Processing: %u%%", percentage_complete); for (uint32 i = 0; i < 7; i++) buf[i] = ' '; console::progress(buf); console::progress(buf); for (uint32 i = 0; i < 7; i++) buf[i] = 8; console::progress(buf); console::progress(buf); console::enable_crlf(); return true; } bool parse_mipmap_params(crn_mipmap_params& mip_params) { dynamic_string val; if (m_params.get_value_as_string("mipMode", 0, val)) { uint32 i; for (i = 0; i < cCRNMipModeTotal; i++) { if (val == crn_get_mip_mode_name( static_cast(i) )) { mip_params.m_mode = static_cast(i); break; } } if (i == cCRNMipModeTotal) { console::error("Invalid MipMode: \"%s\"", val.get_ptr()); return false; } } if (m_params.get_value_as_string("mipFilter", 0, val)) { uint32 i; for (i = 0; i < cCRNMipFilterTotal; i++) { if (val == dynamic_string(crn_get_mip_filter_name( static_cast(i) )) ) { mip_params.m_filter = static_cast(i); break; } } if (i == cCRNMipFilterTotal) { console::error("Invalid MipFilter: \"%s\"", val.get_ptr()); return false; } if (i == cCRNMipFilterBox) mip_params.m_blurriness = 1.0f; } mip_params.m_gamma = m_params.get_value_as_float("gamma", 0, mip_params.m_gamma, .1f, 8.0f); mip_params.m_gamma_filtering = (mip_params.m_gamma != 1.0f); mip_params.m_blurriness = m_params.get_value_as_float("blurriness", 0, mip_params.m_blurriness, .01f, 8.0f); mip_params.m_renormalize = m_params.get_value_as_bool("renormalize", 0, mip_params.m_renormalize != 0); mip_params.m_tiled = m_params.get_value_as_bool("wrap"); mip_params.m_max_levels = m_params.get_value_as_int("maxmips", 0, cCRNMaxLevels, 1, cCRNMaxLevels); mip_params.m_min_mip_size = m_params.get_value_as_int("minmipsize", 0, 1, 1, cCRNMaxLevelResolution); return true; } bool parse_scale_params(crn_mipmap_params &mipmap_params) { if (m_params.has_key("rescale")) { int w = m_params.get_value_as_int("rescale", 0, -1, 1, cCRNMaxLevelResolution, 0); int h = m_params.get_value_as_int("rescale", 0, -1, 1, cCRNMaxLevelResolution, 1); mipmap_params.m_scale_mode = cCRNSMAbsolute; mipmap_params.m_scale_x = (float)w; mipmap_params.m_scale_y = (float)h; } else if (m_params.has_key("relrescale")) { float w = m_params.get_value_as_float("relrescale", 0, 1, 1, 256, 0); float h = m_params.get_value_as_float("relrescale", 0, 1, 1, 256, 1); mipmap_params.m_scale_mode = cCRNSMRelative; mipmap_params.m_scale_x = w; mipmap_params.m_scale_y = h; } else if (m_params.has_key("rescalemode")) { // nearest | hi | lo dynamic_string mode_str(m_params.get_value_as_string_or_empty("rescalemode")); if (mode_str == "nearest") mipmap_params.m_scale_mode = cCRNSMNearestPow2; else if (mode_str == "hi") mipmap_params.m_scale_mode = cCRNSMNextPow2; else if (mode_str == "lo") mipmap_params.m_scale_mode = cCRNSMLowerPow2; else { console::error("Invalid rescale mode: \"%s\"", mode_str.get_ptr()); return false; } } if (m_params.has_key("clamp")) { uint32 w = m_params.get_value_as_int("clamp", 0, 1, 1, cCRNMaxLevelResolution, 0); uint32 h = m_params.get_value_as_int("clamp", 0, 1, 1, cCRNMaxLevelResolution, 1); mipmap_params.m_clamp_scale = false; mipmap_params.m_clamp_width = w; mipmap_params.m_clamp_height = h; } else if (m_params.has_key("clampScale")) { uint32 w = m_params.get_value_as_int("clampscale", 0, 1, 1, cCRNMaxLevelResolution, 0); uint32 h = m_params.get_value_as_int("clampscale", 0, 1, 1, cCRNMaxLevelResolution, 1); mipmap_params.m_clamp_scale = true; mipmap_params.m_clamp_width = w; mipmap_params.m_clamp_height = h; } if (m_params.has_key("window")) { uint32 xl = m_params.get_value_as_int("window", 0, 0, 0, cCRNMaxLevelResolution, 0); uint32 yl = m_params.get_value_as_int("window", 0, 0, 0, cCRNMaxLevelResolution, 1); uint32 xh = m_params.get_value_as_int("window", 0, 0, 0, cCRNMaxLevelResolution, 2); uint32 yh = m_params.get_value_as_int("window", 0, 0, 0, cCRNMaxLevelResolution, 3); mipmap_params.m_window_left = math::minimum(xl, xh); mipmap_params.m_window_top = math::minimum(yl, yh); mipmap_params.m_window_right = math::maximum(xl, xh); mipmap_params.m_window_bottom = math::maximum(yl, yh); } return true; } bool parse_comp_params(texture_file_types::format dst_file_format, crn_comp_params &comp_params) { if (dst_file_format == texture_file_types::cFormatCRN) comp_params.m_quality_level = cDefaultCRNQualityLevel; if (m_params.has_key("q") || m_params.has_key("quality")) { const char *pKeyName = m_params.has_key("q") ? "q" : "quality"; if ((dst_file_format == texture_file_types::cFormatDDS) || (dst_file_format == texture_file_types::cFormatCRN) || (dst_file_format == texture_file_types::cFormatKTX)) { uint32 i = m_params.get_value_as_int(pKeyName, 0, cDefaultCRNQualityLevel, 0, cCRNMaxQualityLevel); comp_params.m_quality_level = i; } else { console::error("/quality or /q option is only invalid when writing DDS, KTX or CRN files!"); return false; } } else { float desired_bitrate = m_params.get_value_as_float("bitrate", 0, 0.0f, .1f, 30.0f); if (desired_bitrate > 0.0f) { comp_params.m_target_bitrate = desired_bitrate; } } int color_endpoints = m_params.get_value_as_int("c", 0, 0, cCRNMinPaletteSize, cCRNMaxPaletteSize); int color_selectors = m_params.get_value_as_int("s", 0, 0, cCRNMinPaletteSize, cCRNMaxPaletteSize); int alpha_endpoints = m_params.get_value_as_int("ca", 0, 0, cCRNMinPaletteSize, cCRNMaxPaletteSize); int alpha_selectors = m_params.get_value_as_int("sa", 0, 0, cCRNMinPaletteSize, cCRNMaxPaletteSize); if ( ((color_endpoints > 0) && (color_selectors > 0)) || ((alpha_endpoints > 0) && (alpha_selectors > 0)) ) { comp_params.set_flag(cCRNCompFlagManualPaletteSizes, true); comp_params.m_crn_color_endpoint_palette_size = color_endpoints; comp_params.m_crn_color_selector_palette_size = color_selectors; comp_params.m_crn_alpha_endpoint_palette_size = alpha_endpoints; comp_params.m_crn_alpha_selector_palette_size = alpha_selectors; } if (m_params.has_key("alphaThreshold")) { int dxt1a_alpha_threshold = m_params.get_value_as_int("alphaThreshold", 0, 128, 0, 255); comp_params.m_dxt1a_alpha_threshold = dxt1a_alpha_threshold; if (dxt1a_alpha_threshold > 0) { comp_params.set_flag(cCRNCompFlagDXT1AForTransparency, true); } } comp_params.set_flag(cCRNCompFlagPerceptual, !m_params.get_value_as_bool("uniformMetrics")); comp_params.set_flag(cCRNCompFlagHierarchical, !m_params.get_value_as_bool("noAdaptiveBlocks")); if (m_params.has_key("helperThreads")) comp_params.m_num_helper_threads = m_params.get_value_as_int("helperThreads", 0, cCRNMaxHelperThreads, 0, cCRNMaxHelperThreads); else if (g_number_of_processors > 1) comp_params.m_num_helper_threads = g_number_of_processors - 1; dynamic_string comp_name; if (m_params.get_value_as_string("compressor", 0, comp_name)) { uint32 i; for (i = 0; i < cCRNTotalDXTCompressors; i++) { if (comp_name == get_dxt_compressor_name(static_cast(i))) { comp_params.m_dxt_compressor_type = static_cast(i); break; } } if (i == cCRNTotalDXTCompressors) { console::error("Invalid compressor: \"%s\"", comp_name.get_ptr()); return false; } } dynamic_string dxt_quality_str; if (m_params.get_value_as_string("dxtquality", 0, dxt_quality_str)) { uint32 i; for (i = 0; i < cCRNDXTQualityTotal; i++) { if (dxt_quality_str == crn_get_dxt_quality_string(static_cast(i))) { comp_params.m_dxt_quality = static_cast(i); break; } } if (i == cCRNDXTQualityTotal) { console::error("Invalid DXT quality: \"%s\"", dxt_quality_str.get_ptr()); return false; } } else { comp_params.m_dxt_quality = cCRNDXTQualityUber; } comp_params.set_flag(cCRNCompFlagDisableEndpointCaching, m_params.get_value_as_bool("noendpointcaching")); comp_params.set_flag(cCRNCompFlagGrayscaleSampling, m_params.get_value_as_bool("grayscalesampling")); comp_params.set_flag(cCRNCompFlagUseBothBlockTypes, !m_params.get_value_as_bool("forceprimaryencoding")); if (comp_params.get_flag(cCRNCompFlagUseBothBlockTypes)) comp_params.set_flag(cCRNCompFlagUseTransparentIndicesForBlack, m_params.get_value_as_bool("usetransparentindicesforblack")); else comp_params.set_flag(cCRNCompFlagUseTransparentIndicesForBlack, false); return true; } convert_status display_file_info(uint32 file_index, uint32 num_files, const char* pSrc_filename) { if (num_files > 1) console::message("[%u/%u] Source texture: \"%s\"", file_index + 1, num_files, pSrc_filename); else console::message("Source texture: \"%s\"", pSrc_filename); texture_file_types::format src_file_format = texture_file_types::determine_file_format(pSrc_filename); if (src_file_format == texture_file_types::cFormatInvalid) { console::error("Unrecognized file type: %s", pSrc_filename); return cCSFailed; } mipmapped_texture src_tex; if (!src_tex.read_from_file(pSrc_filename, src_file_format)) { if (src_tex.get_last_error().is_empty()) console::error("Failed reading source file: \"%s\"", pSrc_filename); else console::error("%s", src_tex.get_last_error().get_ptr()); return cCSFailed; } uint64 input_file_size; file_utils::get_file_size(pSrc_filename, input_file_size); uint32 total_in_pixels = 0; for (uint32 i = 0; i < src_tex.get_num_levels(); i++) { uint32 width = math::maximum(1, src_tex.get_width() >> i); uint32 height = math::maximum(1, src_tex.get_height() >> i); total_in_pixels += width*height*src_tex.get_num_faces(); } vector src_tex_bytes; if (!cfile_stream::read_file_into_array(pSrc_filename, src_tex_bytes)) { console::error("Failed loading source file: %s", pSrc_filename); return cCSFailed; } if (!src_tex_bytes.size()) { console::warning("Source file is empty: %s", pSrc_filename); return cCSSkipped; } uint32 compressed_size = 0; if (m_params.has_key("lzmastats")) { lzma_codec lossless_codec; vector cmp_tex_bytes; if (lossless_codec.pack(src_tex_bytes.get_ptr(), src_tex_bytes.size(), cmp_tex_bytes)) { compressed_size = cmp_tex_bytes.size(); } } console::info("Source texture dimensions: %ux%u, Levels: %u, Faces: %u, Format: %s\nPacked Format: %u, Apparent Type: %s, Flipped: %u, Can Unflip Without Unpacking: %u", src_tex.get_width(), src_tex.get_height(), src_tex.get_num_levels(), src_tex.get_num_faces(), pixel_format_helpers::get_pixel_format_string(src_tex.get_format()), src_tex.is_packed(), get_texture_type_desc(src_tex.determine_texture_type()), src_tex.is_flipped(), src_tex.can_unflip_without_unpacking()); console::info("Total pixels: %u, Source file size: " CRNLIB_UINT64_FORMAT_SPECIFIER ", Source file bits/pixel: %1.3f", total_in_pixels, input_file_size, (input_file_size * 8.0f) / total_in_pixels); if (compressed_size) { console::info("LZMA compressed file size: %u bytes, %1.3f bits/pixel", compressed_size, compressed_size * 8.0f / total_in_pixels); } double entropy = math::compute_entropy(src_tex_bytes.get_ptr(), src_tex_bytes.size()); console::info("Source file entropy: %3.6f bits per byte", entropy / src_tex_bytes.size()); if (src_file_format == texture_file_types::cFormatCRN) { crnd::crn_texture_info tex_info; tex_info.m_struct_size = sizeof(crnd::crn_texture_info); crn_bool success = crnd::crnd_get_texture_info(src_tex_bytes.get_ptr(), src_tex_bytes.size(), &tex_info); if (!success) console::error("Failed retrieving CRN texture info!"); else { console::info("CRN texture info:"); console::info("Width: %u, Height: %u, Levels: %u, Faces: %u\nBytes per block: %u, User0: 0x%08X, User1: 0x%08X, CRN Format: %u", tex_info.m_width, tex_info.m_height, tex_info.m_levels, tex_info.m_faces, tex_info.m_bytes_per_block, tex_info.m_userdata0, tex_info.m_userdata1, tex_info.m_format); } } return cCSSucceeded; } void print_stats(texture_conversion::convert_stats &stats, bool force_image_stats = false) { dynamic_string csv_filename; const char *pCSVStatsFilename = m_params.get_value_as_string("csvfile", 0, csv_filename) ? csv_filename.get_ptr() : NULL; bool image_stats = force_image_stats || m_params.get_value_as_bool("imagestats") || m_params.get_value_as_bool("mipstats") || (pCSVStatsFilename != NULL); bool mip_stats = m_params.get_value_as_bool("mipstats"); bool grayscale_sampling = m_params.get_value_as_bool("grayscalesampling"); if (!stats.print(image_stats, mip_stats, grayscale_sampling, pCSVStatsFilename)) { console::warning("Unable to compute/display full output file statistics."); } } convert_status compare_file(uint32 file_index, uint32 num_files, const char* pSrc_filename, const char* pDst_filename, texture_file_types::format out_file_type) { if (num_files > 1) console::message("[%u/%u] Comparing source texture \"%s\" to output texture \"%s\"", file_index + 1, num_files, pSrc_filename, pDst_filename); else console::message("Comparing source texture \"%s\" to output texture \"%s\"", pSrc_filename, pDst_filename); texture_file_types::format src_file_format = texture_file_types::determine_file_format(pSrc_filename); if (src_file_format == texture_file_types::cFormatInvalid) { console::error("Unrecognized file type: %s", pSrc_filename); return cCSFailed; } mipmapped_texture src_tex; if (!src_tex.read_from_file(pSrc_filename, src_file_format)) { if (src_tex.get_last_error().is_empty()) console::error("Failed reading source file: \"%s\"", pSrc_filename); else console::error("%s", src_tex.get_last_error().get_ptr()); return cCSFailed; } texture_conversion::convert_stats stats; if (!stats.init(pSrc_filename, pDst_filename, src_tex, out_file_type, m_params.has_key("lzmastats"))) return cCSFailed; print_stats(stats, true); return cCSSucceeded; } convert_status convert_file(uint32 file_index, uint32 num_files, const char* pSrc_filename, const char* pDst_filename, texture_file_types::format out_file_type) { timer tim; if (num_files > 1) console::message("[%u/%u] Reading source texture: \"%s\"", file_index + 1, num_files, pSrc_filename); else console::message("Reading source texture: \"%s\"", pSrc_filename); texture_file_types::format src_file_format = texture_file_types::determine_file_format(pSrc_filename); if (src_file_format == texture_file_types::cFormatInvalid) { console::error("Unrecognized file type: %s", pSrc_filename); return cCSFailed; } mipmapped_texture src_tex; tim.start(); if (!src_tex.read_from_file(pSrc_filename, src_file_format)) { if (src_tex.get_last_error().is_empty()) console::error("Failed reading source file: \"%s\"", pSrc_filename); else console::error("%s", src_tex.get_last_error().get_ptr()); return cCSFailed; } double total_time = tim.get_elapsed_secs(); console::info("Texture successfully loaded in %3.3fs", total_time); if (m_params.get_value_as_bool("converttoluma")) src_tex.convert(image_utils::cConversion_Y_To_RGB); if (m_params.get_value_as_bool("setalphatoluma")) src_tex.convert(image_utils::cConversion_Y_To_A); texture_conversion::convert_params params; params.m_texture_type = src_tex.determine_texture_type(); params.m_pInput_texture = &src_tex; params.m_dst_filename = pDst_filename; params.m_dst_file_type = out_file_type; params.m_lzma_stats = m_params.has_key("lzmastats"); params.m_write_mipmaps_to_multiple_files = m_params.has_key("split"); params.m_always_use_source_pixel_format = m_params.has_key("usesourceformat"); params.m_y_flip = m_params.has_key("yflip"); params.m_unflip = m_params.has_key("unflip"); if ((!m_params.get_value_as_bool("noprogress")) && (!m_params.get_value_as_bool("quiet"))) params.m_pProgress_func = progress_callback_func; if (m_params.get_value_as_bool("debug")) { params.m_debugging = true; params.m_comp_params.set_flag(cCRNCompFlagDebugging, true); } if (m_params.get_value_as_bool("paramdebug")) params.m_param_debugging = true; if (m_params.get_value_as_bool("quick")) params.m_quick = true; params.m_no_stats = m_params.get_value_as_bool("nostats"); params.m_dst_format = PIXEL_FMT_INVALID; for (uint32 i = 0; i < pixel_format_helpers::get_num_formats(); i++) { pixel_format trial_fmt = pixel_format_helpers::get_pixel_format_by_index(i); if (m_params.has_key(pixel_format_helpers::get_pixel_format_string(trial_fmt))) { params.m_dst_format = trial_fmt; break; } } if (texture_file_types::supports_mipmaps(src_file_format)) { params.m_mipmap_params.m_mode = cCRNMipModeUseSourceMips; } if (!parse_mipmap_params(params.m_mipmap_params)) return cCSBadParam; if (!parse_comp_params(params.m_dst_file_type, params.m_comp_params)) return cCSBadParam; if (!parse_scale_params(params.m_mipmap_params)) return cCSBadParam; print_texture_info("Source texture", params, src_tex); if (params.m_texture_type == cTextureTypeNormalMap) { params.m_comp_params.set_flag(cCRNCompFlagPerceptual, false); } texture_conversion::convert_stats stats; tim.start(); bool status = texture_conversion::process(params, stats); total_time = tim.get_elapsed_secs(); if (!status) { if (params.m_error_message.is_empty()) console::error("Failed writing output file: \"%s\"", pDst_filename); else console::error(params.m_error_message.get_ptr()); return cCSFailed; } console::info("Texture successfully processed in %3.3fs", total_time); if (!m_params.get_value_as_bool("nostats")) print_stats(stats); return cCSSucceeded; } }; //----------------------------------------------------------------------------------------------------------------------- static bool check_for_option(int argc, char *argv[], const char *pOption) { for (int i = 1; i < argc; i++) { if ((argv[i][0] == '/') || (argv[i][0] == '-')) { if (crn_stricmp(&argv[i][1], pOption) == 0) return true; } } return false; } //----------------------------------------------------------------------------------------------------------------------- static void print_title() { console::printf("crunch: Advanced DXTn Texture Compressor - https://github.com/BinomialLLC/crunch"); console::printf("Copyright (c) 2010-2016 Richard Geldreich, Jr. and Binomial LLC"); console::printf("crnlib version v%u.%02u %s Built %s, %s", CRNLIB_VERSION / 100U, CRNLIB_VERSION % 100U, crnlib_is_x64() ? "x64" : "x86", __DATE__, __TIME__); console::printf(""); } //----------------------------------------------------------------------------------------------------------------------- static int main_internal(int argc, char *argv[]) { argc; argv; colorized_console::init(); if (check_for_option(argc, argv, "quiet")) console::disable_output(); print_title(); dynamic_string cmd_line; get_command_line_as_single_string(cmd_line, argc, argv); bool status = false; if (check_for_option(argc, argv, "corpus_gen")) { corpus_gen generator; status = generator.generate(cmd_line.get_ptr()); } else if (check_for_option(argc, argv, "corpus_test")) { corpus_tester tester; status = tester.test(cmd_line.get_ptr()); } else { crunch converter; status = converter.convert(cmd_line.get_ptr()); } colorized_console::deinit(); crnlib_print_mem_stats(); return status ? EXIT_SUCCESS : EXIT_FAILURE; } static void pause_and_wait(void) { console::enable_output(); console::message("\nPress a key to continue."); for ( ; ; ) { if (crn_getch() != -1) break; } } //----------------------------------------------------------------------------------------------------------------------- int main(int argc, char *argv[]) { int status = EXIT_FAILURE; if (crnlib_is_debugger_present()) { status = main_internal(argc, argv); } else { #ifdef _MSC_VER __try { status = main_internal(argc, argv); } __except(EXCEPTION_EXECUTE_HANDLER) { console::error("Uncached exception! crunch command line tool failed!"); } #else status = main_internal(argc, argv); #endif } console::printf("\nExit status: %i", status); if (check_for_option(argc, argv, "pause")) { if ((status == EXIT_FAILURE) || (console::get_num_messages(cErrorConsoleMessage))) pause_and_wait(); } return status; }