ixa/entity/multi_property.rs
1#![allow(dead_code)]
2//! Utilities for managing and querying multi-properties.
3//!
4//! A multi-property is a derived property composed of a tuple of other properties. They are
5//! primarily used to enable joint indexing and efficient multi-column style queries.
6//!
7//! ## Normalization
8//!
9//! To ensure that queries can efficiently find matching indexes, multi-properties that consist
10//! of the same set of component properties are considered equivalent, regardless of their
11//! definition order. Component properties are sorted alphabetically by property name to produce
12//! a canonical identity for that unordered component set. A query with the same component set can
13//! then resolve to the index (if it exists) of an equivalent (multi-)property regardless of the
14//! order client code provides the component values.
15//!
16//! ## Query Integration
17//!
18//! The querying subsystem uses the utilities in this module to detect when a query involving
19//! multiple individual properties can be satisfied by an existing multi-property index. If a
20//! match is found, the query can perform a fast index lookup instead of iterating over component
21//! properties.
22//!
23//! ## Implementation Details
24//!
25//! Multi-properties are defined using the [`define_multi_property!`](crate::define_multi_property) macro, which
26//! handles the registration and mapping between the property set and its representative
27//! property ID. This module provides the runtime registries
28//! and reordering logic used by both the macro-generated code and the query engine.
29
30use std::any::TypeId;
31use std::sync::{LazyLock, Mutex};
32
33use crate::hashing::{one_shot_128, HashMap, HashValueType};
34use crate::warn;
35
36/// Local convenience type; we store the name of the property for use in diagnostics.
37#[derive(Clone, Copy)]
38struct MultiPropertyId {
39 pub id: usize,
40 pub name: &'static str,
41}
42
43enum PreMainDiagnostic {
44 Warning(String),
45}
46
47/// A map from an entity-scoped, sorted list of component `TypeId`s to the representative
48/// multi-property ID for that unordered component set.
49///
50/// Query tuple lookup uses this to resolve unordered query component sets to the
51/// multi-property index that can satisfy the query.
52static MULTI_PROPERTY_ID_MAP: LazyLock<Mutex<HashMap<(usize, HashValueType), MultiPropertyId>>> =
53 LazyLock::new(|| Mutex::new(HashMap::default()));
54
55/// A map from an entity-scoped concrete multi-property logical `TypeId` to the representative
56/// multi-property ID for that unordered component set.
57///
58/// Singleton multi-property queries, source identity, and index creation guards use this to
59/// resolve a concrete multi-property type to the representative selected for its component set.
60static MULTI_PROPERTY_TYPE_ID_MAP: LazyLock<Mutex<HashMap<(usize, TypeId), MultiPropertyId>>> =
61 LazyLock::new(|| Mutex::new(HashMap::default()));
62
63/// Diagnostics generated by ctors before execution reaches `main`.
64///
65/// Ctors run before normal logging setup, so warnings and errors are queued here and emitted when
66/// the first `Context` is constructed. Right now detecting equivalent multi-property registration
67/// is our only use case.
68static PRE_MAIN_DIAGNOSTICS: LazyLock<Mutex<Vec<PreMainDiagnostic>>> =
69 LazyLock::new(|| Mutex::new(Vec::new()));
70
71/// Looks up the representative multi-property ID for a sorted list of component `TypeId`s.
72#[must_use]
73pub fn type_ids_to_multi_property_id(entity_id: usize, type_ids: &[TypeId]) -> Option<usize> {
74 let hash = one_shot_128(&type_ids);
75 MULTI_PROPERTY_ID_MAP
76 .lock()
77 .unwrap()
78 .get(&(entity_id, hash))
79 .map(|value| value.id)
80}
81
82/// Looks up the representative multi-property ID and name for a concrete multi-property
83/// logical `TypeId`.
84pub(crate) fn multi_property_id_for_property_type_id(
85 entity_id: usize,
86 property_type_id: TypeId,
87) -> Option<(usize, &'static str)> {
88 MULTI_PROPERTY_TYPE_ID_MAP
89 .lock()
90 .unwrap()
91 .get(&(entity_id, property_type_id))
92 .map(|value| (value.id, value.name))
93}
94
95/// Registers the representative multi-property ID for an unordered component set.
96///
97/// Returns `None` if `id` became the representative. Returns the existing representative ID and
98/// name if an equivalent multi-property had already been registered.
99pub fn register_type_ids_to_multi_property_id(
100 entity_id: usize,
101 type_ids: &[TypeId],
102 property_type_id: TypeId,
103 id: usize,
104 name: &'static str,
105) -> Option<(usize, &'static str)> {
106 let hash = one_shot_128(&type_ids);
107 let key = (entity_id, hash);
108 let value = MultiPropertyId { id, name };
109 let existing = {
110 let mut map = MULTI_PROPERTY_ID_MAP.lock().unwrap();
111 match map.get(&key).copied() {
112 Some(existing) => Some(existing),
113 None => {
114 map.insert(key, value);
115 None
116 }
117 }
118 };
119
120 let representative = existing.unwrap_or(value);
121 MULTI_PROPERTY_TYPE_ID_MAP
122 .lock()
123 .unwrap()
124 .entry((entity_id, property_type_id))
125 .or_insert(representative);
126
127 existing.map(|existing| (existing.id, existing.name))
128}
129
130#[doc(hidden)]
131pub fn record_pre_main_warning(message: String) {
132 PRE_MAIN_DIAGNOSTICS
133 .lock()
134 .unwrap()
135 .push(PreMainDiagnostic::Warning(message));
136}
137
138pub(crate) fn emit_pre_main_diagnostics() {
139 for diagnostic in PRE_MAIN_DIAGNOSTICS.lock().unwrap().drain(..) {
140 match diagnostic {
141 PreMainDiagnostic::Warning(message) => warn!("{message}"),
142 }
143 }
144}
145
146// The following free functions are utilities used to normalize query parts into the same order as
147// an equivalent multi-property canonical value. Query tuple impls have to do this dynamically,
148// because they do not (and cannot) have a proc-macro-generated trait impl.
149
150/// An iota function that returns an array of the form `[0, 1, 2, 3, ..., N-1]`. The size of the array
151/// is statically known, avoiding `Vec` allocations.
152const fn make_indices<const N: usize>() -> [usize; N] {
153 let mut arr = [0; N];
154 let mut i = 0;
155 while i < N {
156 arr[i] = i;
157 i += 1;
158 }
159 arr
160}
161
162/// Returns the indices of `keys` in sorted order. These indices are used to reorder some other
163/// array according to the sorted order of the `keys`, e.g. by `static_apply_reordering`.
164///
165/// "Static" in the name refers to the fact that it takes and returns an array of statically
166/// known size, avoiding `Vec` allocations.
167#[must_use]
168pub fn static_sorted_indices<T: Ord, const N: usize>(keys: &[T; N]) -> [usize; N] {
169 let mut indices = make_indices::<N>();
170 indices.sort_unstable_by_key(|&i| &keys[i]);
171 indices
172}
173
174/// Reorders the `values` in place according to the ordering defined by `indices`. The `indices`
175/// is an ordering produced by `sorted_indices`/`static_sorted_indices` and encodes the sorted
176/// order of the `keys` (the names of the tag types).
177///
178/// "Static" in the name refers to the fact that it takes and returns an array of statically
179/// known size, avoiding `Vec` allocations.
180pub fn static_apply_reordering<T: Copy, const N: usize>(values: &mut [T; N], indices: &[usize; N]) {
181 let tmp_values: [T; N] = *values;
182 for (old_index, new_index) in indices.iter().enumerate() {
183 values[old_index] = tmp_values[*new_index];
184 }
185}
186
187/// Returns the inverse of a reordering produced by [`static_sorted_indices`].
188///
189/// If `indices[sorted_position] == original_position`, the returned array maps
190/// `original_position` back to `sorted_position`.
191#[must_use]
192pub fn static_inverse_indices<const N: usize>(indices: &[usize; N]) -> [usize; N] {
193 let mut inverse = [0; N];
194 for (sorted_position, &original_position) in indices.iter().enumerate() {
195 inverse[original_position] = sorted_position;
196 }
197 inverse
198}
199
200/// Reorder `values` in place according to the sorted order of `keys`.
201///
202/// Both slices must have the same length. "Static" in the name refers to the fact that it
203/// takes and returns an array of statically known size, avoiding `Vec` allocations.
204pub fn static_reorder_by_keys<T: Ord + Copy, U: Copy, const N: usize>(
205 keys: &[T; N],
206 values: &mut [U; N],
207) {
208 let indices: [usize; N] = static_sorted_indices(keys);
209 static_apply_reordering(values, &indices);
210}