ECS library wip: rewrite tables using explicit columns

This commit is contained in:
Nikita Lisitsa 2023-08-26 12:33:14 +03:00
parent 1d20bd5a17
commit 59c803d31c
10 changed files with 383 additions and 296 deletions

View file

@ -66,7 +66,7 @@ namespace psemek::ecs::detail
std::size_t row_count;
entity_handle const * entity_handles_pointer;
// (+1) to prevent zero-sized array
table::component_pointer pointers[sizeof...(Components) + 1];
std::uint8_t * pointers[sizeof...(Components) + 1];
static_apply_helper(entity_container & parent, util::span<entity_handle const> entity_handles)
: parent(parent)
@ -83,7 +83,7 @@ namespace psemek::ecs::detail
template <typename Function, std::size_t ... I>
void apply_impl(Function && function, std::index_sequence<I...>)
{
invoke(function, parent, *entity_handles_pointer, *reinterpret_cast<Components *>(pointers[I].data) ...);
invoke(function, parent, *entity_handles_pointer, *reinterpret_cast<Components *>(pointers[I]) ...);
}
template <typename Function>
@ -95,7 +95,7 @@ namespace psemek::ecs::detail
template <typename Function, std::size_t ... I>
void batch_apply_impl(Function && function, std::index_sequence<I...>)
{
batch_invoke(function, parent, row_count, entity_handles_pointer, reinterpret_cast<Components *>(pointers[I].data) ...);
batch_invoke(function, parent, row_count, entity_handles_pointer, reinterpret_cast<Components *>(pointers[I]) ...);
}
std::size_t size() const
@ -108,7 +108,7 @@ namespace psemek::ecs::detail
++entity_handles_pointer;
std::size_t i = 0;
((pointers[i++].data += stride<Components>()), ...);
((pointers[i++] += stride<Components>()), ...);
}
};

View file

