diff --git a/FlingEngine/Core/inc/Random.h b/FlingEngine/Core/inc/Random.h index de014037..74c58056 100644 --- a/FlingEngine/Core/inc/Random.h +++ b/FlingEngine/Core/inc/Random.h @@ -1,44 +1,159 @@ #pragma once -#include /* srand, rand */ -#include /* time */ +#include #include "FlingMath.h" +#include "FlingTypes.h" namespace Fling { + /** + * A RandomStream object is a random number generator. + * + * Each RandomStream object has its own random number generator. + * + * Do not call the methods of one RandomStream object from more than one + * thread at the same time. + */ + class FLING_API RandomStream + { + public: + + /** + * This constructor makes a new RandomStream object. It seeds the + * object with a random value from the operating system. + */ + RandomStream(); + + /** + * This constructor makes a new RandomStream object. It seeds the + * object with the value t_Seed. + * + * Two RandomStream objects with the same seed value give the same + * sequence of numbers. + * + * @param t_Seed Use this value to seed the random number generator. + */ + explicit RandomStream(uint32 t_Seed); + + /** + * This method gives the stream a new seed. It gets the seed from + * the operating system. + */ + void Reseed(); + + /** + * This method gives the stream the seed value t_Seed. The stream + * then starts a new sequence of numbers. + * + * @param t_Seed Use this value to seed the random number generator. + */ + void Reseed(uint32 t_Seed); + + /** + * This method gives a random whole number. The number is 0 or + * more. The number is less than t_max. + * + * @param t_max t_max must be more than 0. + */ + int Random0ToN(const int t_max); + + /** + * This method gives a random whole number between t_min and + * t_max. The number can be equal to t_min. The number can be + * equal to t_max. + * + * @param t_min This is the lowest possible value. + * @param t_max This is the highest possible value. + */ + int RandomBetween(const int t_min, const int t_max); + + /** + * This method gives a random decimal number. The number is greater + * than or equal to t_Min. The number is less than t_Max. + */ + float GetRandomFloat(float t_Min, float t_Max); + + /** + * This method gives a random vec3 value. Each component of the + * vec3 value (x, y, z) is a separate random number. Each number is + * more than or equal to the related component of t_Min. Each + * number is less than the related component of t_Max. + */ + glm::vec3 GetRandomVec3(const glm::vec3 t_Min, const glm::vec3 t_Max); + + private: + + /** + * This is the random number engine for this stream. It is + * separate from the engine of every other RandomStream object. It + * is separate from the engine of the Random class. + */ + std::mt19937 m_Engine; + }; + + /** + * Random is a static class. It gives random numbers to the whole + * process. It uses one shared RandomStream object. + * + * Call methods of the Random class from one thread only. If you need + * random numbers on a worker thread, make your own RandomStream + * object. + */ class FLING_API Random { public: /** - * Initialize the random number generator + * Initialize the random number generator with a non-deterministic seed * * @return True if successful - */ + */ static bool Init(); /** - * Get a random int between 0 and the max - * - * @param t_max number to generate to - * @return Number between 0 and the given + * Initialize the random number generator with an explicit seed. Two + * engines seeded with the same value produce the same sequence of + * results, which is useful for reproducing a run in tests or when + * tracking down a gameplay bug. + * + * @param t_Seed Seed value to reproduce a given sequence of random values + * @return True if successful */ - static const int Random0ToN( const int t_max ); + static bool Init(uint32 t_Seed); + + /** + * Get a random int in [0, t_max): inclusive of 0, exclusive of t_max. + * + * @param t_max Exclusive upper bound; must be greater than 0 + */ + static const int Random0ToN(const int t_max); /** - * Generate a random number between the two given values - * - * @param t_min Min number to gerneate between - * @param t_max Max number to generate between - * @return Random int between the two values + * Generate a random int in [t_min, t_max]: inclusive of both bounds. + * + * @param t_min Inclusive lower bound + * @param t_max Inclusive upper bound */ - static const int RandomBetween( const int t_min, const int t_max ); + static const int RandomBetween(const int t_min, const int t_max); /** Flag to determine if random has been initialized */ static bool bIsInitalized; + /** + * Generate a random vec3 with each component independently sampled + * from [t_Min, t_Max): inclusive of t_Min, exclusive of t_Max. + */ static glm::vec3 GetRandomVec3(const glm::vec3 t_Min, const glm::vec3 t_Max); + /** + * Generate a random float in [t_Min, t_Max): inclusive of t_Min, + * exclusive of t_Max. + */ static float GetRandomFloat(float t_Min, float t_Max); + + private: + + /** This is the shared RandomStream object for the Random class. */ + static RandomStream s_Stream; }; -} // namespace Fling \ No newline at end of file +} // namespace Fling diff --git a/FlingEngine/Core/src/Random.cpp b/FlingEngine/Core/src/Random.cpp index cfc8c779..bc39f7e0 100644 --- a/FlingEngine/Core/src/Random.cpp +++ b/FlingEngine/Core/src/Random.cpp @@ -3,42 +3,122 @@ namespace Fling { + namespace + { + uint32 GenerateEntropySeed() + { + std::random_device seedSource; + return seedSource(); + } + + int Random0ToNImpl(std::mt19937& t_Engine, const int t_max) + { + assert(t_max > 0); + // value from 0 to max; + std::uniform_int_distribution dist(0, t_max - 1); + return dist(t_Engine); + } + + int RandomBetweenImpl(std::mt19937& t_Engine, const int t_min, const int t_max) + { + // Range from min to max, inclusive + std::uniform_int_distribution dist(t_min, t_max); + return dist(t_Engine); + } + + float GetRandomFloatImpl(std::mt19937& t_Engine, float t_Min, float t_Max) + { + std::uniform_real_distribution dist(t_Min, t_Max); + return dist(t_Engine); + } + + glm::vec3 GetRandomVec3Impl(std::mt19937& t_Engine, const glm::vec3 t_Min, const glm::vec3 t_Max) + { + glm::vec3 rand = {}; + rand.x = GetRandomFloatImpl(t_Engine, t_Min.x, t_Max.x); + rand.y = GetRandomFloatImpl(t_Engine, t_Min.y, t_Max.y); + rand.z = GetRandomFloatImpl(t_Engine, t_Min.z, t_Max.z); + return rand; + } + } // namespace + + RandomStream::RandomStream() + : m_Engine(GenerateEntropySeed()) + { + } + + RandomStream::RandomStream(uint32 t_Seed) + : m_Engine(t_Seed) + { + } + + void RandomStream::Reseed() + { + m_Engine.seed(GenerateEntropySeed()); + } + + void RandomStream::Reseed(uint32 t_Seed) + { + m_Engine.seed(t_Seed); + } + + int RandomStream::Random0ToN(const int t_max) + { + return Random0ToNImpl(m_Engine, t_max); + } + + int RandomStream::RandomBetween(const int t_min, const int t_max) + { + return RandomBetweenImpl(m_Engine, t_min, t_max); + } + + float RandomStream::GetRandomFloat(float t_Min, float t_Max) + { + return GetRandomFloatImpl(m_Engine, t_Min, t_Max); + } + + glm::vec3 RandomStream::GetRandomVec3(const glm::vec3 t_Min, const glm::vec3 t_Max) + { + return GetRandomVec3Impl(m_Engine, t_Min, t_Max); + } + bool Random::bIsInitalized = false; + RandomStream Random::s_Stream; bool Random::Init() { - /* initialize random seed */ - srand( static_cast( time( NULL ) ) ); + // Seed once from a real entropy source; every call afterwards just + // advances the mt19937 state, which is cheap (no per-call syscalls). + return Init(GenerateEntropySeed()); + } + + bool Random::Init(uint32 t_Seed) + { + s_Stream.Reseed(t_Seed); bIsInitalized = true; return bIsInitalized; } - const int Random::Random0ToN( const int t_max ) + const int Random::Random0ToN(const int t_max) { - assert( t_max > 0 && bIsInitalized ); - // value from 0 to max; - return rand() % t_max; + assert(bIsInitalized); + return s_Stream.Random0ToN(t_max); } - const int Random::RandomBetween( const int t_min, const int t_max ) + const int Random::RandomBetween(const int t_min, const int t_max) { - assert( bIsInitalized ); - // Range from minx to max - return rand() % t_max + t_min; + assert(bIsInitalized); + return s_Stream.RandomBetween(t_min, t_max); } glm::vec3 Random::GetRandomVec3(const glm::vec3 t_Min, const glm::vec3 t_Max) { - glm::vec3 rand = {}; - rand.x = GetRandomFloat(t_Min.x, t_Max.x); - rand.y = GetRandomFloat(t_Min.y, t_Max.y); - rand.z = GetRandomFloat(t_Min.z, t_Max.z); - return rand; + return s_Stream.GetRandomVec3(t_Min, t_Max); } float Random::GetRandomFloat(float t_Min, float t_Max) { - return (static_cast(rand()) / static_cast(static_cast(RAND_MAX) / (t_Max - t_Min))) + t_Min; + return s_Stream.GetRandomFloat(t_Min, t_Max); } -} // namespace Fling \ No newline at end of file +} // namespace Fling diff --git a/FlingTests/src/UtilsTests.cpp b/FlingTests/src/UtilsTests.cpp index 3c6aea53..b2204d70 100644 --- a/FlingTests/src/UtilsTests.cpp +++ b/FlingTests/src/UtilsTests.cpp @@ -13,6 +13,8 @@ #include "Memory.h" #include "CircularBuffer.hpp" +#include + TEST_CASE("Timing", "[utils]") { SECTION("valid Config") @@ -31,11 +33,243 @@ TEST_CASE("Timing", "[utils]") TEST_CASE("Random", "[utils]") { - REQUIRE_FALSE(Fling::Random::bIsInitalized); + using namespace Fling; + + SECTION("Init sets the initialized flag") + { + REQUIRE_FALSE(Random::bIsInitalized); + + Random::Init(); + + REQUIRE(Random::bIsInitalized); + } + + SECTION("The same seed reproduces the same sequence") + { + Random::Init(1234u); + std::vector first; + for (int i = 0; i < 20; ++i) + { + first.push_back(Random::RandomBetween(0, 1000)); + } + + Random::Init(1234u); + std::vector second; + for (int i = 0; i < 20; ++i) + { + second.push_back(Random::RandomBetween(0, 1000)); + } + + REQUIRE(first == second); + } + + SECTION("Different seeds produce different sequences") + { + Random::Init(1u); + std::vector first; + for (int i = 0; i < 20; ++i) + { + first.push_back(Random::RandomBetween(0, 1000000)); + } + + Random::Init(2u); + std::vector second; + for (int i = 0; i < 20; ++i) + { + second.push_back(Random::RandomBetween(0, 1000000)); + } + + REQUIRE(first != second); + } - Fling::Random::Init(); + SECTION("Random0ToN stays within [0, max)") + { + Random::Init(42u); + for (int i = 0; i < 1000; ++i) + { + const int value = Random::Random0ToN(10); + REQUIRE(value >= 0); + REQUIRE(value < 10); + } + } + + SECTION("RandomBetween stays within [min, max]") + { + Random::Init(42u); + for (int i = 0; i < 1000; ++i) + { + const int value = Random::RandomBetween(5, 15); + REQUIRE(value >= 5); + REQUIRE(value <= 15); + } + } - REQUIRE(Fling::Random::bIsInitalized); + SECTION("GetRandomFloat stays within [min, max]") + { + Random::Init(42u); + for (int i = 0; i < 1000; ++i) + { + const float value = Random::GetRandomFloat(-1.0f, 1.0f); + REQUIRE(value >= -1.0f); + REQUIRE(value <= 1.0f); + } + } + + SECTION("GetRandomVec3 stays within component-wise bounds") + { + Random::Init(42u); + const glm::vec3 min(-1.0f, 0.0f, 2.0f); + const glm::vec3 max(1.0f, 5.0f, 3.0f); + for (int i = 0; i < 100; ++i) + { + const glm::vec3 value = Random::GetRandomVec3(min, max); + REQUIRE(value.x >= min.x); + REQUIRE(value.x <= max.x); + REQUIRE(value.y >= min.y); + REQUIRE(value.y <= max.y); + REQUIRE(value.z >= min.z); + REQUIRE(value.z <= max.z); + } + } +} + +TEST_CASE("RandomStream", "[utils]") +{ + using namespace Fling; + + SECTION("Default construction is usable without a separate seed call") + { + RandomStream stream; + const int value = stream.RandomBetween(0, 1000); + REQUIRE(value >= 0); + REQUIRE(value <= 1000); + } + + SECTION("The same seed reproduces the same sequence") + { + RandomStream first(1234u); + std::vector firstValues; + for (int i = 0; i < 20; ++i) + { + firstValues.push_back(first.RandomBetween(0, 1000)); + } + + RandomStream second(1234u); + std::vector secondValues; + for (int i = 0; i < 20; ++i) + { + secondValues.push_back(second.RandomBetween(0, 1000)); + } + + REQUIRE(firstValues == secondValues); + } + + SECTION("Different seeds produce different sequences") + { + RandomStream first(1u); + std::vector firstValues; + for (int i = 0; i < 20; ++i) + { + firstValues.push_back(first.RandomBetween(0, 1000000)); + } + + RandomStream second(2u); + std::vector secondValues; + for (int i = 0; i < 20; ++i) + { + secondValues.push_back(second.RandomBetween(0, 1000000)); + } + + REQUIRE(firstValues != secondValues); + } + + SECTION("Reseed restarts the sequence") + { + RandomStream stream(42u); + std::vector firstValues; + for (int i = 0; i < 20; ++i) + { + firstValues.push_back(stream.RandomBetween(0, 1000)); + } + + stream.Reseed(42u); + std::vector secondValues; + for (int i = 0; i < 20; ++i) + { + secondValues.push_back(stream.RandomBetween(0, 1000)); + } + + REQUIRE(firstValues == secondValues); + } + + SECTION("Two streams advance independently of each other") + { + RandomStream first(7u); + RandomStream second(7u); + + // Draw from `first` only; `second` must be unaffected and still + // reproduce the same sequence `first` started with. + for (int i = 0; i < 10; ++i) + { + first.RandomBetween(0, 1000); + } + + RandomStream reference(7u); + for (int i = 0; i < 10; ++i) + { + REQUIRE(second.RandomBetween(0, 1000) == reference.RandomBetween(0, 1000)); + } + } + + SECTION("Random0ToN stays within [0, max)") + { + RandomStream stream(42u); + for (int i = 0; i < 1000; ++i) + { + const int value = stream.Random0ToN(10); + REQUIRE(value >= 0); + REQUIRE(value < 10); + } + } + + SECTION("RandomBetween stays within [min, max]") + { + RandomStream stream(42u); + for (int i = 0; i < 1000; ++i) + { + const int value = stream.RandomBetween(5, 15); + REQUIRE(value >= 5); + REQUIRE(value <= 15); + } + } + + SECTION("GetRandomFloat stays within [min, max]") + { + RandomStream stream(42u); + for (int i = 0; i < 1000; ++i) + { + const float value = stream.GetRandomFloat(-1.0f, 1.0f); + REQUIRE(value >= -1.0f); + REQUIRE(value <= 1.0f); + } + } + + SECTION("GetRandomVec3 stays within component-wise bounds") + { + RandomStream stream(42u); + const glm::vec3 min(-1.0f, 0.0f, 2.0f); + const glm::vec3 max(1.0f, 5.0f, 3.0f); + for (int i = 0; i < 100; ++i) + { + const glm::vec3 value = stream.GetRandomVec3(min, max); + REQUIRE(value.x >= min.x); + REQUIRE(value.x <= max.x); + REQUIRE(value.y >= min.y); + REQUIRE(value.y <= max.y); + REQUIRE(value.z >= min.z); + REQUIRE(value.z <= max.z); + } + } } TEST_CASE("Logger", "[utils]") @@ -45,7 +279,7 @@ TEST_CASE("Logger", "[utils]") SECTION("Logger Console Creation") - { + { // Require the current console to exist REQUIRE(Logger::GetCurrentConsole() != nullptr); } @@ -65,14 +299,14 @@ TEST_CASE("Free List", "[utils]") FreeList freelist( /* start = */ buf, /* end = */buf + 1024, - /* elm size */ 32, + /* elm size */ 32, /* alignment */ 8, /* offset */ 0); void* obj0 = freelist.Obtain(); REQUIRE(obj0 != nullptr); - void* obj1 = freelist.Obtain(); + void* obj1 = freelist.Obtain(); REQUIRE(obj1 != nullptr); freelist.Return(obj1); @@ -86,7 +320,7 @@ TEST_CASE("Stack Allocator", "[utils]") char buf[1024] = {}; StackAllocator stackAllocator(buf, buf + 1024); - // @TODO The stack allocator is broken and it seems to be rooted in + // @TODO The stack allocator is broken and it seems to be rooted in // the AlignPointer method on linux. That's a relatively large problem } @@ -102,7 +336,7 @@ TEST_CASE("Aligned Alloc", "[utils]") TEST_CASE("Circular Buffer", "[utils]") { - // Circular buffer of char's + // Circular buffer of char's Fling::CircularBuffer CircBuf {}; -} \ No newline at end of file +}