Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce system/phase timers #186

Merged
merged 1 commit into from
Nov 22, 2024
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
65 changes: 65 additions & 0 deletions doc/Tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,71 @@ I strongly recommend you to use enum types, or const values/macros.
registry.runSystemsPhase<Pipeline::PredefinedPhases::OnUpdate>();
```

### Timers

By default, phases and systems are runned at each frame (ie each `registry.run()` call).
However you may want to limit them to run every two frames, or every 5 seconds.
This is possible through the @ref ecstasy::Timer "Timer" class.

Every @ref ecstasy::ISystem "ISystem" and @ref ecstasy::Pipeline::Phase "Phase" have a timer instance accessible through `getTimer()` getter.

#### Interval

Want to limit your system to run at a specific time ? Use @ref ecstasy::Timer::setInterval "setInterval()" function:

@warning
Setting an interval means it will always wait **at least** the required interval between two systems calls but it can be longer.

```cpp
ecstasy::Registry registry;

registry.addSystem<MySystem>().getTimer().setInterval(std::chrono::milliseconds(500));
// Thanks to std::chrono, you can easily use other time units
registry.addSystem<MySystem>().getTimer().setInterval(std::chrono::seconds(5));

// Set render to every 16ms -> ~60Hz
registry.getPipeline().getPhase(Pipeline::PredefinedPhases::OnStore).getTimer().setInterval(std::chrono::milliseconds(16));
```

#### Rate

Want to limit your system to run at a frame frequency instead ? Use @ref ecstasy::Timer::setRate "setRate()" function:

```cpp
ecstasy::Registry registry;

// Will run every two frames (ie run one, skip one)
registry.addSystem<MySystem>().getTimer().setRate(2);

