Croutine
Croutines are RE:MAKE 2D's concurrency system, combining the simplicity of coroutines with the power of multithreading. They allow running tasks asynchronously without blocking the main loop.
Overview
Croutines are represented by the Croutine class, contained in the header "remake2d/croutine.hpp". Internally, they are scheduled by a thread
pool (CroutinePool) that manages three priority lanes:
- engine : reserved for internal engine tasks
- heavy : for CPU-intensive background work
- user : for standard game logic (default)
rmk::Croutine<> coro; // no arguments
rmk::Croutine<f32> coro; // one argument
rmk::Croutine<i32, f32> coro; // multiple arguments
Methods
template<typename F> void load(F&& function); // attach a coroutine function
void run(Args... args); // start the coroutine
void resume(void) noexcept; // resume after a pause
void stop(void) noexcept; // stop permanently
void wait(void) noexcept; // block caller until coroutine finishes
bool isRunning(void) noexcept; // check if currently running
u64 id(void) const noexcept; // get coroutine ID
u64 majorID(void) const noexcept; // get worker thread ID
void priority(croutine::priority p) noexcept; // set scheduling priority
void isEngine(bool v) noexcept; // mark as engine priority
void isHeavy(bool v) noexcept; // mark as heavy priority
auto operator co_await(void) noexcept; // await completion from another coroutine
Inside a coroutine function, three macros are available:
rmk_pause(); // yield execution back to the scheduler (resumes next frame)
rmk_await(coro); // suspend until another croutine finishes
rmk_lockedPause(lock); // yield while safely releasing a PauseGuard lock
Warning
A croutine function must return rmk::Task and use rmk_pause() to yield. A croutine that never yields will block its worker thread.
Lock
When sharing data between croutines or between a croutine and the main thread, standard mutexes are not safe to use across a rmk_pause() — they do not
release the lock during the yield, which causes a deadlock.
RE:MAKE 2D provides two coroutine-safe lock types in "remake2d/lock.hpp":
PauseGuard: wraps astd::mutexfor exclusive write accessSharedPause: wraps astd::shared_mutexfor shared read access
Both follow the RAII pattern — the lock is acquired on construction and released on destruction.
// PauseGuard
explicit PauseGuard(std::mutex& mtx); // acquire exclusive lock
void unlock(void) noexcept; // manually release
void relock(void) noexcept; // manually re-acquire
// SharedPause
explicit SharedPause(std::shared_mutex& mtx); // acquire shared lock
void unlock(void) noexcept; // manually release
void relock(void) noexcept; // manually re-acquire
Used with the macro:
Warning
Never use std::lock_guard or std::unique_lock inside a croutine. They will not release the mutex during rmk_pause(), causing a deadlock.
Info
Use PauseGuard when writing to shared data and SharedPause when only reading. Multiple SharedPause locks can coexist, but a PauseGuard will wait
for all shared locks to be released first.
Usage
Basic usage
rmk::Croutine<> coro;
coro.load([]() -> rmk::Task {
while (true) {
doBackgroundWork();
rmk_pause(); // yield every frame
}
});
coro.run();
With arguments
rmk::Croutine<f32> coro;
coro.load([](f32 duration) -> rmk::Task {
f32 elapsed = 0.0f;
while (elapsed < duration) {
elapsed += rmk::delta.tick();
rmk_pause();
}
std::cout << "Done after " << duration << "s\n";
});
coro.run(3.0f);
Awaiting another croutine
rmk::Croutine<> loader;
rmk::Croutine<> game;
loader.load([]() -> rmk::Task {
loadAssets();
rmk_pause();
});
game.load([&]() -> rmk::Task {
rmk_await(loader); // wait for loader to finish
startGame();
rmk_pause();
});
loader.run();
game.run();
Priority
rmk::Croutine<> heavy;
heavy.isHeavy(true); // runs on the heavy thread, won't compete with user croutines
heavy.load([]() -> rmk::Task {
while (true) {
computePathfinding();
rmk_pause();
}
});
heavy.run();
Thread-safe yield
Use PauseGuard when writing to shared data across a yield:
std::mutex mtx;
rmk::Croutine<> coro;
coro.load([&]() -> rmk::Task {
while (true) {
rmk::PauseGuard lock(mtx);
sharedData.update();
rmk_lockedPause(lock); // release, yield, re-acquire
}
});
coro.run();
Use SharedPause when multiple croutines only need to read shared data simultaneously:
std::shared_mutex mtx;
rmk::Croutine<> reader;
reader.load([&]() -> rmk::Task {
while (true) {
rmk::SharedPause lock(mtx);
processData(sharedData);
rmk_lockedPause(lock); // release, yield, re-acquire
}
});
reader.run();