#ifndef ENTT_PROCESS_SCHEDULER_HPP #define ENTT_PROCESS_SCHEDULER_HPP #include #include #include #include #include #include #include "process.hpp" namespace entt { /** * @brief Cooperative scheduler for processes. * * A cooperative scheduler runs processes and helps managing their life cycles. * * Each process is invoked once per tick. If a process terminates, it's * removed automatically from the scheduler and it's never invoked again.
* A process can also have a child. In this case, the process is replaced with * its child when it terminates if it returns with success. In case of errors, * both the process and its child are discarded. * * Example of use (pseudocode): * * @code{.cpp} * scheduler.attach([](auto delta, void *, auto succeed, auto fail) { * // code * }).then(arguments...); * @endcode * * In order to invoke all scheduled processes, call the `update` member function * passing it the elapsed time to forward to the tasks. * * @sa process * * @tparam Delta Type to use to provide elapsed time. */ template class scheduler { struct process_handler { using instance_type = std::unique_ptr; using update_fn_type = bool(scheduler &, std::size_t, Delta, void *); using abort_fn_type = void(scheduler &, std::size_t, bool); using next_type = std::unique_ptr; instance_type instance; update_fn_type *update; abort_fn_type *abort; next_type next; }; struct continuation { continuation(process_handler *ref) noexcept : handler{ref} {} template continuation then(Args &&...args) { static_assert(std::is_base_of_v, Proc>, "Invalid process type"); auto proc = typename process_handler::instance_type{new Proc{std::forward(args)...}, &scheduler::deleter}; handler->next.reset(new process_handler{std::move(proc), &scheduler::update, &scheduler::abort, nullptr}); handler = handler->next.get(); return *this; } template continuation then(Func &&func) { return then, Delta>>(std::forward(func)); } private: process_handler *handler; }; template [[nodiscard]] static bool update(scheduler &owner, std::size_t pos, const Delta delta, void *data) { auto *process = static_cast(owner.handlers[pos].instance.get()); process->tick(delta, data); if(process->rejected()) { return true; } else if(process->finished()) { if(auto &&handler = owner.handlers[pos]; handler.next) { handler = std::move(*handler.next); // forces the process to exit the uninitialized state return handler.update(owner, pos, {}, nullptr); } return true; } return false; } template static void abort(scheduler &owner, std::size_t pos, const bool immediately) { static_cast(owner.handlers[pos].instance.get())->abort(immediately); } template static void deleter(void *proc) { delete static_cast(proc); } public: /*! @brief Unsigned integer type. */ using size_type = std::size_t; /*! @brief Default constructor. */ scheduler() : handlers{} {} /*! @brief Default move constructor. */ scheduler(scheduler &&) = default; /*! @brief Default move assignment operator. @return This scheduler. */ scheduler &operator=(scheduler &&) = default; /** * @brief Number of processes currently scheduled. * @return Number of processes currently scheduled. */ [[nodiscard]] size_type size() const noexcept { return handlers.size(); } /** * @brief Returns true if at least a process is currently scheduled. * @return True if there are scheduled processes, false otherwise. */ [[nodiscard]] bool empty() const noexcept { return handlers.empty(); } /** * @brief Discards all scheduled processes. * * Processes aren't aborted. They are discarded along with their children * and never executed again. */ void clear() { handlers.clear(); } /** * @brief Schedules a process for the next tick. * * Returned value is an opaque object that can be used to attach a child to * the given process. The child is automatically scheduled when the process * terminates and only if the process returns with success. * * Example of use (pseudocode): * * @code{.cpp} * // schedules a task in the form of a process class * scheduler.attach(arguments...) * // appends a child in the form of a lambda function * .then([](auto delta, void *, auto succeed, auto fail) { * // code * }) * // appends a child in the form of another process class * .then(); * @endcode * * @tparam Proc Type of process to schedule. * @tparam Args Types of arguments to use to initialize the process. * @param args Parameters to use to initialize the process. * @return An opaque object to use to concatenate processes. */ template auto attach(Args &&...args) { static_assert(std::is_base_of_v, Proc>, "Invalid process type"); auto proc = typename process_handler::instance_type{new Proc{std::forward(args)...}, &scheduler::deleter}; auto &&ref = handlers.emplace_back(process_handler{std::move(proc), &scheduler::update, &scheduler::abort, nullptr}); // forces the process to exit the uninitialized state ref.update(*this, handlers.size() - 1u, {}, nullptr); return continuation{&handlers.back()}; } /** * @brief Schedules a process for the next tick. * * A process can be either a lambda or a functor. The scheduler wraps both * of them in a process adaptor internally.
* The signature of the function call operator should be equivalent to the * following: * * @code{.cpp} * void(Delta delta, void *data, auto succeed, auto fail); * @endcode * * Where: * * * `delta` is the elapsed time. * * `data` is an opaque pointer to user data if any, `nullptr` otherwise. * * `succeed` is a function to call when a process terminates with success. * * `fail` is a function to call when a process terminates with errors. * * The signature of the function call operator of both `succeed` and `fail` * is equivalent to the following: * * @code{.cpp} * void(); * @endcode * * Returned value is an opaque object that can be used to attach a child to * the given process. The child is automatically scheduled when the process * terminates and only if the process returns with success. * * Example of use (pseudocode): * * @code{.cpp} * // schedules a task in the form of a lambda function * scheduler.attach([](auto delta, void *, auto succeed, auto fail) { * // code * }) * // appends a child in the form of another lambda function * .then([](auto delta, void *, auto succeed, auto fail) { * // code * }) * // appends a child in the form of a process class * .then(arguments...); * @endcode * * @sa process_adaptor * * @tparam Func Type of process to schedule. * @param func Either a lambda or a functor to use as a process. * @return An opaque object to use to concatenate processes. */ template auto attach(Func &&func) { using Proc = process_adaptor, Delta>; return attach(std::forward(func)); } /** * @brief Updates all scheduled processes. * * All scheduled processes are executed in no specific order.
* If a process terminates with success, it's replaced with its child, if * any. Otherwise, if a process terminates with an error, it's removed along * with its child. * * @param delta Elapsed time. * @param data Optional data. */ void update(const Delta delta, void *data = nullptr) { for(auto pos = handlers.size(); pos; --pos) { const auto curr = pos - 1u; if(const auto dead = handlers[curr].update(*this, curr, delta, data); dead) { std::swap(handlers[curr], handlers.back()); handlers.pop_back(); } } } /** * @brief Aborts all scheduled processes. * * Unless an immediate operation is requested, the abort is scheduled for * the next tick. Processes won't be executed anymore in any case.
* Once a process is fully aborted and thus finished, it's discarded along * with its child, if any. * * @param immediately Requests an immediate operation. */ void abort(const bool immediately = false) { for(auto pos = handlers.size(); pos; --pos) { const auto curr = pos - 1u; handlers[curr].abort(*this, curr, immediately); } } private: std::vector handlers{}; }; } // namespace entt #endif