Skip to content

compression packing

drpetersonfernandes edited this page Aug 15, 2026 · 1 revision

Compression & packing

Compression methods

compression field of the disc struct:

ID Name Read support Write support Notes
0 None identity
1 Purge ✔ (WIA) ✖ (RVZ) WIA-only; rejected by RVZ readers
2 Bzip2 level 1–9
3 LZMA LZMA1, 7-Zip properties
4 LZMA2 raw LZMA2 chunk framing
5 Zstd level 1–22 (default 3)

compr_data (disc struct offset 0xD5)

Method Properties
None / Purge / Bzip2 / Zstd empty
LZMA 5 bytes: lc + lp·9 + pb·45 (7-Zip default 0x5D) + 4-byte little-endian dictionary size
LZMA2 1 byte: dictionary-size property (2^(prop/2+12) style, 7-Zip table)

The writer's LZMA dictionary size depends on the level: 1<<18 (≤1), 1<<20 (≤3), 1<<23 (≤5), 1<<25 (≤7), 1<<27 otherwise; position bits 2, literal context 3, literal position 0.

Stream formats written by RVZSharp

  • LZMA1: the 7-Zip encoder's output (range-coder data) after the 5-byte properties; the properties travel in compr_data, not in the stream. The encoder runs with a known size (no end marker) — both Dolphin and RVZSharp decode with a known output size.
  • LZMA2: a sequence of chunks, each [control][unpack_size-1:2][pack_size-1:2][props:1] [LZMA1 stream], ended by a 0x00 control byte. Chunk payloads are capped at 0xF800 bytes and each chunk resets the dictionary (control 0xE0 | (size-1)>>16). The single props byte (0x5D) must match the compr_data property.

Group compression semantics

  • Lists + data are compressed together for methods > Purge; otherwise the lists are stored raw (padded to 4) ahead of the compressed stream.
  • The compressed flag (bit 31 of the group size) is set only when compression shrank the data.
  • All-zero chunks with no exceptions become zero groups (size 0) — nothing is stored.

RVZ packing (junk detection)

Wii discs are full of padding — pseudo-random junk produced by Nintendo's generator. Instead of storing it, RVZ stores a seed that regenerates the junk, plus segment headers. Packing is optional (Packing option / --no-packing); a file without packing is perfectly valid.

The junk PRNG

  • Lagged Fibonacci generator: k = 521, j = 32, XOR combination.
  • Seed: 17 big-endian u32 words (68 bytes). Words 17..520 are derived by word[i] = (word[i-17] << 23) ^ (word[i-16] >> 9) ^ word[i-1], then 4 warm-up advances.
  • Output byte order per word: >>24, >>18 (not >>16 — this is the famous oddity), >>8, >>0.
  • The stream position is defined as offset % 0x8000: junk at disc offset P equals the generator output starting at position P % 0x8000. This is why discs' junk looks periodic per 32 KiB.

Segment stream

A packed chunk is a sequence of segments; rvz_packed_size is the total bytes of the segment stream (headers included):

Segment Layout
literal u32 BE size (no MSB) + size literal bytes
junk `u32 BE size

Special case: if a chunk contains no junk at all, the writer stores the whole chunk literally without any headers and rvz_packed_size stays 0 — readers treat 0 as "no packing".

The reader processes segments sequentially and skips the PRNG by the running data offset ((chunkStart + bytesEmitted) % 0x8000) before each junk segment.

Seed recovery (GetSeed)

LaggedFibonacciGenerator.GetSeed(data, size, dataOffsetMod) implements Dolphin's reverse algorithm:

  1. Quick filter: the first 521 words must satisfy the generator's bit-structure property (bits 22–23 == bits 20–21 of the swapped word); random data fails almost always.
  2. Fill the 521-word state from the junk and rewind it to position 0 (partial Backward for the word offset, full Backward per 521-word cycle).
  3. Reconstruct the 17-word seed (undoing the transform and recovering bits 16–17 from the neighbouring words), re-derive words 17..520 and verify them.
  4. Re-advance to the junk position and count how many bytes the seed actually reproduces.

GetSeed is best-effort: junk regions that fail the checks (or diverge) are stored literally. The writer scans forward in 32 KiB-aligned steps with a running data offset (offset % 0x8000), records zero runs as zero-junk when uncompressed, and emits segments per chunk.

Recovered seeds are canonical: the writer's generator and the reader's PRNG agree on them even though their internal word formats differ (the reconstruction makes the seed invariant under the output transform).

Why Zstd by default?

Dolphin's default for RVZ is Zstandard — best ratio/speed trade-off for game data, and the only method that consistently beats LZMA2 on random-ish data. LZMA2 is the strongest compressor for highly compressible data; Bzip2 is a middle ground; None is useful for speed-critical round trips.

Clone this wiki locally