BLOG

nibnib: A 4bpp Image Format Designed for Six Clock Cycles

Yongliang Talks AI
AI
广告 · Advertisement

What It Is: A Decoding Simple, Encoding Complex Image Format

On October 30, 2026, Raph Levien (GitHub account raphlinus) publicly introduced a new image format and its corresponding Rust reference implementation called nibnib. The one-line description in the repository is restrained: “Image compression at 4bpp with extreme decompression speed.” This means image compression at 4bpp with extremely fast decompression. About 21 minutes after the repository was made public, he posted the information on Hacker News, earning 88 points. By early November 2026, the repository had 58 stars, 2 forks, 0 open issues, no releases, and a total of 10 commits over its history. It operates under both Apache-2.0 and MIT licenses.

The goal of nibnib is not compression ratio or image quality, but one thing: to minimize decoding costs to the utmost. It compresses images to 4 bits per pixel and decodes two pixels from one byte during decoding. The core loop consists of just one line of multiplication and addition. The tradeoff is extreme asymmetry at the encoding end, as the README explicitly states: the current encoder is several orders of magnitude slower than the decoder.

The author deserves more introduction because this project bears his decades of expertise. According to his personal homepage, Levien worked for Google Fonts from December 2020 to October 2025, mainly focusing on GPU font rendering and related technical research. Earlier, he worked on text processing and Rust in the Fuchsia team. He remains active in the Rust community, participating in the Xilem series of projects. He was also a maintainer of Ghostscript, and his doctoral thesis focused on interactive curve design. The Inconsolata font was created by him. Another detail is directly related to nibnib: he developed a high-quality halftone algorithm for inkjet printers called Even Toned Screening. Halftone and error diffusion are technologies from the same family, and the encoder in nibnib is based on Floyd-Steinberg error diffusion. This isn’t a cross-border move; it’s an expert returning to their familiar territory.

Why Now: A Single Microcontroller and a Budget of 6 Clock Cycles per Pixel

The README explains the origin: nibnib came from an experiment where they wanted to see where the limits of image quality and performance were when DVI video output was run on a RP2350 microcontroller.

The RP2350 is a low-cost microcontroller that allows it to directly output DVI video signals, meaning each pixel receives very little computational resources. The conventional approach for such devices is to use low-bit-depth images and lookup tables in the palette. Levien set a clear budget: approximately 6 clock cycles per pixel. What can 6 cycles do? Take pixels, look up in the palette, write to the line buffer—that’s all, with no extra resources.

This constraint almost excludes all mainstream formats. Decoding JPEG requires Huffman decoding, inverse quantization, IDCT, and chroma upsampling; just the IDCT alone exceeds the budget of 6 cycles per pixel. PNG uses DEFLATE, a serial algorithm on the bitstream, which also cannot fit within the budget. With a 6-cycle budget, the only possible decoding method is a linear code without branches, entropy decoding, and only involving table lookups and addition.

nibnib is the answer to this problem. The real-time decoding implementation on RP2350 is included in Levien’s other repository, vic-2350, where the format and target hardware are developed together. Looking at the commit history, this project was primarily written from December 5th to 14th, 2025. Within ten days, Initial version, Gaussian palette, Weight RGB error, Floyd-Steinberg, Semi-structured palette, and Tune error balance were continuously committed. It was then shelved for about ten months, with a line alignment change made on October 22, 2026, and the README was completed and officially released on October 30th. A typical “ten-day experiment, one-year release” scenario.

A user in the HN comments section expanded on the applicability of this asymmetric design: In broadcast-style content distribution, the central source writes once while many terminals read multiple times. The writing end can be slow, but the reading end must be fast—such scenarios naturally benefit from this design. This is not just a specialized technique for single-chip microcontrollers; it’s a general choice that determines where complexity is placed.

How it’s done: delta, pixel pairs, and line multiplication

The core of nibnib lies in three design decisions, each aimed at meeting the 6-cycle budget.

