reworked the general update strategy interface

This commit is contained in:
2021-04-28 19:38:25 +02:00
parent b8a5cd7cf4
commit efad254193
53 changed files with 756 additions and 889 deletions

View File

@ -1,236 +0,0 @@
#include "./default_strategy.hpp"
#include <cassert>
#include <functional>
namespace MM::UpdateStrategies {
#define __L_ASSERT(cond) if (!(cond)) { return false; }
SingleThreadedDefault::Graph& SingleThreadedDefault::getGraph(update_phase_t type) {
using type_t = update_phase_t;
switch (type) {
case type_t::PRE:
return _pre_graph;
case type_t::MAIN:
return _main_graph;
case type_t::POST:
return _post_graph;
}
return _main_graph; // unreachable
}
std::set<update_key_t>& SingleThreadedDefault::getActiveSet(update_phase_t type) {
using type_t = update_phase_t;
switch (type) {
case type_t::PRE:
return _pre_active;
case type_t::MAIN:
return _main_active;
case type_t::POST:
return _post_active;
}
return _main_active; // unreachable
}
void SingleThreadedDefault::runType(MM::Engine& engine, update_phase_t type) {
auto aset = getActiveSet(type); // copy
const auto& graph = getGraph(type);
while (!aset.empty()) {
for (const auto key : aset) {
// check if dependencies are resolved (or dont exist)
bool resolved = true;
for (const auto deps : graph.at(key)) {
if (aset.count(deps)) {
resolved = false;
break;
}
}
if (resolved) {
// do task
_tasks[key].fn(engine);
// delete task from set
aset.erase(key);
break; // this might be optional but makes the delete easy
} else {
// continue the for / dont do anything with this task yet
}
}
}
}
SingleThreadedDefault::~SingleThreadedDefault(void) {
}
bool SingleThreadedDefault::registerService(const entt::id_type s_id, std::vector<UpdateCreationInfo>&& info_array) {
__L_ASSERT(_service_tasks.count(s_id) == 0);
// early out
if (info_array.empty()) {
return true;
}
auto& service_tasks = _service_tasks[s_id];
for (const UpdateCreationInfo& reg_e : info_array) {
// test if already exists
__L_ASSERT(_tasks.count(reg_e.key) == 0);
// also the graphs
__L_ASSERT(_pre_graph.count(reg_e.key) == 0);
__L_ASSERT(_main_graph.count(reg_e.key) == 0);
__L_ASSERT(_post_graph.count(reg_e.key) == 0);
// also the enabled taks
__L_ASSERT(_pre_active.count(reg_e.key) == 0);
__L_ASSERT(_main_active.count(reg_e.key) == 0);
__L_ASSERT(_post_active.count(reg_e.key) == 0);
// potentially check for cicles (can be done by provided decorator)
}
for (const UpdateCreationInfo& reg_e : info_array) {
// add to tasks
{
auto& new_task = _tasks[reg_e.key];
new_task.name = reg_e.name;
new_task.fn = reg_e.fn;
new_task.phase = reg_e.phase;
new_task.auto_enable = reg_e.auto_enable;
new_task.enabled = false;
}
// add relation to service
service_tasks.emplace_back(reg_e.key);
// fill in dependencies
auto& graph = getGraph(reg_e.phase);
graph[reg_e.key].insert(reg_e.dependencies.begin(), reg_e.dependencies.end());
}
return true;
}
bool SingleThreadedDefault::enable(const update_key_t key) {
__L_ASSERT(_tasks.count(key) == 1);
__L_ASSERT(_tasks[key].enabled == false);
auto ret = getActiveSet(_tasks[key].phase).emplace(key);
_tasks[key].enabled = true;
return ret.second;
}
bool SingleThreadedDefault::disable(const update_key_t key) {
__L_ASSERT(_tasks.count(key) == 1);
__L_ASSERT(_tasks[key].enabled == true);
auto& aset = getActiveSet(_tasks[key].phase);
__L_ASSERT(aset.count(key) == 1);
aset.erase(key);
_tasks[key].enabled = false;
return true;
}
bool SingleThreadedDefault::enableService(const entt::id_type s_id) {
bool succ = true;
for (const auto id : _service_tasks[s_id]) {
auto& task = _tasks[id];
// there should be no task running, if the service is no enabled!!
assert(!task.enabled);
if (task.auto_enable) {
succ &= enable(id);
}
}
return succ;
}
bool SingleThreadedDefault::disableService(const entt::id_type s_id) {
bool succ = true;
for (const auto id : _service_tasks[s_id]) {
auto& task = _tasks[id];
if (task.auto_enable) {
assert(task.enabled); // this should never happen
}
if (task.enabled) {
succ &= disable(id);
}
}
return succ;
}
bool SingleThreadedDefault::depend(const update_key_t A, const update_key_t B) {
// TODO: error checking lol
if (_tasks.count(A) == 0) {
return false; // can not add a dependecy of a non existing task
// TODO: or do we?
}
if (_tasks.count(B) == 1) {
if (_tasks[A].phase != _tasks[B].phase) {
// cross graph tasks are not allowed
return false;
}
}
auto& graph = getGraph(_tasks[A].phase);
auto ret = graph[A].emplace(B);
return ret.second; // returns whether it was inserted (or already existed)
}
void SingleThreadedDefault::doUpdate(MM::Engine& engine) {
// pre
runType(engine, update_phase_t::PRE);
// main
runType(engine, update_phase_t::MAIN);
// post
runType(engine, update_phase_t::POST);
// simulate async
for (size_t i = 0; !_async_queue.empty() && i < _max_async_per_tick; i++) {
_async_queue.back()(engine);
_async_queue.pop_back();
}
if (!_deferred_queue.empty()) {
for (auto&& fn : _deferred_queue) {
fn(engine);
}
_deferred_queue.clear();
}
}
void SingleThreadedDefault::addDeferred(std::function<void(Engine&)> function) {
_deferred_queue.emplace_back(std::move(function));
}
void SingleThreadedDefault::addAsync(std::function<void(Engine&)> function) {
_async_queue.emplace_back(std::move(function));
}
#undef __L_ASSERT
} // MM::UpdateStrategies

