本文将带你了解世嘉 Dreamcast 与 PlayStation 2 游戏中出现的一种 LZSS 类压缩算法变体,看看这些游戏如何用更少的光盘空间存储贴图、音效、字体等资源。

背景:为什么游戏需要压缩?

在 20 世纪 90 年代末到 2000 年代初,游戏机主要依靠 CD-ROM 和 DVD-ROM 读取资源。与现代 SSD 相比,光盘不仅传输速度有限,随机寻道的延迟也更高。压缩资源可以减少存储空间占用和读取的数据量。

LZSS(Lempel–Ziv–Storer–Szymanski)是一类常见的字典压缩算法。它的核心思想很简单:重复出现的数据不必反复保存,只需记录它曾经出现的位置和长度。

例如,原始数据是:

1
ABCABCABC

解压器可以先输出 ABC,再用一个引用表示“复制之前的数据 3 个字节”,重复两次即可还原原文。

因此,压缩数据通常包含两种内容:

  • 明文(Literal):直接输出一个字节或一个字。
  • 引用(Reference):从已经解压的数据中复制一段内容。

解压器需要知道每一段数据属于哪一种。BZ 和 BZW 的一个共同特点,就是使用**控制掩码(Control Mask)**来标记接下来应该执行什么操作。

8-bit BZ:一个控制字节,八次操作

控制掩码:决定接下来怎么解压

BZ 每次读取一个控制字节,其中的 8 个比特分别控制接下来的 8 次解码操作。

1
control_mask = 10110010

解码时,解压器按照约定的位顺序逐位检查掩码。根据你逆向得到的实现,其规则是:

1
2
bit = 1 → 明文:直接输出下一个字节
bit = 0 → 引用:读取引用信息并回溯复制

例如,某一位为 1,解压器就读取一个字节并原样输出;如果为 0,则继续读取引用编码,确定从之前的数据中复制多少内容。

这里的关键是:控制掩码只负责告诉解压器执行哪种操作,实际要输出的数据仍然来自后续的明文或引用编码。

引用编码:短引用与长引用

当控制位为 0 时,BZ 根据下一个字节的最高位,判断引用采用哪种编码。

1. 短引用:一个字节

如果最高位为 1,这个字节本身就包含了回溯距离和复制长度。

1
2
3
4
5
  7   6 5 4 3   2 1 0
┌───┬─────────┬───────┐
│ 1 │ 距离编码 │ 长度码 │
└───┴─────────┴───────┘
   1    4 bit    3 bit

对应的解码公式为:

1
2
length = (b & 0x07) + 2
offset = ((b >> 3) & 0x0F) - 16

其中:

  • length 表示需要复制的字节数,范围为 2~9。
  • offset 是相对于当前输出位置的负偏移,范围为 -16~-1。

例如,offset = -3、length = 4,就表示从当前输出位置往前数 3 个字节,复制 4 个字节到输出末尾。

短引用只需占用一个字节,因此适合表示距离较近、长度较短的重复片段。

2. 长引用:两个字节

如果最高位为 0,解压器会再读取一个字节。两个字节共同表示回溯距离和复制长度。

1
2
3
4
5
第 1 字节                 第 2 字节
 7   6 5 4 3 2 1 0        7 ... 0
┌───┬───────────────┐    ┌─────────┐
│ 0 │   距离编码     │    │ 长度编码  │
└───┴───────────────┘    └─────────┘

对应的解码公式为:

1
2
offset = b1 - 128
length = b2 + 1

其中,offset 的范围为 -128~-1,长度编码可表示 1~256。由于长度为 1 的情况被用作特殊标记,正常的长引用长度为 2~256。

与短引用相比,长引用多占用一个字节,但可以表示更远的回溯距离和更长的重复片段。

EOF:用特殊引用结束解压

BZ 用长引用中第二个字节为 0(即 length = 0)作为流结束标记。因为长引用的长度实际是 bVar2 + 1,bVar2 = 0 时本应表示"长度 1",这个无意义的短引用被"征用"为 EOF。这是一种典型的保留值复用设计——节省了一个字节的结束标记。

需要注意,这种 EOF 规则属于具体格式的约定,并不是所有 LZSS 算法都采用相同的结束方式。

为什么同时需要短引用和长引用?

可以把两种引用理解成两种不同长度的“快捷指令”:

  • 短引用:只占一个字节,适合附近的小段重复数据。
  • 长引用:占两个字节,能够表达更长的复制长度和更远的回溯距离。

BZ 的短引用只能回溯 16 个字节,而长引用可以回溯最多 128 个字节。这里的偏移是相对于当前输出位置计算的负偏移。