First, the palette stores delta instead of absolute colors. Traditional index-color images (GIF, PNG’s palette mode) store absolute colors, with pixels limited to 256 colors. Smooth gradients require a hard accumulation of color counts. In nibnib, the palette stores the difference between the current pixel and the previous one: after decoding the left pixel, the right pixel becomes “left pixel + delta.” This allows for small incremental changes to cover large areas with soft transitions. The README explains that this makes it superior to conventional palette images in terms of image quality, with soft areas benefiting the most.

Second, one byte decodes two pixels, achieving 4bpp. Decoding progresses by pairing pixels: the last 16-bit decoded pixel is copied into the high and low halves of a 32-bit register. One byte is read from the input stream, a 32-bit delta is obtained from the palette, and then added back. The lower 16 bits of the result are the left pixel, and the upper 16 bits are the right pixel, both in RGB565 format. The core of the inner loop is a single line of code (the Rust snippet in the README):

pix = ((pix >> 16) * 0x10001).wrapping_add(pal[inp as usize]);

Among them, (pix >> 16) * 0x10001 completes “pixel copying,” spreading a 16-bit value across the high and low halves of 32 bits; wrapping_add is an overflow-resistant wrapping addition. The overhead for each pixel pair consists of one byte load, one table lookup, one shift, one multiplication, one addition, and two storage operations, with no conditional branches. The decoding is highly deterministic: any implementation will produce identical output bit by bit for the same byte stream (the number of instructions is my interpretation of the source code, not official statistics).

Thirdly, the file format is simple to the point of raw: 2 bytes wide, 2 bytes high, with 256 32-bit palette items (totaling 1028 bytes of header), followed by data line by line. Each line starts with two RGB565 seed pixels occupying 4 bytes, followed by (width - 2) / 2 index bytes, with the line spacing filled to a multiple of 4 bytes. There is no version number, no checksum, and no metadata. The README states clearly: this format is not designed for archiving; the most likely use is for close integration with decoders.

The encoding side is another world. The README explains clearly: “nibnib is defined by the decoding process”—decoding is deterministic and simple, while encoding is essentially a search to find the best match with the original image among all possible encoding results. The search has two dimensions.

One is the selection of the palette. The current version of the palette has a fixed structure: 133 cube items plus 123 random items. The cube part uses geometric combinations of 3×5×3 on three delta scales (1, 2, 4), ensuring that it is accessible near 0 and throughout the full range without overflowing. The README explicitly states that removing the cube part will result in poor artifacts, and the repository includes comparison images of pure random palettes and cube+random encodings. The random part uses weighted Gaussian sampling with logarithmic均匀分布 scales; the error metric is 0.3r² + 0.7g² + 0.1b², with the green channel weight being more than twice that of red and blue, corresponding to the human eye’s sensitivity to green (these constants come from the encode.rs source code). The README also openly admits that this random process was ad hoc-arranged based on reasonable values, and that the palette currently does not adapt to input images at all. The author personally notes this as the most clear direction for improvement.

The second step is to select a codeword using Floyd-Steinberg error diffusion. For each pixel pair, the encoder tries all 256 palette items to find the one with the smallest decoding error. Then, the error is diffused to three positions in the right neighbor and the next row using the classic weights of Floyd-Steinberg (7/16, 1/16, 5/16, 3/16). The goal of subsequent pixel fitting is not the original value but the original value plus the cumulative error. The difference from classical error diffusion is that the classic method processes one pixel at a time, while nibnib produces two pixels at once. Therefore, the codeword chosen is the one that minimizes the combined error of the two pixels, and the weight of the sum of the errors is higher than the difference, modeling the fact that the human eye is more sensitive to low spatial frequencies. A user on HN (zahlman) questioned whether this was stretching the definition of “compression”; it’s just delta filtering plus palette quantization. However, he was also surprised by its effectiveness because intuitively, delta sequences would accumulate errors. The reason the error does not get out of control lies in the fitting goal: each step of the encoder incorporates the cumulative error into the target, continuously digesting drift (this half-sentence is my inference from the source code).