// Will render every 5 frames
registry.getPipeline().getPhase(Pipeline::PredefinedPhases::OnStore).getTimer().setRate(5);
```

#### Tips when using timers on Systems and Phases

The one sentence to remember about combining system and phases timers is this one:
**The system timer is only evaluated if the phase timer succeed.**

Below are some resulting behaviors.

1. System and Phase rates

Combined rates are multiplied. Inverting the values will have the same behaviors.
Ex: Rate limit of 5 on the system and 2 on the phase will result in a system rate of 10 frames.

2. Phase interval and system rate

The final interval is the phase interval multiplied by the system rate.
Ex: Interval of 5s with rate of 3 will result in a system interval of 15s.

3. Phase rate (R) and system interval (I)

The system will be called at frames when the frame id is a multiple of the `R` **and** at least `I` seconds elapsed since last system call.

4. Phase and system intervals

The longest interval need to be satisfied. They are not added.

## Using resources

Creating a resource is even simpler than creating a system: you only have to inherit @ref ecstasy::IResource "IResource".
Expand Down
5 changes: 4 additions & 1 deletion src/ecstasy/registry/Registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ namespace ecstasy

void Registry::runSystem(const std::type_index &systemId)
{
_systems.get(systemId).run(*this);
ISystem &system = _systems.get(systemId);

if (system.getTimer().trigger())
system.run(*this);
}

void Registry::runSystems()
Expand Down
2 changes: 2 additions & 0 deletions src/ecstasy/system/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ set(SRC
${INCROOT}/Pipeline.hpp
${SRCROOT}/Pipeline.cpp
${INCROOT}/ISystem.hpp
${INCROOT}/Timer.hpp
${SRCROOT}/Timer.cpp
PARENT_SCOPE
)
38 changes: 38 additions & 0 deletions src/ecstasy/system/ISystem.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#ifndef ECSTASY_SYSTEM_ISYSTEM_HPP_
#define ECSTASY_SYSTEM_ISYSTEM_HPP_

#include "ecstasy/system/Timer.hpp"

namespace ecstasy
{
/// @brief Forward declaration of Registry class.
Expand All @@ -37,6 +39,42 @@ namespace ecstasy
/// @since 1.0.0 (2022-10-17)
///
virtual void run(Registry &registry) = 0;

///
/// @brief Get the system timer.
///
/// @note The timer is used to control the execution of the system. You can also use
/// @ref ecstasy::Pipeline::Phase "Phase" scale timers.
///
/// @return Timer& Reference to the system timer.
///
/// @author Andréas Leroux ([email protected])
/// @since 1.0.0 (2024-11-22)
///
[[nodiscard]] constexpr Timer &getTimer() noexcept
{
return _timer;
}

///
/// @brief Get the system timer.
///
/// @note The timer is used to control the execution of the system. You can also use
/// @ref ecstasy::Pipeline::Phase "Phase" scale timers.
///
/// @return const Timer& Reference to the system timer.
///
/// @author Andréas Leroux ([email protected])
/// @since 1.0.0 (2024-11-22)
///
[[nodiscard]] constexpr const Timer &getTimer() const noexcept
{
return _timer;
}

private:
/// @brief Timer to control the execution of the system.
Timer _timer;
};
} // namespace ecstasy

Expand Down
8 changes: 5 additions & 3 deletions src/ecstasy/system/Pipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ namespace ecstasy
return _pipeline._systemsIds.begin() + static_cast<long>(end_idx());
}

void Pipeline::Phase::run() const
void Pipeline::Phase::run()
{
if (!_timer.trigger())
return;
for (auto it = begin(); it < end(); ++it) {
_pipeline._registry.runSystem(*it);
}
Expand Down Expand Up @@ -54,14 +56,14 @@ namespace ecstasy
}
}

void Pipeline::run() const
void Pipeline::run()
{
for (auto &phase : _phases) {
phase.second.run();
}
}

void Pipeline::run(Pipeline::PhaseId phase) const
void Pipeline::run(Pipeline::PhaseId phase)
{
auto phaseIt = _phases.find(phase);

Expand Down
49 changes: 45 additions & 4 deletions src/ecstasy/system/Pipeline.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <typeindex>
#include <vector>
#include "ecstasy/system/ISystem.hpp"
#include "ecstasy/system/Timer.hpp"

namespace ecstasy
{
Expand Down Expand Up @@ -72,7 +73,7 @@ namespace ecstasy
/// @author Andréas Leroux ([email protected])
/// @since 1.0.0 (2024-11-21)
///
constexpr Phase(Pipeline &pipeline, PhaseId id) : _pipeline(pipeline), _begin(), _size(0), _id(id)
Phase(Pipeline &pipeline, PhaseId id) : _pipeline(pipeline), _begin(), _size(0), _id(id)
{
}

Expand All @@ -83,7 +84,7 @@ namespace ecstasy
/// @author Andréas Leroux ([email protected])
/// @since 1.0.0 (2024-11-21)
///
void run() const;
void run();

///
/// @brief Get the number of systems in this phase.
Expand Down Expand Up @@ -132,6 +133,44 @@ namespace ecstasy
///
[[nodiscard]] SystemIterator end() const noexcept;

///
/// @brief Get the phase timer.
///
/// @note The timer is used to control the execution of the phase. You can also use
/// @ref ecstasy::ISystem "ISystem" scale timers.
///
/// @warning Rate limiting both the phase and the systems will result in multiplied timers (not added).
/// However interval timers will be cumulative.
///
/// @return Timer& Reference to the system timer.
///
/// @author Andréas Leroux ([email protected])
/// @since 1.0.0 (2024-11-22)
///
[[nodiscard]] constexpr Timer &getTimer() noexcept
{
return _timer;
}

///
/// @brief Get the phase timer.
///
/// @note The timer is used to control the execution of the phase. You can also use
/// @ref ecstasy::ISystem "ISystem" scale timers.
///
/// @warning Rate limiting both the phase and the systems will result in multiplied timers (not added).
/// However interval timers will be cumulative.
///
/// @return const Timer& Reference to the system timer.
///
/// @author Andréas Leroux ([email protected])
/// @since 1.0.0 (2024-11-22)
///
[[nodiscard]] constexpr const Timer &getTimer() const noexcept
{
return _timer;
}

private:
/// @brief Owning pipeline.
Pipeline &_pipeline;
Expand All @@ -141,6 +180,8 @@ namespace ecstasy
std::size_t _size;
/// @brief Identifier of the phase.
PhaseId _id;
/// @brief Timer to control the execution of the phase.
Timer _timer;

///
/// @brief Get the index of the first system in this phase.
Expand Down Expand Up @@ -266,7 +307,7 @@ namespace ecstasy
/// @author Andréas Leroux ([email protected])
/// @since 1.0.0 (2024-11-21)
///
void run() const;
void run();

///
/// @brief Run a specific phase of the pipeline.
Expand All @@ -276,7 +317,7 @@ namespace ecstasy
/// @author Andréas Leroux ([email protected])
/// @since 1.0.0 (2024-11-21)
///
void run(PhaseId phase) const;
void run(PhaseId phase);

private:
/// @brief Ordered list of systems.
Expand Down
88 changes: 88 additions & 0 deletions src/ecstasy/system/Timer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
///
/// @file Timer.cpp
/// @author Andréas Leroux ([email protected])
/// @brief
/// @version 1.0.0
/// @date 2024-11-21
///
/// @copyright Copyright (c) ECSTASY 2022 - 2024
///
///

#include "Timer.hpp"
#include <stdexcept>

namespace ecstasy
{
Timer::Timer()
{
_lastTrigger = TimePoint::min();
setRate(0);
}

Timer::Timer(Timer::Interval interval) : Timer()
{
setInterval(interval);
}

Timer::Timer(std::uint32_t rate) : Timer()
{
setRate(rate);
}

void Timer::setRate(std::uint32_t rate) noexcept
{
_type = Type::Rate;
if (rate == 0) {
rate = 1;
}
_rate.rate = rate;
// Reset the countdown to trigger to run the first time
_rate.triggerCountdown = 0;
}

std::uint32_t Timer::getRate() const
{
if (_type != Type::Rate)
throw std::runtime_error("Timer is not of type Rate");
return _rate.rate;
}

void Timer::setInterval(Timer::Interval interval) noexcept
{
_type = Type::TimeInterval;
_timer.interval = interval;
}

Timer::Interval Timer::getInterval() const
{
if (_type != Type::TimeInterval)
throw std::runtime_error("Timer is not of type Rate");
return _timer.interval;
}

bool Timer::trigger() noexcept
{
switch (_type) {
case Type::TimeInterval: {
auto tp = std::chrono::system_clock::now();

if (tp - _timer.interval >= _lastTrigger) {
_lastTrigger = tp;
return true;
}
return false;
}
case Type::Rate: {
if (_rate.triggerCountdown == 0) {
_rate.triggerCountdown = _rate.rate - 1;
_lastTrigger = std::chrono::system_clock::now();
return true;
}
--_rate.triggerCountdown;
return false;
}
}
return false;
}
} // namespace ecstasy
Loading
Loading