2 Commits

34 changed files with 341 additions and 1827 deletions

66
Cargo.lock generated
View File

@@ -62,12 +62,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lexopt"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478ee9e62aaeaf5b140bd4138753d1f109765488581444218d3ddda43234f3e8"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.108" version = "0.2.108"
@@ -96,22 +90,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "proc-macro2" name = "pico-args"
version = "1.0.44" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
"proc-macro2",
]
[[package]] [[package]]
name = "sacabase" name = "sacabase"
@@ -122,37 +104,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "syn"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a99cb8c4b9a8ef0e7907cd3b617cc8dc04d571c4e73c8ae403d80ac160bb122"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a891860d3c8d66fec8e73ddb3765f90082374dbaaa833407b904a94f1a7eb43"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.44" version = "0.1.44"
@@ -164,21 +115,14 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "unicode-ident"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
[[package]] [[package]]
name = "upkr" name = "upkr"
version = "0.2.0-pre3" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cdivsufsort", "cdivsufsort",
"lexopt",
"pbr", "pbr",
"thiserror", "pico-args",
] ]
[[package]] [[package]]

View File

@@ -1,14 +1,12 @@
[package] [package]
name = "upkr" name = "upkr"
version = "0.2.0-pre3" version = "0.1.0"
edition = "2021" edition = "2021"
[profile.release] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
strip = "debuginfo"
[dependencies] [dependencies]
cdivsufsort = "2" cdivsufsort = "2"
lexopt = "0.2.1" pico-args = "0.4"
anyhow = "1" anyhow = "1"
thiserror = "1.0.36"
pbr = "1" pbr = "1"

View File

@@ -1 +0,0 @@
/build/

View File

