fixed

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 3 Imported by: 0

README

fixed

fixed is a small Go package for signed fixed-point arithmetic. Equal inputs produce the same result bits on every supported architecture.

The package provides three formats without choosing a default. Q32 stores Q32.32 in an int64, with resolution 2⁻³² and range [-2³¹, 2³¹ - 2⁻³²]. Q16 stores Q16.16 in an int32, with resolution 2⁻¹⁶ and range [-2¹⁵, 2¹⁵ - 2⁻¹⁶]. Q48 stores Q48.16 in an int64, with resolution 2⁻¹⁶ and range [-2⁴⁷, 2⁴⁷ - 2⁻¹⁶]; it accumulates Q16 products. Consumers choose the format that fits their range and storage requirements.

The module is pre-v1. Its import path may change before the first stable release. It requires Go 1.26.4 or newer.

Install

go get github.com/dhannyell/fixed

Quick start

package main

import (
	"fmt"

	"github.com/dhannyell/fixed"
)

func main() {
	a := fixed.Vec2{X: fixed.Q32FromInt(1), Y: fixed.Q32FromInt(2)}
	b := fixed.Vec2{X: fixed.Q32FromInt(4), Y: fixed.Q32FromInt(6)}
	distance := a.Distance(b)

	quarterTurn := fixed.RotFromTurns(fixed.Q32FromRatio(1, 4))
	direction := quarterTurn.Apply(fixed.Vec2{X: fixed.Q32One()})

	fmt.Println(distance)                 // 5
	fmt.Println(direction.X, direction.Y) // 0 1
}

Why there is no float constructor

Floating-point input can already contain small differences caused by an earlier computation. The package cannot recover the intended exact value from those bits. For this reason, fixed accepts only explicit inputs:

  • Q32FromInt, Q16FromInt, and Q48FromInt for integers.
  • Q32FromRatio, Q16FromRatio, and Q48FromRatio for exact ratios.
  • Q32MustParse, Q16MustParse, and Q48MustParse for decimal literals.
  • Q32FromRaw, Q16FromRaw, and Q48FromRaw for exact bit patterns.

String provides the inverse text boundary. It emits a canonical decimal representation, and every value satisfies:

fixed.Q32MustParse(q32.String()) == q32
fixed.Q16MustParse(q16.String()) == q16
fixed.Q48MustParse(q48.String()) == q48

Arithmetic contract

All formats use the same explicit rule for each operation:

Operation Result
Add, Sub Exact result in the selected format, with saturation on overflow
Mul Product floored to the selected format, with saturation on overflow
Div, FromRatio Quotient truncated toward zero, with saturation on overflow
Sqrt Square root floored to the selected format
Round, MustParse Nearest representable value; exact halves round away from zero
Q48.MulAdd16 Exact Q16 product floored to Q48.16, then added with saturation on overflow
Q48.Mul16 Q48 times Q16, floored to Q48.16 with saturation on overflow; same bits as Mul on the widened factor
Q16.ToQ32, Q16.ToQ48 Exact; the grid gets finer and the range gets wider
Q32.ToQ16 Floored to the coarser grid, with saturation outside the narrower range
Q32.ToQ48 Floored to the coarser grid; the range gets wider, so no saturation
Q48.ToQ16 Exact on the shared grid, with saturation outside the narrower range
Q48.ToQ32 Exact on the finer grid, with saturation outside the narrower range

A conversion applies two independent rules. A finer fraction grid is exact and a coarser one floors. A wider integer range never saturates and a narrower one saturates. Q48.ToQ32 refines the grid and narrows the range at the same time. Conversion and division deliberately use different rounding: a coarser grid floors, while division truncates toward zero.

Q48.Int, Q48FromInt, and Q48FromRatio use int64. The 48-bit integer range does not fit an int on 32-bit architectures, and a truncated int would break determinism between architectures. Q32 and Q16 keep int, because their integer range fits 32 bits.

Q48 exists for sums of Q16 products. A product has at most 32 integer bits, and MulAdd16 keeps 16 bits of headroom above it, so a sum of up to 2¹⁶ full-range products cannot saturate:

var acc fixed.Q48
for i := range a {
	acc = acc.MulAdd16(a[i], b[i])
}
dot := acc.ToQ16() // Narrow only when the value is stored.

Division by zero panics. Sqrt of a negative value also panics.

Saturated addition is not associative near the limits. Reordering a sum can therefore change its result. Accumulators should have enough headroom to avoid saturation.

Every saturation increments a process-wide atomic counter. SaturationCount provides diagnostics without changing any fixed-point value or operation result.

Cost of operations

The contract promises bits, not speed. The numbers below are a guide for design choices on one machine: AMD Ryzen 7 5800X3D (amd64), Go 1.26.4. They are medians of ten runs with -benchtime=500ms; Windows scheduling produced occasional high outliers that the median excludes.

Operation Latency (ns) Throughput (ns) Throughput (M op/s) Fixed time vs float
Q16.Add 0.5 0.5 2,000 −17% (faster)
Q16.Mul 1.7 0.8 1,200 +30% (slower)
Q16.Div 3.3 1.1 910 +35% (slower)
Q16.Sqrt 9.7 2.2 460 +86% (slower)
Q32.Add 0.4 0.5 1,900 −27% (faster)
Q32.Mul 2.8 1.4 690 +114% (slower)
Q32.Div 4.7 3.1 330 +138% (slower)
Q32.Sqrt 11.1 2.9 350 +54% (slower)
Q48.Add 0.5 0.5 2,100 −30% (faster)
Q48.Mul 2.6 1.5 680 +105% (slower)
Q48.MulAdd16 1.0 0.7 1,500 +73% (slower)
Q48.Div 4.3 5.1 200 +270% (slower)
Q48.Sqrt 11.1 3.0 330 +91% (slower)
Vec2.Dot 3.3 3.4 290 +435% (slower)
Vec2.Len 16.0 13.1 76 +247% (slower)
Vec2.Normalize 26.4 18.3 55 +382% (slower)
Vec2.Normalize axial 12.5 11.4 87 +280% (slower)
Rot.Apply 5.5 5.9 170 +688% (slower)
Rot.Mul 5.6 6.1 160 +718% (slower)
Rot.Normalize 26.7 18.5 54 +379% (slower)
SinTurns + CosTurns — 4.3 per pair 230 pairs −61% (faster)
RotFromTurns — 3.5 280 −64% (faster)
Atan2Turns — 3.9 250 −54% (faster)

