syren 0.6.0

A parallel Rust framework for agent-based models with ECS storage, scheduling, messaging, environments, and optional GPU execution.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
//! ECS System Abstractions
//!
//! This module defines the core *system execution model* used by the engine.
//!
//! A **system** is a unit of logic that operates over the ECS world. Systems:
//! - declare which components they read and write,
//! - are scheduled based on access conflicts,
//! - may be executed sequentially or in parallel,
//! - operate through a controlled [`ECSReference`] rather than direct world access.
//!
//! ## Design Goals
//!
//! The system abstraction is designed to:
//!
//! - **Enable parallel scheduling**
//!   by statically declaring component access (`read` / `write`) via [`AccessSets`],
//!   and channel ordering (`produces` / `consumes`) via [`ChannelSet`].
//!
//! - **Decouple logic from storage**
//!   so systems operate on *views* of the world rather than concrete data layouts.
//!
//! - **Support lightweight system definitions**
//!   through function-backed systems (`FnSystem`) without requiring boilerplate
//!   types for every system.
//!
//! ## Scheduling Model
//!
//! Systems are scheduled by the engine using their declared access sets:
//!
//! - Systems with *non-conflicting* access may run in parallel.
//! - Systems with conflicting writes are serialized relative to one another.
//! - Systems where one produces a channel the other consumes are placed in
//!   strictly ordered stages (producer first).
//! - Ordering is stabilised using system IDs.
//!
//! The scheduler is free to group systems into execution stages based on this
//! information.
//!
//! ## System Trait
//!
//! The [`System`] trait defines the minimal interface required for execution:
//!
//! - [`System::id`] provides a stable identifier.
//! - [`System::access`] declares component read/write requirements.
//! - [`System::run`] executes the system logic.
//!
//! All systems must be `Send + Sync` to allow execution on worker threads.
//!
//! ## Function-backed Systems
//!
//! [`FnSystem`] provides a convenient way to define systems using closures or
//! functions. This is the preferred mechanism for most gameplay and simulation
//! logic, as it avoids unnecessary type definitions while remaining fully
//! schedulable and parallel-safe.
//!
//! ## Thread Safety
//!
//! Systems do **not** receive direct mutable access to the world. Instead, they
//! operate through [`ECSReference`], which provides controlled entry points into
//! ECS execution phases.
//!
//! Correctness is enforced at runtime via borrow tracking and execution-phase discipline;
//! the scheduler optimises parallelism.
//!
//! ## Intended Usage
//!
//! This module is intended to be used in conjunction with:
//! - the scheduler (`scheduler` module),
//! - query construction (`query` module),
//! - and deferred command processing (`manager` module).
//!
//! Together, these components form the execution layer of the ECS.

use crate::engine::component::Signature;
use crate::engine::error::{ECSError, ECSResult, ExecutionError, InvalidAccessReason};
use crate::engine::manager::ECSReference;
#[cfg(feature = "gpu")]
use crate::engine::types::GPUResourceID;
use crate::engine::types::{ChannelID, ComponentID, SystemID};

use smallvec::SmallVec;

/// Bitset of [`ChannelID`]s for non-component scheduling dependencies.
///
/// Compact, deterministic, and cheap to intersect. Storage grows in 64-bit
/// words as needed; empty sets allocate nothing inline beyond the `SmallVec`
/// header. Two words inline covers channels 0-127, which is sufficient for
/// most simulations without heap allocation.
///
/// `ChannelSet` is the channel analogue of [`Signature`] for components.
/// It is attached to [`AccessSets::produces`] and [`AccessSets::consumes`]
/// to express ordering dependencies between systems beyond what component
/// read/write conflicts capture.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ChannelSet {
    bits: SmallVec<[u64; 2]>,
}

impl ChannelSet {
    /// Creates a new, empty channel set.
    #[inline]
    pub fn new() -> Self {
        Self::default()
    }

    /// Marks `id` as present in this set.
    ///
    /// Grows the backing storage automatically if `id` exceeds 127.
    #[inline]
    pub fn insert(&mut self, id: ChannelID) {
        let word = (id as usize) / 64;
        let bit = (id as usize) % 64;
        if self.bits.len() <= word {
            self.bits.resize(word + 1, 0);
        }
        self.bits[word] |= 1u64 << bit;
    }

    /// Returns `true` if `id` is present in this set.
    #[inline]
    pub fn contains(&self, id: ChannelID) -> bool {
        let word = (id as usize) / 64;
        let bit = (id as usize) % 64;
        self.bits
            .get(word)
            .is_some_and(|w| (w & (1u64 << bit)) != 0)
    }

