在讨论游戏主机图形管线时,加法混合(Additive Blending)是一种让光源类物体 —— 爆炸、魔法特效、等离子光束 —— 在渲染时只增亮而不遮暗的核心技术。PlayStation 通过固定公式 src + dst 配合 GPU 自动 clamp,轻松实现了这一效果;而 Nintendo 64 的 Reality Display Processor(RDP)虽然拥有更灵活的 Color Combiner,却在实现加法混合时遭遇了一个根本性的硬件缺陷:结果不经过 clamp,直接回绕。本文深入解析该问题的成因、两缓冲区分阶段设计的解决方案,以及将 32-bit 累积结果转换至 16-bit 显示缓冲区时的性能优化细节。
1. 问题根源:RDP Color Combiner 的不 clamp 行为
RDP 的 Color Combiner 支持类似 OpenGL glBlendFunc() 的可配置混合方程。Libdragon 将其暴露为 RDPQ_BLENDER((P, A, Q, B)) 宏,对应执行 (P * A) + (Q * B),其中每个槽位可从 IN_RGB、IN_ALPHA、MEMORY_RGB、ONE 等输入中选择。设置加法混合只需一行:
RDPQ_BLENDER(( IN_RGB, IN_ALPHA, MEMORY_RGB, ONE ))
这对应公式 src * src_alpha + dst * 1,即标准的加法混合。然而,RDP 在计算完 (P * A) + (Q * B) 后不会将结果钳制到 0–255 范围。若源像素 RGB 为 (171, 42, 226)、目标帧缓冲为 (63, 141, 170),加法结果应为 (234, 183, 396),其中 B 通道的 396 超过 8-bit 上限后直接环绕,产生错误颜色。这正是 N64 游戏爆炸效果远不如 PSX 的根本原因 ——PSX GPU 在加法后自动执行了 clamp。
2. 核心解法:双缓冲区架构
解决 RDP 不 clamp 的思路并非在硬件层面修复(无此可能),而是通过分阶段设计将累积与呈现解耦:在高精度缓冲区中累积,在低精度缓冲区中展示。
2.1 32-bit 累积缓冲区
分配一块 320×240 分辨率的 RGBA8888 表面作为渲染目标。8-bit 分量提供 0–255 的完整动态范围,多次加法混合的结果即使累加到较高值也不会在 32-bit 阶段发生环绕:
surface_t render32 = surface_alloc(FMT_RGBA32, 320, 240);
rdpq_set_color_image(render32);
2.2 利用 fog alpha 避免预缩放
如果将 16-bit RGBA5551 资产(5-bit 色深)直接绘制到 32-bit 缓冲区,单次加法仍可能超过 255。传统方案要求离线预处理所有纹理,将 RGB 除以 8(即右移 3 位)。但 PhobosLab 发现了一个更优雅的运行时技巧:滥用 fog alpha 值作为缩放因子。
RDP 的 fog 功能除了传统的雾化效果外,还允许将雾颜色的 alpha 分量注入混合方程。通过将 fog color 设为 (0, 0, 0, 256/8) 即灰度 alpha=32,对应 1/8 缩放:
rdpq_set_fog_color(RGBA32(0, 0, 0, 256/8));
rdpq_mode_blender(RDPQ_BLENDER(( IN_RGB, FOG_ALPHA, MEMORY_RGB, ONE )));
这使得所有绘制到 32-bit 缓冲区的像素自动衰减至 1/8 亮度,为多次加法混合预留了充足的 headroom。每次混合都是原值的 1/8 累加,8 次叠加后才能达到原色亮度 —— 既实现了预缩放,又无需修改任何资产数据。
2.3 16-bit 显示缓冲区与 clamp 转换
显示阶段使用标准的 16-bit RGBA5551 帧缓冲(5-bit R、5-bit G、5-bit B、1-bit alpha),通过 RSP 将 32-bit 结果转换至此:
display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_DISABLED);
转换逻辑遍历每个像素,将 8-bit 分量钳制到 5-bit 范围后重新打包:
void cpu_rgba_8888_to_5551(uint32_t *rgba32_in, uint16_t *rgba16_out) {
for (int i = 0; i < 320 * 240; i++) {
color_t c = color_from_packed32(rgba32_in[i]);
if (c.r > 31) { c.r = 31; }
if (c.g > 31) { c.g = 31; }
if (c.b > 31) { c.b = 31; }
rgba16_out[i] = (c.r << 11) | (c.g << 6) | (c.b << 1) | 0x1;
}
}
3. 性能优化:RSP 向量化转换
纯 CPU 实现上述循环需要约 70ms 才能完成 320×240 的转换,这对实时渲染是不可接受的。N64 的 Reality Signal Processor(RSP)是 MIPS 架构的向量协处理器,其 128-bit SIMD 指令每次可处理 8 个像素(16-bit × 8 = 128-bit),效率远高于标量 CPU 循环。
经过 HailToDodongo(#N64Brew Discord)优化的 RSP 微码将转换时间从 70ms 降至约 3.1ms,降幅超过 95%。这是因为 RSP 的向量加载 / 存储指令可以一次性吞吐多个像素的分量数据,并在单个指令内完成饱和(saturating)打包操作。完整渲染管线的初始化与执行流程如下:
// 初始化 16-bit 显示缓冲
display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_DISABLED);
// 分配 32-bit 渲染表面并设为渲染目标
surface_t render32 = surface_alloc(FMT_RGBA32, 320, 240);
rdpq_set_color_image(render32);
// 配置 fog alpha 实现 1/8 亮度衰减
rdpq_set_fog_color(RGBA32(0, 0, 0, 256/8));
rdpq_mode_blender(RDPQ_BLENDER((IN_RGB, FOG_ALPHA, MEMORY_RGB, ONE)));
// 渲染包含大量加法混合精灵的场景
render_scene();
// 调用 RSP 向量化转换:32-bit → 16-bit + clamp
rsp_rgba_8888_to_5551(render32->buffer, screen->buffer);
// 提交显示
display_show(screen);
4. 局限性与后续优化方向
双缓冲区方案并非无代价。N64 的 RDRAM 带宽极为紧张,32-bit 渲染相比 16-bit 需要多近一倍的内存传输量 —— 这是当年大多数 N64 游戏选择 16-bit 直接渲染的核心原因。对于特效密集的场景,带宽压力可能成为帧率瓶颈。
PhobosLab 提出了两项潜在优化方向:一是仅将需要加法混合的精灵绘制到 32-bit 缓冲区,场景其余部分保留在 16-bit 路径,最后由 RSP 合并两个缓冲区;二是对 32-bit 缓冲采用更低的分辨率渲染,再通过 RSP 上采样混入会进一步降低带宽占用。这些技巧在现代工具链(Libdragon + RSPL 微码编译器)的支持下已具备可操作性。
5. 工程参数速查
| 参数 / 阈值 | 值 | 说明 |
|---|---|---|
| 渲染缓冲区格式 | RGBA8888 | 8-bit / 分量,32-bit/pixel,无环绕 |
| 显示缓冲区格式 | RGBA5551 | 5+5+5+1 bit,用于最终输出 |
| Fog alpha 缩放值 | 256/8 = 32 |
对应 1/8 亮度衰减 |
| 显示分辨率 | 320×240 | 典型 N64 分辨率 |
| CPU 转换耗时 | ~70ms | 标量循环,帧率杀手 |
| RSP 转换耗时 | ~3.1ms | 向量化 128-bit / 次,8 像素并行 |
| 混合公式 | (IN_RGB * FOG_ALPHA) + (MEMORY_RGB * ONE) |
源色 1/8 + 目标色全量 |
资料来源:https://phoboslab.org/log/2026/05/n64-additive-blending
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。