diff --git a/CMakeLists.txt b/CMakeLists.txt index cebed69..840077f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,9 +51,14 @@ add_library(ecs_engine STATIC src/engine/FrameClock.h src/engine/WorldQuery.h src/engine/CommandBuffer.h + src/engine/UpdatePhase.h + src/engine/TriggerPolicy.h + src/engine/SystemDescriptor.h + src/engine/SystemScheduler.h src/engine/EntityRegistry.cpp src/engine/EngineRuntime.cpp src/engine/FrameClock.cpp + src/engine/SystemScheduler.cpp ) target_include_directories(ecs_engine @@ -112,6 +117,7 @@ if (BUILD_TESTING) tests/FacilityLayoutBuilderTests.cpp tests/WorldQueryTests.cpp tests/CommandBufferTests.cpp + tests/SystemSchedulerTests.cpp ) target_include_directories(safecrowd_tests diff --git a/src/engine/EngineRuntime.cpp b/src/engine/EngineRuntime.cpp index 2cc7e0f..5a3803e 100644 --- a/src/engine/EngineRuntime.cpp +++ b/src/engine/EngineRuntime.cpp @@ -23,12 +23,14 @@ EngineConfig normalizeConfig(EngineConfig config) { EngineRuntime::EngineRuntime(EngineConfig config) : config_(normalizeConfig(config)), + scheduler_(core_, buffer_), world_(core_, buffer_), frameClock_(config_) { } -void EngineRuntime::addSystem(std::unique_ptr system) { - systems_.push_back(std::move(system)); +void EngineRuntime::addSystem(std::unique_ptr system, + SystemDescriptor descriptor) { + scheduler_.registerSystem(std::move(system), descriptor); } void EngineRuntime::initialize() { @@ -39,10 +41,16 @@ void EngineRuntime::initialize() { stats_.state = EngineState::Ready; ++runIndex_; - for (auto& system : systems_) { - system->configure(world_); - buffer_.flush(core_); - } + scheduler_.configure(world_); + + const EngineStepContext startupCtx{ + .frameIndex = stats_.frameIndex, + .fixedStepIndex = stats_.fixedStepIndex, + .alpha = 0.0, + .runIndex = runIndex_, + .derivedSeed = 0, + }; + scheduler_.executeStartup(world_, startupCtx); } void EngineRuntime::play() { @@ -82,12 +90,23 @@ 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, + }; + + scheduler_.executePhase(UpdatePhase::PreSimulation, TriggerPolicy::EveryFrame, + world_, ctx); + while (frameClock_.shouldRunFixedStep()) { frameClock_.consumeFixedStep(); ++stats_.fixedStepIndex; ++stats_.fixedStepsThisFrame; - const EngineStepContext ctx{ + ctx = EngineStepContext{ .frameIndex = stats_.frameIndex, .fixedStepIndex = stats_.fixedStepIndex, .alpha = frameClock_.alpha(), @@ -95,14 +114,19 @@ void EngineRuntime::stepFrame(double deltaSeconds) { .derivedSeed = 0, }; - for (auto& system : systems_) { - system->update(world_, ctx); - } - - buffer_.flush(core_); + scheduler_.executePhase(UpdatePhase::FixedSimulation, TriggerPolicy::FixedStep, + world_, ctx); } + ctx.alpha = frameClock_.alpha(); + scheduler_.executePhase(UpdatePhase::PostSimulation, TriggerPolicy::EveryFrame, + world_, ctx); + stats_.alpha = frameClock_.alpha(); + + ctx.alpha = stats_.alpha; + scheduler_.executePhase(UpdatePhase::RenderSync, TriggerPolicy::EveryFrame, + world_, ctx); } EngineWorld& EngineRuntime::world() noexcept { diff --git a/src/engine/EngineRuntime.h b/src/engine/EngineRuntime.h index 05c5336..340e7d1 100644 --- a/src/engine/EngineRuntime.h +++ b/src/engine/EngineRuntime.h @@ -2,7 +2,6 @@ #include #include -#include #include "engine/CommandBuffer.h" #include "engine/EcsCore.h" @@ -10,6 +9,8 @@ #include "engine/EngineStats.h" #include "engine/EngineSystem.h" #include "engine/FrameClock.h" +#include "engine/SystemDescriptor.h" +#include "engine/SystemScheduler.h" namespace safecrowd::engine { @@ -17,7 +18,8 @@ class EngineRuntime { public: explicit EngineRuntime(EngineConfig config = {}); - void addSystem(std::unique_ptr system); + void addSystem(std::unique_ptr system, + SystemDescriptor descriptor = {}); void initialize(); void play(); @@ -33,14 +35,14 @@ class EngineRuntime { std::uint64_t runIndex() const noexcept; private: - EngineConfig config_; - EngineStats stats_; - EcsCore core_; - CommandBuffer buffer_; - EngineWorld world_; - FrameClock frameClock_; - std::uint64_t runIndex_{0}; - std::vector> systems_; + EngineConfig config_; + EngineStats stats_; + EcsCore core_; + CommandBuffer buffer_; + SystemScheduler scheduler_; + EngineWorld world_; + FrameClock frameClock_; + std::uint64_t runIndex_{0}; }; } // namespace safecrowd::engine diff --git a/src/engine/SystemDescriptor.h b/src/engine/SystemDescriptor.h new file mode 100644 index 0000000..110c8bc --- /dev/null +++ b/src/engine/SystemDescriptor.h @@ -0,0 +1,14 @@ +#pragma once + +#include "engine/TriggerPolicy.h" +#include "engine/UpdatePhase.h" + +namespace safecrowd::engine { + +struct SystemDescriptor { + UpdatePhase phase{UpdatePhase::FixedSimulation}; + int order{0}; + TriggerPolicy triggerPolicy{TriggerPolicy::FixedStep}; +}; + +} // namespace safecrowd::engine diff --git a/src/engine/SystemScheduler.cpp b/src/engine/SystemScheduler.cpp new file mode 100644 index 0000000..ef9845b --- /dev/null +++ b/src/engine/SystemScheduler.cpp @@ -0,0 +1,69 @@ +#include "engine/SystemScheduler.h" + +#include +#include + +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) { + throw std::invalid_argument( + "FixedSimulation systems must use TriggerPolicy::FixedStep."); + } + + if (descriptor.phase != UpdatePhase::FixedSimulation && + descriptor.phase != UpdatePhase::Startup && + descriptor.triggerPolicy != TriggerPolicy::EveryFrame) { + throw std::invalid_argument( + "Frame phases must use TriggerPolicy::EveryFrame."); + } +} + +} // namespace + +void SystemScheduler::registerSystem(std::unique_ptr system, + SystemDescriptor descriptor) { + validateDescriptor(descriptor); + entries_.push_back({std::move(system), descriptor}); + std::stable_sort(entries_.begin(), entries_.end(), [](const Entry& a, const Entry& b) { + if (a.descriptor.phase != b.descriptor.phase) { + return static_cast(a.descriptor.phase) < static_cast(b.descriptor.phase); + } + return a.descriptor.order < b.descriptor.order; + }); +} + +void SystemScheduler::configure(EngineWorld& world) { + for (auto& e : entries_) { + e.system->configure(world); + buffer_.flush(core_); + } +} + +void SystemScheduler::executeStartup(EngineWorld& world, const EngineStepContext& ctx) { + for (auto& e : entries_) { + if (e.descriptor.phase == UpdatePhase::Startup) { + e.system->update(world, ctx); + } + } + buffer_.flush(core_); +} + +void SystemScheduler::executePhase(UpdatePhase phase, TriggerPolicy triggerPolicy, + EngineWorld& world, const EngineStepContext& ctx) { + for (auto& e : entries_) { + if (e.descriptor.phase == phase && + e.descriptor.triggerPolicy == triggerPolicy) { + e.system->update(world, ctx); + } + } + buffer_.flush(core_); +} + +} // namespace safecrowd::engine diff --git a/src/engine/SystemScheduler.h b/src/engine/SystemScheduler.h new file mode 100644 index 0000000..a6fec7a --- /dev/null +++ b/src/engine/SystemScheduler.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include "engine/EcsCore.h" +#include "engine/EngineSystem.h" +#include "engine/SystemDescriptor.h" +#include "engine/UpdatePhase.h" + +namespace safecrowd::engine { + +class SystemScheduler { +public: + SystemScheduler(EcsCore& core, CommandBuffer& buffer) + : core_(core), buffer_(buffer) {} + + 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); + +private: + struct Entry { + std::unique_ptr system; + SystemDescriptor descriptor; + }; + + EcsCore& core_; + CommandBuffer& buffer_; + std::vector entries_; +}; + +} // namespace safecrowd::engine diff --git a/src/engine/TriggerPolicy.h b/src/engine/TriggerPolicy.h new file mode 100644 index 0000000..6f101d6 --- /dev/null +++ b/src/engine/TriggerPolicy.h @@ -0,0 +1,11 @@ +#pragma once + +namespace safecrowd::engine { + +enum class TriggerPolicy { + EveryFrame, + FixedStep, + Interval, +}; + +} // namespace safecrowd::engine diff --git a/src/engine/UpdatePhase.h b/src/engine/UpdatePhase.h new file mode 100644 index 0000000..7fd694f --- /dev/null +++ b/src/engine/UpdatePhase.h @@ -0,0 +1,13 @@ +#pragma once + +namespace safecrowd::engine { + +enum class UpdatePhase { + Startup, + PreSimulation, + FixedSimulation, + PostSimulation, + RenderSync, +}; + +} // namespace safecrowd::engine diff --git a/tests/EngineRuntimeTests.cpp b/tests/EngineRuntimeTests.cpp index f3ac57f..e87969e 100644 --- a/tests/EngineRuntimeTests.cpp +++ b/tests/EngineRuntimeTests.cpp @@ -1,7 +1,9 @@ #include "TestSupport.h" #include +#include #include +#include #include "engine/EngineRuntime.h" @@ -49,6 +51,19 @@ class ConfigureObserveMarkerSystem : public safecrowd::engine::EngineSystem { } }; +class RecordPhaseSystem : public safecrowd::engine::EngineSystem { +public: + std::vector& log; + int marker; + + explicit RecordPhaseSystem(std::vector& l, int value) + : log(l), marker(value) {} + + void update(safecrowd::engine::EngineWorld&, const safecrowd::engine::EngineStepContext&) override { + log.push_back(marker); + } +}; + } // namespace SC_TEST(EngineRuntimePlayAndStepUpdatesStats) { @@ -117,6 +132,48 @@ SC_TEST(EngineRuntime_ConfigureCommands_AreVisibleToLaterSystems) { SC_EXPECT_EQ(runtime.world().query().view().size(), std::size_t{1}); } +SC_TEST(EngineRuntime_ExecutesStartupAndFramePhases) { + std::vector log; + + safecrowd::engine::EngineRuntime runtime({ + .fixedDeltaTime = 0.25, + .maxCatchUpSteps = 4, + .baseSeed = 1, + }); + + runtime.addSystem( + std::make_unique(log, 10), + {.phase = safecrowd::engine::UpdatePhase::Startup, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + runtime.addSystem( + std::make_unique(log, 20), + {.phase = safecrowd::engine::UpdatePhase::PreSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + runtime.addSystem( + std::make_unique(log, 30), + {.phase = safecrowd::engine::UpdatePhase::FixedSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::FixedStep}); + runtime.addSystem( + std::make_unique(log, 40), + {.phase = safecrowd::engine::UpdatePhase::PostSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + runtime.addSystem( + std::make_unique(log, 50), + {.phase = safecrowd::engine::UpdatePhase::RenderSync, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + + runtime.play(); + runtime.stepFrame(0.50); + + SC_EXPECT_EQ(log.size(), std::size_t{6}); + SC_EXPECT_EQ(log[0], 10); + SC_EXPECT_EQ(log[1], 20); + SC_EXPECT_EQ(log[2], 30); + SC_EXPECT_EQ(log[3], 30); + SC_EXPECT_EQ(log[4], 40); + SC_EXPECT_EQ(log[5], 50); +} + SC_TEST(EngineRuntime_Stop_ClearsWorldAndPendingCommandsBeforeNextRun) { safecrowd::engine::EngineRuntime runtime({ .fixedDeltaTime = 0.25, @@ -166,6 +223,23 @@ SC_TEST(EngineRuntime_PausedRuntime_DoesNotAdvanceSimulation) { SC_EXPECT_EQ(count, 2); } +SC_TEST(EngineRuntime_AddSystem_RejectsUnsupportedIntervalTriggerPolicy) { + int count = 0; + safecrowd::engine::EngineRuntime runtime; + + bool threw = false; + try { + runtime.addSystem( + std::make_unique(count), + {.phase = safecrowd::engine::UpdatePhase::PreSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval}); + } catch (const std::exception&) { + threw = true; + } + + SC_EXPECT_TRUE(threw); +} + SC_TEST(EngineRuntimePauseAndStopResetLifecycleState) { safecrowd::engine::EngineRuntime runtime({ .fixedDeltaTime = 0.25, diff --git a/tests/SystemSchedulerTests.cpp b/tests/SystemSchedulerTests.cpp new file mode 100644 index 0000000..a40d6a7 --- /dev/null +++ b/tests/SystemSchedulerTests.cpp @@ -0,0 +1,211 @@ +#include "TestSupport.h" + +#include +#include +#include +#include + +#include "engine/CommandBuffer.h" +#include "engine/EcsCore.h" +#include "engine/SystemScheduler.h" + +namespace { + +class RecordingSystem : public safecrowd::engine::EngineSystem { +public: + std::vector& log; + int id; + explicit RecordingSystem(std::vector& l, int id) : log(l), id(id) {} + void update(safecrowd::engine::EngineWorld&, + const safecrowd::engine::EngineStepContext&) override { + log.push_back(id); + } +}; + +struct Tag {}; + +class SpawnTagSystem : public safecrowd::engine::EngineSystem { +public: + void update(safecrowd::engine::EngineWorld& world, + const safecrowd::engine::EngineStepContext&) override { + world.commands().spawnEntity(Tag{}); + } +}; + +class ConfigureSpawnTagSystem : public safecrowd::engine::EngineSystem { +public: + void configure(safecrowd::engine::EngineWorld& world) override { + world.commands().spawnEntity(Tag{}); + } + + void update(safecrowd::engine::EngineWorld&, + const safecrowd::engine::EngineStepContext&) override { + } +}; + +class ConfigureObserveTagSystem : public safecrowd::engine::EngineSystem { +public: + std::size_t& count; + + explicit ConfigureObserveTagSystem(std::size_t& c) : count(c) {} + + void configure(safecrowd::engine::EngineWorld& world) override { + count = world.query().view().size(); + } + + void update(safecrowd::engine::EngineWorld&, + const safecrowd::engine::EngineStepContext&) override { + } +}; + +} // namespace + +SC_TEST(SystemScheduler_ExecutesSystemsInPhaseOrder) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + + safecrowd::engine::EcsCore dummyCore; + safecrowd::engine::CommandBuffer dummyBuffer; + safecrowd::engine::EngineWorld world{dummyCore, dummyBuffer}; + + std::vector log; + scheduler.registerSystem( + std::make_unique(log, 1), + {.phase = safecrowd::engine::UpdatePhase::PostSimulation, + .order = 0, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + scheduler.registerSystem( + std::make_unique(log, 2), + {.phase = safecrowd::engine::UpdatePhase::PreSimulation, + .order = 0, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + + 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); + + SC_EXPECT_EQ(log.size(), std::size_t{2}); + SC_EXPECT_EQ(log[0], 2); + SC_EXPECT_EQ(log[1], 1); +} + +SC_TEST(SystemScheduler_ExecutesSystemsInOrderWithinPhase) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + + safecrowd::engine::EcsCore dummyCore; + safecrowd::engine::CommandBuffer dummyBuffer; + safecrowd::engine::EngineWorld world{dummyCore, dummyBuffer}; + + std::vector log; + scheduler.registerSystem( + std::make_unique(log, 10), + {.phase = safecrowd::engine::UpdatePhase::FixedSimulation, .order = 1}); + scheduler.registerSystem( + std::make_unique(log, 20), + {.phase = safecrowd::engine::UpdatePhase::FixedSimulation, .order = 0}); + + const safecrowd::engine::EngineStepContext ctx{}; + scheduler.executePhase( + safecrowd::engine::UpdatePhase::FixedSimulation, + safecrowd::engine::TriggerPolicy::FixedStep, + world, + ctx); + + SC_EXPECT_EQ(log.size(), std::size_t{2}); + SC_EXPECT_EQ(log[0], 20); + SC_EXPECT_EQ(log[1], 10); +} + +SC_TEST(SystemScheduler_PhaseIsolation_OtherPhaseSystemsNotExecuted) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + + safecrowd::engine::EcsCore dummyCore; + safecrowd::engine::CommandBuffer dummyBuffer; + safecrowd::engine::EngineWorld world{dummyCore, dummyBuffer}; + + std::vector log; + scheduler.registerSystem( + std::make_unique(log, 1), + {.phase = safecrowd::engine::UpdatePhase::PostSimulation, + .order = 0, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + + const safecrowd::engine::EngineStepContext ctx{}; + scheduler.executePhase( + safecrowd::engine::UpdatePhase::FixedSimulation, + safecrowd::engine::TriggerPolicy::FixedStep, + world, + ctx); + + SC_EXPECT_EQ(log.size(), std::size_t{0}); +} + +SC_TEST(SystemScheduler_ConfigureFlushesCommandsBetweenSystems) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + safecrowd::engine::EngineWorld world{core, buffer}; + + std::size_t configuredCount = 0; + scheduler.registerSystem(std::make_unique(), {}); + scheduler.registerSystem(std::make_unique(configuredCount), {}); + + scheduler.configure(world); + + SC_EXPECT_EQ(configuredCount, std::size_t{1}); + SC_EXPECT_EQ(world.query().view().size(), std::size_t{1}); +} + +SC_TEST(SystemScheduler_FlushesCommandBufferAfterPhase) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + + safecrowd::engine::EngineWorld world{core, buffer}; + + scheduler.registerSystem( + std::make_unique(), + {.phase = safecrowd::engine::UpdatePhase::FixedSimulation, .order = 0}); + + const safecrowd::engine::EngineStepContext ctx{}; + scheduler.executePhase( + safecrowd::engine::UpdatePhase::FixedSimulation, + safecrowd::engine::TriggerPolicy::FixedStep, + world, + ctx); + + const auto entities = safecrowd::engine::WorldQuery{core}.view(); + SC_EXPECT_EQ(entities.size(), std::size_t{1}); +} + +SC_TEST(SystemScheduler_RegisterSystem_RejectsUnsupportedIntervalPolicy) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + + std::vector log; + bool threw = false; + try { + scheduler.registerSystem( + std::make_unique(log, 1), + {.phase = safecrowd::engine::UpdatePhase::PreSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval}); + } catch (const std::exception&) { + threw = true; + } + + SC_EXPECT_TRUE(threw); +}