    /// Returns `true` if this set is empty (no channels present).
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.bits.iter().all(|&w| w == 0)
    }

    /// Returns `true` if this set and `other` share at least one channel ID.
    #[inline]
    pub fn intersects(&self, other: &ChannelSet) -> bool {
        let n = self.bits.len().min(other.bits.len());
        for i in 0..n {
            if (self.bits[i] & other.bits[i]) != 0 {
                return true;
            }
        }
        false
    }

    /// Merges all channel IDs from `other` into `self` (bitwise OR in place).
    #[inline]
    pub fn or_in_place(&mut self, other: &ChannelSet) {
        if other.bits.len() > self.bits.len() {
            self.bits.resize(other.bits.len(), 0);
        }
        for (a, &b) in self.bits.iter_mut().zip(other.bits.iter()) {
            *a |= b;
        }
    }

    /// Iterates all channel IDs present in this set, in ascending order.
    ///
    /// Channel IDs are `u32`; the `w * 64 + b` combination is guaranteed to
    /// fit in `u32` as long as word index `w < (u32::MAX / 64)` ~= 67M words
    /// ~= 4B channels. In practice `w` is bounded by the allocator's issued
    /// count, so overflow is impossible under any realistic simulation. The
    /// `debug_assert!` documents and enforces this invariant during testing
    /// without costing anything in release.
    pub fn iter(&self) -> impl Iterator<Item = ChannelID> + '_ {
        self.bits.iter().enumerate().flat_map(|(w, &word)| {
            debug_assert!(
                w <= (u32::MAX as usize) / 64,
                "ChannelSet word index exceeds u32 channel-id space"
            );
            let base = (w as u32).saturating_mul(64);
            (0u32..64).filter_map(move |b| {
                if (word & (1u64 << b)) != 0 {
                    Some(base + b)
                } else {
                    None
                }
            })
        })
    }
}

/// Directional ordering constraint derived from channel produces/consumes.
///
/// Returned by [`AccessSets::channel_ordering`] when two access sets have a
/// channel dependency. The scheduler uses this to ensure producers always run
/// in an earlier stage than their consumers.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChannelOrder {
    /// `self` must be placed in an earlier stage than `other`.
    SelfBeforeOther,
    /// `other` must be placed in an earlier stage than `self`.
    OtherBeforeSelf,
}

/// Declares the component access set of a system.
#[derive(Clone, Debug, Default)]
pub struct AccessSets {
    /// Components read by the system.
    pub read: Signature,
    /// Components written by the system.
    pub write: Signature,

    /// Channels whose data this system produces this tick.
    ///
    /// Common sources: messages emitted by the system, environment keys written.
    /// A system that produces channel C must run in a strictly earlier stage than
    /// any system that consumes C.
    pub produces: ChannelSet,

    /// Channels whose data this system consumes this tick.
    ///
    /// Common sources: messages read, environment keys read via a keyed handle.
    /// A consumer must always run in a strictly later stage than all producers
    /// of the same channel.
    pub consumes: ChannelSet,
}

impl AccessSets {
    /// Returns `true` if this access set conflicts with `other`, meaning the two
    /// systems cannot be placed in the same execution stage.
    ///
    /// Conflict sources (any one is sufficient):
    ///
    /// 1. **Component write/write**: both systems write the same component.
    /// 2. **Component write/read**: one writes a component the other reads.
    /// 3. **Channel ordering**: one system produces a channel the other consumes -
    ///    they must be in strictly ordered stages, so same-stage placement is
    ///    forbidden.
    ///
    /// Note: two systems that both *produce* the same channel are compatible
    /// (deterministic thread-local merge), so producer-producer is not a conflict.
    #[inline]
    pub fn conflicts_with(&self, other: &AccessSets) -> bool {
        if self.component_conflict(other) {
            return true;
        }
        // Channel ordering: if either direction produces what the other consumes,
        // they must be in different stages (producer first).
        if self.produces.intersects(&other.consumes) {
            return true;
        }
        if other.produces.intersects(&self.consumes) {
            return true;
        }
        false
    }

    /// Component-only conflict check (W intersection W, W intersection R, R intersection W).
    ///
    /// Extracted so `conflicts_with` can chain channel checks without
    /// duplicating the bitset loop.
    #[inline]
    pub(crate) fn component_conflict(&self, other: &AccessSets) -> bool {
        for ((a_w, a_r), (b_w, b_r)) in self
            .write
            .components
            .iter()
            .zip(self.read.components.iter())
            .zip(
                other
                    .write
                    .components
                    .iter()
                    .zip(other.read.components.iter()),
            )
        {
            if (a_w & b_w) != 0 {
                return true;
            } // W intersection W
            if (a_w & b_r) != 0 {
                return true;
            } // W intersection R
            if (a_r & b_w) != 0 {
                return true;
            } // R intersection W
        }
        false
    }