@ -0,0 +1,219 @@
#pragma once
#include <psemek/ecs/detail/stride.hpp>
#include <psemek/util/uuid.hpp>
#include <memory>
namespace psemek::ecs::detail
{
struct column
{
column(util::uuid const & uuid)
: uuid_(uuid)
{}
std::uint8_t * data()
{
return data_;
}
util::uuid const & uuid() const
{
return uuid_;
}
virtual void push_row() = 0;
virtual void emplace_rows(std::uint8_t * data, std::size_t count) = 0;
virtual void swap_rows(std::size_t row1, std::size_t row2) = 0;
virtual void pop_row() = 0;
virtual void clear() = 0;
virtual std::unique_ptr<column> clone() const = 0;
virtual ~column() = default;
protected:
std::uint8_t * data_ = nullptr;
util::uuid uuid_;
};
template <typename Component, bool Empty = detail::is_empty_v<Component>>
struct column_impl
: column
{
column_impl();
void push_row() override;
void emplace_rows(std::uint8_t * data, std::size_t count) override;
void swap_rows(std::size_t row1, std::size_t row2) override;
void pop_row() override;
void clear() override;
std::unique_ptr<column> clone() const override;
~column_impl() override;
private:
std::size_t capacity_ = 0;
std::size_t row_count_ = 0;
void allocate(std::size_t min_capacity);
};
template <typename Component>
struct column_impl<Component, true>
: column
{
column_impl();
void push_row() override;
void emplace_rows(std::uint8_t * data, std::size_t count) override;
void swap_rows(std::size_t row1, std::size_t row2) override;
void pop_row() override;
void clear() override;
std::unique_ptr<column> clone() const override;
~column_impl() override;
};
template <typename Component, bool Empty>
column_impl<Component, Empty>::column_impl()
: column(Component::uuid())
{}
template <typename Component, bool Empty>
void column_impl<Component, Empty>::push_row()
{
allocate(row_count_ + 1);
new (reinterpret_cast<Component *>(data_) + row_count_) Component{};
++row_count_;
}
template <typename Component, bool Empty>
void column_impl<Component, Empty>::emplace_rows(std::uint8_t * data, std::size_t count)
{
allocate(row_count_ + count);
auto src = reinterpret_cast<Component *>(data);
auto dst = reinterpret_cast<Component *>(data_);
auto src_end = src + count;
while (src != src_end)
{
new (dst) Component{std::move(*src)};
++src;
++dst;
}
row_count_ += count;
}
template <typename Component, bool Empty>
void column_impl<Component, Empty>::swap_rows(std::size_t row1, std::size_t row2)
{
auto data = reinterpret_cast<Component *>(data_);
std::swap(data[row1], data[row2]);
}
template <typename Component, bool Empty>
void column_impl<Component, Empty>::pop_row()
{
(reinterpret_cast<Component *>(data_) + row_count_ - 1)->~Component();
--row_count_;
}
template <typename Component, bool Empty>
void column_impl<Component, Empty>::clear()
{
auto data = reinterpret_cast<Component *>(data_);
for (auto p = data; p < data + row_count_; ++p)
p->~Component();
row_count_ = 0;
}
template <typename Component, bool Empty>
std::unique_ptr<column> column_impl<Component, Empty>::clone() const
{
return std::make_unique<column_impl<Component, Empty>>();
}
template <typename Component, bool Empty>
column_impl<Component, Empty>::~column_impl()
{
clear();
delete [] data_;
data_ = nullptr;
row_count_ = 0;
}
template <typename Component, bool Empty>
void column_impl<Component, Empty>::allocate(std::size_t min_capacity)
{
if (capacity_ >= min_capacity)
return;
std::size_t new_capacity = std::max<std::size_t>(64, capacity_);
while (new_capacity < min_capacity)
new_capacity *= 2;
auto new_data = new (std::align_val_t(alignof(Component))) std::uint8_t[new_capacity * sizeof(Component)];
auto old_begin = reinterpret_cast<Component *>(data_);
auto old_end = old_begin + row_count_;
auto new_begin = reinterpret_cast<Component *>(new_data);
for (; old_begin != old_end; ++old_begin, ++new_begin)
{
new (new_begin) Component{std::move(*old_begin)};
old_begin->~Component();
}
delete [] data_;
data_ = new_data;
capacity_ = new_capacity;
}
template <typename Component>
column_impl<Component, true>::column_impl()
: column(Component::uuid())
{
data_ = reinterpret_cast<std::uint8_t *>(new Component[1]);
}
template <typename Component>
void column_impl<Component, true>::push_row()
{}
template <typename Component>
void column_impl<Component, true>::emplace_rows(std::uint8_t *, std::size_t)
{}
template <typename Component>
void column_impl<Component, true>::swap_rows(std::size_t, std::size_t)
{}
template <typename Component>
void column_impl<Component, true>::pop_row()
{}
template <typename Component>
void column_impl<Component, true>::clear()
{}
template <typename Component>
std::unique_ptr<column> column_impl<Component, true>::clone() const
{
return std::make_unique<column_impl<Component, true>>();
}
template <typename Component>
column_impl<Component, true>::~column_impl()
{
delete [] reinterpret_cast<Component *>(data_);
data_ = nullptr;
}
}

View file

@ -1,18 +1,28 @@
#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)
struct component_hasher
{
std::size_t result = 0xcd5694d2b3f3443eull;
for (auto const & uuid : uuids)
void operator()(util::uuid const & uuid)
{
result ^= std::hash<util::uuid>{}(uuid);
return result;
}
};
template <typename UUIDs>
std::size_t component_hash(UUIDs const & uuids)
{
component_hasher hasher;
for (auto const & uuid : uuids)
hasher(uuid);
return hasher.result;
}
}

View file