Read each column alone; the columns measure different situations. Latency is the cost when each result feeds the next operation, as in an iterative solver. Throughput is the cost when independent operations overlap in the pipeline, as in a loop over many values. The rate column is the reciprocal of the throughput column, rounded to two digits; use it to size a frame budget. Each latency chain also contains one cheap companion operation that keeps the value in domain; bench_test.go shows the exact chains.

The comparison column reports the raw change in throughput time: (fixed time / float time - 1) × 100. A negative value means fixed was faster; a positive value means it was slower. The paired benchmarks use the same prebuilt inputs. Q16 is compared with float32; Q32, Q48, vectors, and rotations are compared with float64. Q48.MulAdd16 is compared with a float64 sum of float32 products, the shape a float solver uses for the same dot product. These safe-domain float kernels do not reproduce the package's saturation, rounding, or cross-architecture bit contract. Run them with:

go test -run '^$' -bench '^BenchmarkCompare' -benchtime=500ms -count=10

The batch functions are measured separately, because they are compared with a loop rather than with a float. The numbers are nanoseconds per element at 1024 elements, medians of twenty runs over two sessions on the same machine. The per-call loop column writes dst[i] = a[i].Op(b[i]) by hand; scalar is the exported batch function in a default build; avx2 is the same exported call in a build with GOEXPERIMENT=simd on Go 1.27. The benchmark goes through the exported function, so the length check and the counter update are inside the number.

Operation per-call loop scalar avx2
BatchAdd16 0.98 0.71 0.35
BatchSub16 0.97 0.72 0.35
BatchMul16 0.96 0.85 0.44
BatchClamp16 1.30 1.06 0.16
BatchQ32FromQ16 0.50 0.38 0.27
BatchQ16FromQ32 0.75 0.42 0.38
BatchDot16 1.18 1.40 0.57
BatchQ48Mul16 1.51 1.48 0.73

In these measurements, every scalar Q16 batch function was faster than its hand-written loop. The default build therefore had no batch abstraction penalty for this workload. BatchDot16 is the exception: its scalar kernel keeps eight partial sums so that every path shares one order, and that costs more than a serial loop when nothing saturates. The per-call loop for BatchDot16 is a serial Q48.MulAdd16 accumulator; for BatchQ48Mul16 it is Q48.Mul16.

arm64 has a NEON path for the six Q16 functions. Its numbers are not published here because only shared CI runners have measured it, and a shared runner cannot support the comparison above. The two Q48 functions stay scalar on arm64. NEON has two 64-bit lanes per register and no 64-bit multiply, and the measured candidates stayed under the 2.0x gain that a vector kernel must show over the scalar batch. On a shared arm64 runner the scalar BatchDot16 was still 2.2x faster than the serial Q48.MulAdd16 loop. SVE may change this; it is not in simd/archsimd yet.

Two portability notes. Div costs more on arm64, because the 128-bit division is a software routine there. Sqrt does not divide on any architecture: its hardware seed plus integer corrections stay within multiplications.

Vectors and angles

Vec2 provides the usual 2D operations over Q32: addition, scaling, dot product, length, normalization, distance, and interpolation. LenSq follows the scalar operation order and can saturate even when the length still fits. Len uses a 128-bit intermediate and saturates only when the final magnitude does not fit. Normalize scales the components before squaring them, which avoids intermediate overflow and underflow.

Angles use turns instead of radians. Q32One() is one complete revolution, Q32Half() is half a revolution, and Q32FromRatio(1, 4) is a quarter turn. This maps the fractional bits of Q32 directly onto the circle and avoids reduction through an approximation of pi.

SinTurns, CosTurns, and Atan2Turns use committed lookup tables and linear interpolation. Their maximum absolute error is 2⁻²⁰. Rot stores a rotation as its sine and cosine, which makes application, composition, and inversion available without another trigonometric lookup. The zero value of Rot is not a valid rotation; start with RotIdentity or RotFromTurns. Rot.Inv is the conjugate and is an inverse when the rotation has unit length. Use Rot.InvNormalized after accumulated rounding drift when normalization is required as part of the operation.

Batch operations

BatchAdd16, BatchSub16, BatchMul16, and BatchClamp16 apply one operation across whole slices of Q16. Every slice in a call must share one length. The destination may be the same slice as a source, so an operation can run in place; any other overlap is undefined. BatchQ32FromQ16 and BatchQ16FromQ32 move whole slices across the format boundary and follow the conversion rules of Q16.ToQ32 and Q32.ToQ16. A batch call adds the number of saturated elements to the saturation counter in one update, so SaturationCount reports the same total as a loop over the scalar methods.

BatchDot16 sums Q16 products into one Q48. Its order is fixed: element i joins partial sum i mod 8, and the eight partials reduce as a balanced tree. Without saturation this equals a loop over Q48.MulAdd16. With saturation the order decides the bits, so every kernel keeps it, and the result is the same on every host. BatchQ48Mul16 scales a Q48 slice by a Q16 slice with the rules of Q48.Mul16.

Every build runs the scalar kernels. Building with GOEXPERIMENT=simd on Go 1.27 or later selects vector kernels at package initialization: AVX2 on amd64 when the CPU reports it, and NEON on arm64. BatchPath returns "scalar", "avx2", or "neon" so a program can report which family it got. The "neon" family covers the Q16 functions only; BatchDot16 and BatchQ48Mul16 run their scalar kernels on arm64 in every build.

