Skip to content

Commit d1ca059

Browse files
committed
resgen: add compression options
- with `-z` each resource is compressed individually - with `-Z` the whole package is compressed
1 parent c71a229 commit d1ca059

File tree

2 files changed

+110
-30
lines changed

2 files changed

+110
-30
lines changed

tools/resgen/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ set(SRCS src/main.cpp)
1212
# Target definitions
1313
# ==================================================================================================
1414
add_executable(${TARGET} ${SRCS})
15-
target_link_libraries(${TARGET} PRIVATE utils getopt)
15+
target_link_libraries(${TARGET} PRIVATE utils getopt zstd)
1616
set_target_properties(${TARGET} PROPERTIES FOLDER Tools)
1717

1818
# =================================================================================================
1919
# Licenses
2020
# ==================================================================================================
21-
set(MODULE_LICENSES getopt)
21+
set(MODULE_LICENSES getopt zstd)
2222
set(GENERATION_ROOT ${CMAKE_CURRENT_BINARY_DIR}/generated)
2323
list_licenses(${GENERATION_ROOT}/licenses/licenses.inc ${MODULE_LICENSES})
2424
target_include_directories(${TARGET} PRIVATE ${GENERATION_ROOT})

tools/resgen/src/main.cpp

Lines changed: 108 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include <vector>
3232

3333
#include <string.h>
34+
#include <zstd.h>
3435

3536
using namespace utils;
3637

@@ -43,6 +44,8 @@ struct AppConfig {
4344
bool generateC = false;
4445
bool quietMode = false;
4546
bool embedJson = false;
47+
bool compress = false;
48+
bool compressPackage = false;
4649
std::vector<Path> inputPaths;
4750
};
4851

