Files
unity/crunch/crunch.cpp
T
Alexander Suvorov f284523b15 Add compression support for ETC1 textures
Explanation:

Crunch algorithms are normally used for compression of DXTn textures. However, Crunch algorithms are much more powerful, and with some minor adjustments, those algorithms can be directly used to compress other texture formats. For example, the current commit demonstrates how to use the existing Crunch algorithms to compress ETC1 textures.

Basics:

In general, Crunch is performing the following steps:
- tiling (determines block encodings)
- quantization of the tile endpoints (determines endpoint indices)
- optimization of the endpoints for each tile group (determines endpoint dictionary)
- quantization of the selectors (determines selector indices)
- selector refinement for each selector group (determines selector dictionary)
- compression of the previously determined block encodings, dictionaries and indices

Dictionary element:

When applying Crunch algorithms to a new texture format, it is necessary to first define the dictionary element. In context of Crunch, this means thats the whole image consists of smaller non-overlapping blocks, while the contents of each individual block is determined by an endpoint and a selector from the corresponding dictionaries. For example, in case of DXT format, each endpoint and selector codebook element corresponds to a 4x4 pixel block. In general, the size of the blocks, which form the encoded image, depends on the texture format and quality considerations.

It is proposed to define the dictionaries according to the following limitations:
- The dictionary elements should be compatible with the existing Crunch algorithms, while the image blocks defined by those dictionary elements should be compatible with the texture encoding format.
- It should be possible to cover a wide range of image quality and bitrates by just changing the size of the endpoint and selector dictionaries. If there is no limitation on the dictionary size, the encoding should preferably become lossless or near-lossless (not considering the quality loss implied by the texture format itself).

In case of ETC1, the texture format itself determines the minimal size of the image block, defined by endpoint and selector: it can be either 2x4 or 4x2 rectangle, aligned to the borders of the 4x4 grid. It is not possible to use higher granularity, because each of those rectangles can have only one base color, according to the ETC1 format. For the same reason, any image block, defined by an endpoint and a selector from the dictionary, should be combined from those aligned 2x4 or 4x2 rectangles.

Let's investigate the possibilities for the endpoint dictionary. According to the ETC1 format, each 4x4 ETC1 block is split in half, while each ETC1 subblock has it's own base color and a modifier table index. In fact, the base color and the modifier table index simply define the high and the low colors for the subblock (while there are some limitations on the position of those high and low colors, implied by the ETC1 encoding). If we define the endpoint dictionary element in such a way that it contains information about more than one ETC1 base color, then such a dictionary will become incompatible with the existing tile quantization algorithm, and the reason for this is the following. The Crunch tiling algorithm first performs quantization of all the tile pixel colors, down to just 2 colors. Then it quantizes all those color pairs, coming from different tiles. This approach works quite well for 4x4 DXT blocks, as those 2 colors approximately represent the principle component of the tile pixel colors. In case of ETC1 however, mixing together pixels, which correspond to different base colors, does not make much sense, as each group of those pixels has it's own low and high color values, independent from other groups. When those pixels are mixed together, the information about the original principle components of each subblock gets lost.

For the mentioned reason, each endpoint dictionary element should correspond to a single ETC1 base color. In such case, the tile quantization algorithm will work almost the same way as for DXT format. Each pair of colors, generated by the tile palletizer, will normally have the subblock base color value somewhere in the middle between those 2 colors, so quantizing those color pairs should also automatically quantize the corresponding base colors. Moreover, each color pair implicitly contains information about the modifier table index (which corresponds to the distance between the high and the low colors), and therefore the corresponding table index will also get automatically quantized.

Endpoint and selector dictionary elements, which define a single 2x4 or 4x2 ETC1 subblock, are fully compatible with the existing Crunch algorithms (because each ETC1 subblock is associated with a single base color and a single modifier table index). At the same time, those subblocks are minimal possible blocks, which can be defined by a dictionary element for ETC1 format (as has been shown earlier). Of course, it is also possible to use blocks larger than 2x4 or 4x2 (assuming that all the ETC1 subblocks, which form such a block, will have the same base color and the same modifier table index), however, with a larger block area it would be not possible to achieve near-lossless quality when the dictionary size is not limited.

As the result, it is proposed to define the dictionaries in the following way:
- Each element of the endpoint dictionary defines a single base color and a single modifier table index of a 2x4 or a 4x2 pixel block (which represents an ETC1 subblock).
- Each endpoint is encoded as 3555 (3 bits for the table index and 5 bits for each component of the base color).
- Each element of the selector dictionary defines selectors for a 2x4 or a 4x2 block.
- Each selector is encoded using 16 bits.

ETC1-specific adjustments:

In case of DXT, the size of the encoded block is 4x4, while the tiling is performed in a 8x8 area (4 blocks). In case of ETC1, the tiling can be performed either in a 4x4 area (2 blocks), or in a 8x8 area (8 blocks), while other possibilities are either not symmetrical or too complex. For simplicity it is proposed to use 4x4 area for tiling. There are therefore 3 possible encodings: the 4x4 block is not split (encoded with a single endpoint), the 4x4 block is split horizontally, the 4x4 block is split vertically.