GOEXPERIMENT=simd go build ./...

CI compares the result bits and saturation counts of the AVX2 and NEON kernels with the scalar kernels.

Architecture

fixed is a leaf module. The portable files import only math, math/bits, and sync/atomic; files behind the goexperiment.simd build tag also use unsafe and simd/archsimd. Every unsafe operation lives in one file, batch16_raw.go, which only reinterprets a Q16 or Q32 slice as the raw words the vector loads take, under compile-time assertions on the layout. The math import provides hardware seeds; exact integer comparisons close every result, so floating point never decides a bit. This small dependency surface lets applications use the numeric type without importing unrelated systems.

The Q32 and Q16 types are opaque. Their prefixed constructors make the chosen format explicit. Operations own saturation and rounding, and Raw is the boundary for exact bit access. Consumers that standardize on one format can define local aliases without imposing that choice on other users of the library.

The module is one flat package by design. Every public type shares one contract, so subpackages would only split the documentation and add import noise. File names carry the layers: q*/decimal* for the scalar, vec2* and rot* for the plane, trig* for the kernel. Directories exist only for content outside the package interface: internal/ for tools and .github/ for CI.

The Go implementation defines the bit-level contract. An independent implementation must preserve the rounding, saturation, and raw representation before it exchanges values with this package. A change to one of these rules is a semantic change, not an internal refactor.

Development

Run the standard checks before submitting a change:

go test ./...
go vet ./...
golangci-lint run ./...

The batch benchmarks separate steady-state throughput from edge cases. Use BenchmarkBatchBoundaries to inspect empty calls, vector-width crossings, and scalar tails. BenchmarkBatchSaturation compares workloads with no saturation, sparse saturation, and saturation in every element:

go test -run '^$' -bench '^BenchmarkBatch$' -benchmem ./...
go test -run '^$' -bench '^BenchmarkBatch(Boundaries|Saturation)$' -benchmem ./...

See the package documentation for the API reference and the full behavioral contract.

License

fixed is available under the MIT License.

Documentation

Overview

Package fixed implements deterministic signed fixed-point arithmetic.

The package provides three formats without choosing a default. Q32 stores Q32.32 in an int64, with resolution 2⁻³² and range [-2³¹, 2³¹ - 2⁻³²]. Q16 stores Q16.16 in an int32, with resolution 2⁻¹⁶ and range [-2¹⁵, 2¹⁵ - 2⁻¹⁶]. Q48 stores Q48.16 in an int64, with resolution 2⁻¹⁶ and range [-2⁴⁷, 2⁴⁷ - 2⁻¹⁶]; it is the accumulator for Q16 products. Each operation produces the same bits on every supported architecture.

Overflow

An overflow saturates to the minimum or maximum of the selected format. SaturationCount reports saturation events for diagnostics. The counter does not affect fixed-point values or operation results.

Div panics for a zero divisor in every format. Q32FromRatio, Q16FromRatio, and Q48FromRatio panic for a zero denominator. Sqrt panics for a negative input in every format.

Saturated addition is not associative near the range limits. Do not reorder an accumulation. Use enough numeric headroom to prevent saturation.

Rounding

Q32.Mul and Q48.Mul floor the 128-bit product when they convert the result to their format. Q16.Mul floors the exact 62-bit product when it converts the result to Q16.16. All three operations match an arithmetic right shift. Q48.MulAdd16 floors the exact Q16 product to Q48.16 and then adds it.

Div and FromRatio truncate toward zero in every format. Sqrt floors its result in every format. Round and MustParse round to the nearest representable value in every format. An exact half rounds away from zero.

Formats and conversion

A conversion changes the fraction grid, the integer range, or both. Each change follows one rule:

  • A finer fraction grid is exact. A coarser fraction grid floors.
  • A wider integer range never saturates. A narrower integer range saturates outside the target range.

Q16.ToQ32 and Q16.ToQ48 widen both. They are exact and never saturate. Q32.ToQ16 narrows both. It floors and saturates. Q16 and Q48 share one fraction grid, so Q48.ToQ16 only saturates and Q32.ToQ48 only floors. Q48.ToQ32 moves in both directions: the grid gets finer, so it is exact, and the integer range shrinks to 32 bits, so it saturates.

Int returns the integer part of a value. Q32.Int and Q16.Int return an int, because their integer range fits a 32-bit int. Q48.Int returns an int64, because its integer range does not. For the same reason Q48FromInt and Q48FromRatio take int64 arguments.

For any Q16 values a and b, a.Mul(b) equals a.ToQ32().Mul(b.ToQ32()).ToQ16() and also equals Q48Zero().MulAdd16(a, b).ToQ16(). The same identity does not hold for Div because division truncates toward zero and narrowing floors.

Accumulation

A Q16 product has at most 32 integer bits. Q48.MulAdd16 stores it with 16 bits of headroom, so a sum of up to 2¹⁶ products of full-range Q16 values cannot saturate. Narrow the sum with Q48.ToQ16 only when the value is stored, and check SaturationCount when the term count is not bounded.

Construction and text

Q32, Q16, and Q48 are opaque. Construct Q32 values with Q32FromInt, Q32FromRatio, Q32MustParse, or Q32FromRaw. Q16 and Q48 have the same four constructors under their own prefixes. The package does not accept float values because a computed float can contain architecture-dependent bits.

String returns the exact canonical decimal form in every format. For every value q, parsing q.String() with the constructor for its format returns q.

Angles

Angles use turns. Q32One is a full revolution. The fractional bits of a Q32 map directly to the circle, so range reduction does not use pi and does not round.

SinTurns and CosTurns accept every Q32 value. They never panic or saturate, and their results stay in [-Q32One, Q32One]. They use a 1024-interval quarter-wave table. Table entries round to nearest, and linear interpolation floors. The maximum absolute error is 2⁻²⁰.

