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.
This commit is contained in:
Damian Yerrick 2019-07-16 16:58:16 -04:00
parent eb95f1de55
commit 4504de828a
3 changed files with 389 additions and 43 deletions

View File

@ -532,7 +532,7 @@ TrademarkSymbol:
db $3c,$42,$b9,$a5,$b9,$a5,$42,$3c db $3c,$42,$b9,$a5,$b9,$a5,$42,$3c
SameBoyLogo: SameBoyLogo:
incbin "SameBoyLogo.rle" incbin "SameBoyLogo.pb8"
AnimationColors: AnimationColors:
dw $7FFF ; White dw $7FFF ; White
@ -634,41 +634,55 @@ ReadCGBLogoHalfTile:
ld a, e ld a, e
ret 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: LoadTileset:
; Copy SameBoy Logo ld hl, SameBoyLogo
ld de, SameBoyLogo ld de, SameBoyLogo_dst
ld hl, $8080 ld c, SameBoyLogo_length
.sameboyLogoLoop .pb8BlockLoop:
ld a, [de] ; Register map for PB8 decompression
inc de ; HL: source address in boot ROM
; DE: destination address in VRAM
ld b, a ; A: Current literal value
and $0f ; B: Repeat bits, terminated by 1000...
jr z, .skipLiteral ; C: Number of 8-byte blocks left in this block
ld c, a ; Source address in HL lets the repeat bits go straight to B,
; bypassing A and avoiding spilling registers to the stack.
.literalLoop ld b, [hl]
ld a, [de]
ldi [hl], a
inc 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 ; Shift a 1 into lower bit of shift value. Once this bit
ldi [hl], a ; reaches the carry, B becomes 0 and the byte is over
inc hl 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 dec c
jr nz, .repeatLoop jr nz, .pb8BlockLoop
jr .sameboyLogoLoop
; End PB8 decoding. The rest uses HL as the destination
ld h, d
ld l, e
.sameboyLogoEnd .sameboyLogoEnd
; Copy (unresized) ROM logo ; Copy (unresized) ROM logo

330
BootROMs/pb8.c Normal file
View File

@ -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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <assert.h>
// For setting stdin/stdout to binary mode
#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__))
#include <unistd.h>
#define fd_isatty isatty
#elif defined (_WIN32)
#include <io.h>
#include <fcntl.h>
#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 <https://pineight.com/contact/>\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 : "<stdin>");
has_ferror = EOF;
}
fclose(infp);
if (ferror(outfp)) {
fprintf(stderr, "pb8: error writing %s\n",
outfilename ? outfilename : "<stdout>");
has_ferror = EOF;
}
fclose(outfp);
if (compfailed && !has_ferror) {
fputs("pb8: unknown compression failure\n", stderr);
}
return (compfailed || has_ferror) ? EXIT_FAILURE : EXIT_SUCCESS;
}

View File

@ -13,13 +13,15 @@ ifneq ($(findstring MSYS,$(PLATFORM)),)
PLATFORM := windows32 PLATFORM := windows32
endif endif
LOGO_COMPRESS := build/logo-compress
ifeq ($(PLATFORM),windows32) ifeq ($(PLATFORM),windows32)
_ := $(shell chcp 65001) _ := $(shell chcp 65001)
LOGO_COMPRESS := build/logo-compress.exe EXESUFFIX:=.exe
else
EXESUFFIX:=
endif endif
PB8_COMPRESS := build/pb8$(EXESUFFIX)
ifeq ($(PLATFORM),Darwin) ifeq ($(PLATFORM),Darwin)
DEFAULT := cocoa DEFAULT := cocoa
else else
@ -302,11 +304,11 @@ $(BIN)/tester/sameboy_tester.exe: $(CORE_OBJECTS) $(SDL_OBJECTS)
$(BIN)/SDL/%.bin $(BIN)/tester/%.bin: $(BOOTROMS_DIR)/%.bin $(BIN)/SDL/%.bin $(BIN)/tester/%.bin: $(BOOTROMS_DIR)/%.bin
-@$(MKDIR) -p $(dir $@) -@$(MKDIR) -p $(dir $@)
cp -f $^ $@ cp -f $^ $@
$(BIN)/SameBoy.app/Contents/Resources/%.bin: $(BOOTROMS_DIR)/%.bin $(BIN)/SameBoy.app/Contents/Resources/%.bin: $(BOOTROMS_DIR)/%.bin
-@$(MKDIR) -p $(dir $@) -@$(MKDIR) -p $(dir $@)
cp -f $^ $@ cp -f $^ $@
$(BIN)/SDL/LICENSE: LICENSE $(BIN)/SDL/LICENSE: LICENSE
-@$(MKDIR) -p $(dir $@) -@$(MKDIR) -p $(dir $@)
cp -f $^ $@ cp -f $^ $@
@ -314,7 +316,7 @@ $(BIN)/SDL/LICENSE: LICENSE
$(BIN)/SDL/registers.sym: Misc/registers.sym $(BIN)/SDL/registers.sym: Misc/registers.sym
-@$(MKDIR) -p $(dir $@) -@$(MKDIR) -p $(dir $@)
cp -f $^ $@ cp -f $^ $@
$(BIN)/SDL/background.bmp: SDL/background.bmp $(BIN)/SDL/background.bmp: SDL/background.bmp
-@$(MKDIR) -p $(dir $@) -@$(MKDIR) -p $(dir $@)
cp -f $^ $@ cp -f $^ $@
@ -329,17 +331,17 @@ $(OBJ)/%.1bpp: %.png
-@$(MKDIR) -p $(dir $@) -@$(MKDIR) -p $(dir $@)
rgbgfx -d 1 -h -o $@ $< rgbgfx -d 1 -h -o $@ $<
$(OBJ)/BootROMs/SameBoyLogo.rle: $(OBJ)/BootROMs/SameBoyLogo.1bpp $(LOGO_COMPRESS) $(OBJ)/BootROMs/SameBoyLogo.pb8: $(OBJ)/BootROMs/SameBoyLogo.1bpp $(PB8_COMPRESS)
$(realpath $(LOGO_COMPRESS)) < $< > $@ $(realpath $(PB8_COMPRESS)) -l 384 $< $@
$(LOGO_COMPRESS): BootROMs/logo-compress.c $(PB8_COMPRESS): BootROMs/pb8.c
$(CC) $< -o $@ $(CC) $< -o $@
$(BIN)/BootROMs/agb_boot.bin: BootROMs/cgb_boot.asm $(BIN)/BootROMs/agb_boot.bin: BootROMs/cgb_boot.asm
$(BIN)/BootROMs/cgb_boot_fast.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/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 $@) -@$(MKDIR) -p $(dir $@)
rgbasm -i $(OBJ)/BootROMs/ -i BootROMs/ -o $@.tmp $< rgbasm -i $(OBJ)/BootROMs/ -i BootROMs/ -o $@.tmp $<
rgblink -o $@.tmp2 $@.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 Core (uses its own build system)
libretro: libretro:
$(MAKE) -C libretro $(MAKE) -C libretro
# Clean # Clean
clean: clean:
rm -rf build rm -rf build