LZSS Compression
LZSS Compression
The KN5000 firmware uses two closely related LZSS (Lempel-Ziv-Storer-Szymanski) container formats, told apart by a single character in their magic string:
- SLIDE4K — 4 KB sliding window. Used for the 19 demo-song preset blocks in the Table Data ROM and for the compressed Sub-CPU firmware-update images.
- SLIDE8K — 8 KB sliding window. Used for the six multilingual HELP
databases in the Table Data ROM. This variant was undocumented before
2026-08-07; its decoder was recovered from the firmware and its round-trip
tooling landed in commit
f40a93dof the disassembly repository.
Both are handled by the same firmware entry point, SLIDE_Parse_Header, which
dispatches on the 4/8 character of the magic. This page documents both
formats, every compressed region found in the dumped ROMs, the decompression
routines, and the build-time round-trip that keeps rebuilt ROMs byte-identical.
The Two Variants at a Glance
| Property | SLIDE4K | SLIDE8K |
|---|---|---|
| Magic (8 bytes) | "SLIDE4K", 0x00 |
"SLIDE8K", 0x00 |
| Header size | 11 bytes | 11 bytes |
| Size field | 24-bit big-endian at header+8 | 24-bit big-endian at header+8 |
| Ring buffer size | 0x1000 (4,096) | 0x2000 (8,192) |
| Ring mask | 0xFFF | 0x1FFF |
| Zero pre-fill | ring[0x0000-0x0FED] | ring[0x0000-0x1FF5] |
| Initial write position | 0x0FEE | 0x1FF6 |
| Match offset | ((high & 0xF0) << 4) \| low — 12 bits |
((high & 0xF8) << 5) \| low — 13 bits |
| Match count | (high & 0x0F) + 3 → 3-18 bytes |
(high & 0x07) + 3 → 3-10 bytes |
| Flag byte | 8 elements, LSB first, 1 = literal | identical |
| Termination | declared output size only | declared output size only |
| Firmware decoder | SLIDE_Decompress_4K_Init (0xEF3FAB) |
SLIDE_Decompress_8K_Init (0xEF40C5) |
| Blocks known | 19 in table_data (+ 3 archived update images) | 6 in table_data |
Addresses are Main CPU program ROM (v10). Both routines and the dispatcher live
in v10/maincpu/boot/system_handlers.s (lines 6980, 7103 and 7226).
The dispatcher (SLIDE_Parse_Header, 0xEF41E3,
v10/maincpu/boot/system_handlers.s:7226) compares the first five bytes of
the block against SLIDE_STRING and then reads the sixth character:
ld a, (xwa)
cp a, 0x34 ; '4'
jr nz, SLIDE_Parse_Check8K
...
calr SLIDE_Decompress_4K_Init
SLIDE_Parse_Check8K:
cp a, 0x38 ; '8'
jr nz, SLIDE_Parse_ReturnOK
...
calr SLIDE_Decompress_8K_Init
(system_handlers.s:7245-7263.) An unrecognised sixth character is silently
ignored — the routine returns success without decompressing anything. Only a
missing SLIDE prefix returns the 0xFFFF failure code.
Compressed Data Regions
Compressed Preset/Parameter Data (SLIDE4K Format)
| Property | Value |
|---|---|
| ROM Component | table_data |
| Label | Compressed_Preset_Data_LZSS (a.k.a. DemoSongPreset18) |
| Start Address | 0x8E0000 (CPU address) / 0xE0000 (offset in kn5000_table_data.rom) |
| End Address | 0x8E6D3E (last payload byte; 0xFF padding starts at 0x8E6D3F) |
| Header Size | 11 bytes |
| Compressed Payload Size | 27,956 bytes (27,881-byte LZSS stream + 75 verbatim tail bytes) |
| Total Size | 27,967 bytes |
| Decompressed Size | 38,144 bytes (0x9500, exact — declared by the header size field) |
| Binary File | table_data/includes/demo_presets/demo_preset_18_compressed.bin |
Header Structure (11 bytes):
Offset Size Content
------ ---- -------
0x00 8 "SLIDE4K", 0x00 (NUL-terminated format signature)
0x08 3 Decompressed size, 24-bit BIG-ENDIAN
(here: 00 95 00 = 0x009500 = 38,144 bytes)
The LZSS stream begins immediately at offset 0x0B. The bytes 7D 5A EE at
offsets 0x0B-0x0D — previously documented as three extra header metadata bytes
of a supposed 14-byte header — are actually the first flag byte and the first
two payload bytes of the compressed stream.
Endianness evidence: the v142 Sub-CPU update image
(kn5000_subprogram_v142_compressed.rom) carries size field 03 00 00;
big-endian that is 0x030000 = 196,608, exactly the decompressed size of the
Sub-CPU program. Read little-endian it would be 3. (The preset block’s
00 95 00 is endian-symmetric and cannot discriminate.) The firmware agrees:
both SLIDE_Decompress_*_Init routines build the count as
(hdr[8] << 16) + ((hdr[9] << 8) | hdr[10])
(v10/maincpu/boot/system_handlers.s:7121-7133).
The decoder stops as soon as the declared output size (38,144 bytes) is
reached, which happens at ROM address 0x8E6CF4 after consuming 27,881 stream
bytes. The remaining 75 bytes up to 0x8E6D3E are real (non-0xFF) ROM content
that the decoder never reads; they are preserved verbatim so the rebuilt ROM
stays byte-identical.
Important Clarification:
This compressed region does NOT contain the Sub CPU ROM. Decompression yields 38,144 bytes (~37KB) of parameter-like data, not the 192KB executable code found in
kn5000_subprogram_v142.rom.
Decompressed Data Characteristics:
- Size: 38,144 bytes (0x9500) (vs 196,608 bytes for Sub CPU ROM)
- Content: Parameter/preset data structure, not executable code
- Most bytes are in MIDI range (0-127)
- Contains repeating structural patterns (e.g.,
0x80 XXflags,00 03record markers) - Does NOT match the Sub CPU ROM byte patterns
The Other 18 SLIDE4K Blocks (Demo Song Presets)
The 0x8E0000 block is entry 18 of a set of 19. Entries 0-17 run from
0x9C4050 to 0x9F94CA, abutting one another except for a single 0xFF pad
byte at 0x9C9017; 0xFF fill follows from 0x9F94CB. Each has its own 11-byte
SLIDE4K header; the last of them starts at 0x9F494E. A scan of
original_ROMs/kn5000_table_data.rom for the magic finds exactly these 19
blocks and no others; their declared decompressed sizes range from 0x1700 to
0x9500 bytes. They are built from checked-in MIDI + YAML sources (see
Source Files below).
SLIDE8K Help Databases (0x983B3A - 0x9999CB)
Six SLIDE8K blocks exist in the entire dumped ROM set — all of them in the Table
Data ROM, inside the region historically covered by
table_data/includes/icons_to_strings.bin. Five are live multilingual HELP
databases; the sixth is an orphan (below).
| # | ROM address | Language | Stream length | Compressed slice | Decompressed |
|---|---|---|---|---|---|
| 0 | 0x983B3A |
German (stale, orphaned) | 0x3EEE (16,110) | 16,111 B | 0x9000 |
| 1 | 0x988690 |
English (also used for slot 4) | 0x349E (13,470) | 13,471 B | 0x9000 |
| 2 | 0x98BB3A |
German | 0x3594 (13,716) | 13,717 B | 0x9000 |
| 3 | 0x98F0DA |
French | 0x3926 (14,630) | 14,631 B | 0x9000 |
| 4 | 0x992A0C |
Spanish | 0x39E2 (14,818) | 14,819 B | 0x9000 |
| 5 | 0x9963FA |
Indonesian | 0x35C6 (13,766) | 13,767 B | 0x9000 |
“Stream length” is the compressed bitstream proper (header excluded); the
“compressed slice” checked in as
original_ROMs/help_db_<lang>_compressed.original.bin is that stream plus the
one trailing alignment pad byte (see
Byte-Exact Recompression). Every block declares —
and produces — exactly 0x9000 (36,864) bytes.
What the payload is. Each decompressed database starts with a table of
32-bit little-endian pointers into itself, based at RAM 0x69800 (the English
database’s first entries are 0x69B22, 0x69E5C, 0x6A1B8, …), followed by the
help-string pool. Strings are Latin-1 with ~0d as the newline escape. The
payload’s internal record format has not been decoded further: the
decompressed databases are checked in as binaries
(table_data/includes/help_databases/help_db_<lang>.bin), not yet as text
sources.
How the firmware gets there. The help-language load path in the Main CPU ROM reads the language number from RAM 0x0340E4, indexes the pointer table at 0x988018, and calls the dispatcher with the destination in XBC:
f4779f: ld A,(0x0340e4) ; help language number
f477a4: sll 0x02,A ; × 4
f477ab: add XWA,0x00988018 ; HelpDB_LanguageTable
f477b1: ld XWA,(XWA) ; -> SLIDE8K block address
f477b3: ld XBC,0x00069800 ; destination RAM
f477d3: call 0xef41e3 ; SLIDE_Parse_Header
There are only five distinct languages: slot 4 of the six-entry table reuses the
English block at 0x988690. See
Table Data ROM for the language index and
intro strings, and table_data/help_databases.s for the annotated source.
The orphaned block at 0x983B3A
Block 0 is a superseded revision of the German database. No 32-bit little-endian pointer to 0x983B3A exists in any dumped ROM (v7, v9, v10 program ROMs, table data, custom data, Sub-CPU) — nothing references it.
Its stream is not merely “corrupt”: decoding it reproduces the live German
database byte-for-byte for exactly 0x55E0 output bytes, and then diverges.
The reason is visible in the ROM layout: the factory image wrote the two Music
Stylist pointer tables (StyleRec_PtrTable_C2C5 at 0x986000 and
StyleRec_PtrTable_Default at 0x987000) straight over this obsolete block’s
tail. The last element decoded entirely from surviving bytes ends at ROM
0x985FFE; the very next element reads its two bytes at 0x985FFF/0x986000 —
i.e. straddles into the overwritten region — and everything from output byte
0x55E0 onward is therefore garbage. The decoder still walks the (now foreign)
bytes until the output count reaches 0x9000, consuming its last stream byte at
ROM 0x987A32.
The block is preserved byte-exactly and deliberately not “fixed”: the
surviving stream is emitted as a raw slice (table_data/help_databases.s),
because its true tail no longer exists anywhere. Its whole walked extent is
still round-trip verified by make verify-help-databases so the bytes stay
pinned.
✅ RESOLVED: Address 0x3E0000 is Custom Data Flash (Firmware Update Staging)
Resolution: The
0x3E0000address mystery has been solved by analyzing the firmware update routines.
Firmware Update System and 0x3E0000
Discovery: 0x3E0000 is a Firmware Update Destination
The address 0x3E0000 is Custom Data Flash (not an alternate ROM mapping). During firmware updates, compressed Sub CPU payload data is written here:
File Type 007 Handler (HANDLE_UPDATE_FILE_TYPE_ID_007h at 0xEF47FA):
; "Technics KN5000 Program DATA FILE PCK"
HANDLE_UPDATE_FILE_TYPE_ID_007h:
LD WA, 1 ; Select Custom Data Flash
LD XBC, 003e0000h ; Write to 0x3E0000
CALL Flash_EraseSectorWithBankSelect ; Flash write routine
LD WA, 1
LD XBC, 003f0000h ; Write to 0x3F0000
CALL Flash_EraseSectorWithBankSelect ; Flash write routine
The flash write routine (Flash_EraseSectorWithBankSelect) uses CUSTOM_DATA_FLASH__BASE_ADDR (0x300000) as the base when WA=1.
Update File Types
| ID | File Type | Destination | Format |
|---|---|---|---|
| 007 | Program DATA FILE PCK | Custom Data Flash 0x3E0000 + 0x3F0000 | LZSS compressed |
| 008 | Table DATA FILE PCK | Table Data ROM | LZSS compressed |
| 001 | Program DATA FILE 1/2 | Table Data ROM 0x800000 | Uncompressed |
| 003 | Table DATA FILE 1/2 | Table Data ROM 0x800000 | Uncompressed |
The three archived update images
(original_ROMs/kn5000_subprogram_v14{0,1,2}_compressed.rom, 93,124 / 93,181 /
93,203 bytes) are whole-file SLIDE4K containers: the 11-byte header followed by
one stream, decompressing to the 196,608-byte Sub-CPU payload. Only v1.42
has a full source tree behind it; see Round-Trip Coverage.
Boot Sequence Behavior
After a firmware update:
- Compressed payload exists at Custom Data Flash 0x3E0000
SubCPU_Send_Payloaddecompresses from 0x3E0000 → success- Updated Sub CPU firmware is loaded
Factory state (no update):
- Custom Data Flash at 0x3E0000 contains user data or is empty
SubCPU_Send_Payloadtries to decompress → fails (returns 0xFFFF)- Falls back to
TABLE_DATA_ROM__BASE_ADDR(0x800000)
Memory Map Clarification
| Address | Memory Region | Purpose |
|---|---|---|
| 0x3E0000 | Custom Data Flash (offset 0xE0000) | Firmware update staging area |
| 0x8E0000 | Table Data ROM (offset 0xE0000) | Factory LZSS preset data |
| 0x830000-0x87FFFF | Table Data ROM (offset 0x30000-0x7FFFF) | Tone database – data shipped to Sub CPU RAM 0x050000 |
The addresses 0x3E0000 and 0x8E0000 are NOT the same physical data - they are different chips:
0x3E0000= Custom Data Flash IC19 (user-writable)0x8E0000= Table Data ROM IC7/IC8 (factory programmed)
Remaining Questions (Partially Disputed)
While the 0x3E0000 address is now understood, some aspects of the preset data transfer remain unclear:
Decompressed Data Structure:
| Section | Offset | Size | Runtime Destination |
|---|---|---|---|
| Main CPU Header | 0x0000-0x00AF | 176 bytes | Main CPU only (word at 0x100 → RAM 0x0404) |
| Sub CPU Audio Params | 0x00B0-0x808D | 32,734 bytes | Sub CPU address 0xF000+ (uncertain) |
Note: this breakdown was derived from a truncated 32,910-byte decompression; the true output is 38,144 bytes (0x9500), so the section boundaries above need re-derivation.
Outstanding questions:
- The bulk transfers send 64KB blocks (much larger than the ~37KB of meaningful data)
- The fallback to
TABLE_DATA_ROM__BASE_ADDR(0x800000) produces mostly 0xF7 padding bytes - The exact purpose of the preset parameters at Sub CPU 0xF000+ is not fully understood
Main CPU Header (0x00-0xAF):
- Mostly zero bytes with sparse configuration values
- Key non-zero positions: 0x18-0x1B, 0x2E, 0x45, 0x57-0x5A, 0x61, 0x94-0x9E, 0xA4-0xA5
- Contains record marker
00 03 01 00at offset 0x94 - Word at offset 0x100 is copied to Main CPU RAM 0x0404 before bulk transfer
Sub CPU Audio Parameters (0x100+) - per Claude’s interpretation:
- Destination: Sub CPU address 0xF000 (overwrites ROM defaults) - DISPUTED
- Variable-length records, often starting with
00 03marker - 24 occurrences of
00 03record markers - Flag byte
0x80indicates “value set” (actual value in next byte) - Pattern
18 XXappears to indicate parameter type codes - Common sequence:
64 03 00 7F 20 00 70 80 - Most values are MIDI-range (0-127), suggesting voice/audio parameters
Sub CPU 0xF000 Area Usage (IF Claude’s interpretation is correct):
The Sub CPU ROM (kn5000_subprogram_v142) contains default values at 0xF000+. These are audio engine configuration tables:
| Address | Purpose |
|---|---|
| 0xF000-0xF01F | System configuration, counters |
| 0xF010-0xF100 | Voice parameters (ADSR envelopes) |
| 0xF100-0xF420 | Pitch tables, envelope lookups |
| 0xF420-0xF434 | Runtime buffer initialization |
| 0xF434-0xF460 | Serial buffer structures |
| 0xF48C+ | Voice polyphony/index tables |
If the interpretation is correct, the preset data would overwrite these defaults during boot, configuring factory presets for the audio engine.
Open Questions - Preset Data Destination (Added due to AI/Human disagreement):
The following questions remain open regarding where the LZSS preset data actually ends up:
- What does 0x3E0000 actually map to? The memory bank configuration during boot needs investigation.
- Are there other ~33KB transfers? Search for data transfers matching the preset data size.
- What happens to the “extra” bytes? The 64KB transfer vs ~37KB data discrepancy is suspicious.
- Why does fallback produce 0xF7 bytes? This suggests the fallback path may never be intended to work.
Open Questions - Sub CPU Payload Transfer:
The Sub CPU executable payload (~192KB) is transferred to the Sub CPU via the inter-CPU communication latches during boot. Where the Main CPU reads it from is unresolved.
-
0x830000-0x87FFFFis not it. That region is the tone database – sound-parameter data, copied into the Sub CPU’s data window at RAM 0x050000, not into its code area at 0x400+. See Tone Database and the retraction on SubCPU Payload Loading. -
SubCPU_Send_Payload’s two source bases are the LZSS image at Custom Data Flash0x3E0000if it decompresses, and table-data0x800000otherwise. In the images this project holds, neither contains the payload: custom-data0x3E0000is 131,072 bytes of 0xFF with noSLIDEmagic anywhere in the 1 MB dump, and no byte ofkn5000_subprogram_v142.romappears anywhere in the table-data or custom-data dumps. Whether that means no File Type 007 update was ever applied to the dumped unit is not established — an interrupted install and a truncated, 0xFF-padded dump are byte-indistinguishable from a never-programmed chip. See Sub-CPU Payload Provenance, where the mechanism is now closed end-to-end and only the dump provenance remains open. -
The executable is known only from its own ROM dump (
kn5000_subprogram_v142.rom) and from the compressed update-disc images. The runtime code-payload source path is flagged as open, not explained.
See Boot Sequence and Inter-CPU Protocol for related documentation.
SLIDE4K Format Specification
SLIDE4K is a variant of LZSS (Lempel-Ziv-Storer-Szymanski) compression with the following parameters:
| Parameter | Value |
|---|---|
| Sliding Window Size | 4,096 bytes (4KB) |
| Window Offset Bits | 12 bits (0x000 - 0xFFF) |
| Match Length Bits | 4 bits (encoded length + 3) |
| Minimum Match Length | 3 bytes |
| Maximum Match Length | 18 bytes (15 + 3) |
| Window Pre-fill | First 4,078 bytes (0xFEE) filled with 0x00 |
Encoding Format
The compressed data consists of flag bytes followed by literal bytes or back-references:
- Flag Byte: Each bit (LSB first) indicates the type of the next 8 elements:
- Bit = 1: Literal byte follows
- Bit = 0: Back-reference follows
-
Literal Byte: Single byte copied directly to output
- Back-Reference: Two bytes encoding position and length:
Byte 1: Low 8 bits of window offset Byte 2: [High 4 bits of offset][4-bit length] offset = (byte2 & 0xF0) << 4 | byte1 length = (byte2 & 0x0F) + 3
Decompression Algorithm
def decompress_slide4k(data):
window = bytearray(4096)
window[0:0xFEE] = bytes(0xFEE) # Pre-fill with zeros
window_pos = 0xFEE
output = bytearray()
i = 0
while i < len(data):
flags = data[i]
i += 1
for bit in range(8):
if i >= len(data):
break
if flags & (1 << bit):
# Literal byte
byte = data[i]
i += 1
output.append(byte)
window[window_pos] = byte
window_pos = (window_pos + 1) & 0xFFF
else:
# Back-reference
if i + 1 >= len(data):
break
low = data[i]
high = data[i + 1]
i += 2
offset = ((high & 0xF0) << 4) | low
length = (high & 0x0F) + 3
for _ in range(length):
byte = window[offset]
output.append(byte)
window[window_pos] = byte
window_pos = (window_pos + 1) & 0xFFF
offset = (offset + 1) & 0xFFF
return bytes(output)
SLIDE8K Format Specification
SLIDE8K is the same scheme with a doubled ring and one bit moved from the match
count into the match offset. The authority for everything below is
scripts/build/decompress_slide8k.py / compress_slide8k.py in the
disassembly repository — they round-trip all six factory blocks byte-identically
— cross-checked line by line against SLIDE_Decompress_8K_Init
(v10/maincpu/boot/system_handlers.s:7103-7224).
Container
Offset Size Content
------ ---- -------
0x00 7 "SLIDE8K"
0x07 1 0x00 terminator
0x08 3 Decompressed size, 24-bit BIG-ENDIAN
(all six factory blocks: 00 90 00 = 0x9000 = 36,864 bytes)
0x0B ... compressed stream
Identical in shape to SLIDE4K; only the magic differs.
Ring Buffer
The firmware mallocs 0x2000 bytes and zero-fills only up to the initial
write position, leaving the last ten ring bytes as allocator garbage:
ef40cc: push 0x2000 ; ring size
ef40cf: call 0xff0e80 ; Malloc
ef40db: lda XBC,XHL+0x1ff6 ; fill limit
ef40e0: ld (XWA+),0x00 ; zero-fill ring[0x0000-0x1FF5]
ef40e4: cp XWA,XBC
ef40e6: jr C,0xef40e0
ef40e8: ld BC,0x1ff6 ; initial write position
So ring positions 0x0000-0x1FF5 are zero and the write position starts at
0x1FF6, wrapping modulo 0x2000. (SLIDE4K: 0x1000-byte ring, zero-filled to
0x0FED, write position 0x0FEE — system_handlers.s:6984-6995.) A valid encoder
must never emit a match reading ring[0x1FF6-0x1FFF] before the corresponding
output bytes have been written; compress_slide8k.py enforces exactly that
(find_longest_match, lines 195-243).
Flag Bytes
The firmware keeps the flag in a 16-bit word as 0xFF00 | flags and shifts it
right once per element; the reload happens when the 0xFF sentinel has been
shifted out of bit 8 (system_handlers.s:7139-7148). That is what guarantees
exactly 8 elements per flag byte. Bit 0 after each shift selects the element
type — LSB first, 1 = literal, 0 = back-reference (:7152).
Match Encoding
A back-reference is two bytes, low then high:
offset = ((high & 0xF8) << 5) | low ; 13-bit ABSOLUTE ring position
count = (high & 0x07) + 3 ; 3 .. 10 bytes
The offset is an absolute ring position, not a distance, and the ring is written while the copy runs — so a match may legally read bytes it has just produced (this is how runs longer than the distance to the write position are encoded).
The firmware forms the offset with and wa, 0xf8 / sll wa, 5
(system_handlers.s:7182-7183). The +3 base is confirmed twice over: the
count field is masked with and iz, 0x7 and then incremented by 2
(:7187-7188), and the copy loop is inclusive — cp iy, iz / jr ule
(:7211-7212) — so it runs iy = 0 … iz, i.e. (high & 0x07) + 3 iterations.
The SLIDE4K routine is structurally identical with 0xF0/<<4, and iz, 0xf
(:7059-7065, :7088-7089), giving (high & 0x0F) + 3.
Termination
Termination is purely by output count against the header’s declared size. The count is checked before every compressed-stream byte read and after every element, but the match copy loop itself is not bounds-checked, so a final back-reference could in principle overrun the declared size. None of the six factory blocks do — all produce exactly 0x9000 bytes.
There is no end-of-stream marker, and the trailing bits of the last flag byte are never examined.
Reference Decoder
WINDOW_SIZE, WINDOW_MASK, START = 0x2000, 0x1FFF, 0x1FF6
def decompress_slide8k(buf, offset=0):
assert buf[offset:offset + 8] == b"SLIDE8K\x00"
size = (buf[offset+8] << 16) | (buf[offset+9] << 8) | buf[offset+10]
i = offset + 11
window = bytearray(WINDOW_SIZE) # zero prefill
wpos = START
out = bytearray()
flag = 0 # 16-bit flag word, 0xFF00 | flags
while True:
flag >>= 1
if not (flag & 0x100): # sentinel gone: reload
if len(out) >= size: break
flag = 0xFF00 | buf[i]; i += 1
if flag & 1: # literal
if len(out) >= size: break
b = buf[i]; i += 1
out.append(b); window[wpos] = b
wpos = (wpos + 1) & WINDOW_MASK
else: # back-reference
if len(out) >= size: break
low = buf[i]; i += 1
if len(out) >= size: break
high = buf[i]; i += 1
ref = ((high & 0xF8) << 5) | low
for k in range((high & 0x07) + 3): # ring is live during the copy
b = window[(ref + k) & WINDOW_MASK]
out.append(b); window[wpos] = b
wpos = (wpos + 1) & WINDOW_MASK
if len(out) >= size: break
return bytes(out)
Byte-Exact Recompression
Rebuilding a ROM from source means the compressed blocks must come out
byte-identical, and for both SLIDE variants that is only achievable by
replaying the original encoder’s decisions (--reference). Plain re-encoding
produces a valid stream, but not the factory bytes: the original Technics
compressor’s offset/length choices do not follow a simple greedy or lazy
matching rule.
Two further subtleties, specific to how these streams end, would defeat a naive re-encode even if the match choices were reproduced:
1. Partially consumed final flag byte with nonzero unused bits. Because the decoder stops on the output count, the last flag byte is usually consumed mid-group, and its never-read high bits are not guaranteed to be zero. Measured on the six factory streams:
| Block | Last flag byte | Elements consumed | Unused bits |
|---|---|---|---|
| German (stale, 0x983B3A) | 0xA0 |
1 | nonzero (0x50 remaining) |
| English | 0x30 |
6 | 0 |
| German | 0x00 |
3 | 0 |
| French | 0x00 |
8 | 0 |
| Spanish | 0x00 |
4 | 0 |
| Indonesian | 0x00 |
5 | 0 |
The replay path therefore re-emits each original flag byte verbatim, while
still verifying that its consumed bits agree with the replayed decisions
(compress_slide8k.py, encode_decisions, lines 111-161).
2. An alignment pad byte after every stream. Each factory block is followed
by exactly one byte of leftover encoder output that the decoder never reads,
so that the next block starts on an even address. The values are arbitrary:
0x20, 0x7F, 0xCF, 0xCD, 0xCE after the English, German, French, Spanish and
Indonesian streams respectively. Such a byte yields no decision and would be
lost on re-encode, so when the replayed output is an exact prefix of the
reference the remaining original bytes are carried over unchanged
(compress_slide8k.py, compress_with_reference, lines 295-297).
With --strict, anything that cannot be reproduced from the reference decisions
is a hard error, so a rebuilt ROM can never silently diverge from the factory
bytes.
Round-Trip Coverage
| Data | Format | Source of truth in the repo | Build gate |
|---|---|---|---|
| 19 demo-song presets | SLIDE4K | .mid + .yaml per preset |
make verify-demo-presets |
| 5 live help databases | SLIDE8K | decompressed .bin per language |
make verify-help-databases |
| stale German block | SLIDE8K | raw slice of the dump (tail lost) | round-trip checked, not rebuilt |
| v1.42 update image | SLIDE4K | the source-built v142 payload | cmp inside make all |
| v1.40 / v1.41 update images | SLIDE4K | archived images only | not source-built |
Still open: kn5000_subprogram_v141.rom has no source tree (tracked as issue
kn5000-v41), and the v1.40 payload exists only because it was decompressed out
of its update image and committed for preservation. The help-database payloads
are likewise still opaque binaries — decoding their pointer-table/string-pool
record format is future work.
Decompression Routines
The KN5000 contains two independent decompressor implementations.
Main CPU program ROM (both variants)
These are the routines the running instrument uses, e.g. for the help databases
and the demo presets. Source: v10/maincpu/boot/system_handlers.s.
| Label | Address (v10) | Purpose |
|---|---|---|
SLIDE_Parse_Header |
0xEF41E3 |
Validate "SLIDE", dispatch on '4'/'8' |
SLIDE_Decompress_4K_Init |
0xEF3FAB |
SLIDE4K decoder (0x1000 ring) |
SLIDE_Decompress_8K_Init |
0xEF40C5 |
SLIDE8K decoder (0x2000 ring) |
Calling convention: XWA = pointer to the block, XBC = destination address.
Returns HL = 0 on success, 0xFFFF when the "SLIDE" prefix is absent.
Table Data ROM bootloader (SLIDE4K only)
A second, self-contained decompressor lives in the first-stage bootloader at the
end of the table_data ROM. It is used during boot and firmware-update
operations, reads its input through a sector buffer, and paints a progress
indicator. Source: table_data/kn5000_table_data.s.
LZSS_Decompress (Main Decompressor)
| Property | Value |
|---|---|
| Address | 0xFFCA50 (CPU) / 0x09FCA50 (ROM) |
| Label | LZSS_Decompress |
| Purpose | Main SLIDE4K decompression routine |
Description: This is the primary decompression routine that:
- Allocates a 4KB sliding window buffer via
malloc - Pre-fills the window with zeros (positions 0x0000 to 0x0FED)
- Reads the compressed size from the header
- Processes flag bytes and handles literal/back-reference encoding
- Displays progress during decompression
- Frees the window buffer when complete
Stack Frame Layout:
XSP+0x00: (local variable)
XSP+0x04: Flag byte (with sentinel in high byte)
XSP+0x06: Copy counter for back-reference
XSP+0x08: Match length
XSP+0x0A: Window write position
XSP+0x0C: Window base address (copy of +0x10)
XSP+0x10: Window buffer pointer (from malloc)
LZSS_ReadByte
| Property | Value |
|---|---|
| Address | 0xFFC8C2 (CPU) / 0x09FC8C2 (ROM) |
| Label | LZSS_ReadByte |
| Purpose | Read next byte from compressed input stream |
| Returns | HL = byte read, or 0xFFFF if EOF |
Description: Handles sector buffering for reading compressed data. Reads 0x2400 bytes per sector from the table_data ROM and manages sector advancement for large compressed data.
LZSS_OutputByte
| Property | Value |
|---|---|
| Address | 0xFFC935 (CPU) / 0x09FC935 (ROM) |
| Label | LZSS_OutputByte |
| Purpose | Write decompressed byte to output buffer |
| Input | A = byte to output |
Description: Buffers 4 bytes and writes them as a 32-bit word to the destination for efficient memory access. Uses buffer at RAM address 0x0C0A.
LZSS_OutputByte_Alt
| Property | Value |
|---|---|
| Address | 0xFFC974 (CPU) / 0x09FC974 (ROM) |
| Label | LZSS_OutputByte_Alt |
| Purpose | Alternate output handler for different buffer |
Description: Variant of LZSS_OutputByte that uses buffer at RAM address 0x0C0E instead of 0x0C0A. Used in flash update operations.
LZSS_ParseHeader
| Property | Value |
|---|---|
| Address | 0xFFC9B3 (CPU) / 0x09FC9B3 (ROM) |
| Label | LZSS_ParseHeader |
| Purpose | Parse and validate LZSS header for flash updates |
Description: Sets up the source address (0x3E0000 = Custom Data Flash),
reads six header bytes and memcmps five of them against the "SLIDE" string
at 0xFFA150 (= table_data 0x9FA150), then pre-reads sectors for the initial
buffer fill. Note that this bootloader path compares only the five-character
prefix and always uses its 4K decoder — it has no SLIDE8K support. Only the
Main CPU’s SLIDE_Parse_Header inspects the 4/8 character.
RAM Variables
The table_data bootloader’s LZSS routines use the following RAM locations:
| Address | Size | Purpose |
|---|---|---|
0x0C20 |
4 | Expected decompressed output size |
0x0C24 |
4 | Current output position |
0x0C28 |
4 | Source ROM address pointer |
0x0C2C |
4 | Sector read buffer pointer |
0x0C30 |
2 | Display X coordinate (progress) |
0x0C32 |
2 | Display Y coordinate (progress) |
0x0C34 |
2 | Sector offset for progress display |
0x0C36 |
1 | Output byte counter (0-3 for 32-bit writes) |
0x0C0A |
4 | Temporary output buffer (LZSS_OutputByte) |
0x0C0E |
4 | Temporary output buffer (LZSS_OutputByte_Alt) |
The Main CPU’s SLIDE_Decompress_*_Init routines keep their state on the stack
and in the malloc’d ring instead; the ring pointer is stashed at RAM 0x0622.
SLIDE String Markers
The firmware contains "SLIDE" string markers used to validate SLIDE headers
before decompression. There are exactly two in the Main CPU program ROM and one
in the table_data ROM:
| Location | Address | Label |
|---|---|---|
| Main CPU | 0xE00032 |
SLIDE_STRING |
| Main CPU | 0xE00188 |
SLIDE_STRING_2 |
| Table Data | 0x9FA150 |
(within boot data) |
SLIDE_Parse_Header compares against SLIDE_STRING; the table_data bootloader’s
LZSS_ParseHeader compares against the copy at 0x9FA150. Only the five-character
prefix is compared in both cases — the variant character is checked separately
(and only by the Main CPU routine).
Usage Context
Boot Sequence
During the boot sequence, the Main CPU’s SubCPU_Send_Payload routine:
- Transfers the tone database from 0x830000-0x87FFFF to Sub CPU data RAM 0x050000 (five 64 KB bulk transfers) – this is data, not the executable
- Attempts to decompress the SLIDE4K image at 0x3E0000 in Custom Data Flash
- If decompression fails (returns 0xFFFF), falls back to base 0x800000
- Transfers the code blocks (Sub CPU 0x400 and 0xF000-0x3EEFF) from whichever base survived step 2/3 – and in the dumped images neither base holds the payload, so the actual factory source is unresolved
See Boot Sequence for details.
Help System
Pressing HELP and then any panel button looks the button up in the active language’s database. The database is decompressed on demand from one of the five SLIDE8K blocks into RAM 0x69800 (see SLIDE8K Help Databases above).
Firmware Updates
The LZSS decompressor is also used during firmware updates to handle compressed
update files. The bootloader’s flash-update path (Boot_FlashUpdate_Main, 0x9FCC2A
onward) processes compressed ROM images in SLIDE4K form; the archived Sub-CPU update
images are all SLIDE4K. The region 0x9FD8A5-0x9FEA9C, which older notes called
“flash update handlers”, is not part of that path – it is the bootloader’s uPD72068
FDC command-layer driver (see FDC Subsystem).
Compression Tools
All tools live in scripts/build/ in the disassembly repository.
SLIDE4K
# Re-derive all 19 demo-preset blocks from the factory ROM
python3 scripts/build/decompress_demo_presets.py \
--rom original_ROMs/kn5000_table_data.rom \
--output-dir table_data/includes/demo_presets --emit-references
# Byte-identical recompression (replays the factory encoder's decisions)
python3 scripts/build/compress_lzss.py input.bin output.bin \
--strict --reference original_ROMs/demo_preset_18_compressed.original.bin
# ... and with the 11-byte "SLIDE4K\0" + 24-bit BE size header prepended
python3 scripts/build/compress_lzss.py input.bin output.rom \
--strict --with-header --reference original_ROMs/kn5000_subprogram_v142_compressed.rom
SLIDE8K
# Extract all six known blocks (payload + raw stream) from the dump slice
python3 scripts/build/decompress_slide8k.py --all --outdir /tmp/slide8k
# Decompress one block at a known offset, asserting the expected size
python3 scripts/build/decompress_slide8k.py table_data/includes/icons_to_strings.bin \
--offset 0x43918 --expected-size 0x9000 --output help_db_english.bin
# Byte-identical recompression
python3 scripts/build/compress_slide8k.py help_db_english.bin out.bin \
--strict --reference original_ROMs/help_db_english_compressed.original.bin
decompress_slide8k.py --compressed-out writes the stream without the header
and without the alignment pad, which is the form --reference expects to
compare against.
Note: the --reference option decodes the compression decisions from the
original file and replays them. Without it, both compressors emit a valid but
different stream — see Byte-Exact Recompression.
Makefile Targets
# --- SLIDE4K: 19 demo-song presets ---------------------------------------
make decompress-demo-presets # bootstrap: re-extract from the factory ROM
make demo-midi # regenerate the .mid sources
make demo-sidecars # regenerate the .yaml sidecars
make rebuild-demo-presets # .mid + .yaml -> .bin -> compressed .bin
make verify-demo-presets # byte-compare all 19 against the factory streams
# --- SLIDE8K: 6 help databases -------------------------------------------
make decompress-help-databases # bootstrap: re-extract the decompressed sources
make rebuild-help-databases # recompress the checked-in sources
make verify-help-databases # byte-compare all 6 against the factory streams
make all rebuilds every ROM and additionally regenerates
rebuilt_ROMs/kn5000_subprogram_v142_compressed.rom from the source-built v142
payload with compress_lzss.py --strict --with-header --reference, sealing it
with a cmp against the archived update image.
Both verify-* targets print one OK/MISMATCH line per block. As of
2026-08-07 all 25 blocks (19 SLIDE4K + 6 SLIDE8K) report OK.
Source Files
Demo-song presets (SLIDE4K)
The compressed blocks are build products. The checked-in sources are MIDI files plus YAML sidecars carrying everything MIDI cannot express (song header, cell topology, padding, stream order, running-status flags):
table_data/includes/demo_presets/midi/demo_preset_NN.mid <- musical content
table_data/includes/demo_presets/sidecar/demo_preset_NN.yaml <- everything else
↓ (midi_to_preset.py)
table_data/includes/demo_presets/demo_preset_NN.bin <- uncompressed
↓ (compress_lzss.py --strict --reference)
table_data/includes/demo_presets/demo_preset_NN_compressed.bin <- in ROM
Both .bin stages are generated and git-ignored. (This supersedes the earlier
table_data/preset_data.asm flow described in older revisions of this page;
that file no longer exists.)
Help databases (SLIDE8K)
table_data/includes/help_databases/help_db_<lang>.bin <- checked-in source
↓ (compress_slide8k.py --strict --reference)
table_data/includes/help_databases/help_db_<lang>_compressed.bin <- in ROM
table_data/help_databases.s is the assembly module that emits the language
pointer tables, the intro strings, the 11-byte SLIDE8K headers and these
payloads — and the stale German block as a raw slice.
Reference files
| File | Description |
|---|---|
original_ROMs/demo_preset_NN_compressed.original.bin |
Factory SLIDE4K stream, ×19 |
original_ROMs/help_db_<lang>_compressed.original.bin |
Factory SLIDE8K stream + pad byte, ×6 |
original_ROMs/kn5000_subprogram_v14{0,1,2}_compressed.rom |
Sub-CPU update images (whole-file SLIDE4K) |
Future Work
- Decode the help-database payload format (pointer table + string pool at RAM 0x69800) into per-topic sources
- Reverse engineer the preset parameter record format in detail
- Determine the exact purpose of each parameter type (
18 XXcodes) - Build a source tree for the v1.41 Sub-CPU payload (issue
kn5000-v41) - Re-derive the 0x8E0000 section boundaries from the full 38,144-byte decompression