Atan2Turns returns an angle in (-1/2, 1/2] turns. It reduces the input to a ratio in [0, 1], truncates that ratio to Q32.32, and uses a 1024-interval table. Table entries round to nearest, and linear interpolation floors. Octant reconstruction is exact.

Vectors and rotations

Vec2.LenSq composes scalar multiplication and addition, so it can saturate even when the length fits in Q32.32. Vec2.Len computes the length with a 128-bit intermediate and saturates only when the final length is out of range. Vec2.Normalize scales the components before it squares them, so intermediate underflow cannot turn a nonzero vector into the zero vector.

Rot stores a rotation as its sine and cosine. The zero Rot is invalid. Use RotIdentity or RotFromTurns to construct a rotation. Repeated composition can introduce rounding drift; Rot.Normalize restores unit length. Rot.Inv is the conjugate and is an inverse for a unit rotation; Rot.InvNormalized normalizes first when drift may be present.

Batch operations

BatchAdd16, BatchSub16, BatchMul16, and BatchClamp16 apply one operation across whole slices of Q16. Their results and saturation counts are identical to a loop over the scalar methods; the counter receives one update per call with the number of saturated elements. The destination may be the same slice as a source; any other overlap is undefined. BatchQ32FromQ16 and BatchQ16FromQ32 move whole slices across the format boundary and follow the conversion rules of Q16.ToQ32 and Q32.ToQ16.

BatchDot16 sums Q16 products into one Q48 in a fixed order: element i joins partial sum i mod 8, and the eight partials reduce as a balanced tree. Without saturation this equals a loop over Q48.MulAdd16; with saturation the order decides the bits, so every kernel keeps it. BatchQ48Mul16 scales a Q48 slice by a Q16 slice with the rules of Q48.Mul16.

Every build runs the scalar kernels. A build with GOEXPERIMENT=simd on Go 1.27 or later selects vector kernels at package initialization on amd64 with AVX2 and on arm64. On arm64 only the Q16 functions have vector kernels; BatchDot16 and BatchQ48Mul16 stay scalar there. BatchPath reports the active family. The selection changes speed, never bits.

Compatibility contract

The three raw representations, their conversions, saturation rules, and rounding rules are part of the public contract. An independent implementation must reproduce these rules before it exchanges raw values with this package. The trigonometric raw outputs are also part of the contract. A compatible implementation may use a different representation, but it must produce the same output bits.

Dependencies

The portable files import only math, math/bits, and sync/atomic. The math import provides hardware seeds; exact integer comparisons close every result, so floating point never decides a bit. Files behind the goexperiment.simd build tag also import unsafe and simd/archsimd; no default build reaches them. The unsafe import is confined to batch16_raw.go, which reinterprets a Q16 or Q32 slice as the raw words the vector loads take and asserts the layout at compile time.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func BatchAdd16 added in v0.4.0

func BatchAdd16(dst, a, b []Q16)

BatchAdd16 stores a[i]+b[i] into dst. Each element saturates on overflow. All three slices must share one length. dst may be the same slice as a or b; no other overlap is allowed.

func BatchClamp16 added in v0.4.0

func BatchClamp16(dst, a []Q16, lo, hi Q16)

BatchClamp16 stores a[i] limited to [lo, hi] into dst. It requires lo <= hi. A clamp is not an overflow, so it records no saturation event. Both slices must share one length. dst may be the same slice as a; no other overlap is allowed.

func BatchMul16 added in v0.4.0

func BatchMul16(dst, a, b []Q16)

BatchMul16 stores a[i]*b[i] into dst. It floors each product to Q16.16 and saturates on overflow. All three slices must share one length. dst may be the same slice as a or b; no other overlap is allowed.

func BatchPath added in v0.4.0

func BatchPath() string

BatchPath reports the kernel family behind the Q16 batch functions on this host: "scalar", "avx2" or "neon". The Q48 batch functions follow "avx2"; on "neon" they run their scalar kernels. Every path produces the same bits.

func BatchQ16FromQ32 added in v0.4.0

func BatchQ16FromQ32(dst []Q16, a []Q32)

BatchQ16FromQ32 stores a[i] floored to the Q16.16 grid into dst. Each element saturates outside the Q16 range. Both slices must share one length.

func BatchQ32FromQ16 added in v0.4.0

func BatchQ32FromQ16(dst []Q32, a []Q16)

BatchQ32FromQ16 stores a[i] converted to Q32 into dst. The conversion is exact and never saturates. Both slices must share one length.

func BatchQ48Mul16 added in v0.6.0

func BatchQ48Mul16(dst, q []Q48, f []Q16)

BatchQ48Mul16 stores q[i]*f[i] into dst. It floors each product to Q48.16 and saturates on overflow, as Q48.Mul16 does. All three slices must share one length. dst may be the same slice as q; no other overlap is allowed.

func BatchSub16 added in v0.4.0

func BatchSub16(dst, a, b []Q16)

BatchSub16 stores a[i]-b[i] into dst. Each element saturates on overflow. All three slices must share one length. dst may be the same slice as a or b; no other overlap is allowed.

func ResetSaturationCount

func ResetSaturationCount()

ResetSaturationCount zeroes the saturation counter.

func SaturationCount

func SaturationCount() uint64

SaturationCount reports the number of saturation events since the last reset.

Types

type Q16 added in v0.3.0

type Q16 struct {
	// contains filtered or unexported fields
}

Q16 stores an opaque signed Q16.16 value. Its zero value is 0.

Example
package main

import (
	"fmt"

	"github.com/dhannyell/fixed"
)

func main() {
	// Q16 is the compact format. It converts to and from Q32.
	price := fixed.Q16FromRatio(5, 2)
	total := price.Mul(fixed.Q16FromInt(4))
	fmt.Println(total)
	fmt.Println(total.ToQ32().Eq(fixed.Q32FromInt(10)))
}
Output:
10
true

func Q16FromInt added in v0.3.0

func Q16FromInt(i int) Q16

