Each frame, DbspPlugin chains two systems so the first runs to completion
before the second starts: cache_state_for_dbsp_system reads ECS component
state into the DBSP circuit's input handles, then apply_dbsp_outputs_system
steps the circuit and writes its outputs back onto ECS components.
DbspState exposes frame-rollback methods that keep Rust-side bookkeeping in
step with the circuit, called in this order:
begin_frame_rollback— start of the cache pass; clears the previous frame's rollback log.record_unsequenced_undo— during damage ingestion, captures anapplied_unsequencedentry's pre-frame value before it is mutated.stash_frame_rollback— saves the pre-frame health-snapshot and pending-damage values the cache pass extracted.commit_frame_tracking— on a successful step, discards the rollback log.rollback_frame_tracking— on a failed step, restores the pre-frame tracking.
When state.step_circuit() returns Err, the output system clears the circuit
inputs, restores the Rust-side tracking (rollback_frame_tracking), emits a
DbspSyncError event, and applies no ECS output writes that frame.
apply_positions, apply_velocities, and apply_health_deltas apply only
consolidated records with a positive Z-set weight; non-positive (retraction)
weights are skipped.
For the detailed walkthrough, see DBSP synchronization developer's guide.
Movement-aggregation diagnostics
movement_decision_streams returns the same deduplicated MovementDecision
stream as movement_decision_stream, plus a diagnostic
Stream<RootCircuit, OrdZSet<MovementAggregation>>. The deduplication boundary
still guarantees at most one emitted movement decision per entity, and a
net-zero total weight still emits no decision; the diagnostic stream adds
visibility without changing that behaviour.
let (decisions, aggregations) =
movement_decision_streams(fear, targets, positions);
MovementAggregation { entity, total_weight } reports that the circuit
collapsed movement decisions for one entity into one normalized vector. The
circuit emits an aggregation record only when the accumulated total_weight
falls outside -1..=1: a single decision emits no diagnostic, and a net-zero
total emits neither a movement decision nor an aggregation record.
DbspCircuit::movement_aggregation_out() exposes the diagnostic stream as
OutputHandle<OrdZSet<MovementAggregation>>. As with every other circuit
output, consumers must consolidate the handle, process only records with a
positive Z-set weight, and drain the handle every frame — otherwise diagnostics
can accumulate and be reported again.
apply_dbsp_outputs_system performs that lifecycle:
report_movement_aggregations emits the warning in the command layer, then the
system calls take_from_all() on movement_aggregation_out(). Keep the
distinction explicit: the DBSP fold stays pure and does not log; the output
system owns logging. See
Movement-aggregation diagnostics
in the user's guide for the consumer-facing contract.
Asserting Z-set weights with `collect_weighted`
Because those weight gates are part of the contract, tests need to see the
weights, not just the records. test_utils::collect_weighted consolidates a
dbsp::OutputHandle<OrdZSet<T>> and returns Vec<(T, ZWeight)>, retaining
each consolidated Z-set weight rather than discarding it.
That retained weight is what lets a test assert multiplicity and retractions. A
record pushed twice consolidates into one record with weight 2, so a
deduplicated output can be asserted to have multiplicity 1 — which
distinguishes "emitted once" from "emitted twice and collapsed only when read".
use dbsp::RootCircuit;
use test_utils::collect_weighted;
let (circuit, (input, output)) = RootCircuit::build(|circuit| {
let (stream, handle) = circuit.add_input_zset::<i64>();
Ok((handle, stream.output()))
})?;
// Pushing the same record twice consolidates to one record of weight 2.
input.push(7, 1);
input.push(7, 1);
circuit.step()?;
assert_eq!(collect_weighted(&output), vec![(7, 2)]);