View File

@ -1,83 +0,0 @@
#pragma once
#include "./update_strategy.hpp"
#include <memory>
#include <unordered_map>
#include <vector>
#include <set>
// fwd
namespace MM::Services {
class ImGuiEngineTools;
}
namespace MM::UpdateStrategies {
class SingleThreadedDefault : public MM::UpdateStrategies::UpdateStrategy {
friend MM::Services::ImGuiEngineTools;
private:
struct Task {
std::string name;
std::function<void(Engine&)> fn;
update_phase_t phase;
bool auto_enable;
bool enabled = false;
};
std::unordered_map<update_key_t, Task> _tasks;
// the tasks a service has registered
std::unordered_map<entt::id_type, std::vector<update_key_t>> _service_tasks;
// tasks dependencies
using Graph = std::unordered_map<update_key_t, std::set<update_key_t>>;
// TODO: do vector for perf?
Graph _pre_graph;
Graph _main_graph;
Graph _post_graph;
std::set<update_key_t> _pre_active;
std::set<update_key_t> _main_active;
std::set<update_key_t> _post_active;
std::vector<std::function<void(Engine&)>> _deferred_queue;
std::vector<std::function<void(Engine&)>> _async_queue;
const size_t _max_async_per_tick = 5; // prevent blocking, this should be finetuned
private:
Graph& getGraph(update_phase_t phase);
std::set<update_key_t>& getActiveSet(update_phase_t phase);
void runType(MM::Engine& engine, update_phase_t phase);
public:
SingleThreadedDefault(void) = default;
virtual ~SingleThreadedDefault(void);
const char* name(void) override { return "SingleThreadedDefault"; }
protected:
bool registerService(const entt::id_type s_id, std::vector<UpdateCreationInfo>&& info_array) override;
bool enableService(const entt::id_type s_id) override;
bool disableService(const entt::id_type s_id) override;
void doUpdate(MM::Engine& engine) override;
public:
bool enable(const update_key_t key) override;
bool disable(const update_key_t key) override;
bool depend(const update_key_t A, const update_key_t B) override;
void addDeferred(std::function<void(Engine&)> function) override;
void addAsync(std::function<void(Engine&)> function) override;
};
} // MM::UpdateStrategies

View File