Q16FromInt returns i as a Q16 value. It saturates outside [-2¹⁵, 2¹⁵-1].

func Q16FromRatio added in v0.3.0

func Q16FromRatio(num, den int) Q16

Q16FromRatio returns num/den truncated toward zero. It saturates on overflow. It panics when den is zero.

func Q16FromRaw added in v0.3.0

func Q16FromRaw(raw int32) Q16

Q16FromRaw returns the Q16 value with the specified signed bit pattern.

func Q16Half added in v0.3.0

func Q16Half() Q16

Q16Half returns the fixed-point value 1/2.

func Q16MaxValue added in v0.3.0

func Q16MaxValue() Q16

Q16MaxValue returns the largest representable Q16 value, 2¹⁵ - 2⁻¹⁶.

func Q16MinValue added in v0.3.0

func Q16MinValue() Q16

Q16MinValue returns the smallest representable Q16 value, -2¹⁵.

func Q16MustParse added in v0.3.0

func Q16MustParse(s string) Q16

Q16MustParse parses a decimal literal. It rounds to the nearest Q16 value, with exact halves away from zero. It saturates outside the Q16 range and panics on malformed input.

func Q16One added in v0.3.0

func Q16One() Q16

Q16One returns the fixed-point value 1.

func Q16Zero added in v0.3.0

func Q16Zero() Q16

Q16Zero returns the fixed-point value 0.

func (Q16) Abs added in v0.3.0

func (q Q16) Abs() Q16

Abs returns the magnitude of q. Abs of the minimum saturates to the maximum.

func (Q16) Add added in v0.3.0

func (q Q16) Add(o Q16) Q16

Add returns q+o. It saturates on overflow.

func (Q16) Ceil added in v0.3.0

func (q Q16) Ceil() Q16

Ceil returns the smallest integer multiple of Q16One not below q. It saturates when the result is outside the Q16 range.

func (Q16) Clamp added in v0.3.0

func (q Q16) Clamp(lo, hi Q16) Q16

Clamp returns q limited to [lo, hi]. It requires lo <= hi.

func (Q16) Cmp added in v0.3.0

func (q Q16) Cmp(o Q16) int

Cmp returns -1 when q < o, 0 when q == o, and 1 when q > o.

func (Q16) Div added in v0.3.0

func (q Q16) Div(o Q16) Q16

Div returns q/o truncated toward zero. It saturates on overflow. It panics when o is zero.

func (Q16) Eq added in v0.3.0

func (q Q16) Eq(o Q16) bool

Eq reports whether q == o.

func (Q16) Floor added in v0.3.0

func (q Q16) Floor() Q16

Floor returns the largest integer multiple of Q16One not above q.

func (Q16) Greater added in v0.3.0

func (q Q16) Greater(o Q16) bool

Greater reports whether q > o.

func (Q16) Int added in v0.3.0

func (q Q16) Int() int

Int returns the integer part truncated toward zero.

func (Q16) Less added in v0.3.0

func (q Q16) Less(o Q16) bool

Less reports whether q < o.

func (Q16) Max added in v0.3.0

func (q Q16) Max(o Q16) Q16

Max returns the larger of q and o.

func (Q16) Min added in v0.3.0

func (q Q16) Min(o Q16) Q16

Min returns the smaller of q and o.

func (Q16) Mul added in v0.3.0

func (q Q16) Mul(o Q16) Q16

Mul returns q*o. It floors the product to Q16.16 and saturates on overflow.

func (Q16) Neg added in v0.3.0

func (q Q16) Neg() Q16

Neg returns -q. Neg of the minimum saturates to the maximum.

func (Q16) Raw added in v0.3.0

func (q Q16) Raw() int32

Raw returns the signed Q16.16 bit pattern of q.

func (Q16) Round added in v0.3.0

func (q Q16) Round() Q16

Round returns the nearest integer multiple of Q16One. An exact half rounds away from zero. Round saturates when the result is outside the Q16 range.

func (Q16) Sqrt added in v0.3.0

func (q Q16) Sqrt() Q16

Sqrt returns floor(sqrt(q)). It panics when q is negative. The result cannot overflow.

func (Q16) String added in v0.3.0

func (q Q16) String() string

String returns the exact canonical decimal form of q. The widening conversion is exact, so the Q32 formatter emits the same value.

func (Q16) Sub added in v0.3.0

func (q Q16) Sub(o Q16) Q16

Sub returns q-o. It saturates on overflow.

func (Q16) ToQ32 added in v0.3.0

func (q Q16) ToQ32() Q32

ToQ32 returns q widened to Q32.32. The conversion is exact and never saturates.

func (Q16) ToQ48 added in v0.5.0

func (q Q16) ToQ48() Q48

ToQ48 returns q widened to Q48.16. Both types share one fraction grid, so the conversion is a sign extension. It never saturates.

type Q32 added in v0.3.0

type Q32 struct {
	// contains filtered or unexported fields
}

Q32 stores an opaque signed Q32.32 value. Its zero value is 0.

func Atan2Turns added in v0.2.1

func Atan2Turns(y, x Q32) Q32

Atan2Turns returns the angle of (x, y) in (-1/2, 1/2] turns. Atan2Turns(Zero(), Zero()) returns Zero. It never panics or saturates. The unit ratio is truncated to Q32.32, table entries round to nearest, and linear interpolation floors. The maximum absolute error is 2⁻²⁰ turn.

func CosTurns added in v0.2.1

func CosTurns(t Q32) Q32

CosTurns returns the cosine of t in turns. It uses the same rules as SinTurns with a quarter-turn shift.

func Q32FromInt added in v0.3.0

func Q32FromInt(i int) Q32

Q32FromInt returns i as a Q32 value. It saturates outside [-2³¹, 2³¹-1].

Example
package main

import (
	"fmt"

	"github.com/dhannyell/fixed"
)

func main() {
	fmt.Println(fixed.Q32FromInt(3))
	fmt.Println(fixed.Q32FromInt(-2))
}
Output:
3
-2

