Compare commits

...

3 commits

Author SHA1 Message Date
219af9d803 std::set works better for entity groups 2025-02-20 14:36:48 +03:00
fbbe04f942 Specify optimization flags + -Wall 2025-02-20 14:36:09 +03:00
acc03a75aa Add benchmarks 2025-02-20 14:35:22 +03:00
3 changed files with 452 additions and 40 deletions

View file

@ -2,6 +2,14 @@ cmake_minimum_required(VERSION 3.22.0)
set(PROJECT_NAME zecsy) set(PROJECT_NAME zecsy)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
set(CMAKE_CXX_FLAGS "-Wall -Wextra")
set(CMAKE_CXX_FLAGS_DEBUG "-g")
set(CMAKE_CXX_FLAGS_RELEASE "-O3")
project(${PROJECT_NAME}) project(${PROJECT_NAME})
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

View file

@ -7,7 +7,7 @@
using namespace zecsy; using namespace zecsy;
TEST_CASE("Create a single entity and verify its existence") TEST_CASE("Create a single entity and verify its existence", "[test]")
{ {
world w; world w;
@ -16,7 +16,8 @@ TEST_CASE("Create a single entity and verify its existence")
REQUIRE(w.is_alive(e)); REQUIRE(w.is_alive(e));
} }
TEST_CASE("Destroy an entity and ensure it no longer exists in the world") TEST_CASE("Destroy an entity and ensure it no longer exists in the world",
"[test]")
{ {
world w; world w;
@ -26,7 +27,7 @@ TEST_CASE("Destroy an entity and ensure it no longer exists in the world")
REQUIRE_FALSE(w.is_alive(e)); REQUIRE_FALSE(w.is_alive(e));
} }
TEST_CASE("Entity #0 should be reserved and never used") TEST_CASE("Entity #0 should be reserved and never used", "[test]")
{ {
world w; world w;
@ -40,7 +41,8 @@ struct ChoosenOne
{ {
}; };
TEST_CASE("Entity shouldn't have a component that wasn't attached to it") TEST_CASE("Entity shouldn't have a component that wasn't attached to it",
"[test]")
{ {
world w; world w;
@ -49,7 +51,7 @@ TEST_CASE("Entity shouldn't have a component that wasn't attached to it")
REQUIRE_FALSE(w.has<ChoosenOne>(e)); REQUIRE_FALSE(w.has<ChoosenOne>(e));
} }
TEST_CASE("Attempt of getting non-owned component should throw") TEST_CASE("Attempt of getting non-owned component should throw", "[test]")
{ {
world w; world w;
@ -59,7 +61,8 @@ TEST_CASE("Attempt of getting non-owned component should throw")
} }
TEST_CASE("Attach a simple component to an entity and verify it is correctly " TEST_CASE("Attach a simple component to an entity and verify it is correctly "
"associated") "associated",
"[test]")
{ {
world w; world w;
@ -80,7 +83,8 @@ struct Comp
}; };
TEST_CASE("Retrieve a component from an entity and verify its data matches " TEST_CASE("Retrieve a component from an entity and verify its data matches "
"what was set") "what was set",
"[test]")
{ {
world w; world w;
@ -95,7 +99,8 @@ TEST_CASE("Retrieve a component from an entity and verify its data matches "
} }
TEST_CASE( TEST_CASE(
"Remove a component from an entity and verify it is no longer attached") "Remove a component from an entity and verify it is no longer attached",
"[test]")
{ {
world w; world w;
@ -111,7 +116,7 @@ TEST_CASE(
REQUIRE_FALSE(w.has<ChoosenOne>(e)); REQUIRE_FALSE(w.has<ChoosenOne>(e));
} }
TEST_CASE("Addresses of removed components should be reused") TEST_CASE("Addresses of removed components should be reused", "[test]")
{ {
world w; world w;
std::vector<entity_id> entities; std::vector<entity_id> entities;
@ -156,7 +161,8 @@ TEST_CASE("Addresses of removed components should be reused")
} }
TEST_CASE("Attach multiple components to an entity and verify all are " TEST_CASE("Attach multiple components to an entity and verify all are "
"correctly stored and retrievable") "correctly stored and retrievable",
"[test]")
{ {
world w; world w;
@ -173,7 +179,8 @@ TEST_CASE("Attach multiple components to an entity and verify all are "
} }
TEST_CASE("Create a simple system that processes entities with a specific " TEST_CASE("Create a simple system that processes entities with a specific "
"component and verify it executes correctly") "component and verify it executes correctly",
"[test]")
{ {
struct Component struct Component
{ {
@ -200,7 +207,8 @@ TEST_CASE("Create a simple system that processes entities with a specific "
} }
TEST_CASE("Test a systems ability to query and process only entities with a " TEST_CASE("Test a systems ability to query and process only entities with a "
"specific combination of components") "specific combination of components",
"[test]")
{ {
struct C0 struct C0
{ {
@ -226,7 +234,8 @@ TEST_CASE("Test a systems ability to query and process only entities with a "
REQUIRE(w.get<C0>(e0).value == 0); REQUIRE(w.get<C0>(e0).value == 0);
REQUIRE(w.get<C1>(e0).value == 10); REQUIRE(w.get<C1>(e0).value == 10);
w.query<C0, C1>([e0](entity_id e, C0& c0, C1& c1) w.query<C0, C1>(
[e0](entity_id e, C0& c0, C1& c1)
{ {
REQUIRE(e == e0); REQUIRE(e == e0);
c0.value++; c0.value++;
@ -243,7 +252,7 @@ TEST_CASE("Test a systems ability to query and process only entities with a "
REQUIRE_FALSE(w.has<C0>(e2)); REQUIRE_FALSE(w.has<C0>(e2));
} }
TEST_CASE("Systems execute at correct frequencies") TEST_CASE("Systems execute at correct frequencies", "[test]")
{ {
world w; world w;
system_scheduler scheduler; system_scheduler scheduler;
@ -268,7 +277,7 @@ TEST_CASE("Systems execute at correct frequencies")
REQUIRE(slow_count == 2); // 1 Hz system should execute 1 time REQUIRE(slow_count == 2); // 1 Hz system should execute 1 time
} }
TEST_CASE("Systems handle zero-frequency gracefully") TEST_CASE("Systems handle zero-frequency gracefully", "[test]")
{ {
world w; world w;
system_scheduler scheduler; system_scheduler scheduler;
@ -288,7 +297,7 @@ TEST_CASE("Systems handle zero-frequency gracefully")
REQUIRE(zero_count == 0); REQUIRE(zero_count == 0);
} }
TEST_CASE("Systems handle varying update rates") TEST_CASE("Systems handle varying update rates", "[test]")
{ {
world w; world w;
system_scheduler scheduler; system_scheduler scheduler;
@ -308,7 +317,7 @@ TEST_CASE("Systems handle varying update rates")
REQUIRE(varying_count == 10); REQUIRE(varying_count == 10);
} }
TEST_CASE("Systems handle large time steps") TEST_CASE("Systems handle large time steps", "[test]")
{ {
world w; world w;
system_scheduler scheduler; system_scheduler scheduler;
@ -325,7 +334,7 @@ TEST_CASE("Systems handle large time steps")
REQUIRE(large_step_count == 2); REQUIRE(large_step_count == 2);
} }
TEST_CASE("Systems handle multiple frequencies") TEST_CASE("Systems handle multiple frequencies", "[test]")
{ {
world w; world w;
system_scheduler scheduler; system_scheduler scheduler;
@ -351,7 +360,7 @@ TEST_CASE("Systems handle multiple frequencies")
REQUIRE(slow_count == 1); // 1 Hz system REQUIRE(slow_count == 1); // 1 Hz system
} }
TEST_CASE("Systems handle fractional frequencies") TEST_CASE("Systems handle fractional frequencies", "[test]")
{ {
world w; world w;
system_scheduler scheduler; system_scheduler scheduler;
@ -372,7 +381,7 @@ TEST_CASE("Systems handle fractional frequencies")
REQUIRE(fractional_count == 2); REQUIRE(fractional_count == 2);
} }
TEST_CASE("Systems handle zero delta time") TEST_CASE("Systems handle zero delta time", "[test]")
{ {
world w; world w;
system_scheduler scheduler; system_scheduler scheduler;
@ -389,7 +398,7 @@ TEST_CASE("Systems handle zero delta time")
REQUIRE(zero_dt_count == 0); REQUIRE(zero_dt_count == 0);
} }
TEST_CASE("Systems handle negative delta time") TEST_CASE("Systems handle negative delta time", "[test]")
{ {
world w; world w;
system_scheduler scheduler; system_scheduler scheduler;
@ -410,7 +419,7 @@ TEST_CASE("Systems handle negative delta time")
REQUIRE(count == 2); REQUIRE(count == 2);
} }
TEST_CASE("Entity count tracking") TEST_CASE("Entity count tracking", "[test]")
{ {
world w; world w;
REQUIRE(w.entity_count() == 0); REQUIRE(w.entity_count() == 0);
@ -423,7 +432,7 @@ TEST_CASE("Entity count tracking")
REQUIRE(w.entity_count() == 1); REQUIRE(w.entity_count() == 1);
} }
TEST_CASE("Component counting mechanisms") TEST_CASE("Component counting mechanisms", "[test]")
{ {
struct Health struct Health
{ {
@ -454,7 +463,7 @@ TEST_CASE("Component counting mechanisms")
REQUIRE(w.total_component_count() == 1); REQUIRE(w.total_component_count() == 1);
} }
TEST_CASE("Archetype signature management") TEST_CASE("Archetype signature management", "[test]")
{ {
struct A struct A
{ {
@ -509,7 +518,7 @@ TEST_CASE("Archetype signature management")
REQUIRE(w.archetype_count() == 0); REQUIRE(w.archetype_count() == 0);
} }
TEST_CASE("Component distribution across archetypes") TEST_CASE("Component distribution across archetypes", "[test]")
{ {
struct A struct A
{ {
@ -547,7 +556,7 @@ TEST_CASE("Component distribution across archetypes")
REQUIRE(w.archetype_count() == 3); //<A>, <A, B>, <B> REQUIRE(w.archetype_count() == 3); //<A>, <A, B>, <B>
} }
TEST_CASE("Entity inspection") TEST_CASE("Entity inspection", "[test]")
{ {
struct Transform struct Transform
{ {
@ -572,3 +581,400 @@ TEST_CASE("Entity inspection")
w.remove<Transform>(e); w.remove<Transform>(e);
REQUIRE(w.components_in_entity(e) == 1); REQUIRE(w.components_in_entity(e) == 1);
} }
namespace
{
// Benchmark components
struct Position
{
float x, y;
};
struct Velocity
{
float dx, dy;
};
struct Health
{
int value;
};
struct BigData
{
char buffer[4096];
};
// Benchmark entity counts
constexpr int SMALL = 1'000;
constexpr int MEDIUM = 5'000;
constexpr int LARGE = 10'000;
} // namespace
TEST_CASE("Core operations benchmarks", "[benchmark]")
{
BENCHMARK_ADVANCED("Create entities [1000]")(
Catch::Benchmark::Chronometer meter)
{
meter.measure(
[&]
{
world w;
for(int i = 0; i < SMALL; ++i)
{
w.make_entity();
}
return w.entity_count();
});
};
BENCHMARK_ADVANCED("Create entities [5000]")(
Catch::Benchmark::Chronometer meter)
{
meter.measure(
[&]
{
world w;
for(int i = 0; i < MEDIUM; ++i)
{
w.make_entity();
}
return w.entity_count();
});
};
BENCHMARK_ADVANCED("Create entities [10000]")(
Catch::Benchmark::Chronometer meter)
{
meter.measure(
[&]
{
world w;
for(int i = 0; i < LARGE; ++i)
{
w.make_entity();
}
return w.entity_count();
});
};
BENCHMARK_ADVANCED("Create entities with components [1000]")(
Catch::Benchmark::Chronometer meter)
{
meter.measure(
[&]
{
world w;
for(int i = 0; i < SMALL; ++i)
{
auto e = w.make_entity();
w.set<Position>(e, {1.0f, 2.0f});
w.set<Velocity>(e, {3.0f, 4.0f});
}
return w.total_component_count();
});
};
BENCHMARK_ADVANCED("Create entities with components [5000]")(
Catch::Benchmark::Chronometer meter)
{
meter.measure(
[&]
{
world w;
for(int i = 0; i < MEDIUM; ++i)
{
auto e = w.make_entity();
w.set<Position>(e, {1.0f, 2.0f});
w.set<Velocity>(e, {3.0f, 4.0f});
}
return w.total_component_count();
});
};
BENCHMARK_ADVANCED("Create entities with components [10000]")(
Catch::Benchmark::Chronometer meter)
{
meter.measure(
[&]
{
world w;
for(int i = 0; i < LARGE; ++i)
{
auto e = w.make_entity();
w.set<Position>(e, {1.0f, 2.0f});
w.set<Velocity>(e, {3.0f, 4.0f});
}
return w.total_component_count();
});
};
}
TEST_CASE("Component operations benchmarks", "[benchmark]")
{
BENCHMARK_ADVANCED("Add component to existing entities [1000]")(
Catch::Benchmark::Chronometer meter)
{
world w;
std::vector<entity_id> entities;
for(int i = 0; i < SMALL; ++i)
{
entities.push_back(w.make_entity());
}
meter.measure(
[&]
{
for(auto e: entities)
{
w.set<Health>(e, {100});
}
return w.component_count<Health>();
});
};
BENCHMARK_ADVANCED("Add component to existing entities [5000]")(
Catch::Benchmark::Chronometer meter)
{
world w;
std::vector<entity_id> entities;
for(int i = 0; i < MEDIUM; ++i)
{
entities.push_back(w.make_entity());
}
meter.measure(
[&]
{
for(auto e: entities)
{
w.set<Health>(e, {100});
}
return w.component_count<Health>();
});
};
BENCHMARK_ADVANCED("Add component to existing entities [10000]")(
Catch::Benchmark::Chronometer meter)
{
world w;
std::vector<entity_id> entities;
for(int i = 0; i < LARGE; ++i)
{
entities.push_back(w.make_entity());
}
meter.measure(
[&]
{
for(auto e: entities)
{
w.set<Health>(e, {100});
}
return w.component_count<Health>();
});
};
BENCHMARK_ADVANCED("Remove component from entities [1000]")(
Catch::Benchmark::Chronometer meter)
{
world w;
std::vector<entity_id> entities;
for(int i = 0; i < SMALL; ++i)
{
auto e = w.make_entity();
w.set<Health>(e, {100});
entities.push_back(e);
}
meter.measure(
[&]
{
for(auto e: entities)
{
w.remove<Health>(e);
}
return w.component_count<Health>();
});
};
BENCHMARK_ADVANCED("Remove component from entities [5000]")(
Catch::Benchmark::Chronometer meter)
{
world w;
std::vector<entity_id> entities;
for(int i = 0; i < MEDIUM; ++i)
{
auto e = w.make_entity();
w.set<Health>(e, {100});
entities.push_back(e);
}
meter.measure(
[&]
{
for(auto e: entities)
{
w.remove<Health>(e);
}
return w.component_count<Health>();
});
};
BENCHMARK_ADVANCED("Remove component from entities [10000]")(
Catch::Benchmark::Chronometer meter)
{
world w;
std::vector<entity_id> entities;
for(int i = 0; i < LARGE; ++i)
{
auto e = w.make_entity();
w.set<Health>(e, {100});
entities.push_back(e);
}
meter.measure(
[&]
{
for(auto e: entities)
{
w.remove<Health>(e);
}
return w.component_count<Health>();
});
};
}
TEST_CASE("Query performance benchmarks", "[benchmark]")
{
BENCHMARK_ADVANCED("Dense query (90% match)")(
Catch::Benchmark::Chronometer meter)
{
world w;
for(int i = 0; i < LARGE; ++i)
{
auto e = w.make_entity();
w.set<Position>(e);
if(i % 10 != 0)
w.set<Velocity>(e); // 90% match
}
meter.measure(
[&]
{
int count = 0;
w.query<Position, Velocity>([&](auto...) { count++; });
return count;
});
};
BENCHMARK_ADVANCED("Sparse query (10% match)")(
Catch::Benchmark::Chronometer meter)
{
world w;
for(int i = 0; i < LARGE; ++i)
{
auto e = w.make_entity();
w.set<Position>(e);
if(i % 10 == 0)
w.set<Velocity>(e); // 10% match
}
meter.measure(
[&]
{
int count = 0;
w.query<Position, Velocity>([&](auto...) { count++; });
return count;
});
};
}
TEST_CASE("Memory intensive benchmarks", "[benchmark]")
{
BENCHMARK_ADVANCED("Large component allocation")(
Catch::Benchmark::Chronometer meter)
{
meter.measure(
[&]
{
world w;
for(int i = 0; i < SMALL; ++i)
{
auto e = w.make_entity();
w.set<BigData>(e);
}
return w.total_component_count();
});
};
BENCHMARK_ADVANCED("Component memory reuse")(
Catch::Benchmark::Chronometer meter)
{
world w;
std::vector<entity_id> entities;
for(int i = 0; i < SMALL; ++i)
{
auto e = w.make_entity();
w.set<BigData>(e);
entities.push_back(e);
}
meter.measure(
[&]
{
// Remove and re-add components
for(auto e: entities)
{
w.remove<BigData>(e);
w.set<BigData>(e);
}
return w.component_count<BigData>();
});
};
}
TEST_CASE("Archetype transition benchmarks", "[benchmark]")
{
BENCHMARK_ADVANCED("Single component addition")(
Catch::Benchmark::Chronometer meter)
{
world w;
std::vector<entity_id> entities;
for(int i = 0; i < MEDIUM; ++i)
{
entities.push_back(w.make_entity());
}
meter.measure(
[&]
{
for(auto e: entities)
{
w.set<Health>(e);
}
return w.archetype_count();
});
};
BENCHMARK_ADVANCED("Multi-component transition")(
Catch::Benchmark::Chronometer meter)
{
world w;
std::vector<entity_id> entities;
for(int i = 0; i < MEDIUM; ++i)
{
auto e = w.make_entity();
w.set<Position>(e);
entities.push_back(e);
}
meter.measure(
[&]
{
for(auto e: entities)
{
w.set<Velocity>(e);
w.set<Health>(e);
}
return w.archetype_count();
});
};
}

View file

@ -97,7 +97,7 @@ namespace zecsy
std::unordered_map<comp_id, component_pool> pools; std::unordered_map<comp_id, component_pool> pools;
using archetype_signature = std::vector<comp_id>; using archetype_signature = std::vector<comp_id>;
using entity_group = std::vector<entity_id>; using entity_group = std::set<entity_id>;
struct archetype_hash struct archetype_hash
{ {
@ -186,7 +186,7 @@ namespace zecsy
std::vector<comp_id> key; std::vector<comp_id> key;
auto& group = archetypes[key]; auto& group = archetypes[key];
group.emplace_back(id); group.emplace(id);
return id; return id;
} }
@ -199,7 +199,7 @@ namespace zecsy
std::vector<comp_id> key(comp_set.begin(), comp_set.end()); std::vector<comp_id> key(comp_set.begin(), comp_set.end());
auto& group = archetypes[key]; auto& group = archetypes[key];
group.erase(std::remove(group.begin(), group.end(), e), group.end()); group.erase(e);
if(archetypes[key].empty()) if(archetypes[key].empty())
{ {
@ -279,8 +279,7 @@ namespace zecsy
std::vector<comp_id> old_key(old_set.begin(), old_set.end()); std::vector<comp_id> old_key(old_set.begin(), old_set.end());
auto& group = archetypes[old_key]; auto& group = archetypes[old_key];
group.erase(std::remove(group.begin(), group.end(), e), group.erase(e);
group.end());
if(archetypes[old_key].empty()) if(archetypes[old_key].empty())
{ {
@ -288,7 +287,7 @@ namespace zecsy
} }
std::vector<comp_id> new_key(comp_set.begin(), comp_set.end()); std::vector<comp_id> new_key(comp_set.begin(), comp_set.end());
archetypes[new_key].emplace_back(e); archetypes[new_key].emplace(e);
} }
} }
@ -311,13 +310,12 @@ namespace zecsy
std::sort(old_key.begin(), old_key.end()); std::sort(old_key.begin(), old_key.end());
auto& old_group = archetypes[old_key]; auto& old_group = archetypes[old_key];
old_group.erase(std::remove(old_group.begin(), old_group.end(), e), old_group.erase(e);
old_group.end());
std::vector<comp_id> new_key(comp_set.begin(), comp_set.end()); std::vector<comp_id> new_key(comp_set.begin(), comp_set.end());
std::sort(new_key.begin(), new_key.end()); std::sort(new_key.begin(), new_key.end());
archetypes[new_key].emplace_back(e); archetypes[new_key].emplace(e);
if(archetypes[old_key].empty()) if(archetypes[old_key].empty())
{ {