lzsa/asm/8088/LZSA2FTA.ASM
mobygamer c38b582e73 Time-efficient LZSA2 decompressor
This commit provides a time-effecient LZSA2 decompressor for
the 8088 (and higher) CPU.  Decompression speed is roughly 50% faster
than ZX7 on the same hardware.
2019-07-13 22:35:28 -05:00

303 lines
12 KiB
NASM

; lzsa2fta.asm - LZSA v2 time-efficient decompressor implementation for 8088
; Turbo Assembler IDEAL mode dialect; can also be assembled with NASM.
;
; Usual DOS assembler SMALL model assumptions apply. This code:
; - Assumes it was invoked via NEAR call (change RET to RETF for FAR calls)
; - Is interrupt-safe
; - Is not re-entrant (do not decompress while already running decompression)
; - Trashes all data and segment registers
;
; Copyright (C) 2019 Jim Leonard, Emmanuel Marty
;
; 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.
IDEAL
P8086
MODEL SMALL
CODESEG
;While LZSA2 is technically capable of generating a match offset of -2,
;this sequence never actually showed up in my LZSA2 test corpus, likely due
;to compressor optimizations and the LZSA2 format itself. If you know your
;test data will contain a match offset of -2, you can enable code to write
;out the sequence very quickly at the cost of 18 bytes of code.
HANDLE_WORD_RUN EQU 0
PUBLIC lzsa2_decompress_speed
; ---------------------------------------------------------------------------
; Decompress raw LZSA2 block
; inputs:
; * ds:si: raw LZSA2 block
; * es:di: output buffer
; output:
; * ax: decompressed size
; ---------------------------------------------------------------------------
PROC lzsa2_decompress_speed NEAR
MACRO get_nybble
LOCAL has_nybble
neg bh ;nybble ready?
jns has_nybble
xchg bx,ax
lodsb ;load two nybbles
xchg bx,ax
has_nybble:
mov cl,4 ;swap 4 high and low bits of nybble
ror bl,cl
mov cl,0FH
and cl,bl
ENDM
lzsa2_speed_start:
push di ;remember decompression offset
cld ;make string operations go forward
xor cx,cx
mov bx,0100H ;bx used by get_nybble
@@decode_token:
mov ax,cx ;clear ah - cx is zero (and must stay that way)
lodsb ;read token byte: XYZ|LL|MMMM
mov dx,ax ;keep copy of token in dl
and al,018H ;isolate literals length in token (LL)
jz @@check_offset ;no literals? stop decoding, go to matches
;At this point, al can be in three (unshifted) states: 1, 2, or 3.
;3 = not done yet.
cmp al,(2 shl 3) ;LITERALS_RUN_LEN_V2? (original: cmp al,03h)
jb @@lit1b ;LZSA2 output 1-byte more often, so test first
je @@lit2b
mov cl,3
shr al,cl ;shift literals length into place
get_nybble ;cl := get extra literals length nybble
add al,cl ;add len from token to nybble
cmp al,012H ;LITERALS_RUN_LEN_V2 + 15 ?
jne @@got_literals ;if not, we have the full literals count
lodsb ;grab extra length byte
add al,012H ;overflow?
jnc @@got_literals_big ;if not, we have a big full literals count
lodsw ;grab 16-bit extra length
;For larger counts, it pays to set up a faster copy
@@got_literals_big:
xchg cx,ax
shr cx,1
rep movsw
adc cx,0
rep movsb
jmp @@check_offset
@@got_literals:
xchg cx,ax
rep movsb ;copy cx literals from ds:si to es:di
jmp @@check_offset
;LZSA2 likes to produce tiny literals of 1 or 2 bytes. Handle them here.
@@lit2b:movsb
@@lit1b:movsb
@@check_offset:
test dl,dl ;check match offset mode in token (X bit)
js @@rep_match_or_large_offset
cmp dl,040H ;check if this is a 5 or 9-bit offset (Y bit)
jnb @@offset_9_bit
;5 bit offset:
xchg cx,ax ;clear ah - cx is zero from prior rep movs
mov al,020H ;shift Z (offset bit 4) in place
and al,dl
shl al,1
shl al,1
get_nybble ;get nybble for offset bits 0-3
or al,cl ;merge nybble
rol al,1
xor al,0E1H ;set offset bits 7-5 to 1
dec ah ;set offset bits 15-8 to 1
jmp @@get_match_length
@@rep_match_or_16_bit:
test dl,020H ;test bit Z (offset bit 8)
jne @@repeat_match ;rep-match
;16 bit offset:
lodsw ;Get 2-byte match offset
xchg ah,al
jmp @@get_match_length
@@offset_9_bit:
;9 bit offset:
xchg cx,ax ;clear ah - cx is zero from prior rep movs
lodsb ;get 8 bit offset from stream in A
dec ah ;set offset bits 15-8 to 1
test dl,020H ;test bit Z (offset bit 8)
je @@get_match_length
dec ah ;clear bit 8 if Z bit is clear
jmp @@get_match_length
@@rep_match_or_large_offset:
cmp dl,0c0H ;check if this is a 13-bit offset
;or a 16-bit offset/rep match (Y bit)
jnb @@rep_match_or_16_bit
;13 bit offset:
mov ah,020H ;shift Z (offset bit 12) in place
and ah,dl
shl ah,1
shl ah,1
get_nybble ;get nybble for offset bits 8-11
or ah,cl ;merge nybble
rol ah,1
xor ah,0E1H ;set offset bits 15-13 to 1
sub ah,2 ;substract 512
lodsb ;load match offset bits 0-7
@@get_match_length:
mov bp,ax ;bp:=offset
@@repeat_match:
mov ax,dx ;ax: original token
and al,07H ;isolate match length in token (MMM)
add al,2 ;add MIN_MATCH_SIZE_V2
cmp al,09H ;MIN_MATCH_SIZE_V2 + MATCH_RUN_LEN_V2?
jne @@got_matchlen ;no, we have full match length from token
get_nybble ;get extra literals length nybble
add al,cl ;add len from token to nybble
cmp al,018H ;MIN_MATCH_SIZE_V2 + MATCH_RUN_LEN_V2 + 15?
jne @@got_matchlen ;no, we have full match length from token
lodsb ;grab extra length byte
add al,018H ;overflow?
jnc @@got_matchlen_big ;if not, we have entire (big) length
je @@done_decompressing ; detect EOD code
lodsw ;grab 16-bit length
;If we're here, we have a larger match copy and can optimize how we do that
@@got_matchlen_big:
xchg cx,ax ;copy match length into cx
mov dx,ds ;save ds
mov ax,es
mov ds,ax ;ds:=es
xchg si,ax ;dx:ax = old ds:si
mov si,di ;ds:si now points at back reference in output data
add si,bp
IF HANDLE_WORD_RUN
cmp bp,-2 ;do we have a byte/word run to optimize?
jae @@do_run ;perform a run
ELSE
cmp bp,-1 ;do we have a byte run to optimize?
je @@do_run_1 ;perform a byte run
ENDIF
;You may be tempted to change "jae" to "jge" because DX is a signed number.
;Don't! The total window is 64k, so if you treat this as a signed comparison,
;you will get incorrect results for offsets over 32K.
;
;If we're here, we have a long copy and it isn't byte-overlapping (if it
;overlapped, we'd be in @@do_run_1) So, let's copy faster with REP MOVSW.
;This won't affect 8088 that much, but it speeds up 8086 and higher.
shr cx,1
rep movsw
adc cx,0
rep movsb
xchg si,ax
mov ds,dx ;restore ds:si
jmp @@decode_token ;go decode another token
;Smaller match copies handled here:
@@got_matchlen:
xchg cx,ax ;copy match length into cx
mov dx,ds ;save ds
mov ax,es
mov ds,ax ;ds:=es
xchg si,ax ;dx:ax = old ds:si
mov si,di ;ds:si = back reference in output data
add si,bp
rep movsb ;copy match
xchg si,ax
mov ds,dx ;restore ds:si
jmp @@decode_token ;go decode another token
@@done_decompressing:
pop ax ;retrieve the original decompression offset
xchg di,ax ;compute decompressed size
sub ax,di
ret ;done
IF HANDLE_WORD_RUN
@@do_run:
je @@do_run_2 ;fall through to byte (common) if not word run
ENDIF
@@do_run_1:
push ax
lodsb ;load first byte of run into al
mov ah,al
shr cx,1
rep stosw ;perform word run
adc cx,0
rep stosb ;finish word run
pop si
mov ds,dx
jmp @@decode_token ;go decode another token
IF HANDLE_WORD_RUN
@@do_run_2:
push ax
lodsw ;load first word of run
shr cx,1
rep stosw ;perform word run
adc cx,0 ;despite 2-byte offset, compressor might
rep stosb ;output odd length. better safe than sorry.
pop si
mov ds,dx
jmp @@decode_token ;go decode another token
ENDIF
ENDP lzsa2_decompress_speed
ENDS
END
;Speed optimization history (decompression times in microseconds @ 4.77 MHz):
;Compression corpus:shuttle alice robotro rletest largetx linewar ...... ..
;Start of exercise 160828 113311 665900 238507 1053865 1004237 ******
;add al,val -> al,cl 160813 113296 668721 237484 1053604 1003815 ++-+++
;sub ah,2 -> dec dec 160907 113585 666744 237484 1056651 1005172 --+*-- rb
;mov ax,cx->xchgcxax 159741 112460 660594 237477 1046770 998323 ++++++
;unroll get_nibble 152552 106327 621119 237345 982381 942373 ++++++
;early exit if LL=0 147242 103842 615559 239318 946863 942932 +++-+-
;push/pop->mov/mov 145447 100832 604822 237288 927017 931366 ++++++
;push/pop->mov/mov(2)143214 98817 592920 239298 908217 910955 +++-++
;rep stos for -1, -2 143289 102812 617087 237164 942081 940688 ---+-- rb
;larger literal cpys 143214 98817 591940 238296 907237 909657 **++++
;larger copys & runs 132440 98802 586551 178768 904129 896709 ++++++ :-)
;smaller lit. copies 131991 99131 583933 177760 901824 898308 +-+++-
;swap smal lit compa 131828 99022 585121 177757 901793 894054 ++-*++
;compare before shif 130587 95970 569908 177753 889221 872461 +++*++
;getmatchlength base 130587 95970 570634 177753 893536 871556 ...... ===
; f->rep_match_or_16 xxxxxx xxxxx 569910 xxxxxx 889266 871435 ..+.++
; f->rep_match_or_la 129966 94748 566169 xxxxxx 880870 867030 +++.++ +++
; f->offset_9_bit 132126 95258 568869 xxxxxx 893169 870364 -++.-+
;final fallthrough 129966 94748 566169 177753 880870 865023 ******