-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
I initially experimented with LZ4 compression due to its speed. In Gangnam Style, for example, LZ4 reduced the file size by 49%, and improved overall throughput from 3.0Mbps to 3.6Mbps. However, a simple RLE compression scheme reduced the file size by 54%, and boosted throughput from 3.0Mbps to 5.1Mbps. The LZ4 library also needs memory, as much as 8kB (significant for a microcontroller), and wants to write all output to a buffer in memory, but the RLE decompression algorithm can operate on just a few bytes of extra memory and can output directly to SRAM over serial.
- Loading branch information
1 parent
b3ff129
commit 024631e
Showing
8 changed files
with
293 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
// Kinetoscope: A Sega Genesis Video Player | ||
// | ||
// Copyright (c) 2024 Joey Parrish | ||
// | ||
// See MIT License in LICENSE.txt | ||
|
||
// Shared RLE code. | ||
|
||
static int _rle_control_byte = -1; | ||
static int _rle_more_literals = 0; | ||
|
||
static int _rle_output_literals(const uint8_t* data, int bytes); | ||
static void _rle_process(uint8_t control_byte, const uint8_t* data, int bytes); | ||
|
||
#if !defined(MIN) | ||
# define MIN(a, b) ((a) < (b) ? (a) : (b)) | ||
#endif | ||
|
||
/** | ||
* Requires these macros: | ||
* | ||
* #define SRAM_WRITE(buffer, size) | ||
*/ | ||
static void rle_to_sram(const uint8_t* buffer, int bytes) { | ||
if (_rle_control_byte != -1) { | ||
// Now we have the data to process this cached control byte from another | ||
// callback. | ||
int control_byte = _rle_control_byte; | ||
_rle_control_byte = -1; // clear cache | ||
_rle_process(control_byte, buffer, bytes); | ||
} else if (_rle_more_literals) { | ||
int consumed = _rle_output_literals(buffer, bytes); | ||
buffer += consumed; | ||
bytes -= consumed; | ||
} | ||
|
||
if (bytes) { | ||
_rle_process(buffer[0], buffer + 1, bytes - 1); | ||
} | ||
} | ||
|
||
static void rle_reset() { | ||
_rle_control_byte = -1; | ||
_rle_more_literals = 0; | ||
} | ||
|
||
static int _rle_output_literals(const uint8_t* data, int bytes) { | ||
// We are still copying literal bytes to the output stream. | ||
int available = MIN(_rle_more_literals, bytes); | ||
SRAM_WRITE(data, available); | ||
_rle_more_literals -= available; | ||
return available; | ||
} | ||
|
||
static void _rle_process(uint8_t control_byte, const uint8_t* data, int bytes) { | ||
while (true) { | ||
bool repeat = control_byte & 0x80; | ||
int size = control_byte & 0x7f; | ||
|
||
if (repeat) { | ||
if (!bytes) { | ||
// We don't have the byte to repeat. | ||
// Save the control byte for next time and return. | ||
_rle_control_byte = control_byte; | ||
return; | ||
} | ||
|
||
// Output the next byte |size| times. | ||
for (int i = 0; i < size; ++i) { | ||
SRAM_WRITE(data, 1); | ||
} | ||
|
||
// Consume that byte. | ||
data++; | ||
bytes--; | ||
} else { | ||
_rle_more_literals = size; | ||
int consumed = _rle_output_literals(data, bytes); | ||
data += consumed; | ||
bytes -= consumed; | ||
} | ||
|
||
if (!bytes) { | ||
// Nothing left in our input. | ||
return; | ||
} | ||
|
||
// Set up the next control byte. | ||
control_byte = data[0]; | ||
data++; | ||
bytes--; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
__pycache__ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
# Kinetoscope: A Sega Genesis Video Player | ||
# | ||
# Copyright (c) 2024 Joey Parrish | ||
# | ||
# See MIT License in LICENSE.txt | ||
|
||
# The Kinetoscope RLE format is a byte-based sequence of commands. | ||
# | ||
# The first byte of a command is a control byte. | ||
# | ||
# The top bit (mask 0x80) is the type, and the bottom 7 bits (mask 0x7f) | ||
# are a size field. The meaning of size depends on the type. | ||
# | ||
# type (mask 0x80): | ||
# 0x00: literal bytes follow, |size| of them, copy directly to output | ||
# 0x80: a single byte follows, repeat |size| times in the output | ||
|
||
|
||
import os | ||
|
||
|
||
# How many repeated bytes we need to make compression worth it. Anything more | ||
# than 2 is technically "worth it" for those bytes themselves, but more | ||
# frequent small repeats means more frequent non-repeating literal sequences, | ||
# too, which increases the overhead for those. So this is a balancing act. | ||
MIN_REPEAT_FOR_COMPRESSION = 8 | ||
|
||
# This is the largest number that fits in the size field. We can't repeat more | ||
# times than this per command, nor output more literal bytes in a row than | ||
# this. | ||
MAX_SIZE_FIELD = 127 | ||
|
||
# Constants for the type bit. | ||
TYPE_LITERAL = 0x00 | ||
TYPE_REPEAT = 0x80 | ||
|
||
|
||
def _count_repeats(block, offset): | ||
original_offset = offset | ||
while offset < len(block) and block[offset] == block[original_offset]: | ||
offset += 1 | ||
# Offset is now the number of repeats. | ||
return offset - original_offset | ||
|
||
|
||
def rle_compress(block): | ||
# The compressed output. | ||
output = b'' | ||
|
||
# A buffer of literals to be flushed later. | ||
literals = b'' | ||
|
||
def flush_buffered_literals(): | ||
# Take these from the outer scope | ||
nonlocal literals, output | ||
|
||
offset = 0 | ||
while offset < len(literals): | ||
# Don't output more at once than fits in this size field | ||
literal_block_size = min(len(literals) - offset, MAX_SIZE_FIELD) | ||
literal_block = literals[offset:offset+literal_block_size] | ||
offset += literal_block_size | ||
|
||
control_byte = TYPE_LITERAL | literal_block_size | ||
output += control_byte.to_bytes(1, 'big') | ||
output += literal_block | ||
|
||
literals = b'' | ||
|
||
def compress_repeats(data, count): | ||
# Take these from the outer scope | ||
nonlocal output | ||
|
||
while count: | ||
# Don't output more at once than fits in this size field | ||
repeat_count = min(count, MAX_SIZE_FIELD) | ||
count -= repeat_count | ||
|
||
control_byte = TYPE_REPEAT | repeat_count | ||
output += control_byte.to_bytes(1, 'big') | ||
output += data | ||
|
||
i = 0 | ||
while i < len(block): | ||
count = _count_repeats(block, i) | ||
this_byte = block[i:i+1] # Still bytes type, not int as block[i] would be | ||
i += count | ||
|
||
if count < MIN_REPEAT_FOR_COMPRESSION: | ||
# Buffer literals for later | ||
literals += this_byte | ||
else: | ||
# Flush buffered literals first | ||
flush_buffered_literals() | ||
# Compress repeated sequence | ||
compress_repeats(this_byte, count) | ||
|
||
# Flush any remaining buffered literals | ||
flush_buffered_literals() | ||
return output |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../common/rle-common.h |
Oops, something went wrong.