Add project files
This commit is contained in:
commit
c92f6a1865
15 files changed
+1672
No files matched your search
@@ -0,0 +1,132 @@
|
||||
#pragma once
|
||||
|
||||
// ============================================================================
|
||||
// Coroutine.hpp — the coroutine return type (the "Task") and its
|
||||
// promise_type.
|
||||
//
|
||||
// Nothing here needs to change for arguments or member coroutines: in C++20
|
||||
// the promise_type used for a coroutine is selected purely from its RETURN
|
||||
// TYPE (Coroutine), never from whether the function is free, static, or a
|
||||
// non-static member, and never from its parameter list. So:
|
||||
//
|
||||
// Coroutine foo(int a, float b); // args: just works
|
||||
// Coroutine Enemy::attackSequence(int n); // member: just works
|
||||
//
|
||||
// The one thing this library genuinely cannot do for you: a non-static
|
||||
// member coroutine captures `this` in its coroutine frame like any other
|
||||
// closure. If the owning object is destroyed while the coroutine is still
|
||||
// suspended, resuming it is a use-after-free. Stop a member coroutine
|
||||
// (CoroutineRunner::stopCoroutine) in the owner's destructor if there's any
|
||||
// chance it's still running.
|
||||
//
|
||||
// What a coroutine can co_yield is intentionally small and closed here:
|
||||
// - nullptr -- wait one frame
|
||||
// - a CoroutineHandle -- wait for another coroutine to finish
|
||||
// - a child Coroutine -- start it and wait for it to finish (defined in
|
||||
// CoroutineRunner.hpp; needs CoroutineRunner)
|
||||
// - anything invocable as bool() -- wait until it returns true
|
||||
//
|
||||
// That last case is deliberately generic rather than a fixed enum of "kinds
|
||||
// of waiting." waitForSeconds()/waitUntil()/waitWhile()/waitFor() (see
|
||||
// coroutines/standard/WaitConditions.h) are just ordinary functions that
|
||||
// return small callable objects satisfying this same bool() contract --
|
||||
// this header has no idea they exist, and doesn't need to. Anyone is free
|
||||
// to write their own wait conditions the same way, without touching this
|
||||
// file or CoroutineRunner.hpp.
|
||||
// ============================================================================
|
||||
|
||||
#include <coroutine>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include <acrux/coroutines/CoroutineHandle.h>
|
||||
#include <acrux/coroutines/Predicate.h>
|
||||
|
||||
namespace acrux::coroutines
|
||||
{
|
||||
struct Coroutine
|
||||
{
|
||||
struct promise_type
|
||||
{
|
||||
// Note: get_return_object, initial_suspend, final_suspend,
|
||||
// unhandled_exception, return_void, and yield_value are compiler
|
||||
// hook names mandated by the coroutine machinery and cannot be
|
||||
// renamed to match the library's camelCase convention.
|
||||
|
||||
enum class WaitKind : uint8_t
|
||||
{
|
||||
NextFrame,
|
||||
Condition,
|
||||
NestedHandle
|
||||
};
|
||||
|
||||
WaitKind m_waitKind = WaitKind::NextFrame;
|
||||
CoroutineHandle m_nestedHandle = INVALID_COROUTINE_HANDLE;
|
||||
Predicate m_condition;
|
||||
CoroutineHandle m_assignedId = INVALID_COROUTINE_HANDLE;
|
||||
|
||||
Coroutine get_return_object()
|
||||
{
|
||||
return Coroutine{ std::coroutine_handle<promise_type>::from_promise(*this) };
|
||||
}
|
||||
|
||||
std::suspend_always initial_suspend() noexcept { return {}; }
|
||||
std::suspend_always final_suspend() noexcept { return {}; }
|
||||
void unhandled_exception() { std::terminate(); }
|
||||
void return_void() noexcept {}
|
||||
|
||||
// co_yield nullptr -- wait exactly one frame.
|
||||
std::suspend_always yield_value(std::nullptr_t) noexcept
|
||||
{
|
||||
m_waitKind = WaitKind::NextFrame;
|
||||
return {};
|
||||
}
|
||||
|
||||
// co_yield someHandle -- wait for another coroutine to finish.
|
||||
std::suspend_always yield_value(CoroutineHandle handle) noexcept
|
||||
{
|
||||
m_waitKind = WaitKind::NestedHandle;
|
||||
m_nestedHandle = handle;
|
||||
return {};
|
||||
}
|
||||
|
||||
// co_yield myObject.myFunc(...) -- defined in CoroutineRunner.hpp,
|
||||
// see the file-level comment above for why.
|
||||
std::suspend_always yield_value(Coroutine&& childTask);
|
||||
|
||||
// co_yield <anything invocable as bool()> -- wait until it
|
||||
// returns true. This is the extension point every wait-condition
|
||||
// helper (waitForSeconds, waitUntil, waitFor, ...) is built on;
|
||||
// see coroutines/standard/WaitConditions.h for the reference
|
||||
// implementations, or write your own the same way.
|
||||
template <typename F, typename = std::enable_if_t<std::is_invocable_r_v<bool, const F&>>>
|
||||
std::suspend_always yield_value(F condition)
|
||||
{
|
||||
m_waitKind = WaitKind::Condition;
|
||||
m_condition = Predicate(std::move(condition));
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
std::coroutine_handle<promise_type> m_handle;
|
||||
|
||||
Coroutine() noexcept : m_handle(nullptr) {}
|
||||
explicit Coroutine(std::coroutine_handle<promise_type> h) noexcept : m_handle(h) {}
|
||||
~Coroutine() { if (m_handle) m_handle.destroy(); }
|
||||
|
||||
Coroutine(const Coroutine&) = delete;
|
||||
Coroutine& operator=(const Coroutine&) = delete;
|
||||
|
||||
Coroutine(Coroutine&& other) noexcept : m_handle(other.m_handle) { other.m_handle = nullptr; }
|
||||
Coroutine& operator=(Coroutine&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
if (m_handle) m_handle.destroy();
|
||||
m_handle = other.m_handle;
|
||||
other.m_handle = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#pragma once
|
||||
|
||||
// ============================================================================
|
||||
// CoroutineHandle.h — an opaque, cheap-to-copy VALUE TYPE identifying a
|
||||
// coroutine registered with a CoroutineRunner.
|
||||
//
|
||||
// Internally it's still just a packed 32-bit slot index + 32-bit generation
|
||||
// counter (so CoroutineRunner can find/validate it in O(1), no ABA bugs),
|
||||
// but it's wrapped in a real type instead of a bare uint64_t so that:
|
||||
//
|
||||
// - you can call handle.isRunning() / handle.stop() directly on it,
|
||||
// instead of always having to go through CoroutineRunner or a free
|
||||
// function that takes it by reference;
|
||||
// - handle.stop() always correctly invalidates the handle it was called
|
||||
// on — there's no reference to forget to take, and no way to
|
||||
// accidentally stop a copy and leave the original looking "still
|
||||
// running";
|
||||
// - `if (handle)` reads naturally (explicit operator bool: valid <=> not
|
||||
// INVALID_COROUTINE_HANDLE);
|
||||
// - it can't accidentally be compared against / mixed up with an
|
||||
// unrelated uint64_t (e.g. a frame counter or an entity id).
|
||||
//
|
||||
// It's still a small trivially-copyable value (one uint64_t) — pass it
|
||||
// around, store it, return it by value freely.
|
||||
// ============================================================================
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
namespace acrux::coroutines
|
||||
{
|
||||
class CoroutineRunner; // needs friend access to construct/decode handles
|
||||
|
||||
class CoroutineHandle
|
||||
{
|
||||
public:
|
||||
CoroutineHandle() noexcept = default;
|
||||
|
||||
// Explicit so a handle can't silently decay into an integer/bool in
|
||||
// unrelated contexts — only `if (handle)` / `while (!handle)` etc.
|
||||
explicit operator bool() const noexcept { return m_id != kInvalidId; }
|
||||
|
||||
friend bool operator==(const CoroutineHandle& a, const CoroutineHandle& b) noexcept { return a.m_id == b.m_id; }
|
||||
friend bool operator!=(const CoroutineHandle& a, const CoroutineHandle& b) noexcept { return !(a == b); }
|
||||
|
||||
// Equivalent to CoroutineRunner::isCoroutineRunning(*this).
|
||||
// Declared here, defined in CoroutineRunner.h once CoroutineRunner
|
||||
// is a complete type.
|
||||
bool isRunning() const;
|
||||
|
||||
// Equivalent to CoroutineRunner::stopCoroutine(*this) — stops the
|
||||
// coroutine AND resets *this to INVALID_COROUTINE_HANDLE. Declared
|
||||
// here, defined in CoroutineRunner.h.
|
||||
void stop();
|
||||
|
||||
// The handle's raw packed id (index + generation), exposed so a
|
||||
// CoroutineHandle can be used as a key in a hash map/set -- e.g. by a
|
||||
// CoroutineRunner subclass tracking its own per-coroutine data (see
|
||||
// CoroutineRunner.h). Not meant to be decoded; treat it as opaque.
|
||||
uint64_t id() const noexcept { return m_id; }
|
||||
|
||||
private:
|
||||
friend class CoroutineRunner;
|
||||
|
||||
static constexpr uint64_t kInvalidId = 0;
|
||||
|
||||
explicit CoroutineHandle(uint64_t id) noexcept : m_id(id) {}
|
||||
|
||||
uint64_t raw() const noexcept { return m_id; }
|
||||
|
||||
uint64_t m_id = kInvalidId;
|
||||
};
|
||||
|
||||
inline constexpr CoroutineHandle INVALID_COROUTINE_HANDLE{};
|
||||
}
|
||||
|
||||
// Implementation detail: only CoroutineRunner needs these, and only to
|
||||
// build/decode the raw id packed inside a CoroutineHandle. Kept out of
|
||||
// acrux::coroutines so it can't leak into the public API by accident.
|
||||
namespace detail::acrux::coroutines
|
||||
{
|
||||
constexpr uint32_t IndexBits = 32;
|
||||
|
||||
// index+1 is stored so that (index == 0, generation == 0) never collides
|
||||
// with an all-zero (invalid) id.
|
||||
inline uint64_t packHandle(uint32_t index, uint32_t generation) noexcept
|
||||
{
|
||||
return (static_cast<uint64_t>(generation) << IndexBits) |
|
||||
(static_cast<uint64_t>(index) + 1u);
|
||||
}
|
||||
|
||||
inline uint32_t unpackIndex(uint64_t raw) noexcept
|
||||
{
|
||||
return static_cast<uint32_t>(raw & 0xFFFFFFFFull) - 1u;
|
||||
}
|
||||
|
||||
inline uint32_t unpackGeneration(uint64_t raw) noexcept
|
||||
{
|
||||
return static_cast<uint32_t>(raw >> IndexBits);
|
||||
}
|
||||
}
|
||||
|
||||
// Lets CoroutineHandle be used directly as a key in std::unordered_map /
|
||||
// std::unordered_set -- intended for a CoroutineRunner subclass that keeps
|
||||
// its own side table of per-coroutine data (see CoroutineRunner.h).
|
||||
template <>
|
||||
struct std::hash<acrux::coroutines::CoroutineHandle>
|
||||
{
|
||||
size_t operator()(const acrux::coroutines::CoroutineHandle& handle) const noexcept
|
||||
{
|
||||
return std::hash<uint64_t>{}(handle.id());
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,399 @@
|
||||
#pragma once
|
||||
|
||||
// ============================================================================
|
||||
// CoroutineRunner.hpp — the engine: a generational slot map of running
|
||||
// coroutines.
|
||||
//
|
||||
// - CoroutineHandle packs a 32-bit slot index + a 32-bit generation.
|
||||
// - startCoroutine / stopCoroutine / isCoroutineRunning are O(1) in the
|
||||
// common case (index straight into a vector, generation check to reject
|
||||
// stale/reused handles — no ABA bugs).
|
||||
// - Freed slots go on a free list and get reused before growing.
|
||||
// - update() reuses a persistent scratch buffer for finished slots instead
|
||||
// of allocating one every frame.
|
||||
// - getActiveCount() is tracked incrementally (O(1)) instead of
|
||||
// re-counting.
|
||||
//
|
||||
// Growing the backing store (i.e. adding a coroutine when there is no free
|
||||
// slot to reuse) has to be deferred to the start of the next update(),
|
||||
// because update() holds a reference into the slot array while it resumes
|
||||
// coroutines, and those coroutines are allowed to start new ones. A
|
||||
// std::vector resize while that reference is alive would dangle it. Reusing
|
||||
// an already-existing free slot has no such restriction (no reallocation),
|
||||
// so that path is handled immediately instead of staged.
|
||||
//
|
||||
// EXTENDING THIS: CoroutineRunner is not `final` specifically so it can be
|
||||
// derived from. A subclass can add its own overload(s) of startCoroutine
|
||||
// that accept extra data -- an EntityHandle in a game engine, or whatever
|
||||
// your program needs -- and track that data itself, keyed by CoroutineHandle
|
||||
// (which is hashable; see CoroutineHandle::id()/std::hash specialization in
|
||||
// CoroutineHandle.h). onCoroutineStarted(handle, parent)/onCoroutineFinished(handle)
|
||||
// are called at the right points to keep that side table in sync
|
||||
// automatically -- `parent` lets a subclass propagate its data from a
|
||||
// coroutine to any child it starts (via `co_yield childTask()` or a direct
|
||||
// startCoroutine() call from within its body). This base class has no
|
||||
// opinion on what "extra data" means -- that's entirely up to the derived
|
||||
// class. See coroutines/example.md for a worked example.
|
||||
// ============================================================================
|
||||
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cassert>
|
||||
|
||||
#include <acrux/coroutines/CoroutineHandle.h>
|
||||
#include <acrux/coroutines/Coroutine.h>
|
||||
|
||||
namespace acrux::coroutines
|
||||
{
|
||||
class CoroutineRunner
|
||||
{
|
||||
private:
|
||||
struct Slot
|
||||
{
|
||||
Coroutine m_task;
|
||||
uint32_t m_generation = 0;
|
||||
bool m_alive = false;
|
||||
};
|
||||
|
||||
std::vector<Slot> m_slots;
|
||||
std::vector<uint32_t> m_freeIndices;
|
||||
|
||||
struct PendingTask
|
||||
{
|
||||
uint32_t m_index;
|
||||
Coroutine m_task;
|
||||
CoroutineHandle m_parent; // whichever coroutine was resuming when this was started, or invalid
|
||||
};
|
||||
|
||||
// Tasks that need a brand-new slot (no free slot to reuse) get staged
|
||||
// here and only actually inserted into `m_slots` at the top of update(),
|
||||
// because that's the only point at which growing `m_slots` is safe.
|
||||
std::vector<PendingTask> m_pendingTasks;
|
||||
uint32_t m_logicalSize = 0; // m_slots.size() + slots reserved for pending growth
|
||||
|
||||
std::vector<uint32_t> m_deadIndices; // scratch buffer, reused every frame
|
||||
size_t m_liveCount = 0;
|
||||
|
||||
// Set for the duration of a single h.resume() call in update() (see
|
||||
// step 2 below), otherwise INVALID_COROUTINE_HANDLE. Lets
|
||||
// startCoroutine() record which coroutine, if any, was actually
|
||||
// running when it was called -- whether via `co_yield childTask()`
|
||||
// or a direct startCoroutine() call made from inside a coroutine's
|
||||
// body -- so onCoroutineStarted() can tell a subclass "this one was
|
||||
// started by that one" and propagate whatever it needs to.
|
||||
CoroutineHandle m_currentlyResuming = INVALID_COROUTINE_HANDLE;
|
||||
|
||||
bool shouldResume(Coroutine::promise_type& promise) const
|
||||
{
|
||||
switch (promise.m_waitKind)
|
||||
{
|
||||
case Coroutine::promise_type::WaitKind::NextFrame:
|
||||
return true;
|
||||
case Coroutine::promise_type::WaitKind::Condition:
|
||||
return promise.m_condition();
|
||||
case Coroutine::promise_type::WaitKind::NestedHandle:
|
||||
return !isCoroutineRunning(promise.m_nestedHandle);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
// Called once a coroutine becomes live in the slot map -- either
|
||||
// immediately (startCoroutine() reusing a free slot) or at the start
|
||||
// of the next update() (a newly-grown slot). Not called for a task
|
||||
// that never becomes live (e.g. stopped again before its first
|
||||
// update()).
|
||||
//
|
||||
// `parent` is whichever coroutine was actually resuming at the
|
||||
// moment `handle` was started -- set whether it was started via
|
||||
// `co_yield childTask()` or a plain `startCoroutine()` call made
|
||||
// from inside a coroutine's body -- or INVALID_COROUTINE_HANDLE if
|
||||
// it was started from outside any coroutine (e.g. from main()).
|
||||
// A subclass can use this to propagate its own per-coroutine data
|
||||
// from parent to child automatically; see coroutines/example.md.
|
||||
//
|
||||
// Override to hook "a coroutine started" in a subclass; the default
|
||||
// implementation does nothing.
|
||||
virtual void onCoroutineStarted(CoroutineHandle handle, CoroutineHandle parent) { (void)handle; (void)parent; }
|
||||
|
||||
// Called when a coroutine's slot is reclaimed, whether it finished
|
||||
// naturally (in update()) or was stopped explicitly
|
||||
// (stopCoroutine()). Only called for a handle that previously
|
||||
// triggered onCoroutineStarted(). Override to clean up any
|
||||
// per-coroutine data a subclass is tracking; the default
|
||||
// implementation does nothing.
|
||||
virtual void onCoroutineFinished(CoroutineHandle handle) { (void)handle; }
|
||||
|
||||
public:
|
||||
static CoroutineRunner* m_instance;
|
||||
|
||||
CoroutineRunner() { m_instance = this; }
|
||||
virtual ~CoroutineRunner() { if (m_instance == this) m_instance = nullptr; }
|
||||
|
||||
CoroutineRunner(const CoroutineRunner&) = delete;
|
||||
CoroutineRunner& operator=(const CoroutineRunner&) = delete;
|
||||
|
||||
// O(1): reuses a free slot in place if one exists (no reallocation, safe
|
||||
// even while update() is mid-iteration). Only falls back to the staging
|
||||
// queue when the backing store actually needs to grow.
|
||||
CoroutineHandle startCoroutine(Coroutine task)
|
||||
{
|
||||
if (!task.m_handle) return INVALID_COROUTINE_HANDLE;
|
||||
|
||||
if (!m_freeIndices.empty())
|
||||
{
|
||||
uint32_t index = m_freeIndices.back();
|
||||
m_freeIndices.pop_back();
|
||||
|
||||
CoroutineHandle newHandle(detail::acrux::coroutines::packHandle(index, m_slots[index].m_generation));
|
||||
task.m_handle.promise().m_assignedId = newHandle;
|
||||
|
||||
m_slots[index].m_task = std::move(task);
|
||||
m_slots[index].m_alive = true;
|
||||
++m_liveCount;
|
||||
onCoroutineStarted(newHandle, m_currentlyResuming);
|
||||
return newHandle;
|
||||
}
|
||||
|
||||
uint32_t index = m_logicalSize++;
|
||||
CoroutineHandle newHandle(detail::acrux::coroutines::packHandle(index, 0));
|
||||
task.m_handle.promise().m_assignedId = newHandle;
|
||||
m_pendingTasks.push_back(PendingTask{ index, std::move(task), m_currentlyResuming });
|
||||
return newHandle;
|
||||
}
|
||||
|
||||
void startSingleCoroutine(CoroutineHandle& slotHandle, Coroutine task)
|
||||
{
|
||||
stopCoroutine(slotHandle);
|
||||
slotHandle = startCoroutine(std::move(task));
|
||||
}
|
||||
|
||||
// O(1): decode the slot index directly; generation check rejects stale
|
||||
// handles from a slot that has since been reused (no ABA bugs). Falls
|
||||
// back to a short linear scan only for handles started earlier this
|
||||
// same frame and not yet drained into `m_slots`.
|
||||
bool isCoroutineRunning(CoroutineHandle handle) const
|
||||
{
|
||||
if (handle == INVALID_COROUTINE_HANDLE) return false;
|
||||
|
||||
uint32_t index = detail::acrux::coroutines::unpackIndex(handle.raw());
|
||||
uint32_t generation = detail::acrux::coroutines::unpackGeneration(handle.raw());
|
||||
|
||||
if (index < m_slots.size() && m_slots[index].m_alive && m_slots[index].m_generation == generation)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
for (const auto& pending : m_pendingTasks)
|
||||
{
|
||||
if (pending.m_index == index && pending.m_task.m_handle && pending.m_task.m_handle.promise().m_assignedId == handle)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// O(1) in the common case; same fallback as isCoroutineRunning for
|
||||
// handles stopped in the same frame they were started.
|
||||
void stopCoroutine(CoroutineHandle& handle)
|
||||
{
|
||||
if (handle == INVALID_COROUTINE_HANDLE) return;
|
||||
|
||||
uint32_t index = detail::acrux::coroutines::unpackIndex(handle.raw());
|
||||
uint32_t generation = detail::acrux::coroutines::unpackGeneration(handle.raw());
|
||||
|
||||
if (index < m_slots.size() && m_slots[index].m_alive && m_slots[index].m_generation == generation)
|
||||
{
|
||||
m_slots[index].m_task = Coroutine{};
|
||||
m_slots[index].m_alive = false;
|
||||
++m_slots[index].m_generation;
|
||||
m_freeIndices.push_back(index);
|
||||
--m_liveCount;
|
||||
onCoroutineFinished(handle);
|
||||
handle = INVALID_COROUTINE_HANDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = m_pendingTasks.begin(); it != m_pendingTasks.end(); ++it)
|
||||
{
|
||||
if (it->m_index == index && it->m_task.m_handle && it->m_task.m_handle.promise().m_assignedId == handle)
|
||||
{
|
||||
m_pendingTasks.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
handle = INVALID_COROUTINE_HANDLE;
|
||||
}
|
||||
|
||||
void update()
|
||||
{
|
||||
// Step 1: grow the backing store exactly once (if needed) and drain
|
||||
// staged insertions into it. This is the only place `m_slots`
|
||||
// grows, so it's safe to do it here, before any references into it
|
||||
// are taken below.
|
||||
if (!m_pendingTasks.empty())
|
||||
{
|
||||
if (m_logicalSize > m_slots.size())
|
||||
{
|
||||
m_slots.resize(m_logicalSize);
|
||||
}
|
||||
for (auto& pending : m_pendingTasks)
|
||||
{
|
||||
CoroutineHandle handle = pending.m_task.m_handle.promise().m_assignedId;
|
||||
m_slots[pending.m_index].m_task = std::move(pending.m_task);
|
||||
m_slots[pending.m_index].m_alive = true;
|
||||
++m_liveCount;
|
||||
onCoroutineStarted(handle, pending.m_parent);
|
||||
}
|
||||
m_pendingTasks.clear();
|
||||
}
|
||||
|
||||
// Step 2: tick every live slot. New coroutines started from within
|
||||
// a resumed coroutine go through startCoroutine, which either lands
|
||||
// in an existing free slot immediately or gets staged for next
|
||||
// frame — either way `m_slots` itself never reallocates during this
|
||||
// loop. m_currentlyResuming is set for the duration of each
|
||||
// h.resume() call so that a nested startCoroutine() (from
|
||||
// `co_yield childTask()` or a direct call in the coroutine's body)
|
||||
// can see who's resuming it right now.
|
||||
m_deadIndices.clear();
|
||||
|
||||
const size_t size = m_slots.size();
|
||||
for (size_t i = 0; i < size; ++i)
|
||||
{
|
||||
Slot& slot = m_slots[i];
|
||||
if (!slot.m_alive) continue;
|
||||
|
||||
auto h = slot.m_task.m_handle;
|
||||
if (!h || h.done())
|
||||
{
|
||||
m_deadIndices.push_back(static_cast<uint32_t>(i));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shouldResume(h.promise()))
|
||||
{
|
||||
m_currentlyResuming = h.promise().m_assignedId;
|
||||
h.resume();
|
||||
m_currentlyResuming = INVALID_COROUTINE_HANDLE;
|
||||
}
|
||||
|
||||
if (!h || h.done())
|
||||
{
|
||||
m_deadIndices.push_back(static_cast<uint32_t>(i));
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: reclaim finished slots.
|
||||
for (uint32_t index : m_deadIndices)
|
||||
{
|
||||
Slot& slot = m_slots[index];
|
||||
if (!slot.m_alive) continue; // may have been explicitly stopped already this frame
|
||||
// An alive slot always has a valid coroutine handle -- startCoroutine()
|
||||
// rejects an empty Coroutine before a slot is ever marked alive.
|
||||
CoroutineHandle handle = slot.m_task.m_handle.promise().m_assignedId;
|
||||
slot.m_task = Coroutine{};
|
||||
slot.m_alive = false;
|
||||
++slot.m_generation;
|
||||
m_freeIndices.push_back(index);
|
||||
--m_liveCount;
|
||||
onCoroutineFinished(handle);
|
||||
}
|
||||
}
|
||||
|
||||
size_t getActiveCount() const { return m_liveCount + m_pendingTasks.size(); }
|
||||
};
|
||||
|
||||
inline CoroutineRunner* CoroutineRunner::m_instance = nullptr;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Out-of-line definition of promise_type::yield_value(Coroutine&&). Needs
|
||||
// CoroutineRunner to be a complete type, so it lives here rather than inline
|
||||
// inside Coroutine.hpp (see the comment at the top of that file).
|
||||
// ----------------------------------------------------------------------------
|
||||
inline std::suspend_always Coroutine::promise_type::yield_value(Coroutine&& childTask)
|
||||
{
|
||||
assert(CoroutineRunner::m_instance &&
|
||||
"co_yield of a child Coroutine requires an active CoroutineRunner "
|
||||
"(construct one before starting/resuming coroutines).");
|
||||
|
||||
CoroutineHandle childHandle = CoroutineRunner::m_instance->startCoroutine(std::move(childTask));
|
||||
|
||||
m_waitKind = WaitKind::NestedHandle;
|
||||
m_nestedHandle = childHandle;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Free-function convenience wrapper around CoroutineRunner::startCoroutine,
|
||||
// using the active runner (CoroutineRunner::m_instance) — the same
|
||||
// singleton that co_yield of a child Coroutine relies on internally.
|
||||
//
|
||||
// Starts `task` immediately and returns its handle WITHOUT suspending the
|
||||
// caller, so it can be called from anywhere: main(), a plain function, or
|
||||
// from inside another running coroutine. This is the "fire it, keep the
|
||||
// handle, wait later (or never)" entry point:
|
||||
//
|
||||
// CoroutineHandle bg = startCoroutine(controller.fadeOut(delta));
|
||||
// // ... keep going, do other work ...
|
||||
// co_yield waitFor(bg); // wait on it whenever you're ready (optional)
|
||||
//
|
||||
// (Contrast with `co_yield controller.fadeOut(delta);`, which starts AND
|
||||
// immediately blocks the caller until it finishes.)
|
||||
//
|
||||
// This always calls CoroutineRunner::startCoroutine specifically -- if the
|
||||
// active runner is actually a subclass with its own extended
|
||||
// startCoroutine() overload(s), call those directly on the runner instance
|
||||
// instead of through this free function.
|
||||
inline CoroutineHandle startCoroutine(Coroutine task)
|
||||
{
|
||||
assert(CoroutineRunner::m_instance &&
|
||||
"startCoroutine() requires an active CoroutineRunner "
|
||||
"(construct one before starting/resuming coroutines).");
|
||||
|
||||
return CoroutineRunner::m_instance->startCoroutine(std::move(task));
|
||||
}
|
||||
|
||||
// Free-function convenience wrapper around CoroutineRunner::stopCoroutine,
|
||||
// using the active runner (CoroutineRunner::m_instance).
|
||||
//
|
||||
// Takes `handle` by REFERENCE, same as the member function — it stops the
|
||||
// coroutine AND resets `handle` to INVALID_COROUTINE_HANDLE, so a stale
|
||||
// handle can never accidentally be reused or re-stopped:
|
||||
//
|
||||
// CoroutineHandle bg = startCoroutine(controller.fadeOut(delta));
|
||||
// ...
|
||||
// stopCoroutine(bg); // bg is now INVALID_COROUTINE_HANDLE
|
||||
// stopCoroutine(bg); // safe no-op, doesn't crash or double-free
|
||||
inline void stopCoroutine(CoroutineHandle& handle)
|
||||
{
|
||||
handle.stop();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Out-of-line definitions of CoroutineHandle::isRunning()/stop(). Needs
|
||||
// CoroutineRunner to be a complete type, so they live here rather than
|
||||
// inline inside CoroutineHandle.h (see the comment at the top of that
|
||||
// file). This is what lets you write `buster.stop();` /
|
||||
// `buster.isRunning()` directly on a handle instead of going through
|
||||
// CoroutineRunner or a free function.
|
||||
// ----------------------------------------------------------------------------
|
||||
inline bool CoroutineHandle::isRunning() const
|
||||
{
|
||||
assert(CoroutineRunner::m_instance &&
|
||||
"CoroutineHandle::isRunning() requires an active CoroutineRunner "
|
||||
"(construct one before starting/resuming coroutines).");
|
||||
|
||||
return CoroutineRunner::m_instance->isCoroutineRunning(*this);
|
||||
}
|
||||
|
||||
inline void CoroutineHandle::stop()
|
||||
{
|
||||
assert(CoroutineRunner::m_instance &&
|
||||
"CoroutineHandle::stop() requires an active CoroutineRunner "
|
||||
"(construct one before starting/resuming coroutines).");
|
||||
|
||||
CoroutineRunner::m_instance->stopCoroutine(*this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
|
||||
// ============================================================================
|
||||
// Predicate.hpp — small-buffer-optimized type-erased bool() callable.
|
||||
//
|
||||
// Stores any callable (lambda, functor) up to kBufferSize bytes INLINE — no
|
||||
// heap allocation — which is exactly what you need for the common case of a
|
||||
// lambda capturing `this` and/or a couple of coroutine-local values, e.g.:
|
||||
//
|
||||
// WaitUntil([this]{ return this->targetInRange; })
|
||||
// WaitUntil([hp = enemy->hp]{ return hp() <= 0; })
|
||||
// ============================================================================
|
||||
|
||||
#include <cstddef>
|
||||
#include <new>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <cassert>
|
||||
|
||||
namespace acrux::coroutines
|
||||
{
|
||||
class Predicate
|
||||
{
|
||||
public:
|
||||
Predicate() noexcept = default;
|
||||
|
||||
template <typename F, typename = std::enable_if_t<!std::is_same_v<std::decay_t<F>, Predicate>>>
|
||||
Predicate(F&& f)
|
||||
{
|
||||
using Fn = std::decay_t<F>;
|
||||
static_assert(sizeof(Fn) <= BufferSize,
|
||||
"Predicate: captured callable is too large for the inline buffer. "
|
||||
"Capture by pointer/reference (e.g. [this]) instead of by value, "
|
||||
"or increase Predicate::kBufferSize.");
|
||||
static_assert(alignof(Fn) <= alignof(std::max_align_t),
|
||||
"Predicate: captured callable's alignment exceeds max_align_t.");
|
||||
|
||||
::new (static_cast<void*>(&m_storage)) Fn(std::forward<F>(f));
|
||||
m_vtable = &VTableFor<Fn>::table;
|
||||
}
|
||||
|
||||
Predicate(const Predicate&) = delete;
|
||||
Predicate& operator=(const Predicate&) = delete;
|
||||
|
||||
Predicate(Predicate&& other) noexcept { moveFrom(other); }
|
||||
|
||||
Predicate& operator=(Predicate&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
destroy();
|
||||
moveFrom(other);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~Predicate() { destroy(); }
|
||||
|
||||
bool operator()() const
|
||||
{
|
||||
assert(m_vtable && "Predicate invoked while empty");
|
||||
return m_vtable->invoke(&m_storage);
|
||||
}
|
||||
|
||||
explicit operator bool() const noexcept { return m_vtable != nullptr; }
|
||||
|
||||
private:
|
||||
static constexpr size_t BufferSize = 32;
|
||||
|
||||
struct VTable
|
||||
{
|
||||
bool (*invoke)(const void*);
|
||||
void (*move)(void* dst, void* src);
|
||||
void (*destroy)(void*);
|
||||
};
|
||||
|
||||
template <typename Fn>
|
||||
struct VTableFor
|
||||
{
|
||||
static bool invoke(const void* p)
|
||||
{
|
||||
return (*reinterpret_cast<const Fn*>(p))();
|
||||
}
|
||||
static void move(void* dst, void* src)
|
||||
{
|
||||
::new (dst) Fn(std::move(*reinterpret_cast<Fn*>(src)));
|
||||
reinterpret_cast<Fn*>(src)->~Fn();
|
||||
}
|
||||
static void destroy(void* p)
|
||||
{
|
||||
reinterpret_cast<Fn*>(p)->~Fn();
|
||||
}
|
||||
static inline const VTable table{ &invoke, &move, &destroy };
|
||||
};
|
||||
|
||||
void moveFrom(Predicate& other) noexcept
|
||||
{
|
||||
m_vtable = other.m_vtable;
|
||||
if (m_vtable)
|
||||
{
|
||||
m_vtable->move(&m_storage, &other.m_storage);
|
||||
other.m_vtable = nullptr;
|
||||
}
|
||||
}
|
||||
void destroy() noexcept
|
||||
{
|
||||
if (m_vtable)
|
||||
{
|
||||
m_vtable->destroy(&m_storage);
|
||||
m_vtable = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
alignas(std::max_align_t) unsigned char m_storage[BufferSize];
|
||||
const VTable* m_vtable = nullptr;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
// ============================================================================
|
||||
// WaitConditions.hpp — waitForSeconds / waitUntil / waitWhile / waitFor.
|
||||
//
|
||||
// This file is NOT part of the coroutine engine. Everything here is built
|
||||
// entirely on top of the public core API (Coroutine's generic "co_yield
|
||||
// anything invocable as bool()" support, and CoroutineHandle::isRunning()) --
|
||||
// Coroutine.h and CoroutineRunner.h have no idea any of this exists.
|
||||
//
|
||||
// Each helper below just returns a small callable object with a
|
||||
// `bool operator()() const`. co_yield-ing that object works because of
|
||||
// Coroutine::promise_type's generic yield_value<F> overload, not because of
|
||||
// anything specific to these types. Writing your own wait condition is the
|
||||
// same one-line pattern: define a small callable, co_yield it (optionally
|
||||
// through a helper function like the ones below, for a readable call site).
|
||||
// ============================================================================
|
||||
|
||||
#include <chrono>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include <acrux/coroutines/CoroutineHandle.h>
|
||||
|
||||
namespace acrux::coroutines::standard
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
// co_yield waitForSeconds(2.5f) -- resumes once `seconds` of real time
|
||||
// has elapsed. The clock starts on this condition's FIRST evaluation
|
||||
// (the first time CoroutineRunner::update() checks it after the
|
||||
// coroutine suspends), not when waitForSeconds() itself was called.
|
||||
class SecondsCondition
|
||||
{
|
||||
public:
|
||||
explicit SecondsCondition(float seconds) noexcept : m_durationSeconds(seconds) {}
|
||||
|
||||
bool operator()() const
|
||||
{
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (!m_started)
|
||||
{
|
||||
m_startTime = now;
|
||||
m_started = true;
|
||||
return false;
|
||||
}
|
||||
std::chrono::duration<float> elapsed = now - m_startTime;
|
||||
return elapsed.count() >= m_durationSeconds;
|
||||
}
|
||||
|
||||
private:
|
||||
float m_durationSeconds;
|
||||
mutable std::chrono::steady_clock::time_point m_startTime{};
|
||||
mutable bool m_started = false;
|
||||
};
|
||||
|
||||
// co_yield waitFor(handle) -- resumes once `handle` is no longer
|
||||
// running. Implemented purely through CoroutineHandle's public API
|
||||
// (isRunning()), so it works the same as co_yield-ing the handle
|
||||
// directly (a core feature); this exists only so a call site reading
|
||||
// "waitFor" lines up with the other wait* helpers.
|
||||
class HandleCondition
|
||||
{
|
||||
public:
|
||||
explicit HandleCondition(CoroutineHandle handle) noexcept : m_handle(handle) {}
|
||||
|
||||
bool operator()() const { return !m_handle.isRunning(); }
|
||||
|
||||
private:
|
||||
CoroutineHandle m_handle;
|
||||
};
|
||||
|
||||
// co_yield waitWhile(predicate) -- resumes once `predicate()` returns
|
||||
// false. A thin wrapper negating whatever predicate was passed in.
|
||||
template <typename F>
|
||||
class NegatedCondition
|
||||
{
|
||||
public:
|
||||
explicit NegatedCondition(F predicate) : m_predicate(std::move(predicate)) {}
|
||||
|
||||
bool operator()() const { return !m_predicate(); }
|
||||
|
||||
private:
|
||||
F m_predicate;
|
||||
};
|
||||
}
|
||||
|
||||
inline detail::SecondsCondition waitForSeconds(float seconds)
|
||||
{
|
||||
return detail::SecondsCondition(seconds);
|
||||
}
|
||||
|
||||
// Waits for a coroutine that was started earlier (e.g. via the free
|
||||
// function startCoroutine() in CoroutineRunner.h) WITHOUT yielding at
|
||||
// that time. Equivalent to `co_yield handle` directly — provided here
|
||||
// so waiting on a handle reads the same way as the other wait* helpers:
|
||||
//
|
||||
// CoroutineHandle bg = startCoroutine(controller.fadeOut(delta)); // fire, don't wait yet
|
||||
// ... do other stuff, possibly across several co_yields ...
|
||||
// co_yield waitFor(bg); // now block until it's done
|
||||
inline detail::HandleCondition waitFor(CoroutineHandle handle)
|
||||
{
|
||||
return detail::HandleCondition(handle);
|
||||
}
|
||||
|
||||
// Accepts any callable convertible to bool() — including lambdas that
|
||||
// capture `this` or a member coroutine's arguments/locals by value or
|
||||
// reference. This is what makes waitUntil/waitWhile usable from
|
||||
// non-static member coroutines. Returned by value; co_yield can also
|
||||
// accept `predicate` directly without this wrapper, since Coroutine's
|
||||
// core yield_value already accepts any bool()-invocable object -- this
|
||||
// exists purely so the call site reads "waitUntil".
|
||||
template <typename F>
|
||||
inline std::decay_t<F> waitUntil(F&& predicate)
|
||||
{
|
||||
static_assert(std::is_invocable_r_v<bool, std::decay_t<F>&>,
|
||||
"waitUntil requires a callable convertible to bool().");
|
||||
return std::forward<F>(predicate);
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
inline detail::NegatedCondition<std::decay_t<F>> waitWhile(F&& predicate)
|
||||
{
|
||||
static_assert(std::is_invocable_r_v<bool, std::decay_t<F>&>,
|
||||
"waitWhile requires a callable convertible to bool().");
|
||||
return detail::NegatedCondition<std::decay_t<F>>(std::forward<F>(predicate));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user