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
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ add_library(ecs_engine STATIC
src/engine/ComponentRegistry.h
src/engine/EcsCore.h
src/engine/EngineConfig.h
src/engine/DeterministicRng.h
src/engine/EngineRuntime.h
src/engine/EngineWorld.h
src/engine/EngineState.h
Expand All @@ -61,6 +62,7 @@ add_library(ecs_engine STATIC
src/engine/EngineRuntime.cpp
src/engine/FrameClock.cpp
src/engine/internal/EngineWorldFactory.h
src/engine/internal/EngineRuntimeTestAccess.h
src/engine/SystemScheduler.cpp
)

Expand Down Expand Up @@ -138,6 +140,7 @@ if (BUILD_TESTING)
tests/SystemSchedulerTests.cpp
tests/EngineIntegrationTests.cpp
tests/ResourceStoreTests.cpp
tests/DeterministicRngTests.cpp
)

target_include_directories(safecrowd_tests
Expand Down
47 changes: 47 additions & 0 deletions src/engine/DeterministicRng.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#pragma once

#include <cstdint>

namespace safecrowd::engine {

class DeterministicRng {
public:
explicit DeterministicRng(std::uint64_t seed = 1) noexcept {
reseed(seed);
}

void reseed(std::uint64_t seed) noexcept {
baseSeed_ = seed == 0 ? 1 : seed;
state_ = mix(baseSeed_);
}

[[nodiscard]] std::uint64_t baseSeed() const noexcept {
return baseSeed_;
}

[[nodiscard]] std::uint64_t derive(std::uint64_t runIndex,
std::uint64_t fixedStepIndex) const noexcept {
auto state = mix(baseSeed_);
state = mix(state ^ mix(runIndex));
state = mix(state ^ mix(fixedStepIndex));
return state;
}

[[nodiscard]] std::uint64_t next() noexcept {
state_ = mix(state_);
return state_;
}

private:
static std::uint64_t mix(std::uint64_t value) noexcept {
value += 0x9e3779b97f4a7c15ULL;
value = (value ^ (value >> 30U)) * 0xbf58476d1ce4e5b9ULL;
value = (value ^ (value >> 27U)) * 0x94d049bb133111ebULL;
return value ^ (value >> 31U);
}

std::uint64_t baseSeed_{1};
std::uint64_t state_{0};
};

} // namespace safecrowd::engine
44 changes: 22 additions & 22 deletions src/engine/EngineRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,26 @@ EngineConfig normalizeConfig(EngineConfig config) {
return config;
}

EngineStepContext makeStepContext(const DeterministicRng& rng, std::uint64_t frameIndex,
std::uint64_t fixedStepIndex, double alpha,
std::uint64_t runIndex) {
return EngineStepContext{
.frameIndex = frameIndex,
.fixedStepIndex = fixedStepIndex,
.alpha = alpha,
.runIndex = runIndex,
.derivedSeed = rng.derive(runIndex, fixedStepIndex),
};
}

} // namespace