Greedy search has a known cost. The README states that current artifacts tend to trail on the right side of sharp edges. The author provided directions for improvement: change the encoding order from right to left, or even in a snake pattern (alternating directions per row), because addition steps are actually reversible in practice; or perform deeper searches. When the error of two choices for this pixel is comparable, one might make it much easier for the next pixel. These improvements will slow down the encoder, but the decoding speed remains unchanged. The README’s original words are: All improvements can be achieved on the existing decoder.

## Actual Measurements and Data: No numbers were provided in the repository; I ran it once

There are no benchmarks in the nibnib repository, which was directly pointed out in the HN comments section: User augment_me felt that emphasizing extreme decoding speed without providing numbers for comparison with JPEG was unreasonable. Another user, DoctorOetker, responded that the purpose is precisely to target platforms where JPEG cannot be decoded in real time.

I compiled the reference implementation on my own machine (Apple silicon Mac, Rust 1.96, release build) and ran the example image flowers7.png provided by the repository (1000×750, RGB 8bit). All the numbers below were measured on my own machine; they are not official statistics:

- The encoded output is 379,028 bytes, which strictly matches the format formula 1028 + 750 × 504. nibnib uses fixed-length encoding without entropy coding, and the output size is determined solely by the image size.
- The effective bit depth is 4.043 bpp (the overhead due to the color palette, seed pixels per line, and line alignment is slightly higher than the nominal 4bpp). It is approximately 3.96 times that of raw RGB565 storage and 5.94 times that of RGB888. The PNG file in the repository is 792,470 bytes, but since PNG is lossless, their compression ratios are not comparable.
- Image quality: The PSNR of the decoded output compared to the original image is about 36.5 dB. Subjectively, the jitter noise in the smooth areas is most noticeable, and there is a trailing effect on the right side of the sharp edges, consistent with the description in the README.
- Time: The full command-line encoding takes about 0.5 seconds on user time, while decoding and writing to PNG takes about 0.12 seconds on wall time. These numbers indicate that PNG I/O and CLI overhead account for most of the time, and the desktop CPU is not its target platform; these are merely for reference in terms of magnitude.

Two structural observations are more useful than numbers. First, fixed-length encoding with line alignment means that encoding can occur while decoding is done, without waiting for the entire file to arrive. This aligns with the role of the “on-the-fly decoder” in vic-2350. Second, all costs at the encoding end can be paid offline: once encoded on a desktop machine, only the decoding cost is incurred on the terminal device. Raphlinus has responded to a comment on HN (where someone pointed out that code optimized for RP2350 is almost certainly not suitable for modern desktop cores with deep pipelines and wide SIMD), and his answer was: “Absolutely. This is optimized for highly resource-constrained devices, not modern machines.” Optimized for highly resource-limited devices, not modern machines.

## Horizontal comparison: JPEG, Amiga HAM, and BCn

A comparison with JPEG helps to clarify the design philosophy of nibnib. The README includes a positive comparison: JPEG’s DCT transformation is mathematically reversible, quantization is easy, and encoding and decoding speeds are roughly equal at baseline levels, with search used only for fine-tuning parameters and improving image quality. nibnib is completely the opposite: decoding is minimal, and encoding is a search problem several orders of magnitude slower. Both are essentially two answers to the same question—where to place complexity. JPEG symmetrically places complexity on both ends, resulting in versatility and an ecosystem; nibnib pushes all complexity to the encoding end, achieving the lowest decoding cost on terminals. The tradeoff is that the upper limit of image quality at the same bit depth is lower than JPEG’s: it has only 256 delta color palettes and 565 colors, and no frequency-domain transformations are available (this is my judgment based on format specifications, not actual comparative tests).

HN’s comment section unearthed its old relative: Two users (wiml, vardump) pointed out that nibnib resembles the HAM (Hold-And-Modify) display mode of Amiga. According to the parameters provided in the comments, HAM uses 6 input bits on OCS chips to display 12-bit 4096 colors, while later AGA chips use 8 bits for 262144 colors. The idea is also “carry the information of the previous pixel and only transmit minor changes.” The horizontal streaky artifact is similar. The difference is that HAM is a display mode, solving the color limit problem; nibnib is a storage format, solving the decoding speed limit problem, and delta acts on all three RGB channels, processing two pixels at once. A technique from 1985 for home computers became an engineering option again forty years later on single-chip DVI outputs. This association itself is very interesting.