func Q32FromRatio added in v0.3.0

func Q32FromRatio(num, den int) Q32

Q32FromRatio returns num/den truncated toward zero. It saturates on overflow. It panics when den is zero.

Example
package main

import (
	"fmt"

	"github.com/dhannyell/fixed"
)

func main() {
	// Use a ratio for exact fractional constants. There is no FromFloat.
	q := fixed.Q32FromRatio(5, 2)
	fmt.Println(q)
}
Output:
2.5

func Q32FromRaw added in v0.3.0

func Q32FromRaw(raw int64) Q32

Q32FromRaw returns the Q32 value with the specified signed bit pattern.

func Q32Half added in v0.3.0

func Q32Half() Q32

Q32Half returns the fixed-point value 1/2.

func Q32MaxValue added in v0.3.0

func Q32MaxValue() Q32

Q32MaxValue returns the largest representable Q32 value, 2³¹ - 2⁻³².

func Q32MinValue added in v0.3.0

func Q32MinValue() Q32

Q32MinValue returns the smallest representable Q32 value, -2³¹.

func Q32MustParse added in v0.3.0

func Q32MustParse(s string) Q32

Q32MustParse parses a decimal literal. It rounds to the nearest Q32 value, with exact halves away from zero. It saturates outside the Q32 range and panics on malformed input.

Example
package main

import (
	"fmt"

	"github.com/dhannyell/fixed"
)

func main() {
	fmt.Println(fixed.Q32MustParse("6.25"))
	fmt.Println(fixed.Q32MustParse("-0.001"))
}
Output:
6.25
-0.00099999993108212947845458984375

func Q32One added in v0.3.0

func Q32One() Q32

Q32One returns the fixed-point value 1.

func Q32Zero added in v0.3.0

func Q32Zero() Q32

Q32Zero returns the fixed-point value 0.

func SinTurns added in v0.2.1

func SinTurns(t Q32) Q32

SinTurns returns the sine of t, where One is a full revolution. It uses only the fractional part of t, accepts every Q32 value, and returns a value in [-One, One] without saturation.

The 1024-interval quarter-wave table rounds entries to nearest. Linear interpolation floors. The maximum absolute error is 2⁻²⁰.

func (Q32) Abs added in v0.3.0

func (q Q32) Abs() Q32

Abs returns the magnitude of q. Abs of MinValue saturates to MaxValue.

func (Q32) Add added in v0.3.0

func (q Q32) Add(o Q32) Q32

Add returns q+o. It saturates on overflow.

func (Q32) Ceil added in v0.3.0

func (q Q32) Ceil() Q32

Ceil returns the smallest integer multiple of One that is not below q. It saturates when the result is outside the Q32 range.

func (Q32) Clamp added in v0.3.0

func (q Q32) Clamp(lo, hi Q32) Q32

Clamp returns q limited to [lo, hi]. It requires lo <= hi.

Example
package main

import (
	"fmt"

	"github.com/dhannyell/fixed"
)

func main() {
	speed := fixed.Q32FromInt(150)
	limited := speed.Clamp(fixed.Q32Zero(), fixed.Q32FromInt(100))
	fmt.Println(limited)
}
Output:
100

func (Q32) Cmp added in v0.3.0

func (q Q32) Cmp(o Q32) int

Cmp returns -1 when q < o, 0 when q == o, and 1 when q > o.

func (Q32) Div added in v0.3.0

func (q Q32) Div(o Q32) Q32

Div returns q/o truncated toward zero. It saturates on overflow. It panics when o is zero.

func (Q32) Eq added in v0.3.0

func (q Q32) Eq(o Q32) bool

Eq reports whether q == o.

func (Q32) Floor added in v0.3.0

func (q Q32) Floor() Q32

Floor returns the largest integer multiple of One not above q.

func (Q32) Greater added in v0.3.0

func (q Q32) Greater(o Q32) bool

Greater reports whether q > o.

func (Q32) Int added in v0.3.0

func (q Q32) Int() int

Int returns the integer part truncated toward zero.

func (Q32) Less added in v0.3.0

func (q Q32) Less(o Q32) bool

Less reports whether q < o.

func (Q32) Max added in v0.3.0

func (q Q32) Max(o Q32) Q32

Max returns the larger of q and o.

func (Q32) Min added in v0.3.0

func (q Q32) Min(o Q32) Q32

Min returns the smaller of q and o.

func (Q32) Mul added in v0.3.0

func (q Q32) Mul(o Q32) Q32

Mul returns q*o. It floors the product to Q32.32 and saturates on overflow.

Example
package main

import (
	"fmt"

	"github.com/dhannyell/fixed"
)

func main() {
	area := fixed.Q32FromRatio(5, 2).Mul(fixed.Q32FromInt(4))
	fmt.Println(area)
}
Output:
10

func (Q32) Neg added in v0.3.0

func (q Q32) Neg() Q32

Neg returns -q. Neg of MinValue saturates to MaxValue.

func (Q32) Raw added in v0.3.0

func (q Q32) Raw() int64

Raw returns the signed Q32.32 bit pattern of q.

func (Q32) Round added in v0.3.0

func (q Q32) Round() Q32

Round returns the nearest integer multiple of One. An exact half rounds away from zero. Round saturates when the result is outside the Q32 range.

func (Q32) Sqrt added in v0.3.0

func (q Q32) Sqrt() Q32

Sqrt returns floor(sqrt(q)). It panics when q is negative. The result cannot overflow.

func (Q32) String added in v0.3.0

func (q Q32) String() string

String returns the exact canonical decimal form of q, such as "-6.25". For every q, MustParse(q.String()) == q. Use Raw for the exact bit pattern.

Example
package main

import (
	"fmt"

	"github.com/dhannyell/fixed"
)

