update graphic compression documentation

This commit is contained in:
Michael Miceli 2023-07-16 23:30:30 -04:00
parent 3483be127a
commit 6099316fbc
2 changed files with 111 additions and 29 deletions

View File

@ -370,7 +370,7 @@ addresses in NES like Object Attribute Memory (OAM).
# Palette
The NES supports 4 palettes for the nametables (background) and 4 palettes for
sprites. A palette is a set of 3 colors. For nametables, one palette is shared
sprites. A palette is a set of 4 colors. For nametables, one palette is shared
for each square 16x16 set of 4 pattern table tiles. For sprites, each tile in a
sprite has its own palette.
@ -388,7 +388,8 @@ level header starting at byte offset 15 (see `LEVEL_PALETTE_INDEX`). Each of
the next 8 bytes specify an entry into `game_palettes`. The first 4 bytes are
the nametable palette colors, and the next 4 bytes are the sprite palette
colors. Each entry in `game_palettes` specifies the 3 colors that make up the
palette.
palette. Each palette always starts with a black color #$0f. Combined this
is how the 4 colors of each palette are determined.
## Fade-In Effect
_Contra_ supports a fade in effect for nametable palettes. This effect is used
@ -448,17 +449,33 @@ compression algorithm known as
The idea of this algorithm is that the graphics data will have long "runs" of
repeated information. So instead of specifying the same byte over and over, it
can be encoded so that the number of repetitions is specified. Essentially,
instead of
* #$00 #$00 #$00 #$00 #$00 #$00 #$00 #$00 #$00
It can be represented like
* #$89 #$00
can be encoded so that the number of repetitions is specified. In addition,
_Contra_'s algorithm specifies when a run of bytes isn't the same. Below is
an example that can help clarify.
Below is an example of un-compressed graphics data
```
#$00 #$00 #$00 #00 #$00 #$00 #$0e #$1f #$07 #$04 #$c0
```
When compressed, it becomes
```
#$06 #$00 #$85 #$0e #$1f #$07 #$0f #$c0 #$ff
```
The special codes in this sequence are #$06, #$85, and #$ff
#$06 means the next byte #$00 is repeated 6 times. Since the next code has bit
7 set, #$85 means to write the next 5 bytes to the PPU. Finally, #$ff means the
sequence is complete.
Interestingly, this algorithm is implemented twice in _Contra_: once for pattern
table tiles (`write_graphic_data_to_ppu` and once again for super-tile
screen indexes (`load_supertiles_screen_indexes`).
screen indexes (`load_supertiles_screen_indexes`). Each implementation is
slightly different.
## Pattern Table Decompression
## graphic_data_xx Decompression
The graphic data is loaded from ROM directly to the PPU. Not only is the
graphic data compressed, it also includes command bytes that specify where in
the PPU to write the data to. This section outlines how the data is read and
@ -474,15 +491,77 @@ starting at PPU address $0680 (pattern table).
After the first two bytes are read, the following algorithm reads the graphics
data section. When reading the graphic data, if the most significant bit of the
graphic data byte is set, i.e 1, then the byte is treated as a command byte.
There are 3 command byte types
There are 4 command byte types, evaluated in the following order
* #$ff - specifies the end of graphic data
* #$7f - tells the algorithm to change PPU write address to the next 2 bytes
specified.
* bit 7 set - any byte larger than #$7f, or alternatively, any byte that has
its most significant bit set to 1 is treated as a RLE command to write the
next byte of data multiple times to the PPU. This excludes #$ff, which
always means end of graphic data
* #$7f - command byte specifies to change PPU write address to the next 2
bytes specified.
* less than #$7f - or alternatively, any command byte that has its most
significant bit clear is treated as a RLE command to write the subsequent
byte to the PPU multiple times. The number of times to repeatedly write the
next byte is specified by the command byte value.
* greater than #$7f - or alternatively, any command byte that has its most
significant bit set is (but not #$ff) is interpreted as a command to write
the next string of bytes to the PPU. The number of bytes to write to the
PPU is specified by bits 0-6 of the command byte.
Below is some pseudocode for decompressing a graphics section and writing it to
the PPU. Note that due to the implementation, you can only flip graphics data
horizontally when that data only writes to a single PPU address location.
```
parse_ppu_address() {
byte b = read_next_byte();
write_to_PPUADDR(b);
b = read_next_byte();
write_to_PPUADDR(b);
if(flip_horizontally) {
read_next_byte();
read_next_byte();
}
}
while(true) {
parse_ppu_address();
byte b = read_next_byte();
if(b == 0xff) {
// finished decompressing graphics data
return;
}
while(true) {
if(b == 0x7f) {
// finished reading one section of compressed graphic
// next bytes are new PPU address
break;
}
if(b < 0x7f) {
// write same byte multiple times
byte numberOfRepetitions = b;
for (byte i = 0; i < numberOfRepetitions; i++) {
if(flip_horizontally) {
b = flip_horizontally(b);
}
write_to_PPUDATA(b);
}
} else if(b > 0x7f) {
// write next numberOfBytesToWrite bytes directly to PPU
byte numberOfBytesToWrite = b & 0x7f;
for (byte i = 0; i < numberOfBytesToWrite; i++) {
byte d = read_next_byte();
if(flip_horizontally) {
d = flip_horizontally(d);
}
write_to_PPUDATA(d);
}
}
}
}
```
Level section data not encoded exactly the same way
`level_1_supertiles_screen_ptr_table`

View File

@ -2293,25 +2293,26 @@ write_graphic_data_sequences_to_ppu:
beq end_graphic_code ; loaded entire graphics data, restore previously loaded bank, re-init PPU
cmp #$7f ; used to specify the PPU write address should change to the address specified in the next 2 bytes)
beq change_ppu_write_address
tay ; store number of repetitions in Y
bpl write_graphic_byte_a_times ; if byte is < #$7f, write the following byte multiple times
and #$7f ; byte is < #$7f, clear bit 7
tay ; store command code byte in y
bpl write_graphic_byte_a_times ; branch if byte is < #$7f to write the next byte multiple times (RLE-command)
and #$7f ; byte has bit 7 set (negative), code is writing a string of bytes
; clear bit 7 to get number of bytes to write from compressed data
sta $02 ; store positive portion in $02, this is the number of bytes to write to PPU
ldy #$01 ; skip past size byte, prepare to read next n graphic bytes
; writes the next n bytes of the graphic compression sequence to the PPU, starting at offset Y
; writes the next n bytes of the compressed graphic data to the PPU, starting at offset Y
; input
; * $02 - the number of bytes to write
; * $02 - the number of bytes to write (n)
; * y - the graphic data read offset
write_next_n_sequence_bytes:
lda ($00),y ; read graphic byte
ldx $04 ; load graphic data horizontal flip bit value
bpl write_repetition_sequence_byte ; branch if not flipping graphic byte
bpl @write_byte_to_ppu ; branch if not flipping graphic byte
jsr horizontal_flip_graphic_byte ; flipping horizontally, flip data before writing to PPU
; writes value of a to the PPU
; then determines if done with n bytes of graphic data
write_repetition_sequence_byte:
; then determines if done with the n-length byte sequence of graphic data
@write_byte_to_ppu:
sta PPUDATA ; write graphic byte to PPU
cpy $02 ; see if written all n repetitions
beq advance_graphic_read_addr_n_bytes ; written all n bytes, update base graphic read address
@ -2320,10 +2321,12 @@ write_repetition_sequence_byte:
; advances the address of the current graphic byte offset by n bytes
; where n is the value in $02 + #$1
; the #$01 is necessary to skip passed the command byte
advance_graphic_read_addr_n_bytes:
lda #$01
lda #$01 ; advancing graphic byte read address by 1 (size of repetition string)
clc ; clear carry in preparation for addition
adc $02 ; set A to the number of bytes to skip (they've already been written to the PPU)
adc $02 ; skip over the bytes just written the PPU
; a now has the number of bytes to skip
advance_ppu_write_addr:
ldx #$00 ; specifies that the 2-byte graphic read address is located at $00