Commit f9699ca1 authored by Florian Oetke's avatar Florian Oetke
Browse files

fixed ecs/pool (fixes #35)

parent b154b2f2
Pipeline #2611 failed with stage
in 1 minute and 30 seconds
......@@ -210,7 +210,7 @@ namespace mirrage::ecs {
struct Pool_storage_policy_sort {
static constexpr bool sorted = false;
};
/*
// FIXME: sorted pool returns/erases the wrong values under heavy contention
template <class T>
struct Pool_storage_policy_sort<T, util::void_t<decltype(T::sort_key), decltype(T::sort_key_index)>> {
......@@ -218,7 +218,7 @@ namespace mirrage::ecs {
static constexpr auto sort_key = T::sort_key;
static constexpr auto sort_key_constructor_idx = T::sort_key_index;
};
*/
template <class T, std::size_t Holes>
struct Pool_storage_policy_value_traits : Pool_storage_policy_sort<T> {
static constexpr int_fast32_t max_free = Holes;
......@@ -364,27 +364,35 @@ namespace mirrage::ecs {
protected:
void restore(Entity_handle owner, Deserializer& deserializer) override
{
auto entity_id = get_entity_id(owner, _manager);
if(entity_id == invalid_entity_id) {
MIRRAGE_FAIL("emplace_or_find_now of component from invalid/deleted entity");
}
auto& comp = [&]() -> T& {
auto comp_idx = _index.find(entity_id);
if(comp_idx.is_some()) {
return _storage.get(comp_idx.get_or_throw());
if constexpr(std::is_constructible_v<T, Entity_handle, Entity_manager&>) {
auto entity_id = get_entity_id(owner, _manager);
if(entity_id == invalid_entity_id) {
MIRRAGE_FAIL("emplace_or_find_now of component from invalid/deleted entity");
}
auto relocator = [&](auto, auto& comp, auto new_idx) {
_index.attach(comp.owner_handle().id(), new_idx);
};
auto& comp = [&]() -> T& {
auto comp_idx = _index.find(entity_id);
if(comp_idx.is_some()) {
return _storage.get(comp_idx.get_or_throw());
}
auto comp = _storage.emplace(relocator, owner, _manager);
_index.attach(entity_id, std::get<1>(comp));
return std::get<0>(comp);
}();
auto relocator = [&](auto, auto& comp, auto new_idx) {
_index.attach(comp.owner_handle().id(), new_idx);
};
load_component(deserializer, comp);
auto comp = _storage.emplace(relocator, owner, _manager);
_index.attach(entity_id, std::get<1>(comp));
return std::get<0>(comp);
}();
load_component(deserializer, comp);
} else {
(void) owner;
(void) deserializer;
MIRRAGE_FAIL("Tried to load component " << T::name()
<< " that has no two-argument constructor!");
}
}
bool save(Entity_handle owner, Serializer& serializer) override
......@@ -416,10 +424,10 @@ namespace mirrage::ecs {
if(_unoptimized_deletes > 32) {
_unoptimized_deletes = 0;
_index.shrink_to_fit();
_storage.shrink_to_fit([&](auto, auto& comp, auto new_idx) {
_index.attach(comp.owner_handle().id(), new_idx);
});
_index.shrink_to_fit();
}
}
......@@ -448,24 +456,11 @@ namespace mirrage::ecs {
auto comp_idx = comp_idx_mb.get_or_throw();
_index.detach(entity_id);
Insertion insertion;
if(_queued_insertions.try_dequeue(insertion)) {
auto entity_id = get_entity_id(std::get<1>(insertion), _manager);
if(entity_id == invalid_entity_id) {
_storage.erase(comp_idx, [&](auto, auto& comp, auto new_idx) {
_index.attach(comp.owner_handle().id(), new_idx);
});
} else {
_storage.replace(comp_idx, std::move(std::get<0>(insertion)));
_index.attach(std::get<1>(insertion).id(), comp_idx);
}
} else {
_storage.erase(comp_idx, [&](auto, auto& comp, auto new_idx) {
auto entity_id = get_entity_id(comp.owner_handle(), _manager);
_index.attach(entity_id, new_idx);
});
_unoptimized_deletes++;
}
_storage.erase(comp_idx, [&](auto, auto& comp, auto new_idx) {
auto entity_id = get_entity_id(comp.owner_handle(), _manager);
_index.attach(entity_id, new_idx);
});
_unoptimized_deletes++;
}
} else {
break;
......
......@@ -44,25 +44,25 @@ namespace mirrage::ecs {
constexpr packed_t pack() const noexcept { return _data; }
static constexpr Entity_handle unpack(packed_t d) noexcept { return Entity_handle{d}; }
constexpr friend bool operator==(const Entity_handle& lhs, const Entity_handle& rhs) noexcept
{
return lhs._data == rhs._data;
}
constexpr friend bool operator!=(const Entity_handle& lhs, const Entity_handle& rhs) noexcept
{
return lhs._data != rhs._data;
}
constexpr inline friend bool operator<(const Entity_handle& lhs, const Entity_handle& rhs) noexcept
{
return lhs._data < rhs._data;
}
private:
uint32_t _data;
constexpr Entity_handle(uint32_t data) : _data(data) {}
};
constexpr inline bool operator==(const Entity_handle& lhs, const Entity_handle& rhs) noexcept
{
return lhs.id() == rhs.id() && lhs.revision() == rhs.revision();
}
constexpr inline bool operator!=(const Entity_handle& lhs, const Entity_handle& rhs) noexcept
{
return lhs.id() != rhs.id() || lhs.revision() != rhs.revision();
}
inline bool operator<(const Entity_handle& lhs, const Entity_handle& rhs) noexcept
{
return std::make_tuple(lhs.id(), lhs.revision()) < std::make_tuple(rhs.id(), rhs.revision());
}
static_assert(sizeof(Entity_handle::packed_t) <= sizeof(void*),
"what the hell is wrong with your plattform?!");
......@@ -109,12 +109,10 @@ namespace mirrage::ecs {
}
} while(!success && expected_rev == cas);
if(success)
if(success) {
return h;
else if(tries++ >= 4) {
LOG(plog::warning) << "My handle got stolen: expected="
<< int(h.revision() | Entity_handle::free_rev)
<< ", found=" << int(expected_rev) << ", rev=" << int(rev.load());
} else if(tries++ >= 20) {
LOG(plog::error) << "Too many handle tries (" << tries << ") giving up!";
break;
}
}
......@@ -140,7 +138,7 @@ namespace mirrage::ecs {
auto valid(Entity_handle h) const noexcept -> bool
{
return h
&& (static_cast<Entity_id>(_slots.size()) > h.id() - 1
&& (static_cast<std::size_t>(h.id() - 1) >= _slots.size()
|| util::at(_slots, static_cast<std::size_t>(h.id() - 1)) == h.revision());
}
......
......@@ -43,6 +43,7 @@ namespace mirrage::ecs {
class Entity_manager {
public:
Entity_manager(asset::Asset_manager&, util::any_ptr userdata);
~Entity_manager();
// user interface; thread-safe
auto emplace() noexcept -> Entity_facet;
......
......@@ -55,6 +55,7 @@ namespace mirrage::ecs {
extern Component_type blueprint_comp_id;
extern void init_serializer(Entity_manager&);
extern void deinit_serializer(Entity_manager&);
extern void apply_blueprint(asset::Asset_manager&, Entity_facet e, const std::string& blueprint);
......
......@@ -19,9 +19,9 @@ namespace mirrage::ecs {
Entity_manager::Entity_manager(asset::Asset_manager& assets, util::any_ptr ud)
: _assets(assets), _userdata(ud)
{
init_serializer(*this);
}
Entity_manager::~Entity_manager() { deinit_serializer(*this); }
Entity_facet Entity_manager::emplace() noexcept { return {*this, _handles.get_new()}; }
Entity_facet Entity_manager::emplace(const std::string& blueprint)
......
......@@ -21,6 +21,8 @@ namespace mirrage::ecs {
const std::string import_key = "$import";
void apply(const Blueprint& b, Entity_facet e);
// used for hot-reloading (TODO: better ideas???)
std::vector<Entity_manager*> entity_managers;
class Blueprint {
public:
......@@ -30,16 +32,13 @@ namespace mirrage::ecs {
~Blueprint() noexcept;
Blueprint& operator=(Blueprint&&) noexcept;
void detach(Entity_handle target) const;
void on_reload();
mutable std::vector<std::tuple<Entity_handle, Entity_manager*>> users;
mutable std::vector<Blueprint*> children;
std::string id;
std::string content;
asset::Ptr<Blueprint> parent;
asset::Asset_manager* asset_mgr;
mutable Entity_manager* entity_manager = nullptr;
mutable std::vector<Blueprint*> children;
std::string id;
std::string content;
asset::Ptr<Blueprint> parent;
asset::Asset_manager* asset_mgr;
};
......@@ -67,9 +66,7 @@ namespace mirrage::ecs {
, content(std::move(rhs.content))
, parent(std::move(rhs.parent))
, asset_mgr(rhs.asset_mgr)
, entity_manager(rhs.entity_manager)
{
if(parent) {
util::erase_fast(parent->children, &rhs);
parent->children.push_back(this);
......@@ -86,7 +83,7 @@ namespace mirrage::ecs {
Blueprint& Blueprint::operator=(Blueprint&& o) noexcept
{
// swap data but keep user-list
id = o.id;
id = std::move(o.id);
content = std::move(o.content);
if(parent) {
util::erase_fast(parent->children, this);
......@@ -103,26 +100,6 @@ namespace mirrage::ecs {
return *this;
}
void Blueprint::on_reload()
{
for(auto&& c : children) {
c->on_reload();
}
for(auto&& [u, manager] : users) {
auto facet = manager->get(u);
if(facet.is_some()) {
apply(*this, facet.get_or_throw());
} else {
LOG(plog::error) << "dead entity in blueprint.";
}
}
}
void Blueprint::detach(Entity_handle target) const
{
util::erase_if(users, [&](auto& e) { return std::get<0>(e) == target; });
}
} // namespace
} // namespace mirrage::ecs
......@@ -157,22 +134,9 @@ namespace mirrage::ecs {
}
Blueprint_component(Blueprint_component&&) noexcept = default;
Blueprint_component& operator=(Blueprint_component&&) = default;
~Blueprint_component()
{
if(blueprint) {
blueprint->detach(owner_handle());
blueprint.reset();
}
}
~Blueprint_component() {}
void set(asset::Ptr<Blueprint> blueprint)
{
if(this->blueprint) {
this->blueprint->detach(owner_handle());
}
this->blueprint = std::move(blueprint);
}
void set(asset::Ptr<Blueprint> blueprint) { this->blueprint = std::move(blueprint); }
auto& manager() { return *_manager; }
......@@ -192,8 +156,6 @@ namespace mirrage::ecs {
auto owner = comp.owner(comp.manager())
.get_or_throw("instanciated Blueprint_component on dead entity");
blueprint->users.emplace_back(owner, &comp.manager());
blueprint->entity_manager = &comp.manager();
apply(*blueprint, owner);
}
......@@ -226,6 +188,20 @@ namespace mirrage::ecs {
<< column << ": " << msg;
};
}
void Blueprint::on_reload()
{
for(auto&& c : children) {
c->on_reload();
}
for(auto entity_manager : entity_managers) {
for(auto&& [owner, b] : entity_manager->list<Entity_facet, Blueprint_component>()) {
(void) b;
apply(*this, owner);
}
}
}
} // namespace
......@@ -244,7 +220,12 @@ namespace mirrage::ecs {
{
}
void init_serializer(Entity_manager& ecs) { ecs.register_component_type<Blueprint_component>(); }
void init_serializer(Entity_manager& ecs)
{
ecs.register_component_type<Blueprint_component>();
entity_managers.emplace_back(&ecs);
}
void deinit_serializer(Entity_manager& ecs) { util::erase_fast(entity_managers, &ecs); }
Component_type blueprint_comp_id = component_type_id<Blueprint_component>();
......@@ -263,8 +244,6 @@ namespace mirrage::ecs {
else
e.get<Blueprint_component>().get_or_throw().set(b);
b->users.emplace_back(e.handle(), &e.manager());
apply(*b, e);
}
......
......@@ -101,7 +101,7 @@ namespace mirrage::util {
/// The behaviour is undefined if i is not a valid index.
/// Complexity: O(1) for spare or unsorted pools and O(N) else
template <typename F>
void erase(IndexType i, F&& relocation, bool leave_holes = true);
void erase(IndexType i, F&& relocation);
/// Tries to compact the elements and free unused memory.
/// Complexity: O(N) for sparse pools and O(1) else
......@@ -145,9 +145,18 @@ namespace mirrage::util {
/// Moves the range [src, src+count) to [dst, dst+count), calling on_relocate accordingly
/// The behaviour is undefined if any of the ranges contains an empty/invalid value!
/// If last_empty is true the last element in the destination range has to be empty.
/// The non-overlapping parts of the src range will be in the moved-from state afterwards.
template <typename F>
void _move_elements(index_t src, index_t dst, F&& on_relocate, index_t count = 1);
void _move_elements(
index_t src, index_t dst, F&& on_relocate, index_t count = 1, bool last_empty = false);
/// Moves the range [src, src+count) to [dst, dst+count), calling on_relocate accordingly
/// The behaviour is undefined if the two ranges overlap!
/// The behaviour is undefined if the src ranges contains an empty/invalid value!
/// The behaviour is undefined if the dst ranges contains any intitialized/valid values!
template <typename F>
void _move_elements_uninitialized(index_t src, index_t dst, F&& on_relocate, index_t count = 1);
void _pop_back();
};
......@@ -256,7 +265,7 @@ namespace mirrage::util {
#define MIRRAGE_UTIL_POOL_INCLUDED
#include "pool.hxx"
/*
namespace mirrage::util::tests {
struct accessor {
std::size_t chunk_count;
......@@ -393,3 +402,4 @@ namespace mirrage::util::tests {
}
}
} // namespace mirrage::util::tests
*/
......@@ -62,7 +62,7 @@ namespace mirrage::util {
MIRRAGE_POOL_HEADER
template <typename F>
void MIRRAGE_POOL::erase(IndexType i, F&& relocation, bool leave_holes)
void MIRRAGE_POOL::erase(IndexType i, F&& relocation)
{
MIRRAGE_INVARIANT(i < _used_elements, "erase is out of range: " << i << ">=" << _used_elements);
......@@ -71,19 +71,13 @@ namespace mirrage::util {
} else {
if constexpr(max_free_slots > 0) {
if(leave_holes) {
// empty slot allowed => leave a hole
auto& e = get(i);
e.~T();
std::memset(reinterpret_cast<char*>(&e), 0, sizeof(T));
_freelist.insert(i);
return;
}
}
if constexpr(ValueTraits::sorted) {
// TODO[optimization]: _move_elements could be called with a larger range
// empty slot allowed => leave a hole
auto& e = get(i);
e.~T();
std::memset(reinterpret_cast<char*>(&e), 0, sizeof(T));
_freelist.insert(i);
} else if constexpr(ValueTraits::sorted) {
// shift all later elements and delete the last (now empty)
_move_elements(i + 1, i, relocation, _used_elements - i - 1);
_pop_back();
......@@ -124,7 +118,7 @@ namespace mirrage::util {
auto block_size = next_free - read_position;
// move block of non-free slots to insert_position
_move_elements(read_position, write_position, relocation, block_size);
_move_elements_uninitialized(read_position, write_position, relocation, block_size);
read_position += block_size;
write_position += block_size;
}
......@@ -132,9 +126,30 @@ namespace mirrage::util {
_used_elements = write_position;
} else {
for(auto i : range_reverse(_freelist)) {
erase(i, relocation, false);
auto last_used_idx = std::int64_t(_used_elements) - 1;
auto next_free = std::int64_t(_freelist.size()) - 1;
for(auto to_fill : util::range(std::int64_t(_freelist.size()))) {
for(; next_free > to_fill; next_free--) {
if(_freelist[next_free] == last_used_idx)
last_used_idx--;
else if(_freelist[next_free] < last_used_idx)
break;
}
if(last_used_idx < 0 || next_free <= to_fill)
break;
auto& src = get(last_used_idx);
auto addr = new(_get_raw(_freelist[to_fill])) T(std::move(src));
src.~T();
std::memset(reinterpret_cast<char*>(&src), 0, sizeof(T));
relocation(last_used_idx, *addr, _freelist[to_fill]);
last_used_idx--;
}
_used_elements -= _freelist.size();
}
_freelist.clear();
......@@ -143,7 +158,8 @@ namespace mirrage::util {
// free unused chunks
auto min_chunks = std::ceil(static_cast<float>(_used_elements) / chunk_len);
_chunks.resize(static_cast<std::size_t>(min_chunks));
_chunks.resize(
util::max(static_cast<std::size_t>(min_chunks), util::min(_chunks.size(), std::size_t(1))));
}
MIRRAGE_POOL_HEADER
......@@ -181,6 +197,7 @@ namespace mirrage::util {
}
}();
// find insert position
auto iter = std::lower_bound(begin(), end(), sort_key, [](auto& lhs, auto& rhs) {
return lhs.*(ValueTraits::sort_key) < rhs;
......@@ -194,7 +211,7 @@ namespace mirrage::util {
// find first free slot
auto first_empty = [&] {
if constexpr(max_free_slots > 0) {
auto min = std::lower_bound(_freelist.begin(), _freelist.end(), i - 1);
auto min = std::lower_bound(_freelist.begin(), _freelist.end(), i);
if(min != _freelist.end()) {
auto min_v = *min;
_freelist.erase(min);
......@@ -209,20 +226,33 @@ namespace mirrage::util {
return _used_elements - 1;
}();
if(first_empty < i)
i = first_empty;
MIRRAGE_INVARIANT(first_empty >= i, "first_empty (" << first_empty << ") < i (" << i << ")");
if(first_empty > i) {
new(_get_raw(first_empty)) T(std::move(get(first_empty - 1)));
// shift to make room for new element
if(first_empty - i > 1) {
_move_elements(i, i + 1, relocation, first_empty - i - 1);
}
_move_elements(i, i + 1, relocation, first_empty - i, true);
iter->~T();
}
// create new element
auto instance = new(addr(i)) T(std::forward<Args>(args)...);
#ifdef MIRRAGE_SLOW_INVARIANTS
for(auto& e : *this) {
MIRRAGE_INVARIANT(e.*(ValueTraits::sort_key), "invalid key");
}
MIRRAGE_INVARIANT(std::is_sorted(begin(),
end(),
[](auto& lhs, auto& rhs) {
return lhs.*(ValueTraits::sort_key)
< rhs.*(ValueTraits::sort_key);
}),
"pool is not sorted anymore");
#endif
return {*instance, i};
} else {
// insert at the end
_used_elements++;
......@@ -243,6 +273,15 @@ namespace mirrage::util {
(void) instance2;
MIRRAGE_INVARIANT(!ValueTraits::sorted
|| std::is_sorted(begin(),
end(),
[](auto& lhs, auto& rhs) {
return lhs.*(ValueTraits::sort_key)
< rhs.*(ValueTraits::sort_key);
}),
"pool is not sorted anymore");
return {*instance, i};
}
......@@ -277,18 +316,34 @@ namespace mirrage::util {
MIRRAGE_POOL_HEADER
template <typename F>
void MIRRAGE_POOL::_move_elements(index_t src, index_t dst, F&& on_relocate, index_t count)
void MIRRAGE_POOL::_move_elements(
const index_t src, const index_t dst, F&& on_relocate, const index_t count, const bool last_empty)
{
MIRRAGE_INVARIANT(src != dst, "_move_elements with src==dst");
(void) last_empty;
if constexpr(!std::is_trivially_copyable_v<T>) {
if(dst > src && last_empty) {
new(_get_raw(dst + count - 1)) T(std::move(get(src + count - 1)));
if(count > 1)
_move_elements(src, dst, on_relocate, count - 1, false);
on_relocate(src + count - 1, get(dst + count - 1), dst + count - 1);
return;
}
}
auto c_src = src;
auto c_dst = dst;
while(c_src - src < count) {
auto step = util::min(count, chunk_len - c_src % chunk_len, chunk_len - c_dst % chunk_len);
if constexpr(std::is_trivially_copyable_v<T>) {
// yay, we can memmove
std::memmove(_get_raw(c_dst), _get_raw(c_src), std::size_t(step));
std::memmove(_get_raw(c_dst), _get_raw(c_src), std::size_t(step) * sizeof(T));
} else {
// nay, have to check if valid and call move-assignment / placement-new
if(c_dst > c_src) {
// nay, we hava to move the objects
if(dst > src) {
std::move_backward(&get(c_src), &get(c_src) + step, &get(c_dst) + step);
} else {
std::move(&get(c_src), &get(c_src) + step, &get(c_dst));
......@@ -303,6 +358,37 @@ namespace mirrage::util {