// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Random { using System; using System.Runtime.Serialization; using System.Text.Json.Serialization; using Extension; using ProtoBuf; using WallstopStudios.UnityHelpers.Core.Helper; using WallstopStudios.UnityHelpers.Utils; /// /// PhotonSpin32: a 20-word ring-buffer generator inspired by SHISHUA, tuned for high throughput and large period. /// /// /// /// Reference: Will Stafford Parsons (wileylooper/photonspin, repository offline). /// Ported from wileylooper/photonspin, this generator produces batches of 20 new 32-bit values per round, /// offering a huge period (~2512) and robust statistical performance. It shines when large streams are /// required, while still supporting deterministic state capture and serialization. /// /// Pros: /// /// Large state with excellent distribution; great for heavy simulation workloads. /// Thread-local friendly via . /// Deterministic snapshots through . /// /// Cons: /// /// Higher per-instance memory (~20×4 bytes). /// Not cryptographically secure. /// /// When to use: /// /// Procedural workloads that benefit from bulk generation and long non-overlapping streams. /// /// When not to use: /// /// Security or adversarial scenarios. /// /// /// /// /// using WallstopStudios.UnityHelpers.Core.Random; /// /// PhotonSpinRandom rng = new PhotonSpinRandom(seed: 42u); /// float noise = rng.NextFloat(); /// Guid guid = rng.NextGuid(); /// /// [RandomGeneratorMetadata( RandomQuality.Excellent, "SHISHUA-inspired generator; independent testing (PractRand 128GB) by author indicates excellent distribution properties.", "Will Stafford Parsons", "" // Original repository wileylooper/photonspin is offline )] [Serializable] [DataContract] [ProtoContract(SkipConstructor = true)] public sealed class PhotonSpinRandom : AbstractRandom, IEquatable, IComparable, IComparable { private const uint Increment = 111_111U; private const int BlockSize = 20; private const int ElementByteSize = BlockSize * sizeof(uint); public static PhotonSpinRandom Instance => ThreadLocalRandom.Instance; public override RandomState InternalState { get { using PooledArray payloadLease = WallstopArrayPool.Get( ElementByteSize, out byte[] buffer ); Buffer.BlockCopy(_elements, 0, buffer, 0, ElementByteSize); uint packedIndex = (uint)(_index & 0x7FFFFFFF); if (_hasPrimed) { packedIndex |= 0x80000000U; } ulong state1 = ((ulong)_a << 32) | _b; ulong state2 = ((ulong)_c << 32) | packedIndex; return BuildState( state1, state2, new ArraySegment(buffer, 0, ElementByteSize) ); } } [ProtoMember(6)] private uint[] _elements = new uint[BlockSize]; [ProtoMember(7)] private uint _a; [ProtoMember(8)] private uint _b; [ProtoMember(9)] private uint _c; [ProtoMember(10)] private int _index; [ProtoMember(11)] private bool _hasPrimed; public PhotonSpinRandom() : this(Guid.NewGuid()) { } public PhotonSpinRandom(Guid guid) { InitializeFromGuid(guid); } public PhotonSpinRandom(uint seed) { uint seedA = seed; uint seedB = seed ^ 0x9E3779B9U; if (seedB == 0) { seedB = 1U; } uint seedC = seed + 0x6C8E9CF5U; InitializeFromScalars(seedA, seedB, seedC); } public PhotonSpinRandom(uint seedA, uint seedB, uint seedC) { InitializeFromScalars(seedA, seedB == 0 ? 1U : seedB, seedC); } [JsonConstructor] public PhotonSpinRandom(RandomState internalState) { ulong state1 = internalState.State1; ulong state2 = internalState.State2; _a = (uint)(state1 >> 32); _b = (uint)state1; _c = (uint)(state2 >> 32); uint packedIndex = (uint)state2; _hasPrimed = (packedIndex & 0x80000000U) != 0; _index = (int)(packedIndex & 0x7FFFFFFF); if (_index < 0 || _index > BlockSize) { _index = BlockSize; } LoadSerializedElements(internalState._payload); NormalizeIndex(); RestoreCommonState(internalState); } public override uint NextUint() { if (!_hasPrimed) { GenerateBlock(); _hasPrimed = true; _index = BlockSize; } if (_index >= BlockSize) { GenerateBlock(); } uint value = _elements[_index]; _index += 1; return value; } public override IRandom Copy() { return new PhotonSpinRandom(InternalState); } public override bool Equals(object obj) { return Equals(obj as PhotonSpinRandom); } public bool Equals(PhotonSpinRandom other) { if (other == null) { return false; } if (_a != other._a || _b != other._b || _c != other._c) { return false; } if (_index != other._index || _hasPrimed != other._hasPrimed) { return false; } if (!_elements.AsSpan().SequenceEqual(other._elements)) { return false; } // ReSharper disable once CompareOfFloatsByEqualityOperator return _cachedGaussian == other._cachedGaussian; } public override int GetHashCode() { return Objects.HashCode(_a, _b, _c, _index, _hasPrimed); } public override string ToString() { return this.ToJson(); } public int CompareTo(object obj) { return CompareTo(obj as PhotonSpinRandom); } public int CompareTo(PhotonSpinRandom other) { if (other == null) { return -1; } int comparison = _a.CompareTo(other._a); if (comparison != 0) { return comparison; } comparison = _b.CompareTo(other._b); if (comparison != 0) { return comparison; } comparison = _c.CompareTo(other._c); if (comparison != 0) { return comparison; } comparison = _index.CompareTo(other._index); if (comparison != 0) { return comparison; } comparison = _hasPrimed.CompareTo(other._hasPrimed); if (comparison != 0) { return comparison; } for (int i = 0; i < BlockSize; ++i) { comparison = _elements[i].CompareTo(other._elements[i]); if (comparison != 0) { return comparison; } } return 0; } private void LoadSerializedElements(byte[] payload) { if (_elements == null || _elements.Length != BlockSize) { _elements = new uint[BlockSize]; } if (payload != null && payload.Length >= ElementByteSize) { Buffer.BlockCopy(payload, 0, _elements, 0, ElementByteSize); return; } Array.Clear(_elements, 0, _elements.Length); } private void NormalizeIndex() { if (_index < 0 || _index > BlockSize) { _index = BlockSize; } } private void InitializeFromGuid(Guid guid) { (ulong seed0, ulong seed1) = RandomUtilities.GuidToUInt64Pair(guid); InitializeFromUlongs(seed0, seed1); } private void InitializeFromScalars(uint seedA, uint seedB, uint seedC) { ulong seed0 = ((ulong)seedA << 32) | seedB; ulong seed1 = ((ulong)seedC << 32) | (seedB ^ 0xA5A5A5A5U); InitializeFromUlongs(seed0, seed1); } private void InitializeFromUlongs(ulong seed0, ulong seed1) { if (_elements == null || _elements.Length != BlockSize) { _elements = new uint[BlockSize]; } ulong mixer = seed0 ^ (seed1 << 1) ^ 0x9E3779B97F4A7C15UL; for (int i = 0; i < BlockSize; ++i) { _elements[i] = Mix32(ref mixer); } _a = Mix32(ref mixer); _b = Mix32(ref mixer); _c = Mix32(ref mixer); _index = BlockSize; _hasPrimed = false; NormalizeIndex(); } private void GenerateBlock() { unchecked { Span mix = stackalloc uint[4]; int baseIndex = (int)(_a & 15U); mix[0] = _elements[baseIndex]; mix[1] = _elements[(baseIndex + 3) & 15]; mix[2] = _elements[(baseIndex + 6) & 15]; mix[3] = _elements[(baseIndex + 9) & 15]; _a += Increment; int k = 0; for (int i = 0; i < 4; ++i) { _b += _a; _c ^= _a; mix[i] += RotateLeft(_b, 9); for (int j = 0; j < 5; ++j) { _elements[k] += RotateLeft(mix[i], 25); _elements[k] ^= _c; mix[i] += _elements[k]; k++; } if ( _elements[k - 1] == _elements[k - 2] && _elements[k - 3] == _elements[k - 4] ) { _elements[k - 1] += mix[i] ^ 81_016U; _elements[k - 2] += mix[i] ^ 297_442_265U; _elements[k - 3] += (mix[i] ^ 5_480U) | (mix[i] & 1U); _elements[k - 4] += mix[i] ^ 19_006_808U; _elements[k - 5] += mix[i]; } } _b += RotateLeft(mix[0], 23); _b ^= mix[1]; _c += RotateLeft(mix[2], 13); _c ^= mix[3]; } _index = 0; } } }