Skip to content

Commit 9fa89d7

Browse files
authored
[Engine] runtime-owned reset contract 확장 (#132)
* 엔진 runtime-owned reset contract 확장 * 리뷰 반영: RNG 계약 문서와 reset 테스트 보강
1 parent 58a5233 commit 9fa89d7

10 files changed

Lines changed: 239 additions & 39 deletions

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ add_library(ecs_engine STATIC
4343
src/engine/ComponentRegistry.h
4444
src/engine/EcsCore.h
4545
src/engine/EngineConfig.h
46+
src/engine/DeterministicRng.h
4647
src/engine/EngineRuntime.h
4748
src/engine/EngineWorld.h
4849
src/engine/EngineState.h
@@ -61,6 +62,7 @@ add_library(ecs_engine STATIC
6162
src/engine/EngineRuntime.cpp
6263
src/engine/FrameClock.cpp
6364
src/engine/internal/EngineWorldFactory.h
65+
src/engine/internal/EngineRuntimeTestAccess.h
6466
src/engine/SystemScheduler.cpp
6567
)
6668

@@ -138,6 +140,7 @@ if (BUILD_TESTING)
138140
tests/SystemSchedulerTests.cpp
139141
tests/EngineIntegrationTests.cpp
140142
tests/ResourceStoreTests.cpp
143+
tests/DeterministicRngTests.cpp
141144
)
142145

143146
target_include_directories(safecrowd_tests

src/engine/DeterministicRng.h

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#pragma once
2+
3+
#include <cstdint>
4+
5+
namespace safecrowd::engine {
6+
7+
class DeterministicRng {
8+
public:
9+
explicit DeterministicRng(std::uint64_t seed = 1) noexcept {
10+
reseed(seed);
11+
}
12+
13+
void reseed(std::uint64_t seed) noexcept {
14+
baseSeed_ = seed == 0 ? 1 : seed;
15+
state_ = mix(baseSeed_);
16+
}
17+
18+
[[nodiscard]] std::uint64_t baseSeed() const noexcept {
19+
return baseSeed_;
20+
}
21+
22+
[[nodiscard]] std::uint64_t derive(std::uint64_t runIndex,
23+
std::uint64_t fixedStepIndex) const noexcept {
24+
auto state = mix(baseSeed_);
25+
state = mix(state ^ mix(runIndex));
26+
state = mix(state ^ mix(fixedStepIndex));
27+
return state;
28+
}
29+
30+
[[nodiscard]] std::uint64_t next() noexcept {
31+
state_ = mix(state_);
32+
return state_;
33+
}
34+
35+
private:
36+
static std::uint64_t mix(std::uint64_t value) noexcept {
37+
value += 0x9e3779b97f4a7c15ULL;
38+
value = (value ^ (value >> 30U)) * 0xbf58476d1ce4e5b9ULL;
39+
value = (value ^ (value >> 27U)) * 0x94d049bb133111ebULL;
40+
return value ^ (value >> 31U);
41+
}
42+
43+
std::uint64_t baseSeed_{1};
44+
std::uint64_t state_{0};
45+
};
46+
47+
} // namespace safecrowd::engine

src/engine/EngineRuntime.cpp

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,26 @@ EngineConfig normalizeConfig(EngineConfig config) {
1919
return config;
2020
}
2121

22+
EngineStepContext makeStepContext(const DeterministicRng& rng, std::uint64_t frameIndex,
23+
std::uint64_t fixedStepIndex, double alpha,
24+
std::uint64_t runIndex) {
25+
return EngineStepContext{
26+
.frameIndex = frameIndex,
27+
.fixedStepIndex = fixedStepIndex,
28+
.alpha = alpha,
29+
.runIndex = runIndex,
30+
.derivedSeed = rng.derive(runIndex, fixedStepIndex),
31+
};
32+
}
33+
2234
} // namespace
2335

2436
EngineRuntime::EngineRuntime(EngineConfig config)
2537
: config_(normalizeConfig(config)),
2638
scheduler_(core_, buffer_),
2739
world_(EngineWorld::ConstructionToken{}, core_, resources_, buffer_),
28-
frameClock_(config_) {
40+
frameClock_(config_),
41+
rng_(config_.baseSeed) {
2942
}
3043

3144
void EngineRuntime::addSystem(std::unique_ptr<EngineSystem> system,
@@ -39,19 +52,15 @@ void EngineRuntime::initialize() {
3952
resources_ = ResourceStore{};
4053
buffer_ = CommandBuffer{};
4154
scheduler_.resetCadenceState();
55+
rng_.reseed(config_.baseSeed);
4256
stats_ = {};
4357
stats_.state = EngineState::Ready;
4458
++runIndex_;
4559

4660
scheduler_.configure(world_);
4761

48-
const EngineStepContext startupCtx{
49-
.frameIndex = stats_.frameIndex,
50-
.fixedStepIndex = stats_.fixedStepIndex,
51-
.alpha = 0.0,
52-
.runIndex = runIndex_,
53-
.derivedSeed = 0,
54-
};
62+
const EngineStepContext startupCtx = makeStepContext(
63+
rng_, stats_.frameIndex, stats_.fixedStepIndex, 0.0, runIndex_);
5564
scheduler_.executeStartup(world_, startupCtx);
5665
}
5766

@@ -75,6 +84,7 @@ void EngineRuntime::stop() {
7584
resources_ = ResourceStore{};
7685
buffer_ = CommandBuffer{};
7786
scheduler_.resetCadenceState();
87+
rng_.reseed(config_.baseSeed);
7888
stats_ = {};
7989
stats_.state = EngineState::Stopped;
8090
}
@@ -94,13 +104,8 @@ void EngineRuntime::stepFrame(double deltaSeconds) {
94104
++stats_.frameIndex;
95105
stats_.fixedStepsThisFrame = 0;
96106

97-
EngineStepContext ctx{
98-
.frameIndex = stats_.frameIndex,
99-
.fixedStepIndex = stats_.fixedStepIndex,
100-
.alpha = frameClock_.alpha(),
101-
.runIndex = runIndex_,
102-
.derivedSeed = 0,
103-
};
107+
auto ctx = makeStepContext(rng_, stats_.frameIndex, stats_.fixedStepIndex,
108+
frameClock_.alpha(), runIndex_);
104109

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

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

112-
ctx = EngineStepContext{
113-
.frameIndex = stats_.frameIndex,
114-
.fixedStepIndex = stats_.fixedStepIndex,
115-
.alpha = frameClock_.alpha(),
116-
.runIndex = runIndex_,
117-
.derivedSeed = 0,
118-
};
117+
ctx = makeStepContext(rng_, stats_.frameIndex, stats_.fixedStepIndex,
118+
frameClock_.alpha(), runIndex_);
119119

120120
scheduler_.executePhase(UpdatePhase::FixedSimulation, world_, ctx);
121121
}

src/engine/EngineRuntime.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <memory>
55

66
#include "engine/CommandBuffer.h"
7+
#include "engine/DeterministicRng.h"
78
#include "engine/EcsCore.h"
89
#include "engine/EngineConfig.h"
910
#include "engine/EngineStats.h"
@@ -16,6 +17,10 @@
1617

1718
namespace safecrowd::engine {
1819

20+
namespace internal {
21+
class EngineRuntimeTestAccess;
22+
}
23+
1924
class EngineRuntime {
2025
public:
2126
explicit EngineRuntime(EngineConfig config = {});
@@ -37,6 +42,8 @@ class EngineRuntime {
3742
std::uint64_t runIndex() const noexcept;
3843

3944
private:
45+
friend class internal::EngineRuntimeTestAccess;
46+
4047
EngineConfig config_;
4148
EngineStats stats_;
4249
EcsCore core_;
@@ -45,6 +52,7 @@ class EngineRuntime {
4552
SystemScheduler scheduler_;
4653
EngineWorld world_;
4754
FrameClock frameClock_;
55+
DeterministicRng rng_;
4856
std::uint64_t runIndex_{0};
4957
};
5058

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#pragma once
2+
3+
#include "engine/EngineRuntime.h"
4+
5+
namespace safecrowd::engine::internal {
6+
7+
class EngineRuntimeTestAccess {
8+
public:
9+
[[nodiscard]] static DeterministicRng& rng(EngineRuntime& runtime) noexcept {
10+
return runtime.rng_;
11+
}
12+
13+
[[nodiscard]] static const DeterministicRng& rng(
14+
const EngineRuntime& runtime) noexcept {
15+
return runtime.rng_;
16+
}
17+
};
18+
19+
} // namespace safecrowd::engine::internal

tests/DeterministicRngTests.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#include "TestSupport.h"
2+
3+
#include "engine/DeterministicRng.h"
4+
5+
SC_TEST(DeterministicRng_Derive_IsStableForSameInputs) {
6+
safecrowd::engine::DeterministicRng rng{17};
7+
8+
const auto first = rng.derive(2, 5);
9+
const auto second = rng.derive(2, 5);
10+
const auto differentRun = rng.derive(3, 5);
11+
const auto differentStep = rng.derive(2, 6);
12+
13+
SC_EXPECT_EQ(first, second);
14+
SC_EXPECT_TRUE(first != differentRun);
15+
SC_EXPECT_TRUE(first != differentStep);
16+
}
17+
18+
SC_TEST(DeterministicRng_Reseed_RestartsSequence) {
19+
safecrowd::engine::DeterministicRng rng{23};
20+
21+
const auto first = rng.next();
22+
const auto second = rng.next();
23+
24+
rng.reseed(23);
25+
26+
SC_EXPECT_EQ(rng.next(), first);
27+
SC_EXPECT_EQ(rng.next(), second);
28+
}

tests/EngineRuntimeTests.cpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
#include "TestSupport.h"
22

33
#include <cstddef>
4+
#include <cstdint>
45
#include <exception>
56
#include <memory>
67
#include <vector>
78

89
#include "engine/EngineRuntime.h"
10+
#include "engine/internal/EngineRuntimeTestAccess.h"
911

1012
namespace {
1113

@@ -67,6 +69,22 @@ class RecordPhaseSystem : public safecrowd::engine::EngineSystem {
6769
}
6870
};
6971

72+
class RecordSeedSystem : public safecrowd::engine::EngineSystem {
73+
public:
74+
std::vector<std::uint64_t>& seeds;
75+
std::vector<std::uint64_t>& runs;
76+
77+
RecordSeedSystem(std::vector<std::uint64_t>& seedLog,
78+
std::vector<std::uint64_t>& runLog)
79+
: seeds(seedLog), runs(runLog) {}
80+
81+
void update(safecrowd::engine::EngineWorld&,
82+
const safecrowd::engine::EngineStepContext& step) override {
83+
seeds.push_back(step.derivedSeed);
84+
runs.push_back(step.runIndex);
85+
}
86+
};
87+
7088
class ResourceSetupSystem : public safecrowd::engine::EngineSystem {
7189
public:
7290
void configure(safecrowd::engine::EngineWorld& world) override {
@@ -371,3 +389,80 @@ SC_TEST(EngineRuntime_Initialize_ClearsExistingWorldResources) {
371389

372390
SC_EXPECT_TRUE(!runtime.world().resources().contains<SharedCounter>());
373391
}
392+
393+
SC_TEST(EngineRuntime_Initialize_RebuildsDeterministicRngState) {
394+
safecrowd::engine::EngineRuntime runtime({
395+
.fixedDeltaTime = 0.25,
396+
.maxCatchUpSteps = 4,
397+
.baseSeed = 23,
398+
});
399+
400+
runtime.initialize();
401+
auto& rng = safecrowd::engine::internal::EngineRuntimeTestAccess::rng(runtime);
402+
const auto firstAfterInitialize = rng.next();
403+
(void)rng.next();
404+
405+
runtime.initialize();
406+
407+
SC_EXPECT_EQ(rng.next(), firstAfterInitialize);
408+
}
409+
410+
SC_TEST(EngineRuntime_Stop_RebuildsDeterministicRngState) {
411+
safecrowd::engine::EngineRuntime runtime({
412+
.fixedDeltaTime = 0.25,
413+
.maxCatchUpSteps = 4,
414+
.baseSeed = 29,
415+
});
416+
417+
auto& rng = safecrowd::engine::internal::EngineRuntimeTestAccess::rng(runtime);
418+
const auto expectedFirstValue = rng.next();
419+
(void)rng.next();
420+
421+
runtime.stop();
422+
423+
SC_EXPECT_EQ(rng.next(), expectedFirstValue);
424+
}
425+
426+
SC_TEST(EngineRuntime_StopAndRestart_RebuildsDeterministicSeedStream) {
427+
std::vector<std::uint64_t> firstSeeds;
428+
std::vector<std::uint64_t> firstRuns;
429+
safecrowd::engine::EngineRuntime firstRuntime({
430+
.fixedDeltaTime = 0.25,
431+
.maxCatchUpSteps = 4,
432+
.baseSeed = 19,
433+
});
434+
435+
firstRuntime.addSystem(std::make_unique<RecordSeedSystem>(firstSeeds, firstRuns));
436+
firstRuntime.play();
437+
firstRuntime.stepFrame(0.25);
438+
firstRuntime.stop();
439+
firstRuntime.play();
440+
firstRuntime.stepFrame(0.25);
441+
442+
std::vector<std::uint64_t> secondSeeds;
443+
std::vector<std::uint64_t> secondRuns;
444+
safecrowd::engine::EngineRuntime secondRuntime({
445+
.fixedDeltaTime = 0.25,
446+
.maxCatchUpSteps = 4,
447+
.baseSeed = 19,
448+
});
449+
450+
secondRuntime.addSystem(std::make_unique<RecordSeedSystem>(secondSeeds, secondRuns));
451+
secondRuntime.play();
452+
secondRuntime.stepFrame(0.25);
453+
secondRuntime.stop();
454+
secondRuntime.play();
455+
secondRuntime.stepFrame(0.25);
456+
457+
SC_EXPECT_EQ(firstSeeds.size(), std::size_t{2});
458+
SC_EXPECT_EQ(secondSeeds.size(), std::size_t{2});
459+
SC_EXPECT_EQ(firstRuns.size(), std::size_t{2});
460+
SC_EXPECT_EQ(secondRuns.size(), std::size_t{2});
461+
SC_EXPECT_EQ(firstRuns[0], 1ULL);
462+
SC_EXPECT_EQ(firstRuns[1], 2ULL);
463+
SC_EXPECT_EQ(secondRuns[0], 1ULL);
464+
SC_EXPECT_EQ(secondRuns[1], 2ULL);
465+
SC_EXPECT_EQ(firstSeeds[0], secondSeeds[0]);
466+
SC_EXPECT_EQ(firstSeeds[1], secondSeeds[1]);
467+
SC_EXPECT_TRUE(firstSeeds[0] != firstSeeds[1]);
468+
}

uml/engine-overview.puml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ note bottom of Runtime
4545
Time, phase order, deferred mutation,
4646
deterministic random streams,
4747
and execution orchestration.
48+
Runtime-owned cadence and seed state
49+
reset at initialize/stop boundaries
50+
so restarts do not inherit hidden state.
4851
Initial engine scope prioritizes
4952
fixed-step scheduling over advanced
5053
event or snapshot extensions.

0 commit comments

Comments
 (0)