    /// Returns the directional ordering required between `self` and `other`
    /// based on channel dependencies, or `None` if there is no such dependency.
    ///
    /// Component conflicts are handled separately by the scheduler's packing
    /// logic and are not reflected here.
    pub fn channel_ordering(&self, other: &AccessSets) -> Option<ChannelOrder> {
        if self.produces.intersects(&other.consumes) {
            return Some(ChannelOrder::SelfBeforeOther);
        }
        if other.produces.intersects(&self.consumes) {
            return Some(ChannelOrder::OtherBeforeSelf);
        }
        None
    }

    /// Validates that this access set is internally consistent.
    ///
    /// A system's own access set must not alias itself. Two self-aliases
    /// would cause the scheduler to silently mis-pack the system:
    ///
    /// 1. **Component read/write self-alias** - the same component appearing
    ///    in both `read` and `write`. Declaring this weakens the write lock
    ///    contract at iteration time and conflates conflict semantics.
    /// 2. **Channel produce/consume self-alias** - the same channel in both
    ///    `produces` and `consumes`. The stage packer would place the system
    ///    in a stage where it both produces and consumes the channel, so the
    ///    consume would observe the channel before its own produce had been
    ///    finalised at a boundary. This is always a bug in the system's
    ///    declaration.
    ///
    /// Called by [`Scheduler::add_boxed`](crate::engine::scheduler::Scheduler::add_boxed)
    /// at registration time so that malformed systems are rejected early
    /// rather than producing silently-wrong schedules.
    pub fn validate(&self) -> ECSResult<()> {
        // Component read/write self-alias.
        for (i, (rw, ww)) in self
            .read
            .components
            .iter()
            .zip(self.write.components.iter())
            .enumerate()
        {
            let overlap = rw & ww;
            if overlap != 0 {
                let bit = overlap.trailing_zeros();
                let cid: ComponentID = ((i as u32) * 64 + bit) as ComponentID;
                return Err(ECSError::Execute(ExecutionError::InvalidQueryAccess {
                    component_id: cid,
                    reason: InvalidAccessReason::ReadAndWrite,
                }));
            }
        }

        // Channel produces/consumes self-alias.
        if self.produces.intersects(&self.consumes) {
            let offender = self
                .produces
                .iter()
                .find(|ch| self.consumes.contains(*ch))
                .unwrap_or(0);
            return Err(ECSError::Execute(ExecutionError::SelfChannelAlias {
                channel_id: offender,
            }));
        }

        Ok(())
    }
}

/// Execution backend for a system.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SystemBackend {
    /// Standard Rust / Rayon execution on CPU.
    CPU,
    /// GPU dispatch.
    GPU,
}

/// GPU capability trait (feature-gated).
/// A GPU system is still a `System`, but additionally provides WGSL.
#[cfg(feature = "gpu")]
pub trait GpuSystem {
    /// WGSL source
    fn shader(&self) -> &'static str;

    /// Entry point name (default "main")
    fn entry_point(&self) -> &'static str {
        "main"
    }

    /// Workgroup size (default 256)
    fn workgroup_size(&self) -> u32 {
        256
    }

    /// GPU resources read or used by this kernel.
    fn uses_resources(&self) -> &[GPUResourceID] {
        &[]
    }

    /// GPU resources that this kernel may write.
    fn writes_resources(&self) -> &[GPUResourceID] {
        &[]
    }
}

/// A unit of executable logic operating on the ECS world.
///
/// A `System` represents a scheduled computation that:
/// - declares which components it reads and writes,
/// - can be ordered and parallelized based on access conflicts,
/// - is executed with a shared reference to the ECS world.
///
/// Systems must be `Send + Sync` so they can be scheduled and executed
/// in parallel across threads.
pub trait System: Send + Sync {
    /// Human-readable name (used for debugging/profiling).
    ///
    /// The default implementation returns `std::any::type_name_of_val(self)`,
    /// which is `&'static str` and coerces to the `&str` return type. Concrete
    /// implementations may return a borrow from `&self` to support dynamic
    /// names without leaking memory.
    #[inline]
    fn name(&self) -> &str {
        std::any::type_name_of_val(self)
    }

    /// Returns the unique identifier of this system.
    fn id(&self) -> SystemID;

    /// Returns a reference to the component access sets required by this system.
    fn access(&self) -> &AccessSets;

    /// Returns which backend this system should run on.
    /// Defaults to [`SystemBackend::CPU`].
    #[inline]
    fn backend(&self) -> SystemBackend {
        SystemBackend::CPU
    }

