前文我们已经剖析了 BZ/BZW 这对 LZSS 变体。但你也许会问:PS2 游戏中不是还有一套基于 RLE 的图片压缩算法吗?它和 BZ/BZW 又是什么关系?
RLE 和 LZSS:两种不同的压缩思路
它们都是无损压缩算法,但利用的是不同的数据特征。
RLE(Run-Length Encoding,游程编码) 关注连续重复的数据:一段相同的值出现多次时,只需记录这个值和它重复的次数。
LZSS(Lempel–Ziv–Storer–Szymanski) 关注数据与历史内容之间的重复关系:即使重复片段相隔很远,也可以通过回溯引用来表示。
可以用下面这张图理解它们的位置:
| |
注意:本文把 PS2 的 RLE 变体称为 PRLE。
严格来说,这些分类并非互斥:实际压缩格式可以组合多种技术,例如 Deflate 就将 LZ77 与 Huffman 编码结合使用。RLE 通常单独作为一种简单的游程编码技术讨论,不属于 LZSS 的字典引用机制。
两者的核心区别如下:
| 特性 | RLE / PRLE | LZSS(BZ/BZW) |
|---|---|---|
| 主要利用的数据特征 | 连续重复值 | 历史数据中的重复片段 |
| 重复数据的表示 | 值 + 重复次数 | 回溯距离 + 复制长度 |
| 非重复数据的处理 | 依赖字面量块等机制 | 直接编码为明文 |
| 是否需要历史窗口 | 不需要 | 需要 |
| 解码复杂度 | 通常较低 | 需要维护输出历史 |
| 适合的数据 | 大片相同颜色、填充值等 | 文本、结构化数据、重复图案等 |
用一句话概括:
RLE 说的是“这个值连续出现了 N 次”;LZSS 说的是“把刚才出现过的那段数据再复制一遍”。
关键对比:如何处理不重复的数据?
假设输入数据为:
| |
其中有很多连续重复值,RLE 可以将它们压缩成若干个游程。
但如果输入变成:
| |
每个字符都只出现一次,RLE 就很难从中获益。若编码格式没有专门的字面量块机制,甚至可能因为每段数据都需要额外的长度信息而使结果膨胀。
这正是我们讨论的 PS2 PRLE 与 BZ/BZW 的一个重要区别。
PRLE 如何避免无意义的逐值开销?
之前分析的 PRLE 格式不仅能表示重复游程,还能用一条指令表示一整段不重复的数据,也就是字面量块(Literal Block)。
假设该格式使用 16-bit word 作为数据单位,并以控制 word 的最高位区分两种模式,那么一段长度为 5 的字面量块可以概念性地表示为:
| |
这里的控制 word 具体如何编码长度,必须以实际解码器的位运算和长度公式为准;上面只用于说明机制。
如果每个数据 word 占 2 字节,那么这 5 个 word 原本占 10 字节,加入一个 2 字节控制 word 后,总共占 12 字节。
额外开销为:
| |
这比为每个 word 单独附加控制信息要好得多。不过,字面量块仍然需要控制信息,因此对这段完全不重复的数据而言,PRLE 仍然会产生一定膨胀。
BZ/BZW 如何处理非重复数据?
BZ/BZW 则通过控制掩码区分明文与引用。以 BZ 为例,一个控制字节可以标记接下来的 8 次解码操作。
对于 5 个互不重复的字节,可以用 5 个明文标记配合原始数据表示:
| |
如果这 5 个字节都使用明文编码,那么只需要一个控制字节和 5 个数据字节,总计 6 字节。
相对于原始的 5 字节,额外开销为:
| |
如果有足够多的明文操作可以共用同一个控制字节,控制开销就会逐渐降低。对于连续的纯明文数据,每 8 个字节共用 1 个控制字节,渐近额外开销为:
| |
这里有一个容易忽略的细节:12.5% 是纯明文编码在控制掩码上的渐近开销,不是整个 BZ 算法在所有输入下的最坏膨胀率。 引用编码、结束标记以及输入长度不足一个控制组等因素,都会影响实际结果。
膨胀率对比
假设 PRLE 使用 16-bit word,且一段字面量块只需一个 16-bit 控制 word;BZ 使用一个控制字节控制 8 次操作。在不考虑额外文件头、对齐和结束标记的情况下,可以作如下定性比较:
| 数据特征 | PRLE | BZ/BZW |
|---|---|---|
| 长段连续相同值 | 通常很有效 | 也可能有效,取决于引用编码 |
| 大量短游程 | 取决于游程编码开销 | 取决于引用长度与控制位开销 |
| 完全不重复的数据 | 字面量块带来固定开销 | 控制掩码带来分组开销 |
| 远距离重复片段 | 通常不能直接利用 | 可以利用回溯引用,但受窗口限制 |
| 混合数据 | 对连续重复程度敏感 | 对历史重复片段的数量、长度和距离敏感 |
因此,不能简单地说 RLE 的压缩率一定更低,或者 LZSS 在所有输入上都更好。具体结果取决于数据分布、引用编码、窗口大小和格式本身的设计。
写在最后:算法没有过时,只有场景在变化
今天的处理器拥有更高的运算能力、更大的内存和更成熟的压缩库。面对通用数据压缩需求,我们通常可以直接选择经过充分优化的现代实现,而不必从头编写 RLE 或 LZSS。
但这并不意味着这些算法失去了价值。
在 Dreamcast 和 PS2 时代,资源压缩不仅要考虑压缩率,还要考虑解压速度、内存占用、实现复杂度,以及游戏读取资源时的实际需求。一个简单的 RLE 解码器可能非常适合大量重复颜色的数据;一个 LZSS 变体则可以利用更广泛的历史重复片段。不同算法服务于不同的数据特征,没有必要强求统一。
对今天的复古游戏研究而言,它们还有另一层价值:理解压缩算法,就是理解游戏资源如何组织、存储和加载。 识别一段数据究竟使用游程编码、字典引用,还是两者结合,不仅能帮助我们编写解包器,也能避免把压缩格式、资源封装和图像编码混为一谈。
所以,RLE 与 BZ/BZW 并不是谁取代谁的关系。它们代表了两种不同的工程取舍:一种用极少的状态描述连续重复,另一种用历史引用复用已经出现过的数据。
逆向工程真正有意思的地方,正是从这些看似简单的指令中,逐步还原出当年开发者面对的约束,以及他们为解决具体问题做出的选择。
欢迎关注我的公众号,第一时间获取最新文章。
