复古 Demo 场景的开发者们在一系列严苛的硬件约束下,展现了令人惊叹的工程创造力。相较于现代 GPU 的充裕带宽与并行处理能力,8 位和 16 位处理器(如 Z80、68000)仅具备极少的寄存器、狭窄的数据总线与受限的内存带宽。本文将从位操作技巧、内存布局策略与 CPU 周期压缩三个维度,系统化地解析这些极致优化技术的核心原理与可落地参数。

位操作:把一个字节拆成多个像素

位操作是复古图形编程的基石,其核心理念是用一个字节存储多个像素的颜色索引,从而将内存带宽消耗减半甚至更多。最常见的做法是将两个 4 位颜色索引 pack 进同一个字节,高四位存第一个像素,低四位存第二个像素。提取时仅需使用掩码与移位操作:先通过 AND 0x0F 取低四位,再通过 AND 0xF0 取高四位并右移四位。这种技术在 Commodore 64 的位图模式与 Amiga 的平面图形(Planar)模式中广泛使用。

在 320×200 分辨率、256 色调色板的典型 VGA 模式 13h 下,每个像素占 1 字节,直接读取即为颜色索引。若采用双像素打包方案,可将显存读取次数降低 50%,代价是内层循环中增加两次位操作与一次移位。对于主频仅 4.77MHz 的原始 IBM PC 而言,这一 trade-off 往往值得,因为内存访问的延迟远高于寄存器层面的位运算。实际测量表明,在 25MHz 的 386 处理器上,未经优化的逐像素渲染与经过位打包的渲染之间可相差近一倍帧率。

更激进的优化出现在单色位图(Bitmap)模式。以 16 色模式为例,4 个像素的颜色信息可以压缩进 1 个字节,每个像素仅占 4 位。提取时使用 SHR 右移与 AND 掩码即可定位特定像素。这种方式在 Atari ST 的低分辨率模式与 Amiga 的 16 色 ECS 模式下尤为高效,因为视频硬件直接支持位平面读取,CPU 只需负责生成压缩后的位流。

内存布局:行内连续与缓存对齐

内存布局对渲染性能的影响在复古硬件上尤为显著。早期的视频硬件(如 MOS 6567 VDC for C64、Texas Instruments TMS9918A for MSX)采用行内连续布局,即同一扫描线上的像素在内存中连续存放。这种布局的优势在于遍历效率:只需一次基址加偏移即可访问任意像素,无需跨行跳转。

以典型的 256×192 分辨率为例,屏幕首地址设为 SCREEN_BASE,每个像素的地址计算公式为 SCREEN_BASE + (y << 5) + x,其中 y << 5 相当于 y * 32。在 6502 处理器上,这一步可以通过预先计算的查找表进一步加速:建立两个长度为 256 的表 ROW_LO 与 ROW_HI,分别存储低 8 位与高 8 位偏移量,从而将乘加操作转化为两次查表与一次加法。

缓存友好性是另一个关键考量。在 16 位系统如 Amiga(MC68000)中,数据对齐要求严格:Word 操作必须偶数地址对齐,DWord 操作必须四字节对齐。未对齐的访问会导致总线错误或额外的等待周期。优化建议如下:精灵(Sprite)数据按 2 字节或 4 字节对齐;位图扫描线起始地址为 16 的倍数;铜列表(Copper List)条目按 4 字节(两个 Word)对齐,其中第一个 Word 为指令,第二个 Word 为数据或地址。

对于需要频繁切换的调色板,推荐采用调色板缓存策略:将当前帧使用的 16 色调色板预加载到视频硬件的专用寄存器区域,而非每帧重新写入完整 256 色调色板。Amiga 的自定义芯片允许通过铜列表在每个扫描线切换调色板条目,这一特性被大量用于实现光栅条(Raster Bars)效果,而 CPU 仅需在每帧开始时更新铜列表的起始地址。

CPU 周期压缩:消除除法与分支

CPU 周期是复古 Demo 最稀缺的资源。优化策略的核心是:用位运算替代算术运算,用查表替代计算,用分支预测友好的代码替代深度分支。

除法是 CPU 周期消耗的大户。在没有浮点协处理器(如 8087)的环境下,除法操作可能消耗数十甚至上百个时钟周期。幸运的是,Demo 场景中的除法请求往往可以转化为位移操作。当需要将一个数除以 2 的幂次时,直接使用右移 SHR(在 6502 中为 LSR,在 68000 中为 ASR)即可。例如 x / 8 等价于 x >> 3。若需除以非 2 的幂次,可以预先计算倒数值并使用乘法近似:例如 x / 10 可近似为 (x * 205) >> 11,因为 205/2048 ≈ 0.1001,与 1/10 = 0.1 非常接近。

