Remove component bitsets from ecs; use uuid hash tables instead

This commit is contained in:
Nikita Lisitsa 2023-08-24 17:33:19 +03:00
parent 809a0ec212
commit 31ffd4dc54
10 changed files with 202 additions and 124 deletions

View file

@ -0,0 +1,18 @@
#pragma once
#include <psemek/util/uuid.hpp>
#include <psemek/util/span.hpp>
#include <psemek/util/hash.hpp>
namespace psemek::ecs::detail
{
inline std::size_t component_hash(util::span<util::uuid const> const & uuids)
{
std::size_t result = 0xcd5694d2b3f3443eull;
for (auto const & uuid : uuids)
result ^= std::hash<util::uuid>{}(uuid);
return result;
}
}

View file

@ -1,53 +0,0 @@
#pragma once
#include <psemek/ecs/detail/component_mask.hpp>
#include <psemek/util/uuid.hpp>
#include <psemek/util/span.hpp>
#include <psemek/util/unique_sequential_storage.hpp>
namespace psemek::ecs::detail
{
template <typename ... Components>
struct component_uuid_holder
{
util::uuid uuids[sizeof...(Components)] { Components::uuid() ... };
auto get() const
{
return util::span<util::uuid const>(uuids);
}
};
template <>
struct component_uuid_holder<>
{
auto get() const
{
return util::span<util::uuid const>();
}
};
struct component_index
{
template <typename ... UUIDS>
component_mask make_component_mask(UUIDS const & ... uuids)
{
component_mask result;
(result.set(storage_.insert(uuids), true), ...);
return result;
}
component_mask make_component_mask(util::span<util::uuid const> uuids)
{
component_mask result;
for (util::uuid const & uuid : uuids)
result.set(storage_.insert(uuid), true);
return result;
}
private:
util::unique_sequential_storage<util::uuid> storage_;
};
}

View file

@ -1,10 +0,0 @@
#pragma once
#include <psemek/util/dynamic_bitset.hpp>
namespace psemek::ecs::detail
{
using component_mask = util::dynamic_bitset;
}

View file

@ -0,0 +1,29 @@
#pragma once
#include <psemek/util/uuid.hpp>
#include <psemek/util/span.hpp>
namespace psemek::ecs::detail
{
template <typename ... Components>
struct component_uuid_helper
{
util::uuid uuids[sizeof...(Components)] { Components::uuid() ... };
auto get() const
{
return util::span<util::uuid const>(uuids);
}
};
template <>
struct component_uuid_helper<>
{
auto get() const
{
return util::span<util::uuid const>();
}
};
}

View file

