From e836f665f40a3a5b5dd6162f72a8716c13cee265 Mon Sep 17 00:00:00 2001 From: spicyjpeg Date: Thu, 4 Apr 2024 14:39:52 +0200 Subject: [PATCH] Add main executable compression, bump to 0.4.1 --- CMakeLists.txt | 63 +++++---- resources.json | 76 +++++----- src/boot/crt0.s | 22 +++ src/boot/main.cpp | 76 ++++++++++ src/common/args.cpp | 94 +++++++++++++ src/common/args.hpp | 59 ++++++++ src/common/rom.cpp | 2 +- src/common/rom.hpp | 6 +- src/common/util.cpp | 97 +++++++++++-- src/common/util.hpp | 31 ++++- src/launcher/launcher.cpp | 115 +++++++++++++++ src/launcher/launcher.hpp | 42 ++++++ src/launcher/main.cpp | 255 +++++----------------------------- src/libc/string.c | 29 ++-- src/main/app/miscworkers.cpp | 78 +++++------ src/main/main.cpp | 103 ++------------ src/ps1/system.c | 21 ++- tools/buildResourceArchive.py | 54 ++++--- tools/requirements.txt | 1 + 19 files changed, 750 insertions(+), 474 deletions(-) create mode 100644 src/boot/crt0.s create mode 100644 src/boot/main.cpp create mode 100644 src/common/args.cpp create mode 100644 src/common/args.hpp create mode 100644 src/launcher/launcher.cpp create mode 100644 src/launcher/launcher.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0501238..6a4c731 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/cmake/toolchain.cmake") project( cart_tool_private LANGUAGES C CXX ASM - VERSION 0.4.0 + VERSION 0.4.1 DESCRIPTION "Konami System 573 security cartridge tool" ) @@ -31,7 +31,7 @@ set(cdVolumeName "CART_TOOL_${_version}") add_library( common OBJECT - src/libc/crt0.c + #src/libc/crt0.c src/libc/cxxsupport.cpp src/libc/malloc.c src/libc/memset.s @@ -49,10 +49,11 @@ target_include_directories( src/libc ) target_compile_definitions(common PUBLIC VERSION="${PROJECT_VERSION}") +link_libraries(common) -function(addExecutable name stackTop) +function(addExecutable name address stackTop) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE common) + target_link_options(${name} PRIVATE -Ttext=0x${address}) add_custom_command( TARGET ${name} POST_BUILD @@ -70,8 +71,12 @@ endfunction() ## Main executable +# IMPORTANT: these addresses assume the boot executable's size (including code, +# heap and stack allocations as well as the resource archive) is <448 KB +# (0x70000 bytes). addExecutable( - main 801dfff0 + main 80080000 801dfff0 + src/common/args.cpp src/common/file.cpp src/common/gpu.cpp src/common/gpufont.cpp @@ -82,6 +87,7 @@ addExecutable( src/common/rom.cpp src/common/spu.cpp src/common/util.cpp + src/libc/crt0.c src/main/cart.cpp src/main/cartdata.cpp src/main/cartio.cpp @@ -109,7 +115,6 @@ target_compile_definitions( $, ENABLE_LOGGING=1 ENABLE_FILE_WRITING=1 - #ENABLE_ARGV=1 ENABLE_LOG_BUFFER=1 ENABLE_PS1_CONTROLLER=1 #ENABLE_X76F100_DRIVER=1 @@ -118,7 +123,6 @@ target_compile_definitions( , ENABLE_LOGGING=1 ENABLE_FILE_WRITING=1 - #ENABLE_ARGV=1 ENABLE_LOG_BUFFER=1 ENABLE_PS1_CONTROLLER=1 #ENABLE_X76F100_DRIVER=1 @@ -127,42 +131,52 @@ target_compile_definitions( > ) -list(APPEND packageContents main.psexe) - -## Executable launchers +## Boot stub and executable launchers # NOTE: in order to make sure -Os is passed after -Og or -O3 (see # cmake/setup.cmake) and thus overrides it, it must be added to a separate # target rather than directly to the executables. -add_library(launcherFlags INTERFACE) -target_compile_options(launcherFlags INTERFACE -Os) +add_library(bootFlags INTERFACE) +target_compile_options(bootFlags INTERFACE -Os) target_compile_definitions( - launcherFlags INTERFACE - $, - #ENABLE_LOGGING=1 - , + bootFlags INTERFACE + $<$: + #ENABLE_ARGV=1 #ENABLE_LOGGING=1 > ) +addExecutable( + boot 80010000 0 + src/boot/crt0.s + src/boot/main.cpp + src/common/util.cpp +) +target_link_libraries(boot PRIVATE bootFlags) + +list(APPEND packageContents boot.psexe) + function(addLauncher address stackTop) addExecutable( - launcher${address} ${stackTop} + launcher${address} ${address} ${stackTop} + src/common/args.cpp src/common/ide.cpp src/common/ideglue.cpp src/common/io.cpp src/common/util.cpp + src/launcher/launcher.cpp src/launcher/main.cpp + src/libc/crt0.c src/vendor/ff.c src/vendor/ffunicode.c src/vendor/printf.c ) - target_link_options(launcher${address} PRIVATE -Ttext=0x${address}) - target_link_libraries(launcher${address} PRIVATE launcherFlags) + target_link_libraries(launcher${address} PRIVATE bootFlags) endfunction() -# Note that the launchers must be <40 KB (0xa000 bytes) in order for this to -# work properly. +# IMPORTANT: these addresses assume the launcher's total size (including code, +# heap and stack allocations, but excluding the executable header) is <32 KB +# (0x8000 bytes). addLauncher(801f8000 801ffff0) addLauncher(803f8000 803ffff0) @@ -218,13 +232,14 @@ add_custom_command( resources.json assets/app.palette.json assets/app.strings.json + main launcher801f8000 launcher803f8000 COMMENT "Building resource archive" VERBATIM ) addBinaryFile( - main _resources _resourcesSize + boot _resourceArchive _resourceArchiveLength "${PROJECT_BINARY_DIR}/resources.zip" ) @@ -248,7 +263,7 @@ if(EXISTS "${XORRISO_PATH}") -system_id "PLAYSTATION" -preparer_id "CART_TOOL BUILD SCRIPT" -map readme.txt README.TXT - -map main.psexe PSX.EXE + -map boot.psexe PSX.EXE -clone PSX.EXE GSE.NXX -clone PSX.EXE NSE.GXX -clone PSX.EXE OSE.FXX @@ -269,7 +284,7 @@ if(EXISTS "${XORRISO_PATH}") -clone PSX.EXE TSY.AXD -clone PSX.EXE TSZ.AXC OUTPUT "${releaseName}.iso" - DEPENDS main + DEPENDS boot COMMENT "Building CD-ROM image" VERBATIM ) diff --git a/resources.json b/resources.json index f504513..dc800d0 100644 --- a/resources.json +++ b/resources.json @@ -1,4 +1,21 @@ [ + { + "type": "binary", + "name": "binaries/main.psexe.lz4", + "source": "main.psexe", + "compression": "lz4" + }, + { + "type": "binary", + "name": "binaries/launcher801f8000.psexe", + "source": "launcher801f8000.psexe" + }, + { + "type": "binary", + "name": "binaries/launcher803f8000.psexe", + "source": "launcher803f8000.psexe" + }, + { "type": "tim", "name": "assets/textures/background.tim", @@ -21,40 +38,40 @@ "source": "${PROJECT_SOURCE_DIR}/assets/textures/font.metrics.json" }, { - "type": "binary", - "name": "assets/sounds/about.vag", - "source": "${PROJECT_SOURCE_DIR}/assets/sounds/about.vag", - "compress": null + "type": "binary", + "name": "assets/sounds/about.vag", + "source": "${PROJECT_SOURCE_DIR}/assets/sounds/about.vag", + "compression": "none" }, { - "type": "binary", - "name": "assets/sounds/alert.vag", - "source": "${PROJECT_SOURCE_DIR}/assets/sounds/alert.vag", - "compress": null + "type": "binary", + "name": "assets/sounds/alert.vag", + "source": "${PROJECT_SOURCE_DIR}/assets/sounds/alert.vag", + "compression": "none" }, { - "type": "binary", - "name": "assets/sounds/move.vag", - "source": "${PROJECT_SOURCE_DIR}/assets/sounds/move.vag", - "compress": null + "type": "binary", + "name": "assets/sounds/move.vag", + "source": "${PROJECT_SOURCE_DIR}/assets/sounds/move.vag", + "compression": "none" }, { - "type": "binary", - "name": "assets/sounds/enter.vag", - "source": "${PROJECT_SOURCE_DIR}/assets/sounds/enter.vag", - "compress": null + "type": "binary", + "name": "assets/sounds/enter.vag", + "source": "${PROJECT_SOURCE_DIR}/assets/sounds/enter.vag", + "compression": "none" }, { - "type": "binary", - "name": "assets/sounds/exit.vag", - "source": "${PROJECT_SOURCE_DIR}/assets/sounds/exit.vag", - "compress": null + "type": "binary", + "name": "assets/sounds/exit.vag", + "source": "${PROJECT_SOURCE_DIR}/assets/sounds/exit.vag", + "compression": "none" }, { - "type": "binary", - "name": "assets/sounds/click.vag", - "source": "${PROJECT_SOURCE_DIR}/assets/sounds/click.vag", - "compress": null + "type": "binary", + "name": "assets/sounds/click.vag", + "source": "${PROJECT_SOURCE_DIR}/assets/sounds/click.vag", + "compression": "none" }, { "type": "palette", @@ -90,16 +107,5 @@ "type": "binary", "name": "data/zs01.cartdb", "source": "${PROJECT_SOURCE_DIR}/data/zs01.cartdb" - }, - - { - "type": "binary", - "name": "launchers/801f8000.psexe", - "source": "launcher801f8000.psexe" - }, - { - "type": "binary", - "name": "launchers/803f8000.psexe", - "source": "launcher803f8000.psexe" } ] diff --git a/src/boot/crt0.s b/src/boot/crt0.s new file mode 100644 index 0000000..4c014ed --- /dev/null +++ b/src/boot/crt0.s @@ -0,0 +1,22 @@ + +.set noreorder + +.set _STACK_SIZE, 0x100 + +# We're going to override ps1-bare-metal's _start() with a minimal version that +# skips .bss initialization (getting rid of memset() in the process) and moves +# the stack to a statically allocated buffer. +.section .text._start, "ax", @progbits +.global _start +.type _start, @function + +_start: + la $gp, _gp + j main + addiu $sp, $gp, %gprel(_stackBuffer) + _STACK_SIZE - 16 + +.section .sbss._stackBuffer, "aw" +.type _stackBuffer, @object + +_stackBuffer: + .space _STACK_SIZE diff --git a/src/boot/main.cpp b/src/boot/main.cpp new file mode 100644 index 0000000..6bafeb3 --- /dev/null +++ b/src/boot/main.cpp @@ -0,0 +1,76 @@ + +#include +#include +#include "common/io.hpp" +#include "common/util.hpp" + +extern "C" const uint8_t _resourceArchive[]; +extern "C" const size_t _resourceArchiveLength; + +static char _ptrArg[]{ "resource.ptr=xxxxxxxx" }; +static char _lengthArg[]{ "resource.length=xxxxxxxx" }; + +struct [[gnu::packed]] ZIPFileHeader { +public: + uint32_t magic; + uint16_t version, flags, compType; + uint16_t fileTime, fileDate; + uint32_t crc, compLength, uncompLength; + uint16_t nameLength, extraLength; + + inline bool validateMagic(void) const { + return (magic == 0x04034b50); + } + inline size_t getHeaderLength(void) const { + return sizeof(ZIPFileHeader) + nameLength + extraLength; + } +}; + +int main(int argc, const char **argv) { + //io::init(); + + // Parse the header of the archive's first entry manually. This avoids + // pulling in miniz and bloating the binary. + // NOTE: this assumes the main executable is always the first file in the + // archive. + auto zipHeader = reinterpret_cast(_resourceArchive); + auto ptr = &_resourceArchive[zipHeader->getHeaderLength()]; + auto compLength = zipHeader->compLength; + + //assert(zipHeader->validateMagic()); + //assert(!zipHeader->compType); + + // Decompress only the header to determine where to place the binary in + // memory, then rerun the decompressor on the entire executable. + util::ExecutableHeader exeHeader; + + util::decompressLZ4( + reinterpret_cast(&exeHeader), ptr, sizeof(exeHeader), + compLength + ); + + auto offset = exeHeader.textOffset - util::EXECUTABLE_BODY_OFFSET; + auto length = exeHeader.textLength + util::EXECUTABLE_BODY_OFFSET; + + util::decompressLZ4( + reinterpret_cast(offset), ptr, length, compLength + ); + + util::ExecutableLoader loader(exeHeader, nullptr); + + util::hexValueToString( + &_ptrArg[13], reinterpret_cast(_resourceArchive), 8 + ); + loader.addArgument(_ptrArg); + util::hexValueToString(&_lengthArg[16], _resourceArchiveLength, 8); + loader.addArgument(_lengthArg); + +#ifdef ENABLE_ARGV + for (; argc > 0; argc--) + loader.addArgument(*(argv++)); +#endif + + io::clearWatchdog(); + loader.run(); + return 0; +} diff --git a/src/common/args.cpp b/src/common/args.cpp new file mode 100644 index 0000000..af7c703 --- /dev/null +++ b/src/common/args.cpp @@ -0,0 +1,94 @@ + +#include +#include +#include "common/args.hpp" +#include "common/util.hpp" + +namespace args { + +/* Command line argument parsers */ + +bool CommonArgs::parseArgument(const char *arg) { + if (!arg) + return false; + + switch (util::hash(arg, VALUE_SEPARATOR)) { +#if 0 + case "boot.rom"_h: + LOG("boot.rom=%s", &arg[9]); + return true; + + case "boot.from"_h: + LOG("boot.from=%s", &arg[10]); + return true; +#endif + + case "console"_h: + baudRate = int(strtol(&arg[8], nullptr, 0)); + return true; + + default: + return false; + } +} + +bool MainArgs::parseArgument(const char *arg) { + if (!arg) + return false; + + switch (util::hash(arg, VALUE_SEPARATOR)) { + case "screen.width"_h: + screenWidth = int(strtol(&arg[13], nullptr, 0)); + return true; + + case "screen.height"_h: + screenHeight = int(strtol(&arg[14], nullptr, 0)); + return true; + + case "screen.interlace"_h: + forceInterlace = bool(strtol(&arg[17], nullptr, 0)); + return true; + + // Allow the default assets to be overridden by passing a pointer to an + // in-memory ZIP file as a command-line argument. + case "resource.ptr"_h: + resourcePtr = reinterpret_cast( + strtol(&arg[13], nullptr, 16) + ); + return true; + + case "resource.length"_h: + resourceLength = size_t(strtol(&arg[16], nullptr, 16)); + return true; + + default: + return CommonArgs::parseArgument(arg); + } +} + +bool ExecutableLauncherArgs::parseArgument(const char *arg) { + if (!arg) + return false; + + switch (util::hash(arg, VALUE_SEPARATOR)) { + case "launcher.drive"_h: + drive = &arg[15]; + return true; + + case "launcher.path"_h: + path = &arg[14]; + return true; + + case "launcher.arg"_h: + if (argCount >= int(util::countOf(executableArgs))) + return false; + + executableArgs[argCount++] = &arg[13]; + return true; + + default: + return CommonArgs::parseArgument(arg); + } +} + +} diff --git a/src/common/args.hpp b/src/common/args.hpp new file mode 100644 index 0000000..87cf244 --- /dev/null +++ b/src/common/args.hpp @@ -0,0 +1,59 @@ + +#pragma once + +#include +#include "common/util.hpp" + +namespace args { + +/* Command line argument parsers */ + +static constexpr char VALUE_SEPARATOR = '='; + +static constexpr int DEFAULT_BAUD_RATE = 115200; +static constexpr int DEFAULT_SCREEN_WIDTH = 320; +static constexpr int DEFAULT_SCREEN_HEIGHT = 240; + +class CommonArgs { +public: + int baudRate; + +#ifdef NDEBUG + inline CommonArgs(void) + : baudRate(0) {} +#else + // Enable serial port logging by default in debug builds. + inline CommonArgs(void) + : baudRate(DEFAULT_BAUD_RATE) {} +#endif + + bool parseArgument(const char *arg); +}; + +class MainArgs : public CommonArgs { +public: + int screenWidth, screenHeight; + bool forceInterlace; + const void *resourcePtr; + size_t resourceLength; + + inline MainArgs(void) + : screenWidth(DEFAULT_SCREEN_WIDTH), screenHeight(DEFAULT_SCREEN_HEIGHT), + forceInterlace(false), resourcePtr(nullptr), resourceLength(0) {} + + bool parseArgument(const char *arg); +}; + +class ExecutableLauncherArgs : public CommonArgs { +public: + int argCount; + const char *drive, *path; + const char *executableArgs[util::MAX_EXECUTABLE_ARGS]; + + inline ExecutableLauncherArgs(void) + : argCount(0), drive(nullptr), path(nullptr) {} + + bool parseArgument(const char *arg); +}; + +} diff --git a/src/common/rom.cpp b/src/common/rom.cpp index d267db0..f787bbf 100644 --- a/src/common/rom.cpp +++ b/src/common/rom.cpp @@ -201,7 +201,7 @@ enum FlashIdentifier : uint16_t { bool FlashRegion::hasBootExecutable(void) const { // FIXME: this implementation will not detect executables that cross bank // boundaries (but it shouldn't matter as executables must be <4 MB anyway) - auto data = reinterpret_cast(ptr + FLASH_EXE_OFFSET); + auto data = reinterpret_cast(ptr + FLASH_EXECUTABLE_OFFSET); auto crcPtr = reinterpret_cast(ptr + FLASH_CRC_OFFSET); auto table = reinterpret_cast(CACHE_BASE); diff --git a/src/common/rom.hpp b/src/common/rom.hpp index 81b28d3..fd40b04 100644 --- a/src/common/rom.hpp +++ b/src/common/rom.hpp @@ -11,9 +11,9 @@ namespace rom { /* ROM region dumpers */ -static constexpr size_t FLASH_BANK_LENGTH = 0x400000; -static constexpr uint32_t FLASH_CRC_OFFSET = 0x20; -static constexpr uint32_t FLASH_EXE_OFFSET = 0x24; +static constexpr size_t FLASH_BANK_LENGTH = 0x400000; +static constexpr uint32_t FLASH_CRC_OFFSET = 0x20; +static constexpr uint32_t FLASH_EXECUTABLE_OFFSET = 0x24; class Driver; diff --git a/src/common/util.cpp b/src/common/util.cpp index 8f9738f..6b7cb9f 100644 --- a/src/common/util.cpp +++ b/src/common/util.cpp @@ -95,6 +95,60 @@ void Logger::log(const char *format, ...) { enableInterrupts(); } +/* LZ4 decompressor */ + +void decompressLZ4( + uint8_t *output, const uint8_t *input, size_t maxOutputLength, + size_t inputLength +) { + auto outputEnd = &output[maxOutputLength]; + auto inputEnd = &input[inputLength]; + + while (input < inputEnd) { + uint8_t token = *(input++); + + // Copy literals from the input stream. + int literalLength = token >> 4; + + if (literalLength == 0xf) { + uint8_t addend; + + do { + addend = *(input++); + literalLength += addend; + } while (addend == 0xff); + } + + for (; literalLength && (output < outputEnd); literalLength--) + *(output++) = *(input++); + if (input >= inputEnd) + break; + + int offset = input[0] | (input[1] << 8); + input += 2; + + // Copy from previously decompressed data. Note that this *must* be done + // one byte at a time, as the compressor relies on out-of-bounds copies + // repeating the last byte. + int copyLength = token & 0xf; + + if (copyLength == 0xf) { + uint8_t addend; + + do { + addend = *(input++); + copyLength += addend; + } while (addend == 0xff); + } + + auto copySource = output - offset; + copyLength += 4; + + for (; copyLength && (output < outputEnd); copyLength--) + *(output++) = *(copySource++); + } +} + /* CRC calculation */ static constexpr uint8_t _CRC8_POLY = 0x8c; @@ -176,7 +230,19 @@ extern "C" uint32_t mz_crc32(uint32_t crc, const uint8_t *data, size_t length) { static const char _HEX_CHARSET[]{ "0123456789ABCDEF" }; -size_t hexToString(char *output, const uint8_t *input, size_t length, char sep) { +size_t hexValueToString(char *output, uint32_t value, size_t numDigits) { + output += numDigits; + *output = 0; + + for (size_t i = numDigits; i; i--, value >>= 4) + *(--output) = _HEX_CHARSET[value & 0xf]; + + return numDigits; +} + +size_t hexToString( + char *output, const uint8_t *input, size_t length, char separator +) { size_t outLength = 0; for (; length; length--) { @@ -185,8 +251,8 @@ size_t hexToString(char *output, const uint8_t *input, size_t length, char sep) *(output++) = _HEX_CHARSET[value >> 4]; *(output++) = _HEX_CHARSET[value & 0xf]; - if (sep && (length > 1)) { - *(output++) = sep; + if (separator && (length > 1)) { + *(output++) = separator; outLength += 3; } else { outLength += 2; @@ -264,42 +330,43 @@ ExecutableLoader::ExecutableLoader( if (!stackTop) stackTop = defaultStackTop; - _argListPtr = reinterpret_cast(uintptr_t(stackTop) & ~7) + _argListPtr = reinterpret_cast(uintptr_t(stackTop) & ~7) - MAX_EXECUTABLE_ARGS; _currentStackPtr = reinterpret_cast(_argListPtr); } -void ExecutableLoader::addArgument(const char *arg) { +void ExecutableLoader::copyArgument(const char *arg) { // Command-line arguments must be copied to the top of the new stack in // order to ensure the executable is going to be able to access them at any // time. - size_t length = __builtin_strlen(arg) + 1; - size_t aligned = (length + 7) & ~7; - - _currentStackPtr -= aligned; - _argListPtr[_argCount++] = _currentStackPtr; + size_t length = __builtin_strlen(arg) + 1; + _currentStackPtr -= (length + 7) & ~7; + addArgument(_currentStackPtr); __builtin_memcpy(_currentStackPtr, arg, length); //assert(_argCount <= MAX_EXECUTABLE_ARGS); } -[[noreturn]] void ExecutableLoader::run(void) { +[[noreturn]] void ExecutableLoader::run( + int rawArgc, const char *const *rawArgv +) { disableInterrupts(); flushCache(); - register int a0 __asm__("a0") = _argCount; - register char **a1 __asm__("a1") = _argListPtr; - register uintptr_t gp __asm__("gp") = _header.initialGP; + register int a0 __asm__("a0") = rawArgc; + register const char *const *a1 __asm__("a1") = rawArgv; + register uintptr_t gp __asm__("gp") = _header.initialGP; // Changing the stack pointer and return address is not something that // should be done in a C++ function, but hopefully it's fine here since // we're jumping out right after setting it. __asm__ volatile( + ".set push\n" ".set noreorder\n" "li $ra, %0\n" "jr %1\n" "addiu $sp, %2, -8\n" - ".set reorder\n" + ".set pop\n" :: "i"(DEV2_BASE), "r"(_header.entryPoint), "r"(_currentStackPtr), "r"(a0), "r"(a1), "r"(gp) ); diff --git a/src/common/util.hpp b/src/common/util.hpp index 129e315..3a8323a 100644 --- a/src/common/util.hpp +++ b/src/common/util.hpp @@ -298,24 +298,43 @@ class ExecutableLoader { private: const ExecutableHeader &_header; - int _argCount; - char **_argListPtr; - char *_currentStackPtr; + int _argCount; + const char **_argListPtr; + char *_currentStackPtr; public: + inline void addArgument(const char *arg) { + _argListPtr[_argCount++] = arg; + } + [[noreturn]] inline void run(void) { + run(_argCount, _argListPtr); + } + ExecutableLoader(const ExecutableHeader &header, void *defaultStackTop); - void addArgument(const char *arg); - [[noreturn]] void run(void); + void copyArgument(const char *arg); + [[noreturn]] void run(int rawArgc, const char *const *rawArgv); }; /* Other APIs */ +static inline size_t getLZ4InPlaceMargin(size_t inputLength) { + return (inputLength >> 8) + 32; +} + +void decompressLZ4( + uint8_t *output, const uint8_t *input, size_t maxOutputLength, + size_t inputLength +); + uint8_t dsCRC8(const uint8_t *data, size_t length); uint16_t zsCRC16(const uint8_t *data, size_t length); uint32_t zipCRC32(const uint8_t *data, size_t length, uint32_t crc = 0); void initZipCRC32(void); -size_t hexToString(char *output, const uint8_t *input, size_t length, char sep = 0); +size_t hexValueToString(char *output, uint32_t value, size_t numDigits = 8); +size_t hexToString( + char *output, const uint8_t *input, size_t length, char separator = 0 +); size_t serialNumberToString(char *output, const uint8_t *input); size_t traceIDToString(char *output, const uint8_t *input); size_t encodeBase41(char *output, const uint8_t *input, size_t length); diff --git a/src/launcher/launcher.cpp b/src/launcher/launcher.cpp new file mode 100644 index 0000000..654bc99 --- /dev/null +++ b/src/launcher/launcher.cpp @@ -0,0 +1,115 @@ + +#include +#include +#include "common/ide.hpp" +#include "common/util.hpp" +#include "launcher/launcher.hpp" +#include "vendor/ff.h" + +LauncherError ExecutableLauncher::openFile(void) { + if (!args.drive || !args.path) { + LOG("required arguments missing"); + return INVALID_ARGS; + } + + // The drive index is always a single digit, so there is no need to pull in + // strtol() here. + int drive = args.drive[0] - '0'; + + if (drive < 0 || drive > 1) { + LOG("invalid drive ID: %d", drive); + return INVALID_ARGS; + } + if (ide::devices[drive].enumerate()) { + LOG("IDE init failed, drive=%s", args.drive); + return DRIVE_ERROR; + } + + auto error = f_mount(&_fs, args.drive, 1); + + if (error) { + LOG("FAT mount failed, code=%d, drive=%s", error, args.drive); + return FAT_ERROR; + } + + f_chdrive(args.drive); + error = f_open(&_file, args.path, FA_READ); + + if (error) { + LOG("open failed, code=%d, path=%s", error, args.path); + return FILE_ERROR; + } + + return NO_ERROR; +} + +LauncherError ExecutableLauncher::parseHeader(uint64_t offset) { + LOG("parsing header, offset=0x%lx", offset); + + auto error = f_lseek(&_file, offset); + + if (error) { + LOG("seek to header failed, code=%d, path=%s", error, args.path); + return FILE_ERROR; + } + + size_t length; + error = f_read(&_file, &_header, sizeof(_header), &length); + + if (error) { + LOG("header read failed, code=%d, path=%s", error, args.path); + return FILE_ERROR; + } + if (length != sizeof(_header)) { + LOG("invalid header length: %d", length); + return INVALID_FILE; + } + if (!_header.validateMagic()) { + LOG("invalid executable magic"); + return INVALID_FILE; + } + + _bodyOffset = offset + util::EXECUTABLE_BODY_OFFSET; + return NO_ERROR; +} + +LauncherError ExecutableLauncher::loadBody(void) { + auto error = f_lseek(&_file, _bodyOffset); + + if (error) { + LOG("seek to body failed, code=%d, path=%s", error, args.path); + return FILE_ERROR; + } + + size_t length; + error = f_read(&_file, _header.getTextPtr(), _header.textLength, &length); + + if (error) { + LOG("body read failed, code=%d, path=%s", error, args.path); + return FILE_ERROR; + } + if (length != _header.textLength) { + LOG("invalid body length: %d", length); + return INVALID_FILE; + } + + return NO_ERROR; +} + +void ExecutableLauncher::closeFile(void) { + if (_file.obj.fs) + f_close(&_file); + if (_fs.fs_type) + f_unmount(args.drive); +} + +extern "C" uint8_t _textStart[]; + +[[noreturn]] void ExecutableLauncher::run(void) { + util::ExecutableLoader loader(_header, _textStart - 16); + + for (int i = 0; i < args.argCount; i++) + loader.copyArgument(args.executableArgs[i]); + + loader.run(); +} diff --git a/src/launcher/launcher.hpp b/src/launcher/launcher.hpp new file mode 100644 index 0000000..e6b5fc5 --- /dev/null +++ b/src/launcher/launcher.hpp @@ -0,0 +1,42 @@ + +#pragma once + +#include +#include "common/args.hpp" +#include "common/util.hpp" +#include "vendor/ff.h" + +enum LauncherError { + NO_ERROR = 0, + INVALID_ARGS = 1, + DRIVE_ERROR = 2, + FAT_ERROR = 3, + FILE_ERROR = 4, + INVALID_FILE = 5 +}; + +class ExecutableLauncher { +private: + // Using the FatFs API directly (rather than through file::FATProvider) + // yields a smaller executable as it avoids pulling in malloc. + FATFS _fs; + FIL _file; + + util::ExecutableHeader _header; + uint64_t _bodyOffset; + +public: + args::ExecutableLauncherArgs args; + + inline ExecutableLauncher(void) { + _fs.fs_type = 0; + _file.obj.fs = nullptr; + } + + LauncherError openFile(void); + LauncherError parseHeader(uint64_t offset = 0); + LauncherError loadBody(void); + void closeFile(void); + + [[noreturn]] void run(void); +}; diff --git a/src/launcher/main.cpp b/src/launcher/main.cpp index 04eed92..8f3b062 100644 --- a/src/launcher/main.cpp +++ b/src/launcher/main.cpp @@ -1,237 +1,56 @@ -#include -#include -#include "common/ide.hpp" +#include "common/args.hpp" #include "common/io.hpp" #include "common/rom.hpp" #include "common/util.hpp" -#include "ps1/system.h" -#include "vendor/ff.h" +#include "launcher/launcher.hpp" -extern "C" uint8_t _textStart[]; - -class Settings { -public: - int baudRate, argCount; - const char *drive, *path; - const char *args[util::MAX_EXECUTABLE_ARGS]; - - inline Settings(void) - : baudRate(0), argCount(0), drive(nullptr), path(nullptr) {} - - bool parse(const char *arg); +static const uint32_t _EXECUTABLE_OFFSETS[]{ + 0, + rom::FLASH_EXECUTABLE_OFFSET, + util::EXECUTABLE_BODY_OFFSET }; -bool Settings::parse(const char *arg) { - if (!arg) - return false; - - switch (util::hash(arg, '=')) { -#if 0 - case "boot.rom"_h: - LOG("boot.rom=%s", &arg[9]); - return true; - - case "boot.from"_h: - LOG("boot.from=%s", &arg[10]); - return true; - - case "console"_h: - // Disabled to avoid pulling in strtol. - baudRate = int(strtol(&arg[8], nullptr, 0)); - return true; -#endif - - case "launcher.drive"_h: - drive = &arg[15]; - return true; - - case "launcher.path"_h: - path = &arg[14]; - return true; - - case "launcher.arg"_h: - if (argCount >= int(util::countOf(args))) - return false; - - args[argCount++] = &arg[13]; - return true; - - default: - return false; - } -} - -class Launcher { -private: - Settings &_settings; - - // Using the FatFs API directly (rather than through file::FATProvider) - // yields a smaller executable as it avoids pulling in malloc. - FATFS _fs; - FIL _file; - - util::ExecutableHeader _header; - -public: - inline Launcher(Settings &settings) - : _settings(settings) { - _fs.fs_type = 0; - _file.obj.fs = nullptr; - } - inline ~Launcher(void) { - exit(); - } - - bool openFile(void); - bool readHeader(uint64_t offset); - bool readBody(void); - void exit(void); - [[noreturn]] void run(void); -}; - -bool Launcher::openFile(void) { - if (!_settings.drive || !_settings.path) { - LOG("required arguments missing"); - return false; - } - - // As long as it works... - int drive = _settings.drive[0] - '0'; - - if (drive < 0 || drive > 1) { - LOG("invalid drive ID"); - return false; - } - if (ide::devices[drive].enumerate()) { - LOG("IDE init failed, drive=%s", _settings.drive); - return false; - } - if (f_mount(&_fs, _settings.drive, 1)) { - LOG("FAT mount failed, drive=%s", _settings.drive); - return false; - } - - f_chdrive(_settings.drive); - - if (f_open(&_file, _settings.path, FA_READ)) { - LOG("open failed, path=%s", _settings.path); - return false; - } - - return true; -} - -bool Launcher::readHeader(uint64_t offset) { - size_t length; - - if (f_lseek(&_file, offset)) { - LOG("seek to header failed, path=%s", _settings.path); - return false; - } - if (f_read(&_file, &_header, sizeof(_header), &length)) { - LOG("header read failed, path=%s", _settings.path); - return false; - } - if (length != sizeof(_header)) { - LOG("invalid header length %d", length); - return false; - } - if (!_header.validateMagic()) { - LOG("invalid executable magic"); - return false; - } - - if (f_lseek(&_file, offset + util::EXECUTABLE_BODY_OFFSET)) { - LOG("seek to body failed, path=%s", _settings.path); - return false; - } - - LOG("ptr=0x%08x, length=0x%x", _header.textOffset, _header.textLength); - return true; -} - -bool Launcher::readBody(void) { - size_t length; - - if (f_read(&_file, _header.getTextPtr(), _header.textLength, &length)) { - LOG("body read failed, path=%s", _settings.path); - return false; - } - if (length != _header.textLength) { - LOG("invalid body length %d", length); - return false; - } - - return true; -} - -void Launcher::exit(void) { - if (_file.obj.fs) - f_close(&_file); - if (_fs.fs_type) - f_unmount(_settings.drive); - - //uninstallExceptionHandler(); -} - -[[noreturn]] void Launcher::run(void) { - util::ExecutableLoader loader(_header, _textStart - 16); - - for (int i = 0; i < _settings.argCount; i++) - loader.addArgument(_settings.args[i]); - - exit(); - loader.run(); -} - int main(int argc, const char **argv) { -#if 0 - // Exception handling code bloats the binary significantly (especially in - // debug builds, as it pulls in the crash handler), so the watchdog is - // cleared manually instead. - installExceptionHandler(); io::init(); - setInterruptHandler([](void *dummy) { - if (acknowledgeInterrupt(IRQ_VSYNC)) - io::clearWatchdog(); - }, nullptr); - - IRQ_MASK = 1 << IRQ_VSYNC; - enableInterrupts(); -#endif - - Settings settings; - Launcher launcher(settings); - -#ifndef NDEBUG - // Enable serial port logging by default in debug builds. - settings.baudRate = 115200; -#endif + ExecutableLauncher launcher; for (; argc > 0; argc--) - settings.parse(*(argv++)); + launcher.args.parseArgument(*(argv++)); - //util::logger.setupSyslog(settings.baudRate); +#ifdef ENABLE_LOGGING + util::logger.setupSyslog(launcher.args.baudRate); +#endif + auto error = launcher.openFile(); io::clearWatchdog(); - if (!launcher.openFile()) - return 1; - io::clearWatchdog(); - if (!launcher.readHeader(0)) { - // If the file is not an executable, check if it is a flash image that - // contains an executable. Note that the CRC32 is not validated. - if (!launcher.readHeader(rom::FLASH_EXE_OFFSET)) - return 2; + if (error) + goto _exit; + + // Check for the presence of an executable at several different offsets + // within the file before giving up. + for (auto offset : _EXECUTABLE_OFFSETS) { + error = launcher.parseHeader(offset); + io::clearWatchdog(); + + if (error == INVALID_FILE) + continue; + if (error) + goto _exit; + + error = launcher.loadBody(); + io::clearWatchdog(); + + if (error) + goto _exit; + + launcher.closeFile(); + launcher.run(); } - io::clearWatchdog(); - if (!launcher.readBody()) - return 3; - - io::clearWatchdog(); - launcher.run(); - return 0; +_exit: + launcher.closeFile(); + return error; } diff --git a/src/libc/string.c b/src/libc/string.c index b2a2c74..c65ad7e 100644 --- a/src/libc/string.c +++ b/src/libc/string.c @@ -333,14 +333,13 @@ char *strtok(char *restrict str, const char *restrict delim) { long long strtoll(const char *restrict str, char **restrict str_end, int base) { if (!str) return 0; - while (isspace(*str)) str++; - int negative = (*str == '-'); - if (negative) - str++; + char sign = *str; + if ((sign == '+') || (sign == '-')) + str++; while (isspace(*str)) str++; @@ -349,7 +348,7 @@ long long strtoll(const char *restrict str, char **restrict str_end, int base) { long long value = 0; if (*str == '0') { - int _base; + int foundBase; switch (str[1]) { case 0: @@ -357,32 +356,32 @@ long long strtoll(const char *restrict str, char **restrict str_end, int base) { case 'X': case 'x': - _base = 16; - str += 2; + foundBase = 16; + str += 2; break; case 'O': case 'o': - _base = 8; - str += 2; + foundBase = 8; + str += 2; break; case 'B': case 'b': - _base = 2; - str += 2; + foundBase = 2; + str += 2; break; default: // Numbers starting with a zero are *not* interpreted as octal // unless base = 8. - _base = 0; + foundBase = 0; str++; } if (!base) - base = _base; - else if (base != _base) + base = foundBase; + else if (foundBase && (base != foundBase)) return 0; } @@ -420,7 +419,7 @@ _exit: if (str_end) *str_end = (char *) str; - return negative ? (-value) : value; + return (sign == '-') ? (-value) : value; } long strtol(const char *restrict str, char **restrict str_end, int base) { diff --git a/src/main/app/miscworkers.cpp b/src/main/app/miscworkers.cpp index 18f4ae4..87e3b3b 100644 --- a/src/main/app/miscworkers.cpp +++ b/src/main/app/miscworkers.cpp @@ -67,16 +67,22 @@ public: static const Launcher _LAUNCHERS[]{ { - .path = "launchers/801f8000.psexe", + .path = "binaries/launcher801f8000.psexe", .loadOffset = 0x801f8000, .length = 0x8000 }, { - .path = "launchers/803f8000.psexe", + .path = "binaries/launcher803f8000.psexe", .loadOffset = 0x803f8000, .length = 0x8000 } }; +static const uint32_t _EXECUTABLE_OFFSETS[]{ + 0, + rom::FLASH_EXECUTABLE_OFFSET, + util::EXECUTABLE_BODY_OFFSET +}; + bool App::_executableWorker(void) { _workerStatus.update(0, 1, WSTR("App.executableWorker.init")); @@ -87,27 +93,32 @@ bool App::_executableWorker(void) { goto _fileOpenError; util::ExecutableHeader header; - size_t length; - length = _file->read(&header, sizeof(header)); - - if (length != sizeof(header)) - goto _fileError; - - if (!header.validateMagic()) { - // If the file is not an executable, check if it is a flash image that - // contains an executable. Note that the CRC32 is not validated. - _file->seek(rom::FLASH_EXE_OFFSET); - length = _file->read(&header, sizeof(header)); + // Check for the presence of an executable at several different offsets + // within the file before giving up. + for (auto offset : _EXECUTABLE_OFFSETS) { + _file->seek(offset); + size_t length = _file->read(&header, sizeof(header)); if (length != sizeof(header)) - goto _fileError; - if (!header.validateMagic()) - goto _fileError; + break; + if (header.validateMagic()) + goto _validFile; } delete _file; +_fileOpenError: + _messageScreen.setMessage( + MESSAGE_ERROR, _filePickerScreen, + WSTR("App.executableWorker.fileError"), path + ); + _workerStatus.setNextScreen(_messageScreen); + return false; + +_validFile: + delete _file; + uintptr_t executableEnd, stackTop; executableEnd = header.textOffset + header.textLength; @@ -153,18 +164,6 @@ bool App::_executableWorker(void) { ); data.destroy(); - util::ExecutableLoader loader( - header, reinterpret_cast(launcherEnd) - ); - char arg[128]; - - snprintf( - arg, sizeof(arg), "launcher.drive=%s", _fileProvider.getDriveString() - ); - loader.addArgument(arg); - snprintf(arg, sizeof(arg), "launcher.path=%s", path); - loader.addArgument(arg); - // All destructors must be invoked manually as we are not returning to // main() before starting the new executable. _unloadCartData(); @@ -175,6 +174,18 @@ bool App::_executableWorker(void) { _fileProvider.close(); + util::ExecutableLoader loader( + header, reinterpret_cast(launcherEnd) + ); + char arg[128]; + + snprintf( + arg, sizeof(arg), "launcher.drive=%s", _fileProvider.getDriveString() + ); + loader.copyArgument(arg); + snprintf(arg, sizeof(arg), "launcher.path=%s", path); + loader.copyArgument(arg); + uninstallExceptionHandler(); loader.run(); } @@ -186,17 +197,6 @@ bool App::_executableWorker(void) { ); _workerStatus.setNextScreen(_messageScreen); return false; - -_fileError: - delete _file; - -_fileOpenError: - _messageScreen.setMessage( - MESSAGE_ERROR, _filePickerScreen, - WSTR("App.executableWorker.fileError"), path - ); - _workerStatus.setNextScreen(_messageScreen); - return false; } bool App::_atapiEjectWorker(void) { diff --git a/src/main/main.cpp b/src/main/main.cpp index 3706752..78fcbcf 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -1,6 +1,5 @@ -#include -#include +#include "common/args.hpp" #include "common/file.hpp" #include "common/gpu.hpp" #include "common/io.hpp" @@ -11,72 +10,6 @@ #include "ps1/gpucmd.h" #include "ps1/system.h" -extern "C" const uint8_t _resources[]; -extern "C" const size_t _resourcesSize; - -class Settings { -public: - int width, height; - bool forceInterlace; - int baudRate; - const void *resPtr; - size_t resLength; - - inline Settings(void) - : width(320), height(240), forceInterlace(false), baudRate(0), - resPtr(nullptr), resLength(0) {} - - bool parse(const char *arg); -}; - -bool Settings::parse(const char *arg) { - if (!arg) - return false; - - switch (util::hash(arg, '=')) { -#if 0 - case "boot.rom"_h: - LOG("boot.rom=%s", &arg[9]); - return true; - - case "boot.from"_h: - LOG("boot.from=%s", &arg[10]); - return true; -#endif - - case "console"_h: - baudRate = int(strtol(&arg[8], nullptr, 0)); - return true; - - case "screen.width"_h: - width = int(strtol(&arg[13], nullptr, 0)); - return true; - - case "screen.height"_h: - height = int(strtol(&arg[14], nullptr, 0)); - return true; - - case "screen.interlace"_h: - forceInterlace = bool(strtol(&arg[17], nullptr, 0)); - return true; - - // Allow the default assets to be overridden by passing a pointer to an - // in-memory ZIP file as a command-line argument. - case "resources.ptr"_h: - resPtr = reinterpret_cast( - strtol(&arg[14], nullptr, 16) - ); - return true; - - case "resources.length"_h: - resLength = size_t(strtol(&arg[17], nullptr, 16)); - return true; - - default: - return false; - } -} - int main(int argc, const char **argv) { installExceptionHandler(); gpu::init(); @@ -84,37 +17,29 @@ int main(int argc, const char **argv) { io::init(); util::initZipCRC32(); - Settings settings; + args::MainArgs args; -#ifndef NDEBUG - // Enable serial port logging by default in debug builds. - settings.baudRate = 115200; -#endif - -#ifdef ENABLE_ARGV for (; argc > 0; argc--) - settings.parse(*(argv++)); + args.parseArgument(*(argv++)); + +#ifdef ENABLE_LOGGING + util::logger.setupSyslog(args.baudRate); #endif - util::logger.setupSyslog(settings.baudRate); - - // Load the resource archive, first from memory if a pointer was given and - // then from the HDD. If both attempts fail, fall back to the archive - // embedded into the executable. - auto resourceProvider = new file::ZIPProvider; - - if (settings.resPtr && settings.resLength) { - if (resourceProvider->init(settings.resPtr, settings.resLength)) - goto _resourceInitDone; + // A pointer to the resource archive is always provided on the command line + // by the boot stub. + if (!args.resourcePtr || !args.resourceLength) { + LOG("required arguments missing"); + return 1; } - resourceProvider->init(_resources, _resourcesSize); + auto resourceProvider = new file::ZIPProvider; -_resourceInitDone: + resourceProvider->init(args.resourcePtr, args.resourceLength); io::clearWatchdog(); auto gpuCtx = new gpu::Context( - GP1_MODE_NTSC, settings.width, settings.height, settings.forceInterlace + GP1_MODE_NTSC, args.screenWidth, args.screenHeight, args.forceInterlace ); auto uiCtx = new ui::Context(*gpuCtx); auto app = new App(*uiCtx, *resourceProvider); diff --git a/src/ps1/system.c b/src/ps1/system.c index 3b2a199..d196a2b 100644 --- a/src/ps1/system.c +++ b/src/ps1/system.c @@ -28,10 +28,9 @@ /* Internal state */ -static uint32_t _savedBreakpointVector[4]; -static uint32_t _savedExceptionVector[4]; -static VoidFunction _flushCache = 0; -static Thread _mainThread; +static uint32_t _savedBreakpointVector[4]; +static uint32_t _savedExceptionVector[4]; +static Thread _mainThread; ArgFunction interruptHandler = 0; void *interruptHandlerArg = 0; @@ -41,6 +40,12 @@ Thread *nextThread = &_mainThread; /* Exception handler setup */ +static inline void _flushCache(void) { + // This is the only function that must always run from the BIOS ROM as it + // temporarily disables main RAM. + BIOS_API_TABLE[0x44](); +} + void _exceptionVector(void); void installExceptionHandler(void) { @@ -54,11 +59,6 @@ void installExceptionHandler(void) { // Disable interrupts and the GTE at the COP0 side. cop0_setSR(COP0_SR_CU0); - // Grab a direct pointer to the BIOS function to flush the instruction - // cache. This is the only function that must always run from the BIOS ROM - // as it temporarily disables main RAM. - _flushCache = BIOS_API_TABLE[0x44]; - // Overwrite the default breakpoint and exception handlers placed into RAM // by the BIOS with a function that will jump to our custom handler. __builtin_memcpy(_savedBreakpointVector, BIOS_BP_VECTOR, 16); @@ -100,9 +100,6 @@ void setInterruptHandler(ArgFunction func, void *arg) { } void flushCache(void) { - if (!_flushCache) - _flushCache = BIOS_API_TABLE[0x44]; - bool enable = disableInterrupts(); _flushCache(); diff --git a/tools/buildResourceArchive.py b/tools/buildResourceArchive.py index 4388e13..5e41a6b 100755 --- a/tools/buildResourceArchive.py +++ b/tools/buildResourceArchive.py @@ -13,7 +13,7 @@ from struct import Struct from typing import Any, ByteString, Generator, Mapping, Sequence from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile -import numpy +import lz4.block, numpy from numpy import ndarray from PIL import Image @@ -346,18 +346,20 @@ def createParser() -> ArgumentParser: help = "Show this help message and exit" ) - group = parser.add_argument_group("ZIP compression options") + group = parser.add_argument_group("Compression options") group.add_argument( - "-c", "--compress-level", - type = int, - default = 9, - help = "Set default gzip compression level (default 9)", - metavar = "0-9" + "-c", "--compression", + type = str, + choices = ( "none", "deflate", "lz4" ), + default = "deflate", + help = "Set default compression algorithm (default DEFLATE)" ) group.add_argument( - "-n", "--no-compression", - action = "store_true", - help = "Forcefully disable gzip compression for all files" + "-l", "--compress-level", + type = int, + default = 9, + help = "Set default DEFLATE and LZ4 compression level (default 9)", + metavar = "0-9" ) group = parser.add_argument_group("File paths") @@ -449,14 +451,32 @@ def main(): case _type: raise KeyError(f"unsupported asset type '{_type}'") - gzipLevel: int | None = asset.get("compress", args.compress_level) - disallow: bool = \ - (len(data) < 1024) or (gzipLevel is None) or args.no_compression + compressLevel: int | None = \ + asset.get("compressLevel", args.compress_level) - _zip.writestr( - asset["name"], data, - ZIP_STORED if disallow else ZIP_DEFLATED, gzipLevel - ) + match asset.get("compression", args.compression).strip(): + case "none" | None: + _zip.writestr(asset["name"], data, ZIP_STORED) + + case "deflate": + _zip.writestr( + asset["name"], data, ZIP_DEFLATED, compressLevel + ) + + case "lz4": + # ZIP archives do not "officially" support LZ4 compression, + # so the entry is stored as an uncompressed file. + compressed: bytes = lz4.block.compress( + data, + mode = "high_compression", + compression = compressLevel, + store_size = False + ) + + _zip.writestr(asset["name"], compressed, ZIP_STORED) + + case _type: + raise KeyError(f"unsupported compression type '{_type}'") if __name__ == "__main__": main() diff --git a/tools/requirements.txt b/tools/requirements.txt index f75a8e6..334576e 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -2,5 +2,6 @@ # py -m pip install -r tools/requirements.txt (Windows) # sudo pip install -r tools/requirements.txt (Linux/macOS) +lz4 >= 4.3.2 numpy >= 1.19.4 Pillow >= 8.2.0