EngineRuntime::EngineRuntime(EngineConfig config)
: config_(normalizeConfig(config)),
scheduler_(core_, buffer_),
world_(EngineWorld::ConstructionToken{}, core_, resources_, buffer_),
frameClock_(config_) {
frameClock_(config_),
rng_(config_.baseSeed) {
}

void EngineRuntime::addSystem(std::unique_ptr<EngineSystem> system,
Expand All @@ -39,19 +52,15 @@ void EngineRuntime::initialize() {
resources_ = ResourceStore{};
buffer_ = CommandBuffer{};
scheduler_.resetCadenceState();
rng_.reseed(config_.baseSeed);
stats_ = {};
stats_.state = EngineState::Ready;
++runIndex_;

scheduler_.configure(world_);

const EngineStepContext startupCtx{
.frameIndex = stats_.frameIndex,
.fixedStepIndex = stats_.fixedStepIndex,
.alpha = 0.0,
.runIndex = runIndex_,
.derivedSeed = 0,
};
const EngineStepContext startupCtx = makeStepContext(
rng_, stats_.frameIndex, stats_.fixedStepIndex, 0.0, runIndex_);
scheduler_.executeStartup(world_, startupCtx);
}

Expand All @@ -75,6 +84,7 @@ void EngineRuntime::stop() {
resources_ = ResourceStore{};
buffer_ = CommandBuffer{};
scheduler_.resetCadenceState();
rng_.reseed(config_.baseSeed);
stats_ = {};
stats_.state = EngineState::Stopped;
}
Expand All @@ -94,13 +104,8 @@ void EngineRuntime::stepFrame(double deltaSeconds) {
++stats_.frameIndex;
stats_.fixedStepsThisFrame = 0;

EngineStepContext ctx{
.frameIndex = stats_.frameIndex,
.fixedStepIndex = stats_.fixedStepIndex,
.alpha = frameClock_.alpha(),
.runIndex = runIndex_,
.derivedSeed = 0,
};
auto ctx = makeStepContext(rng_, stats_.frameIndex, stats_.fixedStepIndex,
frameClock_.alpha(), runIndex_);

scheduler_.executePhase(UpdatePhase::PreSimulation, world_, ctx);

Expand All @@ -109,13 +114,8 @@ void EngineRuntime::stepFrame(double deltaSeconds) {
++stats_.fixedStepIndex;
++stats_.fixedStepsThisFrame;

ctx = EngineStepContext{
.frameIndex = stats_.frameIndex,
.fixedStepIndex = stats_.fixedStepIndex,
.alpha = frameClock_.alpha(),
.runIndex = runIndex_,
.derivedSeed = 0,
};
ctx = makeStepContext(rng_, stats_.frameIndex, stats_.fixedStepIndex,
frameClock_.alpha(), runIndex_);

scheduler_.executePhase(UpdatePhase::FixedSimulation, world_, ctx);
}
Expand Down
8 changes: 8 additions & 0 deletions src/engine/EngineRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <memory>

#include "engine/CommandBuffer.h"
#include "engine/DeterministicRng.h"
#include "engine/EcsCore.h"
#include "engine/EngineConfig.h"
#include "engine/EngineStats.h"
Expand All @@ -16,6 +17,10 @@

namespace safecrowd::engine {

namespace internal {
class EngineRuntimeTestAccess;
}

class EngineRuntime {
public:
explicit EngineRuntime(EngineConfig config = {});
Expand All @@ -37,6 +42,8 @@ class EngineRuntime {
std::uint64_t runIndex() const noexcept;

private:
friend class internal::EngineRuntimeTestAccess;

EngineConfig config_;
EngineStats stats_;
EcsCore core_;
Expand All @@ -45,6 +52,7 @@ class EngineRuntime {
SystemScheduler scheduler_;
EngineWorld world_;
FrameClock frameClock_;
DeterministicRng rng_;
std::uint64_t runIndex_{0};
};

Expand Down
19 changes: 19 additions & 0 deletions src/engine/internal/EngineRuntimeTestAccess.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

#include "engine/EngineRuntime.h"

namespace safecrowd::engine::internal {

class EngineRuntimeTestAccess {
public:
[[nodiscard]] static DeterministicRng& rng(EngineRuntime& runtime) noexcept {
return runtime.rng_;
}

[[nodiscard]] static const DeterministicRng& rng(
const EngineRuntime& runtime) noexcept {
return runtime.rng_;
}
};

} // namespace safecrowd::engine::internal
28 changes: 28 additions & 0 deletions tests/DeterministicRngTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#include "TestSupport.h"

#include "engine/DeterministicRng.h"

SC_TEST(DeterministicRng_Derive_IsStableForSameInputs) {
safecrowd::engine::DeterministicRng rng{17};

const auto first = rng.derive(2, 5);
const auto second = rng.derive(2, 5);
const auto differentRun = rng.derive(3, 5);
const auto differentStep = rng.derive(2, 6);

SC_EXPECT_EQ(first, second);
SC_EXPECT_TRUE(first != differentRun);
SC_EXPECT_TRUE(first != differentStep);
}

SC_TEST(DeterministicRng_Reseed_RestartsSequence) {
safecrowd::engine::DeterministicRng rng{23};

const auto first = rng.next();
const auto second = rng.next();

rng.reseed(23);

SC_EXPECT_EQ(rng.next(), first);
SC_EXPECT_EQ(rng.next(), second);
}
95 changes: 95 additions & 0 deletions tests/EngineRuntimeTests.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#include "TestSupport.h"

#include <cstddef>
#include <cstdint>
#include <exception>
#include <memory>
#include <vector>

#include "engine/EngineRuntime.h"
#include "engine/internal/EngineRuntimeTestAccess.h"

namespace {

Expand Down Expand Up @@ -67,6 +69,22 @@ class RecordPhaseSystem : public safecrowd::engine::EngineSystem {
}
};

class RecordSeedSystem : public safecrowd::engine::EngineSystem {
public:
std::vector<std::uint64_t>& seeds;
std::vector<std::uint64_t>& runs;

RecordSeedSystem(std::vector<std::uint64_t>& seedLog,
std::vector<std::uint64_t>& runLog)
: seeds(seedLog), runs(runLog) {}

void update(safecrowd::engine::EngineWorld&,
const safecrowd::engine::EngineStepContext& step) override {
seeds.push_back(step.derivedSeed);
runs.push_back(step.runIndex);
}
};

class ResourceSetupSystem : public safecrowd::engine::EngineSystem {
public:
void configure(safecrowd::engine::EngineWorld& world) override {
Expand Down Expand Up @@ -371,3 +389,80 @@ SC_TEST(EngineRuntime_Initialize_ClearsExistingWorldResources) {

SC_EXPECT_TRUE(!runtime.world().resources().contains<SharedCounter>());
}

SC_TEST(EngineRuntime_Initialize_RebuildsDeterministicRngState) {
safecrowd::engine::EngineRuntime runtime({
.fixedDeltaTime = 0.25,
.maxCatchUpSteps = 4,
.baseSeed = 23,
});

runtime.initialize();
auto& rng = safecrowd::engine::internal::EngineRuntimeTestAccess::rng(runtime);
const auto firstAfterInitialize = rng.next();
(void)rng.next();

runtime.initialize();

SC_EXPECT_EQ(rng.next(), firstAfterInitialize);
}

SC_TEST(EngineRuntime_Stop_RebuildsDeterministicRngState) {
safecrowd::engine::EngineRuntime runtime({
.fixedDeltaTime = 0.25,
.maxCatchUpSteps = 4,
.baseSeed = 29,
});

auto& rng = safecrowd::engine::internal::EngineRuntimeTestAccess::rng(runtime);
const auto expectedFirstValue = rng.next();
(void)rng.next();

runtime.stop();

SC_EXPECT_EQ(rng.next(), expectedFirstValue);
}

SC_TEST(EngineRuntime_StopAndRestart_RebuildsDeterministicSeedStream) {
std::vector<std::uint64_t> firstSeeds;
std::vector<std::uint64_t> firstRuns;
safecrowd::engine::EngineRuntime firstRuntime({
.fixedDeltaTime = 0.25,
.maxCatchUpSteps = 4,
.baseSeed = 19,
});

firstRuntime.addSystem(std::make_unique<RecordSeedSystem>(firstSeeds, firstRuns));
firstRuntime.play();
firstRuntime.stepFrame(0.25);
firstRuntime.stop();
firstRuntime.play();
firstRuntime.stepFrame(0.25);

std::vector<std::uint64_t> secondSeeds;
std::vector<std::uint64_t> secondRuns;
safecrowd::engine::EngineRuntime secondRuntime({
.fixedDeltaTime = 0.25,
.maxCatchUpSteps = 4,
.baseSeed = 19,
});

secondRuntime.addSystem(std::make_unique<RecordSeedSystem>(secondSeeds, secondRuns));
secondRuntime.play();
secondRuntime.stepFrame(0.25);
secondRuntime.stop();
secondRuntime.play();
secondRuntime.stepFrame(0.25);

SC_EXPECT_EQ(firstSeeds.size(), std::size_t{2});
SC_EXPECT_EQ(secondSeeds.size(), std::size_t{2});
SC_EXPECT_EQ(firstRuns.size(), std::size_t{2});
SC_EXPECT_EQ(secondRuns.size(), std::size_t{2});
SC_EXPECT_EQ(firstRuns[0], 1ULL);
SC_EXPECT_EQ(firstRuns[1], 2ULL);
SC_EXPECT_EQ(secondRuns[0], 1ULL);
SC_EXPECT_EQ(secondRuns[1], 2ULL);
SC_EXPECT_EQ(firstSeeds[0], secondSeeds[0]);
SC_EXPECT_EQ(firstSeeds[1], secondSeeds[1]);
SC_EXPECT_TRUE(firstSeeds[0] != firstSeeds[1]);
}
3 changes: 3 additions & 0 deletions uml/engine-overview.puml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ note bottom of Runtime
Time, phase order, deferred mutation,
deterministic random streams,
and execution orchestration.
Runtime-owned cadence and seed state
reset at initialize/stop boundaries
so restarts do not inherit hidden state.
Initial engine scope prioritizes
fixed-step scheduling over advanced
event or snapshot extensions.
Expand Down
Loading
Loading