ECS library wip: support explicit query cache

This commit is contained in:
Nikita Lisitsa 2023-08-22 21:18:53 +03:00
parent e0e0df8128
commit c1991cbb57
6 changed files with 280 additions and 16 deletions

View file

@ -0,0 +1,26 @@
#pragma once
#include <psemek/util/uuid.hpp>
#include <vector>
namespace psemek::ecs::detail
{
struct table;
struct query_cache_entry
{
struct table * table = nullptr;
std::vector<std::size_t> column_ids;
};
struct query_cache
{
std::vector<util::uuid> component_uuids;
std::vector<query_cache_entry> tables;
void add(table * table);
};
}

View file

@ -0,0 +1,50 @@
#pragma once
#include <psemek/ecs/detail/query_cache.hpp>
#include <psemek/ecs/detail/component_mask.hpp>
#include <memory>
namespace psemek::ecs::detail
{
// 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)
{
auto result = std::make_shared<query_cache>();
result->component_uuids.assign(component_uuids.begin(), component_uuids.end());
get(mask).emplace_back(result);
return result;
}
template <typename Function>
void apply(Function && function, component_mask const & mask)
{
for (auto & caches : caches_)
{
if (!util::is_subset(caches.first, mask)) continue;
filter(caches.second);
for (auto & cache : caches.second)
function(*cache.lock());
}
}
private:
std::unordered_map<component_mask, 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());
}
std::vector<std::weak_ptr<query_cache>> & get(component_mask const & mask)
{
auto & result = caches_[mask];
filter(result);
return result;
}
};
}

View file

@ -9,11 +9,10 @@ namespace psemek::ecs::detail
{
// TODO: store tables in a bitmask trie balanced by subtree size
// TODO: support explicit or implicit query cache
struct table_container
{
template <typename ... Components>
table_impl<Components...> & insert(component_mask const & mask, util::span<util::uuid const> component_uuids);
std::pair<table_impl<Components...> *, bool> insert(component_mask const & mask, util::span<util::uuid const> component_uuids);
template <typename Function>
void apply(Function && function, component_mask const & mask);
@ -23,12 +22,16 @@ namespace psemek::ecs::detail
};
template <typename ... Components>
table_impl<Components...> & table_container::insert(component_mask const & mask, util::span<util::uuid const> component_uuids)
std::pair<table_impl<Components...> *, bool> table_container::insert(component_mask const & mask, 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);
return *static_cast<table_impl<Components...> *>(result.get());
created = true;
}
return {static_cast<table_impl<Components...> *>(result.get()), created};
}
template <typename Function>

View file

@ -3,6 +3,7 @@
#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/entity_accessor.hpp>
#include <psemek/util/span.hpp>
@ -10,6 +11,8 @@
namespace psemek::ecs
{
using query_cache = std::shared_ptr<detail::query_cache>;
struct entity_container
{
template <typename ... Components>
@ -18,13 +21,22 @@ namespace psemek::ecs
detail::component_uuid_holder<Components...> uuids;
detail::component_mask mask = component_index_.make_component_mask(uuids.get());
auto & table = table_container_.insert<Components...>(mask, uuids.get());
auto insert_result = table_container_.insert<Components...>(mask, uuids.get());
auto table = insert_result.first;
bool created = insert_result.second;
auto id = entity_list_.create(&table, table.row_count());
if (created)
{
query_cache_container_.apply([table](detail::query_cache & cache){
cache.add(table);
}, mask);
}
auto id = entity_list_.create(table, table->row_count());
entity_handle handle{id, entity_list_.get_entities()[id].epoch};
[[maybe_unused]] entity_accessor accessor = get(handle);
table.push_row(id);
table->push_row(id);
((accessor.get<Components>() = std::move(components)), ...);
return handle;
@ -49,28 +61,40 @@ namespace psemek::ecs
entity_list_.destroy(entity.id);
}
template <typename ... Components, typename Function>
void apply(Function && function)
template <typename ... Components>
query_cache cache()
{
detail::component_uuid_holder<Components...> uuids;
detail::component_mask mask = component_index_.make_component_mask(uuids.get());
auto result = query_cache_container_.create(mask, uuids.get());
table_container_.apply([&](detail::table & table){
detail::static_apply_helper<Components...> apply_helper(table.get_entity_ids(), entity_list_.get_entities());
result->add(&table);
}, mask);
return result;
}
template <typename ... Components, typename Function>
void apply(Function && function, query_cache cache = {})
{
if (!cache)
cache = this->cache<Components...>();
for (auto const & entry : cache->tables)
{
detail::static_apply_helper<Components...> apply_helper(entry.table->get_entity_ids(), entity_list_.get_entities());
for (std::size_t i = 0; i < sizeof...(Components); ++i)
for (std::size_t j = 0; j < table.component_count(); ++j)
if (uuids.get()[i] == table.get_component_uuids()[j])
apply_helper.pointers[i++] = table.get_component_pointers()[j];
apply_helper.pointers[i] = entry.table->get_component_pointers()[entry.column_ids[i]];
for (std::size_t i = 0; i < apply_helper.size(); ++i)
{
apply_helper.apply(function);
apply_helper.advance();
}
}, mask);
}
}
entity_accessor get(entity_handle const & entity)
@ -83,6 +107,7 @@ namespace psemek::ecs
detail::entity_list entity_list_;
mutable detail::component_index component_index_;
detail::table_container table_container_;
detail::query_cache_container query_cache_container_;
};
}