For simplicity, endpoint references are currently determined only within the tiling area, while the encoding of the endpoint references has been adjusted in the following way:
- The first ETC1 subblock will always have the reference value of 0
- The second ETC1 subblock can have the reference value of 0 if it has the same endpoint as the first subblock (note that in such case the flip of the ETC1 block does not need to be defined), the value of 1 if the corresponding ETC1 block is split horizontally, and the value of 2 if the corresponding ETC1 block is split vertically

According to the ETC1 format, the base colors within an ETC1 block can be encoded either as 444 and 444, or differentially as 555 and 333. For simplicity, this aspect is currently not taken into account (all the endpoints are encoded as 3555 in the codebook). If it appears that the base colors in the resulting ETC1 block can not be encoded differentially, the decoder will convert both base colors from 555 to 444.

At first, it might look like the ETC1 block flipping can bring some complications for Crunch, as the subblock structure might not look like a grid. This can be easily resolved by mirroring all the vertical ETC1 blocks across the main diagonal of the block after the tiling step (so that all the ETC1 subblocks will become 4x2 and form a regular grid). The decoder can mirror the ETC1 selector back according to the decoded block flip.

The code adjustments for the ETC1 compression support are pretty straightforward and mostly trivial. Just note that when format-specific adjustments affect performance critical code, it makes sense to duplicate the body of the affected function and perform format-specific optimizations in each copy of the function individually. For performance reasons, the following 4 functions now got both ETC and DTX specific versions:
- determine_tiles_task_etc() is an ETC-optimized version of the determine_tiles_task(), where dxt_fast class has been replaced with the etc1_optimizer class.
- determine_color_endpoint_codebook_task_etc() is an ETC-optimized version of the determine_color_endpoint_codebook_task(), where dxt1_endpoint_optimizer class has been replaced with the etc1_optimizer class.
- pack_color_endpoints_etc() is an ETC-optimized version of the pack_color_endpoints(), where 565565 DXT color endpoint encoding has been replaced with 3555 ETC color endpoint encoding.
- unpack_etc1() is an ETC version of the unpack_dxt1() function.

The color_quality_power_mul and m_adaptive_tile_color_psnr_derating parameters for ETC1 format have been selected in such a way, so that ETC1 compression gives approximately the same average Luma PSNR as the equivalent DXT1 compression, when compressing the Kodak test set without mipmaps using default quality.

In order to use ETC1 compression, use the -ETC1 command line option (i.e. "crunch_x64.exe -ETC1 input.png"). By default, compressed ETC1 textures will be decompressed into KTX file format.

DXT Testing:
The modified algorithm has been tested on the Kodak test set using 64-bit build with default settings (running on Windows 10, i7-4790, 3.6GHz). All the decompressed test images are identical to the images being compressed and decompressed using original version of Crunch.

[Compressing Kodak set without mipmaps using DXT1 encoding]
Original: 1582222 bytes / 28.876 sec
Modified: 1482780 bytes / 13.255 sec
Improvement: 6.28% (compression ratio) / 54.10% (compression time)

[Compressing Kodak set with mipmaps using DXT1 encoding]
Original: 2065243 bytes / 36.987 sec
Modified: 1931586 bytes / 18.068 sec
Improvement: 6.47% (compression ratio) / 51.15% (compression time)

ETC Testing:
The modified algorithm has been tested on the Kodak test set using 64-bit build with default settings (running on Windows 10, i7-4790, 3.6GHz). The ETC1 quantization parameters have been selected in such a way, so that ETC1 compression gives approximately the same average Luma PSNR as the corresponding DXT1 compression (which is equal to 34.044 dB for the Kodak test set compressed without mipmaps using DXT1 encoding and default quality settings).

[Compressing Kodak set without mipmaps using ETC1 encoding]
Total size: 1887265 bytes
Total time: 14.954 sec
Average bitrate: 1.600 bpp
Average Luma PSNR: 34.049 dB
2017-07-05 18:19:23 +02:00

1211 lines
48 KiB
C++

// 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"
#include "crn_defs.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 <left> <top> <right> <bottom> - Crop window before processing");
console::printf("-clamp <width> <height> - Crop image if larger than width/height");
console::printf("-clampscale <width> <height> - 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 <int> <int> - Rescale image to specified resolution");
console::printf("-relscale <float> <float> - Rescale image to specified relative resolution");
console::printf("-rescalemode <nearest | hi | lo> - 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<command_line_params::param_desc> 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) {
out_file_type = texture_file_types::cFormatDDS;
cfile_stream in_stream;
crnd::crn_header in_header;
if (in_stream.open(in_filename.get_ptr()) && in_stream.read(&in_header, sizeof(in_header)) == sizeof(in_header) && in_header.m_format == cCRNFmtETC1)
out_file_type = texture_file_types::cFormatKTX;
} 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<crn_mip_mode>(i))) {
mip_params.m_mode = static_cast<crn_mip_mode>(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<crn_mip_filter>(i)))) {
mip_params.m_filter = static_cast<crn_mip_filter>(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<crn_dxt_compressor_type>(i))) {
comp_params.m_dxt_compressor_type = static_cast<crn_dxt_compressor_type>(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<crn_dxt_quality>(i))) {
comp_params.m_dxt_quality = static_cast<crn_dxt_quality>(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<uint32>(1, src_tex.get_width() >> i);
uint32 height = math::maximum<uint32>(1, src_tex.get_height() >> i);
total_in_pixels += width * height * src_tex.get_num_faces();
}
vector<uint8> 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<uint8> 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;
}