diff --git a/libs/ecs/include/psemek/ecs/detail/query_cache.hpp b/libs/ecs/include/psemek/ecs/detail/query_cache.hpp new file mode 100644 index 00000000..ef3995ef --- /dev/null +++ b/libs/ecs/include/psemek/ecs/detail/query_cache.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include + +namespace psemek::ecs::detail +{ + + struct table; + + struct query_cache_entry + { + struct table * table = nullptr; + std::vector column_ids; + }; + + struct query_cache + { + std::vector component_uuids; + std::vector tables; + + void add(table * table); + }; + +} diff --git a/libs/ecs/include/psemek/ecs/detail/query_cache_container.hpp b/libs/ecs/include/psemek/ecs/detail/query_cache_container.hpp new file mode 100644 index 00000000..cd54e3ec --- /dev/null +++ b/libs/ecs/include/psemek/ecs/detail/query_cache_container.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +#include + +namespace psemek::ecs::detail +{ + + // TODO: store query caches in a bitmask trie balanced by subtree size + struct query_cache_container + { + std::shared_ptr create(component_mask const & mask, util::span component_uuids) + { + auto result = std::make_shared(); + result->component_uuids.assign(component_uuids.begin(), component_uuids.end()); + get(mask).emplace_back(result); + return result; + } + + template + 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>> caches_; + + static void filter(std::vector> & caches) + { + caches.erase(std::remove_if(caches.begin(), caches.end(), [](std::weak_ptr const & weak){ return !weak.lock(); }), caches.end()); + } + + std::vector> & get(component_mask const & mask) + { + auto & result = caches_[mask]; + filter(result); + return result; + } + }; + +} diff --git a/libs/ecs/include/psemek/ecs/detail/table_container.hpp b/libs/ecs/include/psemek/ecs/detail/table_container.hpp index f7d5cc2c..2b86215d 100644 --- a/libs/ecs/include/psemek/ecs/detail/table_container.hpp +++ b/libs/ecs/include/psemek/ecs/detail/table_container.hpp @@ -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 - table_impl & insert(component_mask const & mask, util::span component_uuids); + std::pair *, bool> insert(component_mask const & mask, util::span component_uuids); template void apply(Function && function, component_mask const & mask); @@ -23,12 +22,16 @@ namespace psemek::ecs::detail }; template - table_impl & table_container::insert(component_mask const & mask, util::span component_uuids) + std::pair *, bool> table_container::insert(component_mask const & mask, util::span component_uuids) { auto & result = tables_[mask]; + bool created = false; if (!result) + { result = std::make_unique>(component_uuids); - return *static_cast *>(result.get()); + created = true; + } + return {static_cast *>(result.get()), created}; } template diff --git a/libs/ecs/include/psemek/ecs/entity_container.hpp b/libs/ecs/include/psemek/ecs/entity_container.hpp index 85d9f16d..0e751bc7 100644 --- a/libs/ecs/include/psemek/ecs/entity_container.hpp +++ b/libs/ecs/include/psemek/ecs/entity_container.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -10,6 +11,8 @@ namespace psemek::ecs { + using query_cache = std::shared_ptr; + struct entity_container { template @@ -18,13 +21,22 @@ namespace psemek::ecs detail::component_uuid_holder uuids; detail::component_mask mask = component_index_.make_component_mask(uuids.get()); - auto & table = table_container_.insert(mask, uuids.get()); + auto insert_result = table_container_.insert(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() = std::move(components)), ...); return handle; @@ -49,28 +61,40 @@ namespace psemek::ecs entity_list_.destroy(entity.id); } - template - void apply(Function && function) + template + query_cache cache() { detail::component_uuid_holder 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 apply_helper(table.get_entity_ids(), entity_list_.get_entities()); + result->add(&table); + }, mask); + + return result; + } + + template + void apply(Function && function, query_cache cache = {}) + { + if (!cache) + cache = this->cache(); + + for (auto const & entry : cache->tables) + { + detail::static_apply_helper 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_; }; } diff --git a/libs/ecs/source/detail/query_cache.cpp b/libs/ecs/source/detail/query_cache.cpp new file mode 100644 index 00000000..f381bbcd --- /dev/null +++ b/libs/ecs/source/detail/query_cache.cpp @@ -0,0 +1,25 @@ +#include +#include + +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; + } + } + } + } + +} diff --git a/libs/ecs/tests/cache.cpp b/libs/ecs/tests/cache.cpp new file mode 100644 index 00000000..ce452b22 --- /dev/null +++ b/libs/ecs/tests/cache.cpp @@ -0,0 +1,135 @@ +#include + +#include +#include +#include + +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(); + + 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(); + + 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(); + + 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(counter, cache); + expect_equal(call_count, 0); + + for (int i = 0; i < count_0; ++i) + container.create(); + call_count = 0; + container.apply(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(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(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(counter, cache); + expect_equal(call_count, count_1 + count_12); +}