Skip to content

Commit

Permalink
Add internal timer to life driver
Browse files Browse the repository at this point in the history
  • Loading branch information
dedztbh committed May 4, 2024
1 parent c45dad9 commit 94cf0e1
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ UseTab: Always
---
### C++ specific config ###
Language: Cpp
Standard: c++17
Standard: c++20
---
### ObjC specific config ###
Language: ObjC
Expand Down
39 changes: 18 additions & 21 deletions game/board.gd
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
extends GridContainer

signal next_iteration
signal speed_changed

@export var rows = 64
Expand All @@ -9,41 +8,39 @@ var init_matrix : Variant = null
var cells = []
var life_driver = LifeDriver.new()
var ruleset = Dictionary()
const colors = [Color.WHITE, Color.BLACK, Color.RED]
const COLORS = [Color.WHITE, Color.BLACK, Color.RED]
var interval_us : int = 100000

# Called when the node enters the scene tree for the first time.
func _ready():
for i in range(rows):
cells.append([])
for j in range(columns):
var new_cell = ColorRect.new()
new_cell.custom_minimum_size = Vector2(10, 10)
add_child(new_cell)
cells[-1].append(new_cell)
cells.append(new_cell)
if init_matrix != null:
_update_cell(i, j, init_matrix[i * columns + j])

life_driver.update_cell.connect(_update_cell)
life_driver.update_done.connect(_update_done)
next_iteration.connect(life_driver.next_iteration)
update_cell(i, j, init_matrix[i * columns + j])

life_driver.setup(rows, columns, init_matrix, LifeDriver.BASIC, ruleset)

emit_signal("speed_changed", 1 / $Timer.wait_time)
life_driver.auto_run_set_interval(interval_us)
life_driver.start_auto_run()
emit_signal("speed_changed", 1000000.0 / interval_us)

func _process(_delta):
life_driver.consume_updates(update_done)


func _update_cell(i: int, j: int, state: int):
cells[i][j].color = colors[state]

func _update_done():
pass
func update_done(color_updates : Dictionary):
for ij : Vector2i in color_updates:
update_cell(ij.x, ij.y, color_updates[ij])


func _on_timer_timeout():
emit_signal("next_iteration")
func update_cell(i: int, j: int, state: int):
cells[i * columns + j].color = COLORS[state]


func change_speed(ratio):
$Timer.wait_time /= ratio
$Timer.start()
emit_signal("speed_changed", 1 / $Timer.wait_time)
interval_us = clampi(interval_us / ratio, 1, 1000000)
life_driver.auto_run_set_interval(interval_us)
emit_signal("speed_changed", 1000000.0 / interval_us)
6 changes: 0 additions & 6 deletions game/board.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,3 @@ theme_override_constants/h_separation = 1
theme_override_constants/v_separation = 1
columns = 64
script = ExtResource("1_o7lpq")

[node name="Timer" type="Timer" parent="."]
wait_time = 0.1
autostart = true

