using System; namespace GlitchyEngine.Math; /// Represents a 16bit floating point number. (IEEE 754 half-precision binary floating-point (binary16)) /// Note: Eventhough it is possible, it is not recommended to perform calculations on this type. /// Most operations will simply convert the halfs to floats, perform the calculation and convert the result back to half. struct half : IFloating, ISigned, IFormattable, IHashable, IEquatable, ICanBeNaN { public const half MinValue = half(-65504); // Should be 0xFBFF public const half MaxValue = half(65504); // Should be 0x7BFF // The numbers for Inifnity, NaN and Zero need to be hardcoded as binaries, // because the conversion intself relies on them. public const half PositiveInfinity = half(0x7C00); public const half NegativeInfinity = half(0xFC00); public const half NaN = half(0x7CFF); public const half Zero = half(0x0000); public const half NegativeZero = half(0x8000); private uint16 _data; public bool IsNegative => (_data & Half_Sign_Mask) > 0; public bool IsFinite => (_data & ~Half_Sign_Mask) < Half_Exponent_Mask; public bool IsInfinity => (_data & ~Half_Sign_Mask) == Half_Exponent_Mask; public bool IsPositiveInfinity => _data == PositiveInfinity._data; public bool IsNegativeInfinity => _data == NegativeInfinity._data; public bool IsNaN => (_data & ~Half_Sign_Mask) > Half_Exponent_Mask; public bool IsSubnormal { get { var unsignedBits = _data & ~Half_Sign_Mask; // Zero isn't normalized and if exponent is 0 we are unnormalized return (unsignedBits != 0) && ((unsignedBits & Half_Exponent_Mask) == 0); } } public this(float value) { this = FromFloat32(value); } private this(uint16 data) { _data = data; } public explicit static operator half(float value) => FromFloat32(value); public explicit static operator float(half value) => ToFloat32(value); public static half FromFloat32(float value) { if (value == 0.0f) return half(0x0000); if (value == -0.0f) return half(0x8000); #unwarn uint32 singleBits = *(uint32*)&value; uint32 sign = GetSingleSignBit(singleBits); int32 exponent = (int32)GetSingleExponent(singleBits); uint32 mantissa = GetSingleMantissa(singleBits); // Shift 13 so we truncate mantissa from 23 to 10 bits. uint32 halfMantissa = mantissa >> 13; if (exponent == 0xFF) { // largest possible single exponent -> either infinity or NaN if (mantissa == 0) { // Infinity return (sign == 1) ? NegativeInfinity : PositiveInfinity; } else { // NaN -> Keeps sign and mantissa intact uint16 halfBits = SetHalfSignBit(0, (uint16)sign); // Set all five exponent bits to 1 halfBits = SetHalfExponent(halfBits, 0x1F); halfBits = SetHalfMantissa(halfBits, (uint16)halfMantissa); return half(halfBits); } } // Normalized single // excess-K decode and encode -127 is k for single, 15 is k for half exponent = exponent - 127 + 15; if (exponent < 0) // The given number is too small for a half (even denormalized) -> return 0 return (sign == 1) ? NegativeZero : Zero; if (exponent > 31) // The given number is too large for a half -> return infinity return (sign == 1) ? NegativeInfinity : PositiveInfinity; // The given number fits -> convert (might become denormalized) uint16 halfBits = SetHalfSignBit(0, (uint16)sign); halfBits = SetHalfExponent(halfBits, (uint16)exponent); halfBits = SetHalfMantissa(halfBits, (uint16)halfMantissa); return half(halfBits); } public static float ToFloat32(half value) { #unwarn uint16 halfBits = *(uint16*)&value; if (halfBits == Zero._data) return 0.0f; if (halfBits == NegativeZero._data) return -0.0f; uint16 sign = GetHalfSignBit(halfBits); int16 exponent = (int16)GetHalfExponent(halfBits); uint16 mantissa = GetHalfMantissa(halfBits); // Shift 13 so we extend mantissa from 10 to 23 bits. uint32 singleMantissa = (uint32)mantissa << 13; if (exponent == 0x1F) { // largest possible half exponent -> either infinity or NaN if (mantissa == 0) { // Infinity return (sign == 1) ? float.NegativeInfinity : float.PositiveInfinity; } else { // NaN -> Keeps sign and mantissa intact uint32 singleBits = SetSingleSignBit(0, sign); // Set all five exponent bits to 1 singleBits = SetSingleExponent(halfBits, 0xFF); singleBits = SetSingleMantissa(halfBits, singleMantissa); return *(float*)&singleBits; } } // Normalized half // excess-K decode and encode -127 is k for single, 15 is k for half exponent = exponent - 15 + 127; uint32 singleBits = SetSingleSignBit(0, sign); singleBits = SetSingleExponent(singleBits, (uint32)exponent); singleBits = SetSingleMantissa(singleBits, singleMantissa); return *(float*)&singleBits; } #region Single <-> Half Helpers const uint32 Single_Sign_Shift = 31; const uint32 Single_Sign_Mask = 0x8000'0000; // 1 bit, offset 31 const uint32 Single_Exponent_Shift = 23; const uint32 Single_Exponent_Mask = 0x7F80'0000; // 8 bit, offset 23 const uint32 Single_Mantissa_Mask = 0x007F'FFFF; // 23 bit, offset 0 const uint16 Half_Sign_Shift = 15; const uint16 Half_Sign_Mask = 0x8000; // 1 bit, offset 15 const uint16 Half_Exponent_Shift = 10; const uint16 Half_Exponent_Mask = 0x7C00; // 5 bit, offset 10 const uint16 Half_Mantissa_Mask = 0x03FF; // 10 bit, offset 0 private static uint32 GetSingleSignBit(uint32 singleData) { return (singleData >> Single_Sign_Shift); } private static uint32 GetSingleExponent(uint32 singleData) { return (singleData & Single_Exponent_Mask) >> Single_Exponent_Shift; } private static uint32 GetSingleMantissa(uint32 singleData) { return (singleData & Single_Mantissa_Mask); } private static uint32 SetSingleSignBit(uint32 singleData, uint32 signBit) { return ((signBit << Single_Sign_Shift) & Single_Sign_Mask) | (singleData & ~Single_Sign_Mask); } private static uint32 SetSingleExponent(uint32 singleData, uint32 exponent) { return ((exponent << Single_Exponent_Shift) & Single_Exponent_Mask) | (singleData & ~Single_Exponent_Mask); } private static uint32 SetSingleMantissa(uint32 singleData, uint32 mantissa) { return (mantissa & Single_Mantissa_Mask) | (singleData & ~Single_Mantissa_Mask); } private static uint16 GetHalfSignBit(uint16 HalfData) { return (HalfData >> Half_Sign_Shift); } private static uint16 GetHalfExponent(uint16 HalfData) { return (HalfData & Half_Exponent_Mask) >> Half_Exponent_Shift; } private static uint16 GetHalfMantissa(uint16 HalfData) { return (HalfData & Half_Mantissa_Mask); } private static uint16 SetHalfSignBit(uint16 HalfData, uint16 signBit) { return ((signBit << Half_Sign_Shift) & Half_Sign_Mask) | (HalfData & ~Half_Sign_Mask); } private static uint16 SetHalfExponent(uint16 HalfData, uint16 exponent) { return ((exponent << Half_Exponent_Shift) & Half_Exponent_Mask) | (HalfData & ~Half_Exponent_Mask); } private static uint16 SetHalfMantissa(uint16 HalfData, uint16 mantissa) { return (mantissa & Half_Mantissa_Mask) | (HalfData & ~Half_Mantissa_Mask); } #endregion public int GetHashCode() { return _data; } public void ToString(String outString) { ToFloat32(this).ToString(outString); } public void ToString(String outString, String format, IFormatProvider formatProvider) { ToFloat32(this).ToString(outString, format, formatProvider); } public static half operator +(half lhs, half rhs) { return (half)((float)lhs + (float)rhs); } public static half operator +(half value) => value; public static half operator -(half lhs, half rhs) { return (half)((float)lhs - (float)rhs); } public static half operator -(half value) { uint16 signBit = GetHalfSignBit(value._data); // Invert the sign bit (~) return half(SetHalfSignBit(value._data, ~signBit)); } public static half operator *(half lhs, half rhs) { return (half)((float)lhs * (float)rhs); } public static half operator /(half lhs, half rhs) { return (half)((float)lhs / (float)rhs); } public static half operator %(half lhs, half rhs) { return (half)((float)lhs % (float)rhs); } public static half operator ++(half value) { return (half)((float)value + 1.0f); } public static half operator --(half value) { return (half)((float)value - 1.0f); } public static bool operator==(half value1, half value2) => value1._data == value2._data; public static bool operator!=(half value1, half value2) => value1._data != value2._data; public static int operator<=>(half value1, half value2) => (float)value1 <=> (float)value2; public bool Equals(half other) { return _data == other._data; } }