fixed

package module
v1.0.1 Latest Latest
Warning

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

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

README

fixed

fixed is a Go library for signed fixed-point arithmetic. It gives the same result bits for the same inputs on every supported architecture, with explicit rules for rounding and overflow.

It includes three numeric formats, 2D vectors, rotations, trigonometry, and batch operations. Choose the format that fits your data; the library has no default numeric type.

Requires Go 1.26.4 or newer. The module is pre-v1, and its import path may change before the first stable release.

go get github.com/dhannyell/fixed

This lib has a WGSL version that reproduces the same bits as the GO version on GPU fixed-wgsl.

Getting started

Construct values from integers, ratios, decimal strings, or raw bits. Methods return new values, so arithmetic reads much like the calculation itself:

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)}

	fmt.Println(a.Distance(b)) // 5

	quarterTurn := fixed.RotFromTurns(fixed.Q32FromRatio(1, 4))
	direction := quarterTurn.Apply(fixed.Vec2{X: fixed.Q32One()})
	fmt.Println(direction.X, direction.Y) // 0 1
}

See the package documentation for the full API.

Choosing a format

A fixed-point value is an integer with a fixed scale. For example, one raw unit in Q16 represents 1/65536. The format determines both the smallest step you can represent and how large a value can get.

Type Format Storage Smallest step Range
Q16 Q16.16 int32 2⁻¹⁶ −2¹⁵ to 2¹⁵ − 2⁻¹⁶
Q32 Q32.32 int64 2⁻³² −2³¹ to 2³¹ − 2⁻³²
Q48 Q48.16 int64 2⁻¹⁶ −2⁴⁷ to 2⁴⁷ − 2⁻¹⁶

Use Q16 when compact storage and 16 fractional bits are enough. Q32 gives you more fractional precision and is the format used by vectors and rotations. Q48 keeps the resolution of Q16 while providing more room for large values and accumulated products.

The underlying fields are private. Use constructors and Raw to move between numeric values and their stored representation.

Creating and printing values

Each format has the same constructor names:

Input Q32 example Meaning
Integer fixed.Q32FromInt(3) The whole number 3
Ratio fixed.Q32FromRatio(1, 3) 1/3, truncated to the Q32 grid
Decimal fixed.Q32MustParse("0.25") A decimal rounded to the nearest representable value
Raw bits fixed.Q32FromRaw(1) One raw unit, or 2⁻³²

Replace Q32 with Q16 or Q48 to use another format. Q48FromInt and Q48FromRatio take int64 arguments, and Q48.Int returns int64; its range does not fit a 32-bit int. The corresponding Q16 and Q32 APIs use int.

There is no float constructor. A floating-point calculation may already have introduced rounding differences before the value reaches this library. Integers, ratios, text, and raw bits make that input boundary explicit.

String produces a canonical decimal that parses back to the same value:

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

Rounding and overflow

Overflow clamps a result to the format's minimum or maximum value. This is called saturation. A build with the fixed_satcounter tag also increments the process-wide SaturationCount counter, which you can use for diagnostics without affecting the calculation.

Enabling the saturation counter

Counting is off by default. Add the fixed_satcounter build tag when you want the diagnostic counter:

go build -tags=fixed_satcounter ./...

The same tag works for WebAssembly. In PowerShell:

$env:GOOS = "js"
$env:GOARCH = "wasm"
go build -tags=fixed_satcounter ./...

Overflow clamps to the same limits and every numeric result keeps the same bits either way. The tag adds diagnostic increments, counter access, and local event counting in batch kernels. The checks that saturate arithmetic are always present. There is no runtime switch to check on each operation.

On js/wasm and wasip1 the counter is a plain variable rather than an atomic. Those targets run on one thread without asynchronous preemption, so an increment cannot be interrupted, and the atomic call would otherwise keep the scalar methods from inlining on WebAssembly.

SaturationCountingEnabled is a compile-time constant. Without the tag it is false, SaturationCount() always returns zero, and ResetSaturationCount() does nothing. You can combine the tag with GOEXPERIMENT=simd.

Before v1.0.0 the counter was on by default and fixed_nosatcounter removed it. If you read SaturationCount(), add fixed_satcounter. If you passed fixed_nosatcounter, drop it; the new default already leaves the counter out.