func main() {
	q := fixed.Q32FromRaw(1)
	text := q.String()
	fmt.Println(text)
	fmt.Println(fixed.Q32MustParse(text).Eq(q))
}
Output:
0.00000000023283064365386962890625
true

func (Q32) Sub added in v0.3.0

func (q Q32) Sub(o Q32) Q32

Sub returns q-o. It saturates on overflow.

func (Q32) ToQ16 added in v0.3.0

func (q Q32) ToQ16() Q16

ToQ16 floors q to the Q16.16 grid and saturates outside the Q16 range. Flooring matches the rounding of Mul.

func (Q32) ToQ48 added in v0.5.0

func (q Q32) ToQ48() Q48

ToQ48 floors q to the Q48.16 grid. It never saturates.

type Q48 added in v0.5.0

type Q48 struct {
	// contains filtered or unexported fields
}

Q48 stores an opaque signed Q48.16 value. Its zero value is 0. Q48 shares the fraction grid of Q16, so it accumulates Q16 products with 16 bits of integer headroom.

func BatchDot16 added in v0.6.0

func BatchDot16(a, b []Q16) Q48

BatchDot16 returns the sum of a[i]*b[i] as Q48. Each product is exact and floored to the shared grid; each addition saturates. Both slices must share one length.

The summation order is fixed: element i joins partial sum i mod 8, and the eight partials then reduce as a balanced tree. Without saturation this equals a loop over Q48.MulAdd16. With saturation the order decides the result, so every kernel follows this one order and the bits match on every host.

func Q48FromInt added in v0.5.0

func Q48FromInt(i int64) Q48

Q48FromInt returns i as a Q48 value. It saturates outside [-2⁴⁷, 2⁴⁷-1]. The integer range exceeds a 32-bit int, so the argument is an int64.

func Q48FromRatio added in v0.5.0

func Q48FromRatio(num, den int64) Q48

Q48FromRatio returns num/den truncated toward zero. It saturates on overflow. It panics when den is zero.

func Q48FromRaw added in v0.5.0

func Q48FromRaw(raw int64) Q48

Q48FromRaw returns the Q48 value with the specified signed bit pattern.

func Q48Half added in v0.5.0

func Q48Half() Q48

Q48Half returns the fixed-point value 1/2.

func Q48MaxValue added in v0.5.0

func Q48MaxValue() Q48

Q48MaxValue returns the largest representable Q48 value, 2⁴⁷ - 2⁻¹⁶.

func Q48MinValue added in v0.5.0

func Q48MinValue() Q48

Q48MinValue returns the smallest representable Q48 value, -2⁴⁷.

func Q48MustParse added in v0.5.0

func Q48MustParse(s string) Q48

Q48MustParse parses a decimal literal. It rounds to the nearest Q48 value, with exact halves away from zero. It saturates outside the Q48 range and panics on malformed input.

func Q48One added in v0.5.0

func Q48One() Q48

Q48One returns the fixed-point value 1.

func Q48Zero added in v0.5.0

func Q48Zero() Q48

Q48Zero returns the fixed-point value 0.

func (Q48) Abs added in v0.5.0

func (q Q48) Abs() Q48

Abs returns the magnitude of q. Abs of MinValue saturates to MaxValue.

func (Q48) Add added in v0.5.0

func (q Q48) Add(o Q48) Q48

Add returns q+o. It saturates on overflow.

func (Q48) Ceil added in v0.5.0

func (q Q48) Ceil() Q48

Ceil returns the smallest integer multiple of Q48One not below q. It saturates when the result is outside the Q48 range.

func (Q48) Clamp added in v0.5.0

func (q Q48) Clamp(lo, hi Q48) Q48

Clamp returns q limited to [lo, hi]. It requires lo <= hi.

func (Q48) Cmp added in v0.5.0

func (q Q48) Cmp(o Q48) int

Cmp returns -1 when q < o, 0 when q == o, and 1 when q > o.

func (Q48) Div added in v0.5.0

func (q Q48) Div(o Q48) Q48

Div returns q/o truncated toward zero. It saturates on overflow. It panics when o is zero.

func (Q48) Eq added in v0.5.0

func (q Q48) Eq(o Q48) bool

Eq reports whether q == o.

func (Q48) Floor added in v0.5.0

func (q Q48) Floor() Q48

Floor returns the largest integer multiple of Q48One not above q.

func (Q48) Greater added in v0.5.0

func (q Q48) Greater(o Q48) bool

Greater reports whether q > o.

func (Q48) Int added in v0.5.0

func (q Q48) Int() int64

Int returns the integer part truncated toward zero. The integer part does not fit a 32-bit int, so the result is an int64 on every architecture.

func (Q48) Less added in v0.5.0

func (q Q48) Less(o Q48) bool

Less reports whether q < o.

func (Q48) Max added in v0.5.0

func (q Q48) Max(o Q48) Q48

Max returns the larger of q and o.

func (Q48) Min added in v0.5.0

func (q Q48) Min(o Q48) Q48

Min returns the smaller of q and o.

func (Q48) Mul added in v0.5.0

func (q Q48) Mul(o Q48) Q48

Mul returns q*o. It floors the product to Q48.16 and saturates on overflow.

func (Q48) Mul16 added in v0.6.0

func (q Q48) Mul16(f Q16) Q48

Mul16 returns q*f for a Q16 factor. It floors the product to Q48.16 and saturates on overflow; the bits equal q.Mul(f.ToQ48()). The sign corrections are branch-free so the method stays inside the inlining budget.

func (Q48) MulAdd16 added in v0.5.0

func (q Q48) MulAdd16(a, b Q16) Q48

MulAdd16 returns q + a*b. The Q16 product is exact in 64 bits. The sum floors it to the Q48.16 grid and saturates on overflow.

Example
package main

import (
	"fmt"

	"github.com/dhannyell/fixed"
)

