From f699ba7ac4242e5c80cc165073c42d0dc5c5cff3 Mon Sep 17 00:00:00 2001 From: learncold Date: Mon, 13 Apr 2026 20:18:56 +0900 Subject: [PATCH] Implement trigger policy cadence execution --- src/engine/EngineRuntime.cpp | 14 +-- src/engine/SystemDescriptor.h | 3 + src/engine/SystemScheduler.cpp | 79 ++++++++++--- src/engine/SystemScheduler.h | 6 +- tests/EngineRuntimeTests.cpp | 71 +++++++++++- tests/SystemSchedulerTests.cpp | 105 ++++++++++++++++-- ...ime-core.puml \355\225\264\354\204\244.md" | 2 + 7 files changed, 245 insertions(+), 35 deletions(-) diff --git a/src/engine/EngineRuntime.cpp b/src/engine/EngineRuntime.cpp index 8f94624..c0161b0 100644 --- a/src/engine/EngineRuntime.cpp +++ b/src/engine/EngineRuntime.cpp @@ -38,6 +38,7 @@ void EngineRuntime::initialize() { core_ = EcsCore{}; resources_ = ResourceStore{}; buffer_ = CommandBuffer{}; + scheduler_.resetCadenceState(); stats_ = {}; stats_.state = EngineState::Ready; ++runIndex_; @@ -73,6 +74,7 @@ void EngineRuntime::stop() { core_ = EcsCore{}; resources_ = ResourceStore{}; buffer_ = CommandBuffer{}; + scheduler_.resetCadenceState(); stats_ = {}; stats_.state = EngineState::Stopped; } @@ -100,8 +102,7 @@ void EngineRuntime::stepFrame(double deltaSeconds) { .derivedSeed = 0, }; - scheduler_.executePhase(UpdatePhase::PreSimulation, TriggerPolicy::EveryFrame, - world_, ctx); + scheduler_.executePhase(UpdatePhase::PreSimulation, world_, ctx); while (frameClock_.shouldRunFixedStep()) { frameClock_.consumeFixedStep(); @@ -116,19 +117,16 @@ void EngineRuntime::stepFrame(double deltaSeconds) { .derivedSeed = 0, }; - scheduler_.executePhase(UpdatePhase::FixedSimulation, TriggerPolicy::FixedStep, - world_, ctx); + scheduler_.executePhase(UpdatePhase::FixedSimulation, world_, ctx); } ctx.alpha = frameClock_.alpha(); - scheduler_.executePhase(UpdatePhase::PostSimulation, TriggerPolicy::EveryFrame, - world_, ctx); + scheduler_.executePhase(UpdatePhase::PostSimulation, world_, ctx); stats_.alpha = frameClock_.alpha(); ctx.alpha = stats_.alpha; - scheduler_.executePhase(UpdatePhase::RenderSync, TriggerPolicy::EveryFrame, - world_, ctx); + scheduler_.executePhase(UpdatePhase::RenderSync, world_, ctx); } EngineWorld& EngineRuntime::world() noexcept { diff --git a/src/engine/SystemDescriptor.h b/src/engine/SystemDescriptor.h index 110c8bc..4315789 100644 --- a/src/engine/SystemDescriptor.h +++ b/src/engine/SystemDescriptor.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "engine/TriggerPolicy.h" #include "engine/UpdatePhase.h" @@ -9,6 +11,7 @@ struct SystemDescriptor { UpdatePhase phase{UpdatePhase::FixedSimulation}; int order{0}; TriggerPolicy triggerPolicy{TriggerPolicy::FixedStep}; + std::uint32_t intervalTicks{1}; }; } // namespace safecrowd::engine diff --git a/src/engine/SystemScheduler.cpp b/src/engine/SystemScheduler.cpp index ef9845b..7d91cc7 100644 --- a/src/engine/SystemScheduler.cpp +++ b/src/engine/SystemScheduler.cpp @@ -7,21 +7,37 @@ namespace safecrowd::engine { namespace { void validateDescriptor(const SystemDescriptor& descriptor) { - if (descriptor.triggerPolicy == TriggerPolicy::Interval) { - throw std::invalid_argument("TriggerPolicy::Interval is not supported yet."); - } - - if (descriptor.phase == UpdatePhase::FixedSimulation && - descriptor.triggerPolicy != TriggerPolicy::FixedStep) { + if (descriptor.triggerPolicy == TriggerPolicy::Interval && + descriptor.intervalTicks == 0) { throw std::invalid_argument( - "FixedSimulation systems must use TriggerPolicy::FixedStep."); + "TriggerPolicy::Interval requires intervalTicks > 0."); } - if (descriptor.phase != UpdatePhase::FixedSimulation && - descriptor.phase != UpdatePhase::Startup && - descriptor.triggerPolicy != TriggerPolicy::EveryFrame) { - throw std::invalid_argument( - "Frame phases must use TriggerPolicy::EveryFrame."); + switch (descriptor.phase) { + case UpdatePhase::Startup: + if (descriptor.triggerPolicy != TriggerPolicy::EveryFrame) { + throw std::invalid_argument( + "Startup systems must use TriggerPolicy::EveryFrame."); + } + break; + + case UpdatePhase::FixedSimulation: + if (descriptor.triggerPolicy == TriggerPolicy::EveryFrame) { + throw std::invalid_argument( + "FixedSimulation systems must use TriggerPolicy::FixedStep or " + "TriggerPolicy::Interval."); + } + break; + + case UpdatePhase::PreSimulation: + case UpdatePhase::PostSimulation: + case UpdatePhase::RenderSync: + if (descriptor.triggerPolicy == TriggerPolicy::FixedStep) { + throw std::invalid_argument( + "Frame phases must use TriggerPolicy::EveryFrame or " + "TriggerPolicy::Interval."); + } + break; } } @@ -55,15 +71,46 @@ void SystemScheduler::executeStartup(EngineWorld& world, const EngineStepContext buffer_.flush(core_); } -void SystemScheduler::executePhase(UpdatePhase phase, TriggerPolicy triggerPolicy, - EngineWorld& world, const EngineStepContext& ctx) { +void SystemScheduler::executePhase(UpdatePhase phase, EngineWorld& world, + const EngineStepContext& ctx) { + auto shouldExecuteInterval = [](Entry& entry) { + if (entry.intervalCountdown == 0) { + entry.intervalCountdown = entry.descriptor.intervalTicks - 1; + return true; + } + + --entry.intervalCountdown; + return false; + }; + for (auto& e : entries_) { - if (e.descriptor.phase == phase && - e.descriptor.triggerPolicy == triggerPolicy) { + if (e.descriptor.phase != phase) { + continue; + } + + bool shouldExecute = false; + switch (e.descriptor.triggerPolicy) { + case TriggerPolicy::EveryFrame: + case TriggerPolicy::FixedStep: + shouldExecute = true; + break; + + case TriggerPolicy::Interval: + shouldExecute = shouldExecuteInterval(e); + break; + } + + if (shouldExecute) { e.system->update(world, ctx); } } buffer_.flush(core_); } +void SystemScheduler::resetCadenceState() { + for (auto& e : entries_) { + e.intervalCountdown = 0; + } +} + } // namespace safecrowd::engine diff --git a/src/engine/SystemScheduler.h b/src/engine/SystemScheduler.h index a6fec7a..a82082d 100644 --- a/src/engine/SystemScheduler.h +++ b/src/engine/SystemScheduler.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -18,13 +19,14 @@ class SystemScheduler { void registerSystem(std::unique_ptr system, SystemDescriptor descriptor); void configure(EngineWorld& world); void executeStartup(EngineWorld& world, const EngineStepContext& ctx); - void executePhase(UpdatePhase phase, TriggerPolicy triggerPolicy, - EngineWorld& world, const EngineStepContext& ctx); + void executePhase(UpdatePhase phase, EngineWorld& world, const EngineStepContext& ctx); + void resetCadenceState(); private: struct Entry { std::unique_ptr system; SystemDescriptor descriptor; + std::uint32_t intervalCountdown{0}; }; EcsCore& core_; diff --git a/tests/EngineRuntimeTests.cpp b/tests/EngineRuntimeTests.cpp index 7c2a45f..4b5836d 100644 --- a/tests/EngineRuntimeTests.cpp +++ b/tests/EngineRuntimeTests.cpp @@ -236,7 +236,43 @@ SC_TEST(EngineRuntime_PausedRuntime_DoesNotAdvanceSimulation) { SC_EXPECT_EQ(count, 2); } -SC_TEST(EngineRuntime_AddSystem_RejectsUnsupportedIntervalTriggerPolicy) { +SC_TEST(EngineRuntime_IntervalSystemsFollowTheirPhaseCadence) { + int frameCadenceCount = 0; + int fixedCadenceCount = 0; + + safecrowd::engine::EngineRuntime runtime({ + .fixedDeltaTime = 0.25, + .maxCatchUpSteps = 4, + .baseSeed = 1, + }); + + runtime.addSystem( + std::make_unique(frameCadenceCount), + {.phase = safecrowd::engine::UpdatePhase::PreSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval, + .intervalTicks = 2}); + runtime.addSystem( + std::make_unique(fixedCadenceCount), + {.phase = safecrowd::engine::UpdatePhase::FixedSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval, + .intervalTicks = 2}); + + runtime.play(); + + runtime.stepFrame(0.50); + SC_EXPECT_EQ(frameCadenceCount, 1); + SC_EXPECT_EQ(fixedCadenceCount, 1); + + runtime.stepFrame(0.25); + SC_EXPECT_EQ(frameCadenceCount, 1); + SC_EXPECT_EQ(fixedCadenceCount, 2); + + runtime.stepFrame(0.25); + SC_EXPECT_EQ(frameCadenceCount, 2); + SC_EXPECT_EQ(fixedCadenceCount, 2); +} + +SC_TEST(EngineRuntime_AddSystem_RejectsZeroIntervalTicks) { int count = 0; safecrowd::engine::EngineRuntime runtime; @@ -245,7 +281,8 @@ SC_TEST(EngineRuntime_AddSystem_RejectsUnsupportedIntervalTriggerPolicy) { runtime.addSystem( std::make_unique(count), {.phase = safecrowd::engine::UpdatePhase::PreSimulation, - .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval}); + .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval, + .intervalTicks = 0}); } catch (const std::exception&) { threw = true; } @@ -253,6 +290,36 @@ SC_TEST(EngineRuntime_AddSystem_RejectsUnsupportedIntervalTriggerPolicy) { SC_EXPECT_TRUE(threw); } +SC_TEST(EngineRuntime_InitializeAndStop_ResetIntervalCadenceState) { + int count = 0; + + safecrowd::engine::EngineRuntime runtime({ + .fixedDeltaTime = 0.25, + .maxCatchUpSteps = 4, + .baseSeed = 1, + }); + + runtime.addSystem( + std::make_unique(count), + {.phase = safecrowd::engine::UpdatePhase::FixedSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval, + .intervalTicks = 3}); + + runtime.play(); + runtime.stepFrame(0.25); + runtime.stepFrame(0.25); + SC_EXPECT_EQ(count, 1); + + runtime.initialize(); + runtime.stepFrame(0.25); + SC_EXPECT_EQ(count, 2); + + runtime.stop(); + runtime.play(); + runtime.stepFrame(0.25); + SC_EXPECT_EQ(count, 3); +} + SC_TEST(EngineRuntimePauseAndStopResetLifecycleState) { safecrowd::engine::EngineRuntime runtime({ .fixedDeltaTime = 0.25, diff --git a/tests/SystemSchedulerTests.cpp b/tests/SystemSchedulerTests.cpp index 8681127..aa246f7 100644 --- a/tests/SystemSchedulerTests.cpp +++ b/tests/SystemSchedulerTests.cpp @@ -88,12 +88,10 @@ SC_TEST(SystemScheduler_ExecutesSystemsInPhaseOrder) { const safecrowd::engine::EngineStepContext ctx{}; scheduler.executePhase( safecrowd::engine::UpdatePhase::PreSimulation, - safecrowd::engine::TriggerPolicy::EveryFrame, world, ctx); scheduler.executePhase( safecrowd::engine::UpdatePhase::PostSimulation, - safecrowd::engine::TriggerPolicy::EveryFrame, world, ctx); @@ -124,7 +122,6 @@ SC_TEST(SystemScheduler_ExecutesSystemsInOrderWithinPhase) { const safecrowd::engine::EngineStepContext ctx{}; scheduler.executePhase( safecrowd::engine::UpdatePhase::FixedSimulation, - safecrowd::engine::TriggerPolicy::FixedStep, world, ctx); @@ -154,7 +151,6 @@ SC_TEST(SystemScheduler_PhaseIsolation_OtherPhaseSystemsNotExecuted) { const safecrowd::engine::EngineStepContext ctx{}; scheduler.executePhase( safecrowd::engine::UpdatePhase::FixedSimulation, - safecrowd::engine::TriggerPolicy::FixedStep, world, ctx); @@ -195,7 +191,6 @@ SC_TEST(SystemScheduler_FlushesCommandBufferAfterPhase) { const safecrowd::engine::EngineStepContext ctx{}; scheduler.executePhase( safecrowd::engine::UpdatePhase::FixedSimulation, - safecrowd::engine::TriggerPolicy::FixedStep, world, ctx); @@ -203,7 +198,102 @@ SC_TEST(SystemScheduler_FlushesCommandBufferAfterPhase) { SC_EXPECT_EQ(entities.size(), std::size_t{1}); } -SC_TEST(SystemScheduler_RegisterSystem_RejectsUnsupportedIntervalPolicy) { +SC_TEST(SystemScheduler_IntervalSystemsUseFrameCadenceDeterministically) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + + safecrowd::engine::EcsCore dummyCore; + safecrowd::engine::ResourceStore dummyResources; + safecrowd::engine::CommandBuffer dummyBuffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create( + dummyCore, dummyResources, dummyBuffer); + + std::vector log; + scheduler.registerSystem( + std::make_unique(log, 1), + {.phase = safecrowd::engine::UpdatePhase::PreSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval, + .intervalTicks = 2}); + + const safecrowd::engine::EngineStepContext ctx{}; + for (int i = 0; i < 5; ++i) { + scheduler.executePhase( + safecrowd::engine::UpdatePhase::PreSimulation, + world, + ctx); + } + + SC_EXPECT_EQ(log.size(), std::size_t{3}); +} + +SC_TEST(SystemScheduler_IntervalSystemsUseFixedStepCadenceDeterministically) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + + safecrowd::engine::EcsCore dummyCore; + safecrowd::engine::ResourceStore dummyResources; + safecrowd::engine::CommandBuffer dummyBuffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create( + dummyCore, dummyResources, dummyBuffer); + + std::vector log; + scheduler.registerSystem( + std::make_unique(log, 1), + {.phase = safecrowd::engine::UpdatePhase::FixedSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval, + .intervalTicks = 3}); + + const safecrowd::engine::EngineStepContext ctx{}; + for (int i = 0; i < 5; ++i) { + scheduler.executePhase( + safecrowd::engine::UpdatePhase::FixedSimulation, + world, + ctx); + } + + SC_EXPECT_EQ(log.size(), std::size_t{2}); +} + +SC_TEST(SystemScheduler_ResetCadenceState_RestartsIntervalSequence) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + + safecrowd::engine::EcsCore dummyCore; + safecrowd::engine::ResourceStore dummyResources; + safecrowd::engine::CommandBuffer dummyBuffer; + auto world = safecrowd::engine::internal::EngineWorldFactory::create( + dummyCore, dummyResources, dummyBuffer); + + std::vector log; + scheduler.registerSystem( + std::make_unique(log, 1), + {.phase = safecrowd::engine::UpdatePhase::PreSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval, + .intervalTicks = 2}); + + const safecrowd::engine::EngineStepContext ctx{}; + scheduler.executePhase( + safecrowd::engine::UpdatePhase::PreSimulation, + world, + ctx); + scheduler.executePhase( + safecrowd::engine::UpdatePhase::PreSimulation, + world, + ctx); + + scheduler.resetCadenceState(); + scheduler.executePhase( + safecrowd::engine::UpdatePhase::PreSimulation, + world, + ctx); + + SC_EXPECT_EQ(log.size(), std::size_t{2}); +} + +SC_TEST(SystemScheduler_RegisterSystem_RejectsZeroIntervalTicks) { safecrowd::engine::EcsCore core; safecrowd::engine::CommandBuffer buffer; safecrowd::engine::SystemScheduler scheduler{core, buffer}; @@ -214,7 +304,8 @@ SC_TEST(SystemScheduler_RegisterSystem_RejectsUnsupportedIntervalPolicy) { scheduler.registerSystem( std::make_unique(log, 1), {.phase = safecrowd::engine::UpdatePhase::PreSimulation, - .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval}); + .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval, + .intervalTicks = 0}); } catch (const std::exception&) { threw = true; } diff --git "a/uml/engine-runtime-core.puml \355\225\264\354\204\244.md" "b/uml/engine-runtime-core.puml \355\225\264\354\204\244.md" index 5ac673b..a419183 100644 --- "a/uml/engine-runtime-core.puml \355\225\264\354\204\244.md" +++ "b/uml/engine-runtime-core.puml \355\225\264\354\204\244.md" @@ -50,6 +50,7 @@ ## `SystemDescriptor` - 개요: 시스템의 phase, 순서, 간격, 의존성 제약을 설명하는 메타데이터다. - 목적: 시스템 실행 순서를 코드 바깥의 선언 정보로 표현하기 위해 필요하다. +- 현재 구현 포인트: `TriggerPolicy::Interval`을 쓰는 시스템은 `intervalTicks`로 cadence를 선언하고, frame phase와 fixed-step phase에서 각각 자기 cadence 기준으로 판정된다. - 유의사항: descriptor가 너무 많은 실행 정책을 한꺼번에 품기 시작하면 scheduler가 과도하게 복잡해진다. - 후속 개선 사항: conflict validation, category grouping, debug print를 추가할 수 있다. @@ -104,6 +105,7 @@ ## `SystemScheduler` - 개요: 시스템 등록과 phase별 실행을 담당하는 스케줄러다. - 목적: 어떤 시스템이 언제 어떤 순서로 실행되는지 중앙에서 관리한다. +- 현재 구현 포인트: phase 호출이 들어오면 descriptor의 `TriggerPolicy`를 직접 판정하고, `Interval`용 cadence 상태는 scheduler가 보관했다가 `initialize()`/`stop()` 경계에서 reset된다. - 유의사항: scheduler는 직접 도메인 규칙을 품는 객체가 아니라 실행 순서를 보장하는 범용 도구여야 한다. - 후속 개선 사항: descriptor validation, parallel phase, system profiling을 확장할 수 있다.