Complete conversion from single precision to half precision. This is a direct copy from my SSE version, so it's branch-less. It makes use of the fact that in GCC (-true == ~0), may be true for VisualStudio too but, I don't have a copy.
class Float16Compressor
{
union Bits
{
float f;
int32_t si;
uint32_t ui;
};
static int const shift = 13;
static int const shiftSign = 16;
static int32_t const infN = 0x7F800000; // flt32 infinity
static int32_t const maxN = 0x477FE000; // max flt16 normal as a flt32
static int32_t const minN = 0x38800000; // min flt16 normal as a flt32
static int32_t const signN = 0x80000000; // flt32 sign bit
static int32_t const infC = infN >> shift;
static int32_t const nanN = (infC + 1) << shift; // minimum flt16 nan as a flt32
static int32_t const maxC = maxN >> shift;
static int32_t const minC = minN >> shift;
static int32_t const signC = signN >> shiftSign; // flt16 sign bit
static int32_t const mulN = 0x52000000; // (1 << 23) / minN
static int32_t const mulC = 0x33800000; // minN / (1 << (23 - shift))
static int32_t const subC = 0x003FF; // max flt32 subnormal down shifted
static int32_t const norC = 0x00400; // min flt32 normal down shifted
static int32_t const maxD = infC - maxC - 1;
static int32_t const minD = minC - subC - 1;
public:
static uint16_t compress(float value)
{
Bits v, s;
v.f = value;
uint32_t sign = v.si & signN;
v.si ^= sign;
sign >>= shiftSign; // logical shift
s.si = mulN;
s.si = s.f * v.f; // correct subnormals
v.si ^= (s.si ^ v.si) & -(minN > v.si);
v.si ^= (infN ^ v.si) & -((infN > v.si) & (v.si > maxN));
v.si ^= (nanN ^ v.si) & -((nanN > v.si) & (v.si > infN));
v.ui >>= shift; // logical shift
v.si ^= ((v.si - maxD) ^ v.si) & -(v.si > maxC);
v.si ^= ((v.si - minD) ^ v.si) & -(v.si > subC);
return v.ui | sign;
}
static float decompress(uint16_t value)
{
Bits v;
v.ui = value;
int32_t sign = v.si & signC;
v.si ^= sign;
sign <<= shiftSign;
v.si ^= ((v.si + minD) ^ v.si) & -(v.si > subC);
v.si ^= ((v.si + maxD) ^ v.si) & -(v.si > maxC);
Bits s;
s.si = mulC;
s.f *= v.si;
int32_t mask = -(norC > v.si);
v.si <<= shift;
v.si ^= (s.si ^ v.si) & mask;
v.si |= sign;
return v.f;
}
};
So that's a lot to take in but, it handles all subnormal values, both infinities, quiet NaNs, signaling NaNs, and negative zero. Of course full IEEE support isn't always needed. So compressing generic floats:
class FloatCompressor
{
union Bits
{
float f;
int32_t si;
uint32_t ui;
};
bool hasNegatives;
bool noLoss;
int32_t _maxF;
int32_t _minF;
int32_t _epsF;
int32_t _maxC;
int32_t _zeroC;
int32_t _pDelta;
int32_t _nDelta;
int _shift;
static int32_t const signF = 0x80000000;
static int32_t const absF = ~signF;
public:
FloatCompressor(float min, float epsilon, float max, int precision)
{
// legal values
// min <= 0 < epsilon < max
// 0 <= precision <= 23
_shift = 23 - precision;
Bits v;
v.f = min;
_minF = v.si;
v.f = epsilon;
_epsF = v.si;
v.f = max;
_maxF = v.si;
hasNegatives = _minF < 0;
noLoss = _shift == 0;
int32_t pepsU, nepsU;
if(noLoss) {
nepsU = _epsF;
pepsU = _epsF ^ signF;
_maxC = _maxF ^ signF;
_zeroC = signF;
} else {
nepsU = uint32_t(_epsF ^ signF) >> _shift;
pepsU = uint32_t(_epsF) >> _shift;
_maxC = uint32_t(_maxF) >> _shift;
_zeroC = 0;
}
_pDelta = pepsU - _zeroC - 1;
_nDelta = nepsU - _maxC - 1;
}
float clamp(float value)
{
Bits v;
v.f = value;
int32_t max = _maxF;
if(hasNegatives)
max ^= (_minF ^ _maxF) & -(0 > v.si);
v.si ^= (max ^ v.si) & -(v.si > max);
v.si &= -(_epsF <= (v.si & absF));
return v.f;
}
uint32_t compress(float value)
{
Bits v;
v.f = clamp(value);
if(noLoss)
v.si ^= signF;
else
v.ui >>= _shift;
if(hasNegatives)
v.si ^= ((v.si - _nDelta) ^ v.si) & -(v.si > _maxC);
v.si ^= ((v.si - _pDelta) ^ v.si) & -(v.si > _zeroC);
if(noLoss)
v.si ^= signF;
return v.ui;
}
float decompress(uint32_t value)
{
Bits v;
v.ui = value;
if(noLoss)
v.si ^= signF;
v.si ^= ((v.si + _pDelta) ^ v.si) & -(v.si > _zeroC);
if(hasNegatives)
v.si ^= ((v.si + _nDelta) ^ v.si) & -(v.si > _maxC);
if(noLoss)
v.si ^= signF;
else
v.si <<= _shift;
return v.f;
}
};
This forces all values into the accepted range, no support for NaNs, infinities or negative zero. Epsilon is the smallest allowable value in the range. Precision is how many bits of precision to retain from the float. While there are a lot of branches above, they are all static and will be cached by the branch predictor in the CPU.
Of course if your values don't require logarithmic resolution approaching zero. Then linearizing them to a fixed point format is much faster, as was already mentioned.
I use the FloatCompressor (SSE version) in graphics library for reducing the size of linear float color values in memory. Compressed floats have the advantage of creating small look-up tables for time consuming functions, like gamma correction or transcendentals. Compressing linear sRGB values reduces to a max of 12 bits or a max value of 3011, which is great for a look-up table size for to/from sRGB.