@ -1,50 +1,104 @@
#pragma once #pragma once
#include <psemek/ecs/detail/query_cache.hpp> #include <psemek/ecs/detail/query_cache.hpp>
#include <psemek/ecs/detail/component_mask.hpp> #include <psemek/ecs/detail/component_hash.hpp>
#include <psemek/util/hash_table.hpp>
#include <memory> #include <memory>
namespace psemek::ecs::detail namespace psemek::ecs::detail
{ {
struct query_cache_set
{
std::size_t hash;
util::hash_set<util::uuid> uuids;
std::vector<std::weak_ptr<query_cache>> caches;
};
struct query_cache_hash
{
std::size_t operator()(util::span<util::uuid const> const & uuids) const
{
return component_hash(uuids);
}
std::size_t operator()(std::unique_ptr<query_cache_set> const & set) const
{
return set->hash;
}
};
struct query_cache_equal
{
bool operator()(util::span<util::uuid const> const & uuids, std::unique_ptr<query_cache_set> const & set) const
{
if (uuids.size() != set->uuids.size())
return false;
for (auto const & uuid : uuids)
if (!set->uuids.contains(uuid))
return false;
return true;
}
bool operator()(std::unique_ptr<query_cache_set> const & set1, std::unique_ptr<query_cache_set> const & set2) const
{
return set1.get() == set2.get();
}
};
// TODO: store query caches in a bitmask trie balanced by subtree size // TODO: store query caches in a bitmask trie balanced by subtree size
struct query_cache_container struct query_cache_container
{ {
std::shared_ptr<query_cache> create(component_mask const & mask, util::span<util::uuid const> component_uuids) std::shared_ptr<query_cache> create(util::span<util::uuid const> component_uuids)
{ {
auto result = std::make_shared<query_cache>(); auto result = std::make_shared<query_cache>();
result->component_uuids.assign(component_uuids.begin(), component_uuids.end()); result->component_uuids.assign(component_uuids.begin(), component_uuids.end());
get(mask).emplace_back(result); auto it = caches_.find(component_uuids);
if (it == caches_.end())
{
auto value = std::make_unique<query_cache_set>();
for (auto const & uuid : component_uuids)
value->uuids.insert(uuid);
value->hash = component_hash(component_uuids);
it = caches_.insert(std::move(value)).first;
}
it->get()->caches.push_back(result);
return result; return result;
} }
template <typename Function> template <typename Function, typename ContainsUUID>
void apply(Function && function, component_mask const & mask) void apply(Function && function, ContainsUUID && contains_uuid)
{ {
for (auto & caches : caches_) for (auto & cache_set : caches_)
{ {
if (!util::is_subset(caches.first, mask)) continue; bool good = true;
filter(caches.second); for (auto const & uuid : cache_set->uuids)
for (auto & cache : caches.second) if (!contains_uuid(uuid))
{
good = false;
break;
}
if (!good)
continue;
filter(cache_set->caches);
for (auto & cache : cache_set->caches)
function(*cache.lock()); function(*cache.lock());
} }
} }
private: private:
std::unordered_map<component_mask, std::vector<std::weak_ptr<query_cache>>> caches_; util::hash_set<std::unique_ptr<query_cache_set>, query_cache_hash, query_cache_equal> caches_;
static void filter(std::vector<std::weak_ptr<query_cache>> & caches) static void filter(std::vector<std::weak_ptr<query_cache>> & caches)
{ {
caches.erase(std::remove_if(caches.begin(), caches.end(), [](std::weak_ptr<query_cache> const & weak){ return !weak.lock(); }), caches.end()); caches.erase(std::remove_if(caches.begin(), caches.end(), [](std::weak_ptr<query_cache> const & weak){ return !weak.lock(); }), caches.end());
} }
std::vector<std::weak_ptr<query_cache>> & get(component_mask const & mask)
{
auto & result = caches_[mask];
filter(result);
return result;
}
}; };
} }

View file

@ -2,6 +2,7 @@
#include <psemek/ecs/entity_handle.hpp> #include <psemek/ecs/entity_handle.hpp>
#include <psemek/ecs/detail/stride.hpp> #include <psemek/ecs/detail/stride.hpp>
#include <psemek/ecs/detail/component_hash.hpp>
#include <psemek/util/uuid.hpp> #include <psemek/util/uuid.hpp>
#include <psemek/util/span.hpp> #include <psemek/util/span.hpp>
#include <psemek/util/hash_table.hpp> #include <psemek/util/hash_table.hpp>
@ -28,31 +29,31 @@ namespace psemek::ecs::detail
std::size_t current_row = 0; std::size_t current_row = 0;
}; };
util::span<util::uuid const> get_component_uuids() const std::size_t hash() const
{ {
return component_uuids_; return hash_;
} }
std::optional<std::size_t> get_component_column(util::uuid const & uuid) const std::optional<std::size_t> component_column(util::uuid const & uuid) const
{ {
if (auto it = component_uuid_to_column_.find(uuid); it != component_uuid_to_column_.end()) if (auto it = component_uuid_to_column_.find(uuid); it != component_uuid_to_column_.end())
return it->second; return it->second;
return std::nullopt; return std::nullopt;
} }
util::span<component_pointer const> get_component_pointers() const util::span<component_pointer const> component_pointers() const
{ {
return component_pointers_; return component_pointers_;
} }
util::span<entity_handle const> get_entity_handles() const util::span<entity_handle const> entity_handles() const
{ {
return entity_handles_; return entity_handles_;
} }
std::size_t component_count() const std::size_t component_count() const
{ {
return component_uuids_.size(); return component_pointers_.size();
} }
std::size_t row_count() const std::size_t row_count() const
@ -93,6 +94,7 @@ namespace psemek::ecs::detail
virtual ~table() = default; virtual ~table() = default;
protected: protected:
std::size_t hash_;
std::vector<util::uuid> component_uuids_; std::vector<util::uuid> component_uuids_;
util::hash_map<util::uuid, std::size_t> component_uuid_to_column_; util::hash_map<util::uuid, std::size_t> component_uuid_to_column_;
std::vector<component_pointer> component_pointers_; std::vector<component_pointer> component_pointers_;
@ -137,10 +139,11 @@ namespace psemek::ecs::detail
table_impl<Components...>::table_impl(util::span<util::uuid const> component_uuids) table_impl<Components...>::table_impl(util::span<util::uuid const> component_uuids)
{ {
assert(sizeof...(Components) == component_uuids.size()); assert(sizeof...(Components) == component_uuids.size());
hash_ = component_hash(component_uuids);
component_uuids_.assign(component_uuids.begin(), component_uuids.end()); component_uuids_.assign(component_uuids.begin(), component_uuids.end());
component_pointers_.resize(sizeof...(Components)); component_pointers_.resize(sizeof...(Components));
for (std::size_t i = 0; i < component_uuids_.size(); ++i) for (std::size_t i = 0; i < component_uuids.size(); ++i)
component_uuid_to_column_.insert({component_uuids_[i], i}); component_uuid_to_column_.insert({component_uuids[i], i});
} }
template <typename ... Components> template <typename ... Components>