@ -1,56 +0,0 @@
#pragma once
#include "./update_strategy.hpp"
#include <memory>
#include <type_traits>
namespace MM::UpdateStrategies {
// checks for cyles
template<class T>
class DependencyCheckDecorator : public T {
static_assert(std::is_base_of_v<UpdateStrategy, T>);
public:
//DependencyCheckDecorator(void) = default;
//template<typename ...Args>
//DependencyCheckDecorator(Args... args) : T(args) {
//}
virtual ~DependencyCheckDecorator(void) {}
private:
void doUpdate(MM::Engine& engine) override {
T::doUpdate(engine);
}
bool registerService(const entt::id_type s_id, std::vector<UpdateCreationInfo>&& info_array) override {
return T::registerService(s_id, std::move(info_array));
}
bool enableService(const entt::id_type s_id) override {
return T::enableService(s_id);
}
bool disableService(const entt::id_type s_id) override {
return T::disableService(s_id);
}
public:
bool enable(const update_key_t key) override {
return T::enable(key);
}
bool disable(const update_key_t key) override {
return T::disable(key);
}
bool depend(const update_key_t A, const update_key_t B) override {
return T::depend(A, B);
}
};
} // MM::UpdateStrategies

View File

@ -0,0 +1,28 @@
#pragma once
#include "./update_strategy.hpp"
// Rezz x Deadmau5 - hypnocurrency
namespace MM::UpdateStrategies {
// does nothing, even less then a mock class
// for testing only!
class Dummy : public UpdateStrategy {
public:
~Dummy(void) {}
const char* name(void) override { return "Dummy"; }
protected: // the engine facing interface
bool enableService(const entt::id_type, std::vector<TaskInfo>&&) override { return true; }
bool disableService(const entt::id_type) override { return true; }
void doUpdate(MM::Engine&) override {}
public: // the user facing interface
void addDeferred(std::function<void(Engine&)>) override {}
void addAsync(std::function<void(Engine&)>) override {}
};
} // MM::UpdateStrategies

View File

@ -0,0 +1,126 @@
#include "./sequential_strategy.hpp"
#include "./tasking_utils.hpp"
#include <cassert>
#include <functional>
#include <tracy/Tracy.hpp>
namespace MM::UpdateStrategies {
//#define __L_ASSERT(cond) if (!(cond)) { return false; }
void Sequential::doGraphSequential(MM::Engine& engine, const std::set<update_key_t>& tasks) {
Graph graph = build_task_graph(tasks,
[this](update_key_t key) -> const TaskInfo& {
return _tasks.at(key);
}
);
walk_graph_sequential(graph, [this, &engine](update_key_t task_id ) {
_tasks.at(task_id)._fn(engine);
});
}
Sequential::~Sequential(void) {
}
bool Sequential::enableService(const entt::id_type service_id, std::vector<TaskInfo>&& task_array) {
if (_service_tasks.count(service_id)) {
// error
return false;
}
auto& service = _service_tasks[service_id];
for (const auto& task : task_array) {
_tasks.emplace(task._key, task);
service.emplace(task._key);
}
return true;
}
bool Sequential::disableService(const entt::id_type service_id) {
if (!_service_tasks.count(service_id)) {
// error
return false;
}
for (const auto task_id : _service_tasks[service_id]) {
_tasks.erase(task_id);
}
_service_tasks.erase(service_id);
return true;
}
void Sequential::doUpdate(MM::Engine& engine) {
ZoneScopedN("MM::UpdateStrategies::Sequential::doUpdate")
// TODO: caching
std::set<update_key_t> pre_tasks;
std::set<update_key_t> main_tasks;
std::set<update_key_t> post_tasks;
for (const auto& [task_id, task] : _tasks) {
switch (task._phase) {
case update_phase_t::PRE:
pre_tasks.emplace(task_id);
break;
case update_phase_t::MAIN:
main_tasks.emplace(task_id);
break;
case update_phase_t::POST:
post_tasks.emplace(task_id);
break;
}
}
{ // pre
ZoneScopedN("MM::UpdateStrategies::Sequential::doUpdate::pre")
doGraphSequential(engine, pre_tasks);
}
{ // main
ZoneScopedN("MM::UpdateStrategies::Sequential::doUpdate::main")
doGraphSequential(engine, main_tasks);
}
{ // post
ZoneScopedN("MM::UpdateStrategies::Sequential::doUpdate::post")
doGraphSequential(engine, post_tasks);
}
{ // simulate async
ZoneScopedN("MM::UpdateStrategies::Sequential::doUpdate::async")
for (size_t i = 0; !_async_queue.empty() && i < _max_async_per_tick; i++) {
_async_queue.back()(engine);
_async_queue.pop_back();
}
}
{
ZoneScopedN("MM::UpdateStrategies::Sequential::doUpdate::deferred")
if (!_deferred_queue.empty()) {
for (auto&& fn : _deferred_queue) {
fn(engine);
}
_deferred_queue.clear();
}
}
}
void Sequential::addDeferred(std::function<void(Engine&)> function) {
_deferred_queue.emplace_back(std::move(function));
}
void Sequential::addAsync(std::function<void(Engine&)> function) {
_async_queue.emplace_back(std::move(function));
}
//#undef __L_ASSERT
} // MM::UpdateStrategies

