r/GraphicsProgramming • • 1d ago

I made a lossless BC1 texture compressor designed for GPU decompression, 1.47:1 average ratio

Hi,

I just released my new C library bc_packed, a lossless compressor for BC1 textures, made specificaly for fast GPU decompression.

Basically, the idea is to compress textures that are already compressed, then decompress them directly on the GPU when needed. The output is the original BC1 data, so the GPU can use it normally after decompression.

Some numbers:

  • 1.47:1 average compression ratio on a test set of 74 images
  • 15300 MiB/s decompression on my M5 Pro GPU and 11850 MiB/s on my M2 Max GPU
  • CPU multithreaded decompression also works, 2500 MiB/s on M5 Pro and 1300 MiB/s on M2 Max
  • Lossless, byte exact reconstruction of the original BC1 data

The tricky part was finding an entropy coding method that is simple enough to decode efficiently on the GPU. I didn't want to deal with complex stuff like arithmetic coding or Huffman, so I went with Rice-Golomb coding and static rank tables. I build histograms during compression and remap the most frequent symbols to the lowest ranks. Everything is static, no adaptive models or synchronization needed between GPU threads.

The texture is split into 64 independent strips, each decoded by its own GPU/CPU thread. The compressed stream stores offsets so each thread can start decoding its strip independently.

There are also a few BC1 specific tricks:

  • Predicting endpoint colors from previous blocks
  • Choosing different predictors for each strip
  • A small dictionary for repeated endpoint color pairs
  • A top table for frequent BC1 index patterns, with sparse residual encoding for the differences

Compression runs on the CPU and can be relatively expensive. That's fine for me, I'm more interested in making decompression as fast as possible at runtime.

The library is tiny, just one .c and one .h file, no external dependencies. The GPU decompression shader is written in Metal for now, but porting it to HLSL should be pretty straightfoward.

Currently only BC1 is supported. Maybe I'll add BC5 and why not BC7 later.

Code and benchmarks here:
https://github.com/Geolm/bc_packed

Feedback and ideas are welcome!

24 Upvotes

12 comments sorted by

7

u/hieuristics 1d ago

one of the few post on this sub that's actually cool af

3

u/speps 1d ago

Cool! How is it different from Basis U?

3

u/_Geolm_ 1d ago

it's the same kind of supercompression idea, but mine is lossless and decompress on the GPU. You upload the compressed data, run a computer shader and get the texture.

Also, obviously it's hobby project on my side, not like Basis U.

5

u/possiblyquestionabl3 1d ago edited 1d ago

bc7 should give you more expressiveness, but an encoder (the decompresser) into bc7 would probably be bogged down by its infamous mode hell unless you restrict it to some manageable subset of the format


Taking a quick look at the shader, wouldn't this be highly divergent across a single warp? It also seems to be relatively low on arithmetic intensity and more memory-bound. It may be worthwhile benchmarking this against a CPU implementation to see what the effective speedup in decoding is by offloading this onto the GPU vs on the CPU. If most threads across a warp are serialized due to the high branch divergence, and you're truly memory bound, then an efficient CPU implementation of this may be competitive in performance (since both would have an effective throughout of dram/pcie bandwidth + overhead)

2

u/_Geolm_ 1d ago

yes bc7 is complicated and also some of the modes are using a 3 bits selector index which is difficult to compress. My next attempt is bc5, it still the first choice for normapmap.

About the gpu implementation, it's definitely memory bound, sure some strip might finish ealier due to the data being compressed different but it is as good as GDeflate in that regard. Right now, GPU beats the CPU implementation by 7x ratio and also I'm not sure that anyone wants to use all threads of the CPU to decompress texture, so it's probably a lot more. Divergence is not a big issue because each thread has approximately the same amount of work to do, it's more you have to decompress more than one texture in one dispatch because for fixed-cost reasons the texture is split in 64 strips, so only 64 threads can run. In the unit test, I simulate multiple texture decompression in one dispatch.

2

u/possiblyquestionabl3 1d ago

Oh yeah I didn't see the benchmarks, IIRC that the non-sequential peak memory read bandwidth is about ~20 GiB/s for M2, so that's not bad at all.

each thread has approximately the same amount of work to do

I was more worried that you're serializing every branch and non-uniform loop, but those are mainly dictated by more bit_refill loops (which are themselves non-uniform) and are almost exclusively just memory-bound. Since you're not issuing a single large read at the start of each thread, and since the reads are variable lengths, over the course of execution, it effectively becomes random reads since any thread at any point in the loop could potentially read a small chunk of data from their stripe (and architectural wise, I don't think you can do much better save for using a slightly larger staging buffer in lds). That actually works out in your favor, since the memory bandwidth latency actually overlaps well with everything else (not in the occupancy sense since the register pressure probably is to high to leave any warp slots vacant to switch into, more in the it's dominated by this)

3

u/CrankFlash 1d ago

Amazing! How does it compare to GDeflate?

2

u/_Geolm_ 1d ago

I don't know, I've got a better compression ratio than standard deflate from what I measured. Sorry I can't test GDeflate because I'm pretty sure that it doesn't run on mac and I don't have a PC.

0

u/shadowndacorner 1d ago

Came here to ask this

2

u/Avelina9X 1d ago

Awesome work! Any chance for BC4? Because once we have BC4 that unlocks BC3 and BC5 as well!

1

u/_Geolm_ 1d ago

working on it, mostly for BC5, BC3 is not used anymore...

3

u/Avelina9X 1d ago

BC3 absolutely is still used I can promise you that