View file

@ -0,0 +1,25 @@
#include <psemek/ecs/detail/query_cache.hpp>
#include <psemek/ecs/detail/table.hpp>
namespace psemek::ecs::detail
{
void query_cache::add(table * table)
{
auto & entry = tables.emplace_back();
entry.table = table;
for (auto const & uuid : component_uuids)
{
for (std::size_t i = 0; i < table->component_count(); ++i)
{
if (uuid == table->get_component_uuids()[i])
{
entry.column_ids.push_back(i);
break;
}
}
}
}
}

135
libs/ecs/tests/cache.cpp Normal file
View file

@ -0,0 +1,135 @@
#include <psemek/test/test.hpp>
#include <psemek/ecs/entity_container.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/uniform.hpp>
using namespace psemek;
using namespace psemek::ecs;
namespace
{
struct component_1
{
int value;
static constexpr util::uuid uuid()
{
return {1, 0};
}
};
struct component_2
{
int value;
static constexpr util::uuid uuid()
{
return {2, 0};
}
};
}
test_case(ecs_cache_empty)
{
entity_container container;
container.create();
container.create(component_1{10});
container.create(component_2{20});
container.create(component_1{100}, component_2{200});
auto cache = container.cache();
expect_different_ptr(cache.get(), nullptr);
expect(cache->component_uuids.empty());
expect_equal(cache->tables.size(), 4);
}
test_case(ecs_cache_components)
{
entity_container container;
container.create();
container.create(component_1{10});
container.create(component_2{20});
container.create(component_1{100}, component_2{200});
auto cache = container.cache<component_1>();
expect_different_ptr(cache.get(), nullptr);
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->tables.size(), 2);
}
test_case(ecs_cache_update)
{
entity_container container;
auto cache = container.cache<component_1>();
expect_different_ptr(cache.get(), nullptr);
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->tables.size(), 0);
container.create();
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->tables.size(), 0);
container.create(component_1{10});
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->tables.size(), 1);
container.create(component_2{20});
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->tables.size(), 1);
container.create(component_1{100}, component_2{200});
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->tables.size(), 2);
}
test_case(ecs_cache_apply)
{
entity_container container;
auto cache = container.cache<component_1>();
int call_count = 0;
auto counter = [&](entity_handle const &, component_1 const &){ ++call_count; };
int count_0 = 16;
int count_1 = 32;
int count_2 = 64;
int count_12 = 128;
call_count = 0;
container.apply<component_1>(counter, cache);
expect_equal(call_count, 0);
for (int i = 0; i < count_0; ++i)
container.create();
call_count = 0;
container.apply<component_1>(counter, cache);
expect_equal(call_count, 0);
for (int i = 0; i < count_1; ++i)
container.create(component_1{10});
call_count = 0;
container.apply<component_1>(counter, cache);
expect_equal(call_count, count_1);
for (int i = 0; i < count_2; ++i)
container.create(component_2{20});
call_count = 0;
container.apply<component_1>(counter, cache);
expect_equal(call_count, count_1);
for (int i = 0; i < count_12; ++i)
container.create(component_1{100}, component_2{200});
call_count = 0;
container.apply<component_1>(counter, cache);
expect_equal(call_count, count_1 + count_12);
}