View File

@ -0,0 +1,63 @@
#pragma once
#include "./update_strategy.hpp"
#include <memory>
#include <unordered_map>
#include <vector>
#include <set>
// HACK: welp
// fwd
namespace MM::Services {
class ImGuiEngineTools;
}
namespace MM::UpdateStrategies {
class Sequential : public MM::UpdateStrategies::UpdateStrategy {
friend MM::Services::ImGuiEngineTools;
private:
std::unordered_map<update_key_t, TaskInfo> _tasks;
// the tasks a service has registered
std::unordered_map<entt::id_type, std::set<update_key_t>> _service_tasks;
std::vector<std::function<void(Engine&)>> _deferred_queue;
std::vector<std::function<void(Engine&)>> _async_queue;
private: // utils
void doGraphSequential(MM::Engine& engine, const std::set<update_key_t>& tasks);
public:
Sequential(void) = default;
virtual ~Sequential(void);
const char* name(void) override { return "Sequential"; }
protected: // engine facing interface
bool enableService(const entt::id_type s_id, std::vector<TaskInfo>&& task_array) override;
bool disableService(const entt::id_type s_id) override;
void doUpdate(MM::Engine& engine) override;
public: // user facing interface
void addDeferred(std::function<void(Engine&)> function) override;
void addAsync(std::function<void(Engine&)> function) override;
size_t _max_async_per_tick = 5; // prevent blocking, this should be finetuned
protected: // engine tools interface
void forEachTask(std::function<bool(TaskInfo&)> fn) override {
for (auto&& t : _tasks) {
if (!fn(t.second)) {
break;
}
}
}
};
} // MM::UpdateStrategies

View File

@ -0,0 +1,73 @@
#pragma once
#include <mm/update_strategies/update_strategy.hpp>
namespace MM::UpdateStrategies {
// all tasks and it dependencies
using Graph = std::unordered_map<update_key_t, std::set<update_key_t>>;
template<typename FN>
void walk_graph_sequential(const Graph& graph, FN&& fn) {
// set of all tasks, each completed task will be removed from this until none are left
std::set<update_key_t> unworked_node_set{};
for (auto it = graph.begin(); it != graph.end(); it++) {
unworked_node_set.emplace(it->first);
}
// TODO: check for inf-loops
while (!unworked_node_set.empty()) {
for (auto node_it = unworked_node_set.begin(); node_it != unworked_node_set.end();) {
// check if deps are resolved
bool resolved = true;
for (const auto dep : graph.at(*node_it)) {
if (unworked_node_set.count(dep)) {
resolved = false;
break;
}
}
if (resolved) {
// to task
fn(*node_it);
node_it = unworked_node_set.erase(node_it);
} else {
node_it++;
}
}
}
}
template<typename TaskGet>
Graph build_task_graph(const std::set<update_key_t>& tasks, TaskGet&& task_get) {
Graph graph;
// phase 1: create all tasks
for (const auto task_id : tasks) {
graph[task_id];
}
// phase 2: do all dependencies (and dependents)
for (const auto task_id : tasks) {
auto& graph_node = graph[task_id];
{ // dependencies
//graph_node.merge(task_get(task_id)._dependencies);
const auto& dependencies = task_get(task_id)._dependencies;
graph_node.insert(dependencies.cbegin(), dependencies.cend());
}
// dependents
for (const auto dependent : task_get(task_id)._dependents) {
if (graph.count(dependent)) {
graph[dependent].emplace(task_id);
}
}
}
return graph;
}
} // MM::UpdateStrategies

