From 4504de828a188b520a324687a1181af6c45a7e3a Mon Sep 17 00:00:00 2001 From: Damian Yerrick Date: Tue, 16 Jul 2019 16:58:16 -0400 Subject: [PATCH] cgb_boot: Compress logo with PB8 The logo is compressed using PB8, a form of RLE with unary-coded run lengths. Each block representing 8 bytes consists of a control byte, where each bit (MSB to LSB) is 0 for literal or 1 for repeat previous, followed by the literals in that block. PB8 compression is also used in a few NES games. A variant called PB16, where 1 means repeat 2 bytes back, is used in the Game Boy port of 240p Test Suite and in Libbet and the Magic Floor. Switching from logo-compress RLE to PB8 decreases the compressed logo data size from 287 bytes to 253 bytes, saving 34 bytes. The decompression code is also about 10 bytes smaller. --- BootROMs/cgb_boot.asm | 78 ++++++---- BootROMs/pb8.c | 330 ++++++++++++++++++++++++++++++++++++++++++ Makefile | 24 +-- 3 files changed, 389 insertions(+), 43 deletions(-) create mode 100644 BootROMs/pb8.c diff --git a/BootROMs/cgb_boot.asm b/BootROMs/cgb_boot.asm index 0472cbe..6ae869b 100644 --- a/BootROMs/cgb_boot.asm +++ b/BootROMs/cgb_boot.asm @@ -532,7 +532,7 @@ TrademarkSymbol: db $3c,$42,$b9,$a5,$b9,$a5,$42,$3c SameBoyLogo: - incbin "SameBoyLogo.rle" + incbin "SameBoyLogo.pb8" AnimationColors: dw $7FFF ; White @@ -634,41 +634,55 @@ ReadCGBLogoHalfTile: ld a, e ret +; LoadTileset using PB8 codec, 2019 Damian Yerrick +; +; The logo is compressed using PB8, a form of RLE with unary-coded +; run lengths. Each block representing 8 bytes consists of a control +; byte, where each bit (MSB to LSB) is 0 for literal or 1 for repeat +; previous, followed by the literals in that block. + +SameBoyLogo_dst = $8080 +SameBoyLogo_length = (128 * 24) / 64 + LoadTileset: -; Copy SameBoy Logo - ld de, SameBoyLogo - ld hl, $8080 -.sameboyLogoLoop - ld a, [de] - inc de - - ld b, a - and $0f - jr z, .skipLiteral - ld c, a - -.literalLoop - ld a, [de] - ldi [hl], a + ld hl, SameBoyLogo + ld de, SameBoyLogo_dst + ld c, SameBoyLogo_length +.pb8BlockLoop: + ; Register map for PB8 decompression + ; HL: source address in boot ROM + ; DE: destination address in VRAM + ; A: Current literal value + ; B: Repeat bits, terminated by 1000... + ; C: Number of 8-byte blocks left in this block + ; Source address in HL lets the repeat bits go straight to B, + ; bypassing A and avoiding spilling registers to the stack. + ld b, [hl] inc hl - inc de - dec c - jr nz, .literalLoop -.skipLiteral - swap b - ld a, b - and $0f - jr z, .sameboyLogoEnd - ld c, a - ld a, [de] - inc de -.repeatLoop - ldi [hl], a - inc hl + ; Shift a 1 into lower bit of shift value. Once this bit + ; reaches the carry, B becomes 0 and the byte is over + scf + rl b + +.pb8BitLoop: + ; If not a repeat, load a literal byte + jr c,.pb8Repeat + ld a, [hli] +.pb8Repeat: + ; Decompressed data uses colors 0 and 1, so write once, inc twice + ld [de], a + inc de + inc de + sla b + jr nz, .pb8BitLoop + dec c - jr nz, .repeatLoop - jr .sameboyLogoLoop + jr nz, .pb8BlockLoop + +; End PB8 decoding. The rest uses HL as the destination + ld h, d + ld l, e .sameboyLogoEnd ; Copy (unresized) ROM logo diff --git a/BootROMs/pb8.c b/BootROMs/pb8.c new file mode 100644 index 0000000..03a196e --- /dev/null +++ b/BootROMs/pb8.c @@ -0,0 +1,330 @@ +/* + +PB8 compressor and decompressor + +Copyright 2019 Damian Yerrick + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +*/ + +#include +#include +#include +#include +#include +#include + +// For setting stdin/stdout to binary mode +#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__)) +#include +#define fd_isatty isatty +#elif defined (_WIN32) +#include +#include +#define fd_isatty _isatty +#endif + +/* + +; The logo is compressed using PB8, a form of RLE with unary-coded +; run lengths. Each block representing 8 bytes consists of a control +; byte, where each bit (MSB to LSB) is 0 for literal or 1 for repeat +; previous, followed by the literals in that block. + +SameBoyLogo_dst = $8080 +SameBoyLogo_length = (128 * 24) / 64 + +LoadTileset: + ld hl, SameBoyLogo + ld de, SameBoyLogo_dst + ld c, SameBoyLogo_length +.pb8BlockLoop: + ; Register map for PB8 decompression + ; HL: source address in boot ROM + ; DE: destination address in VRAM + ; A: Current literal value + ; B: Repeat bits, terminated by 1000... + ; C: Number of 8-byte blocks left in this block + ; Source address in HL lets the repeat bits go straight to B, + ; bypassing A and avoiding spilling registers to the stack. + ld b, [hl] + inc hl + + ; Shift a 1 into lower bit of shift value. Once this bit + ; reaches the carry, B becomes 0 and the byte is over + scf + rl b + +.pb8BitLoop: + ; If not a repeat, load a literal byte + jr c,.pb8Repeat + ld a, [hli] +.pb8Repeat: + ; Decompressed data uses colors 0 and 1, so write once, inc twice + ld [de], a + inc de + inc de + sla b + jr nz, .pb8BitLoop + + dec c + jr nz, .pb8BlockLoop + ret + +*/ + +/* Compressor and decompressor *************************************/ + +/** + * Compresses an input stream to PB8 data on an output stream. + * @param infp input stream + * @param outfp output stream + * @param blocklength size of an independent input block in bytes + * @return 0 for reaching infp end of file, or EOF for error + */ +int pb8(FILE *infp, FILE *outfp, size_t blocklength) { + blocklength >>= 3; // convert bytes to blocks + assert(blocklength > 0); + while (1) { + int last_byte = EOF; // value that never occurs in a file + for (size_t blkleft = blocklength; blkleft > 0; --blkleft) { + unsigned int control_byte = 0x0001; + unsigned char literals[8]; + size_t nliterals = 0; + while (control_byte < 0x100) { + int c = fgetc(infp); + if (c == EOF) break; + + control_byte <<= 1; + if (c == last_byte) { + control_byte |= 0x01; + } else { + literals[nliterals++] = last_byte = c; + } + } + if (control_byte > 1) { + // Fill partial block with repeats + while (control_byte < 0x100) { + control_byte = (control_byte << 1) | 1; + } + + // Write control byte and check for write failure + int ok = fputc(control_byte & 0xFF, outfp); + if (ok == EOF) return EOF; + size_t ok2 = fwrite(literals, 1, nliterals, outfp); + if (ok2 < nliterals) return EOF; + } + + // If finished, return success or failure + if (ferror(infp) || ferror(outfp)) return EOF; + if (feof(infp)) return 0; + } // End 8-byte block + } // End packet, resetting last_byte +} + +/** + * Decompresses PB8 data on an input stream to an output stream. + * @param infp input stream + * @param outfp output stream + * @return 0 for reaching infp end of file, or EOF for error + */ +int unpb8(FILE *infp, FILE *outfp) { + int last_byte = 0; + while (1) { + int control_byte = fgetc(infp); + if (control_byte == EOF) { + return feof(infp) ? 0 : EOF; + } + control_byte &= 0xFF; + for (size_t bytesleft = 8; bytesleft > 0; --bytesleft) { + if (!(control_byte & 0x80)) { + last_byte = fgetc(infp); + if (last_byte == EOF) return EOF; // read error + } + control_byte <<= 1; + int ok = fputc(last_byte, outfp); + if (ok == EOF) return EOF; + } + } +} + +/* CLI frontend ****************************************************/ + +static inline void set_fd_binary(unsigned int fd) { +#ifdef _WIN32 + _setmode(fd, _O_BINARY); +#else + (void) fd; +#endif +} + +static const char *usage_msg = +"usage: pb8 [-d] [-l blocklength] [infile [outfile]]\n" +"Compresses a file using RLE with unary run and literal lengths.\n" +"\n" +"options:\n" +" -d decompress\n" +" -l blocklength allow RLE packets to span up to blocklength\n" +" input bytes (multiple of 8; default 8)\n" +" -h, -?, --help show this usage page\n" +" --version show copyright info\n" +"\n" +"If infile is - or missing, it is standard input.\n" +"If outfile is - or missing, it is standard output.\n" +"You cannot compress to or decompress from a terminal.\n" +; +static const char *version_msg = +"PB8 compressor (C version) v0.01\n" +"Copyright 2019 Damian Yerrick \n" +"This software is provided 'as-is', without any express or implied\n" +"warranty.\n" +; +static const char *toomanyfilenames_msg = +"pb8: too many filenames; try pb8 --help\n"; + +int main(int argc, char **argv) { + const char *infilename = NULL; + const char *outfilename = NULL; + bool decompress = false; + size_t blocklength = 8; + + for (int i = 1; i < argc; ++i) { + if (argv[i][0] == '-' && argv[i][1] != 0) { + if (!strcmp(argv[i], "--help")) { + fputs(usage_msg, stdout); + return 0; + } + if (!strcmp(argv[i], "--version")) { + fputs(version_msg, stdout); + return 0; + } + + // -t1 or -t 1 + int argtype = argv[i][1]; + switch (argtype) { + case 'h': + case '?': + fputs(usage_msg, stdout); + return 0; + + case 'd': + decompress = true; + break; + + case 'l': { + const char *argvalue = argv[i][2] ? argv[i] + 2 : argv[++i]; + const char *endptr = NULL; + + unsigned long tvalue = strtoul(argvalue, (char **)&endptr, 10); + if (endptr == argvalue || tvalue == 0 || tvalue > SIZE_MAX) { + fprintf(stderr, "pb8: block length %s not a positive integer\n", + argvalue); + return EXIT_FAILURE; + } + if (tvalue % 8 != 0) { + fprintf(stderr, "pb8: block length %s not a multiple of 8\n", + argvalue); + return EXIT_FAILURE; + } + blocklength = tvalue; + } break; + + default: + fprintf(stderr, "pb8: unknown option -%c\n", argtype); + return EXIT_FAILURE; + } + } else if (!infilename) { + infilename = argv[i]; + } else if (!outfilename) { + outfilename = argv[i]; + } else { + fputs(toomanyfilenames_msg, stderr); + return EXIT_FAILURE; + } + } + if (infilename && !strcmp(infilename, "-")) { + infilename = NULL; + } + if (!infilename && decompress && fd_isatty(0)) { + fputs("pb8: cannot decompress from terminal; try redirecting stdin\n", + stderr); + return EXIT_FAILURE; + } + if (outfilename && !strcmp(outfilename, "-")) { + outfilename = NULL; + } + if (!outfilename && !decompress && fd_isatty(1)) { + fputs("pb8: cannot compress to terminal; try redirecting stdout or pb8 --help\n", + stderr); + return EXIT_FAILURE; + } + + FILE *infp = NULL; + if (infilename) { + infp = fopen(infilename, "rb"); + if (!infp) { + fprintf(stderr, "pb8: error opening %s ", infilename); + perror("for reading"); + return EXIT_FAILURE; + } + } else { + infp = stdin; + set_fd_binary(0); + } + + FILE *outfp = NULL; + if (outfilename) { + outfp = fopen(outfilename, "wb"); + if (!outfp) { + fprintf(stderr, "pb8: error opening %s ", outfilename); + perror("for writing"); + fclose(infp); + return EXIT_FAILURE; + } + } else { + outfp = stdout; + set_fd_binary(1); + } + + int compfailed = 0; + int has_ferror = 0; + if (decompress) { + compfailed = unpb8(infp, outfp); + } else { + compfailed = pb8(infp, outfp, blocklength); + } + fflush(outfp); + if (ferror(infp)) { + fprintf(stderr, "pb8: error reading %s\n", + infilename ? infilename : ""); + has_ferror = EOF; + } + fclose(infp); + if (ferror(outfp)) { + fprintf(stderr, "pb8: error writing %s\n", + outfilename ? outfilename : ""); + has_ferror = EOF; + } + fclose(outfp); + + if (compfailed && !has_ferror) { + fputs("pb8: unknown compression failure\n", stderr); + } + + return (compfailed || has_ferror) ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/Makefile b/Makefile index 401fa3a..5db8c42 100644 --- a/Makefile +++ b/Makefile @@ -13,13 +13,15 @@ ifneq ($(findstring MSYS,$(PLATFORM)),) PLATFORM := windows32 endif -LOGO_COMPRESS := build/logo-compress - ifeq ($(PLATFORM),windows32) _ := $(shell chcp 65001) -LOGO_COMPRESS := build/logo-compress.exe +EXESUFFIX:=.exe +else +EXESUFFIX:= endif +PB8_COMPRESS := build/pb8$(EXESUFFIX) + ifeq ($(PLATFORM),Darwin) DEFAULT := cocoa else @@ -302,11 +304,11 @@ $(BIN)/tester/sameboy_tester.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) $(BIN)/SDL/%.bin $(BIN)/tester/%.bin: $(BOOTROMS_DIR)/%.bin -@$(MKDIR) -p $(dir $@) cp -f $^ $@ - + $(BIN)/SameBoy.app/Contents/Resources/%.bin: $(BOOTROMS_DIR)/%.bin -@$(MKDIR) -p $(dir $@) cp -f $^ $@ - + $(BIN)/SDL/LICENSE: LICENSE -@$(MKDIR) -p $(dir $@) cp -f $^ $@ @@ -314,7 +316,7 @@ $(BIN)/SDL/LICENSE: LICENSE $(BIN)/SDL/registers.sym: Misc/registers.sym -@$(MKDIR) -p $(dir $@) cp -f $^ $@ - + $(BIN)/SDL/background.bmp: SDL/background.bmp -@$(MKDIR) -p $(dir $@) cp -f $^ $@ @@ -329,17 +331,17 @@ $(OBJ)/%.1bpp: %.png -@$(MKDIR) -p $(dir $@) rgbgfx -d 1 -h -o $@ $< -$(OBJ)/BootROMs/SameBoyLogo.rle: $(OBJ)/BootROMs/SameBoyLogo.1bpp $(LOGO_COMPRESS) - $(realpath $(LOGO_COMPRESS)) < $< > $@ +$(OBJ)/BootROMs/SameBoyLogo.pb8: $(OBJ)/BootROMs/SameBoyLogo.1bpp $(PB8_COMPRESS) + $(realpath $(PB8_COMPRESS)) -l 384 $< $@ -$(LOGO_COMPRESS): BootROMs/logo-compress.c +$(PB8_COMPRESS): BootROMs/pb8.c $(CC) $< -o $@ $(BIN)/BootROMs/agb_boot.bin: BootROMs/cgb_boot.asm $(BIN)/BootROMs/cgb_boot_fast.bin: BootROMs/cgb_boot.asm $(BIN)/BootROMs/sgb2_boot: BootROMs/sgb_boot.asm -$(BIN)/BootROMs/%.bin: BootROMs/%.asm $(OBJ)/BootROMs/SameBoyLogo.rle +$(BIN)/BootROMs/%.bin: BootROMs/%.asm $(OBJ)/BootROMs/SameBoyLogo.pb8 -@$(MKDIR) -p $(dir $@) rgbasm -i $(OBJ)/BootROMs/ -i BootROMs/ -o $@.tmp $< rgblink -o $@.tmp2 $@.tmp @@ -349,7 +351,7 @@ $(BIN)/BootROMs/%.bin: BootROMs/%.asm $(OBJ)/BootROMs/SameBoyLogo.rle # Libretro Core (uses its own build system) libretro: $(MAKE) -C libretro - + # Clean clean: rm -rf build