View file

@ -1,46 +1,85 @@
#pragma once #pragma once
#include <psemek/ecs/detail/component_mask.hpp> #include <psemek/ecs/detail/component_hash.hpp>
#include <psemek/ecs/detail/table.hpp> #include <psemek/ecs/detail/table.hpp>
#include <psemek/util/hash_table.hpp>
#include <vector> #include <vector>
namespace psemek::ecs::detail namespace psemek::ecs::detail
{ {
struct table_hashset_hash
{
std::size_t operator()(util::span<util::uuid const> const & uuids) const
{
return component_hash(uuids);
}
std::size_t operator()(std::unique_ptr<table> const & table) const
{
return table->hash();
}
};
struct table_hashset_equal
{
bool operator()(util::span<util::uuid const> const & uuids, std::unique_ptr<table> const & table) const
{
if (uuids.size() != table->component_count())
return false;
for (auto const & uuid : uuids)
if (!table->component_column(uuid))
return false;
return true;
}
bool operator()(std::unique_ptr<table> const & table1, std::unique_ptr<table> const & table2) const
{
return table1.get() == table2.get();
}
};
// TODO: store tables in a bitmask trie balanced by subtree size // TODO: store tables in a bitmask trie balanced by subtree size
struct table_container struct table_container
{ {
template <typename ... Components> template <typename ... Components>
std::pair<table *, bool> insert(component_mask const & mask, util::span<util::uuid const> component_uuids); std::pair<table *, bool> insert(util::span<util::uuid const> component_uuids);
template <typename Function> template <typename Function>
void apply(Function && function, component_mask const & mask); void apply(Function && function, util::span<util::uuid const> component_uuids);
private: private:
std::unordered_map<component_mask, std::unique_ptr<table>> tables_; util::hash_set<std::unique_ptr<table>, table_hashset_hash, table_hashset_equal> tables_;
}; };
template <typename ... Components> template <typename ... Components>
std::pair<table *, bool> table_container::insert(component_mask const & mask, util::span<util::uuid const> component_uuids) std::pair<table *, bool> table_container::insert(util::span<util::uuid const> component_uuids)
{ {
auto & result = tables_[mask]; auto it = tables_.find(component_uuids);
bool created = false; if (it != tables_.end())
if (!result) return {it->get(), false};
{
result = std::make_unique<table_impl<Components...>>(component_uuids); auto table = std::make_unique<table_impl<Components...>>(component_uuids);
created = true; auto result = table.get();
} tables_.insert(std::move(table));
return {result.get(), created}; return {result, true};
} }
template <typename Function> template <typename Function>
void table_container::apply(Function && function, component_mask const & mask) void table_container::apply(Function && function, util::span<util::uuid const> component_uuids)
{ {
for (auto & table : tables_) for (auto & table : tables_)
{ {
if (!util::is_subset(mask, table.first)) continue; bool good = true;
function(*table.second); for (auto const & uuid : component_uuids)
if (!table->component_column(uuid))
{
good = false;
break;
}
if (good) function(*table);
} }
} }

View file

@ -1,11 +1,11 @@
#pragma once #pragma once
#include <psemek/ecs/detail/component_index.hpp>
#include <psemek/ecs/detail/entity_list.hpp> #include <psemek/ecs/detail/entity_list.hpp>
#include <psemek/ecs/detail/table_container.hpp> #include <psemek/ecs/detail/table_container.hpp>
#include <psemek/ecs/detail/query_cache_container.hpp> #include <psemek/ecs/detail/query_cache_container.hpp>
#include <psemek/ecs/detail/apply_helper.hpp> #include <psemek/ecs/detail/apply_helper.hpp>
#include <psemek/ecs/detail/all_different_types.hpp> #include <psemek/ecs/detail/all_different_types.hpp>
#include <psemek/ecs/detail/component_uuid_helper.hpp>
#include <psemek/util/span.hpp> #include <psemek/util/span.hpp>
#include <psemek/util/range.hpp> #include <psemek/util/range.hpp>
#include <psemek/util/exception.hpp> #include <psemek/util/exception.hpp>
@ -164,7 +164,7 @@ namespace psemek::ecs
* types, the behavior is undefined. * types, the behavior is undefined.
* If the function accesses passed components after destroying the * If the function accesses passed components after destroying the
* currently visited entity, the behavior is undefined. * currently visited entity, the behavior is undefined.
* If the function recursively calls `apply()`, the behavior is undefined. * If the function recursively calls `apply()` or `batch_apply()`, the behavior is undefined.
*/ */
template <typename ... Components, typename Function> template <typename ... Components, typename Function>
void apply(Function && function, query_cache cache = {}); void apply(Function && function, query_cache cache = {});
@ -182,13 +182,13 @@ namespace psemek::ecs
* If the query cache wasn't created with the exact sequence of component * If the query cache wasn't created with the exact sequence of component
* types, the behavior is undefined. * types, the behavior is undefined.
* If the function creates or destroyes entities, the behavior is undefined. * If the function creates or destroyes entities, the behavior is undefined.
* If the function recursively calls `apply()` or `batch_apply()`, the behavior is undefined.
*/ */
template <typename ... Components, typename Function> template <typename ... Components, typename Function>
void batch_apply(Function && function, query_cache cache = {}); void batch_apply(Function && function, query_cache cache = {});
private: private:
detail::entity_list entity_list_; detail::entity_list entity_list_;
mutable detail::component_index component_index_;
detail::table_container table_container_; detail::table_container table_container_;
detail::query_cache_container query_cache_container_; detail::query_cache_container query_cache_container_;
@ -200,10 +200,9 @@ namespace psemek::ecs
{ {
static_assert(detail::all_different_types_v<Components...>, "all component types must be different"); static_assert(detail::all_different_types_v<Components...>, "all component types must be different");
detail::component_uuid_holder<Components...> uuids; detail::component_uuid_helper<Components...> uuids;
detail::component_mask mask = component_index_.make_component_mask(uuids.get());
auto insert_result = table_container_.insert<Components...>(mask, uuids.get()); auto insert_result = table_container_.insert<Components...>(uuids.get());
auto table = insert_result.first; auto table = insert_result.first;
bool created = insert_result.second; bool created = insert_result.second;
@ -211,7 +210,7 @@ namespace psemek::ecs
{ {
query_cache_container_.apply([table](detail::query_cache & cache){ query_cache_container_.apply([table](detail::query_cache & cache){
cache.add(table); cache.add(table);
}, mask); }, [table](util::uuid const & uuid){ return table->component_column(uuid) != std::nullopt; });
} }
if (table->get_iteration_data()) if (table->get_iteration_data())
@ -230,14 +229,13 @@ namespace psemek::ecs
template <typename ... Components> template <typename ... Components>
query_cache entity_container::cache() query_cache entity_container::cache()
{ {
detail::component_uuid_holder<Components...> uuids; detail::component_uuid_helper<Components...> uuids;
detail::component_mask mask = component_index_.make_component_mask(uuids.get());
auto result = query_cache_container_.create(mask, uuids.get()); auto result = query_cache_container_.create(uuids.get());
table_container_.apply([&](detail::table & table){ table_container_.apply([&](detail::table & table){
result->add(&table); result->add(&table);
}, mask); }, uuids.get());
return result; return result;
} }
@ -252,10 +250,10 @@ namespace psemek::ecs
{ {
auto & iteration_data = entry.table->get_iteration_data(); auto & iteration_data = entry.table->get_iteration_data();
iteration_data.emplace(); iteration_data.emplace();
detail::static_apply_helper<Components...> apply_helper(*this, entry.table->get_entity_handles()); detail::static_apply_helper<Components...> apply_helper(*this, entry.table->entity_handles());
for (std::size_t i = 0; i < sizeof...(Components); ++i) for (std::size_t i = 0; i < sizeof...(Components); ++i)
apply_helper.pointers[i] = entry.table->get_component_pointers()[entry.column_ids[i]]; apply_helper.pointers[i] = entry.table->component_pointers()[entry.column_ids[i]];
for (std::size_t i = 0; i < entry.table->row_count(); ++i) for (std::size_t i = 0; i < entry.table->row_count(); ++i)
{ {
@ -290,10 +288,10 @@ namespace psemek::ecs
for (auto const & entry : cache->tables) for (auto const & entry : cache->tables)
{ {
detail::static_apply_helper<Components...> apply_helper(*this, entry.table->get_entity_handles()); detail::static_apply_helper<Components...> apply_helper(*this, entry.table->entity_handles());
for (std::size_t i = 0; i < sizeof...(Components); ++i) for (std::size_t i = 0; i < sizeof...(Components); ++i)
apply_helper.pointers[i] = entry.table->get_component_pointers()[entry.column_ids[i]]; apply_helper.pointers[i] = entry.table->component_pointers()[entry.column_ids[i]];
apply_helper.batch_apply(function); apply_helper.batch_apply(function);
} }
@ -309,11 +307,11 @@ namespace psemek::ecs
{ {
util::uuid const uuid = Component::uuid(); util::uuid const uuid = Component::uuid();
auto column = table_->get_component_column(uuid); auto column = table_->component_column(uuid);
if (!column) if (!column)
return nullptr; return nullptr;
return reinterpret_cast<Component *>(table_->get_component_pointers()[*column].data + detail::stride<Component>() * row_); return reinterpret_cast<Component *>(table_->component_pointers()[*column].data + detail::stride<Component>() * row_);
} }
template <typename Component> template <typename Component>
@ -321,7 +319,7 @@ namespace psemek::ecs
{ {
if (auto ptr = get_if<Component>()) if (auto ptr = get_if<Component>())
return *ptr; return *ptr;
throw component_not_found_exception(typeid(Component), table_->get_entity_handles()[row_]); throw component_not_found_exception(typeid(Component), table_->entity_handles()[row_]);
} }
template <typename Component> template <typename Component>

View file

@ -10,7 +10,7 @@ namespace psemek::ecs::detail
entry.table = table; entry.table = table;
for (auto const & uuid : component_uuids) for (auto const & uuid : component_uuids)
entry.column_ids.push_back(*(table->get_component_column(uuid))); entry.column_ids.push_back(*(table->component_column(uuid)));
} }
} }

View file

@ -35,7 +35,7 @@ namespace psemek::ecs
void entity_container::remove_row(detail::table & table, std::uint32_t row, util::span<detail::entity_data> entities) void entity_container::remove_row(detail::table & table, std::uint32_t row, util::span<detail::entity_data> entities)
{ {
// Swap with the last row in that table // Swap with the last row in that table
auto table_entity_handles = table.get_entity_handles(); auto table_entity_handles = table.entity_handles();
table.swap_rows(row, table_entity_handles.size() - 1); table.swap_rows(row, table_entity_handles.size() - 1);
table.pop_row(); table.pop_row();
auto swap_handle = table_entity_handles[row]; auto swap_handle = table_entity_handles[row];