To measure what the counter costs on your target, run the same benchmark both ways:

go test -run '^$' -bench '^BenchmarkSaturationOverhead' -count=10 .
go test -tags=fixed_satcounter -run '^$' -bench '^BenchmarkSaturationOverhead' -count=10 .

These tests include safe and saturating inputs. Scalar counting happens only when an operation saturates; batch kernels can also spend time collecting events locally. The cost depends on the workload and target.

Arithmetic rules
Operation Rounding rule
Add, Sub Exact when the result fits
Mul Round down to the format's grid
Q16.MulRound Nearest step; exact halves go toward positive infinity
Div, FromRatio Truncate toward zero
Sqrt Round down to the format's grid
Round Nearest integer; exact halves go away from zero
MustParse Nearest representable value; exact halves go away from zero
Int Integer part, truncated toward zero

Rounding down and truncating toward zero differ for negative values. The library keeps that distinction: multiplication rounds down, while division truncates toward zero. Results outside the target range saturate.

The nearest-step operations break an exact half toward positive infinity, not away from zero like Round. A negative half therefore moves toward zero.

Division by zero, a zero denominator in FromRatio, and the square root of a negative value panic.

Saturation also makes the order of addition matter. Near the limits, a.Add(b).Add(c) can differ from a.Add(b.Add(c)). Leave enough room in an accumulator if you need to avoid that effect.

Converting between formats

Conversions preserve a value exactly when the destination has enough range and fractional precision. Dropping fractional bits rounds down; exceeding the destination's range saturates.

Conversion Behavior
Q16.ToQ32 Exact; more range and finer resolution
Q16.ToQ48 Exact; more range, same resolution
Q32.ToQ16 Rounds down; saturates outside the Q16 range
Q32.ToQ16Round Nearest step; saturates outside the Q16 range
Q32.ToQ48 Rounds down; the wider range needs no saturation
Q32.ToQ48Round Nearest step; the wider range needs no saturation
Q48.ToQ16 Same resolution; saturates outside the Q16 range
Q48.ToQ32 Exact when in range; saturates outside the Q32 range
Accumulating Q16 products

Q48.MulAdd16 multiplies two Q16 values, rounds the product down to Q48.16, and adds it to the accumulator with saturation. Starting from zero, a sum of up to 2¹⁶ full-range Q16 products fits in Q48.

var acc fixed.Q48
for i := range a {
	acc = acc.MulAdd16(a[i], b[i])
}
dot := acc.ToQ16() // Convert back when you need the narrower value.

Q48.MulAdd16Round rounds each product to the nearest step instead, with exact ties toward positive infinity. The floored form loses less than one step per product, always in the same direction, and a long sum accumulates that.

Q48.Mul16 multiplies a Q48 value by a Q16 factor. It rounds down and saturates just like Mul with the factor converted to Q48.

Vectors, rotations, and angles

Vec2 holds two Q32 components and supports addition, scaling, dot products, length, normalization, distance, and interpolation.

Three details matter near the numeric limits:

  • LenSq uses scalar multiplication and addition, so it can saturate even when the length itself fits. Len uses a 128-bit intermediate and saturates only if the final magnitude is too large.
  • Normalize rescales components before squaring them to avoid intermediate overflow and underflow.
  • Lerp evaluates Sub, Mul, and Add with their usual saturation rules. If target - v overflows, t=1 may miss the target and t=0 still records that intermediate overflow. Keep the intermediate values in range when exact endpoints matter.

Angles are measured in turns: Q32One() is a full revolution, Q32Half() is a half turn, and Q32FromRatio(1, 4) is a quarter turn. This represents a circle directly in the fractional bits without reducing angles through an approximation of pi.

SinTurns, CosTurns, and Atan2Turns use committed lookup tables with linear interpolation. Sine and cosine have a maximum absolute error of 2⁻²⁰; the angle returned by Atan2Turns has a maximum absolute error of 2⁻²⁰ turns. Its output is in [-1/2, 1/2]: the negative x axis returns +1/2, while values just below it can round to -1/2.

