Greenfield C++20 core for the unified firmware+WASM rewrite (architecture.md
streams, meml-dn7). Header-only, platform-agnostic, no heap, no virtual
dispatch.
Components:
- perf.hpp memory section + inlining macros, RP2040/RP2350-aware,
inert on host/Emscripten
- types.hpp stereosample_t (mirrors firmware AudioDriver API),
sample_t/param_t aliases, DriverConfig negotiation struct
- concepts.hpp MLEngine, AudioEngine, Mode (architecture §4.1-4.3)
- fixed_buffer.hpp std::array-backed cursor; replaces std::vector in hot paths
- ring_buffer.hpp SPSC lock-free FIFO, power-of-two capacity, atomic
head/tail; replaces pico/util/queue in core
- rng.hpp xoshiro256+ with splitmix64 seeding, uniform/signed/
gaussian-via-3-uniforms (matches legacy MoveWeights shape)
- math.hpp clamp01, fast_sigmoid (tanh-Padé, ~1.2% max err on [-6,6]),
exact_sigmoid, fast_exp, named Curve catalog (linear/exp/
log/square/sqrt/sigmoid/cubic) — TypeScript twin lives in
playground/src/output/curves.ts (stream 5)
Performance discipline (Chris's rules):
- No heap, no std::vector, no malloc/new in core
- All float literals carry .f suffix
- Memory section attrs syntactically present, inert on non-firmware builds
90 lines
3.3 KiB
C++
90 lines
3.3 KiB
C++
// nisps/core/rng.hpp — deterministic PRNG used everywhere we need
|
||
// reproducibility (weight init, RL noise, dataset shuffle, golden vectors).
|
||
//
|
||
// xoshiro256+ — Blackman & Vigna (2018). 64-bit state, 4× u64 state words,
|
||
// passes BigCrush, fast (a handful of ALU ops), no branches in the hot path.
|
||
// We use the `+` variant rather than `**` because we only consume the high
|
||
// bits for floats; the low-bit linearity that pesters `+` for integer use is
|
||
// irrelevant once you mask off the float mantissa.
|
||
//
|
||
// Seeding: we splitmix64 the user-provided u64 seed to fan it out across the
|
||
// four state words, so seed=0 / seed=1 / seed=N all produce uncorrelated
|
||
// streams. (Pure xoshiro fails badly for all-zero state.)
|
||
//
|
||
// Gaussian: sum-of-three-uniforms. Cheaper than Box-Muller (no log/sin) and
|
||
// the existing JS engine uses the same shape (`gen_randn = sum of 3
|
||
// uniforms scaled by speed`), so by matching it we keep firmware ↔ browser
|
||
// noise statistically equivalent without a parity headache. Box-Muller would
|
||
// be more accurate, but we want compatibility with the legacy MoveWeights
|
||
// shape — see recon/01-ml-stack.md §3.
|
||
|
||
#pragma once
|
||
|
||
#include <cstdint>
|
||
|
||
namespace nisps {
|
||
|
||
class Rng {
|
||
public:
|
||
explicit Rng(std::uint64_t seed) noexcept { this->seed(seed); }
|
||
|
||
void seed(std::uint64_t s) noexcept {
|
||
// splitmix64: avalanche the seed into four uncorrelated state words.
|
||
for (int i = 0; i < 4; ++i) {
|
||
s += 0x9E3779B97F4A7C15ull;
|
||
std::uint64_t z = s;
|
||
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull;
|
||
z = (z ^ (z >> 27)) * 0x94D049BB133111EBull;
|
||
z = z ^ (z >> 31);
|
||
state_[i] = z;
|
||
}
|
||
// Guarantee non-zero state. Astronomically unlikely to be all-zero
|
||
// post-splitmix anyway, but defense in depth.
|
||
if ((state_[0] | state_[1] | state_[2] | state_[3]) == 0ull) {
|
||
state_[0] = 1ull;
|
||
}
|
||
}
|
||
|
||
inline std::uint64_t next_u64() noexcept {
|
||
const std::uint64_t result = state_[0] + state_[3];
|
||
const std::uint64_t t = state_[1] << 17;
|
||
state_[2] ^= state_[0];
|
||
state_[3] ^= state_[1];
|
||
state_[1] ^= state_[2];
|
||
state_[0] ^= state_[3];
|
||
state_[2] ^= t;
|
||
state_[3] = rotl_(state_[3], 45);
|
||
return result;
|
||
}
|
||
|
||
// Uniform float in [0, 1). Standard "top 24 bits → mantissa" trick.
|
||
inline float next_float_uniform() noexcept {
|
||
// 1.f / 2^24 = 5.9604644775390625e-08
|
||
constexpr float kInv = 1.f / 16777216.f;
|
||
return static_cast<float>(next_u64() >> 40) * kInv;
|
||
}
|
||
|
||
// Uniform float in [-1, 1).
|
||
inline float next_float_signed() noexcept {
|
||
return next_float_uniform() * 2.f - 1.f;
|
||
}
|
||
|
||
// Approx Gaussian via sum of three [-1, 1) uniforms.
|
||
// Variance of one such uniform = 1/3, so summing three gives variance 1
|
||
// and stddev 1. We then scale by the requested stddev.
|
||
inline float next_float_gaussian(float stddev = 1.f) noexcept {
|
||
const float a = next_float_signed();
|
||
const float b = next_float_signed();
|
||
const float c = next_float_signed();
|
||
return (a + b + c) * stddev;
|
||
}
|
||
|
||
private:
|
||
static inline std::uint64_t rotl_(std::uint64_t x, int k) noexcept {
|
||
return (x << k) | (x >> (64 - k));
|
||
}
|
||
|
||
std::uint64_t state_[4]{};
|
||
};
|
||
|
||
} // namespace nisps
|