@ -8,17 +8,18 @@ namespace psemek::ecs::detail
{
struct table;
struct column;
struct query_cache_entry
{
struct table * table = nullptr;
std::vector<std::size_t> column_ids;
std::vector<column *> columns;
};
struct query_cache
{
std::vector<util::uuid> component_uuids;
std::vector<query_cache_entry> tables;
std::vector<query_cache_entry> entries;
void add(table * table);
};

View file

@ -1,8 +1,8 @@
#pragma once
#include <psemek/ecs/entity_handle.hpp>
#include <psemek/ecs/detail/stride.hpp>
#include <psemek/ecs/detail/component_hash.hpp>
#include <psemek/ecs/detail/column.hpp>
#include <psemek/util/uuid.hpp>
#include <psemek/util/span.hpp>
#include <psemek/util/hash_table.hpp>
@ -19,308 +19,62 @@ namespace psemek::ecs::detail
struct table
{
struct component_pointer
{
std::uint8_t * data = nullptr;
};
struct iteration_data
{
std::size_t current_row = 0;
};
table(std::vector<std::unique_ptr<detail::column>> columns);
std::size_t hash() const
{
return hash_;
}
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> component_pointers() const
{
return component_pointers_;
}
detail::column * column(util::uuid const & uuid) const;
util::span<entity_handle const> entity_handles() const
{
return entity_handles_;
}
std::size_t component_count() const
std::size_t column_count() const
{
return component_pointers_.size();
return component_uuid_to_column_.size();
}
std::size_t row_count() const
{
return row_count_;
return entity_handles_.size();
}
virtual std::size_t push_row(entity_handle handles) = 0;
virtual void swap_rows(std::size_t row1, std::size_t row2) = 0;
virtual void pop_row() = 0;
std::size_t push_row(entity_handle handle);
void swap_rows(std::size_t row1, std::size_t row2);
void pop_row();
void clear();
virtual void clear() = 0;
struct iteration_data
{
std::size_t current_row = 0;
};
std::optional<iteration_data> & get_iteration_data()
{
return iteration_data_;
}
void push_remove(std::uint32_t row)
{
remove_queue_.push_back(row);
}
void push_remove(std::uint32_t row);
std::vector<std::uint32_t> grab_remove_queue();
std::vector<std::uint32_t> grab_remove_queue()
{
return std::move(remove_queue_);
}
std::unique_ptr<table> clone() const;
table * get_delayed_table()
{
if (!delayed_table_)
delayed_table_ = create_delayed_table();
return delayed_table_.get();
}
virtual util::span<entity_handle const> flush_delayed() = 0;
virtual ~table() = default;
table * get_delayed_table();
util::span<entity_handle const> flush_delayed();
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_;
std::size_t row_count_ = 0;
util::hash_map<util::uuid, std::unique_ptr<detail::column>> component_uuid_to_column_;
std::vector<entity_handle> entity_handles_;
std::optional<iteration_data> iteration_data_;
std::vector<std::uint32_t> remove_queue_;
std::unique_ptr<table> delayed_table_;
virtual std::unique_ptr<table> create_delayed_table() = 0;
};
template <typename ... Components>
struct table_impl
: table
{
table_impl(util::span<util::uuid const> component_uuids);
std::size_t push_row(entity_handle handle) override;
void swap_rows(std::size_t row1, std::size_t row2) override;
void pop_row() override;
void clear() override;
util::span<entity_handle const> flush_delayed() override;
~table_impl() override;
private:
std::size_t capacity_ = 0;
void reallocate();
std::unique_ptr<table> create_delayed_table() override;
};
template <typename ... Components>
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});
}
template <typename ... Components>
std::size_t table_impl<Components...>::push_row(entity_handle handle)
{
if (row_count_ == capacity_)
reallocate();
[[maybe_unused]] auto push_row_impl = [&]<typename Component>(std::uint8_t * data)
{
if constexpr (!detail::is_empty_v<Component>)
{
new (reinterpret_cast<Component *>(data) + row_count_) Component{};
}
};
std::size_t i = 0;
(push_row_impl.template operator()<Components>(component_pointers_[i++].data), ...);
++row_count_;
entity_handles_.push_back(handle);
return row_count_ - 1;
}
template <typename ... Components>
void table_impl<Components...>::swap_rows(std::size_t row1, std::size_t row2)
{
[[maybe_unused]] auto swap_rows_impl = [&]<typename Component>(std::uint8_t * data)
{
if constexpr (!detail::is_empty_v<Component>)
{
auto cdata = reinterpret_cast<Component *>(data);
std::iter_swap(cdata + row1, cdata + row2);
}
};
std::size_t i = 0;
(swap_rows_impl.template operator()<Components>(component_pointers_[i++].data), ...);
std::swap(entity_handles_[row1], entity_handles_[row2]);
}
template <typename ... Components>
void table_impl<Components...>::pop_row()
{
--row_count_;
[[maybe_unused]] auto pop_row_impl = [&]<typename Component>(std::uint8_t * data)
{
if constexpr (!detail::is_empty_v<Component>)
{
(reinterpret_cast<Component *>(data) + row_count_)->~Component();
}
};
std::size_t i = 0;
(pop_row_impl.template operator()<Components>(component_pointers_[i++].data), ...);
entity_handles_.pop_back();
}
template <typename ... Components>
void table_impl<Components...>::clear()
{
[[maybe_unused]] auto clear_column_impl = [&]<typename Component>(std::uint8_t * data)
{
auto ptr = reinterpret_cast<Component *>(data);
if constexpr (!detail::is_empty_v<Component>)
{
for (std::size_t i = 0; i < row_count_; ++i)
ptr[i].~Component();
}
};
std::size_t i = 0;
(clear_column_impl.template operator()<Components>(component_pointers_[i++].data), ...);
row_count_ = 0;
entity_handles_.clear();
}
template <typename ... Components>
util::span<entity_handle const> table_impl<Components...>::flush_delayed()
{
if (!delayed_table_)
return {};
auto delayed = static_cast<table_impl<Components...> *>(delayed_table_.get());
auto old_row_count = row_count_;
while (capacity_ < row_count_ + delayed->row_count_)
reallocate();
std::size_t delayed_row = 0;
[[maybe_unused]] auto move_row_impl = [&]<typename Component>(std::uint8_t * data_tgt, std::uint8_t * data_src)
{
if constexpr (!detail::is_empty_v<Component>)
{
new (reinterpret_cast<Component *>(data_tgt) + row_count_) Component{std::move(*(reinterpret_cast<Component *>(data_src) + delayed_row))};
}
};
for (; delayed_row < delayed->row_count_; ++delayed_row)
{
std::size_t i1 = 0, i2 = 0;
(move_row_impl.template operator()<Components>(component_pointers_[i1++].data, delayed->component_pointers_[i2++].data), ...);
++row_count_;
}
entity_handles_.insert(entity_handles_.end(), delayed->entity_handles_.begin(), delayed->entity_handles_.end());
delayed->clear();
return {entity_handles_.data() + old_row_count, entity_handles_.data() + row_count_};
}
template <typename ... Components>
table_impl<Components...>::~table_impl()
{
clear();
[[maybe_unused]] auto delete_column_impl = [&]<typename Component>(std::uint8_t * & data)
{
if constexpr (detail::is_empty_v<Component>)
{
reinterpret_cast<Component *>(data)->~Component();
}
delete [] data;
data = nullptr;
};
std::size_t i = 0;
(delete_column_impl.template operator()<Components>(component_pointers_[i++].data), ...);
capacity_ = 0;
}
template <typename ... Components>
void table_impl<Components...>::reallocate()
{
std::size_t const new_capacity = capacity_ == 0 ? 64 : capacity_ * 2;
[[maybe_unused]] auto reallocate_impl = [&]<typename Component>(std::uint8_t * & data)
{
if constexpr (detail::is_empty_v<Component>)
{
if (!data)
data = reinterpret_cast<std::uint8_t *>(new Component[1]);
}
else
{
auto new_data = new (std::align_val_t(alignof(Component))) std::uint8_t[new_capacity * sizeof(Component)];
auto old_begin = reinterpret_cast<Component *>(data);
auto old_end = old_begin + row_count_;
auto new_begin = reinterpret_cast<Component *>(new_data);
for (; old_begin != old_end; ++old_begin, ++new_begin)
{
new (new_begin) Component{std::move(*old_begin)};
old_begin->~Component();
}
delete [] data;
data = new_data;
}
};
std::size_t i = 0;
(reallocate_impl.template operator()<Components>(component_pointers_[i++].data), ...);
capacity_ = new_capacity;
}
template <typename ... Components>
std::unique_ptr<table> table_impl<Components...>::create_delayed_table()
{
return std::make_unique<table_impl<Components...>>(component_uuids_);
}
}

