Skip to main content

concinnity_core/behavior/system/
eval.rs

1// The read phase of a tick: what the bodies may see, the buffers they run
2// against, and how those buffers are worked through.
3//
4// Bodies never observe another body's same-tick writes (the apply phase runs
5// after every body), which is what makes evaluation order unobservable and lets
6// a host fan the runs across threads.
7
8use alloc::vec::Vec;
9
10use super::BehaviorSystem;
11use super::instance::Instance;
12use crate::behavior::{Effect, Program, Val, View, exec};
13use crate::components::Transform;
14use crate::ecs::{ComponentStorage, Entity, EntityByName, PipelineContext};
15
16// Below this many firing instances the fan-out costs more than the work.
17pub(super) const PARALLEL_EVAL_MIN_JOBS: usize = 64;
18
19/// One worker's share of a tick's evaluation: a contiguous slice of the tick's
20/// job list, its own effect and trace buffers, and its own binding scratch.
21/// Opaque to a scheduler, which only hands each bucket to the closure it was
22/// given. Everything inside keeps its capacity across ticks.
23#[derive(Debug, Default)]
24pub struct EvalBucket {
25    pub(super) jobs: core::ops::Range<usize>,
26    pub(super) effects: Vec<Effect>,
27    pub(super) produced: Vec<(usize, Option<Entity>, usize)>,
28    pub(super) fired: Vec<(usize, Vec<u32>)>,
29    pub(super) bindings: Vec<Option<Val>>,
30}
31
32/// Runs a tick's evaluation buckets, in any order and on any thread.
33///
34/// The buckets share nothing: each reads only the tick's starting state and
35/// writes only its own effects, so a host with a thread pool can work them
36/// through in parallel. A world whose host installs none evaluates every run on
37/// the calling thread.
38pub trait EvalScheduler: core::fmt::Debug + Send {
39    /// How many buckets to split a tick's firing instances into.
40    fn workers(&self) -> usize;
41
42    /// Apply `eval` to every bucket, then return.
43    fn run(&self, buckets: &mut [EvalBucket], eval: &(dyn Fn(&mut EvalBucket) + Send + Sync));
44}
45
46// The entity sets a body iterates this tick, resolved before anything runs.
47//
48// Single-entity reads (a name, a position, a liveness test) are not here: no
49// body runs while the world is mutable, so they read the world directly and
50// cost one lookup each instead of a whole-world copy per tick.
51#[derive(Debug, Default)]
52pub(super) struct Snapshot {
53    // Per program, per declared query, in stable order.
54    pub(super) queries: Vec<Vec<Vec<Entity>>>,
55    // Per program, the entities its scope matched, in stable order.
56    pub(super) scoped: Vec<Vec<Entity>>,
57}
58
59// What a body run reads that the tick, rather than the body, determines: the
60// per-tick half of the behavior VM's `View`. Everything here is shared and
61// immutable, so evaluation can fan across workers; single-entity reads (a name,
62// a position, a liveness test) read the storage directly and cost one lookup
63// each instead of a whole-world copy per tick.
64pub(super) struct EvalCtx<'a> {
65    pub(super) components: &'a ComponentStorage,
66    // Resolved once per tick rather than per name lookup.
67    pub(super) names: Option<&'a EntityByName>,
68    pub(super) snapshot: &'a Snapshot,
69    pub(super) programs: &'a [Program],
70    pub(super) instances: &'a [Vec<Instance>],
71    pub(super) vars: &'a [Val],
72    pub(super) dt: f32,
73    pub(super) elapsed: f32,
74    pub(super) tracing: bool,
75}
76
77// Run one instance's body against the tick's starting state, appending its
78// effects to `out`. Returns how many it appended, plus the nodes it executed
79// when tracing.
80//
81// `bindings` is the caller's reused buffer: resized to the body's compiled
82// binding high-water mark and cleared per run, so however many instances fire
83// a tick, binding scratch costs zero allocations in steady state. (The
84// previous per-run frame-arena grab exhausted the reserve on behavior-heavy
85// worlds and degraded to contended heap allocation across eval workers.)
86pub(super) fn eval_one(
87    ec: &EvalCtx<'_>,
88    bindings: &mut Vec<Option<Val>>,
89    i: usize,
90    entity: Option<Entity>,
91    out: &mut Vec<Effect>,
92) -> Option<(usize, Vec<u32>)> {
93    let locals = ec.instances[i]
94        .iter()
95        .find(|inst| inst.entity == entity)
96        .map(|inst| inst.locals.as_slice())?;
97    bindings.clear();
98    bindings.resize(ec.programs[i].bindings, None);
99    let mut nodes: Option<Vec<u32>> = ec.tracing.then(Vec::new);
100    let before = out.len();
101    let mut view = View {
102        dt: ec.dt,
103        elapsed: ec.elapsed,
104        vars: ec.vars,
105        locals,
106        bindings: bindings.as_mut_slice(),
107        queries: &ec.snapshot.queries[i],
108        // A name index entry can outlive its entity, so each is confirmed.
109        by_name: &|id| {
110            ec.names
111                .and_then(|n| n.get(id))
112                .filter(|e| ec.components.is_alive(*e))
113        },
114        transforms: &|e| ec.components.get::<Transform>(e).copied(),
115        alive: &|e| ec.components.is_alive(e),
116        self_entity: entity,
117        trace: &mut nodes,
118    };
119    exec(&ec.programs[i].body, &mut view, out);
120    Some((out.len() - before, nodes.unwrap_or_default()))
121}
122
123impl BehaviorSystem {
124    // Entities carrying every one of these component tags, filled into `out`
125    // in stable order. Column order shifts as entities are removed
126    // (swap-remove), so the result is sorted: an unstable iteration order
127    // would make a body's effects depend on unrelated despawns. `scratch`
128    // holds each extra tag's sorted set; both buffers keep their capacity.
129    pub(super) fn entities_matching_into(
130        ctx: &PipelineContext,
131        tags: &[u8],
132        scratch: &mut Vec<Entity>,
133        out: &mut Vec<Entity>,
134    ) {
135        out.clear();
136        let Some((first, rest)) = tags.split_first() else {
137            return;
138        };
139        out.extend_from_slice(ctx.entities_with_tag(*first));
140        for tag in rest {
141            scratch.clear();
142            scratch.extend_from_slice(ctx.entities_with_tag(*tag));
143            scratch.sort_unstable_by_key(|e| e.to_bits());
144            out.retain(|e| {
145                scratch
146                    .binary_search_by_key(&e.to_bits(), |o| o.to_bits())
147                    .is_ok()
148            });
149        }
150        out.sort_unstable_by_key(|e| e.to_bits());
151    }
152
153    // Refill the reused snapshot with this tick's entity sets. The shape
154    // (programs and their query counts) is fixed after `init`, so in steady
155    // state every vector here just refills in place.
156    pub(super) fn gather(&mut self, ctx: &PipelineContext, snapshot: &mut Snapshot) {
157        snapshot.queries.resize_with(self.programs.len(), Vec::new);
158        snapshot.scoped.resize_with(self.programs.len(), Vec::new);
159        for (i, p) in self.programs.iter().enumerate() {
160            snapshot.queries[i].resize_with(p.queries.len(), Vec::new);
161            for (q, tags) in p.queries.iter().enumerate() {
162                Self::entities_matching_into(
163                    ctx,
164                    tags,
165                    &mut self.tag_scratch,
166                    &mut snapshot.queries[i][q],
167                );
168            }
169            Self::entities_matching_into(
170                ctx,
171                &p.scope,
172                &mut self.tag_scratch,
173                &mut snapshot.scoped[i],
174            );
175        }
176    }
177}