The QOI File Format Specification
phoboslab.org
phoboslab.org
The reason turned out to be that Deflate's design makes it very hard to parallelise or SIMD-accelerate. Even the best available decoders are basically forced to process a byte at a time, in a straight line from start to finish, which obviously isn't a good match for modern CPUs. The 3x to 4x decompression speedup here is nice, but I can't help but feel a little sad about how poorly this format is taking advantage of available compute resources. (The ultimate dream would be a format which is so parallel-friendly that you can just send the binary blob to the GPU and decompress it in a compute shader!)
Even a rule like "each row is compressed separately, with a table of row lengths at the beginning of the file" might have been enough - this would have made compression ratios slightly worse, but complexity wouldn't have suffered too much. With only six different chunk types, we could perhaps imagine a branchless decoder where each row's decoding state is stored in its own SIMD lane, and the results for several rows are all emitted at the same time...
The proper solution would of course be to use something more modern than deflate, the PNG format even has a metadata field that specifies the used compression algorithm. But anything but deflate wouldn't be backwards compatible, so nobody seems to use that option.
[0] https://www.hackerfactor.com/blog/index.php?/archives/895-Co...
Surely this exists already?
Texture compression can be very advanced: https://cbloomrants.blogspot.com/2020/06/oodle-texture-slash...
It sounds like developers are stuck between a rock and a hard place. They need one of the specific compressed pixel formats which can be efficiently read by the GPU, but those formats are about ten times larger than a similar JPEG file, they don't losslessly compress well (modulo RAD Game Tools' discoveries here), and recompressing raw pixels to a GPU-addressable format at load time would be orders-of-magnitude too slow.
RAD Game Tools' approach here is clever, but it feels like a bit of a hack. The obvious next step would be a lossy compressed image format which can decompress directly to BC7, exploiting spatial-correlation tricks similar to those which PNG uses to get better results than a gzipped BMP file. Has anybody tried this already?
You can't usefully decompress the entire thing on the GPU, but you can do half of it there. You can decompress DEFLATE right before sending the data over the bus and do the PNG prediction (filtering) in a compute shader. I actually implemented this once (PNG prediction is amenable to wavefront parallelism) and it worked fine, though it wasn't any faster than using SIMD on the CPU because the fact that each row can specify a different prediction method means that you end up with a lot of divergence in the shader. Still, it could get some load off the CPU if your loading is CPU-bound.
Example of image with vertical gradients: https://qoiformat.org/benchmark/images/icon_512/apps-prefere... - qoi compresses to 186kb and libpng to 25kb
Example of image with horizontal structure and repeated colors: https://qoiformat.org/benchmark/images/textures_pk/mod_walld... - qoi compresses to 21kb and libpng to 33kb
I could see a variety of space filling curves working for that.
There's already a bunch of minimal programming languages and even some operating systems. It's great value for learning stuff when you can implement a quite OK version of something in dozens of hours or less.
> all data is now encoded as big-endian.
Why? If the goal is to be fast and simple then surely little endian would make more sense given that basically all processors are little endian. Big endian just means you need to swap everything twice.
Edit: Seems like I'm not the only person that thought this was a super weird choice: https://github.com/phoboslab/qoi/issues/36
The reasoning is:
> The file format for QOI will not change anymore.
If I had to nitpick something, I'd say choosing all-big-endian is kinda odd in 2021 but it's probably not a biggy even for something like this.
The only places where it matters is the header's width/height and order of RGB(A) data. Storing RGBA as ABGR is not a great choice, imho. If you want to be endian independent, you have to read one byte at a time anyway (or swap).
Little endian would imply that RGBA is stored as A,B,G,R. Which was my argument against using LE for a file format.
LE vs BE would only affect multi-byte integers and floats, not arrays or structures of single-byte elements.
It was pretty good, but in the end I don't think I'll be making it part of the graphics system. I would really like a bit of random access into the image without having to divide it up into multiple tiles. And the spec took a direction that didn't really suit me - i wanted to use it for Gui-like textures which have slabs of colours and anti-aliased edges with varying alpha. In the final version of the spec there's no way to code "the alpha changed, but the color is the same". without spending 5 bytes on a single pixel. Previously that took 2 bytes.
So I'm still looking for a format with:
* Very small amount of decode state (GIF's 16k is probably too much - PNG's 32k is certainly too much).
* Fast decode (these embedded chips are not as fast as desktops).
* Alpha channel (not just on-off like GIF)
* A bit of random access like the parallel PNG, but hopefully less bolted-on.
The first thing that comes to mind is that it's not very amenable to parallel encoding or decoding, even less so than PNG. The fact that QOI_OP_INDEX could reach arbitrarily far back basically eliminates parallel decompression. But I guess the simplest workaround for that would be to just break up your images into tiles that are each decoded in parallel. You could imagine a simple wrapper format that specifies how to reconstruct a larger image out of such tiles, which would be amenable to parallelism (albeit still not easily implementable on GPU, though I guess you could wring a little bit of parallelism out of it from by using one GPU SIMD lane per RGBA channel).
Of course, it's entirely possible that your decompression goes so fast it doesn't matter that it's sequential and you're bottlenecked on I/O in any reasonable use case…
On my laptop, QOI encodes a test corpus at about 78 MP/s to a density of about 8.0 bits per pixel. Fast png encoding with fpng is 71 MP/s and reaches about 7.8 bpp. Fast lossless jxl encoding can be done at 162 MP/s (single-core, faster with more cores) and reaches about 6.8 bpp. It can also be decoded in parallel, unlike QOI and PNG. It doesn't have a single-page spec though :)
I especially appreciate that the spec is concise (yet clear) enough to fit on a single page and I was able to code a full encoder/decoder that generated bit-for-bit identical results to the reference implementation in just over an hour just by reading that spec.
Edit: as you can see below, the benchmark also checks for correctness so you can disregard this.
[1] https://github.com/phoboslab/qoi/blob/master/qoifuzz.c
[2] https://qoiformat.org/benchmark/
[3] https://github.com/phoboslab/qoi/blob/master/qoibench.c#L440...
* It shouldn't be necessary, since having encoded all pixels already implies you're at the end.
* It complicates the encoding/decoding of the indexing operation.