    /// Executes the system logic against the ECS world.
    fn run(&self, world: ECSReference<'_>) -> ECSResult<()>;

    /// GPU capability hook.
    /// A GPU system should override this to return `Some(self)` (as `&dyn GpuSystem`)
    /// and also return `SystemBackend::GPU` from `backend()`.
    #[cfg(feature = "gpu")]
    #[inline]
    fn gpu(&self) -> Option<&dyn GpuSystem> {
        None
    }
}

/// A concrete [`System`] backed by a function or closure.
///
/// `FnSystem` allows systems to be defined inline using a function or
/// closure, without requiring a custom system type.
///
/// The function must return `ECSResult<()>` so that
/// execution failures can be propagated through the scheduler.
pub struct FnSystem<F>
where
    F: Fn(ECSReference<'_>) -> ECSResult<()> + Send + Sync + 'static,
{
    id: SystemID,
    name: &'static str,
    access: AccessSets,
    f: F,
}

impl<F> FnSystem<F>
where
    F: Fn(ECSReference<'_>) -> ECSResult<()> + Send + Sync + 'static,
{
    /// Creates a new function-backed system.
    ///
    /// # Parameters
    /// - `id`: Unique identifier for the system.
    /// - `name`: Human-readable name, useful for debugging and profiling.
    /// - `access`: Declared component access used for scheduling.
    /// - `f`: The function or closure executed when the system runs.
    ///
    /// Prefer [`from_queries`](Self::from_queries) when the system's access is
    /// fully determined by the queries it runs; hand-written `AccessSets` can
    /// silently drift from what the system actually touches, which the borrow
    /// tracker only catches at runtime.
    pub fn new(id: SystemID, name: &'static str, access: AccessSets, f: F) -> Self {
        Self {
            id,
            name,
            access,
            f,
        }
    }

    /// Creates a function-backed system whose component access is **derived**
    /// from the queries it executes, eliminating declared-versus-used drift.
    ///
    /// The read/write signatures of every query are unioned. A component that
    /// one query reads and another writes is declared **write-only** in the
    /// merged set: write access already forces exclusive scheduling, and the
    /// queries themselves run sequentially inside the system body, so this
    /// normalisation is both sufficient for correctness and required for
    /// [`AccessSets::validate`] (which rejects read/write self-aliases).
    ///
    /// Channel dependencies are not derivable from queries; attach them with
    /// [`produces`](Self::produces) / [`consumes`](Self::consumes):
    ///
    /// ```text
    /// let system = FnSystem::from_queries(3, "trade", &[&q_read, &q_write], move |ecs| {
    ///     /* run q_read / q_write here */
    ///     Ok(())
    /// })
    /// .consumes(orders_channel)
    /// .produces(receipts_channel);
    /// ```
    pub fn from_queries(
        id: SystemID,
        name: &'static str,
        queries: &[&crate::engine::query::BuiltQuery],
        f: F,
    ) -> Self {
        let mut access = AccessSets::default();
        for query in queries {
            let derived = query.access_sets();
            for (word, other) in access
                .read
                .components
                .iter_mut()
                .zip(derived.read.components.iter())
            {
                *word |= other;
            }
            for (word, other) in access
                .write
                .components
                .iter_mut()
                .zip(derived.write.components.iter())
            {
                *word |= other;
            }
        }
        // Normalise: a component both read (by one query) and written (by
        // another) is declared write-only; see the doc comment above.
        for (read_word, write_word) in access
            .read
            .components
            .iter_mut()
            .zip(access.write.components.iter())
        {
            *read_word &= !write_word;
        }

        Self {
            id,
            name,
            access,
            f,
        }
    }

    /// Declares that this system produces `channel` this tick.
    ///
    /// Producers are scheduled strictly before consumers of the same channel.
    #[must_use]
    pub fn produces(mut self, channel: ChannelID) -> Self {
        self.access.produces.insert(channel);
        self
    }

    /// Declares that this system consumes `channel` this tick.
    ///
    /// Consumers are scheduled strictly after all producers of the channel.
    #[must_use]
    pub fn consumes(mut self, channel: ChannelID) -> Self {
        self.access.consumes.insert(channel);
        self
    }
}

impl<F> System for FnSystem<F>
where
    F: Fn(ECSReference<'_>) -> ECSResult<()> + Send + Sync + 'static,
{
    fn name(&self) -> &str {
        self.name
    }

    fn id(&self) -> SystemID {
        self.id
    }

    fn access(&self) -> &AccessSets {
        &self.access
    }

    fn run(&self, world: ECSReference<'_>) -> ECSResult<()> {
        (self.f)(world)
    }
}