[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]
20 changes: 10 additions & 10 deletions src/basic_engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@
namespace lifepvp::engine {

template <class T>
concept BoardConstructibleBySize = requires(T && board) {
concept BoardConstructibleBySize = requires(T &&board) {
T(board.size());
};

template <class T>
concept BoardResizable = std::is_default_constructible_v<T> &&
requires(T board, size_t x) {
board.resize(x);
};
board.resize(x);
};

template <class T>
concept BasicEngineContainer =
std::move_constructible<T> &&
(BoardResizable<T> || BoardConstructibleBySize<T>)&&requires(T & board, const T &cboard, size_t i) {
{ board[i] } -> std::convertible_to<EngineBase::state_t &>;
{ cboard[i] } -> std::convertible_to<EngineBase::state_t const &>;
};
(BoardResizable<T> || BoardConstructibleBySize<T>)&&requires(T &board, const T &cboard, size_t i) {
{ board[i] } -> std::convertible_to<EngineBase::state_t &>;
{ cboard[i] } -> std::convertible_to<EngineBase::state_t const &>;
};

struct BasicEngineRuleset {
bool wrap_around = false;
Expand All @@ -37,9 +37,9 @@ class BasicEngine : public EngineBase {
using board_t = std::remove_reference_t<T>;

BasicEngine(board_t &&board,
const size_t w,
const size_t h,
const update_cb_t update_cb) :
size_t w,
size_t h,
update_cb_t update_cb) :
EngineBase(w, h, update_cb), m_board_pair([&]() {
if constexpr (BoardConstructibleBySize<board_t>) {
return decltype(m_board_pair){ std::move(board), board_t(board.size()) };
Expand Down
6 changes: 3 additions & 3 deletions src/engine_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ class EngineBase {
using state_t = uint8_t;
using update_cb_t = std::function<void(size_t, size_t, uint8_t)>;

EngineBase(const size_t w,
const size_t h,
const update_cb_t update_cb) :
EngineBase(size_t w,
size_t h,
update_cb_t update_cb) :
W(w), H(h), m_update_cb(update_cb) {}

virtual ~EngineBase() = default;
Expand Down
51 changes: 40 additions & 11 deletions src/life_driver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,31 @@ using namespace lifepvp::engine;
// setup_engine_with_ruleset(tuple{bools...}, f) is same as f([]() { return Ruleset{bools...}; })
// Except bools need not be constexpr for Ruleset{bools...} to be constexpr.
template <class Ruleset, size_t I, bool... bools>
auto setup_engine_with_ruleset(const auto &tup, const auto f) requires(I == 0) {
auto setup_engine_with_ruleset(auto &tup, auto f)
requires(I == 0)
{
return f([]() { return Ruleset{ bools... }; });
}

template <class Ruleset, size_t I, bool... bools>
auto setup_engine_with_ruleset(const auto &tup, const auto f) requires(0 < I && I <= std::tuple_size_v<std::remove_cvref_t<decltype(tup)>>) {
auto setup_engine_with_ruleset(auto &tup, auto f)
requires(0 < I && I <= std::tuple_size_v<std::remove_cvref_t<decltype(tup)>>)
{
if (std::get<I - 1>(tup)) {
return setup_engine_with_ruleset<Ruleset, I - 1, bools..., true>(tup, f);
} else {
return setup_engine_with_ruleset<Ruleset, I - 1, bools..., false>(tup, f);
}
}

void LifeDriver::setup(const size_t w, const size_t h, const Variant &init_board, const EngineType engine, const Variant &ruleset) {
const auto update_cell_cb = [&](size_t i, size_t j, uint8_t state) { emit_signal("update_cell", i, j, state); };
void LifeDriver::setup(size_t w, size_t h, const Variant &init_board, EngineType engine, const Variant &ruleset) {
auto update_cell_cb = [&](size_t i, size_t j, uint8_t state) {
m_iteration_updates[Vector2i(i, j)] = state;
};

if (engine == BASIC) {
// workaround for not being able to pass constexpr Ruleset directly
const auto make_engine = [&](const auto get_ruleset) {
auto make_engine = [&](auto get_ruleset) {
if (init_board.get_type() == Variant::NIL) {
m_engine = std::make_unique<BasicEngine<get_ruleset()>>(std::vector<EngineBase::state_t>(w * h), w, h, update_cell_cb);
} else if (init_board.get_type() == Variant::PACKED_BYTE_ARRAY) {
Expand All @@ -47,7 +53,7 @@ void LifeDriver::setup(const size_t w, const size_t h, const Variant &init_board
make_engine([]() { return BasicEngineRuleset{}; });
} else if (ruleset.get_type() == Variant::DICTIONARY) {
const Dictionary &dict = ruleset;
const auto tup = std::make_tuple<bool>(dict.get(String("wrap_around"), false));
auto tup = std::make_tuple<bool>(dict.get(String("wrap_around"), false));
setup_engine_with_ruleset<BasicEngineRuleset, std::tuple_size_v<decltype(tup)>>(tup, make_engine);
} else {
ERR_PRINT("Unknown ruleset type, should be either null or Dictionary");
Expand All @@ -61,17 +67,40 @@ void LifeDriver::setup(const size_t w, const size_t h, const Variant &init_board
void LifeDriver::next_iteration() {
if (m_engine && !is_busy.exchange(true)) {
m_engine->next_iteration();
emit_signal("update_done");
{
std::lock_guard lock(m_updates_mutex);
m_updates.merge(m_iteration_updates, true);
m_iteration_updates.clear();
}
is_busy = false;
}
}

void LifeDriver::_bind_methods() {
ADD_SIGNAL(MethodInfo("update_cell", PropertyInfo(Variant::INT, "i"), PropertyInfo(Variant::INT, "j"), PropertyInfo(Variant::INT, "state")));
ADD_SIGNAL(MethodInfo("update_done"));
void LifeDriver::start_auto_run() {
m_timer->start();
}

void LifeDriver::stop_auto_run() {
m_timer->stop();
}

ClassDB::bind_method(D_METHOD("setup", "w", "h", "init_board", "engine_type"), &LifeDriver::setup);
void LifeDriver::auto_run_set_interval(uint64_t interval_us) {
m_timer->set_interval(std::chrono::microseconds(interval_us));
}

void LifeDriver::consume_updates(const Callable &cb) {
std::lock_guard lock(m_updates_mutex);
cb.call(m_updates);
m_updates.clear();
}

void LifeDriver::_bind_methods() {
ClassDB::bind_method(D_METHOD("setup", "w", "h", "init_board", "engine_type", "ruleset"), &LifeDriver::setup);
ClassDB::bind_method(D_METHOD("next_iteration"), &LifeDriver::next_iteration);
ClassDB::bind_method(D_METHOD("start_auto_run"), &LifeDriver::start_auto_run);
ClassDB::bind_method(D_METHOD("stop_auto_run"), &LifeDriver::stop_auto_run);
ClassDB::bind_method(D_METHOD("auto_run_set_interval", "interval_us"), &LifeDriver::auto_run_set_interval);
ClassDB::bind_method(D_METHOD("consume_updates"), &LifeDriver::consume_updates);

BIND_ENUM_CONSTANT(BASIC);
}
20 changes: 19 additions & 1 deletion src/life_driver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,44 @@

#include "engine_base.hpp"

#include "util/recurring_timer.hpp"

namespace godot {

class LifeDriver : public RefCounted {
GDCLASS(LifeDriver, RefCounted)

public:
LifeDriver() {
m_timer = std::make_unique<RecurringTimer<>>([&]() { next_iteration(); });
}

enum EngineType {
BASIC
};

void setup(const size_t w, const size_t h, const Variant &init_board, const EngineType engine, const Variant &ruleset);
void setup(size_t w, size_t h, const Variant &init_board, EngineType engine, const Variant &ruleset);

void next_iteration();

void start_auto_run();

void stop_auto_run();

void auto_run_set_interval(uint64_t interval_ms);

void consume_updates(const Callable &cb);

protected:
static void _bind_methods();

private:
std::unique_ptr<lifepvp::engine::EngineBase> m_engine;
std::atomic_bool is_busy = false;
std::unique_ptr<RecurringTimer<>> m_timer;
Dictionary m_updates;
Dictionary m_iteration_updates;
std::mutex m_updates_mutex;
};

} //namespace godot
Expand Down
74 changes: 74 additions & 0 deletions src/util/recurring_timer.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <thread>

template <class Clock = std::chrono::steady_clock>
requires(std::chrono::is_clock_v<Clock>)
class RecurringTimer {
public:
RecurringTimer(
std::function<void()> task,
Clock::duration interval = std::chrono::seconds(1),
bool auto_start = false) :
task(task),
interval(interval) {
if (auto_start) {
start();
}
}

~RecurringTimer() {
stop();
}

void start() {
stop();
thread = std::jthread([&](auto stoken) { run(stoken); });
}

void stop() {
thread.request_stop();
if (mutex.try_lock()) {
// cv waiting, notify it
cv.notify_one();
mutex.unlock();
}
// otherwise let task finish
}

void set_interval(Clock::duration new_interval) {
stop();
interval = new_interval;
next_run_time = Clock::now();
start();
}

private:
void run(std::stop_token stoken) {
std::unique_lock<std::mutex> lock(mutex);

while (!stoken.stop_requested()) {
next_run_time = std::max(next_run_time + interval, Clock::now());

cv.wait_until(lock, next_run_time, [&]() { return Clock::now() >= next_run_time || stoken.stop_requested(); });

if (stoken.stop_requested()) {
return;
}

task();
}
}

const std::function<void()> task;
std::jthread thread;
std::mutex mutex;
std::condition_variable cv;

// Should not need atomic since they are protected by mutex
Clock::duration interval;
Clock::time_point next_run_time;
};

0 comments on commit 94cf0e1

Please sign in to comment.