@@ -1,64 +0,0 @@
build/unpack_riscv64: ../c_unpacker/main.c unpack_riscv.S
mkdir -p build
riscv64-linux-gnu-gcc -g -static -o $@ $^
test_riscv64: build/unpack_riscv64
qemu-riscv64 $< test_data.upk /tmp/out.bin
cmp test_data.bin /tmp/out.bin
build/unpack_riscv64.o: unpack_riscv.S
mkdir -p build
riscv64-linux-gnu-gcc -c -o $@ $?
build/unpack_riscv64.bin: build/unpack_riscv64.o
riscv64-linux-gnu-objcopy -O binary --only-section=.text $? $@
disas-riscv64: build/unpack_riscv64.o
riscv64-linux-gnu-objdump -d $?
build/unpack_riscv32.o: unpack_riscv.S
mkdir -p build
riscv64-linux-gnu-gcc -march=rv32imc -mabi=ilp32 -c -o $@ $?
build/unpack_riscv32.bin: build/unpack_riscv32.o
riscv64-linux-gnu-objcopy -O binary --only-section=.text $? $@
disas-riscv32: build/unpack_riscv32.o
riscv64-linux-gnu-objdump -d $?
build/unpack_armv6m: ../c_unpacker/main.c unpack_armv6m.S
mkdir -p build
arm-linux-gnueabihf-gcc -g -static -o $@ $^
test_armv6m: build/unpack_armv6m
qemu-arm $< test_data.upk /tmp/out.bin
cmp test_data.bin /tmp/out.bin
build/unpack_armv6m.bin: unpack_armv6m.S
mkdir -p build
arm-none-eabi-gcc -march=armv6-m -c -o build/unpack_armv6m.o $?
arm-none-eabi-objcopy -O binary --only-section=.text build/unpack_armv6m.o $@
build/unpack_arm32: ../c_unpacker/main.c unpack_arm32.S
mkdir -p build
arm-linux-gnueabihf-gcc -g -static -o $@ $^
test_arm32: build/unpack_arm32
qemu-arm $< test_data.upk /tmp/out.bin
cmp test_data.bin /tmp/out.bin
build/unpack_arm32.bin: unpack_arm32.S
mkdir -p build
arm-none-eabi-gcc -c -o build/unpack_arm32.o $?
arm-none-eabi-objcopy -O binary --only-section=.text build/unpack_arm32.o $@
build/unpack_c: ../c_unpacker/main.c ../c_unpacker/unpack.c
mkdir -p build
gcc -g -o $@ $^
test_c: build/unpack_c
$< test_data.upk /tmp/out.bin
cmp test_data.bin /tmp/out.bin
sizes: build/unpack_armv6m.bin build/unpack_riscv64.bin build/unpack_riscv32.bin build/unpack_arm32.bin
ls -l build/*.bin

View File

@@ -1,99 +0,0 @@
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned long u32;
u8* upkr_data_ptr;
u8 upkr_probs[1 + 255 + 1 + 2*32 + 2*32];
#ifdef UPKR_BITSTREAM
u16 upkr_state;
u8 upkr_current_byte;
int upkr_bits_left;
#else
u32 upkr_state;
#endif
int upkr_decode_bit(int context_index) {
#ifdef UPKR_BITSTREAM
while(upkr_state < 32768) {
if(upkr_bits_left == 0) {
upkr_current_byte = *upkr_data_ptr++;
upkr_bits_left = 8;
}
upkr_state = (upkr_state << 1) + (upkr_current_byte & 1);
upkr_current_byte >>= 1;
--upkr_bits_left;
}
#else
while(upkr_state < 4096) {
upkr_state = (upkr_state << 8) | *upkr_data_ptr++;
}
#endif
int prob = upkr_probs[context_index];
int bit = (upkr_state & 255) < prob ? 1 : 0;
int tmp = prob;
if(!bit) {
tmp = 256 - tmp;
}
upkr_state = tmp * (upkr_state >> 8) + (upkr_state & 255);
tmp += (256 - tmp + 8) >> 4;
if(!bit) {
upkr_state -= prob;
tmp = 256 - tmp;
}
upkr_probs[context_index] = tmp;
return bit;
}
int upkr_decode_length(int context_index) {
int length = 0;
int bit_pos = 0;
while(upkr_decode_bit(context_index)) {
length |= upkr_decode_bit(context_index + 1) << bit_pos++;
context_index += 2;
}
return length | (1 << bit_pos);
}
void* upkr_unpack(void* destination, void* compressed_data) {
upkr_data_ptr = (u8*)compressed_data;
upkr_state = 0;
#ifdef UPKR_BITSTREAM
upkr_bits_left = 0;
#endif
for(int i = 0; i < sizeof(upkr_probs); ++i)
upkr_probs[i] = 128;
u8* write_ptr = (u8*)destination;
int prev_was_match = 0;
int offset = 0;
for(;;) {
if(upkr_decode_bit(0)) {
if(prev_was_match || upkr_decode_bit(256)) {
offset = upkr_decode_length(257) - 1;
if(offset == 0) {
break;
}
}
int length = upkr_decode_length(257 + 64);
while(length--) {
*write_ptr = write_ptr[-offset];
++write_ptr;
}
prev_was_match = 1;
} else {
int byte = 1;
while(byte < 256) {
int bit = upkr_decode_bit(byte);
byte = (byte << 1) + bit;
}
*write_ptr++ = byte;
prev_was_match = 0;
}
}
return write_ptr;
}

Binary file not shown.

View File

@@ -1,100 +0,0 @@
.arm
.section .text
.global upkr_unpack
.type upkr_unpack, %function
// r0 .. out_ptr (returned)
// r1 .. in_ptr (returned)
// r2 .. state
// r3 .. offset
// r4 .. prev_was_literal / decode_length ret
// r5 .. context index
// r6 .. decode_length temp
// r7 .. probs ptr
// r8-r11 .. decode_bit temp
// r12 .. decode_length return address
upkr_unpack:
push { r3-r11, lr }
mov r2, #384
mov r3, #128
.Lclear:
subs r2, r2, #1
strb r3, [sp, -r2]
bne .Lclear
.Lloop:
mov r5, #0
bl upkr_decode_bit
bcc .Ldata
.Lmatch:
mov r5, #256
rsbs r6, r4, #0
blcc upkr_decode_bit
bcc .Lskip_offset
bl upkr_decode_length
adds r3, r4, #1
popeq { r3-r11, pc }
.Lskip_offset:
mov r5, #256+64
bl upkr_decode_length
.Lcopy_loop:
ldrb r5, [r0, r3]
.Lstore:
strb r5, [r0], #1
adds r4, r4, #1
blt .Lcopy_loop
b .Lloop
.Ldata:
mov r5, #1
.Ldata_loop:
bl upkr_decode_bit
adc r5, r5, r5
movs r4, r5, lsr #8
beq .Ldata_loop
b .Lstore
.type upkr_decode_length, %function
upkr_decode_length:
mov r12, lr
mov r4, #0
mvn r6, #0
.Lbit_loop:
bl upkr_decode_bit_inc
addcc r4, r4, r6
movcc pc, r12
bl upkr_decode_bit_inc
addcs r4, r4, r6
mov r6, r6, lsl #1
b .Lbit_loop
.type upkr_decode_bit, %function
upkr_decode_bit_inc:
add r5, r5, #1
upkr_decode_bit:
cmp r2, #4096
ldrltb r8, [r1], #1
orrlt r2, r8, r2, lsl#8
blt upkr_decode_bit
ldrb r8, [sp, -r5]
and r9, r2, #255
add r9, r9, #1
cmp r8, r9
rsbcs r8, r8, #256
mvn r9, r2, lsr#8
addcs r9, r9, #1
mla r2, r8, r9, r2
add r9, r8, #8
sub r8, r8, r9, lsr#4
rsbcs r8, r8, #256
strb r8, [sp, -r5]
mov pc, r14

View File

@@ -1,162 +0,0 @@
// armv6-m upkr unpacker by yrlf
// some optimizations by exoticorn
.syntax unified
.thumb
.section .text
#define ALIGNUP(n, align) (((n) + (align) - 1) & ~((align) - 1))
#define PROB_LEN (1 + 255 + 1 + 2*32 + 2*32)
#define FRAME_SIZE ALIGNUP(PROB_LEN, 4)
// auto upkr_unpack(uint8_t * out, uint8_t * in) -> tuple<uint8_t *, uint8_t *>
.global upkr_unpack
.type upkr_unpack, %function
// r0 .. out_ptr (returned)
// r1 .. in_ptr (returned)
// r2 .. state
// r3 .. offset
// r4 .. prev_was_literal / decode_length ret
// r5 .. subroutine arg (preserved)
// r6 .. decode_bit ret
// r7 .. probs ptr
upkr_unpack:
push { r4, r5, r6, r7, lr }
sub sp, sp, #FRAME_SIZE
mov r7, sp
movs r2, #255
adds r2, r2, #(PROB_LEN - 255)
movs r3, #128
.Lclear:
subs r2, r2, #1
strb r3, [r7, r2]
bne .Lclear
.Lloop:
movs r5, #0
bl upkr_decode_bit
beq .Ldata
.Lmatch:
// r6 = 1
lsls r5, r6, #8
cmp r4, #0
beq 1f
bl upkr_decode_bit
beq 2f
1:
bl upkr_decode_length
adds r3, r4, #1
beq .Lend
2:
adds r5, r5, #64
bl upkr_decode_length
.Lcopy_loop:
ldrb r5, [r0, r3]
.Lstore:
strb r5, [r0]
adds r0, r0, #1
adds r4, r4, #1
blt .Lcopy_loop
b .Lloop
.Ldata:
movs r5, #1
.Ldata_loop:
bl upkr_decode_bit
adcs r5, r5, r5
lsrs r4, r5, #8
beq .Ldata_loop
b .Lstore
.Lend:
add sp, sp, #FRAME_SIZE
pop { r4, r5, r6, r7, pc }
.type upkr_decode_length, %function
// r0 .. -length tmp (saved)
// r1 ..
// r2 ..
// r3 ..
// r4 .. -length (returned)
// r5 .. context index (saved)
// r6 .. (saved)
// r7 ..
upkr_decode_length:
push { r0, r5, r6, lr }
movs r0, #0
subs r4, r0, #1
.Lbit_loop:
adds r5, r5, #1
bl upkr_decode_bit
beq 1f
adds r5, r5, #1
bl upkr_decode_bit
beq 2f
adds r0, r0, r4
2:
lsls r4, r4, #1
b .Lbit_loop
1:
adds r4, r4, r0
pop { r0, r5, r6, pc }
.type upkr_decode_bit, %function
// r0 .. tmp / prob (saved)
// r1 .. in_ptr (modified)
// r2 .. state (modified)
// r3 .. scratch (saved)
// r4 ..
// r5 .. context index (preserved)
// r6 .. bit (returned)
// r7 .. probs ptr (preserved)
upkr_fill_state:
lsls r2, r2, #8
ldrb r6, [r1]
adds r1, r1, #1
orrs r2, r2, r6
upkr_decode_bit:
lsrs r6, r2, #12
beq upkr_fill_state
push { r0, r1, r3, lr }
ldrb r0, [r7, r5]
lsrs r3, r2, #8
uxtb r1, r2
subs r6, r1, r0
blt 1f
subs r1, r2, r0
rsbs r0, r0, #0
1:
muls r3, r3, r0
adds r2, r1, r3
rsbs r3, r0, #0
uxtb r3, r3
lsrs r3, r3, #4
adcs r0, r0, r3
cmp r6, #0
blt 1f
rsbs r0, r0, #0
1:
strb r0, [r7, r5]
lsrs r6, r6, #31
pop { r0, r1, r3, pc }

View File

@@ -1,131 +0,0 @@
.section .text
// x9 prev was literal
// x10 out ptr
// x11 in ptr
// x12 offset
// x13 state
// x14 context index
.global upkr_unpack
.type upkr_unpack, %function
upkr_unpack:
mv t4, ra
mv x17, x8
mv t6, x9
li x9, 256 + 128
mv x13, x9
1:
sub x8, sp, x13
sb x9, 0(x8)
addi x13, x13, -1
bnez x13, 1b
.Lmainloop:
li x14, 0
jal upkr_decode_bit
beqz x15, .Lliteral
slli x14, x14, 8
beqz x9, .Lread_offset_inc_x14
jal upkr_decode_bit
bnez x15, .Lread_offset
.Lfinished_offset:
addi x14, x14, 64
jalr ra // jal upkr_decode_number
1:
add x14, x10, t0
lbu x14, (x14)
.Lstore_byte:
sb x14, (x10)
addi x10, x10, 1
addi x9, x9, 1
blt x9, x0, 1b
j .Lmainloop
.Lliteral:
jal upkr_decode_bit
addi x14, x14, -1
slli x14, x14, 1
add x14, x14, x15
srli x9, x14, 8
beqz x9, .Lliteral
j .Lstore_byte
.Lread_offset_inc_x14:
addi x14, x14, 1
.Lread_offset:
jalr ra // jal upkr_decode_number
addi t0, x9, 1
bnez t0, .Lfinished_offset
.Ldone:
mv x8, x17
mv x9, t6
jr t4
upkr_load_byte:
lbu x15, 0(x11)
addi x11, x11, 1
slli x13, x13, 8
add x13, x13, x15
// x8 prob array ptr
// x11 in ptr
// x13 state
// x14 context index
// return:
// x14 context index + 1
// x15 decoded bit
upkr_decode_bit:
srli x15, x13, 12
beqz x15, upkr_load_byte
addi x14, x14, 1
sub t2, sp, x14
lbu x12, (t2)
andi x8, x13, 255
sltu x15, x8, x12
beqz x15, 1f
xori x12, x12, 255
addi x12, x12, 1
1:
srli x8, x13, 8
addi x8, x8, 1
sub x8, x8, x15
mul x8, x8, x12
sub x13, x13, x8
addi x8, x12, 8
srli x8, x8, 4
sub x12, x12, x8
beqz x15, 1f
sub x12, x0, x12
1:
sb x12, (t2)
jalr ra
// x14 context index
// return: x9 negtive decoded number
upkr_decode_number:
mv t3, ra
mv t5, x14
li x9, 0
li t1, -1
1:
jal upkr_decode_bit
beqz x15, 1f
jal upkr_decode_bit
beqz x15, 2f
add x9, x9, t1
2:
add t1, t1, t1
j 1b
1:
add x9, x9, t1
mv x14, t5
jr t3

View File

@@ -1,33 +0,0 @@
int upkr_decode_bit(int context_index) {
#ifdef UPKR_BITSTREAM
while(upkr_state < 32768) {
if(upkr_bits_left == 0) {
upkr_current_byte = *upkr_data_ptr++;
upkr_bits_left = 8;
}
upkr_state = (upkr_state << 1) + (upkr_current_byte & 1);
upkr_current_byte >>= 1;
--upkr_bits_left;
}
#else
while(upkr_state < 4096) {
upkr_state = (upkr_state << 8) | *upkr_data_ptr++;
}
#endif
int prob = upkr_probs[context_index];
int bit = (upkr_state & 255) < prob ? 1 : 0;
if(bit) {
prob = 256 - prob;
}
upkr_state -= prob * ((upkr_state >> 8) + (bit ^ 1));
prob -= (prob + 8) >> 4;
if(bit) {
prob = -prob;
}
upkr_probs[context_index] = prob;
return bit;
}

View File

@@ -1,7 +1,7 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
void* upkr_unpack(void* destination, void* compressed_data); int upkr_unpack(void* destination, void* compressed_data);
int main(int argn, char** argv) { int main(int argn, char** argv) {
void* input_buffer = malloc(1024*1024); void* input_buffer = malloc(1024*1024);
@@ -13,8 +13,7 @@ int main(int argn, char** argv) {
printf("Compressed size: %d\n", in_size); printf("Compressed size: %d\n", in_size);
void* end_ptr = upkr_unpack(output_buffer, input_buffer); int out_size = upkr_unpack(output_buffer, input_buffer);
int out_size = (char*)end_ptr - (char*)output_buffer;
printf("Uncompressed size: %d\n", out_size); printf("Uncompressed size: %d\n", out_size);

View File

@@ -19,8 +19,8 @@ int upkr_decode_bit(int context_index) {
upkr_current_byte = *upkr_data_ptr++; upkr_current_byte = *upkr_data_ptr++;
upkr_bits_left = 8; upkr_bits_left = 8;
} }
upkr_state = (upkr_state << 1) + (upkr_current_byte & 1); upkr_state = (upkr_state << 1) + (upkr_current_byte >> 7);
upkr_current_byte >>= 1; upkr_current_byte <<= 1;
--upkr_bits_left; --upkr_bits_left;
} }
#else #else
@@ -30,16 +30,18 @@ int upkr_decode_bit(int context_index) {
#endif #endif
int prob = upkr_probs[context_index]; int prob = upkr_probs[context_index];
int bit = (upkr_state & 255) < prob ? 1 : 0; int bit = (upkr_state & 255) >= prob ? 1 : 0;
int prob_offset = 16;
int state_offset = 0;
int state_scale = prob;
if(bit) { if(bit) {
upkr_state = prob * (upkr_state >> 8) + (upkr_state & 255); state_offset = -prob;
prob += (256 - prob + 8) >> 4; state_scale = 256 - prob;
} else { prob_offset = 0;
upkr_state = (256 - prob) * (upkr_state >> 8) + (upkr_state & 255) - prob;
prob -= (prob + 8) >> 4;
} }
upkr_probs[context_index] = prob; upkr_state = state_offset + state_scale * (upkr_state >> 8) + (upkr_state & 255);
upkr_probs[context_index] = prob_offset + prob - ((prob + 8) >> 4);
return bit; return bit;
} }
@@ -54,7 +56,7 @@ int upkr_decode_length(int context_index) {
return length | (1 << bit_pos); return length | (1 << bit_pos);
} }
void* upkr_unpack(void* destination, void* compressed_data) { int upkr_unpack(void* destination, void* compressed_data) {
upkr_data_ptr = (u8*)compressed_data; upkr_data_ptr = (u8*)compressed_data;
upkr_state = 0; upkr_state = 0;
#ifdef UPKR_BITSTREAM #ifdef UPKR_BITSTREAM
@@ -92,5 +94,5 @@ void* upkr_unpack(void* destination, void* compressed_data) {
} }
} }
return write_ptr; return write_ptr - (u8*)destination;
} }

50
compare-variants Executable file
View File

@@ -0,0 +1,50 @@
#!/bin/env ruby
configs = [
[:master, '-b'],
[:z80, '-b'],
[:z80, ['-b', '-r']],
['old-prob-update', '-b']
]
files = Dir[ARGV[0] + '/*'].select {|f| !(f =~ /\.txt$/) }
short_names = files.map {|f| File.basename(f)[..16] }
results = []
def print_results(configs, names, results)
configs.each_with_index do |config, i|
printf "%d: %s\n", i + 1, config
end
print ' '
configs.each_index do |i|
printf " %-4d", i + 1
end
puts
names.each_with_index do |name, i|
printf "%16s", name
for res in results
res = res[i]
printf " %-4s", res if res
end
puts
end
end
for config in configs
raise unless system('git', 'checkout', config[0].to_s)
config_results = []
results << config_results
for file in files
if system(*['cargo', 'run', '--release', 'pack', '-l', '9', config[1], file, '/tmp/out.upk'].flatten) &&
system(*['cargo', 'run', '--release', 'unpack', config[1], '/tmp/out.upk', '/tmp/out.bin'].flatten) &&
File.read(file) == File.read('/tmp/out.bin')
size = File.size('/tmp/out.upk')
config_results << size
else
config_results << 'ERR'
end
print_results(configs, short_names, results)
end
end

3
fuzz/.gitignore vendored
View File

@@ -1,3 +0,0 @@
target
corpus
artifacts

247
fuzz/Cargo.lock generated
View File

@@ -1,247 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
[[package]]
name = "arbitrary"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f44124848854b941eafdb34f05b3bcf59472f643c7e151eba7c2b69daa469ed5"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
dependencies = [
"jobserver",
]
[[package]]
name = "cdivsufsort"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edefce019197609da416762da75bb000bbd2224b2d89a7e722c2296cbff79b8c"
dependencies = [
"cc",
"sacabase",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "crossbeam-channel"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "jobserver"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
dependencies = [
"libc",
]
[[package]]
name = "lexopt"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478ee9e62aaeaf5b140bd4138753d1f109765488581444218d3ddda43234f3e8"
[[package]]
name = "libc"
version = "0.2.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966"
[[package]]
name = "libfuzzer-sys"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae185684fe19814afd066da15a7cc41e126886c21282934225d9fc847582da58"
dependencies = [
"arbitrary",
"cc",
"once_cell",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "pbr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff5751d87f7c00ae6403eb1fcbba229b9c76c9a30de8c1cf87182177b168cea2"
dependencies = [
"crossbeam-channel",
"libc",
"time",
"winapi",
]
[[package]]
name = "proc-macro2"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
"proc-macro2",
]
[[package]]
name = "sacabase"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9883fc3d6ce3d78bb54d908602f8bc1f7b5f983afe601dabe083009d86267a84"
dependencies = [
"num-traits",
]
[[package]]
name = "syn"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a99cb8c4b9a8ef0e7907cd3b617cc8dc04d571c4e73c8ae403d80ac160bb122"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a891860d3c8d66fec8e73ddb3765f90082374dbaaa833407b904a94f1a7eb43"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi",
"winapi",
]
[[package]]
name = "unicode-ident"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
[[package]]
name = "upkr"
version = "0.2.0-pre3"
dependencies = [
"anyhow",
"cdivsufsort",
"lexopt",
"pbr",
"thiserror",
]
[[package]]
name = "upkr-fuzz"
version = "0.0.0"
dependencies = [
"libfuzzer-sys",
"upkr",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View File

@@ -1,31 +0,0 @@
[package]
name = "upkr-fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
edition = "2018"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
[dependencies.upkr]
path = ".."
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[[bin]]
name = "all_configs"
path = "fuzz_targets/all_configs.rs"
test = false
doc = false
[[bin]]
name = "unpack"
path = "fuzz_targets/unpack.rs"
test = false
doc = false

View File

@@ -1,29 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let mut config = upkr::Config::default();
let mut level = 1;
let mut data = data;
if data.len() > 2 {
let flags1 = data[0];
let flags2 = data[1];
data = &data[2..];
config.use_bitstream = (flags1 & 1) != 0;
config.parity_contexts = if (flags1 & 2) == 0 { 1 } else { 2 };
config.invert_bit_encoding = (flags1 & 4) != 0;
config.is_match_bit = (flags1 & 8) != 0;
config.new_offset_bit = (flags1 & 16) != 0;
config.continue_value_bit = (flags1 & 32) != 0;
config.bitstream_is_big_endian = (flags1 & 64) != 0;
config.simplified_prob_update = (flags1 & 128) != 0;
config.no_repeated_offsets = (flags2 & 32) != 0;
config.eof_in_length = (flags2 & 1) != 0;
config.max_offset = if (flags2 & 2) == 0 { usize::MAX } else { 32 };
config.max_length = if (flags2 & 4) == 0 { usize::MAX } else { 5 };
level = (flags2 >> 3) & 3;
}
let packed = upkr::pack(data, level, &config, None);
let unpacked = upkr::unpack(&packed, &config, 1024 * 1024).unwrap();
assert!(unpacked == data);
});

View File

@@ -1,6 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let _ = upkr::unpack(data, &upkr::Config::default(), 64 * 1024);
});

View File

@@ -1,17 +1,12 @@
use crate::{ use crate::rans::{ONE_PROB, PROB_BITS};
rans::{ONE_PROB, PROB_BITS},
Config,
};
const INIT_PROB: u16 = 1 << (PROB_BITS - 1); const INIT_PROB: u16 = 1 << (PROB_BITS - 1);
const UPDATE_RATE: u32 = 4; const UPDATE_RATE: i32 = 4;
const UPDATE_ADD: u32 = 8; const UPDATE_ADD: i32 = 8;
#[derive(Clone)] #[derive(Clone)]
pub struct ContextState { pub struct ContextState {
contexts: Vec<u8>, contexts: Vec<u8>,
invert_bit_encoding: bool,
simplified_prob_update: bool,
} }
pub struct Context<'a> { pub struct Context<'a> {
@@ -20,11 +15,9 @@ pub struct Context<'a> {
} }
impl ContextState { impl ContextState {
pub fn new(size: usize, config: &Config) -> ContextState { pub fn new(size: usize) -> ContextState {
ContextState { ContextState {
contexts: vec![INIT_PROB as u8; size], contexts: vec![INIT_PROB as u8; size],
invert_bit_encoding: config.invert_bit_encoding,
simplified_prob_update: config.simplified_prob_update,
} }
} }
@@ -40,21 +33,12 @@ impl<'a> Context<'a> {
pub fn update(&mut self, bit: bool) { pub fn update(&mut self, bit: bool) {
let old = self.state.contexts[self.index]; let old = self.state.contexts[self.index];
if bit {
self.state.contexts[self.index] = if self.state.simplified_prob_update { self.state.contexts[self.index] =
let offset = if bit ^ self.state.invert_bit_encoding { old - ((old as i32 + UPDATE_ADD) >> UPDATE_RATE) as u8;
ONE_PROB as i32 >> UPDATE_RATE
} else {
0
};
(offset + old as i32 - ((old as i32 + UPDATE_ADD as i32) >> UPDATE_RATE)) as u8
} else { } else {
if bit ^ self.state.invert_bit_encoding { self.state.contexts[self.index] =
old + ((ONE_PROB - old as u32 + UPDATE_ADD) >> UPDATE_RATE) as u8 old + (((ONE_PROB as i32 - old as i32) + UPDATE_ADD) >> UPDATE_RATE) as u8;
} else { }
old - ((old as u32 + UPDATE_ADD) >> UPDATE_RATE) as u8
}
};
} }
} }

View File

@@ -1,18 +1,16 @@
use crate::lz;
use crate::match_finder::MatchFinder; use crate::match_finder::MatchFinder;
use crate::rans::RansCoder; use crate::rans::RansCoder;
use crate::ProgressCallback; use crate::ProgressCallback;
use crate::{lz, Config};
pub fn pack( pub fn pack(
data: &[u8], data: &[u8],
config: &Config, use_bitstream: bool,
mut progress_callback: Option<ProgressCallback>, mut progress_callback: Option<ProgressCallback>,
) -> Vec<u8> { ) -> Vec<u8> {
let mut match_finder = MatchFinder::new(data); let mut match_finder = MatchFinder::new(data);
let mut rans_coder = RansCoder::new(config); let mut rans_coder = RansCoder::new(use_bitstream);
let mut state = lz::CoderState::new(config); let mut state = lz::CoderState::new();
let mut literal = vec![];
let mut pos = 0; let mut pos = 0;
while pos < data.len() { while pos < data.len() {
@@ -21,20 +19,15 @@ pub fn pack(
} }
let mut encoded_match = false; let mut encoded_match = false;
if let Some(m) = match_finder.matches(pos).next() { if let Some(m) = match_finder.matches(pos).next() {
let max_offset = config.max_offset.min(1 << (m.length * 3 - 1).min(31)); let max_offset = 1 << (m.length * 3 - 1).min(31);
let offset = pos - m.pos; let offset = pos - m.pos;
if offset < max_offset && m.length >= config.min_length() { if offset < max_offset {
if !literal.is_empty() {
lz::Op::Literal(literal).encode(&mut rans_coder, &mut state, config);
literal = vec![];
}
let length = m.length.min(config.max_length);
lz::Op::Match { lz::Op::Match {
offset: offset as u32, offset: offset as u32,
len: length as u32, len: m.length as u32,
} }
.encode(&mut rans_coder, &mut state, config); .encode(&mut rans_coder, &mut state);
pos += length; pos += m.length;
encoded_match = true; encoded_match = true;
} }
} }
@@ -46,18 +39,13 @@ pub fn pack(
.iter() .iter()
.zip(data[(pos - offset)..].iter()) .zip(data[(pos - offset)..].iter())
.take_while(|(a, b)| a == b) .take_while(|(a, b)| a == b)
.count() .count();
.min(config.max_length); if length > 0 {
if length >= config.min_length() {
if !literal.is_empty() {
lz::Op::Literal(literal).encode(&mut rans_coder, &mut state, config);
literal = vec![];
}
lz::Op::Match { lz::Op::Match {
offset: offset as u32, offset: offset as u32,
len: length as u32, len: length as u32,
} }
.encode(&mut rans_coder, &mut state, config); .encode(&mut rans_coder, &mut state);
pos += length; pos += length;
encoded_match = true; encoded_match = true;
} }
@@ -65,14 +53,11 @@ pub fn pack(
} }
if !encoded_match { if !encoded_match {
literal.push(data[pos]); lz::Op::Literal(data[pos]).encode(&mut rans_coder, &mut state);
pos += 1; pos += 1;
} }
} }
if !literal.is_empty() { lz::encode_eof(&mut rans_coder, &mut state);
lz::Op::Literal(literal).encode(&mut rans_coder, &mut state, config);
}
lz::encode_eof(&mut rans_coder, &mut state, config);
rans_coder.finish() rans_coder.finish()
} }

View File

@@ -5,73 +5,20 @@ mod match_finder;
mod parsing_packer; mod parsing_packer;
mod rans; mod rans;
pub use lz::{calculate_margin, unpack, UnpackError}; pub use lz::unpack;
pub type ProgressCallback<'a> = &'a mut dyn FnMut(usize); pub type ProgressCallback<'a> = &'a mut dyn FnMut(usize);
#[derive(Debug)]
pub struct Config {
pub use_bitstream: bool,
pub parity_contexts: usize,
pub invert_bit_encoding: bool,
pub is_match_bit: bool,
pub new_offset_bit: bool,
pub continue_value_bit: bool,
pub bitstream_is_big_endian: bool,
pub simplified_prob_update: bool,
pub no_repeated_offsets: bool,
pub eof_in_length: bool,
pub max_offset: usize,
pub max_length: usize,
}
impl Default for Config {
fn default() -> Config {
Config {
use_bitstream: false,
parity_contexts: 1,
invert_bit_encoding: false,
is_match_bit: true,
new_offset_bit: true,
continue_value_bit: true,
bitstream_is_big_endian: false,
simplified_prob_update: false,
no_repeated_offsets: false,
eof_in_length: false,
max_offset: usize::MAX,
max_length: usize::MAX,
}
}
}
impl Config {
pub fn min_length(&self) -> usize {
if self.eof_in_length {
2
} else {
1
}
}
}
pub fn pack( pub fn pack(
data: &[u8], data: &[u8],
level: u8, level: u8,
config: &Config, use_bitstream: bool,
progress_callback: Option<ProgressCallback>, progress_callback: Option<ProgressCallback>,
) -> Vec<u8> { ) -> Vec<u8> {
if level == 0 { if level == 0 {
greedy_packer::pack(data, config, progress_callback) greedy_packer::pack(data, use_bitstream, progress_callback)
} else { } else {
parsing_packer::pack(data, level, config, progress_callback) parsing_packer::pack(data, level, use_bitstream, progress_callback)
} }
} }

261
src/lz.rs
View File

@@ -1,102 +1,49 @@
use crate::context_state::ContextState; use crate::context_state::ContextState;
use crate::rans::{EntropyCoder, RansDecoder}; use crate::rans::{EntropyCoder, RansDecoder};
use crate::Config;
use thiserror::Error;
#[derive(Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub enum Op { pub enum Op {
Literal(Vec<u8>), Literal(u8),
Match { offset: u32, len: u32 }, Match { offset: u32, len: u32 },
} }
impl Op { impl Op {
pub fn encode(&self, coder: &mut dyn EntropyCoder, state: &mut CoderState, config: &Config) { pub fn encode(&self, coder: &mut dyn EntropyCoder, state: &mut CoderState) {
match self { match self {
&Op::Literal(ref lit) => { &Op::Literal(lit) => {
assert!(state.prev_was_match); encode_bit(coder, state, 0, false);
encode_length( let mut context_index = 1;
coder, for i in (0..8).rev() {
state, let bit = (lit >> i) & 1 != 0;
256 + state.pos % state.parity_contexts * 320, encode_bit(coder, state, context_index, bit);
lit.len() as u32 + 1, context_index = (context_index << 1) | bit as usize;
config,
);
for lit in lit {
let literal_base = state.pos % state.parity_contexts * 320;
let mut context_index = 1;
for i in (0..8).rev() {
let bit = (lit >> i) & 1 != 0;
encode_bit(coder, state, literal_base + context_index, bit);
context_index = (context_index << 1) | bit as usize;
}
state.pos += 1;
} }
state.prev_was_match = false; state.prev_was_match = false;
} }
&Op::Match { offset, len } => { &Op::Match { offset, len } => {
if state.prev_was_match { encode_bit(coder, state, 0, true);
encode_length( if !state.prev_was_match {
coder, encode_bit(coder, state, 256, offset != state.last_offset);
state, } else {
256 + state.pos % state.parity_contexts * 320, assert!(offset != state.last_offset);
1,
config,
);
} }
let mut new_offset = true; if offset != state.last_offset {
if !state.prev_was_match && !config.no_repeated_offsets { encode_length(coder, state, 257, offset + 1);
new_offset = offset != state.last_offset;
encode_bit(
coder,
state,
320 * state.parity_contexts,
new_offset == config.new_offset_bit,
);
}
assert!(offset as usize <= config.max_offset);
if new_offset {
encode_length(
coder,
state,
320 * state.parity_contexts + 1,
offset + if config.eof_in_length { 0 } else { 1 },
config,
);
state.last_offset = offset; state.last_offset = offset;
} }
assert!(len as usize >= config.min_length() && len as usize <= config.max_length); encode_length(coder, state, 257 + 64, len);
encode_length(coder, state, 320 * state.parity_contexts + 65, len, config);
state.prev_was_match = true; state.prev_was_match = true;
state.pos += len as usize;
} }
} }
} }
} }
pub fn encode_eof(coder: &mut dyn EntropyCoder, state: &mut CoderState, config: &Config) { pub fn encode_eof(coder: &mut dyn EntropyCoder, state: &mut CoderState) {
if state.prev_was_match { encode_bit(coder, state, 0, true);
encode_length( if !state.prev_was_match {
coder, encode_bit(coder, state, 256, true);
state,
256 + state.pos % state.parity_contexts * 320,
1,
config,
);
}
if !state.prev_was_match && !config.no_repeated_offsets {
encode_bit(
coder,
state,
320 * state.parity_contexts,
config.new_offset_bit ^ config.eof_in_length,
);
}
if !config.eof_in_length || state.prev_was_match || config.no_repeated_offsets {
encode_length(coder, state, 320 * state.parity_contexts + 1, 1, config);
}
if config.eof_in_length {
encode_length(coder, state, 320 * state.parity_contexts + 65, 1, config);
} }
encode_length(coder, state, 257, 1);
} }
fn encode_bit( fn encode_bit(
@@ -113,18 +60,17 @@ fn encode_length(
state: &mut CoderState, state: &mut CoderState,
context_start: usize, context_start: usize,
mut value: u32, mut value: u32,
config: &Config,
) { ) {
assert!(value >= 1); assert!(value >= 1);
let mut context_index = context_start; let mut context_index = context_start;
while value >= 2 { while value >= 2 {
encode_bit(coder, state, context_index, config.continue_value_bit); encode_bit(coder, state, context_index, true);
encode_bit(coder, state, context_index + 1, value & 1 != 0); encode_bit(coder, state, context_index + 1, value & 1 != 0);
context_index += 2; context_index += 2;
value >>= 1; value >>= 1;
} }
encode_bit(coder, state, context_index, !config.continue_value_bit); encode_bit(coder, state, context_index, false);
} }
#[derive(Clone)] #[derive(Clone)]
@@ -132,18 +78,14 @@ pub struct CoderState {
contexts: ContextState, contexts: ContextState,
last_offset: u32, last_offset: u32,
prev_was_match: bool, prev_was_match: bool,
pos: usize,
parity_contexts: usize,
} }
impl CoderState { impl CoderState {
pub fn new(config: &Config) -> CoderState { pub fn new() -> CoderState {
CoderState { CoderState {
contexts: ContextState::new((64 + 256) * config.parity_contexts + 1 + 64 + 64, config), contexts: ContextState::new(1 + 255 + 1 + 64 + 64),
last_offset: 0, last_offset: 0,
prev_was_match: true, prev_was_match: false,
pos: 0,
parity_contexts: config.parity_contexts,
} }
} }
@@ -152,146 +94,55 @@ impl CoderState {
} }
} }
#[derive(Error, Debug)] pub fn unpack(packed_data: &[u8], use_bitstream: bool) -> Vec<u8> {
pub enum UnpackError { let mut decoder = RansDecoder::new(packed_data, use_bitstream);
#[error("match offset out of range: {offset} > {position}")] let mut contexts = ContextState::new(1 + 255 + 1 + 64 + 64);
OffsetOutOfRange { offset: usize, position: usize },
#[error("Unpacked data over size limit: {size} > {limit}")]
OverSize { size: usize, limit: usize },
#[error("Unexpected end of input data")]
UnexpectedEOF {
#[from]
source: crate::rans::UnexpectedEOF,
},
#[error("Overflow while reading value")]
ValueOverflow,
}
pub fn unpack(
packed_data: &[u8],
config: &Config,
max_size: usize,
) -> Result<Vec<u8>, UnpackError> {
let mut result = vec![]; let mut result = vec![];
let _ = unpack_internal(Some(&mut result), packed_data, config, max_size)?; let mut offset = 0;
Ok(result)
}
pub fn calculate_margin(packed_data: &[u8], config: &Config) -> Result<isize, UnpackError> {
unpack_internal(None, packed_data, config, usize::MAX)
}
pub fn unpack_internal(
mut result: Option<&mut Vec<u8>>,
packed_data: &[u8],
config: &Config,
max_size: usize,
) -> Result<isize, UnpackError> {
let mut decoder = RansDecoder::new(packed_data, &config);
let mut contexts =
ContextState::new((64 + 256) * config.parity_contexts + 1 + 64 + 64, &config);
let mut offset = usize::MAX;
let mut position = 0usize;
let mut prev_was_match = false; let mut prev_was_match = false;
let mut margin = 0isize;
fn decode_length( fn decode_length(
decoder: &mut RansDecoder, decoder: &mut RansDecoder,
contexts: &mut ContextState, contexts: &mut ContextState,
mut context_index: usize, mut context_index: usize,
config: &Config, ) -> usize {
) -> Result<usize, UnpackError> {
let mut length = 0; let mut length = 0;
let mut bit_pos = 0; let mut bit_pos = 0;
while decoder.decode_with_context(&mut contexts.context_mut(context_index))? while decoder.decode_with_context(&mut contexts.context_mut(context_index)) {
== config.continue_value_bit length |= (decoder.decode_with_context(&mut contexts.context_mut(context_index + 1))
{
length |= (decoder.decode_with_context(&mut contexts.context_mut(context_index + 1))?
as usize) as usize)
<< bit_pos; << bit_pos;
bit_pos += 1; bit_pos += 1;
if bit_pos >= 32 {
return Err(UnpackError::ValueOverflow);
}
context_index += 2; context_index += 2;
} }
Ok(length | (1 << bit_pos)) length | (1 << bit_pos)
} }
loop { loop {
margin = margin.max(position as isize - decoder.pos() as isize); if decoder.decode_with_context(&mut contexts.context_mut(0)) {
let literal_length = decode_length( if prev_was_match || decoder.decode_with_context(&mut contexts.context_mut(256)) {
&mut decoder, offset = decode_length(&mut decoder, &mut contexts, 257) - 1;
&mut contexts, if offset == 0 {
256 + position % config.parity_contexts * 320,
config,
)? - 1;
for _ in 0..literal_length {
let literal_base = position % config.parity_contexts * 320;
let mut context_index = 1;
let mut byte = 0;
for i in (0..8).rev() {
let bit = decoder
.decode_with_context(&mut contexts.context_mut(literal_base + context_index))?;
context_index = (context_index << 1) | bit as usize;
byte |= (bit as u8) << i;
}
if let Some(ref mut result) = result {
if result.len() < max_size {
result.push(byte);
}
}
position += 1;
prev_was_match = false;
}
if config.no_repeated_offsets
|| prev_was_match
|| decoder
.decode_with_context(&mut contexts.context_mut(320 * config.parity_contexts))?
== config.new_offset_bit
{
offset = decode_length(
&mut decoder,
&mut contexts,
320 * config.parity_contexts + 1,
&config,
)? - if config.eof_in_length { 0 } else { 1 };
if offset == 0 {
break;
}
}
let length = decode_length(
&mut decoder,
&mut contexts,
320 * config.parity_contexts + 65,
&config,
)?;
if config.eof_in_length && length == 1 {
break;
}
if offset > position {
return Err(UnpackError::OffsetOutOfRange { offset, position });
}
if let Some(ref mut result) = result {
for _ in 0..length {
if result.len() < max_size {
result.push(result[result.len() - offset]);
} else {
break; break;
} }
} }
let length = decode_length(&mut decoder, &mut contexts, 257 + 64);
for _ in 0..length {
result.push(result[result.len() - offset]);
}
prev_was_match = true;
} else {
let mut context_index = 1;
let mut byte = 0;
for i in (0..8).rev() {
let bit = decoder.decode_with_context(&mut contexts.context_mut(context_index));
context_index = (context_index << 1) | bit as usize;
byte |= (bit as u8) << i;
}
result.push(byte);
prev_was_match = false;
} }
position += length;
prev_was_match = true;
} }
if position > max_size { result
return Err(UnpackError::OverSize {
size: position,
limit: max_size,
});
}
Ok(margin + decoder.pos() as isize - position as isize)
} }

View File

@@ -1,184 +1,67 @@
use anyhow::Result; use anyhow::{bail, Result};
use std::ffi::OsStr;
use std::io::prelude::*; use std::io::prelude::*;
use std::process;
use std::{fs::File, path::PathBuf}; use std::{fs::File, path::PathBuf};
fn main() -> Result<()> { fn main() -> Result<()> {
let mut config = upkr::Config::default(); let mut args = pico_args::Arguments::from_env();
let mut reverse = false;
let mut unpack = false;
let mut calculate_margin = false;
let mut level = 2;
let mut infile: Option<PathBuf> = None;
let mut outfile: Option<PathBuf> = None;
let mut max_unpacked_size = 512 * 1024 * 1024;
let mut parser = lexopt::Parser::from_env(); match args.subcommand()?.as_ref().map(|s| s.as_str()) {
while let Some(arg) = parser.next()? { None => print_help(),
use lexopt::prelude::*; Some("pack") => {
match arg { let level = args.opt_value_from_str(["-l", "--level"])?.unwrap_or(2u8);
Short('b') | Long("bitstream") => config.use_bitstream = true, let use_bitstream = args.contains(["-b", "--bitstream"]);
Short('p') | Long("parity") => config.parity_contexts = parser.value()?.parse()?,
Short('r') | Long("reverse") => reverse = true,
Long("invert-is-match-bit") => config.is_match_bit = false,
Long("invert-new-offset-bit") => config.new_offset_bit = false,
Long("invert-continue-value-bit") => config.continue_value_bit = false,
Long("invert-bit-encoding") => config.invert_bit_encoding = true,
Long("simplified-prob-update") => config.simplified_prob_update = true,
Long("big-endian-bitstream") => {
config.use_bitstream = true;
config.bitstream_is_big_endian = true;
}
Long("no-repeated-offsets") => config.no_repeated_offsets = true,
Long("eof-in-length") => config.eof_in_length = true,
Long("max-offset") => config.max_offset = parser.value()?.parse()?, let infile = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
Long("max-length") => config.max_length = parser.value()?.parse()?, let outfile = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
Long("z80") => { let mut data = vec![];
config.use_bitstream = true; File::open(infile)?.read_to_end(&mut data)?;
config.bitstream_is_big_endian = true;
config.invert_bit_encoding = true;
config.simplified_prob_update = true;
level = 9;
}
Long("x86") => {
config.use_bitstream = true;
config.continue_value_bit = false;
config.is_match_bit = false;
config.new_offset_bit = false;
}
Long("x86b") => {
config.use_bitstream = true;
config.continue_value_bit = false;
config.no_repeated_offsets = true;
level = 9;
}
Short('u') | Long("unpack") => unpack = true, let mut pb = pbr::ProgressBar::new(data.len() as u64);
Long("margin") => calculate_margin = true, pb.set_units(pbr::Units::Bytes);
Short('l') | Long("level") => level = parser.value()?.parse()?, let packed_data = upkr::pack(
Short(n) if n.is_ascii_digit() => level = n as u8 - b'0', &data,
Short('h') | Long("help") => print_help(0), level,
Long("max-unpacked-size") => max_unpacked_size = parser.value()?.parse()?, use_bitstream,
Value(val) if infile.is_none() => infile = Some(val.try_into()?), Some(&mut |pos| {
Value(val) if outfile.is_none() => outfile = Some(val.try_into()?), pb.set(pos as u64);
_ => return Err(arg.unexpected().into()), }),
);
pb.finish();
println!(
"Compressed {} bytes to {} bytes ({}%)",
data.len(),
packed_data.len(),
packed_data.len() as f32 * 100. / data.len() as f32
);
File::create(outfile)?.write_all(&packed_data)?;
} }
} Some("unpack") => {
let use_bitstream = args.contains(["-b", "--bitstream"]);
let infile = infile.unwrap_or_else(|| print_help(1)); let infile = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
let outfile = outfile.unwrap_or_else(|| { let outfile = args.free_from_os_str::<PathBuf, bool>(|s| Ok(s.into()))?;
let mut name = infile.clone();
if unpack { let mut data = vec![];
if name.extension().filter(|&e| e == "upk").is_some() { File::open(infile)?.read_to_end(&mut data)?;
name.set_extension(""); let packed_data = upkr::unpack(&data, use_bitstream);
} else { File::create(outfile)?.write_all(&packed_data)?;
name.set_extension("bin");
}
} else {
let mut filename = name
.file_name()
.unwrap_or_else(|| OsStr::new(""))
.to_os_string();
filename.push(".upk");
name.set_file_name(filename);
} }
name Some(other) => {
}); bail!("Unknown subcommand '{}'", other);
if config.parity_contexts != 1 && config.parity_contexts != 2 && config.parity_contexts != 4 {
eprintln!("--parity has to be 1, 2, or 4");
process::exit(1);
}
if !unpack && !calculate_margin {
let mut data = vec![];
File::open(infile)?.read_to_end(&mut data)?;
if reverse {
data.reverse();
}
let mut pb = pbr::ProgressBar::new(data.len() as u64);
pb.set_units(pbr::Units::Bytes);
let mut packed_data = upkr::pack(
&data,
level,
&config,
Some(&mut |pos| {
pb.set(pos as u64);
}),
);
pb.finish();
if reverse {
packed_data.reverse();
}
println!(
"Compressed {} bytes to {} bytes ({}%)",
data.len(),
packed_data.len(),
packed_data.len() as f32 * 100. / data.len() as f32
);
File::create(outfile)?.write_all(&packed_data)?;
} else {
let mut data = vec![];
File::open(infile)?.read_to_end(&mut data)?;
if reverse {
data.reverse();
}
if unpack {
let mut unpacked_data = upkr::unpack(&data, &config, max_unpacked_size)?;
if reverse {
unpacked_data.reverse();
}
File::create(outfile)?.write_all(&unpacked_data)?;
}
if calculate_margin {
println!("{}", upkr::calculate_margin(&data, &config)?);
} }
} }
Ok(()) Ok(())
} }
fn print_help(exit_code: i32) -> ! { fn print_help() {
eprintln!("Usage:"); eprintln!("Usage:");
eprintln!(" upkr [-l level(0-9)] [config options] <infile> [<outfile>]"); eprintln!(" upkr pack [-b] [-l level(0-9)] <infile> <outfile>");
eprintln!(" upkr -u [config options] <infile> [<outfile>]"); eprintln!(" upkr unpack [-b] <infile> <outfile>");
eprintln!(" upkr --margin [config options] <infile>");
eprintln!(); eprintln!();
eprintln!(" -l, --level N compression level 0-9");
eprintln!(" -0, ..., -9 short form for setting compression level");
eprintln!(" -u, --unpack unpack infile");
eprintln!(" --margin calculate margin for overlapped unpacking of a packed file");
eprintln!();
eprintln!("Config presets for specific unpackers:");
eprintln!(" --z80 --big-endian-bitstream --invert-bit-encoding --simplified-prob-update -9");
eprintln!(
" --x86 --bitstream --invert-is-match-bit --invert-continue-value-bit --invert-new-offset-bit"
);
eprintln!(
" --x86b --bitstream --invert-continue-value-bit --no-repeated-offsets -9"
);
eprintln!();
eprintln!("Config options (need to match when packing/unpacking):");
eprintln!(" -b, --bitstream bitstream mode"); eprintln!(" -b, --bitstream bitstream mode");
eprintln!(" -p, --parity N use N (2/4) parity contexts"); eprintln!(" -l, --level N compression level 0-9");
eprintln!(" -r, --reverse reverse input & output"); std::process::exit(1);
eprintln!();
eprintln!("Config options to tailor output to specific optimized unpackers:");
eprintln!(" --invert-is-match-bit");
eprintln!(" --invert-new-offset-bit");
eprintln!(" --invert-continue-value-bit");
eprintln!(" --invert-bit-encoding");
eprintln!(" --simplified-prob-update");
eprintln!(" --big-endian-bitstream (implies --bitstream)");
eprintln!(" --no-repeated-offsets");
eprintln!(" --eof-in-length");
eprintln!(" --max-offset N");
eprintln!(" --max-length N");
process::exit(exit_code);
} }

View File

@@ -6,24 +6,19 @@ use crate::match_finder::MatchFinder;
use crate::rans::{CostCounter, RansCoder}; use crate::rans::{CostCounter, RansCoder};
use crate::{lz, ProgressCallback}; use crate::{lz, ProgressCallback};
pub fn pack( pub fn pack(data: &[u8], level: u8, use_bitstream: bool, progress_cb: Option<ProgressCallback>) -> Vec<u8> {
data: &[u8], let mut parse = parse(data, Config::from_level(level), progress_cb);
level: u8,
config: &crate::Config,
progress_cb: Option<ProgressCallback>,
) -> Vec<u8> {
let mut parse = parse(data, Config::from_level(level), config, progress_cb);
let mut ops = vec![]; let mut ops = vec![];
while let Some(link) = parse { while let Some(link) = parse {
ops.push(link.op.clone()); ops.push(link.op);
parse = link.prev.clone(); parse = link.prev.clone();
} }
let mut state = lz::CoderState::new(config); let mut state = lz::CoderState::new();
let mut coder = RansCoder::new(config); let mut coder = RansCoder::new(use_bitstream);
for op in ops.into_iter().rev() { for op in ops.into_iter().rev() {
op.encode(&mut coder, &mut state, config); op.encode(&mut coder, &mut state);
} }
lz::encode_eof(&mut coder, &mut state, config); lz::encode_eof(&mut coder, &mut state);
coder.finish() coder.finish()
} }
@@ -32,15 +27,9 @@ struct Parse {
op: lz::Op, op: lz::Op,
} }
struct LiteralPrefix {
arrival: Arrival,
prefix: Vec<u8>,
}
struct Arrival { struct Arrival {
parse: Option<Rc<Parse>>, parse: Option<Rc<Parse>>,
state: lz::CoderState, state: lz::CoderState,
literal_prefix: Option<Box<LiteralPrefix>>,
cost: f64, cost: f64,
} }
@@ -49,7 +38,6 @@ type Arrivals = HashMap<usize, Vec<Arrival>>;
fn parse( fn parse(
data: &[u8], data: &[u8],
config: Config, config: Config,
encoding_config: &crate::Config,
mut progress_cb: Option<ProgressCallback>, mut progress_cb: Option<ProgressCallback>,
) -> Option<Rc<Parse>> { ) -> Option<Rc<Parse>> {
let mut match_finder = MatchFinder::new(data) let mut match_finder = MatchFinder::new(data)
@@ -111,22 +99,17 @@ fn parse(
cost_counter: &mut CostCounter, cost_counter: &mut CostCounter,
pos: usize, pos: usize,
offset: usize, offset: usize,
mut length: usize, length: usize,
arrival: &Arrival, arrival: &Arrival,
max_arrivals: usize, max_arrivals: usize,
config: &crate::Config,
) { ) {
if length < config.min_length() {
return;
}
length = length.min(config.max_length);
cost_counter.reset(); cost_counter.reset();
let mut state = arrival.state.clone(); let mut state = arrival.state.clone();
let op = lz::Op::Match { let op = lz::Op::Match {
offset: offset as u32, offset: offset as u32,
len: length as u32, len: length as u32,
}; };
op.encode(cost_counter, &mut state, config); op.encode(cost_counter, &mut state);
add_arrival( add_arrival(
arrivals, arrivals,
pos + length, pos + length,
@@ -136,7 +119,6 @@ fn parse(
op, op,
})), })),
state, state,
literal_prefix: None,
cost: arrival.cost + cost_counter.cost(), cost: arrival.cost + cost_counter.cost(),
}, },
max_arrivals, max_arrivals,
@@ -147,14 +129,13 @@ fn parse(
0, 0,
Arrival { Arrival {
parse: None, parse: None,
state: lz::CoderState::new(encoding_config), state: lz::CoderState::new(),
literal_prefix: None,
cost: 0.0, cost: 0.0,
}, },
max_arrivals, max_arrivals,
); );
let cost_counter = &mut CostCounter::new(encoding_config); let cost_counter = &mut CostCounter::new();
let mut best_per_offset = HashMap::new(); let mut best_per_offset = HashMap::new();
for pos in 0..data.len() { for pos in 0..data.len() {
let match_length = |offset: usize| { let match_length = |offset: usize| {
@@ -195,21 +176,18 @@ fn parse(
for m in match_finder.matches(pos) { for m in match_finder.matches(pos) {
closest_match = Some(closest_match.unwrap_or(0).max(m.pos)); closest_match = Some(closest_match.unwrap_or(0).max(m.pos));
let offset = pos - m.pos; let offset = pos - m.pos;
if offset <= encoding_config.max_offset { found_last_offset |= offset as u32 == arrival.state.last_offset();
found_last_offset |= offset as u32 == arrival.state.last_offset(); add_match(
add_match( &mut arrivals,
&mut arrivals, cost_counter,
cost_counter, pos,
pos, offset,
offset, m.length,
m.length, &arrival,
&arrival, max_arrivals,
max_arrivals, );
encoding_config, if m.length >= config.greedy_size {
); break 'arrival_loop;
if m.length >= config.greedy_size {
break 'arrival_loop;
}
} }
} }
@@ -220,9 +198,6 @@ fn parse(
&& closest_match.iter().all(|p| *p < match_pos) && closest_match.iter().all(|p| *p < match_pos)
{ {
let offset = pos - match_pos; let offset = pos - match_pos;
if offset > encoding_config.max_offset {
break;
}
let length = match_length(offset); let length = match_length(offset);
assert!(length > 0); assert!(length > 0);
add_match( add_match(
@@ -233,7 +208,6 @@ fn parse(
length, length,
&arrival, &arrival,
max_arrivals, max_arrivals,
encoding_config,
); );
found_last_offset |= offset as u32 == arrival.state.last_offset(); found_last_offset |= offset as u32 == arrival.state.last_offset();
if offset < near_matches.len() { if offset < near_matches.len() {
@@ -254,32 +228,24 @@ fn parse(
length, length,
&arrival, &arrival,
max_arrivals, max_arrivals,
encoding_config,
); );
} }
} }
cost_counter.reset(); cost_counter.reset();
let (arrival, mut prefix) = if let Some(prefix) = arrival.literal_prefix { let mut state = arrival.state;
(prefix.arrival, prefix.prefix) let op = lz::Op::Literal(data[pos]);
} else { op.encode(cost_counter, &mut state);
(arrival, vec![])
};
let mut state = arrival.state.clone();
prefix.push(data[pos]);
let op = lz::Op::Literal(prefix.clone());
op.encode(cost_counter, &mut state, encoding_config);
add_arrival( add_arrival(
&mut arrivals, &mut arrivals,
pos + 1, pos + 1,
Arrival { Arrival {
parse: Some(Rc::new(Parse { parse: Some(Rc::new(Parse {
prev: arrival.parse.clone(), prev: arrival.parse,
op, op,
})), })),
state, state,
cost: arrival.cost + cost_counter.cost(), cost: arrival.cost + cost_counter.cost(),
literal_prefix: Some(Box::new(LiteralPrefix { arrival, prefix })),
}, },
max_arrivals, max_arrivals,
); );

View File

@@ -1,5 +1,4 @@
use crate::{context_state::Context, Config}; use crate::context_state::Context;
use thiserror::Error;
pub const PROB_BITS: u32 = 8; pub const PROB_BITS: u32 = 8;
pub const ONE_PROB: u32 = 1 << PROB_BITS; pub const ONE_PROB: u32 = 1 << PROB_BITS;
@@ -16,25 +15,20 @@ pub trait EntropyCoder {
pub struct RansCoder { pub struct RansCoder {
bits: Vec<u16>, bits: Vec<u16>,
use_bitstream: bool, use_bitstream: bool,
bitstream_is_big_endian: bool,
invert_bit_encoding: bool,
} }
impl EntropyCoder for RansCoder { impl EntropyCoder for RansCoder {
fn encode_bit(&mut self, bit: bool, prob: u16) { fn encode_bit(&mut self, bit: bool, prob: u16) {
assert!(prob < 32768); assert!(prob < 32768);
self.bits self.bits.push(prob | ((bit as u16) << 15));
.push(prob | (((bit ^ self.invert_bit_encoding) as u16) << 15));
} }
} }
impl RansCoder { impl RansCoder {
pub fn new(config: &Config) -> RansCoder { pub fn new(use_bitstream: bool) -> RansCoder {
RansCoder { RansCoder {
bits: Vec::new(), bits: Vec::new(),
use_bitstream: config.use_bitstream, use_bitstream,
bitstream_is_big_endian: config.bitstream_is_big_endian,
invert_bit_encoding: config.invert_bit_encoding,
} }
} }
@@ -44,31 +38,18 @@ impl RansCoder {
let mut state = 1 << l_bits; let mut state = 1 << l_bits;
let mut byte = 0u8; let mut byte = 0u8;
let mut bit = if self.bitstream_is_big_endian { 0 } else { 8 }; let mut bit = 0;
let mut flush_state: Box<dyn FnMut(&mut u32)> = if self.use_bitstream { let mut flush_state: Box<dyn FnMut(&mut u32)> = if self.use_bitstream {
if self.bitstream_is_big_endian { Box::new(|state: &mut u32| {
Box::new(|state: &mut u32| { byte |= ((*state & 1) as u8) << bit;
byte |= ((*state & 1) as u8) << bit; bit += 1;
bit += 1; if bit == 8 {
if bit == 8 { buffer.push(byte);
buffer.push(byte); byte = 0;
byte = 0; bit = 0;
bit = 0; }
} *state >>= 1;
*state >>= 1; })
})
} else {
Box::new(|state: &mut u32| {
bit -= 1;
byte |= ((*state & 1) as u8) << bit;
if bit == 0 {
buffer.push(byte);
byte = 0;
bit = 8;
}
*state >>= 1;
})
}
} else { } else {
Box::new(|state: &mut u32| { Box::new(|state: &mut u32| {
buffer.push(*state as u8); buffer.push(*state as u8);
@@ -80,7 +61,7 @@ impl RansCoder {
let max_state_factor: u32 = 1 << (l_bits + num_flush_bits - PROB_BITS); let max_state_factor: u32 = 1 << (l_bits + num_flush_bits - PROB_BITS);
for step in self.bits.into_iter().rev() { for step in self.bits.into_iter().rev() {
let prob = step as u32 & 32767; let prob = step as u32 & 32767;
let (start, prob) = if step & 32768 != 0 { let (start, prob) = if step & 32768 == 0 {
(0, prob) (0, prob)
} else { } else {
(prob, ONE_PROB - prob) (prob, ONE_PROB - prob)
@@ -110,11 +91,10 @@ impl RansCoder {
pub struct CostCounter { pub struct CostCounter {
cost: f64, cost: f64,
log2_table: Vec<f64>, log2_table: Vec<f64>,
invert_bit_encoding: bool,
} }
impl CostCounter { impl CostCounter {
pub fn new(config: &Config) -> CostCounter { pub fn new() -> CostCounter {
let log2_table = (0..ONE_PROB) let log2_table = (0..ONE_PROB)
.map(|prob| { .map(|prob| {
let inv_prob = ONE_PROB as f64 / prob as f64; let inv_prob = ONE_PROB as f64 / prob as f64;
@@ -124,7 +104,6 @@ impl CostCounter {
CostCounter { CostCounter {
cost: 0.0, cost: 0.0,
log2_table, log2_table,
invert_bit_encoding: config.invert_bit_encoding,
} }
} }
@@ -139,7 +118,7 @@ impl CostCounter {
impl EntropyCoder for CostCounter { impl EntropyCoder for CostCounter {
fn encode_bit(&mut self, bit: bool, prob: u16) { fn encode_bit(&mut self, bit: bool, prob: u16) {
let prob = if bit ^ self.invert_bit_encoding { let prob = if !bit {
prob as u32 prob as u32
} else { } else {
ONE_PROB - prob as u32 ONE_PROB - prob as u32
@@ -150,85 +129,60 @@ impl EntropyCoder for CostCounter {
pub struct RansDecoder<'a> { pub struct RansDecoder<'a> {
data: &'a [u8], data: &'a [u8],
pos: usize,
state: u32, state: u32,
use_bitstream: bool, use_bitstream: bool,
byte: u8, byte: u8,
bits_left: u8, bits_left: u8,
invert_bit_encoding: bool,
bitstream_is_big_endian: bool,
} }
const PROB_MASK: u32 = ONE_PROB - 1; const PROB_MASK: u32 = ONE_PROB - 1;
#[derive(Debug, Error)]
#[error("Unexpected end of input")]
pub struct UnexpectedEOF;
impl<'a> RansDecoder<'a> { impl<'a> RansDecoder<'a> {
pub fn new(data: &'a [u8], config: &Config) -> RansDecoder<'a> { pub fn new(data: &'a [u8], use_bitstream: bool) -> RansDecoder<'a> {
RansDecoder { RansDecoder {
data, data,
pos: 0,
state: 0, state: 0,
use_bitstream: config.use_bitstream, use_bitstream,
byte: 0, byte: 0,
bits_left: 0, bits_left: 0,
invert_bit_encoding: config.invert_bit_encoding,
bitstream_is_big_endian: config.bitstream_is_big_endian,
} }
} }
pub fn pos(&self) -> usize { pub fn decode_with_context(&mut self, context: &mut Context) -> bool {
self.pos let bit = self.decode_bit(context.prob());
}
pub fn decode_with_context(&mut self, context: &mut Context) -> Result<bool, UnexpectedEOF> {
let bit = self.decode_bit(context.prob())?;
context.update(bit); context.update(bit);
Ok(bit) bit
} }
pub fn decode_bit(&mut self, prob: u16) -> Result<bool, UnexpectedEOF> { pub fn decode_bit(&mut self, prob: u16) -> bool {
let prob = prob as u32; let prob = prob as u32;
if self.use_bitstream { if self.use_bitstream {
while self.state < 32768 { while self.state < 32768 {
if self.bits_left == 0 { if self.bits_left == 0 {
if self.pos >= self.data.len() { self.byte = self.data[0];
return Err(UnexpectedEOF); self.data = &self.data[1..];
}
self.byte = self.data[self.pos];
self.pos += 1;
self.bits_left = 8; self.bits_left = 8;
} }
if self.bitstream_is_big_endian { self.state = (self.state << 1) | (self.byte >> 7) as u32;
self.state = (self.state << 1) | (self.byte >> 7) as u32; self.byte <<= 1;
self.byte <<= 1;
} else {
self.state = (self.state << 1) | (self.byte & 1) as u32;
self.byte >>= 1;
}
self.bits_left -= 1; self.bits_left -= 1;
} }
} else { } else {
while self.state < 4096 { while self.state < 4096 {
if self.pos >= self.data.len() { self.state = (self.state << 8) | self.data[0] as u32;
return Err(UnexpectedEOF); self.data = &self.data[1..];
}
self.state = (self.state << 8) | self.data[self.pos] as u32;
self.pos += 1;
} }
} }
let bit = (self.state & PROB_MASK) < prob; let bit = (self.state & PROB_MASK) >= prob;
let (start, prob) = if bit { let (start, prob) = if bit {
(0, prob)
} else {
(prob, ONE_PROB - prob) (prob, ONE_PROB - prob)
} else {
(0, prob)
}; };
self.state = prob * (self.state >> PROB_BITS) + (self.state & PROB_MASK) - start; self.state = prob * (self.state >> PROB_BITS) + (self.state & PROB_MASK) - start;
Ok(bit ^ self.invert_bit_encoding) bit
} }
} }

View File

@@ -1,3 +1,4 @@
*.bin *.bin
*.tap *.tap
*.sna
*.lst *.lst

View File

@@ -3,8 +3,7 @@
DEVICE ZXSPECTRUM48,$8FFF DEVICE ZXSPECTRUM48,$8FFF
ORG $9000 ORG $9000
;; forward example data compressed_scr_files: ; border color byte + upkr-packed .scr file
compressed_scr_files.fwd: ; border color byte + upkr-packed .scr file
DB 1 DB 1
INCBIN "screens/Grongy - ZX Spectrum (2022).scr.upk" INCBIN "screens/Grongy - ZX Spectrum (2022).scr.upk"
DB 7 DB 7
@@ -14,87 +13,37 @@ compressed_scr_files.fwd: ; border color byte + upkr-packed .scr f
DB 6 DB 6
INCBIN "screens/diver - Back to Bjork (2015).scr.upk" INCBIN "screens/diver - Back to Bjork (2015).scr.upk"
.e: .e:
;; backward example data (unpacker goes from the end of the data!)
compressed_scr_files.rwd.e: EQU $-1 ; the final IX will point one byte ahead of "$" here
INCBIN "screens.reversed/diver - Back to Bjork (2015).scr.upk"
DB 6
INCBIN "screens.reversed/diver - Mercenary 4. The Heaven's Devil (2014) (Forever 2014 Olympic Edition, 1).scr.upk"
DB 0
INCBIN "screens.reversed/Schafft - Poison (2017).scr.upk"
DB 7
INCBIN "screens.reversed/Grongy - ZX Spectrum (2022).scr.upk"
compressed_scr_files.rwd: ; border color byte + upkr-packed .scr file (backward)
DB 1
start: start:
di di
; OPT --zxnext ; OPT --zxnext
; nextreg 7,3 ; ZX Next: switch to 28Mhz ; nextreg 7,3 ; ZX Next: switch to 28Mhz
ld ix,compressed_scr_files
;;; FORWARD packed/unpacked data demo .slideshow_loop
ld ix,compressed_scr_files.fwd
.slideshow_loop.fwd:
; set BORDER for next image ; set BORDER for next image
ld a,(ix) ldi a,(ix) ; fake: ld a,(ix) : inc ix
inc ix
out (254),a out (254),a
; call unpack of next image directly into VRAM ; call unpack of next image directly into VRAM
ld de,$4000 ; target VRAM ld de,$4000 ; target VRAM
exx exx
; IX = packed data, DE' = destination ($4000) ; IX = packed data, DE' = destination ($4000)
; returned IX will point right after the packed data ; returned IX will point right after the packed data
call fwd.upkr.unpack call upkr.unpack
; do some busy loop with CPU to delay between images ; do some busy loop with CPU to delay between images
call delay
; check if all images were displayed, loop around from first one then
ld a,ixl
cp low compressed_scr_files.fwd.e
jr nz,.slideshow_loop.fwd
;;; BACKWARD packed/unpacked data demo
ld ix,compressed_scr_files.rwd
.slideshow_loop.rwd:
; set BORDER for next image
ld a,(ix)
dec ix
out (254),a
; call unpack of next image directly into VRAM
ld de,$5AFF ; target VRAM
exx
; IX = packed data, DE' = destination
; returned IX will point right ahead of the packed data
call rwd.upkr.unpack
; do some busy loop with CPU to delay between images
call delay
; check if all images were displayed, loop around from first one then
ld a,ixl
cp low compressed_scr_files.rwd.e
jr nz,.slideshow_loop.rwd
jr start
delay:
ld bc,$AA00 ld bc,$AA00
.delay: .delay:
.8 ex (sp),ix .8 ex (sp),ix
dec c dec c
jr nz,.delay jr nz,.delay
djnz .delay djnz .delay
ret ; check if all images were displayed, loop around from first one then
ld a,ixl
cp low compressed_scr_files.e
jr z,start
jr .slideshow_loop
; include the depacker library, optionally putting probs array buffer near end of RAM ; include the depacker library, optionally putting probs array buffer near end of RAM
DEFINE UPKR_PROBS_ORIGIN $FA00 ; if not defined, array will be put after unpack code DEFINE UPKR_PROBS_ORIGIN $FA00 ; if not defined, array will be put after unpack code
INCLUDE "../unpack.asm"
MODULE fwd
INCLUDE "../unpack.asm"
ENDMODULE
MODULE rwd
DEFINE BACKWARDS_UNPACK ; defined to build backwards unpack
; initial IX points at last byte of compressed data
; initial DE' points at last byte of unpacked data
INCLUDE "../unpack.asm"
ENDMODULE
SAVESNA "example.sna",start SAVESNA "example.sna",start

Binary file not shown.

View File

@@ -10,23 +10,10 @@ may be incompatible with files you will produce with current version)
Asm syntax is z00m's sjasmplus: https://github.com/z00m128/sjasmplus Asm syntax is z00m's sjasmplus: https://github.com/z00m128/sjasmplus
Backward direction unpacker added as compile-time option, see example for both forward/backward
depacker in action.
The packed/unpacked data-overlap has to be tested per-case, in worst case the packed data
may need even more than 7 bytes to unpack final byte, but usually 1-4 bytes may suffice.
TODO: TODO:
- build bigger corpus of test data to benchmark future changes in algorithm/format (example and zx48.rom was used to do initial tests) - build base corpus of test data to benchmark future changes in algorithm/format
- maybe try to beat double-loop `decode_number` with different encoding format - review first implementation to identify weak spots where the implementation can be shorter+faster
with acceptable small changes to the format
- review non-bitstream variant, if it's feasible to try to implement it with Z80
- (@ped7g) Z80N version of unpacker for ZX Next devs - (@ped7g) Z80N version of unpacker for ZX Next devs
- (@exoticorn) add Z80 specific packer (to avoid confusion with original MicroW8 variant), and land it all to master branch, maybe in "z80" directory or something? (and overall decide how to organise+merge this upstream into main repo) - (@exoticorn) add Z80 specific packer (to avoid confusion with original MicroW8 variant), and land it all to master branch, maybe in "z80" directory or something? (and overall decide how to organise+merge this upstream into main repo)
- (@exoticorn) add to packer output with possible packed/unpacked region overlap
DONE:
* review non-bitstream variant, if it's feasible to try to implement it with Z80
- Ped7g: IMHO nope, the 12b x 8b MUL code would probably quickly cancel any gains from the simpler state update
* review first implementation to identify weak spots where the implementation can be shorter+faster
with acceptable small changes to the format
- Ped7g: the decode_bit settled down and now doesn't feel so confused and redundant, the code seems pretty on point to me, no obvious simplification from format change
- Ped7g: the decode_number double-loop is surprisingly resilient, especially in terms of code size I failed to beat it, speed wise only negligible gains

View File

@@ -4,7 +4,7 @@
;; initial version by Peter "Ped" Helcmanovsky (C) 2022, licensed same as upkr project ("unlicensed") ;; initial version by Peter "Ped" Helcmanovsky (C) 2022, licensed same as upkr project ("unlicensed")
;; to assemble use z00m's sjasmplus: https://github.com/z00m128/sjasmplus ;; to assemble use z00m's sjasmplus: https://github.com/z00m128/sjasmplus
;; ;;
;; you can define UPKR_PROBS_ORIGIN to specific 256 byte aligned address for probs array (320 bytes), ;; you can define UPKR_PROBS_ORIGIN to specific 256 byte aligned address for probs array (386 bytes),
;; otherwise it will be positioned after the unpacker code (256 aligned) ;; otherwise it will be positioned after the unpacker code (256 aligned)
;; ;;
;; public API: ;; public API:
@@ -12,24 +12,12 @@
;; upkr.unpack ;; upkr.unpack
;; IN: IX = packed data, DE' (shadow DE) = destination ;; IN: IX = packed data, DE' (shadow DE) = destination
;; OUT: IX = after packed data ;; OUT: IX = after packed data
;; modifies: all registers except IY, requires 10 bytes of stack space ;; modifies: all registers except IY, requires 14 bytes of stack space
;; ;;
; DEFINE BACKWARDS_UNPACK ; uncomment to build backwards depacker (write_ptr--, upkr_data_ptr--)
; initial IX points at last byte of compressed data
; initial DE' points at last byte of unpacked data
; DEFINE UPKR_UNPACK_SPEED ; uncomment to get larger but faster unpack routine
; code size hint: if you put probs array just ahead of BASIC entry point, you will get BC
; initialised to probs.e by BASIC `USR` command and you can remove it from unpack init (-3B)
OPT push reset --syntax=abf OPT push reset --syntax=abf
MODULE upkr MODULE upkr
NUMBER_BITS EQU 16+15 ; context-bits per offset/length (16+15 for 16bit offsets/pointers)
; numbers (offsets/lengths) are encoded like: 1a1b1c1d1e0 = 0000'0000'001e'dbca
/* /*
u8* upkr_data_ptr; u8* upkr_data_ptr;
u8 upkr_probs[1 + 255 + 1 + 2*32 + 2*32]; u8 upkr_probs[1 + 255 + 1 + 2*32 + 2*32];
@@ -93,8 +81,8 @@ unpack:
; BC = probs (context_index 0), state HL = 0, A' = 0x80 (no source bits left in upkr_current_byte) ; BC = probs (context_index 0), state HL = 0, A' = 0x80 (no source bits left in upkr_current_byte)
; ** main loop to decompress data ; ** main loop to decompress data
; D = prev_was_match = uninitialised, literal is expected first => will reset D to "false" .decompress_data_reset_match:
; values for false/true of prev_was_match are: false = high(probs), true = 1 + high(probs) ld d,0 ; prev_was_match = 0;
.decompress_data: .decompress_data:
ld c,0 ld c,0
call decode_bit ; if(upkr_decode_bit(0)) call decode_bit ; if(upkr_decode_bit(0))
@@ -109,23 +97,23 @@ unpack:
ld a,c ld a,c
exx exx
ld (de),a ; *write_ptr++ = byte; ld (de),a ; *write_ptr++ = byte;
IFNDEF BACKWARDS_UNPACK : inc de : ELSE : dec de : ENDIF inc de
exx exx
ld d,b ; prev_was_match = false jr .decompress_data_reset_match
jr .decompress_data
; * copy chunk of already decompressed data (match) ; * copy chunk of already decompressed data (match)
.copy_chunk: .copy_chunk:
ld a,b
inc b ; context_index = 256 inc b ; context_index = 256
; if(prev_was_match || upkr_decode_bit(256)) { ; if(prev_was_match || upkr_decode_bit(256)) {
; offset = upkr_decode_length(257) - 1; ; offset = upkr_decode_length(257) - 1;
; if (0 == offset) break; ; if (0 == offset) break;
; } ; }
xor a
cp d ; CF = prev_was_match cp d ; CF = prev_was_match
call nc,decode_bit ; if not prev_was_match, then upkr_decode_bit(256) call nc,decode_bit ; if not prev_was_match, then upkr_decode_bit(256)
jr nc,.keep_offset ; if neither, keep old offset jr nc,.keep_offset ; if neither, keep old offset
call decode_number ; context_index is already 257-1 as needed by decode_number inc c
call decode_length
dec de ; offset = upkr_decode_length(257) - 1; dec de ; offset = upkr_decode_length(257) - 1;
ld a,d ld a,d
or e or e
@@ -138,27 +126,18 @@ unpack:
; ++write_ptr; ; ++write_ptr;
; } ; }
; prev_was_match = 1; ; prev_was_match = 1;
ld c,low(257 + NUMBER_BITS - 1) ; context_index to second "number" set for lengths decoding ld c,low(257+64) ; context_index = 257+64
call decode_number ; length = upkr_decode_length(257 + 64); call decode_length ; length = upkr_decode_length(257 + 64);
push de push de
exx exx
IFNDEF BACKWARDS_UNPACK ld h,d ; DE = write_ptr
; forward unpack (write_ptr++, upkr_data_ptr++) ld l,e
ld h,d ; DE = write_ptr .offset+*: ld bc,0
ld l,e sbc hl,bc ; CF=0 from decode_length ; HL = write_ptr - offset
.offset+*: ld bc,0 pop bc ; BC = length
sbc hl,bc ; CF=0 from decode_number ; HL = write_ptr - offset ldir
pop bc ; BC = length
ldir
ELSE
; backward unpack (write_ptr--, upkr_data_ptr--)
.offset+*: ld hl,0
add hl,de ; HL = write_ptr + offset
pop bc ; BC = length
lddr
ENDIF
exx exx
ld d,b ; prev_was_match = true ld d,b ; prev_was_match = non-zero
djnz .decompress_data ; adjust context_index back to 0..255 range, go to main loop djnz .decompress_data ; adjust context_index back to 0..255 range, go to main loop
/* /*
@@ -190,9 +169,6 @@ int upkr_decode_bit(int context_index) {
return bit; return bit;
} }
*/ */
inc_c_decode_bit:
; ++low(context_index) before decode_bit (to get -1B by two calls in decode_number)
inc c
decode_bit: decode_bit:
; HL = upkr_state ; HL = upkr_state
; IX = upkr_data_ptr ; IX = upkr_data_ptr
@@ -213,7 +189,7 @@ decode_bit:
jr nz,.has_bit ; CF=data, ZF=0 -> some bits + stop bit still available jr nz,.has_bit ; CF=data, ZF=0 -> some bits + stop bit still available
; CF=1 (by stop bit) ; CF=1 (by stop bit)
ld a,(ix) ld a,(ix)
IFNDEF BACKWARDS_UNPACK : inc ix : ELSE : dec ix : ENDIF ; upkr_current_byte = *upkr_data_ptr++; inc ix ; upkr_current_byte = *upkr_data_ptr++;
adc a,a ; CF=data, b0=1 as new stop bit adc a,a ; CF=data, b0=1 as new stop bit
.has_bit: .has_bit:
adc hl,hl ; upkr_state = (upkr_state << 1) + (upkr_current_byte >> 7); adc hl,hl ; upkr_state = (upkr_state << 1) + (upkr_current_byte >> 7);
@@ -226,8 +202,9 @@ decode_bit:
cp l ; CF = bit = prob-1 < (upkr_state & 255) <=> prob <= (upkr_state & 255) cp l ; CF = bit = prob-1 < (upkr_state & 255) <=> prob <= (upkr_state & 255)
inc a inc a
; ** adjust state ; ** adjust state
push bc push af
ld c,l ; C = (upkr_state & 255); (preserving the value) push af
push hl
push af push af
jr nc,.bit_is_0 jr nc,.bit_is_0
neg ; A = -prob == (256-prob), CF=1 preserved neg ; A = -prob == (256-prob), CF=1 preserved
@@ -235,52 +212,38 @@ decode_bit:
ld d,0 ld d,0
ld e,a ; DE = state_scale ; prob || (256-prob) ld e,a ; DE = state_scale ; prob || (256-prob)
ld l,d ; H:L = (upkr_state>>8) : 0 ld l,d ; H:L = (upkr_state>>8) : 0
ld a,8 ; counter
IFNDEF UPKR_UNPACK_SPEED
;; looped MUL for minimum unpack size
ld b,8 ; counter
.mulLoop: .mulLoop:
add hl,hl add hl,hl
jr nc,.mul0 jr nc,.mul0
add hl,de add hl,de
.mul0: .mul0:
djnz .mulLoop ; until HL = state_scale * (upkr_state>>8), also BC becomes (upkr_state & 255) dec a
jr nz,.mulLoop ; until HL = state_scale * (upkr_state>>8)
ELSE pop af
;;; unrolled MUL for better performance, +25 bytes unpack size
ld b,d
DUP 8
add hl,hl
jr nc,0_f
add hl,de
0:
EDUP
ENDIF
add hl,bc ; HL = state_scale * (upkr_state >> 8) + (upkr_state & 255)
pop af ; restore prob and CF=bit
jr nc,.bit_is_0_2 jr nc,.bit_is_0_2
dec d ; DE = -prob (also D = bit ? $FF : $00) dec d ; D = 0xFF (DE = -prob)
add hl,de ; HL += -prob add hl,de ; HL += -prob
; ^ this always preserves CF=1, because (state>>8) >= 128, state_scale: 7..250, prob: 7..250, .bit_is_0_2: ; HL = state_offset + state_scale * (upkr_state >> 8)
; so 7*128 > 250 and thus edge case `ADD hl=(7*128+0),de=(-250)` => CF=1 pop de
.bit_is_0_2: ld d,0 ; DE = (upkr_state & 255)
add hl,de ; HL = state_offset + state_scale * (upkr_state >> 8) + (upkr_state & 255) ; new upkr_state
; *** adjust probs[context_index] ; *** adjust probs[context_index]
rra ; + (bit<<4) ; part of -prob_offset, needs another -16 pop af ; restore prob and bit
and $FC ; clear/keep correct bits to get desired (prob>>4) + extras, CF=0
rra
rra
rra ; A = (bit<<4) + (prob>>4), CF=(prob & 8)
adc a,-16 ; A = (bit<<4) - 16 + ((prob + 8)>>4) ; -prob_offset = (bit<<4) - 16
ld e,a ld e,a
pop bc jr c,.bit_is_1
ld a,(bc) ; A = prob (cheaper + shorter to re-read again from memory) ld d,-16 ; 0xF0
sub e ; A = 16 - (bit<<4) + prob - ((prob + 8)>>4) ; = prob_offset + prob - ((prob + 8)>>4) .bit_is_1: ; D:E = -prob_offset:prob, A = prob
ld (bc),a ; probs[context_index] = prob_offset + prob - ((prob + 8) >> 4); and $F8
add a,d ; restore CF = bit (D = bit ? $FF : $00 && A > 0) rra
rra
rra
rra
adc a,d ; A = -prob_offset + ((prob + 8) >> 4)
neg
add a,e ; A = prob_offset + prob - ((prob + 8) >> 4)
ld (bc),a ; update probs[context_index]
pop af ; restore resulting CF = bit
pop de pop de
ret ret
@@ -295,19 +258,22 @@ int upkr_decode_length(int context_index) {
return length | (1 << bit_pos); return length | (1 << bit_pos);
} }
*/ */
decode_number: decode_length:
; HL = upkr_state ; HL = upkr_state
; IX = upkr_data_ptr ; IX = upkr_data_ptr
; BC = probs+context_index-1 ; BC = probs+context_index
; A' = upkr_current_byte (!!! init to 0x80 at start, not 0x00) ; A' = upkr_current_byte (!!! init to 0x80 at start, not 0x00)
; return length in DE, CF=0 ; return length in DE, CF=0
ld de,$FFFF ; length = 0 with positional-stop-bit ld de,$7FFF ; length = 0 with positional-stop-bit
or a ; CF=0 to skip getting data bit and use only `rr d : rr e` to fix init DE jr .loop_entry
.loop: .loop:
call c,inc_c_decode_bit ; get data bit, context_index + 1 / if CF=0 just add stop bit into DE init inc c ; context_index + 1
call decode_bit
rr d rr d
rr e ; DE = length = (length >> 1) | (bit << 15); rr e ; DE = length = (length >> 1) | (bit << 15);
call inc_c_decode_bit ; context_index += 2 inc c ; context_index += 2
.loop_entry:
call decode_bit
jr c,.loop jr c,.loop
.fix_bit_pos: .fix_bit_pos:
ccf ; NC will become this final `| (1 << bit_pos)` bit ccf ; NC will become this final `| (1 << bit_pos)` bit
@@ -321,61 +287,15 @@ decode_number:
; reserve space for probs array without emitting any machine code (using only EQU) ; reserve space for probs array without emitting any machine code (using only EQU)
IFDEF UPKR_PROBS_ORIGIN ; if specific address is defined by user, move probs array there IFDEF UPKR_PROBS_ORIGIN ; if specific address is defined by user, move probs array there
probs: EQU ((UPKR_PROBS_ORIGIN) + 255) & -$100 ; probs array aligned to 256 ORG UPKR_PROBS_ORIGIN
ELSE
probs: EQU ($ + 255) & -$100 ; probs array aligned to 256
ENDIF ENDIF
.real_c: EQU 1 + 255 + 1 + 2*NUMBER_BITS ; real size of probs array
.c: EQU (.real_c + 1) & -2 ; padding to even size (required by init code) probs: EQU ($+255) & -$100 ; probs array aligned to 256
.real_c: EQU 1 + 255 + 1 + 2*32 + 2*32 ; real size of probs array
.c: EQU (.real_c + 1) & -2 ; padding to even size (required by init code)
.e: EQU probs + .c .e: EQU probs + .c
DISPLAY "upkr.unpack probs array placed at: ",/A,probs,",\tsize: ",/A,probs.c DISPLAY "upkr.unpack probs array placed at: ",/A,probs,",\tsize: ",/A,probs.c
/*
archived: negligibly faster but +6B longer decode_number variant using HL' and BC' to
do `number|=(1<<bit_pos);` type of logic in single loop.
*/
; decode_number:
; exx
; ld bc,1
; ld l,b
; ld h,b ; HL = 0
; .loop
; exx
; inc c
; call decode_bit
; jr nc,.done
; inc c
; call decode_bit
; exx
; jr nc,.b0
; add hl,bc
; .b0:
; sla c
; rl b
; jr .loop
; .done:
; exx
; add hl,bc
; push hl
; exx
; pop de
; ret
/*
archived: possible LUT variant of updating probs value, requires 512-aligned 512B table (not tested)
*/
; code is replacing decode_bit from "; *** adjust probs[context_index]", followed by `ld (bc),a : add a,d ...`
; ld c,a
; ld a,high(probs_update_table)/2 ; must be 512 aligned
; rla
; ld b,a
; ld a,(bc)
; pop bc
; -------------------------------------------
; probs_update_table: EQU probs-512
; -------------------------------------------
; table generator is not obvious and probably not short either, 20+ bytes almost for sure, maybe even 30-40
ENDMODULE ENDMODULE
OPT pop OPT pop