Rot stores sine and cosine together. You can apply, compose, or invert a rotation without another trigonometric lookup. Construct it with RotIdentity or RotFromTurns; the zero value is not a valid rotation. Inv is the conjugate and acts as an inverse for a unit rotation. Use InvNormalized when accumulated rounding drift also needs to be corrected.

Working with slices

The batch API applies arithmetic or conversions to whole slices:

Functions Work performed
BatchAdd16, BatchSub16, BatchMul16, BatchClamp16 Element-wise Q16 arithmetic and clamping
BatchQ32FromQ16, BatchQ16FromQ32 Format conversion using the scalar conversion rules
BatchDot16 Sum of Q16 products in a Q48 accumulator
BatchQ48Mul16 Element-wise Q48 multiplication by Q16 factors

All slices in a call must have the same length. For element-wise operations on the same format, the destination can be exactly the same slice as a source. Other overlap is undefined.

Batch arithmetic records saturation events in one counter update per call when needed. Element-wise results and saturation totals match the scalar operations.

BatchDot16 uses eight partial sums: element i goes into partial i mod 8, then the partials are combined in a balanced tree. Every implementation uses this order. It gives the same result as a serial MulAdd16 loop when no intermediate sum saturates; saturation can make those two orders differ.

SIMD support

Default builds use scalar kernels. With Go 1.27 or newer and GOEXPERIMENT=simd, the package selects AVX2 on supported amd64 CPUs or NEON on arm64 at initialization:

GOEXPERIMENT=simd go build ./...

BatchPath() reports "scalar", "avx2", or "neon". NEON covers the six Q16 arithmetic and conversion functions; BatchDot16 and BatchQ48Mul16 remain scalar on arm64. CI checks that vector kernels preserve the scalar results and saturation counts.

Lanes

The lane API keeps a fixed number of independent values together while a calculation is in progress. Load an array or splat one value, compose lane operations, then store the result:

var a, b, dst [fixed.LaneWidth]fixed.Q16

la := fixed.LoadLane16(&a)
lb := fixed.LoadLane16(&b)
bias := fixed.SplatLane16(fixed.Q16Half())
la.Mul(lb).Add(bias).Store(&dst)

LaneWidth is 8 for the AVX2 path and 4 for the NEON and generic paths. LanePath() reports "avx2", "neon", or "generic". In an amd64 SIMD build, check LanesAvailable() before using the lane API; it reports false on a CPU without AVX2, where calling lane operations is undefined.

Type Operations
Lane16 SplatLane16, LoadLane16, Store, Add, Sub, Neg, AddWrap, SubWrap, Mul, MulRound, ScaleDown, ScaleDownRound, ScaleUp, MulAdd, MulSub, Min, Max, SymClamp, Greater, Equals, ToLane48
Mask16 Or, AllZero, BlendLane16
Shift16 SplatShift16, LoadShift16
Lane48 SplatLane48, LoadLane48, Store, Add, Sub, AddWrap, SubWrap, MulAdd16, MulAdd16Round, ToLane16

MulAdd and MulSub perform two ordered operations rather than a fused operation. Mul and MulAdd16 round down, while the Round forms take the nearest step. Every operation follows its scalar Q16 or Q48 saturation rules.

ScaleDown and ScaleDownRound divide by a power of two that each lane picks for itself, which a single instruction does on both SIMD paths. Up to an amount of 16 they give the same bits as Mul and MulRound by that power of two. Past 16 the divisor leaves the Q16 grid: the multiply gives zero, while the shift keeps going. An amount above 31 acts like 31.

ScaleUp goes the other way. A left shift can leave the Q16 range, so it saturates and records the event. NEON has a saturating variable shift for it; the AVX2 path emulates one with a shift back and a compare. Up to an amount of 14 it gives the same bits as Mul by that power of two; two to the fifteenth is already off the grid.

AddWrap and SubWrap skip the overflow check on both types. A result outside the format wraps and records no saturation event, so the caller must bound the operands itself. They serve a caller that can prove the bound from the surrounding code and does not want to pay for the check. The AVX2, NEON, and generic paths produce identical result bits and saturation counts.

Performance

Performance depends on how the operations are used. The tables below separate three questions:

