renew-fixed 0.1.1

Fixed-point arithmetic for simulation code that must reproduce bit-for-bit
Documentation
  • Coverage
  • 61.54%
    8 out of 13 items documented1 out of 1 items with examples
  • Size
  • Source code size: 157.27 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 965.22 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 3s Average build duration of successful builds.
  • all releases: 2s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • renew-engine/renew
    5 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • CagdasErturk

renew-fixed

Fixed-point arithmetic for simulation code whose output has to reproduce bit-for-bit on every target.

Status: bootstrap. Interface churn expected. See Cargo.toml for the machine-readable manifest — maturity, dependencies and core status live there, not here.

Why this exists

Rust guarantees IEEE 754 semantics for f32 and f64 operators, and the guarantee stops there. sin, cos and their siblings come from the platform's maths library and may differ between targets by an ulp. A simulation that calls them cannot claim to reproduce across machines.

Integer arithmetic is bit-identical everywhere, with nothing to police. That is the entire argument, and it is why physics is written in this type rather than in floats.

The representation

Q47.16 in an i64: 16 fractional bits, resolution 2⁻¹⁶ ≈ 0.0000153, range ±2⁴⁷ ≈ ±1.4 × 10¹⁴.

Sixteen rather than thirty-two fractional bits because physics squares things. A squared value has to fit the type that stores it, so the range that matters is not what is representable but what is squarable — the square root of the representable range:

representable squarable
Q47.16 ±1.4 × 10¹⁴ ±1.2 × 10⁷
Q32.32 ±2.1 × 10⁹ ±4.6 × 10⁴

Two hundred and fifty-six times the working room, for a resolution already finer than anything a game perceives.

What the API refuses

No f32 or f64 in any signature. Converting to a float is a presentation concern; it lives in the maths crate, which depends on this one. A simulation cannot reach that crate, so it cannot perform the conversion — enforced by the structure checker's float-closure rule rather than by anyone remembering.

Values are constructed from integers: from_int, from_ratio (how 9.81 is written without a float ever existing — from_ratio(981, 100)), and from_bits for serialisation. All three are const fn, because game constants are written at compile time.

Two behaviours worth knowing before you use it

Multiplication rounds to nearest, ties away from zero — not by shifting. The obvious implementation, (a as i128 * b as i128) >> 16, uses an arithmetic shift, which rounds toward negative infinity and is therefore asymmetric under negation: (-a) * b and -(a * b) differ for some inputs. Deterministic, and still wrong for physics, because a body moving left and the same body moving right would then accumulate different error. There is a property test for this, and it fails against the shift — verified by swapping one in.

Overflow saturates, in every build profile, and is counted. It never wraps and never differs between debug and release: behaviour that differed by profile would mean the release behaviour is the one no test ever exercises. Saturation is silent by itself, so saturations() reports how many times it happened on this thread, and a test asserts zero the way the frame loop's allocation gate does. The counter is thread-local, which is both what the threading standard already sanctions and the right shape — simulation is single-threaded, so per-thread is per-simulation.

Vectors

Vec2 and Vec3 over Fixed, as two concrete types rather than one generic over dimension — the same choice the physics contract makes for the same reason: a dimension-generic vocabulary infects every signature with a bound, and writing dot twice costs less than every caller reading one.

Two operations are worth knowing about because physics leans on them.

slide_along removes a displacement's component along a unit normal. That is the whole of move-and-slide's inner step, named here so an implementation does not spell it out at each call site and get the sign wrong at one of them.

perpendicular is a quarter turn, and it is exact — a swap and a negation, no trigonometry, no rounding. General rotation is not available: it needs trigonometric functions this type does not have, which is why the physics contract defers rotated shapes.

normalize is fallible, because the zero vector is a value a simulation legitimately produces and an assertion on a path that runs every frame is the wrong shape. The result is unit-length only to the type's resolution — a four parts in 65536 — so callers wanting exact equality should compare squared lengths against a tolerance rather than expecting exactly one.

Angles and rotation

Angle is a binary angle: a full turn is 2³² units, so an angle is a u32 and wrapping is exact integer overflow. That is the reason for the representation rather than a convenience. In radians the modulus is 2π, which is irrational and therefore not representable, so every wrap would lose precision and two machines wrapping at different moments would drift apart. Here, adding a full turn is the identity, exactly, forever — and from_degrees lands on the cardinal angles exactly.

sin and cos read a 513-entry quarter-turn table with rounded linear interpolation, the other three quadrants coming from symmetry. Measured worst error: 1.0322 units in the last place, over a sweep of the whole circle against a double-precision reference. That is the floor for a table of any size — its entries are each rounded to half a unit, interpolating between two inherits that, and the interpolation rounds once more. Sixteen times the entries buys 0.03 of a unit, which is why the table is 2 KB rather than 32.

The table is generated, not hand-written, and a test checks every entry against the reference. That test is the only place in this crate's world that uses floating point, and deliberately: the shipped code has none, and the proof that its table is right needs one.

Vec2::rotate follows. Note that perpendicular is not the same as rotating by a quarter turn — it is exact, where rotation rounds — so the quarter turn keeps its own operation.

Extension points

None. This is a value type; it has no trait to implement and no runtime polymorphism. Growing it means adding operations here.