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
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

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
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