Measurement What it tells you
Dependent operations The time for a step that needs the previous result
Independent inputs The throughput of a loop whose arithmetic can overlap
Representative workloads The cost of a complete numerical task, including loops and data access

All measurements used an AMD Ryzen 7 5800X3D, Windows/amd64, and Go 1.26.4. The first two tables were measured on 2026-09-05; the workloads on 2026-09-06. Each value is the median of ten runs at 500 ms per benchmark. Batch results use a separate procedure described below. All published tables below were measured with saturation counting enabled.

These are measurements of specific loops and inputs. They include compiler and machine effects, and do not establish that fixed-point arithmetic is generally faster or slower than float. Sub-nanosecond results are particularly sensitive to loop overhead and code placement.

Dependent operations

In Benchmark*Latency, each result feeds the next iteration. Some tests need extra arithmetic to keep values in range: the multiplication test, for example, measures Mul + Add. The last column lists the complete step. Loop control is included in every row.

Chain Fixed ns/step Work included in each step
Q16.Add 0.543 Add
Q16.Mul 1.89 Mul + Add
Q16.Div 3.37 Div + Add
Q16.Sqrt 9.9 Sqrt + Mul + Add + input update
Q32.Add 0.382 Add
Q32.Mul 2.74 Mul + Add
Q32.Div 4.76 Div + Add
Q32.Sqrt 11.3 Sqrt + Mul + Add + input update
Q48.Add 0.507 Add
Q48.Mul 2.6 Mul + Add
Q48.MulAdd16 0.86 MulAdd16 + input update
Q48.Div 4.49 Div + Add
Q48.Sqrt 11.1 Sqrt + Mul + Add + input update
Vec2.Dot 4.51 Dot + vector reconstruction
Vec2.Len 16.5 Len + two Mul + vector reconstruction
Vec2.Normalize 27.1 Normalize + two Mul + component swap
Vec2.Normalize axial 3.97 Normalize + Mul + vector reconstruction
Rot.Apply 5.89 Apply
Rot.Mul 6.05 Mul
Rot.Normalize 26.4 Normalize + two Mul + component swap

These are chain costs, not isolated instruction latencies. This table has no float comparison.

go test -run '^$' -bench '^Benchmark(Q16|Q32|Q48|Vec2|Rot).*Latency$' -benchtime=500ms -count=10 .
Independent inputs

BenchmarkCompare reads prepared inputs and rotates between four accumulators, allowing more work to overlap. Each iteration evaluates one named operation; the sine/cosine row evaluates one pair. Array reads, indexing, accumulation, and loop control are timed. Combining the four accumulators at the end is excluded.

The accumulators still have dependencies. In particular, Q48.MulAdd16 uses them as operation inputs, so its row measures four interleaved accumulation chains. The table measures the throughput of these loops, not bare arithmetic instructions. To convert ns/iteration to millions of iterations per second, use 1000 / ns.

Independent-input kernel Fixed ns/iteration Float ns/iteration Fixed time vs float in this loop
Q16.Add 0.885 0.767 +15% (slower)
Q16.Mul 1.25 0.534 +134% (slower)
Q16.Div 1.29 0.827 +56% (slower)
Q16.Sqrt 2.61 1.21 +115% (slower)
Q32.Add 0.654 0.632 +3% (slower)
Q32.Mul 1.96 0.597 +228% (slower)
Q32.Div 3.93 1.12 +251% (slower)
Q32.Sqrt 3.96 2.09 +89% (slower)
Q48.Add 0.691 0.625 +11% (slower)
Q48.Mul 2.02 0.619 +227% (slower)
Q48.MulAdd16 1.47 0.583 +151% (slower)
Q48.Div 4.09 1.08 +277% (slower)
Q48.Sqrt 4.26 2.09 +103% (slower)
Vec2.Dot 5.54 0.945 +486% (slower)
Vec2.Len 9.51 2.17 +338% (slower)
Vec2.Normalize 20.7 4.32 +378% (slower)
Vec2.Normalize axial 3.69 3.21 +15% (slower)
Rot.Apply 8.16 1.22 +567% (slower)
Rot.Mul 8.59 1.19 +620% (slower)
Rot.Normalize 21.5 4.34 +396% (slower)
SinTurns + CosTurns 5.22 11.5 -54% (faster)
RotFromTurns 4.5 11.7 -62% (faster)
Atan2Turns 5.75 12.4 -53% (faster)