分支(Branch)是另一个性能杀手。现代处理器的分支预测机制可以缓解这一问题,但 8/16 位处理器几乎没有这类优化。每一次条件跳转都意味着指令流水线可能被迫清空。解决方案是采用分支 less 编程:使用位操作实现条件选择。例如,在 6502 中实现 “如果 A 大于 B 则执行 X”,可以用 CMP 后根据进位标志决定是否执行,或者使用 BMI(负)、BPL(正)配合 AND 掩码构建位掩码条件。在 68000 中,可以使用 SCC(Set on Condition)指令将条件码直接写入目标寄存器,然后用 ANDANDI 实现条件执行块。

查表法(LUT, Look-Up Table)是压缩 CPU 周期的利器。三角函数(sin/cos)的计算在任何没有浮点硬件的系统上都极其昂贵。以 360° 正弦表为例,预先生成一个长度为 256 的字节表,每个元素为 sin(angle) * 127 + 128 的整数值。查表操作仅需一次数组索引与一次内存读取,远快于使用级数展开或泰勒近似。对于 6502,累计偏移量可通过在零页(Zero Page)维护指针变量来实现 256 字节边界自动回绕,从而无需显式的取模操作。

光栅效果与扫描线技巧

复古 Demo 最具标志性的视觉元素之一是光栅效果(Raster Effects),它通过利用显示硬件的扫描线时序,在单帧内实现多种颜色渐变与动画。实现原理是:视频硬件在每个扫描线结束时触发中断或允许 CPU 修改寄存器,开发者利用这两个扫描线之间的空闲时间(通常为 63.5 微秒在 PAL 制式下)修改调色板、滚动偏移或切换位图。

在 Commodore 64 的 VIC-II 芯片上,典型的光栅条实现需要设置比较寄存器 RASTER,当扫描线到达指定行时触发中断。中断服务程序(ISR)执行 STA $D021(背景色寄存器)或 STA $D020(边框色寄存器)来改变颜色。整个过程需要控制在约 1500 个时钟周期内完成,否则会干扰下一行的显示。

Amiga 的铜列表(Copper List)是另一种更优雅的方案。铜协处理器是一个专门的微代码单元,它按顺序执行一系列指令,每个指令包含一个地址与一个数据值。开发者可以预先编写一个铜指令列表,包含数百条甚至数千条在特定扫描线修改寄存器值的指令。典型的铜列表条目结构为:WAIT(扫描线, 水平位置)MOVE(寄存器地址, 数据)。这种方式完全不占用主 CPU 的时间,所有时序控制由硬件自动完成。

实用参数建议:在 PAL 制式(312 扫描线,每行 63.5 微秒)下,单条指令的执行窗口约为 40 纳秒量级。若在中断服务程序中需要完成 10 条寄存器写操作,总耗时应控制在 500 个周期以内(约 20 微秒),以确保不会丢失下一个扫描线中断。对于 68000 系统,推荐使用 MOVEM 指令批量写入寄存器,可将多个寄存器的保存与恢复合并为单条指令。

实践建议与监控要点

对于希望复现或学习这些技术的开发者,以下是可直接参考的工程参数与阈值建议。

在位操作层面,推荐的像素打包粒度为 2 像素 / 字节(4+4 位)或 4 像素 / 字节(2+2 位),具体取决于目标硬件的调色板大小。若调色板不超过 16 色,使用 4 像素打包可将显存带宽需求降至原来的 25%。提取操作务必使用寄存器间接寻址与移位,避免在循环中重复计算掩码常量。

在内存布局层面,屏幕缓冲区应保持 16 字节对齐;查找表应置于零页(6502)或直接页(68000 的 A5 相对地址)以获得一个周期内的访问优势;铜列表应按 4 字节对齐并放置在芯片内存(Chip RAM)区域以避免总线冲突。

在 CPU 周期压缩层面,单帧可用周期预算应按目标帧率倒推:60fps 时每帧约 8.3 万周期(8MHz 系统),30fps 时约 16.6 万周期。建议将渲染代码的周期消耗控制在总预算的 60% 以内,预留 40% 给音频、游戏逻辑与系统开销。复杂运算(如透视校正、仿射变换)必须全部查表化,禁止在渲染内层循环中出现除法或浮点运算。

复古 Demo 场景的优化技术虽源于历史硬件,但其核心理念 —— 最大化硬件利用率、最小化数据移动、用计算换内存 —— 在现代嵌入式与高性能优化中依然具有重要参考价值。

资料来源:本文技术细节参考了复古图形编程社区的通用实践与位操作优化讨论。