Skip to content

Commit

Permalink
Changed: IteratorXYZ renamed to IterXYZ
Browse files Browse the repository at this point in the history
Tweaked: Iteration benchmark
Tweaked: Uncached queries readme
  • Loading branch information
richardbiely committed Dec 15, 2023
1 parent ed99fd6 commit 63a53af
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 176 deletions.
54 changes: 32 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
* [Component presence](#component-presence)
* [Data processing](#data-processing)
* [Query](#query)
* [Uncached query](#uncached-query)
* [Iteration](#iteration)
* [Constraints](#constraints)
* [Change detection](#change-detection)
Expand Down Expand Up @@ -359,7 +360,7 @@ auto velCopy = w.get<Velocity>(e);
Both read and write operations are also accessible via views. Check the [iteration](#iteration) sections to see how.

### Component presence
Whether or not a certain component is associated with an entity can be checked in two different ways. Either via an instance of a World object or by the means of ***Iterator*** which can be acquired when running [queries](#query).
Whether or not a certain component is associated with an entity can be checked in two different ways. Either via an instance of a World object or by the means of ***Iter*** which can be acquired when running [queries](#query).

```cpp
// Check if entity e has Velocity (via world).
Expand All @@ -370,7 +371,7 @@ const bool hasWheel = w.has(car, wheel);

// Check if entities hidden behind the iterator have Velocity (via iterator).
ecs::Query q = w.query().any<Position, Velocity>();
q.each([&](ecs::Iterator iter) {
q.each([&](ecs::Iter iter) {
const bool hasPosition = iter.has<Position>();
const bool hasVelocity = iter.has<Velocity>();
...
Expand All @@ -385,7 +386,7 @@ auto v = w.add<Velocity>().entity;

// Check if entities hidden behind the iterator have Velocity (via iterator).
ecs::Query q = w.query().any(p).any(v);
q.each([&](ecs::Iterator iter) {
q.each([&](ecs::Iter iter) {
const bool hasPosition = iter.has(p);
const bool hasVelocity = iter.has(v);
...
Expand All @@ -396,7 +397,9 @@ q.each([&](ecs::Iterator iter) {
### Query
For querying data you can use a Query. It can help you find all entities, components or chunks matching the specified set of components and constraints and iterate or return them in the form of an array. You can also use them to quickly check if any entities satisfying your requirements exist or calculate how many of them there are.

>**NOTE:**<br/>Every Query creates a cache internally. Therefore, the first usage is a little bit slower than the subsequent usage is going to be. You likely use the same query multiple times in your program, often without noticing. Because of that, caching becomes useful as it avoids wasting memory and performance when finding matches.
Every Query is cached internally. You likely use the same query multiple times in your program, often without noticing. Because of that, caching becomes useful as it avoids wasting memory and performance when finding matches.

Note, the first Query invocation is always slower than the subsequent ones because internals of the Query need to be initialized.

```cpp
ecs::Query q = w.query();
Expand Down Expand Up @@ -444,13 +447,6 @@ q.all<Position&>()
.none<Player>();
```

All queries are cached by default. This makes sense for queries which happen very often. They are fast to process but might take more time to prepare initially. If caching is not needed you should use uncached queries and save some resources. You would normally do this for one-time initializations or rarely used operations.

```cpp
// Create an uncached query taking into account all entities with either Positon or Velocity components
ecs::QueryUncached q = w.query<false>().any<Position, Velocity>();
```

Queries can be defined using a low-level API (used internally).

```cpp
Expand All @@ -472,7 +468,7 @@ q.add({p, QueryOp::All, QueryAccess::Write})
.add({pl, QueryOp::None, QueryAccess::None});
```
Another way to define queries is using string notation. This allows you to define the entire query or its parts using a string composed of simple expressions. Any spaces in between modifiers and expressions are trimmed.
Another way to define queries is using the string notation. This allows you to define the entire query or its parts using a string composed of simple expressions. Any spaces in between modifiers and expressions are trimmed.
Supported modifiers:
* ***;*** - separates expressions
Expand Down Expand Up @@ -510,6 +506,20 @@ ecs::Query q2 = w.query()
.all(player);
```

### Uncached query
From the implementation standpoint, uncached queries are the same as ordinary queries in all but one aspect - they do not use the query cache internally. This means that two uncached queries using the same setup are going to evaluate matches separately. This means duplicates are possible and therefore more memory and performance can be wasted. However, if you design your queries carefully and they are all different, uncached queries are actually a bit faster to create and match. Creation is faster because there is no hash to compute and matching is faster because no query cache lookups are involved.

Uncached queries are created via ***World::query< false >***.

```cpp
// This is a cached query
ecs::Query q1 = w.query<true>(). ...;
// This is a cached query, shorter version of the above
ecs::Query q2 = w.query(). ...;
// This is an uncached query
ecs::QueryUncached q3 = w.query<false>(). ...;
```

### Iteration
To process data from queries one uses the ***Query::each*** function.
It accepts either a list of components or an iterator as its argument.
Expand All @@ -532,18 +542,18 @@ q.each([&](Position& p, const Velocity& v) {
>**NOTE:**<br/>Iterating over components not present in the query is not supported and results in asserts and undefined behavior. This is done to prevent various logic errors which might sneak in otherwise.
Processing via an iterator gives you even more expressive power and also opens doors for new kinds of optimizations.
Iterator is an abstraction above underlying data structures and gives you access to their public API.
Iter is an abstraction above underlying data structures and gives you access to their public API.

There are three types of iterators:
1) ***Iterator*** - iterates over enabled entities
2) ***IteratorDisabled*** - iterates over distabled entities
3) ***IteratorAll*** - iterate over all entities
1) ***Iter*** - iterates over enabled entities
2) ***IterDisabled*** - iterates over distabled entities
3) ***IterAll*** - iterate over all entities

```cpp
ecs::Query q = w.query();
q.all<Position&, Velocity>();

q.each([](ecs::IteratorAll iter) {
q.each([](ecs::IterAll iter) {
auto p = iter.view_mut<Position>(); // Read-write access to Position
auto v = iter.view<Velocity>(); // Read-only access to Velocity

Expand Down Expand Up @@ -607,17 +617,17 @@ q.arr(entities, ecs::Query::Constraint::AcceptAll);
// Fills the array with only e1 because e1 is disabled.
q.arr(entities, ecs::Query::Constraint::DisabledOnly);

q.each([](ecs::Iterator iter) {
q.each([](ecs::Iter iter) {
auto p = iter.view_mut<Position>(); // Read-Write access to Position
// Iterates over enabled entities
GAIA_EACH(iter) p[i] = {}; // reset the position of each enabled entity
});
q.each([](ecs::IteratorDisabled iter) {
q.each([](ecs::IterDisabled iter) {
auto p = iter.view_mut<Position>(); // Read-Write access to Position
// Iterates over disabled entities
GAIA_EACH(iter) p[i] = {}; // reset the position of each disabled entity
});
q.each([](ecs::IteratorAll iter) {
q.each([](ecs::IterAll iter) {
auto p = iter.view_mut<Position>(); // Read-Write access to Position
// Iterates over all entities
GAIA_EACH(iter) {
Expand All @@ -638,7 +648,7 @@ struct Disabled {};
e.add<Disabled>(); // disable entity

ecs::Query q = w.query().all<Position, Disabled>;
q.each([&](ecs::Iterator iter){
q.each([&](ecs::Iter iter){
// Processes all disabled entities
});

Expand Down Expand Up @@ -985,7 +995,7 @@ struct VelocitySoA {
...

ecs::Query q = w.query().all<PositionSoA&, VelocitySoA>;
q.each([](ecs::Iterator iter) {
q.each([](ecs::Iter iter) {
// Position
auto vp = iter.view_mut<PositionSoA>(); // read-write access to PositionSoA
auto px = vp.set<0>(); // continuous block of "x" from PositionSoA
Expand Down
10 changes: 5 additions & 5 deletions include/gaia/ecs/chunk_iterator.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ namespace gaia {

namespace detail {
template <Constraints IterConstraint>
class ChunkIteratorImpl {
class ChunkIterImpl {
protected:
Chunk& m_chunk;

public:
ChunkIteratorImpl(Chunk& chunk): m_chunk(chunk) {}
ChunkIterImpl(Chunk& chunk): m_chunk(chunk) {}

//! Returns a read-only entity or component view.
//! \warning If \tparam T is a component it is expected it is present. Undefined behavior otherwise.
Expand Down Expand Up @@ -121,8 +121,8 @@ namespace gaia {
};
} // namespace detail

using Iterator = detail::ChunkIteratorImpl<Constraints::EnabledOnly>;
using IteratorDisabled = detail::ChunkIteratorImpl<Constraints::DisabledOnly>;
using IteratorAll = detail::ChunkIteratorImpl<Constraints::AcceptAll>;
using Iter = detail::ChunkIterImpl<Constraints::EnabledOnly>;
using IterDisabled = detail::ChunkIterImpl<Constraints::DisabledOnly>;
using IterAll = detail::ChunkIterImpl<Constraints::AcceptAll>;
} // namespace ecs
} // namespace gaia
72 changes: 36 additions & 36 deletions include/gaia/ecs/query.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ namespace gaia {
//! QueryImpl cache id
QueryId m_queryId = QueryIdBad;
//! QueryImpl cache (stable pointer to parent world's query cache)
QueryCache* m_entityQueryCache{};
QueryCache* m_queryCache{};
};

template <>
Expand Down Expand Up @@ -179,12 +179,12 @@ namespace gaia {

if constexpr (UseCaching) {
// Make sure the query was created by World.query()
GAIA_ASSERT(m_storage.m_entityQueryCache != nullptr);
GAIA_ASSERT(m_storage.m_queryCache != nullptr);

// If queryId is set it means QueryInfo was already created.
// Because caching is used, we expect this to be the common case.
if GAIA_LIKELY (m_storage.m_queryId != QueryIdBad) {
auto& queryInfo = m_storage.m_entityQueryCache->get(m_storage.m_queryId);
auto& queryInfo = m_storage.m_queryCache->get(m_storage.m_queryId);
queryInfo.match(*m_entityToArchetypeMap, last_archetype_id());
return queryInfo;
}
Expand All @@ -193,7 +193,7 @@ namespace gaia {
QueryCtx ctx;
ctx.cc = &comp_cache_mut(*m_world);
commit(ctx);
auto& queryInfo = m_storage.m_entityQueryCache->add(GAIA_MOV(ctx));
auto& queryInfo = m_storage.m_queryCache->add(GAIA_MOV(ctx));
m_storage.m_queryId = queryInfo.id();
queryInfo.match(*m_entityToArchetypeMap, last_archetype_id());
return queryInfo;
Expand Down Expand Up @@ -595,7 +595,7 @@ namespace gaia {
m_world(&world),
m_serBuffer(&comp_cache_mut(world)), m_nextArchetypeId(&nextArchetypeId), m_worldVersion(&worldVersion),
m_archetypes(&archetypes), m_entityToArchetypeMap(&entityToArchetypeMap) {
m_storage.m_entityQueryCache = &queryCache;
m_storage.m_queryCache = &queryCache;
}

template <bool FuncEnabled = !UseCaching>
Expand Down Expand Up @@ -777,26 +777,26 @@ namespace gaia {
GAIA_ASSERT(unpack_args_into_query_has_all(queryInfo, InputArgs{}));
#endif

run_query_on_chunks<Iterator>(queryInfo, [&](Chunk& chunk) {
run_query_on_chunk<Iterator>(chunk, func, InputArgs{});
run_query_on_chunks<Iter>(queryInfo, [&](Chunk& chunk) {
run_query_on_chunk<Iter>(chunk, func, InputArgs{});
});
}

template <typename Func>
void each(Func func) {
auto& queryInfo = fetch();

if constexpr (std::is_invocable_v<Func, IteratorAll>)
run_query_on_chunks<IteratorAll>(queryInfo, [&](Chunk& chunk) {
func(IteratorAll(chunk));
if constexpr (std::is_invocable_v<Func, IterAll>)
run_query_on_chunks<IterAll>(queryInfo, [&](Chunk& chunk) {
func(IterAll(chunk));
});
else if constexpr (std::is_invocable_v<Func, Iterator>)
run_query_on_chunks<Iterator>(queryInfo, [&](Chunk& chunk) {
func(Iterator(chunk));
else if constexpr (std::is_invocable_v<Func, Iter>)
run_query_on_chunks<Iter>(queryInfo, [&](Chunk& chunk) {
func(Iter(chunk));
});
else if constexpr (std::is_invocable_v<Func, IteratorDisabled>)
run_query_on_chunks<IteratorDisabled>(queryInfo, [&](Chunk& chunk) {
func(IteratorDisabled(chunk));
else if constexpr (std::is_invocable_v<Func, IterDisabled>)
run_query_on_chunks<IterDisabled>(queryInfo, [&](Chunk& chunk) {
func(IterDisabled(chunk));
});
else
each(queryInfo, func);
Expand All @@ -805,10 +805,10 @@ namespace gaia {
template <typename Func, bool FuncEnabled = UseCaching, typename std::enable_if<FuncEnabled>::type* = nullptr>
void each(QueryId queryId, Func func) {
// Make sure the query was created by World.query()
GAIA_ASSERT(m_storage.m_entityQueryCache != nullptr);
GAIA_ASSERT(m_storage.m_queryCache != nullptr);
GAIA_ASSERT(queryId != QueryIdBad);

auto& queryInfo = m_storage.m_entityQueryCache->get(queryId);
auto& queryInfo = m_storage.m_queryCache->get(queryId);
each(queryInfo, func);
}

Expand All @@ -828,35 +828,35 @@ namespace gaia {
switch (constraints) {
case Constraints::EnabledOnly: {
for (auto* pArchetype: queryInfo)
if (empty_inter<true, Iterator>(queryInfo, pArchetype->chunks()))
if (empty_inter<true, Iter>(queryInfo, pArchetype->chunks()))
return false;
} break;
case Constraints::DisabledOnly: {
for (auto* pArchetype: queryInfo)
if (empty_inter<true, IteratorDisabled>(queryInfo, pArchetype->chunks()))
if (empty_inter<true, IterDisabled>(queryInfo, pArchetype->chunks()))
return false;
} break;
case Constraints::AcceptAll: {
for (auto* pArchetype: queryInfo)
if (empty_inter<true, IteratorAll>(queryInfo, pArchetype->chunks()))
if (empty_inter<true, IterAll>(queryInfo, pArchetype->chunks()))
return false;
} break;
}
} else {
switch (constraints) {
case Constraints::EnabledOnly: {
for (auto* pArchetype: queryInfo)
if (empty_inter<false, Iterator>(queryInfo, pArchetype->chunks()))
if (empty_inter<false, Iter>(queryInfo, pArchetype->chunks()))
return false;
} break;
case Constraints::DisabledOnly: {
for (auto* pArchetype: queryInfo)
if (empty_inter<false, IteratorDisabled>(queryInfo, pArchetype->chunks()))
if (empty_inter<false, IterDisabled>(queryInfo, pArchetype->chunks()))
return false;
} break;
case Constraints::AcceptAll: {
for (auto* pArchetype: queryInfo)
if (empty_inter<false, IteratorAll>(queryInfo, pArchetype->chunks()))
if (empty_inter<false, IterAll>(queryInfo, pArchetype->chunks()))
return false;
} break;
}
Expand All @@ -882,30 +882,30 @@ namespace gaia {
switch (constraints) {
case Constraints::EnabledOnly: {
for (auto* pArchetype: queryInfo)
entCnt += count_inter<true, Iterator>(queryInfo, pArchetype->chunks());
entCnt += count_inter<true, Iter>(queryInfo, pArchetype->chunks());
} break;
case Constraints::DisabledOnly: {
for (auto* pArchetype: queryInfo)
entCnt += count_inter<true, IteratorDisabled>(queryInfo, pArchetype->chunks());
entCnt += count_inter<true, IterDisabled>(queryInfo, pArchetype->chunks());
} break;
case Constraints::AcceptAll: {
for (auto* pArchetype: queryInfo)
entCnt += count_inter<true, IteratorAll>(queryInfo, pArchetype->chunks());
entCnt += count_inter<true, IterAll>(queryInfo, pArchetype->chunks());
} break;
}
} else {
switch (constraints) {
case Constraints::EnabledOnly: {
for (auto* pArchetype: queryInfo)
entCnt += count_inter<false, Iterator>(queryInfo, pArchetype->chunks());
entCnt += count_inter<false, Iter>(queryInfo, pArchetype->chunks());
} break;
case Constraints::DisabledOnly: {
for (auto* pArchetype: queryInfo)
entCnt += count_inter<false, IteratorDisabled>(queryInfo, pArchetype->chunks());
entCnt += count_inter<false, IterDisabled>(queryInfo, pArchetype->chunks());
} break;
case Constraints::AcceptAll: {
for (auto* pArchetype: queryInfo)
entCnt += count_inter<false, IteratorAll>(queryInfo, pArchetype->chunks());
entCnt += count_inter<false, IterAll>(queryInfo, pArchetype->chunks());
} break;
}
}
Expand Down Expand Up @@ -933,30 +933,30 @@ namespace gaia {
switch (constraints) {
case Constraints::EnabledOnly:
for (auto* pArchetype: queryInfo)
arr_inter<true, Iterator>(queryInfo, pArchetype->chunks(), outArray);
arr_inter<true, Iter>(queryInfo, pArchetype->chunks(), outArray);
break;
case Constraints::DisabledOnly:
for (auto* pArchetype: queryInfo)
arr_inter<true, IteratorDisabled>(queryInfo, pArchetype->chunks(), outArray);
arr_inter<true, IterDisabled>(queryInfo, pArchetype->chunks(), outArray);
break;
case Constraints::AcceptAll:
for (auto* pArchetype: queryInfo)
arr_inter<true, IteratorAll>(queryInfo, pArchetype->chunks(), outArray);
arr_inter<true, IterAll>(queryInfo, pArchetype->chunks(), outArray);
break;
}
} else {
switch (constraints) {
case Constraints::EnabledOnly:
for (auto* pArchetype: queryInfo)
arr_inter<false, Iterator>(queryInfo, pArchetype->chunks(), outArray);
arr_inter<false, Iter>(queryInfo, pArchetype->chunks(), outArray);
break;
case Constraints::DisabledOnly:
for (auto* pArchetype: queryInfo)
arr_inter<false, IteratorDisabled>(queryInfo, pArchetype->chunks(), outArray);
arr_inter<false, IterDisabled>(queryInfo, pArchetype->chunks(), outArray);
break;
case Constraints::AcceptAll:
for (auto* pArchetype: queryInfo)
arr_inter<false, IteratorAll>(queryInfo, pArchetype->chunks(), outArray);
arr_inter<false, IterAll>(queryInfo, pArchetype->chunks(), outArray);
break;
}
}
Expand Down
Loading

0 comments on commit 63a53af

Please sign in to comment.