Skip to content

Commit 58a5233

Browse files
authored
Implement trigger policy cadence execution (#128)
1 parent 8501ceb commit 58a5233

7 files changed

Lines changed: 245 additions & 35 deletions

src/engine/EngineRuntime.cpp

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ void EngineRuntime::initialize() {
3838
core_ = EcsCore{};
3939
resources_ = ResourceStore{};
4040
buffer_ = CommandBuffer{};
41+
scheduler_.resetCadenceState();
4142
stats_ = {};
4243
stats_.state = EngineState::Ready;
4344
++runIndex_;
@@ -73,6 +74,7 @@ void EngineRuntime::stop() {
7374
core_ = EcsCore{};
7475
resources_ = ResourceStore{};
7576
buffer_ = CommandBuffer{};
77+
scheduler_.resetCadenceState();
7678
stats_ = {};
7779
stats_.state = EngineState::Stopped;
7880
}
@@ -100,8 +102,7 @@ void EngineRuntime::stepFrame(double deltaSeconds) {
100102
.derivedSeed = 0,
101103
};
102104

103-
scheduler_.executePhase(UpdatePhase::PreSimulation, TriggerPolicy::EveryFrame,
104-
world_, ctx);
105+
scheduler_.executePhase(UpdatePhase::PreSimulation, world_, ctx);
105106

106107
while (frameClock_.shouldRunFixedStep()) {
107108
frameClock_.consumeFixedStep();
@@ -116,19 +117,16 @@ void EngineRuntime::stepFrame(double deltaSeconds) {
116117
.derivedSeed = 0,
117118
};
118119

119-
scheduler_.executePhase(UpdatePhase::FixedSimulation, TriggerPolicy::FixedStep,
120-
world_, ctx);
120+
scheduler_.executePhase(UpdatePhase::FixedSimulation, world_, ctx);
121121
}
122122

123123
ctx.alpha = frameClock_.alpha();
124-
scheduler_.executePhase(UpdatePhase::PostSimulation, TriggerPolicy::EveryFrame,
125-
world_, ctx);
124+
scheduler_.executePhase(UpdatePhase::PostSimulation, world_, ctx);
126125

127126
stats_.alpha = frameClock_.alpha();
128127

129128
ctx.alpha = stats_.alpha;
130-
scheduler_.executePhase(UpdatePhase::RenderSync, TriggerPolicy::EveryFrame,
131-
world_, ctx);
129+
scheduler_.executePhase(UpdatePhase::RenderSync, world_, ctx);
132130
}
133131

134132
EngineWorld& EngineRuntime::world() noexcept {

src/engine/SystemDescriptor.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#pragma once
22

3+
#include <cstdint>
4+
35
#include "engine/TriggerPolicy.h"
46
#include "engine/UpdatePhase.h"
57

@@ -9,6 +11,7 @@ struct SystemDescriptor {
911
UpdatePhase phase{UpdatePhase::FixedSimulation};
1012
int order{0};
1113
TriggerPolicy triggerPolicy{TriggerPolicy::FixedStep};
14+
std::uint32_t intervalTicks{1};
1215
};
1316

1417
} // namespace safecrowd::engine

src/engine/SystemScheduler.cpp

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,37 @@ namespace safecrowd::engine {
77
namespace {
88

99
void validateDescriptor(const SystemDescriptor& descriptor) {
10-
if (descriptor.triggerPolicy == TriggerPolicy::Interval) {
11-
throw std::invalid_argument("TriggerPolicy::Interval is not supported yet.");
12-
}
13-
14-
if (descriptor.phase == UpdatePhase::FixedSimulation &&
15-
descriptor.triggerPolicy != TriggerPolicy::FixedStep) {
10+
if (descriptor.triggerPolicy == TriggerPolicy::Interval &&
11+
descriptor.intervalTicks == 0) {
1612
throw std::invalid_argument(
17-
"FixedSimulation systems must use TriggerPolicy::FixedStep.");
13+
"TriggerPolicy::Interval requires intervalTicks > 0.");
1814
}
1915

20-
if (descriptor.phase != UpdatePhase::FixedSimulation &&
21-
descriptor.phase != UpdatePhase::Startup &&
22-
descriptor.triggerPolicy != TriggerPolicy::EveryFrame) {
23-
throw std::invalid_argument(
24-
"Frame phases must use TriggerPolicy::EveryFrame.");
16+
switch (descriptor.phase) {
17+
case UpdatePhase::Startup:
18+
if (descriptor.triggerPolicy != TriggerPolicy::EveryFrame) {
19+
throw std::invalid_argument(
20+
"Startup systems must use TriggerPolicy::EveryFrame.");
21+
}
22+
break;
23+
24+
case UpdatePhase::FixedSimulation:
25+
if (descriptor.triggerPolicy == TriggerPolicy::EveryFrame) {
26+
throw std::invalid_argument(
27+
"FixedSimulation systems must use TriggerPolicy::FixedStep or "
28+
"TriggerPolicy::Interval.");
29+
}
30+
break;
31+
32+
case UpdatePhase::PreSimulation:
33+
case UpdatePhase::PostSimulation:
34+
case UpdatePhase::RenderSync:
35+
if (descriptor.triggerPolicy == TriggerPolicy::FixedStep) {
36+
throw std::invalid_argument(
37+
"Frame phases must use TriggerPolicy::EveryFrame or "
38+
"TriggerPolicy::Interval.");
39+
}
40+
break;
2541
}
2642
}
2743

@@ -55,15 +71,46 @@ void SystemScheduler::executeStartup(EngineWorld& world, const EngineStepContext
5571
buffer_.flush(core_);
5672
}
5773

58-
void SystemScheduler::executePhase(UpdatePhase phase, TriggerPolicy triggerPolicy,
59-
EngineWorld& world, const EngineStepContext& ctx) {
74+
void SystemScheduler::executePhase(UpdatePhase phase, EngineWorld& world,
75+
const EngineStepContext& ctx) {
76+
auto shouldExecuteInterval = [](Entry& entry) {
77+
if (entry.intervalCountdown == 0) {
78+
entry.intervalCountdown = entry.descriptor.intervalTicks - 1;
79+
return true;
80+
}
81+
82+
--entry.intervalCountdown;
83+
return false;
84+
};
85+
6086
for (auto& e : entries_) {
61-
if (e.descriptor.phase == phase &&
62-
e.descriptor.triggerPolicy == triggerPolicy) {
87+
if (e.descriptor.phase != phase) {
88+
continue;
89+
}
90+
91+
bool shouldExecute = false;
92+
switch (e.descriptor.triggerPolicy) {
93+
case TriggerPolicy::EveryFrame:
94+
case TriggerPolicy::FixedStep:
95+
shouldExecute = true;
96+
break;
97+
98+
case TriggerPolicy::Interval:
99+
shouldExecute = shouldExecuteInterval(e);
100+
break;
101+
}
102+
103+
if (shouldExecute) {
63104
e.system->update(world, ctx);
64105
}
65106
}
66107
buffer_.flush(core_);
67108
}
68109

110+
void SystemScheduler::resetCadenceState() {
111+
for (auto& e : entries_) {
112+
e.intervalCountdown = 0;
113+
}
114+
}
115+
69116
} // namespace safecrowd::engine

src/engine/SystemScheduler.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <cstdint>
34
#include <memory>
45
#include <vector>
56

@@ -18,13 +19,14 @@ class SystemScheduler {
1819
void registerSystem(std::unique_ptr<EngineSystem> system, SystemDescriptor descriptor);
1920
void configure(EngineWorld& world);
2021
void executeStartup(EngineWorld& world, const EngineStepContext& ctx);
21-
void executePhase(UpdatePhase phase, TriggerPolicy triggerPolicy,
22-
EngineWorld& world, const EngineStepContext& ctx);
22+
void executePhase(UpdatePhase phase, EngineWorld& world, const EngineStepContext& ctx);
23+
void resetCadenceState();
2324

2425
private:
2526
struct Entry {
2627
std::unique_ptr<EngineSystem> system;
2728
SystemDescriptor descriptor;
29+
std::uint32_t intervalCountdown{0};
2830
};
2931

3032
EcsCore& core_;

tests/EngineRuntimeTests.cpp

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,43 @@ SC_TEST(EngineRuntime_PausedRuntime_DoesNotAdvanceSimulation) {
236236
SC_EXPECT_EQ(count, 2);
237237
}
238238

239-
SC_TEST(EngineRuntime_AddSystem_RejectsUnsupportedIntervalTriggerPolicy) {
239+
SC_TEST(EngineRuntime_IntervalSystemsFollowTheirPhaseCadence) {
240+
int frameCadenceCount = 0;
241+
int fixedCadenceCount = 0;
242+
243+
safecrowd::engine::EngineRuntime runtime({
244+
.fixedDeltaTime = 0.25,
245+
.maxCatchUpSteps = 4,
246+
.baseSeed = 1,
247+
});
248+
249+
runtime.addSystem(
250+
std::make_unique<UpdateCounterSystem>(frameCadenceCount),
251+
{.phase = safecrowd::engine::UpdatePhase::PreSimulation,
252+
.triggerPolicy = safecrowd::engine::TriggerPolicy::Interval,
253+
.intervalTicks = 2});
254+
runtime.addSystem(
255+
std::make_unique<UpdateCounterSystem>(fixedCadenceCount),
256+
{.phase = safecrowd::engine::UpdatePhase::FixedSimulation,
257+
.triggerPolicy = safecrowd::engine::TriggerPolicy::Interval,
258+
.intervalTicks = 2});
259+
260+
runtime.play();
261+
262+
runtime.stepFrame(0.50);
263+
SC_EXPECT_EQ(frameCadenceCount, 1);
264+
SC_EXPECT_EQ(fixedCadenceCount, 1);
265+
266+
runtime.stepFrame(0.25);
267+
SC_EXPECT_EQ(frameCadenceCount, 1);
268+
SC_EXPECT_EQ(fixedCadenceCount, 2);
269+
270+
runtime.stepFrame(0.25);
271+
SC_EXPECT_EQ(frameCadenceCount, 2);
272+
SC_EXPECT_EQ(fixedCadenceCount, 2);
273+
}
274+
275+
SC_TEST(EngineRuntime_AddSystem_RejectsZeroIntervalTicks) {
240276
int count = 0;
241277
safecrowd::engine::EngineRuntime runtime;
242278

@@ -245,14 +281,45 @@ SC_TEST(EngineRuntime_AddSystem_RejectsUnsupportedIntervalTriggerPolicy) {
245281
runtime.addSystem(
246282
std::make_unique<UpdateCounterSystem>(count),
247283
{.phase = safecrowd::engine::UpdatePhase::PreSimulation,
248-
.triggerPolicy = safecrowd::engine::TriggerPolicy::Interval});
284+
.triggerPolicy = safecrowd::engine::TriggerPolicy::Interval,
285+
.intervalTicks = 0});
249286
} catch (const std::exception&) {
250287
threw = true;
251288
}
252289

253290
SC_EXPECT_TRUE(threw);
254291
}
255292

293+
SC_TEST(EngineRuntime_InitializeAndStop_ResetIntervalCadenceState) {
294+
int count = 0;
295+
296+
safecrowd::engine::EngineRuntime runtime({
297+
.fixedDeltaTime = 0.25,
298+
.maxCatchUpSteps = 4,
299+
.baseSeed = 1,
300+
});
301+
302+
runtime.addSystem(
303+
std::make_unique<UpdateCounterSystem>(count),
304+
{.phase = safecrowd::engine::UpdatePhase::FixedSimulation,
305+
.triggerPolicy = safecrowd::engine::TriggerPolicy::Interval,
306+
.intervalTicks = 3});
307+
308+
runtime.play();
309+
runtime.stepFrame(0.25);
310+
runtime.stepFrame(0.25);
311+
SC_EXPECT_EQ(count, 1);
312+
313+
runtime.initialize();
314+
runtime.stepFrame(0.25);
315+
SC_EXPECT_EQ(count, 2);
316+
317+
runtime.stop();
318+
runtime.play();
319+
runtime.stepFrame(0.25);
320+
SC_EXPECT_EQ(count, 3);
321+
}
322+
256323
SC_TEST(EngineRuntimePauseAndStopResetLifecycleState) {
257324
safecrowd::engine::EngineRuntime runtime({
258325
.fixedDeltaTime = 0.25,

0 commit comments

Comments
 (0)