The percentage is (fixed median / float median - 1) × 100, calculated before rounding the displayed times. Negative means fixed took less time; positive means it took more time in this test.

Q16 is compared with float32. The other formats, vectors, and rotations use float64. The Q48.MulAdd16 reference multiplies in float32 and accumulates the products in float64. These float versions do not reproduce fixed-point rounding, saturation, or bit guarantees.

Older README results used a single accumulator, which limited float in particular. Those percentages are not directly comparable with this table. The older Benchmark*Throughput tests in bench_test.go also include input generation and a serial accumulation; they are not used here.

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

The workload benchmarks measure two complete numerical tasks. Both reuse 256-element arrays that fit in cache, avoid saturation, and allocated no memory during the measured work. They model common usage patterns; they are not measurements of a complete application.

Transform256 rotates and translates 256 Q32 points into an output array. The float64 version uses the same coordinates and rotation coefficients. The measurement includes arithmetic, calls, loops, reads, and writes. Input preparation and construction of the rotation are excluded.

Dot256 reduces 256 Q16 pairs into a Q48 accumulator. Each task starts from zero and uses one accumulator, because that dependency is part of this calculation. The float version reads float32 arrays, promotes the inputs, and multiplies and accumulates in float64. The fixed version rounds each product down to Q48.16, so the results are not bit-equivalent. Input preparation and the final benchmark result assignment are excluded.

Task (256 elements) Fixed ns/task Float ns/task Fixed task time vs float
Transform256 2110 271 +677% (slower)
Dot256 351 193 +82% (slower)
go test -run '^$' -bench '^BenchmarkWorkload' -benchmem -benchtime=500ms -count=10 .

For larger datasets, frequent saturation, or application-level decisions, measure the workload you actually intend to run. To compare library versions, use the same benchmark source and Go version, alternate runs on the same machine, and analyze repeated samples with a tool such as benchstat. Keep the absolute fixed and float times: a changed percentage alone does not show which side changed.

Batch operations

This table compares batch calls with hand-written loops. Values are ns per element for 1024 elements, using medians of twenty runs over two sessions on the same machine. Scalar results use the default build; AVX2 results use Go 1.27 with GOEXPERIMENT=simd. The batch measurements include the exported call, length checks, and any saturation-counter update.

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

The hand-written loops call the corresponding scalar method for each element. For BatchDot16, the reference is a serial Q48.MulAdd16 loop; for BatchQ48Mul16, it is Q48.Mul16.

Scalar batches took less time than those loops in this workload, except for BatchDot16. Its eight partial sums preserve the same reduction order across implementations, but cost more here than the serial reference.

NEON timings are omitted because the available measurements came from shared CI runners. BatchDot16 and BatchQ48Mul16 currently stay scalar on arm64; the tested vector candidates did not meet the project's twofold speedup threshold. Scalar division also costs more on arm64 because its 128-bit division uses a software routine. Square root uses a hardware seed followed by integer corrections, without division.

Implementation and compatibility

The library is one package with no external runtime dependencies. Portable code imports math, math/bits, and sync/atomic. Some operations use a floating-point seed, then check and correct it with integer arithmetic so the final bits follow the fixed-point contract.

SIMD builds also use simd/archsimd and unsafe. All unsafe operations are in batch16_raw.go, where Q16, Q32, and Q48 slices are viewed as their underlying integer words. Compile-time checks enforce their sizes.

Scalar arithmetic and decimal conversion live in q* and decimal* files; vectors, rotations, and trigonometry live in vec2*, rot*, and trig*. The constructors keep the format explicit. Applications can define local aliases if they choose to standardize on one type.

Raw representation, rounding, and saturation are part of the public contract. An independent implementation must preserve all three to exchange values reliably. Changing them is a compatibility change.

Development

Run the checks with:

go test ./...
go vet ./...
go run ./internal/gentable -check
golangci-lint run ./...

