1 /** 2 Nulib Fixed Point Math 3 4 Copyright: 5 Copyright © 2023-2025, Kitsunebi Games 6 Copyright © 2023-2025, Inochi2D Project 7 8 License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 9 Authors: 10 Luna Nielsen 11 */ 12 module nulib.math.fixed; 13 import numem.casting; 14 import numem.core.traits; 15 16 /** 17 Gets whether the provided value is a instance of $(D Fixed) 18 */ 19 enum isFixed(T) = is(T : Fixed!U, U...); 20 21 /** 22 Fixed-point math type, fractional bits can be specified, but 23 by default is split evenly; eg Fixed!int will be Q16.16 24 25 Note: 26 When converting a fixed-precision number to a floating point 27 number, they may not be exact bit-for-bit equal; as such an 28 approximate equals operation is recommended for comparing 29 fixed point math with 30 */ 31 struct Fixed(T, size_t FRACT_BITS = (8*(T.sizeof/2))) if (__traits(isIntegral, T) && !__traits(isUnsigned, T)) { 32 public: 33 @nogc nothrow: 34 T data; 35 36 /** 37 Shorthand for this type 38 */ 39 alias Self = typeof(this); 40 41 /** 42 Shorthand for the backing type 43 */ 44 alias ValueT = T; 45 46 /** 47 How much to shift the data store in-operation. 48 */ 49 enum T SHIFT = FRACT_BITS; 50 51 /** 52 Mask of fractional part 53 */ 54 enum T FRACT_MASK = (cast(T)1LU << SHIFT) - 1; 55 56 /** 57 Division factor for the given precision of float. 58 */ 59 enum FRACT_DIV(Y) = cast(Y)(1LU << SHIFT); 60 61 /** 62 Mask of integer part 63 */ 64 enum T INT_MASK = ~FRACT_MASK; 65 66 /** 67 Max value of the fixed-precision value 68 */ 69 enum Self min = Self.fromData(T.min); 70 71 /** 72 Max value of the fixed-precision value 73 */ 74 enum Self max = Self.fromData(T.max); 75 76 /** 77 Creates a new instance from raw data. 78 */ 79 static auto fromData(T data) { 80 Self t; 81 t.data = data; 82 return t; 83 } 84 85 /** 86 Constructor 87 */ 88 this(Y)(Y other) { 89 static if (__traits(isIntegral, Y)) { 90 this.data = cast(T)(cast(T)other << SHIFT); 91 } else static if (__traits(isFloating, Y)) { 92 this.data = cast(T)(other * FRACT_DIV!Y); 93 } else static if (isFixed!Y) { 94 95 // Fast path for when you're just assigning between 96 // the same type. 97 static if (other.SHIFT == this.SHIFT) { 98 this.data = cast(T)other.data; 99 return; 100 } 101 102 103 // Shift integer part over so that we can realign it, 104 // then add in the factional part from the other; making sure we cut out 105 // any fractional part that doesn't fit within our own fractional space. 106 T intPart = cast(T)(((other.data & other.INT_MASK) >> other.SHIFT) << SHIFT); 107 T fractPart = cast(T)(other.data & other.FRACT_MASK); 108 109 // This step aligns the fractional part with the size of the container. 110 static if (other.SHIFT > SHIFT) 111 fractPart = (fractPart >> (other.SHIFT-SHIFT)); 112 else 113 fractPart = (fractPart << (SHIFT-other.SHIFT)); 114 115 this.data = (intPart & INT_MASK) | (fractPart & FRACT_MASK); 116 } else static assert(0, "Unsupported construction"); 117 } 118 119 pragma(inline, true) 120 Y opCast(Y)() const { 121 static if (__traits(isIntegral, Y)) { 122 return cast(Y)(data >> SHIFT); 123 } else static if (__traits(isFloating, Y)) { 124 return (cast(Y)data / FRACT_DIV!Y); 125 } else static assert(0, "Unsupported cast."); 126 } 127 128 pragma(inline, true) 129 size_t toHash() const @safe pure nothrow { 130 return cast(size_t)this.data; 131 } 132 133 pragma(inline, true) 134 bool opEquals(R)(const R other) const 135 if (is(R : Fixed!T)) { 136 return this.data == other.data; 137 } 138 139 pragma(inline, true) 140 bool opEquals(R)(const R other) const 141 if (__traits(isScalar, R)) { 142 return this.data == Fixed!T(other).data; 143 } 144 145 pragma(inline, true) 146 typeof(this) opBinary(string op, R)(const R rhs) const 147 if (isFixed!R) { 148 149 // Convert mismatched Fixed!T 150 static if (R.SHIFT != SHIFT) { 151 auto other = typeof(this)(rhs); 152 } else static if (!is(typeof(R.data) == T)) { 153 auto other = typeof(this).fromData(cast(T)rhs.data); 154 } else { 155 auto other = rhs; 156 } 157 158 // Move out to LONG 159 long x = cast(long)data; 160 long y = cast(long)other.data; 161 long result; 162 163 static if (op == "+") { 164 165 result = x + y; 166 } else static if (op == "-") { 167 168 result = x - y; 169 } else static if (op == "*") { 170 171 result = (x * y) >> SHIFT; 172 } else static if (op == "/") { 173 static if (T.sizeof < 8 && R.sizeof < 8) { 174 175 // NOTE: For 32-bit and below division, 64 bit provides 176 // plenty of space to do the operation. 177 result = (x << SHIFT) / y; 178 } else { 179 ulong signBit = 1LU << 63; 180 ulong rem = x & ~signBit; 181 ulong div = y & ~signBit; 182 183 ulong quo = 0; 184 int shift = SHIFT; 185 while(rem && shift > 0) { 186 ulong d = rem / div; 187 rem %= div; 188 quo += d << shift; 189 190 rem <<= 1; 191 --shift; 192 } 193 194 // NOTE: Division generally takes up more space as such, 195 // this is the best way to get a mostly correct 196 // result for 64-bit fixed point numbers. 197 result = (quo >> 1) | ((x & signBit) ^ (y & signBit)); 198 } 199 } else static assert(0, "Operation not supported (yet)"); 200 201 return typeof(this).fromData(cast(T)result); 202 } 203 204 pragma(inline, true) 205 typeof(this) opBinary(string op, R)(const R rhs) const 206 if (__traits(isScalar, R)) { 207 return this.opBinary!(op, Fixed!T)(Fixed!T(rhs)); 208 } 209 210 pragma(inline, true) 211 typeof(this) opOpAssign(string op, R)(const R rhs) 212 if (isFixed!R) { 213 this = this.opBinary!(op, R)(rhs); 214 return this; 215 } 216 217 pragma(inline, true) 218 typeof(this) opOpAssign(string op, R)(const R rhs) 219 if (__traits(isScalar, R)) { 220 this = typeof(this)(this.opBinary!(op, Fixed!T)(Fixed!T(rhs))); 221 return this; 222 } 223 224 pragma(inline, true) 225 typeof(this) opAssign(R)(const R rhs) 226 if (!is(Unqual!R == Unqual!(typeof(this)))) { 227 this.__ctor!R(rhs); 228 return this; 229 } 230 } 231 232 /** 233 Q2.14 fixed-point number (16-bit) 234 */ 235 alias fixed2_14 = Fixed!(short, 14); 236 237 /** 238 Q26.6 fixed-point number (32-bit) 239 */ 240 alias fixed26_6 = Fixed!(int, 6); 241 242 /** 243 Q2.6 fixed-point number (8-bit) 244 */ 245 alias fixed2_6 = Fixed!(byte, 6); 246 247 /** 248 Q8.8 fixed-point number (16-bit) 249 */ 250 alias fixed16 = Fixed!short; 251 252 /** 253 Q16.16 fixed-point number (32-bit) 254 */ 255 alias fixed32 = Fixed!int; 256 257 /** 258 Q32.32 fixed-point number (64-bit) 259 */ 260 alias fixed64 = Fixed!long; 261 262 @("fixed32: int->fixed32") 263 unittest { 264 assert(cast(int)fixed32(16) == 16); 265 } 266 267 @("fixed32: fixed32->float") 268 unittest { 269 import std.math : isClose; 270 assert(isClose(cast(float)fixed32(32.32f), 32.32f)); 271 } 272 273 @("fixed32: +") 274 unittest { 275 assert(fixed32(31) + 1 == 32); 276 assert(fixed32(1.5) + 1.5 == 3); 277 } 278 279 @("fixed32: -") 280 unittest { 281 assert(fixed32(32) - 1 == 31); 282 assert(fixed32(1.5) - 0.5 == 1.0); 283 } 284 285 @("fixed32: *") 286 unittest { 287 assert(fixed32(16)*2 == 32); 288 assert(fixed32(5.5)*2.5 == 5.5*2.5); 289 } 290 291 @("fixed32: /") 292 unittest { 293 assert(fixed32(64) / 2 == 32); 294 } 295 296 @("fixed32: ctor fixed16") 297 unittest { 298 assert(fixed32(fixed16(32)) == 32); 299 assert(fixed32(fixed16(32.5)) == 32.5); 300 } 301 302 @("fixed16: +") 303 unittest { 304 assert(fixed16(31) + 1 == 32); 305 } 306 307 @("fixed16: -") 308 unittest { 309 assert(fixed16(32) - 1 == 31); 310 } 311 312 @("fixed16: *") 313 unittest { 314 assert(fixed16(16)*2 == 32); 315 assert(fixed16(5.5)*2.5 == 5.5*2.5); 316 } 317 318 @("fixed16: /") 319 unittest { 320 assert(fixed16(64) / 2 == 32); 321 } 322 323 @("fixed16: ctor fixed32") 324 unittest { 325 assert(fixed16(fixed32(32)) == 32); 326 assert(fixed16(fixed32(32.5)) == 32.5); 327 } 328 329 @("fixed16: ctor fixed2_14") 330 unittest { 331 assert(fixed16(fixed2_14(1.0)) == 1.0); 332 assert(fixed16(fixed2_14(0.5)) == 0.5); 333 } 334 335 336 337 338 // 339 // MATH OPERATIONS. 340 // 341 342 343 /** 344 Computes the nearest integer value greater than the given value. 345 346 Params: 347 x = The value 348 349 Returns: 350 The nearest integer value greater than $(D x). 351 */ 352 T ceil(T)(T x) if (isFixed!T) { 353 return T.fromData((x.data & T.INT_MASK) + (1 << T.SHIFT)); 354 } 355 356 /** 357 Computes the nearest integer value lower than the given value. 358 359 Params: 360 x = The value 361 362 Returns: 363 The nearest integer value lower than $(D x). 364 */ 365 T floor(T)(T x) if (isFixed!T) { 366 return T.fromData(x.data & T.INT_MASK); 367 } 368 369 /** 370 Computes the nearest integer value lower in magnitude than 371 the given value. 372 373 Params: 374 x = The value 375 376 Returns: 377 The nearest integer value lower in magnitude than $(D x). 378 */ 379 T trunc(T)(T x) if (isFixed!T) { 380 return T.fromData(x.data & T.INT_MASK); 381 } 382 383 /** 384 Gets the fractional part of the value. 385 386 Params: 387 value = The value to get the fractional portion of 388 389 Returns: 390 The factional part of the given value. 391 */ 392 T fract(T)(T value) if (isFixed!T) { 393 return T.fromData(value.data & T.FRACT_MASK); 394 } 395 396 @("fixed32: rounding") 397 unittest { 398 assert(fixed32(1.5).trunc() == fixed32(1.0)); 399 assert(fixed32(1.5).floor() == fixed32(1.0)); 400 assert(fixed32(1.5).ceil() == fixed32(2.0)); 401 assert(fixed32(1.5).fract() == fixed32(0.5)); 402 }