Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/laige-sim/include/laige/sim/particles.h
Original file line number Diff line number Diff line change
Expand Up @@ -507,15 +507,27 @@ class ParticleSystem {
++spawnedTotal_;
}

// An integer v < 2^24 as a Scalar — exact on both backends (the fpx
// raw units; the fp32 24-bit mantissa represents every integer < 2^24
// exactly). Not a static_cast: the fpx16_16 aggregate's single-member
// parenthesized cast is rejected by the older AppleClang of the
// macOS P0 toolchain.
static Scalar fromInt(std::uint32_t v) noexcept {
if constexpr (std::is_same_v<Backend, laige::sim::Fpx16_16>) {
return Scalar{static_cast<std::int32_t>(v)};
} else {
return static_cast<Scalar>(static_cast<std::int32_t>(v));
}
}

// One uniform scalar in [lo, hi]: one 24-bit Prng tap resolved to
// [0, 1) (u / 2^24 — the tap resolution, the header's
// determinism section) and one SimMath lerp (the ADR 0002 rounding
// contract). lo == hi returns exactly lo (no stream perturbation
// beyond the documented draw).
Scalar sampleScalar(Scalar lo, Scalar hi) noexcept {
const std::uint32_t u = rng_.next_range(0, kParticleSampleDenominator);
const Scalar t =
M::div(static_cast<Scalar>(u), static_cast<Scalar>(kParticleSampleDenominator));
const Scalar t = M::div(fromInt(u), fromInt(kParticleSampleDenominator));
return M::lerp(lo, hi, t);
}

Expand Down
19 changes: 17 additions & 2 deletions tests/laige-sim/particles_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,15 +234,30 @@ std::uint64_t particlesStateHash(const ParticleSystem<Backend>& sys) {

// One uniform scalar exactly as the engine samples it (the test-side
// oracle for the 4-draw contract).
// Integer → Scalar, exact on both backends (v < 2^24) — mirrors the
// engine's ParticleSystem::fromInt (the oracle must follow the
// engine's sample path; not a static_cast — the older AppleClang of
// the macOS P0 toolchain rejects the fpx16_16 aggregate cast).
template <typename Backend>
typename M_<Backend>::Scalar sampleFromInt(std::uint32_t v) {
using Scalar = typename M_<Backend>::Scalar;
if constexpr (std::is_same_v<Backend, laige::sim::Fpx16_16>) {
return laige::fpx16_16{static_cast<std::int32_t>(v)};
} else {
return static_cast<Scalar>(static_cast<std::int32_t>(v));
}
}

template <typename Backend>
typename M_<Backend>::Scalar oracleSample(laige::Prng& oracle,
typename M_<Backend>::Scalar lo,
typename M_<Backend>::Scalar hi) {
using M = M_<Backend>;
const std::uint32_t u =
oracle.next_range(0, laige::kParticleSampleDenominator);
const auto t = M::div(static_cast<typename M::Scalar>(u),
static_cast<typename M::Scalar>(laige::kParticleSampleDenominator));
const auto t =
M::div(sampleFromInt<Backend>(u),
sampleFromInt<Backend>(laige::kParticleSampleDenominator));
return M::lerp(lo, hi, t);
}

Expand Down
Loading