View File

@ -2,37 +2,89 @@
#include "../engine_fwd.hpp"
#include <entt/core/hashed_string.hpp>
#include <mm/services/service.hpp>
#include <functional>
#include <string>
#include <vector>
#include <set>
// fwd / hack
namespace MM::Services {
class ImGuiEngineTools;
}
namespace MM::UpdateStrategies {
using update_key_t = entt::id_type;
enum class update_phase_t {
PRE, // for on-main-thread
enum update_phase_t {
PRE = 0, // for on-main-thread
MAIN,
POST // for on-main-thread
};
struct UpdateCreationInfo {
update_key_t key; // key for dependencies
struct TaskInfo {
update_key_t _key; // key for dependencies
std::string _name; // unhashed key
std::string name; // for debugging
update_phase_t _phase = update_phase_t::MAIN;
std::function<void(Engine&)> fn; // the actual payload
update_phase_t phase = update_phase_t::MAIN;
bool auto_enable = true; // whether this update is enabled with the service
// updates we make dependents
std::set<update_key_t> _dependents {};
// this update also depends on (in the same phase)
std::vector<update_key_t> dependencies {};
std::set<update_key_t> _dependencies {};
std::function<void(Engine&)> _fn; // the actual payload
public: // construction and assignment
TaskInfo(void) = delete;
TaskInfo(const TaskInfo&) = default;
TaskInfo(TaskInfo&&) = default;
// TODO: is this right??
TaskInfo& operator=(const TaskInfo&) = default;
TaskInfo& operator=(TaskInfo&&) = default;
explicit TaskInfo(const std::string_view key_in) {
key(key_in);
}
public: // builder interface
// TODO: does this need to be public?
TaskInfo& key(const std::string_view key) {
_name = key;
_key = entt::hashed_string::value(key.data(), key.size());
return *this;
}
// default phase is main
TaskInfo& phase(const update_phase_t phase) {
_phase = phase;
return *this;
}
template<typename Fn>
TaskInfo& fn(Fn&& fn) {
_fn = fn;
return *this;
}
TaskInfo& precede(const std::string_view key) {
_dependents.emplace(entt::hashed_string::value(key.data(), key.size()));
return *this;
}
TaskInfo& succeed(const std::string_view key) {
_dependencies.emplace(entt::hashed_string::value(key.data(), key.size()));
return *this;
}
};
// pure virtual interface for managing the update logic of the engine
// pure virtual interface for managing the update(-task) logic of the engine
class UpdateStrategy {
public:
virtual ~UpdateStrategy(void) {}
@ -42,30 +94,17 @@ class UpdateStrategy {
protected: // the engine facing interface
friend ::MM::Engine;
// TODO: return something?
virtual bool registerService(const entt::id_type s_id, std::vector<UpdateCreationInfo>&& info_array) = 0;
// returns true on success
// failure conditions may include:
// - already en/dis-abled
// - is auto_enable
// - impossible dependencies
virtual bool enableService(const entt::id_type s_id) = 0;
// - impossible dependencies?
virtual bool enableService(const entt::id_type s_id, std::vector<TaskInfo>&& task_array) = 0;
virtual bool disableService(const entt::id_type s_id) = 0;
// runs one update
virtual void doUpdate(MM::Engine& engine) = 0;
public: // the user facing interface
// similar to *ableService, can only be used for non-auto_enable-updates
virtual bool enable(const update_key_t key) = 0;
virtual bool disable(const update_key_t key) = 0;
// add extra dependencies into the tree, the user has the most knowlage about
// the order the services should execute in.
// A -> B (make A depend on B)
virtual bool depend(const update_key_t A, const update_key_t B) = 0;
// WIP:
// dont use, if you are not using it to modify the engine.
@ -77,6 +116,10 @@ class UpdateStrategy {
// note: the US might decide to limit the amount of executed asyncs per tick (eg. when single-threaded)
virtual void addAsync(std::function<void(Engine&)> function) = 0;
//virtual std::future addAsync(std::function<void(Engine&)> function) = 0;
protected: // engine tools inspector
friend ::MM::Services::ImGuiEngineTools;
virtual void forEachTask(std::function<bool(TaskInfo&)> fn) = 0;
};
} // MM::UpdateStrategies