func main() {
	// Q48 accumulates Q16 products that would saturate Q16 on their own.
	x := fixed.Q16FromInt(200)
	var acc fixed.Q48
	for range 4 {
		acc = acc.MulAdd16(x, x) // 4 × 40000 exceeds the Q16 range of 32768.
	}
	fmt.Println(acc)
	fmt.Println(acc.ToQ16().Eq(fixed.Q16MaxValue()))
}
Output:
160000
true

func (Q48) Neg added in v0.5.0

func (q Q48) Neg() Q48

Neg returns -q. Neg of MinValue saturates to MaxValue.

func (Q48) Raw added in v0.5.0

func (q Q48) Raw() int64

Raw returns the signed Q48.16 bit pattern of q.

func (Q48) Round added in v0.5.0

func (q Q48) Round() Q48

Round returns the nearest integer multiple of Q48One. An exact half rounds away from zero. Round saturates when the result is outside the Q48 range.

func (Q48) Sqrt added in v0.5.0

func (q Q48) Sqrt() Q48

Sqrt returns floor(sqrt(q)). It panics when q is negative. The result cannot overflow.

func (Q48) String added in v0.5.0

func (q Q48) String() string

String returns the exact canonical decimal form of q. For every q, Q48MustParse(q.String()) == q. Use Raw for the exact bit pattern.

func (Q48) Sub added in v0.5.0

func (q Q48) Sub(o Q48) Q48

Sub returns q-o. It saturates on overflow.

func (Q48) ToQ16 added in v0.5.0

func (q Q48) ToQ16() Q16

ToQ16 saturates q to the Q16 range. Both types share one fraction grid, so no rounding occurs.

func (Q48) ToQ32 added in v0.5.0

func (q Q48) ToQ32() Q32

ToQ32 returns q as a Q32.32 value. The fraction grid gets finer, so no rounding occurs. The integer range shrinks, so it saturates outside the Q32 range.

type Rot added in v0.2.1

type Rot struct {
	Sin, Cos Q32
}

Rot is a 2D rotation stored as its sine and cosine.

The zero Rot is not a valid rotation; start from RotIdentity or RotFromTurns.

func RotFromTurns added in v0.2.1

func RotFromTurns(t Q32) Rot

RotFromTurns returns the rotation by the angle t in turns. It shares the kernel and the contract of SinTurns and CosTurns.

Example
package main

import (
	"fmt"

	"github.com/dhannyell/fixed"
)

func main() {
	// A quarter turn sends (1, 0) to (0, 1) exactly.
	r := fixed.RotFromTurns(fixed.Q32FromRatio(1, 4))
	v := r.Apply(fixed.Vec2{X: fixed.Q32One(), Y: fixed.Q32Zero()})
	fmt.Println(v.X, v.Y)
}
Output:
0 1

func RotIdentity added in v0.2.1

func RotIdentity() Rot

RotIdentity returns the rotation by zero turns.

func (Rot) Apply added in v0.2.1

func (r Rot) Apply(v Vec2) Vec2

Apply rotates the vector v by r.

func (Rot) Inv added in v0.2.1

func (r Rot) Inv() Rot

Inv returns the conjugate of r. It is the inverse when r has unit length. Use InvNormalized when accumulated rounding drift may have changed the length.

func (Rot) InvNormalized added in v0.6.0

func (r Rot) InvNormalized() Rot

InvNormalized normalizes r and returns its inverse. A zero r returns the identity, following Normalize.

func (Rot) Mul added in v0.2.1

func (r Rot) Mul(o Rot) Rot

Mul composes the rotations: the result rotates by r then by o. It uses the angle-sum identities, so each product floors once.

func (Rot) Normalize added in v0.2.1

func (r Rot) Normalize() Rot

Normalize rescales r to unit length. A zero r returns the identity.

type Vec2 added in v0.2.1

type Vec2 struct {
	X, Y Q32
}

Vec2 is a 2D vector of Q32 components.

func (Vec2) Add added in v0.2.1

func (v Vec2) Add(o Vec2) Vec2

Add returns the sum of two vectors.

func (Vec2) Distance added in v0.2.1

func (v Vec2) Distance(o Vec2) Q32

Distance returns the distance between v and o.

func (Vec2) DistanceSq added in v0.2.1

func (v Vec2) DistanceSq(o Vec2) Q32

DistanceSq returns the squared distance between v and o.

func (Vec2) Div added in v0.2.1

func (v Vec2) Div(s Q32) Vec2

Div returns v divided by s. It panics when s is zero.

func (Vec2) Dot added in v0.2.1

func (v Vec2) Dot(o Vec2) Q32

Dot returns the dot product of two vectors.

func (Vec2) Len added in v0.2.1

func (v Vec2) Len() Q32

Len returns the length of the vector, floored to the Q32.32 grid.

func (Vec2) LenSq added in v0.2.1

func (v Vec2) LenSq() Q32

LenSq returns the squared length of the vector. It uses the scalar multiplication and addition rules, including saturation.

func (Vec2) Lerp added in v0.2.1

func (v Vec2) Lerp(target Vec2, t Q32) Vec2

Lerp linearly interpolates between v and target by t. t is not clamped.

func (Vec2) Mul added in v0.2.1

func (v Vec2) Mul(s Q32) Vec2

Mul returns v scaled by s.

func (Vec2) Normalize added in v0.2.1

func (v Vec2) Normalize() Vec2

Normalize returns a unit vector with the same direction as v. The zero vector returns the zero vector.

Example
package main

import (
	"fmt"

	"github.com/dhannyell/fixed"
)

func main() {
	v := fixed.Vec2{X: fixed.Q32Zero(), Y: fixed.Q32FromInt(-7)}
	u := v.Normalize()
	fmt.Println(u.X, u.Y)
}
Output:
0 -1

func (Vec2) Sub added in v0.2.1

func (v Vec2) Sub(o Vec2) Vec2

Sub returns the difference between two vectors.

Directories

Path Synopsis
internal
gentable command
Command gentable writes the lookup tables used by the trigonometric functions.
Command gentable writes the lookup tables used by the trigonometric functions.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL