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
#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>
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
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>();
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;
}
template <typename Function>
void apply(Function && function, component_mask const & mask)
template <typename Function, typename ContainsUUID>
void apply(Function && function, ContainsUUID && contains_uuid)
{
for (auto & caches : caches_)
for (auto & cache_set : caches_)
{
if (!util::is_subset(caches.first, mask)) continue;
filter(caches.second);
for (auto & cache : caches.second)
bool good = true;
for (auto const & uuid : cache_set->uuids)
if (!contains_uuid(uuid))
{
good = false;
break;
}
if (!good)
continue;
filter(cache_set->caches);
for (auto & cache : cache_set->caches)
function(*cache.lock());
}
}
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)
{
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/detail/stride.hpp>
#include <psemek/ecs/detail/component_hash.hpp>
#include <psemek/util/uuid.hpp>
#include <psemek/util/span.hpp>
#include <psemek/util/hash_table.hpp>
@ -28,31 +29,31 @@ namespace psemek::ecs::detail
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())
return it->second;
return std::nullopt;
}
util::span<component_pointer const> get_component_pointers() const
util::span<component_pointer const> component_pointers() const
{
return component_pointers_;
}
util::span<entity_handle const> get_entity_handles() const
util::span<entity_handle const> entity_handles() const
{
return entity_handles_;
}
std::size_t component_count() const
{
return component_uuids_.size();
return component_pointers_.size();
}
std::size_t row_count() const
@ -93,6 +94,7 @@ namespace psemek::ecs::detail
virtual ~table() = default;
protected:
std::size_t hash_;
std::vector<util::uuid> component_uuids_;
util::hash_map<util::uuid, std::size_t> component_uuid_to_column_;
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)
{
assert(sizeof...(Components) == component_uuids.size());
hash_ = component_hash(component_uuids);
component_uuids_.assign(component_uuids.begin(), component_uuids.end());
component_pointers_.resize(sizeof...(Components));
for (std::size_t i = 0; i < component_uuids_.size(); ++i)
component_uuid_to_column_.insert({component_uuids_[i], i});
for (std::size_t i = 0; i < component_uuids.size(); ++i)
component_uuid_to_column_.insert({component_uuids[i], i});
}
template <typename ... Components>

View file

@ -1,46 +1,85 @@
#pragma once
#include <psemek/ecs/detail/component_mask.hpp>
#include <psemek/ecs/detail/component_hash.hpp>
#include <psemek/ecs/detail/table.hpp>
#include <psemek/util/hash_table.hpp>
#include <vector>
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
struct table_container
{
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>
void apply(Function && function, component_mask const & mask);
void apply(Function && function, util::span<util::uuid const> component_uuids);
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>
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];
bool created = false;
if (!result)
{
result = std::make_unique<table_impl<Components...>>(component_uuids);
created = true;
}
return {result.get(), created};
auto it = tables_.find(component_uuids);
if (it != tables_.end())
return {it->get(), false};
auto table = std::make_unique<table_impl<Components...>>(component_uuids);
auto result = table.get();
tables_.insert(std::move(table));
return {result, true};
}
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_)
{
if (!util::is_subset(mask, table.first)) continue;
function(*table.second);
bool good = true;
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
#include <psemek/ecs/detail/component_index.hpp>
#include <psemek/ecs/detail/entity_list.hpp>
#include <psemek/ecs/detail/table_container.hpp>
#include <psemek/ecs/detail/query_cache_container.hpp>
#include <psemek/ecs/detail/apply_helper.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/range.hpp>
#include <psemek/util/exception.hpp>
@ -164,7 +164,7 @@ namespace psemek::ecs
* types, the behavior is undefined.
* If the function accesses passed components after destroying the
* 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>
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
* types, 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>
void batch_apply(Function && function, query_cache cache = {});
private:
detail::entity_list entity_list_;
mutable detail::component_index component_index_;
detail::table_container table_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");
detail::component_uuid_holder<Components...> uuids;
detail::component_mask mask = component_index_.make_component_mask(uuids.get());
detail::component_uuid_helper<Components...> uuids;
auto insert_result = table_container_.insert<Components...>(mask, uuids.get());
auto insert_result = table_container_.insert<Components...>(uuids.get());
auto table = insert_result.first;
bool created = insert_result.second;
@ -211,7 +210,7 @@ namespace psemek::ecs
{
query_cache_container_.apply([table](detail::query_cache & cache){
cache.add(table);
}, mask);
}, [table](util::uuid const & uuid){ return table->component_column(uuid) != std::nullopt; });
}
if (table->get_iteration_data())
@ -230,14 +229,13 @@ namespace psemek::ecs
template <typename ... Components>
query_cache entity_container::cache()
{
detail::component_uuid_holder<Components...> uuids;
detail::component_mask mask = component_index_.make_component_mask(uuids.get());
detail::component_uuid_helper<Components...> uuids;
auto result = query_cache_container_.create(mask, uuids.get());
auto result = query_cache_container_.create(uuids.get());
table_container_.apply([&](detail::table & table){
result->add(&table);
}, mask);
}, uuids.get());
return result;
}
@ -252,10 +250,10 @@ namespace psemek::ecs
{
auto & iteration_data = entry.table->get_iteration_data();
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)
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)
{
@ -290,10 +288,10 @@ namespace psemek::ecs
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)
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);
}
@ -309,11 +307,11 @@ namespace psemek::ecs
{
util::uuid const uuid = Component::uuid();
auto column = table_->get_component_column(uuid);
auto column = table_->component_column(uuid);
if (!column)
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>
@ -321,7 +319,7 @@ namespace psemek::ecs
{
if (auto ptr = get_if<Component>())
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>

View file

@ -10,7 +10,7 @@ namespace psemek::ecs::detail
entry.table = table;
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)
{
// 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.pop_row();
auto swap_handle = table_entity_handles[row];