🛠️🐜 Antkeeper superbuild with dependencies included https://antkeeper.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

300 lines
9.7 KiB

#ifndef ENTT_PROCESS_SCHEDULER_HPP
#define ENTT_PROCESS_SCHEDULER_HPP
#include <vector>
#include <memory>
#include <utility>
#include <algorithm>
#include <type_traits>
#include "../config/config.h"
#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.<br/>
* 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<my_process>(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<typename Delta>
class scheduler {
struct process_handler {
using instance_type = std::unique_ptr<void, void(*)(void *)>;
using update_fn_type = bool(process_handler &, Delta, void *);
using abort_fn_type = void(process_handler &, bool);
using next_type = std::unique_ptr<process_handler>;
instance_type instance;
update_fn_type *update;
abort_fn_type *abort;
next_type next;
};
struct continuation {
continuation(process_handler *ref)
: handler{ref}
{
ENTT_ASSERT(handler);
}
template<typename Proc, typename... Args>
continuation then(Args &&... args) {
static_assert(std::is_base_of_v<process<Proc, Delta>, Proc>);
auto proc = typename process_handler::instance_type{new Proc{std::forward<Args>(args)...}, &scheduler::deleter<Proc>};
handler->next.reset(new process_handler{std::move(proc), &scheduler::update<Proc>, &scheduler::abort<Proc>, nullptr});
handler = handler->next.get();
return *this;
}
template<typename Func>
continuation then(Func &&func) {
return then<process_adaptor<std::decay_t<Func>, Delta>>(std::forward<Func>(func));
}
private:
process_handler *handler;
};
template<typename Proc>
static bool update(process_handler &handler, const Delta delta, void *data) {
auto *process = static_cast<Proc *>(handler.instance.get());
process->tick(delta, data);
auto dead = process->dead();
if(dead) {
if(handler.next && !process->rejected()) {
handler = std::move(*handler.next);
// forces the process to exit the uninitialized state
dead = handler.update(handler, {}, nullptr);
} else {
handler.instance.reset();
}
}
return dead;
}
template<typename Proc>
static void abort(process_handler &handler, const bool immediately) {
static_cast<Proc *>(handler.instance.get())->abort(immediately);
}
template<typename Proc>
static void deleter(void *proc) {
delete static_cast<Proc *>(proc);
}
public:
/*! @brief Unsigned integer type. */
using size_type = typename std::vector<process_handler>::size_type;
/*! @brief Default constructor. */
scheduler() ENTT_NOEXCEPT = default;
/*! @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.
*/
size_type size() const ENTT_NOEXCEPT {
return handlers.size();
}
/**
* @brief Returns true if at least a process is currently scheduled.
* @return True if there are scheduled processes, false otherwise.
*/
bool empty() const ENTT_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<my_process>(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<my_other_process>();
* @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<typename Proc, typename... Args>
auto attach(Args &&... args) {
static_assert(std::is_base_of_v<process<Proc, Delta>, Proc>);
auto proc = typename process_handler::instance_type{new Proc{std::forward<Args>(args)...}, &scheduler::deleter<Proc>};
process_handler handler{std::move(proc), &scheduler::update<Proc>, &scheduler::abort<Proc>, nullptr};
// forces the process to exit the uninitialized state
handler.update(handler, {}, nullptr);
return continuation{&handlers.emplace_back(std::move(handler))};
}
/**
* @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.<br/>
* 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<my_process>(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<typename Func>
auto attach(Func &&func) {
using Proc = process_adaptor<std::decay_t<Func>, Delta>;
return attach<Proc>(std::forward<Func>(func));
}
/**
* @brief Updates all scheduled processes.
*
* All scheduled processes are executed in no specific order.<br/>
* 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) {
bool clean = false;
for(auto pos = handlers.size(); pos; --pos) {
auto &handler = handlers[pos-1];
const bool dead = handler.update(handler, delta, data);
clean = clean || dead;
}
if(clean) {
handlers.erase(std::remove_if(handlers.begin(), handlers.end(), [](auto &handler) {
return !handler.instance;
}), handlers.end());
}
}
/**
* @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.<br/>
* 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) {
decltype(handlers) exec;
exec.swap(handlers);
std::for_each(exec.begin(), exec.end(), [immediately](auto &handler) {
handler.abort(handler, immediately);
});
std::move(handlers.begin(), handlers.end(), std::back_inserter(exec));
handlers.swap(exec);
}
private:
std::vector<process_handler> handlers{};
};
}
#endif // ENTT_PROCESS_SCHEDULER_HPP