The trigonometric tables and outputs between table entries have fixed bit checksums. gentable -check regenerates tables for comparison without changing the files; CI runs it on amd64. Updating the checksums requires an intentional compatibility change, even if the numerical error remains within tolerance.

Batch benchmarks cover steady workloads, empty slices, vector boundaries, scalar tails, and different amounts of saturation:

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

Their MB/s metric counts logical slice reads and writes, not physical memory traffic: 12 bytes per element for add/sub/mul and conversions, 8 for clamp and dot, and 20 for Q48Mul16. The dot result and benchmark result storage are excluded from that count.

License

MIT.

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. Build with -tags=fixed_satcounter to add diagnostic counting, including local batch counts. Without it SaturationCountingEnabled is false, SaturationCount returns zero and ResetSaturationCount is a no-op. Saturation and rounding of numeric results are unchanged.

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. The negative x axis returns +1/2; quantization just below it can return -1/2. It reduces the input to a ratio in [0, 1], truncates it 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. Vec2.Lerp composes Sub, Mul, and Add, including intermediate saturation. Keep those intermediates in range to preserve the interpolation endpoints.

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

View Source
const LaneWidth = 4

LaneWidth is the number of values in a lane type on this build path.

View Source
const SaturationCountingEnabled = false

SaturationCountingEnabled reports whether this build records saturation events. Build with -tags=fixed_satcounter to enable diagnostic counting. Arithmetic still saturates and produces the same result bits in either mode.

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 LanePath added in v0.8.0

func LanePath() string

LanePath reports the compiled lane implementation: "avx2", "neon", or "generic". Use LanesAvailable before using the amd64 SIMD implementation.

func LanesAvailable added in v0.8.0

func LanesAvailable() bool

LanesAvailable reports whether lane operations may be called. It is false only in an amd64 SIMD build on a CPU without AVX2. Calling any lane operation in that case is undefined. Other build paths always return true.

func ResetSaturationCount

func ResetSaturationCount()

ResetSaturationCount has no effect unless the build enables counting.

func SaturationCount

func SaturationCount() uint64

SaturationCount returns zero unless the build enables counting.

Types

type Lane16 added in v0.8.0

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

Lane16 holds LaneWidth Q16 values for register-resident lane-wise work. Its methods produce the same bits as the corresponding scalar operations. Mul rounds down with the BatchMul16 arithmetic-shift rule, which is also the Q16.Mul rule. Each saturated lane adds one event to SaturationCount when counting is enabled. MulAdd and MulSub execute two operations and count the multiplication and the addition or subtraction separately.

In an amd64 SIMD build, callers must check LanesAvailable before constructing or using a Lane16. Calling a lane operation without AVX2 is undefined.

The value is opaque: use the methods; the layout differs by path.

func BlendLane16 added in v0.8.0

func BlendLane16(m Mask16, a, b Lane16) Lane16

BlendLane16 selects a where m is set and b where m is clear.

func LoadLane16 added in v0.8.0

func LoadLane16(p *[LaneWidth]Q16) Lane16

LoadLane16 loads LaneWidth Q16 values from p.

func SplatLane16 added in v0.8.0

func SplatLane16(q Q16) Lane16

SplatLane16 returns a Lane16 with every lane set to q.

func (Lane16) Add added in v0.8.0

func (a Lane16) Add(b Lane16) Lane16

Add returns a+b lane-wise, with Q16 saturation.

func (Lane16) AddWrap added in v1.0.0

func (a Lane16) AddWrap(b Lane16) Lane16

AddWrap returns a+b lane-wise without overflow detection. A sum outside Q16 wraps and records no event. The caller must bound both operands; Add is the checked form.

func (Lane16) Equals added in v0.8.0

func (a Lane16) Equals(b Lane16) Mask16

Equals reports a==b in each lane.

func (Lane16) Greater added in v0.8.0

func (a Lane16) Greater(b Lane16) Mask16

Greater reports a>b in each lane.

func (Lane16) Max added in v0.8.0

func (a Lane16) Max(b Lane16) Lane16

Max returns the larger value in each lane.

func (Lane16) Min added in v0.8.0

func (a Lane16) Min(b Lane16) Lane16

Min returns the smaller value in each lane.

func (Lane16) Mul added in v0.8.0

