Helios Engine 0.1.0
A modular ECS based data-oriented C++23 game engine
 
Loading...
Searching...
No Matches
query_cache.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/core_pch.hpp>
4
6
7#include <boost/unordered/concurrent_node_map.hpp>
8
9#include <atomic>
10#include <cstddef>
11#include <functional>
12#include <span>
13#include <utility>
14#include <vector>
15
16namespace helios::ecs::details {
17
18class Archetype;
19
20/**
21 * @brief Query state that caches archetype matching results.
22 * @details Inspired by Bevy's QueryState, this stores pre-computed information
23 * about which archetypes match a specific query signature. Each QueryState
24 * corresponds to a unique combination of required and forbidden components.
25 *
26 * The state includes:
27 * - List of matching archetypes (cached)
28 * - Archetype generation counters (for invalidation detection)
29 * - Component type signature (for efficient lookup)
30 */
31struct QueryState {
32 std::vector<std::reference_wrapper<const Archetype>> matching_archetypes; ///< Archetypes that match this query
33 std::vector<size_t> archetype_generations; ///< Generation of each matched archetype when cached
34 std::vector<ComponentTypeId> with_component_types; ///< Required component types (sorted)
35 std::vector<ComponentTypeId> without_component_types; ///< Forbidden component types (sorted)
36 size_t query_generation = 0; ///< Generation when this state was computed
37 size_t query_hash = 0; ///< Hash of the query signature
38 mutable std::atomic<size_t> last_access_time{0}; ///< Last access timestamp for LRU eviction
39
44
47};
48
57
59 : matching_archetypes(std::move(other.matching_archetypes)),
60 archetype_generations(std::move(other.archetype_generations)),
61 with_component_types(std::move(other.with_component_types)),
62 without_component_types(std::move(other.without_component_types)),
63 query_generation(other.query_generation),
64 query_hash(other.query_hash),
65 last_access_time(other.last_access_time.load(std::memory_order_relaxed)) {}
66
68 if (this == &other) [[unlikely]] {
69 return *this;
70 }
71
77 query_hash = other.query_hash;
78 last_access_time.store(other.last_access_time.load(std::memory_order_relaxed), std::memory_order_relaxed);
79
80 return *this;
81}
82
83inline QueryState& QueryState::operator=(QueryState&& other) noexcept {
84 if (this == &other) [[unlikely]] {
85 return *this;
86 }
87
88 matching_archetypes = std::move(other.matching_archetypes);
89 archetype_generations = std::move(other.archetype_generations);
90 with_component_types = std::move(other.with_component_types);
91 without_component_types = std::move(other.without_component_types);
92 query_generation = other.query_generation;
93 query_hash = other.query_hash;
94 last_access_time.store(other.last_access_time.load(std::memory_order_relaxed), std::memory_order_relaxed);
95
96 return *this;
97}
98
99/**
100 * @brief Statistics for query cache performance.
101 * @details Tracks cache effectiveness metrics including hits, misses, and invalidations.
102 * @note Thread-safe.
103 */
105 std::atomic<size_t> hit_count{0}; ///< Number of cache hits
106 std::atomic<size_t> miss_count{0}; ///< Number of cache misses
107 std::atomic<size_t> invalidation_count{0}; ///< Number of cache invalidations
108 std::atomic<size_t> total_queries{0}; ///< Total number of queries executed
109 std::atomic<size_t> archetype_changes{0}; ///< Number of archetype structural changes
110 std::atomic<size_t> partial_invalidations{0}; ///< Number of partial (component-specific) invalidations
111
116
119
120 void Reset() noexcept;
121
122 [[nodiscard]] double HitRate() const noexcept;
123};
124
126 : hit_count(other.hit_count.load(std::memory_order_relaxed)),
127 miss_count(other.miss_count.load(std::memory_order_relaxed)),
128 invalidation_count(other.invalidation_count.load(std::memory_order_relaxed)),
129 total_queries(other.total_queries.load(std::memory_order_relaxed)),
130 archetype_changes(other.archetype_changes.load(std::memory_order_relaxed)),
131 partial_invalidations(other.partial_invalidations.load(std::memory_order_relaxed)) {}
132
133inline QueryCacheStats::QueryCacheStats(QueryCacheStats&& other) noexcept
134 : hit_count(other.hit_count.load(std::memory_order_relaxed)),
135 miss_count(other.miss_count.load(std::memory_order_relaxed)),
136 invalidation_count(other.invalidation_count.load(std::memory_order_relaxed)),
137 total_queries(other.total_queries.load(std::memory_order_relaxed)),
138 archetype_changes(other.archetype_changes.load(std::memory_order_relaxed)),
139 partial_invalidations(other.partial_invalidations.load(std::memory_order_relaxed)) {}
140
141inline QueryCacheStats& QueryCacheStats::operator=(const QueryCacheStats& other) {
142 if (this == &other) [[unlikely]] {
143 return *this;
144 }
145
146 hit_count.store(other.hit_count.load(std::memory_order_relaxed), std::memory_order_relaxed);
147 miss_count.store(other.miss_count.load(std::memory_order_relaxed), std::memory_order_relaxed);
148 invalidation_count.store(other.invalidation_count.load(std::memory_order_relaxed), std::memory_order_relaxed);
149 total_queries.store(other.total_queries.load(std::memory_order_relaxed), std::memory_order_relaxed);
150 archetype_changes.store(other.archetype_changes.load(std::memory_order_relaxed), std::memory_order_relaxed);
151 partial_invalidations.store(other.partial_invalidations.load(std::memory_order_relaxed), std::memory_order_relaxed);
152
153 return *this;
154}
155
156inline QueryCacheStats& QueryCacheStats::operator=(QueryCacheStats&& other) noexcept {
157 if (this == &other) [[unlikely]] {
158 return *this;
159 }
160
161 hit_count.store(other.hit_count.load(std::memory_order_relaxed), std::memory_order_relaxed);
162 miss_count.store(other.miss_count.load(std::memory_order_relaxed), std::memory_order_relaxed);
163 invalidation_count.store(other.invalidation_count.load(std::memory_order_relaxed), std::memory_order_relaxed);
164 total_queries.store(other.total_queries.load(std::memory_order_relaxed), std::memory_order_relaxed);
165 archetype_changes.store(other.archetype_changes.load(std::memory_order_relaxed), std::memory_order_relaxed);
166 partial_invalidations.store(other.partial_invalidations.load(std::memory_order_relaxed), std::memory_order_relaxed);
167
168 return *this;
169}
170
171inline void QueryCacheStats::Reset() noexcept {
172 hit_count.store(0, std::memory_order_relaxed);
173 miss_count.store(0, std::memory_order_relaxed);
174 invalidation_count.store(0, std::memory_order_relaxed);
175 total_queries.store(0, std::memory_order_relaxed);
176 archetype_changes.store(0, std::memory_order_relaxed);
177 partial_invalidations.store(0, std::memory_order_relaxed);
178}
179
180inline double QueryCacheStats::HitRate() const noexcept {
181 const size_t total = total_queries.load(std::memory_order_relaxed);
182 if (total == 0) [[unlikely]] {
183 return 0.0;
184 }
185
186 const size_t hits = hit_count.load(std::memory_order_relaxed);
187 return static_cast<double>(hits) / static_cast<double>(total);
188}
189
190/**
191 * @brief Manages query state caching with Bevy-inspired optimizations.
192 * @details Provides efficient caching of archetype matching results with smart invalidation.
193 *
194 * Key features:
195 * - Archetype generation tracking: Each archetype has a generation counter that increments
196 * on structural changes. Query states cache these generations and can detect stale data.
197 * - Component-aware invalidation: Only queries using specific components are invalidated
198 * when those components are modified.
199 * - Query state reuse: Pre-computed query metadata is cached and reused across frames.
200 * - Efficient validation: Quick checks using generation counters avoid full re-computation.
201 *
202 * @note Thread-safe for read operations, single-writer for updates.
203 */
205public:
206 QueryCacheManager() = default;
208 QueryCacheManager(QueryCacheManager&& other) noexcept;
210
212 QueryCacheManager& operator=(QueryCacheManager&& other) noexcept;
213
214 /**
215 * @brief Clears all cached query states and resets statistics.
216 */
217 void Clear() noexcept;
218
219 /**
220 * @brief Resets cache statistics without clearing query states.
221 */
222 void ResetStats() noexcept { stats_.Reset(); }
223
224 /**
225 * @brief Tries to retrieve a valid cached query state.
226 * @details Checks if cached state exists and validates it against current archetype generations.
227 * If state is stale (archetypes changed), returns nullptr to trigger re-computation.
228 * @param with_components Component types that must be present (must be sorted)
229 * @param without_components Component types that must not be present (must be sorted)
230 * @param current_generation Current world structural generation
231 * @return Pointer to valid cached state, or nullptr if cache miss/stale
232 */
233 [[nodiscard]] const QueryState* TryGetCache(std::span<const ComponentTypeId> with_components,
234 std::span<const ComponentTypeId> without_components,
235 size_t current_generation) const;
236
237 /**
238 * @brief Stores or updates a query state in the cache.
239 * @details Caches the matched archetypes along with their current generation counters.
240 * This allows future queries to validate the cache using generation checks.
241 * @param with_components Component types that must be present (must be sorted)
242 * @param without_components Component types that must not be present (must be sorted)
243 * @param matching_archetypes Archetypes that match the query criteria
244 * @param current_generation Current world structural generation
245 */
246 void StoreCache(std::span<const ComponentTypeId> with_components, std::span<const ComponentTypeId> without_components,
247 std::vector<std::reference_wrapper<const Archetype>> matching_archetypes, size_t current_generation);
248
249 /**
250 * @brief Invalidates all cached query states.
251 * @details Called when major structural changes occur that affect many queries.
252 * Increments global invalidation counter.
253 */
254 void InvalidateAll();
255
256 /**
257 * @brief Invalidates query states that involve specific components.
258 * @details Selective invalidation - only queries using the specified components
259 * need to be recomputed. More efficient than full invalidation.
260 * @param component_ids Component types that were modified
261 */
262 void InvalidateForComponents(std::span<const ComponentTypeId> component_ids);
263
264 /**
265 * @brief Notifies the cache of an archetype structural change.
266 * @details Increments archetype change counter for statistics tracking.
267 * Does not directly invalidate - queries validate themselves using generation checks.
268 */
269 void NotifyArchetypeChange() { ++stats_.archetype_changes; }
270
271 /**
272 * @brief Gets the number of cached query states.
273 * @return Cache entry count
274 */
275 [[nodiscard]] size_t CacheSize() const noexcept { return cache_.size(); }
276
277 /**
278 * @brief Gets cache performance statistics.
279 * @return Struct containing hit/miss counts and other metrics
280 */
282
283 /**
284 * @brief Validates a cached query state against current archetype generations.
285 * @details Compares cached archetype generations with current ones to detect
286 * if any archetypes have changed structure since caching.
287 * @param state Query state to validate
288 * @param current_generation Current world structural generation
289 * @return True if state is still valid, false if stale
290 */
291 [[nodiscard]] static bool ValidateQueryState(const QueryState& state, size_t current_generation);
292
293private:
294 /**
295 * @brief Updates access timestamp for LRU tracking.
296 * @param state Query state to update
297 */
298 void UpdateAccessTime(QueryState& state) const noexcept {
299 state.last_access_time.store(access_counter_.fetch_add(1, std::memory_order_relaxed), std::memory_order_relaxed);
300 }
301
302 /**
303 * @brief Computes hash for a query signature.
304 * @details Combines sorted component type lists into a single hash value.
305 * @param with_components Required components (must be sorted)
306 * @param without_components Forbidden components (must be sorted)
307 * @return Hash value representing the query signature
308 */
309 [[nodiscard]] static size_t ComputeQueryHash(std::span<const ComponentTypeId> with_components,
310 std::span<const ComponentTypeId> without_components);
311
312 /**
313 * @brief Checks if a query involves any of the specified components.
314 * @details Used for selective invalidation - determines if a query needs
315 * to be recomputed when specific components change.
316 * @param state Query state to check
317 * @param component_ids Component types to check against
318 * @return True if query uses any of the specified components
319 */
320 [[nodiscard]] static bool QueryInvolvesAnyComponents(const QueryState& state,
321 std::span<const ComponentTypeId> component_ids);
322
323 boost::unordered::concurrent_node_map<size_t, QueryState> cache_; ///< Hash -> query state mapping
324 mutable QueryCacheStats stats_; ///< Cache performance statistics
325 mutable std::atomic<size_t> access_counter_{0}; ///< Monotonic counter for LRU tracking
326};
327
328inline QueryCacheManager::QueryCacheManager(QueryCacheManager&& other) noexcept
329 : cache_(std::move(other.cache_)),
330 stats_(std::move(other.stats_)),
331 access_counter_(other.access_counter_.load(std::memory_order_relaxed)) {}
332
333inline QueryCacheManager& QueryCacheManager::operator=(QueryCacheManager&& other) noexcept {
334 if (this == &other) {
335 return *this;
336 }
337
338 cache_ = std::move(other.cache_);
339 stats_ = other.stats_;
340 access_counter_.store(other.access_counter_.load(std::memory_order_relaxed), std::memory_order_relaxed);
341
342 return *this;
343}
344
345inline void QueryCacheManager::Clear() noexcept {
346 cache_.clear();
347 stats_.Reset();
348 access_counter_.store(0, std::memory_order_relaxed);
349}
350
351inline const QueryState* QueryCacheManager::TryGetCache(std::span<const ComponentTypeId> with_components,
352 std::span<const ComponentTypeId> without_components,
353 size_t current_generation) const {
354 const size_t hash = ComputeQueryHash(with_components, without_components);
355 const QueryState* found = nullptr;
356
357 cache_.cvisit(hash, [this, &found, current_generation](const auto& pair) {
358 auto& state = const_cast<QueryState&>(pair.second);
359
360 // Validate state against current generation
361 if (!ValidateQueryState(state, current_generation)) {
362 return; // State is stale
363 }
364
365 UpdateAccessTime(state);
366 found = &state;
367 });
368
369 if (found == nullptr) {
370 ++stats_.miss_count;
371 } else {
372 ++stats_.hit_count;
373 }
374 ++stats_.total_queries;
375
376 return found;
377}
378
379inline void QueryCacheManager::InvalidateAll() {
380 cache_.clear();
381 ++stats_.invalidation_count;
382}
383
384inline void QueryCacheManager::InvalidateForComponents(std::span<const ComponentTypeId> component_ids) {
385 if (component_ids.empty()) {
386 return;
387 }
388
389 // Remove cache entries that involve any of the specified components
390 std::vector<size_t> keys_to_remove;
391 cache_.cvisit_all([&keys_to_remove, component_ids](const auto& pair) {
392 if (QueryInvolvesAnyComponents(pair.second, component_ids)) {
393 keys_to_remove.push_back(pair.first);
394 }
395 });
396
397 for (const size_t key : keys_to_remove) {
398 cache_.erase(key);
399 }
400
401 if (!keys_to_remove.empty()) {
402 stats_.partial_invalidations.fetch_add(keys_to_remove.size(), std::memory_order_relaxed);
403 }
404}
405
406} // namespace helios::ecs::details
Manages query state caching with Bevy-inspired optimizations.
void NotifyArchetypeChange()
Notifies the cache of an archetype structural change.
QueryCacheManager(const QueryCacheManager &)=delete
size_t CacheSize() const noexcept
Gets the number of cached query states.
QueryCacheManager & operator=(const QueryCacheManager &)=delete
QueryCacheStats Stats() const noexcept
Gets cache performance statistics.
BasicQuery< World, Allocator, Components... > Query
Type alias for query with mutable world access.
Definition query.hpp:2481
STL namespace.
Statistics for query cache performance.
std::atomic< size_t > archetype_changes
Number of archetype structural changes.
std::atomic< size_t > miss_count
Number of cache misses.
std::atomic< size_t > total_queries
Total number of queries executed.
std::atomic< size_t > invalidation_count
Number of cache invalidations.
std::atomic< size_t > partial_invalidations
Number of partial (component-specific) invalidations.
std::atomic< size_t > hit_count
Number of cache hits.
Query state that caches archetype matching results.
std::vector< ComponentTypeId > with_component_types
Required component types (sorted)
size_t query_generation
Generation when this state was computed.
std::vector< ComponentTypeId > without_component_types
Forbidden component types (sorted)
std::vector< size_t > archetype_generations
Generation of each matched archetype when cached.
size_t query_hash
Hash of the query signature.
std::vector< std::reference_wrapper< const Archetype > > matching_archetypes
Archetypes that match this query.
QueryState & operator=(const QueryState &other)
std::atomic< size_t > last_access_time
Last access timestamp for LRU eviction.