@@ -83,6 +86,10 @@ is the basename of the input file. It produces the following set of files:
8386
of all resource sizes and names. Useful for size analysis.
8487
--quiet, -q
8588
Suppress console output
89+
--compress, -z
90+
Compress each resource individually with zstd.
91+
--compress-package, -Z
92+
Compress the entire package with zstd.
8693
8794
Examples:
8895
RESGEN -cp textures jungle.png beach.png
@@ -144,7 +151,7 @@ static std::string& replaceAll(std::string& context, const std::string& from, co
144151

145152
// Parses command-line arguments using getopt and populates the AppConfig struct.
146153
static AppConfig handleArguments(int const argc, char* argv[]) {
147-
static constexpr const char* OPTSTR = "hLp:x:ktcqj";
154+
static constexpr const char* OPTSTR = "hLp:x:ktcqjzZ";
148155
static const option OPTIONS[] = {
149156
{ "help", no_argument, nullptr, 'h' },
150157
{ "license", no_argument, nullptr, 'L' },
@@ -155,6 +162,8 @@ static AppConfig handleArguments(int const argc, char* argv[]) {
155162
{ "cfile", no_argument, nullptr, 'c' },
156163
{ "quiet", no_argument, nullptr, 'q' },
157164
{ "json", no_argument, nullptr, 'j' },
165+
{ "compress", no_argument, nullptr, 'z' },
166+
{ "compress-package", no_argument, nullptr, 'Z' },
158167
{ nullptr, 0, nullptr, 0 }
159168
};
160169

@@ -189,12 +198,23 @@ static AppConfig handleArguments(int const argc, char* argv[]) {
189198
case 'j':
190199
config.embedJson = true;
191200
break;
201+
case 'z':
202+
config.compress = true;
203+
break;
204+
case 'Z':
205+
config.compressPackage = true;
206+
break;
192207
default:
193208
printUsage(argv[0]);
194209
std::exit(1);
195210
}
196211
}
197212

213+
if (config.compress && config.compressPackage) {
214+
std::cerr << "Cannot use -z and -Z at the same time." << std::endl;
215+
exit(1);
216+
}
217+
198218
// Treat remaining arguments as input file paths.
199219
for (int i = optind; i < argc; ++i) {
200220
config.inputPaths.emplace_back(argv[i]);
@@ -302,10 +322,9 @@ int main(int argc, char* argv[]) {
302322
replaceAll(asmstr, "{RESOURCES}", packagePrefix);
303323
replaceAll(asmstr, "{resources}", packageFile);
304324

305-
// Open all output file streams.
325+
// Open file streams.
306326
auto appleAsmStream = openOutputFile(appleAsmPath);
307327
auto asmStream = openOutputFile(asmPath);
308-
auto binStream = openOutputFile(binPath, std::ios::binary);
309328

310329
// Begin constructing the C header file content.
311330
std::ostringstream headerStream;
@@ -327,17 +346,18 @@ int main(int argc, char* argv[]) {
327346
<< "const uint8_t " << packageSymbol << "[] = {\n";
328347
}
329348

349+
std::vector<uint8_t> packageData;
350+
std::vector<uint8_t> uncompressedPackageData;
351+
size_t totalUncompressedSize = 0;
352+
330353
// Process each input file to build the resource collection.
331354
jsonStream << "{";
332355
std::size_t offset = 0;
333356
for (const auto& inPath : inputPaths) {
334-
std::vector<std::uint8_t> content;
357+
std::vector<std::uint8_t> uncompressedContent;
335358
if (inPath != g_jsonMagicString) {
336-
// For a regular file, read its binary content.
337-
content = readFile(inPath);
359+
uncompressedContent = readFile(inPath);
338360
} else {
339-
// For the JSON placeholder, finalize and embed the JSON summary blob.
340-
// The blob is formatted as: __RESGEN__\0<size>\0{...json...}
341361
std::string jsonString = jsonStream.str();
342362
jsonString[jsonString.size() - 1] = '}';
343363
std::ostringstream jsonBlob;
@@ -346,55 +366,115 @@ int main(int argc, char* argv[]) {
346366
jsonBlob << jsonString;
347367
jsonString = jsonBlob.str();
348368
const auto* jsonPtr = reinterpret_cast<const std::uint8_t*>(jsonString.c_str());
349-
content.assign(jsonPtr, jsonPtr + jsonBlob.str().size());
369+
uncompressedContent.assign(jsonPtr, jsonPtr + jsonBlob.str().size());
350370
}
351371

352-
// Optionally append a null terminator, useful for text resources.
353372
if (config.appendNull) {
354-
content.push_back(0);
373+
uncompressedContent.push_back(0);
374+
}
375+
376+
const size_t uncompressedSize = uncompressedContent.size();
377+
totalUncompressedSize += uncompressedSize;
378+
379+
std::vector<std::uint8_t> finalContent;
380+
bool const shouldCompressThisResource = config.compress && inPath != g_jsonMagicString;
381+
382+
if (shouldCompressThisResource) {
383+
size_t const cBuffSize = ZSTD_compressBound(uncompressedSize);
384+
finalContent.resize(cBuffSize);
385+
size_t const cSize = ZSTD_compress(finalContent.data(), cBuffSize,
386+
uncompressedContent.data(), uncompressedSize, ZSTD_maxCLevel());
387+
if (ZSTD_isError(cSize)) {
388+
std::cerr << "zstd compression error: " << ZSTD_getErrorName(cSize) << std::endl;
389+
exit(1);
390+
}
391+
finalContent.resize(cSize);
392+
} else {
393+
finalContent = std::move(uncompressedContent);
355394
}
356395

357-
// Generate the resource's symbol name from its file name.
358396
std::string rname = config.keepExtension ? inPath.getName() : inPath.getNameWithoutExtension();
359397
replaceAll(rname, ".", "_");
360398
std::transform(rname.begin(), rname.end(), rname.begin(), [](unsigned char c) { return std::toupper(c); });
361399
const std::string prname = packagePrefix + rname;
362400

363-
// Write the resource content to the aggregate binary file.
364-
binStream.write(reinterpret_cast<const char*>(content.data()), content.size());
401+
if (config.compressPackage) {
402+
uncompressedPackageData.insert(uncompressedPackageData.end(), finalContent.begin(), finalContent.end());
403+
} else {
404+
packageData.insert(packageData.end(), finalContent.begin(), finalContent.end());
405+
}
365406

366-
// Generate C preprocessor macros for the resource's offset, size, and data pointer.
367-
headerMacros << "#define " << prname << "_OFFSET " << offset << "\n"
368-
<< "#define " << prname << "_SIZE " << content.size() << "\n"
369-
<< "#define " << prname << "_DATA (" << packageSymbol << " + " << prname << "_OFFSET)\n\n";
407+
size_t const sizeForHeader = config.compressPackage ? uncompressedSize : finalContent.size();
408+
409+
headerMacros << "#define " << prname << "_OFFSET " << offset << "\n";
410+
if (shouldCompressThisResource) {
411+
headerMacros << "#define " << prname << "_UNCOMPRESSED_SIZE " << uncompressedSize << "\n";
412+
}
413+
headerMacros << "#define " << prname << "_SIZE " << sizeForHeader << "\n";
414+
if (!config.compressPackage) {
415+
headerMacros << "#define " << prname << "_DATA (" << packageSymbol << " + " << prname << "_OFFSET)\n\n";
416+
}
370417

371-
// If enabled, write the content to the C source file.
372418
if (config.generateC) {
373-
writeXxdEntry(xxdStream, rname, content);
419+
writeXxdEntry(xxdStream, rname, finalContent);
420+
}
421+
422+
if (inPath != g_jsonMagicString) {
423+
jsonStream << "\"" << rname << "\":{\"uncompressedSize\":" << uncompressedSize;
424+
if (config.compress) {
425+
jsonStream << ",\"compressedSize\":" << finalContent.size();
426+
}
427+
jsonStream << "},";
374428
}
429+
offset += sizeForHeader;
430+
}
375431

376-
// Add an entry to the JSON summary and update the running offset.
377-
jsonStream << "\"" << rname << "\":" << content.size() << ",";
378-
offset += content.size();
432+
if (config.compressPackage) {
433+
size_t const cBuffSize = ZSTD_compressBound(uncompressedPackageData.size());
434+
packageData.resize(cBuffSize);
435+
size_t const cSize = ZSTD_compress(packageData.data(), cBuffSize,
436+
uncompressedPackageData.data(), uncompressedPackageData.size(), ZSTD_maxCLevel());
437+
if (ZSTD_isError(cSize)) {
438+
std::cerr << "zstd compression error: " << ZSTD_getErrorName(cSize) << std::endl;
439+
exit(1);
440+
}
441+
packageData.resize(cSize);
442+
}
443+
444+
size_t const totalCompressedSize = packageData.size();
445+
446+
if (config.compress || config.compressPackage) {
447+
headerStream << " // This package is compressed with zstd.\n";
448+
if (config.compress) {
449+
headerStream << " // Each resource is compressed individually.\n"
450+
<< " // The _SIZE macro is the compressed size.\n"
451+
<< " // The _UNCOMPRESSED_SIZE macro is the original size.\n";
452+
}
453+
if (config.compressPackage) {
454+
headerStream << " // The entire package is compressed as a single blob.\n"
455+
<< " // The _SIZE and _OFFSET macros are for the uncompressed data.\n"
456+
<< " // You must decompress the entire package before using them.\n";
457+
}
458+
headerStream << " // Original package size: " << totalUncompressedSize << " bytes.\n"
459+
<< " // Compressed package size: " << totalCompressedSize << " bytes.\n\n";
379460
}
380461

381-
// Finalize the header file content.
382462
headerStream << "}\n\n";
383463
headerStream << headerMacros.str();
384464
headerStream << "#endif\n";
385465

386-
// Write the header and assembly files.
466+
auto binStream = openOutputFile(binPath, std::ios::binary);
467+
binStream.write(reinterpret_cast<const char*>(packageData.data()), packageData.size());
468+
387469
writeHeaderIfChanged(headerPath, headerStream.str());
388470
asmStream << asmstr << std::endl;
389471
appleAsmStream << aasmstr << std::endl;
390472

391-
// Report generated files to the console unless in quiet mode.
392473
if (!config.quietMode) {
393474
std::cout << "Generated files: " << headerPath << " " << asmPath << " " << appleAsmPath << " "
394475
<< binPath;
395476
}
396477

397-
// Finalize the C file and report it.
398478
if (config.generateC) {
399479
xxdStream << "};\n\n";
400480
if (!config.quietMode) {
@@ -407,4 +487,4 @@ int main(int argc, char* argv[]) {
407487
}
408488

409489
return 0;
410-
}
490+
}

0 commit comments

Comments
 (0)