因此,不能把整个 BZ 算法的回溯范围简单理解为只有 16 字节;16 字节只是短引用所能覆盖的距离。

BZW:以 16-bit word 为单位

BZW 与 BZ 的基本思想相同,但它将解码单位从 8-bit 字节改成了 16-bit word(字)。

这意味着解压器不再一次处理一个字节,而是一次处理两个字节。相应地,控制掩码、明文读取和回溯复制也都需要按照 word 的边界进行处理。

控制掩码:一次控制 16 次操作

BZ 使用 8-bit 控制掩码,BZW 则使用 16-bit 控制掩码,每个掩码控制接下来的 16 次解码操作。

1
2
raw_mask = read_le_u16()
control_mask = swap_u16(raw_mask)

这里的 swap_u16 会交换 16-bit 数值的高、低字节。

为什么需要交换?BZW 的格式是大端序设计(Saturn 血统),PS2 是小端序平台,因此掩码和引用字段必须做字节交换。字面字因为只是字节搬运,不需要交换。

明文与引用:一次读取一个 word

在 BZW 中,一次明文操作会读取一个 16-bit word,并将其写入输出缓冲区:

1
2
word = read_le_u16()
dest_words.append(word)

如果控制位表示引用,则读取一个 word 作为引用编码。先交换引用编码的字节(原因同上),再解析长度和回溯偏移:

1
2
3
4
ref = swap_u16(word)

length = (ref & 0xFF) + 1
offset = ((ref >> 8) & 0xFF) - 256

其中:

  • length 表示复制的 word 数量。
  • offset 表示相对于当前输出位置的负偏移,范围为 -256~-1。
  • offset = -3 表示从当前输出位置之前的第 3 个 word 开始复制。

这里的距离单位是 word,而不是字节。例如,回溯 3 个 word 就相当于回溯 6 个字节。

EOF:保留长度为 1 的编码

BZW 使用长度编码 1 作为特殊结束标记。因为长度计算公式为:

1
length = (ref & 0xFF) + 1

当长度编码的低 8 位为 0 时,解码得到的长度就是 1。根据这套格式的约定,该编码被用作 EOF。

1
2
if length == 1:
    break

这意味着正常的引用不会使用长度为 1 的编码。如果实际压缩器确实不会生成单 word 引用,那么这个编码就可以安全地用于结束标记。

最终输出:将 word 还原为字节流

解压完成后,BZW 需要把 word 数组转换成字节数组。按照当前实现,每个 word 先输出低字节,再输出高字节:

1
2
out_bytes.append(word & 0xFF)
out_bytes.append((word >> 8) & 0xFF)

例如,word 的数值为 0x1234,输出字节就是:

1
34 12

原因同上。

BZ 与 BZW 的区别

特性BZBZW
控制掩码8 bit,控制 8 次操作16 bit,控制 16 次操作
解码单位1 byte1 word(2 bytes)
短引用1 字节,长度 2~9无
长引用2 字节,正常长度 2~2561 word,正常长度 2~256
回溯距离短引用 1~16 字节;长引用 1~128 字节1~256 word,即 2~512 字节
EOF长引用第二字节为 0引用解码长度为 1
输出方式直接输出字节word 展平为字节

表中的距离均指回溯距离的大小,而不是负偏移本身。

写在最后:老算法的现代价值

研究 BZ 与 BZW 的意义,不只是复原一批旧游戏资源,更在于理解游戏引擎如何在存储、读取和解码之间做出取舍。

首先,压缩格式的价值不只在于压缩率。控制掩码、固定宽度的引用编码和简单的回溯复制,都让解压器能够以相对直接的流程还原数据。对于资源读取频繁、内存有限的旧主机,这类设计有实际意义。不过,具体算法是否更快、更省内存,仍需要结合硬件、数据分布和实现方式判断。

其次,逆向分析可以帮助我们区分算法与封装。文件头、控制掩码、引用编码、结束标记和输出字节序,都是压缩格式的一部分。即使两个游戏使用相似的 LZSS 思路,只要其中某个约定不同,就可能无法共用同一个解压器。

最后,BZ 与 BZW 也提醒我们:面对年代久远、缺乏文档的游戏资源,不必一开始就猜测格式名称。找到解压函数,追踪输入读取、控制位判断、回溯复制和输出过程,再用实际样本验证,往往比仅凭文件扩展名或魔数更可靠。

对于复古游戏研究而言,能解释数据如何被编码、如何被还原,比给它贴上一个看似熟悉的算法名称更重要。


欢迎关注我的公众号,第一时间获取最新文章。