Another topic is the GPU texture compression BCn series. augment_me mentioned in the comments that BCx essentially does the same thing as codebook/palette, and allows interpolation between two endpoints, with more equivalent colors and higher efficiency. nibnib does not do interpolation; 256 32-bit deltas are all that’s needed, resulting in no need for interpolation logic in the decoding inner loop. Quality per bit gives way to period per pixel, with a clear choice made.

Finally, let’s isolate the philosophy of “the format is defined by the decoding process.” This was not invented by nibnib; JPEG and PNG specifications also follow the decoding side, leaving freedom for the encoding side, but nibnib took it to an extreme: because the decoder is simple enough, there is great room for improvement in the encoder, and everything is backward compatible. The adaptive palette, deeper search, and snake-like scanning envisioned in the README do not require changing a single byte of the existing decoder. For an experimental format with no ecosystem yet, it’s correct engineering order to lock down the decoding side first.

## How to Use and Calmly Analyze

The usage is simple. The repository is a standard Rust project, with only 3 files and 351 lines of code (main.rs, decode.rs, encode.rs). After running cargo build --release, you get a command-line tool:

```bash
nibnib input.png output.nib -c    # Compress
nibnib output.nib decoded.png -d  # Decode back to PNG

Input only supports PNG; without -c or -d, it will encode once and then decode immediately, making it convenient to directly compare quality. For RP2350 DVI output, use the image demo from the vic-2350 repository for the real-time decoder.

Calmly analyzing is more important than using it; let me explain my judgment item by item:

First, maturity is just an experiment. 10 commits, single-person project, no release, no crate published, no C implementation, no support from any image library. It’s suitable as a learning sample or material for self-made embedded projects. At this stage, it’s not recommended to put it into any production environment, as its ecological accumulation is not on the same level as PNG and JPEG.

Secondly, its applicability is limited, as the author himself explains clearly. nibnib serves scenarios where the decoding budget is measured in clock cycles and the materials can be pre-processed offline: UI resources for embedded devices, retro handheld game screens, and image materials for microcontroller video output fall into this category; photo archiving, web distribution, and printing materials do not. The README states directly: This format was not designed for archiving.

Thirdly, the upper limit of image quality is not high, and the encoder has yet to reach its own limit. 4bpp with a fixed non-adaptive palette means that complex color images will inevitably have jitter noise and edge trailing; two main improvement directions listed by the author (adaptive palette, deeper search) have not been implemented. The README also describes the current situation honestly: “What’s presented is a basic, working image compression technique.” A basic, functional version.

Fourthly, don’t overestimate its popularity. 58 stars, 88 points, represent ‘interesting ideas’ level of popularity, not ‘new standard’ level. The attention it receives comes from a clean design approach and the author’s experience in the graphics community; whether it will be used tomorrow is another matter.

My conclusion in one sentence: The value of nibnib lies not in adding an optional image format, but in demonstrating how a format design prioritizing decoding cost can achieve high purity—fixing everything at the decoding end, shifting all complexity to the encoding end, and letting the format be defined by the decoding process. For engineers working with embedded graphics, retro hardware, or video output toys, these 351 lines of Rust are worth reading line by line; for other readers, remembering this design philosophy is enough.

References

Author: Yongliang

广告 · Advertisement

Frequently Asked Questions

What is nibnib?

nibnib is a 4bpp image format introduced by Raph Levien, designed for fast image decoding with a focus on minimizing decoding costs.

How does nibnib achieve its fast decoding speed?

nibnib achieves fast decoding speed by compressing images to 4 bits per pixel and decoding two pixels from one byte, using a simple multiplication and addition core loop.

What is the tradeoff of nibnib's encoding process?

The tradeoff of nibnib's encoding process is that it is deliberately asymmetric, with the encoder being significantly slower than the decoder, as the encoding process is more complex.