前文我们已经剖析了 BZ/BZW 这对 LZSS 变体。但你也许会问:PS2 游戏中不是还有一套基于 RLE 的图片压缩算法吗?它和 BZ/BZW 又是什么关系?

RLE 和 LZSS:两种不同的压缩思路

它们都是无损压缩算法,但利用的是不同的数据特征。

RLE(Run-Length Encoding,游程编码) 关注连续重复的数据:一段相同的值出现多次时,只需记录这个值和它重复的次数。

LZSS(Lempel–Ziv–Storer–Szymanski) 关注数据与历史内容之间的重复关系:即使重复片段相隔很远,也可以通过回溯引用来表示。

可以用下面这张图理解它们的位置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
无损压缩
├─ 游程编码(RLE)
│  ├─ 连续重复值的编码
│  └─ PRLE 等带有字面量块的变体
│
├─ 字典编码
│  └─ Lempel–Ziv 家族
│     ├─ LZ77 / LZSS
│     └─ LZ4、Deflate 等相关方案
│
└─ 熵编码
   ├─ Huffman
   ├─ 算术编码
   └─ ANS(FSE、rANS 等)

注意:本文把 PS2 的 RLE 变体称为 PRLE。

严格来说,这些分类并非互斥:实际压缩格式可以组合多种技术,例如 Deflate 就将 LZ77 与 Huffman 编码结合使用。RLE 通常单独作为一种简单的游程编码技术讨论,不属于 LZSS 的字典引用机制。

两者的核心区别如下:

特性RLE / PRLELZSS(BZ/BZW)
主要利用的数据特征连续重复值历史数据中的重复片段
重复数据的表示值 + 重复次数回溯距离 + 复制长度
非重复数据的处理依赖字面量块等机制直接编码为明文
是否需要历史窗口不需要需要
解码复杂度通常较低需要维护输出历史
适合的数据大片相同颜色、填充值等文本、结构化数据、重复图案等

用一句话概括:

RLE 说的是“这个值连续出现了 N 次”;LZSS 说的是“把刚才出现过的那段数据再复制一遍”。

关键对比:如何处理不重复的数据?

假设输入数据为:

1
AAAAAABBCDDEEEEEF

其中有很多连续重复值,RLE 可以将它们压缩成若干个游程。

但如果输入变成:

1
ABCDE

每个字符都只出现一次,RLE 就很难从中获益。若编码格式没有专门的字面量块机制,甚至可能因为每段数据都需要额外的长度信息而使结果膨胀。

这正是我们讨论的 PS2 PRLE 与 BZ/BZW 的一个重要区别。

PRLE 如何避免无意义的逐值开销?

之前分析的 PRLE 格式不仅能表示重复游程,还能用一条指令表示一整段不重复的数据,也就是字面量块(Literal Block)。

假设该格式使用 16-bit word 作为数据单位,并以控制 word 的最高位区分两种模式,那么一段长度为 5 的字面量块可以概念性地表示为:

1
2
控制 word:标记非重复块,长度为 5
数据:     A B C D E

这里的控制 word 具体如何编码长度,必须以实际解码器的位运算和长度公式为准;上面只用于说明机制。

如果每个数据 word 占 2 字节,那么这 5 个 word 原本占 10 字节,加入一个 2 字节控制 word 后,总共占 12 字节。

额外开销为:

1
2/10 = 20%

这比为每个 word 单独附加控制信息要好得多。不过,字面量块仍然需要控制信息,因此对这段完全不重复的数据而言,PRLE 仍然会产生一定膨胀。

BZ/BZW 如何处理非重复数据?

BZ/BZW 则通过控制掩码区分明文与引用。以 BZ 为例,一个控制字节可以标记接下来的 8 次解码操作。

对于 5 个互不重复的字节,可以用 5 个明文标记配合原始数据表示:

1
2
控制掩码:5 个明文位
明文数据:A B C D E

如果这 5 个字节都使用明文编码,那么只需要一个控制字节和 5 个数据字节,总计 6 字节。

相对于原始的 5 字节,额外开销为:

1
1/5 = 20%

如果有足够多的明文操作可以共用同一个控制字节,控制开销就会逐渐降低。对于连续的纯明文数据,每 8 个字节共用 1 个控制字节,渐近额外开销为:

1
1/8 = 12.5%

这里有一个容易忽略的细节:12.5% 是纯明文编码在控制掩码上的渐近开销,不是整个 BZ 算法在所有输入下的最坏膨胀率。 引用编码、结束标记以及输入长度不足一个控制组等因素,都会影响实际结果。

膨胀率对比

假设 PRLE 使用 16-bit word,且一段字面量块只需一个 16-bit 控制 word;BZ 使用一个控制字节控制 8 次操作。在不考虑额外文件头、对齐和结束标记的情况下,可以作如下定性比较:

数据特征PRLEBZ/BZW
长段连续相同值通常很有效也可能有效,取决于引用编码
大量短游程取决于游程编码开销取决于引用长度与控制位开销
完全不重复的数据字面量块带来固定开销控制掩码带来分组开销
远距离重复片段通常不能直接利用可以利用回溯引用,但受窗口限制
混合数据对连续重复程度敏感对历史重复片段的数量、长度和距离敏感

因此,不能简单地说 RLE 的压缩率一定更低,或者 LZSS 在所有输入上都更好。具体结果取决于数据分布、引用编码、窗口大小和格式本身的设计。

写在最后:算法没有过时,只有场景在变化

今天的处理器拥有更高的运算能力、更大的内存和更成熟的压缩库。面对通用数据压缩需求,我们通常可以直接选择经过充分优化的现代实现,而不必从头编写 RLE 或 LZSS。

但这并不意味着这些算法失去了价值。

在 Dreamcast 和 PS2 时代,资源压缩不仅要考虑压缩率,还要考虑解压速度、内存占用、实现复杂度,以及游戏读取资源时的实际需求。一个简单的 RLE 解码器可能非常适合大量重复颜色的数据;一个 LZSS 变体则可以利用更广泛的历史重复片段。不同算法服务于不同的数据特征,没有必要强求统一。

对今天的复古游戏研究而言,它们还有另一层价值:理解压缩算法,就是理解游戏资源如何组织、存储和加载。 识别一段数据究竟使用游程编码、字典引用,还是两者结合,不仅能帮助我们编写解包器,也能避免把压缩格式、资源封装和图像编码混为一谈。

所以,RLE 与 BZ/BZW 并不是谁取代谁的关系。它们代表了两种不同的工程取舍:一种用极少的状态描述连续重复,另一种用历史引用复用已经出现过的数据。

逆向工程真正有意思的地方,正是从这些看似简单的指令中,逐步还原出当年开发者面对的约束,以及他们为解决具体问题做出的选择。


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