View file

@ -26,10 +26,10 @@ namespace psemek::ecs::detail
{
bool operator()(util::span<util::uuid const> const & uuids, std::unique_ptr<table> const & table) const
{
if (uuids.size() != table->component_count())
if (uuids.size() != table->column_count())
return false;
for (auto const & uuid : uuids)
if (!table->component_column(uuid))
if (!table->column(uuid))
return false;
return true;
}
@ -60,7 +60,10 @@ namespace psemek::ecs::detail
if (it != tables_.end())
return {it->get(), false};
auto table = std::make_unique<table_impl<Components...>>(component_uuids);
std::vector<std::unique_ptr<column>> columns;
(columns.push_back(std::make_unique<column_impl<Components>>()), ...);
auto table = std::make_unique<detail::table>(std::move(columns));
auto result = table.get();
tables_.insert(std::move(table));
return {result, true};
@ -73,7 +76,7 @@ namespace psemek::ecs::detail
{
bool good = true;
for (auto const & uuid : component_uuids)
if (!table->component_column(uuid))
if (!table->column(uuid))
{
good = false;
break;

View file

@ -192,6 +192,8 @@ namespace psemek::ecs
detail::table_container table_container_;
detail::query_cache_container query_cache_container_;
std::vector<util::uuid> uuid_helper_;
void remove_row(detail::table & table, std::uint32_t row, util::span<detail::entity_data> entities);
};
@ -210,7 +212,7 @@ namespace psemek::ecs
{
query_cache_container_.apply([table](detail::query_cache & cache){
cache.add(table);
}, [table](util::uuid const & uuid){ return table->component_column(uuid) != std::nullopt; });
}, [table](util::uuid const & uuid){ return table->column(uuid) != nullptr; });
}
if (table->get_iteration_data())
@ -246,14 +248,14 @@ namespace psemek::ecs
if (!cache)
cache = this->cache<Components...>();
for (auto const & entry : cache->tables)
for (auto const & entry : cache->entries)
{
auto & iteration_data = entry.table->get_iteration_data();
iteration_data.emplace();
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->component_pointers()[entry.column_ids[i]];
apply_helper.pointers[i] = entry.table->column(cache->component_uuids[i])->data();
for (std::size_t i = 0; i < entry.table->row_count(); ++i)
{
@ -286,12 +288,12 @@ namespace psemek::ecs
if (!cache)
cache = this->cache<Components...>();
for (auto const & entry : cache->tables)
for (auto const & entry : cache->entries)
{
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->component_pointers()[entry.column_ids[i]];
apply_helper.pointers[i] = entry.table->column(cache->component_uuids[i])->data();
apply_helper.batch_apply(function);
}
@ -307,11 +309,11 @@ namespace psemek::ecs
{
util::uuid const uuid = Component::uuid();
auto column = table_->component_column(uuid);
auto column = table_->column(uuid);
if (!column)
return nullptr;
return reinterpret_cast<Component *>(table_->component_pointers()[*column].data + detail::stride<Component>() * row_);
return reinterpret_cast<Component *>(column->data() + detail::stride<Component>() * row_);
}
template <typename Component>

View file

@ -6,11 +6,11 @@ namespace psemek::ecs::detail
void query_cache::add(table * table)
{
auto & entry = tables.emplace_back();
auto & entry = entries.emplace_back();
entry.table = table;
for (auto const & uuid : component_uuids)
entry.column_ids.push_back(*(table->component_column(uuid)));
entry.columns.push_back(table->column(uuid));
}
}

View file

@ -0,0 +1,98 @@
#include <psemek/ecs/detail/table.hpp>
#include <psemek/ecs/detail/component_hash.hpp>
namespace psemek::ecs::detail
{
table::table(std::vector<std::unique_ptr<detail::column>> columns)
{
component_hasher hasher;
for (auto & column : columns)
{
auto uuid = column->uuid();
hasher(uuid);
component_uuid_to_column_.insert({uuid, std::move(column)});
}
hash_ = hasher.result;
}
detail::column * table::column(util::uuid const & uuid) const
{
if (auto it = component_uuid_to_column_.find(uuid); it != component_uuid_to_column_.end())
return it->second.get();
return nullptr;
}
std::size_t table::push_row(entity_handle handle)
{
for (auto & pair : component_uuid_to_column_)
pair.second->push_row();
entity_handles_.push_back(handle);
return entity_handles_.size() - 1;
}
void table::swap_rows(std::size_t row1, std::size_t row2)
{
for (auto & pair : component_uuid_to_column_)
pair.second->swap_rows(row1, row2);
std::swap(entity_handles_[row1], entity_handles_[row2]);
}
void table::pop_row()
{
for (auto & pair : component_uuid_to_column_)
pair.second->pop_row();
entity_handles_.pop_back();
}
void table::clear()
{
for (auto & pair : component_uuid_to_column_)
pair.second->clear();
entity_handles_.clear();
}
void table::push_remove(std::uint32_t row)
{
remove_queue_.push_back(row);
}
std::vector<std::uint32_t> table::grab_remove_queue()
{
return std::move(remove_queue_);
}
std::unique_ptr<table> table::clone() const
{
std::vector<std::unique_ptr<detail::column>> columns;
for (auto const & pair : component_uuid_to_column_)
columns.push_back(pair.second->clone());
return std::make_unique<table>(std::move(columns));
}
table * table::get_delayed_table()
{
if (!delayed_table_)
delayed_table_ = clone();
return delayed_table_.get();
}
util::span<entity_handle const> table::flush_delayed()
{
if (!delayed_table_)
return {};
std::size_t count = delayed_table_->row_count();
for (auto & pair : component_uuid_to_column_)
{
auto * src_column = delayed_table_->column(pair.second->uuid());
pair.second->emplace_rows(src_column->data(), count);
}
entity_handles_.insert(entity_handles_.end(), delayed_table_->entity_handles_.begin(), delayed_table_->entity_handles_.end());
delayed_table_->clear();
return {entity_handles_.data() + entity_handles_.size() - count, count};
}
}

View file

@ -45,7 +45,7 @@ test_case(ecs_cache_empty)
expect_different_ptr(cache.get(), nullptr);
expect(cache->component_uuids.empty());
expect_equal(cache->tables.size(), 4);
expect_equal(cache->entries.size(), 4);
}
test_case(ecs_cache_components)
@ -61,7 +61,7 @@ test_case(ecs_cache_components)
expect_different_ptr(cache.get(), nullptr);
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->tables.size(), 2);
expect_equal(cache->entries.size(), 2);
}
test_case(ecs_cache_update)
@ -72,23 +72,23 @@ test_case(ecs_cache_update)
expect_different_ptr(cache.get(), nullptr);
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->tables.size(), 0);
expect_equal(cache->entries.size(), 0);
container.create();
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->tables.size(), 0);
expect_equal(cache->entries.size(), 0);
container.create(component_1{10});
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->tables.size(), 1);
expect_equal(cache->entries.size(), 1);
container.create(component_2{20});
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->tables.size(), 1);
expect_equal(cache->entries.size(), 1);
container.create(component_1{100}, component_2{200});
expect_equal(cache->component_uuids.size(), 1);
expect_equal(cache->tables.size(), 2);
expect_equal(cache->entries.size(), 2);
}
test_case(ecs_cache_apply)