func (a Lane16) Mul(b Lane16) Lane16

Mul returns a*b lane-wise, rounded down to Q16.16 and saturated.

func (Lane16) MulAdd added in v0.8.0

func (a Lane16) MulAdd(b, c Lane16) Lane16

MulAdd returns a.Add(b.Mul(c)). It never fuses the two operations.

func (Lane16) MulRound added in v1.0.0

func (a Lane16) MulRound(b Lane16) Lane16

MulRound returns a*b lane-wise, rounded to the nearest Q16.16 step with exact ties toward positive infinity, and saturated.

func (Lane16) MulSub added in v0.8.0

func (a Lane16) MulSub(b, c Lane16) Lane16

MulSub returns a.Sub(b.Mul(c)). It never fuses the two operations.

func (Lane16) Neg added in v1.0.0

func (a Lane16) Neg() Lane16

Neg returns -a lane-wise, with Q16 saturation. It subtracts from zero rather than multiplying by -1, which costs a widening product on the SIMD paths. The two agree in every lane, saturation included.

func (Lane16) ScaleDown added in v1.0.0

func (a Lane16) ScaleDown(s Shift16) Lane16

ScaleDown returns a divided by two raised to the matching lane of s. It rounds toward negative infinity, which is the Mul rule, and it cannot overflow or saturate.

For an amount up to 16 the result is the same bits as Mul by the Q16 value of two raised to minus that amount. Past 16 that value is not on the Q16 grid and the two disagree: Mul gives zero, ScaleDown keeps shifting.

func (Lane16) ScaleDownRound added in v1.0.0

func (a Lane16) ScaleDownRound(s Shift16) Lane16

ScaleDownRound returns a divided by two raised to the matching lane of s, rounded to the nearest step with exact ties toward positive infinity. It cannot overflow or saturate.

For an amount up to 16 the result is the same bits as MulRound by the Q16 value of two raised to minus that amount. ScaleDown is the truncating form and costs one instruction, so prefer it when the rounding does not matter.

func (Lane16) ScaleUp added in v1.0.0

func (a Lane16) ScaleUp(s Shift16) Lane16

ScaleUp returns a multiplied by two raised to the matching lane of s, with Q16 saturation. ScaleDown cannot overflow; this direction can, and a lane past the Q16 range clamps to the nearer limit and records an event.

For an amount up to 14 the result is the same bits as Mul by the Q16 value of two raised to that amount. Two to the fifteenth is already off the Q16 grid, so past 14 only the shift keeps going.

func (Lane16) Store added in v0.8.0

func (a Lane16) Store(p *[LaneWidth]Q16)

Store writes every lane of a to p.

func (Lane16) Sub added in v0.8.0

func (a Lane16) Sub(b Lane16) Lane16

Sub returns a-b lane-wise, with Q16 saturation.

func (Lane16) SubWrap added in v1.0.0

func (a Lane16) SubWrap(b Lane16) Lane16

SubWrap returns a-b lane-wise without overflow detection. A difference outside Q16 wraps and records no event. The caller must bound both operands; Sub is the checked form.

func (Lane16) SymClamp added in v0.8.0

func (a Lane16) SymClamp(limit Lane16) Lane16

SymClamp returns Max(-limit, Min(a, limit)) lane-wise. Negating a minimum limit saturates to Q16MaxValue and records one event for that lane.

func (Lane16) ToLane48 added in v0.8.0

func (a Lane16) ToLane48() Lane48

ToLane48 widens every lane to Q48 exactly.

type Lane48 added in v0.8.0

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

Lane48 holds LaneWidth Q48 values for register-resident accumulation. Its methods produce the same bits and saturation events as the corresponding scalar Q48 operations applied LaneWidth times.

The value is opaque: use the methods; the layout differs by path.

func LoadLane48 added in v0.8.0

func LoadLane48(p *[LaneWidth]Q48) Lane48

LoadLane48 loads LaneWidth Q48 values from p.

func SplatLane48 added in v0.8.0

func SplatLane48(q Q48) Lane48

SplatLane48 returns a Lane48 with every lane set to q.

func (Lane48) Add added in v0.8.0

