Saxman compression

From Sega Retro

Saxman compression is the name given to the compression format used in the game Sonic the Hedgehog 2 for the Sega Genesis/Mega Drive. Its name was given in honor of the person who cracked the format, saxman. It is a variation of the LZSS algorithm.


The two uses of Saxman compression in Sonic the Hedgehog 2 are:

  • It is used to compress music data. In this case, the compressed data is prefixed with a small 2-byte header, listing the size of the compressed data in little-endian format.
  • It is used to compress the SMPS sound driver. In this case, the compressed data is not prefixed with a header - rather, the data's size is baked-into the decompressor's code.

Compression Theory

Basic Format

The Saxman compression scheme is a basic implementation of LZSS, but with the quirk that dictionary matches can reference bytes beyond the start of the decompressed data. Such a match is called a 'zero fill', as the bytes it produces are all 00.

The compressed data follows the following format:

AA BB BB .. AA BB BB ..

The A field is referred to as the description field. This is always 1 byte in length, starting at the beginning of the compressed data, and is followed by the B field, which is referred to as the data field. This goes on for as long as is necessary to cover all the data described in the description field. After that, the pattern repeats until the end of the compressed data is reached.

Description Field

The description field is made up of 8 bits in little endian bit (not byte) order, which means that the least significant bit is read first; in other words, each byte is read "backwards." So for example, if the description field is:

FE

then the description field, in bits, in the correct order for interpretation, will be:

[0111 1111]

Now, going from left to right, each bit is interpreted in the following way:

  • If the bit is a 1, it indicates uncompressed data.
  • If the bit is a 0, it indicates either a dictionary match or a zero fill.

When the last bit from a description field is read, the corresponding command is processed normally. After it has finished being processed, if the file has not ended yet, then a new descriptor field will be read from the file. If the end of file is reached (as counted by number of bytes already read, compared to the size of compression data) and there are still description field bits left, they are ignored.

Uncompressed Data

If a 1 is read in the description field, the corresponding byte in the data field is already uncompressed and simply copied to the destination as-is.

Dictionary Match/Zero Fill

Dictionary matches and zero fills are both indicated by a 0 in the description field. The next 2 bytes will then form the offset and length: the format of these bytes is:

[LLLL LLLL] [HHHH CCCC]

The CCCC bits, plus 3, is the number of bytes to be copied (for dictionary matches) or the number of zeros to write (for zero fills). The remaining bits are used to form the position to copy from. Unlike what happens in normal LZSS or in Kosinski, the offset is not completely relative in Saxman compression; instead, the position to copy from is computed based on the following pseudo-code:

base = ((%HHHHLLLLLLLL + $12) & $FFF)                           ; Combine everything into an address
source = ((base - destination) & $FFF) + destination - $1000    ; Rebase it around destination

where "destination" is the relative address where data will be copied to (based on the start of the decoding buffer), and "source" is where data will be copied from in a dictionary match. The "base" address is an absolute address within the $1000-byte block obtained by stripping all but the high 4 bits of "destination"; it has to be rebased around "destination" as shown above to preserve the $1000-byte sliding window of the format. Whenever the resulting "source" address computed above is greater than the "destination" address in an unsigned comparison then, instead of copying bytes from the given location, the decoder will perform a zero fill instead. This can only happen due to integer overflow if "destination" is less than $1000 bytes and "base" ends up being more than "destination".

Note: the Saxman/z80 decoder has only $800 bytes in its decompression buffer, so it does not rebase the address (nor does it need to); this means, however, that the decoder is simply unable to handle files that are longer than $1000 bytes when decompressed even if you give it a large enough decompression buffer to work with. In contrast, the Saxman/68k decoder does rebase it — but while the rebasing process is functionally equivalent to the pseudo-code given above, it is done in a different form which obscures what is happening.

Examples

Zero Fill

The following Saxman/z80-compressed stream:

03 00 00 00 FF

produces the following output:

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

The first two bytes are the compressed data length, in Big endian order: that is, the compressed data is 3 bytes. The next byte is a description field: [0000 0000]. Reading the lowest bit first, we get a zero, indicating either a dictionary match or a zero fill; the next two bytes are the source address and length, and come out to a source position of $0F12 (computed as outlined above) and a length of $12. These flag a zero fill, since the source ($0F12) is after the destination ($0000), so we write $12 zeroes. When reading a new bit, we notice that all 3 bytes in the compressed data have been read, so decompression terminates.

Origin

Saxman is derived from an LZSS implementation by Haruhiko Okumura, which dates back to the late 1980s. The two decompressors used by Sonic the Hedgehog 2 are largely transcriptions of the original C code.

Saxman bears one difference from Okumura's format: dictionary matches that point to before the start of the decompressed data produce 0x00 bytes, instead of the 0x20 bytes of the original.


In order to convert Okumura's compressor/decompressor to support Saxman, both uses of the space ASCII character in LZSS.C must be changed to zero instead. To be more specific, these two lines...


	for (i = s; i < r; i++) text_buf[i] = ' ';  /* Clear the buffer with
		any character that will appear often. */


	for (i = 0; i < N - F; i++) text_buf[i] = ' ';


