Skip to main content

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. Logical property `TypeId`s are sorted and hashed to produce a canonical
12//! identity for that unordered component set. A query with the same component set can then resolve
13//! to the index (if it exists) of an equivalent (multi-)property regardless of the order client
14//! 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
33#[cfg(feature = "profiling")]
34use crate::entity::property_store::registered_property_name;
35use crate::entity::Entity;
36use crate::hashing::{one_shot_128, HashMap, HashValueType};
37use crate::warn;
38
39/// Local convenience type; we store the name of the property for use in diagnostics.
40#[derive(Clone, Copy)]
41struct MultiPropertyId {
42    pub id: usize,
43    pub name: &'static str,
44}
45
46/// Process-local identifier for an entity-scoped unordered property set.
47///
48/// The numeric value is an implementation detail and is not stable across
49/// processes.
50#[cfg(feature = "profiling")]
51#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
52pub(crate) struct QueryIdentityId(usize);
53
54#[cfg(feature = "profiling")]
55impl QueryIdentityId {
56    pub(crate) const fn from_raw(raw: usize) -> Self {
57        Self(raw)
58    }
59}
60
61#[derive(Clone, Copy, Default)]
62struct QueryShapeRecord {
63    representative: Option<MultiPropertyId>,
64    #[cfg(feature = "profiling")]
65    identity: Option<QueryIdentityId>,
66}
67
68#[derive(Default)]
69struct QueryShapeRegistry {
70    shapes: HashMap<(usize, HashValueType), QueryShapeRecord>,
71    #[cfg(feature = "profiling")]
72    labels: Vec<&'static str>,
73}
74
75enum PreMainDiagnostic {
76    Warning(String),
77}
78
79/// Metadata for entity-scoped, unordered component-property sets.
80///
81/// Query tuple lookup uses this to resolve a representative multi-property
82/// index. Profiling additionally interns each observed shape to a compact ID
83/// and stores its reporting label here.
84static QUERY_SHAPE_REGISTRY: LazyLock<Mutex<QueryShapeRegistry>> =
85    LazyLock::new(|| Mutex::new(QueryShapeRegistry::default()));
86
87/// A map from an entity-scoped concrete multi-property logical `TypeId` to the representative
88/// multi-property ID for that unordered component set.
89///
90/// Singleton multi-property queries, source identity, and index creation guards use this to
91/// resolve a concrete multi-property type to the representative selected for its component set.
92static MULTI_PROPERTY_TYPE_ID_MAP: LazyLock<Mutex<HashMap<(usize, TypeId), MultiPropertyId>>> =
93    LazyLock::new(|| Mutex::new(HashMap::default()));
94
95/// Diagnostics generated by ctors before execution reaches `main`.
96///
97/// Ctors run before normal logging setup, so warnings and errors are queued here and emitted when
98/// the first `Context` is constructed. Right now detecting equivalent multi-property registration
99/// is our only use case.
100static PRE_MAIN_DIAGNOSTICS: LazyLock<Mutex<Vec<PreMainDiagnostic>>> =
101    LazyLock::new(|| Mutex::new(Vec::new()));
102
103fn query_shape_key(entity_id: usize, type_ids: &[TypeId]) -> (usize, HashValueType) {
104    (entity_id, one_shot_128(&type_ids))
105}
106
107/// Looks up the representative multi-property ID for a sorted list of component `TypeId`s.
108#[must_use]
109pub fn type_ids_to_multi_property_id(entity_id: usize, type_ids: &[TypeId]) -> Option<usize> {
110    let key = query_shape_key(entity_id, type_ids);
111    QUERY_SHAPE_REGISTRY
112        .lock()
113        .unwrap()
114        .shapes
115        .get(&key)
116        .and_then(|record| record.representative)
117        .map(|representative| representative.id)
118}
119
120/// Looks up the representative multi-property ID and name for a concrete multi-property
121/// logical `TypeId`.
122pub(crate) fn multi_property_id_for_property_type_id(
123    entity_id: usize,
124    property_type_id: TypeId,
125) -> Option<(usize, &'static str)> {
126    MULTI_PROPERTY_TYPE_ID_MAP
127        .lock()
128        .unwrap()
129        .get(&(entity_id, property_type_id))
130        .map(|value| (value.id, value.name))
131}
132
133/// Registers the representative multi-property ID for an unordered component set.
134///
135/// Returns `None` if `id` became the representative. Returns the existing representative ID and
136/// name if an equivalent multi-property had already been registered.
137pub fn register_type_ids_to_multi_property_id(
138    entity_id: usize,
139    type_ids: &[TypeId],
140    property_type_id: TypeId,
141    id: usize,
142    name: &'static str,
143) -> Option<(usize, &'static str)> {
144    let key = query_shape_key(entity_id, type_ids);
145    let value = MultiPropertyId { id, name };
146    let existing = {
147        let mut registry = QUERY_SHAPE_REGISTRY.lock().unwrap();
148        let record = registry.shapes.entry(key).or_default();
149        match record.representative {
150            Some(existing) => Some(existing),
151            None => {
152                record.representative = Some(value);
153                None
154            }
155        }
156    };
157
158    let representative = existing.unwrap_or(value);
159    MULTI_PROPERTY_TYPE_ID_MAP
160        .lock()
161        .unwrap()
162        .entry((entity_id, property_type_id))
163        .or_insert(representative);
164
165    existing.map(|existing| (existing.id, existing.name))
166}
167
168#[cfg(feature = "profiling")]
169fn build_query_identity_label<E: Entity>(type_ids: &[TypeId]) -> String {
170    if type_ids.is_empty() {
171        return format!("{}: All", E::name());
172    }
173
174    let mut names = type_ids
175        .iter()
176        .map(|&type_id| registered_property_name(E::id(), type_id))
177        .collect::<Vec<_>>();
178    names.sort_unstable();
179
180    format!("{}: ({})", E::name(), names.join(", "))
181}
182
183/// Returns the process-local profiling identity for a sorted query shape.
184#[cfg(feature = "profiling")]
185pub(crate) fn intern_query_identity<E: Entity>(type_ids: &[TypeId]) -> QueryIdentityId {
186    let key = query_shape_key(E::id(), type_ids);
187
188    {
189        let registry = QUERY_SHAPE_REGISTRY.lock().unwrap();
190        if let Some(identity) = registry.shapes.get(&key).and_then(|record| record.identity) {
191            return identity;
192        }
193    }
194
195    // Property metadata has its own lock, so build the label without holding
196    // the query-shape registry lock.
197    let label = build_query_identity_label::<E>(type_ids);
198
199    let mut registry = QUERY_SHAPE_REGISTRY.lock().unwrap();
200    if let Some(identity) = registry.shapes.get(&key).and_then(|record| record.identity) {
201        return identity;
202    }
203
204    let identity = QueryIdentityId(registry.labels.len());
205    registry.labels.push(Box::leak(label.into_boxed_str()));
206    registry.shapes.entry(key).or_default().identity = Some(identity);
207    identity
208}
209
210/// Returns the raw process-local profiling identity for an entity-scoped query shape.
211///
212/// This function exists for Ixa's exported entity and property macros. The returned value is an
213/// implementation detail and is not stable across processes.
214#[doc(hidden)]
215pub fn intern_query_identity_raw<E: Entity>(type_ids: &[TypeId]) -> usize {
216    #[cfg(feature = "profiling")]
217    {
218        intern_query_identity::<E>(type_ids).0
219    }
220
221    #[cfg(not(feature = "profiling"))]
222    {
223        let _ = type_ids;
224        panic!("query profiling identities require the profiling feature")
225    }
226}
227
228/// Returns the reporting label for a process-local profiling identity.
229#[cfg(feature = "profiling")]
230pub(crate) fn query_identity_label(identity: QueryIdentityId) -> &'static str {
231    QUERY_SHAPE_REGISTRY.lock().unwrap().labels[identity.0]
232}
233
234#[cfg(all(test, feature = "profiling"))]
235pub(crate) fn test_query_identity(label: &'static str) -> QueryIdentityId {
236    let mut registry = QUERY_SHAPE_REGISTRY.lock().unwrap();
237    let identity = QueryIdentityId(registry.labels.len());
238    registry.labels.push(label);
239    identity
240}
241
242#[doc(hidden)]
243pub fn record_pre_main_warning(message: String) {
244    PRE_MAIN_DIAGNOSTICS
245        .lock()
246        .unwrap()
247        .push(PreMainDiagnostic::Warning(message));
248}
249
250pub(crate) fn emit_pre_main_diagnostics() {
251    for diagnostic in PRE_MAIN_DIAGNOSTICS.lock().unwrap().drain(..) {
252        match diagnostic {
253            PreMainDiagnostic::Warning(message) => warn!("{message}"),
254        }
255    }
256}
257
258// The following free functions are utilities used to normalize query parts into the same order as
259// an equivalent multi-property canonical value. Query tuple impls have to do this dynamically,
260// because they do not (and cannot) have a proc-macro-generated trait impl.
261
262/// An iota function that returns an array of the form `[0, 1, 2, 3, ..., N-1]`. The size of the array
263/// is statically known, avoiding `Vec` allocations.
264const fn make_indices<const N: usize>() -> [usize; N] {
265    let mut arr = [0; N];
266    let mut i = 0;
267    while i < N {
268        arr[i] = i;
269        i += 1;
270    }
271    arr
272}
273
274/// Returns the indices of `keys` in sorted order. These indices are used to reorder some other
275/// array according to the sorted order of the `keys`, e.g. by `static_apply_reordering`.
276///
277/// "Static" in the name refers to the fact that it takes and returns an array of statically
278/// known size, avoiding `Vec` allocations.
279#[must_use]
280pub fn static_sorted_indices<T: Ord, const N: usize>(keys: &[T; N]) -> [usize; N] {
281    let mut indices = make_indices::<N>();
282    indices.sort_unstable_by_key(|&i| &keys[i]);
283    indices
284}
285
286/// Reorders the `values` in place according to the ordering defined by `indices`. The `indices`
287/// is an ordering produced by `sorted_indices`/`static_sorted_indices` and encodes the sorted
288/// order of the `keys` (the names of the tag types).
289///
290/// "Static" in the name refers to the fact that it takes and returns an array of statically
291/// known size, avoiding `Vec` allocations.
292pub fn static_apply_reordering<T: Copy, const N: usize>(values: &mut [T; N], indices: &[usize; N]) {
293    let tmp_values: [T; N] = *values;
294    for (old_index, new_index) in indices.iter().enumerate() {
295        values[old_index] = tmp_values[*new_index];
296    }
297}
298
299/// Returns the inverse of a reordering produced by [`static_sorted_indices`].
300///
301/// If `indices[sorted_position] == original_position`, the returned array maps
302/// `original_position` back to `sorted_position`.
303#[must_use]
304pub fn static_inverse_indices<const N: usize>(indices: &[usize; N]) -> [usize; N] {
305    let mut inverse = [0; N];
306    for (sorted_position, &original_position) in indices.iter().enumerate() {
307        inverse[original_position] = sorted_position;
308    }
309    inverse
310}
311
312/// Reorder `values` in place according to the sorted order of `keys`.
313///
314/// Both slices must have the same length. "Static" in the name refers to the fact that it
315/// takes and returns an array of statically known size, avoiding `Vec` allocations.
316pub fn static_reorder_by_keys<T: Ord + Copy, U: Copy, const N: usize>(
317    keys: &[T; N],
318    values: &mut [U; N],
319) {
320    let indices: [usize; N] = static_sorted_indices(keys);
321    static_apply_reordering(values, &indices);
322}