func (a Lane48) Add(b Lane48) Lane48

Add returns a+b lane-wise, with Q48 saturation.

func (Lane48) AddWrap added in v1.0.0

func (a Lane48) AddWrap(b Lane48) Lane48

AddWrap returns a+b lane-wise without overflow detection. A sum outside Q48 wraps and records no event. The caller must bound both operands; Add is the checked form.

func (Lane48) MulAdd16 added in v0.8.0

func (a Lane48) MulAdd16(b, c Lane16) Lane48

MulAdd16 adds the exact b*c Q16 product to each Q48 accumulator lane. Each product is rounded down to the shared Q48.16 grid before the saturating add.

func (Lane48) MulAdd16Round added in v1.0.0

func (a Lane48) MulAdd16Round(b, c Lane16) Lane48

MulAdd16Round rounds each Q16 product to the nearest Q48.16 step with exact ties toward positive infinity before the saturating lane-wise add.

func (Lane48) Store added in v0.8.0

func (a Lane48) Store(p *[LaneWidth]Q48)

Store writes every lane of a to p.

func (Lane48) Sub added in v0.8.0

func (a Lane48) Sub(b Lane48) Lane48

Sub returns a-b lane-wise, with Q48 saturation.

func (Lane48) SubWrap added in v1.0.0

func (a Lane48) SubWrap(b Lane48) Lane48

SubWrap returns a-b lane-wise without overflow detection. A difference outside Q48 wraps and records no event. The caller must bound both operands; Sub is the checked form.

func (Lane48) ToLane16 added in v0.8.0

func (a Lane48) ToLane16() Lane16

ToLane16 narrows every lane with Q48.ToQ16 saturation semantics.

type Mask16 added in v0.8.0

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

Mask16 holds LaneWidth comparison results. Each lane is all ones when set and all zeros when clear.

The value is opaque: use the methods; the layout differs by path.

func (Mask16) AllZero added in v0.8.0

func (m Mask16) AllZero() bool

AllZero reports whether every mask lane is clear.

func (Mask16) Or added in v0.8.0

func (m Mask16) Or(n Mask16) Mask16

Or returns the lane-wise union of m and n.

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) MulRound added in v1.0.0

func (q Q16) MulRound(o Q16) Q16

MulRound returns q*o rounded to the nearest Q16.16 step, with exact ties toward positive infinity. It 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. The negative x axis returns +1/2; quantization just below it can return -1/2. 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) ToQ16Round added in v1.0.0

func (q Q32) ToQ16Round() Q16

ToQ16Round narrows q to the nearest Q16.16 step, with exact ties toward positive infinity. It saturates outside the Q16 range.

func (Q32) ToQ48 added in v0.5.0

func (q Q32) ToQ48() Q48

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

func (Q32) ToQ48Round added in v1.0.0

func (q Q32) ToQ48Round() Q48

ToQ48Round narrows q to the nearest Q48.16 step, with exact ties toward positive infinity. 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) MulAdd16Round added in v1.0.0

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

MulAdd16Round returns q + a*b, with the product rounded to the nearest Q48.16 step and exact ties toward positive infinity. MulAdd16 floors the product instead. The sum saturates on overflow either way.

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 Shift16 added in v1.0.0

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

Shift16 holds LaneWidth shift amounts, one for each lane of a Lane16.

The value is opaque: build it with SplatShift16 or LoadShift16.

func LoadShift16 added in v1.0.0

func LoadShift16(p *[LaneWidth]uint8) Shift16

LoadShift16 loads LaneWidth shift amounts from p, with the SplatShift16 rule for an amount above 31. It widens every amount, so it is not a single load.

func SplatShift16 added in v1.0.0

func SplatShift16(n uint8) Shift16

SplatShift16 returns a Shift16 with every lane set to n. An amount above 31 is stored as 31; both shift a lane to zero, or to -1 raw when it is negative.

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. Sub, Mul, and Add each apply their scalar saturation rules. Intermediate saturation can change the endpoint at t=1 and is counted even at t=0. Keep the intermediates in range when endpoints must be exact.

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
cmd
fixedtrace command
Command fixedtrace writes one text trace per operation.
Command fixedtrace writes one text trace per operation.
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