...must be changed to this:


	for (i = s; i < r; i++) text_buf[i] = 0;  /* Clear the buffer with
		any character that will appear often. */


	for (i = 0; i < N - F; i++) text_buf[i] = 0;

Decompression code

An annotated version of the Saxman/z80 decompression code is provided below for reference. The code is taken from the Sonic the Hedgehog 2 sound driver.

;zsub_1271
zSaxmanDec:
	exx
	ld	bc,0
	ld	de,0
	exx
	ld	de,zMusicData
	ld	c,(hl)
	inc	hl
	ld	b,(hl)					; bc = (hl) i.e. "size of song"
	inc	hl
	ld	(zGetNextByte+1),hl		; modify inst. @ zGetNextByte -- set to beginning of decompression stream
	inc	bc
	ld	(zDecEndOrGetByte+1),bc ; modify inst. @ zDecEndOrGetByte -- set to length of song, +1

;zloc_1288
zSaxmanReadLoop:
	
	exx							; shadow reg set
	srl	c						; c >> 1 (active control byte)
	srl	b						; b >> 1 (just a mask that lets us know when we need to reload)
	bit	0,b						; test next bit of 'b'
	jr	nz,+					; if it's set, we still have bits left in 'c'; jump to '+'
	; If you get here, we're out of bits in 'c'!
	call	zDecEndOrGetByte	; get next byte -> 'a'
	ld	c,a						; a -> 'c'
	ld	b,0FFh					; b = FFh (8 new bits in 'c')
+
	bit	0,c						; test next bit of 'c'
	exx							; normal reg set
	jr	z,+						; if bit not set, it's a compression bit; jump to '+'
	; If you get here, there's a non-compressed byte
	call	zDecEndOrGetByte	; get next byte -> 'a'	
	ld	(de),a					; store it directly to the target memory address
	inc	de						; de++
	exx							; shadow reg set
	inc	de						; Also increase shadow-side 'de'... relative pointer only, does not point to output Z80_RAM
	exx							; normal reg set
	jr	zSaxmanReadLoop			; loop back around...
+
	call	zDecEndOrGetByte	; get next byte -> 'a'
	ld	c,a						; a -> 'c' (low byte of target address)
	call	zDecEndOrGetByte	; get next byte -> 'a'
	ld	b,a						; a -> 'b' (high byte of target address + count)
	and	0Fh						; keep only lower four bits...
	add	a,3						; add 3 (minimum 3 bytes are to be read in this mode)
	push	af					; save 'a'...
	ld	a,b						; b -> 'a' (low byte of target address)
	rlca
	rlca
	rlca
	rlca
	and	0Fh						; basically (b >> 4) & 0xF (upper four bits now exclusively as lower four bits)
	ld	b,a						; a -> 'b' (only upper four bits of value make up part of the address)
	ld	a,c	
	add	a,12h
	ld	c,a
	adc	a,b
	sub	c
	and	0Fh
	ld	b,a						; bc += 12h
	pop	af						; restore 'a' (byte count to read; no less than 3)
	exx							; shadow reg set
	push	de					; keep current 'de' (relative pointer) value...
	ld	l,a						; how many bytes we will read -> 'hl'
	ld	h,0
	add	hl,de					; add current relative pointer...
	ex	de,hl					; effectively, de += a
	exx							; normal reg set
	pop	hl						; shadow 'de' -> 'hl' (relative pointer, prior to all bytes read, relative)
	or	a						; Clear carry
	sbc	hl,bc					; hl -= bc
	jr	nc,+					; if result positive, jump to '+'
	ex	de,hl					; current output pointer -> 'hl'
	ld	b,a						; how many bytes to load -> 'b'

-	ld	(hl),0					; fill in zeroes that many times
	inc	hl
	djnz	-

	ex	de,hl					; output pointer updated
	jr	zSaxmanReadLoop			; loop back around...
+
	ld	hl,zMusicData			; point at beginning of decompression point
	add	hl,bc					; move ahead however many bytes
	ld	c,a
	ld	b,0
	ldir
	jr	zSaxmanReadLoop
; End of function zSaxmanDec


; ||||||||||||||| S U B	R O U T	I N E |||||||||||||||||||||||||||||||||||||||


; This is an ugly countdown to zero implemented in repeatedly modifying code!!
; But basically, it starts at the full length of the song +1 (so it can decrement)
; and waits until 'hl' decrements to zero
;zsub_12E8
zDecEndOrGetByte:
	ld	hl,0					; "self-modified code" -- starts at full length of song +1, waits until it gets to 1...
	dec	hl						; ... where this will be zero
	ld	(zDecEndOrGetByte+1),hl	; "self-modifying code" -- update the count in case it's not zero
	ld	a,h	
	or	l
	jr	z,+						; If 'h' and 'l' both equal zero, we quit!!
;zloc_12F3
zGetNextByte:
	ld	hl,0				; "self-modified code" -- get address of next compressed byte
	ld	a,(hl)				; put it into -> 'a'
	inc	hl					; next byte...
	ld	(zGetNextByte+1),hl	; change inst @ zGetNextByte so it loads next compressed byte
	ret						; still going...
+
	pop	hl					; throws away return address to this function call so that next 'ret' exits decompressor (we're done!)
	ret						; Exit decompressor
; End of function zDecEndOrGetByte