# Fastest Software Gamma=2.2 for RGB?

With gamma=2.0 you can use that Carmack's inverse sqrt hack or a lookup table. Yet all standard graphic formats mandate the use of gamma=2.2, and SVGA framebuffer also requires it. Unfortunately, for gamma=2.2 a linear lookup table doesn't fit into CPU cache (it is a whooping 192 kilobytes). So should one work with gamma=2.2 directly or convert it into gamma=2.0?

The question of course assumes that everything is done in software, because modern GPUs support gamma=2.2 in hardware. I want to avoid using OpenGL, because it is non-portable unstable API, and a total overkill for a simple 2d indie game.

• My CPU is from 10 years ago and it has a 1MB L2 and 6MB L3 cache. Why wouldn't it fit? – Bálint Aug 7 '19 at 22:45
• You must be confusing something. I've Intel core i5, which has just 64 KB of L1 cache, and 256 KB of L2 (per core). It indeed has several MB of L3 cache. And accessing L3 cache is about 23 times slower than L1 cache. I.e. difference between L1 and L3 is the difference between 1 frame per second and 23 frames per second. – SmugLispWeenie Aug 7 '19 at 23:22
• 192Kb < 256KB however I look at it – Bálint Aug 8 '19 at 8:00
• Have you actually benchmarked these? It should be very quick and easy to set up a comparison of lookup table vs just calculating it directly; you might be surprised by the result. – Maximus Minimus Aug 8 '19 at 8:39
• I just want to mention that the whole point around OpenGL is to be portable, and it can increase performance even on 2D games. Most (cross platform) libraries out there that focus on 2D games use OpenGL in the background. – TomTsagk Aug 8 '19 at 8:58

## 1 Answer

I think I have found a solution to compress any large gamma magnitude back to 8-bits.

First one has to convert said magnitude to a custom floating point format, and then use it as an index into a small lookup table.

Loss of precission is not a huge problem, because values are mapped to 8-bit RGB components anyway.

Here is the code for a custom 16-bit float:

#define CFP_MANTISSA_BITS  11
#define CFP_MANTISSA_MASK  ((1<<CFP_MANTISSA_BITS)-1)
#define CFP_EXPONENT_BITS  (16-CFP_MANTISSA_BITS)
#define CFP_POINT (CFP_MANTISSA_BITS-1)
uint16_t int2cfp(uint32_t n) {
int e = log2(n) - CFP_POINT; //log2 is basically x86 BSF's or ARM's CLZ
if (e < 0) return n; //can be replaced by a table
return (e<<CFP_MANTISSA_BITS) | (n>>e);
}

uint32_t cfp2int(uint16_t n) {
return ((uint32_t)n&CFP_MANTISSA_MASK)<<(n>>CFP_MANTISSA_BITS);
}


That way the size of a lookup table can be made to match cache size perfectly.

Half-floats can also be used for that purpose, but they have a max value of (float)0xFFFF, so one must normalize them before lookup. Both ARM and x86 have fast float-to-half float conversion instructions, which can be accessed from C/C++ using the following code:

#include <smmintrin.h>
__attribute__ ((__target__("f16c"))) uint16_t x86_f2h(float value)
{
return _mm_cvtsi128_si32(_mm_cvtps_ph(_mm_set_ss(value), _MM_FROUND_CUR_DIRECTION));
}