BLOG
nibnib:为 6 个时钟周期设计的 4bpp 图像格式
是什么:一个解码极简、编码极繁的图像格式
2026 年 10 月 30 日,Raph Levien(GitHub 账号 raphlinus)公开了一个新的图像格式和配套的 Rust 参考实现,名字叫 nibnib。仓库的一句话描述很克制:「Image compression at 4bpp with extreme decompression speed」,4bpp 的图像压缩,解压速度极端快。仓库公开约 21 分钟后,他把帖子发上了 Hacker News,拿到 88 points。截至 2026 年 11 月初,仓库有 58 star、2 fork、0 个 open issue、没有任何 release,全部历史 10 个 commit,Apache-2.0 OR MIT 双许可。
nibnib 的目标不是压缩比,也不是画质,而是一件事:解码成本压到极限。它把图像压到每像素 4 bit,解码时从一个字节里一次还原两个像素,核心内循环只有一行乘加。代价是编码端极端不对称,README 自己写明:当前编码器比解码器慢几个数量级。
作者值得多介绍两句,因为这个项目带着他几十年的手艺印记。据他个人主页,Levien 在 2020 年 12 月到 2025 年 10 月间供职于 Google Fonts 团队,主要做 GPU 字体渲染及相关技术研究;更早在 Fuchsia 团队做文本处理和 Rust;至今仍活跃在 Rust 社区,参与 Xilem 系列项目;他当过 Ghostscript 的维护者,博士论文做的是交互曲线设计,Inconsolata 字体出自他手。还有一个细节和 nibnib 关系直接:他做过一套面向喷墨打印机的高质量半色调算法,叫 Even Toned Screening。半色调和误差扩散抖动是同一家族的技术,nibnib 的编码器正是搭在 Floyd-Steinberg 误差扩散上的。这不是转身跨界,是老手回到主场。
为什么现在:一颗单片机和每像素 6 个时钟周期的预算
README 开头交代了起源:nibnib 来自一个实验,想看 DVI 视频输出跑在 RP2350 单片机上时,画质和性能的极限在哪里。
RP2350 是低成本单片机,让它直接输出 DVI 视频信号,意味着每个像素能分到的计算资源少得可怜。这类设备的常规做法是低比特深度图像加调色板查表。Levien 给出了明确预算:每像素大约 6 个时钟周期。6 个周期能干什么?取像素、查调色板、写入行缓冲,仅此而已,没有富余。
这个约束几乎排除了所有主流格式。解 JPEG 要做 Huffman 解码、反量化、IDCT、色度上采样,光 IDCT 一项就远超每像素 6 周期的预算;PNG 用的 DEFLATE 是位流上的串行算法,同样塞不进去。在 6 周期预算下,解码只能是一种形态:无分支、无熵解码、只有查表和加法的直线代码。
nibnib 就是这道题的答案。RP2350 上的实时解码实现放在 Levien 另一个仓库 vic-2350 的 image demo 里,格式和目标硬件是一起长出来的。从 commit 历史看,这个项目主体写于 2025 年 12 月 5 日到 14 日,十天内连续提交 Initial version、Gaussian palette、Weight RGB error、Floyd-Steinberg、Semi-structured palette、Tune error balance,之后搁置了约十个月,2026 年 10 月 22 日补了一个行对齐改动,10 月 30 日写完 README 正式公开。典型的「十日实验,一年后发布」。
HN 评论区有位用户把这种不对称设计的适用面说得更宽:在广播式的内容分发里,中心源写一次、众多终端读多次,写端可以慢、读端必须快,这类场景天然吃这种红利。这不只是单片机的专用技巧,而是一种把复杂度往哪边放的通用取舍。
怎么做的:delta、像素对与一行乘加
nibnib 的核心是三个设计决定,每一个都对着 6 周期预算。
第一,调色板存 delta,不存绝对色。传统索引色图像(GIF、PNG 的 palette 模式)的调色板存绝对颜色,像素只能取 256 个颜色之一,平滑渐变要靠颜色数量硬堆。nibnib 的调色板存的是相对上一个像素的增量:解出左像素后,右像素等于「左像素 + delta」。这样少量小增量就能铺满大片柔和区域。README 的说法是,这让它在画质上超越了普通调色板图像,软区域受益最大。
第二,一个字节解两个像素,凑出 4bpp。解码按像素对推进:把上一个已解码的 16 位像素复制进 32 位寄存器的高低两半,从输入流读一个字节,查调色板得到一个 32 位 delta,回绕相加,结果的低 16 位是左像素、高 16 位是右像素,都是 RGB565 格式。内循环的核心就一行(README 里的 Rust 片段):
pix = ((pix >> 16) * 0x10001).wrapping_add(pal[inp as usize]);
其中 (pix >> 16) * 0x10001 完成「像素复制」,把 16 位值铺到 32 位的高低两半;wrapping_add 是不怕溢出的回绕加法。每个像素对的开销是一次字节载入、一次查表、一次移位、一次乘法、一次加法、两次存储,没有条件分支。解码的确定性强:任何实现对着同一段字节流都会得到逐位相同的输出(指令数是我对源码的判读,不是官方统计)。
第三,文件格式简单到近乎裸奔:2 字节宽、2 字节高、256 个 32 位调色板项(合计 1028 字节头),然后逐行数据。每行以两个 RGB565 种子像素开头占 4 字节,后面跟 (width - 2) / 2 个索引字节,行距补齐到 4 字节的倍数。没有版本号,没有校验和,没有任何 metadata。README 直说:这个格式不为归档设计,更可能的用法是和解码器紧密绑定。
编码端则是另一个世界。README 讲得很清楚:「nibnib 由解码过程定义」——解码确定而简单,编码本质是一场搜索,在全部可能的编码结果里找一个解码出来最像原图的。搜索有两个维度。
一是选调色板。当前版本的调色板是固定结构:133 个 cube 项加 123 个随机项。cube 部分在三个 delta 尺度(1、2、4)上各取 3×5×3 的几何组合,作用是保证 0 附近和满量程都够得着、不会溢出。README 明确说,去掉 cube 部分会得到糟糕的伪影,仓库里附了纯随机调色板和 cube+随机的编码对比图。随机部分用加权高斯采样,尺度取对数均匀分布;误差度量是 0.3r² + 0.7g² + 0.1b²,绿色通道权重约为红蓝的两倍多,对应人眼对绿色的敏感(这些常数出自 encode.rs 源码)。README 也坦率承认:这套随机过程是按合理值的猜测 ad-hoc 调出来的,而且调色板目前完全不随输入图像自适应,作者自己标注这是最明确的改进方向。
二是选 codeword,用 Floyd-Steinberg 误差扩散。对每个像素对,编码器把 256 个调色板项全部试一遍,选解码误差最小的;然后把误差按 Floyd-Steinberg 的经典权重(7/16、1/16、5/16、3/16)扩散给右邻和下一行的三个位置;后续像素拟合的目标不是原始值,而是原始值加上累积误差。和经典误差扩散的差别在于:经典方法一次一个像素,nibnib 一次产出两个,所以选的是让两个像素联合误差最小的 codeword,并且两个像素误差之和的权重高于两者之差,建模的是人眼对低空间频率更敏感这个事实。HN 上有用户(zahlman)质疑这是在拉伸「压缩」的定义,delta 滤波加调色板量化而已,但他同时惊讶于它效果不错,因为按直觉 delta 串会累积误差。误差没有失控的原因就在拟合目标上:编码器每一步都把累积误差纳入目标,漂移被持续消化掉(这半句是我对源码的推断)。
贪心搜索有已知代价。README 写明,当前伪影会拖尾在锐利边缘的右侧。作者给了改进方向:把编码顺序改成从右到左,甚至蛇形(每行交替方向),因为加法步骤在实践上是可逆的;或者做更深的搜索,这个像素的两个选择误差相当时,其中一个可能让下一个像素好办得多。这些改进都会让编码器更慢,但解码速度纹丝不动,README 的原话是:所有改进都可以在现有解码器上完成。
实测与数据:仓库没给数字,我跑了一遍
nibnib 仓库里没有任何 benchmark,这一点在 HN 评论区被直接点了名:用户 augment_me 认为,强调极致解码速度却不给对比 JPEG 的数字说不过去;另一位用户 DoctorOetker 回应说,意义恰恰在于瞄准某些情况下无法实时解码 JPEG 的平台。
我在自己的机器(Apple silicon Mac,Rust 1.96,release 构建)上编译了参考实现,跑了仓库自带的示例图 flowers7.png(1000×750,RGB 8bit)。以下数字全部是我本机测的,不是官方统计:
- 编码输出 379,028 字节,和格式公式 1028 + 750 × 504 严格一致。nibnib 是定长编码,没有熵编码环节,输出大小只由图像尺寸决定。
- 有效码率 4.043 bpp(调色板、每行种子像素和行对齐带来的开销略高于名义 4bpp)。相对 RGB565 裸存储约 3.96 倍,相对 RGB888 约 5.94 倍。仓库里那张 PNG 是 792,470 字节,但 PNG 无损,两者的压缩比不构成可比关系。
- 画质:解码输出对比原图的 PSNR 约 36.5 dB。主观看,平滑区域的抖动噪声最显眼,锐利边缘右侧有拖尾,与 README 的自述一致。
- 时间:命令行全程编码约 0.5 秒 user 时间,解码加写出 PNG 约 0.12 秒 wall 时间。这两个数字里 PNG I/O 和 CLI 开销占大头,桌面 CPU 也不是它的目标平台,只作量级参考。
两个结构性观察比数字更有用。其一,定长加行对齐意味着可以边收边解,不必等完整文件到齐,这与 vic-2350 里「on-the-fly 解码器」的定位吻合。其二,编码端的全部成本都可以离线付掉:桌面机器上编一次,终端设备只付解码的钱。raphlinus 本人在 HN 回复过一条评论(对方指出为 RP2350 优化的代码几乎必然不适合深流水线、宽 SIMD 的现代桌面核),他的回答是:「Absolutely. This is optimized for highly resource constrained devices, not modern machines.」为高度资源受限的设备优化,不为现代机器优化。
横向对比:JPEG、Amiga HAM 与 BCn
和 JPEG 的对照最能看清 nibnib 的设计哲学。README 里有一段正面比较:JPEG 的 DCT 变换在数学上可逆,量化也容易,baseline 下编码和解码速度大致相当,搜索只用来微调参数、榨一点画质。nibnib 完全反过来:解码极简,编码是慢几个数量级的搜索问题。两者其实是同一个问题的两种答案,即把复杂度放在哪一端。JPEG 把复杂度对称地放在两端,换来通用性和生态;nibnib 把复杂度全部推到编码端,换来终端上最低的解码成本。代价是同等码率下画质上限低于 JPEG:它只有 256 项 delta 调色板和 565 色深,没有频域变换可用(这句是我的判断,依据是格式规格,不是实测对比)。
HN 评论区翻出了它的老亲戚:两位用户(wiml、vardump)指出 nibnib 神似 Amiga 的 HAM(Hold-And-Modify)显示模式。按评论区给出的参数,HAM 在 OCS 芯片组上用 6 个输入位呈现 12-bit 的 4096 色,后来的 AGA 芯片组用 8 位呈现 262144 色,思路同样是「携带上一像素的信息,只传小改动」,横向条纹状的伪影也类似。区别在于 HAM 是显示模式,解决的是色数限制;nibnib 是存储格式,解决的是解码速度限制,而且 delta 同时作用于 RGB 三通道、一次处理两个像素。1985 年家用机上的技巧,四十年后在单片机 DVI 输出上重新成为工程选项,这个联想本身很有意思。
另一个坐标是 GPU 纹理压缩 BCn 系列。augment_me 在评论区提到,BCx 本质上做的是同一类 codebook/palette 的事,而且允许在两个端点之间插值,等效色数更大、更高效。nibnib 不做插值,256 个 32 位 delta 就是全部,换来的是解码内循环里连插值逻辑都不需要。画质每比特让位于周期每像素,取舍清晰。
最后把「格式由解码过程定义」这条哲学单独拎出来。这不是 nibnib 的发明,JPEG、PNG 的规范同样以解码端为准、给编码端留自由,但 nibnib 把它推到了极端:因为解码器足够简单,编码器的改进空间极大,而且全部向后兼容。README 设想的自适应调色板、更深搜索、蛇形扫描,都不需要动现有解码器一个字节。对一个还没有生态的实验性格式,先钉死解码端是正确的工程顺序。
怎么用与冷静面
用法很轻。仓库是标准 Rust 工程,源码只有 3 个文件共 351 行(main.rs、decode.rs、encode.rs),cargo build —release 之后得到一个命令行工具:
nibnib input.png output.nib -c # 压缩
nibnib output.nib decoded.png -d # 解码回 PNG
输入只支持 PNG;不带 -c 或 -d 时会做一次编码后立即解码,方便直接对比画质。做 RP2350 DVI 输出的话,实时解码器去 vic-2350 仓库的 image demo 里拿。
冷静面比用法重要,逐条说我的判断:
第一,成熟度就是实验品。10 个 commit、单人项目、无 release、没发 crate、没有 C 实现、没有任何图像库支持。把它当学习样本或自制嵌入式项目的素材都合适,现阶段不建议放进任何生产环境,它和 PNG、JPEG 的生态积累不在一个量级上。
第二,适用面窄,作者自己说得明白。nibnib 服务的是终端解码预算以时钟周期计、素材又能离线预处理好的场景:嵌入式设备的 UI 资源、复古掌机贴图、单片机视频输出的图像素材属于这一类;照片存档、网页分发、印刷素材都不属于。README 原话:这个格式不为归档设计。
第三,画质上限不高,而且编码器还没摸到自己的上限。4bpp 加固定非自适应调色板,意味着复杂彩色图像必然带抖动噪声和边缘拖尾;作者列出的两个主要改进方向(自适应调色板、更深搜索)都还没做。README 对现状的定位也很诚实:「What’s presented is a basic, working image compression technique.」一个基础的、能工作的版本。
第四,别把热度数字读过头。58 star、88 points,是「有意思的想法」级别的热度,不是「新标准」级别的热度。它收获的关注来自干净的设计思路和作者在图形圈的积累,和它明天会不会被人用是两回事。
我的结论一句话:nibnib 的价值不在于多了一个可选的图像格式,而在于它示范了一个解码成本优先的格式设计能做到多干净——先钉死解码端,把全部复杂度推到编码端,让格式由解码过程定义。对做嵌入式图形、复古硬件或者视频输出玩具的工程师,这 351 行 Rust 值得逐行读;对其他读者,记住这条设计哲学就够了。
参考来源
- nibnib 仓库与 README:https://github.com/raphlinus/nibnib(58 star / 10 commits,2026-11-02 口径)
- 源码:src/main.rs、src/decode.rs、src/encode.rs
- HN 讨论(88 points,2026-10-30):https://news.ycombinator.com/item?id=50299811
- 作者主页:http://levien.com/
- vic-2350 仓库:https://github.com/raphlinus/vic-2350
署名:永亮