Files
godot-shader-experiments/addons/gecs/ecs/query_cache_key.gd
2026-01-15 15:27:48 +01:00

185 lines
7.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## QueryCacheKey
## ------------------------------------------------------------------------------
## PURPOSE
## Build a structural query signature (cache key) that is:
## * Order-insensitive inside each domain (with_all / with_any / with_none)
## * Order-sensitive ACROSS domains (the same component in different domains => different key)
## * Extremely fast (single allocation + contiguous integer writes)
## * Stable for the lifetime of loaded component scripts (uses script.instance_id)
##
## WHY NOT JUST MERGE & SORT?
## A naive approach merges all component IDs + domain markers then sorts. That destroys
## domain boundaries and lets these collide:
## with_all([A,B]) vs with_any([A,B])
## After a full sort both become the same multiset {1,2,3,A,B}. We prevent that by
## emitting DOMAIN MARKER then COUNT then the sorted IDs for that domain preserving
## domain structure while still being permutation-insensitive within the domain.
##
## LAYOUT (integers in final array):
## [ 1, |count_all|, sorted(all_ids)...,
## 2, |count_any|, sorted(any_ids)...,
## 3, |count_none|, sorted(ex_ids)... ]
## 1/2/3 : domain sentinels (ALL / ANY / NONE)
## count_* : disambiguates empty vs non-empty ( [] vs [X] ) and prevents boundary ambiguity
## sorted(ids) : order-insensitivity; identical sets different order => same run of ints
##
## COMPLEXITY
## Sorting dominates: O(a log a + y log y + n log n). Typical domain sizes are tiny.
## Allocation: exactly one integer array sized to final layout.
## Hash: Godot's native Array.hash() (64-bit) very fast.
##
## COLLISION PROFILE
## 64-bit space (~1.84e19). Even 1,000,000 distinct structural queries => ~2.7e-8 collision probability.
## Practically zero for real ECS usage. See PERFORMANCE_CACHE_KEY_NOTE.md for math.
##
## EXTENSION POINTS
## * Add a leading VERSION marker if the format evolves.
## * Add extra domains (e.g. relationship structure) by appending new marker + count + IDs.
## * Add enabled-state separation by injecting a synthetic domain marker (kept separate currently).
##
## INLINE COMMENT LEGEND
## all_ids / any_ids / ex_ids : per-domain sorted component script instance IDs
## total : exact integer count used for one-shot allocation (prevents incremental reallocation)
## layout[i] = marker/count/id : sequential write building final signature array
##
class_name QueryCacheKey
extends RefCounted
static func build(
all_components: Array,
any_components: Array,
exclude_components: Array,
relationships: Array = [],
exclude_relationships: Array = [],
groups: Array = [],
exclude_groups: Array = []
) -> int:
# Collect & sort per-domain IDs (order-insensitive inside each domain)
var all_ids: Array[int] = []
for c in all_components: all_ids.append(c.get_instance_id())
all_ids.sort()
var any_ids: Array[int] = []
for c in any_components: any_ids.append(c.get_instance_id())
any_ids.sort()
var ex_ids: Array[int] = []
for c in exclude_components: ex_ids.append(c.get_instance_id())
ex_ids.sort()
# Collect & sort relationship IDs
var rel_ids: Array[int] = []
for rel in relationships:
# Use Script instance ID for type matching (consistent with component queries)
# Relationship.new(C_TestB.new()) creates component instance, we want the Script's ID
if rel.relation:
rel_ids.append(rel.relation.get_script().get_instance_id())
else:
rel_ids.append(0)
# Handle target - use Script instance ID for Components (type matching)
if rel.target is Component:
# Component target: use Script instance ID for type matching
rel_ids.append(rel.target.get_script().get_instance_id())
elif rel.target is Entity:
# Entity target: use entity instance ID (entities are specific instances)
rel_ids.append(rel.target.get_instance_id())
elif rel.target is Script:
# Archetype target: use Script instance ID
rel_ids.append(rel.target.get_instance_id())
elif rel.target != null:
# Other types: use generic hash
rel_ids.append(rel.target.hash())
else:
rel_ids.append(0) # null target
rel_ids.sort()
var ex_rel_ids: Array[int] = []
for rel in exclude_relationships:
# Use Script instance ID for type matching (consistent with component queries)
if rel.relation:
ex_rel_ids.append(rel.relation.get_script().get_instance_id())
else:
ex_rel_ids.append(0)
# Handle target - use Script instance ID for Components (type matching)
if rel.target is Component:
ex_rel_ids.append(rel.target.get_script().get_instance_id())
elif rel.target is Entity:
ex_rel_ids.append(rel.target.get_instance_id())
elif rel.target is Script:
ex_rel_ids.append(rel.target.get_instance_id())
elif rel.target != null:
ex_rel_ids.append(rel.target.hash())
else:
ex_rel_ids.append(0)
ex_rel_ids.sort()
# Collect & sort group name hashes
var group_ids: Array[int] = []
for group_name in groups:
group_ids.append(group_name.hash())
group_ids.sort()
var ex_group_ids: Array[int] = []
for group_name in exclude_groups:
ex_group_ids.append(group_name.hash())
ex_group_ids.sort()
# Compute exact total length: (marker + count) per domain + IDs
var total = 1 + 1 + all_ids.size() # ALL marker + count + ids
total += 1 + 1 + any_ids.size() # ANY marker + count + ids
total += 1 + 1 + ex_ids.size() # NONE marker + count + ids
total += 1 + 1 + rel_ids.size() # RELATIONSHIPS marker + count + ids
total += 1 + 1 + ex_rel_ids.size() # EXCLUDE_RELATIONSHIPS marker + count + ids
total += 1 + 1 + group_ids.size() # GROUPS marker + count + ids
total += 1 + 1 + ex_group_ids.size() # EXCLUDE_GROUPS marker + count + ids
# Single allocation for final signature layout
var layout: Array[int] = []
layout.resize(total)
var i := 0
# --- Domain: ALL ---
layout[i] = 1; i += 1 # Marker for ALL domain
layout[i] = all_ids.size(); i += 1 # Count (disambiguates empty vs non-empty)
for id in all_ids:
layout[i] = id; i += 1 # Sorted ALL component IDs
# --- Domain: ANY ---
layout[i] = 2; i += 1 # Marker for ANY domain
layout[i] = any_ids.size(); i += 1 # Count
for id in any_ids:
layout[i] = id; i += 1 # Sorted ANY component IDs
# --- Domain: NONE (exclude) ---
layout[i] = 3; i += 1 # Marker for NONE domain
layout[i] = ex_ids.size(); i += 1 # Count
for id in ex_ids:
layout[i] = id; i += 1 # Sorted EXCLUDE component IDs
# --- Domain: RELATIONSHIPS ---
layout[i] = 4; i += 1 # Marker for RELATIONSHIPS domain
layout[i] = rel_ids.size(); i += 1 # Count
for id in rel_ids:
layout[i] = id; i += 1 # Sorted relationship IDs
# --- Domain: EXCLUDE_RELATIONSHIPS ---
layout[i] = 5; i += 1 # Marker for EXCLUDE_RELATIONSHIPS domain
layout[i] = ex_rel_ids.size(); i += 1 # Count
for id in ex_rel_ids:
layout[i] = id; i += 1 # Sorted exclude relationship IDs
# --- Domain: GROUPS ---
layout[i] = 6; i += 1 # Marker for GROUPS domain
layout[i] = group_ids.size(); i += 1 # Count
for id in group_ids:
layout[i] = id; i += 1 # Sorted group name hashes
# --- Domain: EXCLUDE_GROUPS ---
layout[i] = 7; i += 1 # Marker for EXCLUDE_GROUPS domain
layout[i] = ex_group_ids.size(); i += 1 # Count
for id in ex_group_ids:
layout[i] = id; i += 1 # Sorted exclude group name hashes
# Hash the structural layout -> 64-bit key
return layout.hash()