feat(fixed): add Q32.32 deterministic fixed-point arithmetic
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
122
pkg/fixed/fixed.go
Normal file
122
pkg/fixed/fixed.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Package fixed provides deterministic Q32.32 fixed-point arithmetic.
|
||||
//
|
||||
// No floating-point operation appears anywhere in this package. Results must be
|
||||
// bit-identical across architectures and between native and WASM builds, which
|
||||
// is what allows a player's browser to independently replay a round and reach
|
||||
// exactly the same outcome as the server.
|
||||
package fixed
|
||||
|
||||
import (
|
||||
"math/bits"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// F is a Q32.32 fixed-point number: an int64 with 32 fractional bits.
|
||||
type F int64
|
||||
|
||||
// One is the fixed-point representation of 1.0.
|
||||
const One F = 1 << 32
|
||||
|
||||
const fracBits = 32
|
||||
|
||||
// FromInt converts a whole number to fixed-point.
|
||||
func FromInt(v int64) F { return F(v << fracBits) }
|
||||
|
||||
// Int truncates toward negative infinity and returns the whole part.
|
||||
func (a F) Int() int64 { return int64(a) >> fracBits }
|
||||
|
||||
func (a F) Add(b F) F { return a + b }
|
||||
func (a F) Sub(b F) F { return a - b }
|
||||
|
||||
// Mul multiplies via a 128-bit intermediate so no precision is lost before the
|
||||
// shift back down. A naive (a*b)>>32 overflows for operands above roughly 2^15.
|
||||
func (a F) Mul(b F) F {
|
||||
neg := false
|
||||
x, y := int64(a), int64(b)
|
||||
if x < 0 {
|
||||
x, neg = -x, !neg
|
||||
}
|
||||
if y < 0 {
|
||||
y, neg = -y, !neg
|
||||
}
|
||||
hi, lo := bits.Mul64(uint64(x), uint64(y))
|
||||
res := int64(lo>>fracBits | hi<<(64-fracBits))
|
||||
if neg {
|
||||
res = -res
|
||||
}
|
||||
return F(res)
|
||||
}
|
||||
|
||||
// Div divides via a 128-bit intermediate for the same reason as Mul.
|
||||
func (a F) Div(b F) F {
|
||||
if b == 0 {
|
||||
panic("fixed: division by zero")
|
||||
}
|
||||
neg := false
|
||||
x, y := int64(a), int64(b)
|
||||
if x < 0 {
|
||||
x, neg = -x, !neg
|
||||
}
|
||||
if y < 0 {
|
||||
y, neg = -y, !neg
|
||||
}
|
||||
hi := uint64(x) >> (64 - fracBits)
|
||||
lo := uint64(x) << fracBits
|
||||
q, _ := bits.Div64(hi, lo, uint64(y))
|
||||
res := int64(q)
|
||||
if neg {
|
||||
res = -res
|
||||
}
|
||||
return F(res)
|
||||
}
|
||||
|
||||
// Sqrt returns the fixed-point square root using integer Newton iteration.
|
||||
// It converges in well under the iteration cap for the full int64 range.
|
||||
func Sqrt(a F) F {
|
||||
if a < 0 {
|
||||
panic("fixed: sqrt of negative")
|
||||
}
|
||||
if a == 0 {
|
||||
return 0
|
||||
}
|
||||
// Initial guess: half the bit length puts us within a factor of two.
|
||||
shift := uint(bits.Len64(uint64(a))+fracBits) / 2
|
||||
x := F(1) << shift
|
||||
for i := 0; i < 64; i++ {
|
||||
next := (x + a.Div(x)) / 2
|
||||
if next == x || next == x-1 {
|
||||
x = next
|
||||
break
|
||||
}
|
||||
x = next
|
||||
}
|
||||
// Newton can land one ulp high; step down while the square exceeds the input.
|
||||
for x > 0 && x.Mul(x) > a {
|
||||
x--
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// String renders the value with six fractional digits, using integer math only.
|
||||
func (a F) String() string {
|
||||
neg := a < 0
|
||||
if neg {
|
||||
a = -a
|
||||
}
|
||||
whole := int64(a) >> fracBits
|
||||
frac := int64(a) & (int64(One) - 1)
|
||||
micros := (frac * 1_000_000) >> fracBits
|
||||
s := strconv.FormatInt(whole, 10) + "." + pad6(micros)
|
||||
if neg {
|
||||
return "-" + s
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func pad6(v int64) string {
|
||||
s := strconv.FormatInt(v, 10)
|
||||
for len(s) < 6 {
|
||||
s = "0" + s
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user