Skip to content

How a tick runs in parallel

Changing. Follow: #181, #190, #367, #364

The vanilla server runs one game tick on one thread and walks the world and every entity in turn. Java forks move single jobs off that thread. mcrs asks of each piece of work what it reads and what it writes, and runs work that does not touch the same data at the same time. This page explains how that is built and which parts run in parallel today.

Systems that touch different data run together

Section titled “Systems that touch different data run together”

mcrs is built on Bevy ECS, short for entity component system. In an ECS, behaviour is a set of functions called systems, and each system is one transformation over every entity that has the data it names. Bevy runs two systems at the same time when their data access does not overlap, so you write no lock for this. The Bevy scheduler finds the conflicts when it builds the schedule graph. The compiler does not check them.

System sets, which are named groups of systems, fix the order between the groups. The project also checks the schedule for ambiguity, as section 13 of the sharding design note describes. The tick is split into a few phases with few sync points.1

Entities read a frozen snapshot and write their own row

Section titled “Entities read a frozen snapshot and write their own row”

One system can still have a lot of entities to process, so the work inside it is split too. For entities mcrs uses the model from section 3 of docs/physics-architecture.md:

  1. At the start of the tick, the engine freezes a snapshot of the state that other entities can see: the blocks as they were at the end of the previous tick, a grid of the bodies, and a table of positions.
  2. Space is cut into tiles of columns. Each tile is a task over the rows of the entities in it.
  3. Every entity reads only the snapshot and writes only its own row. Effects on other entities, such as a push or a pickup, are collected and applied later in a fixed order.

No entity reads the new state of another entity in the same pass, so the tasks do not wait for each other and need no lock. The note calls this design Jacobi, because every row is computed from the old state. A task starts when the tasks it reads from are done. Small dependency counters track that, and the tasks run on the Bevy compute pool.

The result is the same on any thread count

Section titled “The result is the same on any thread count”

The collected effects are merged by a stable key, so the result does not depend on the machine. The key is the persisted UUID of the entity and never a network id, and it also breaks ties between entities.

The project sorts determinism into three tiers:2

Tier Guarantee Where it applies
Exact The result equals the reference World generation, the arithmetic of player movement, anything rolled from a seed stored in the save
Independent of the scheduler The same inputs give the same tick regardless of thread order Everything else
Free Any order is allowed Entities and systems that share no written data

Two checks hold this today. The first is the test tick_determinism.rs. It ticks a world of two cells for 60 ticks, once with every world inline on one thread and once with threads. It hashes the whole simulation state after every tick and fails at the first tick where the hashes differ.

The test: threads and inline must give the same world
#[test]
fn the_world_a_tick_produces_does_not_depend_on_the_threads() {
let inline = run(Placement::Inline);
let threads = run(Placement::Thread);
assert_eq!(
first_divergence(&inline, &threads),
None,
"the threads diverge from Inline at the tick shown (the last entry holds the blocks)"
);
}

crates/mcrs_minecraft_server/tests/it/tick_determinism.rs:274-283

The second check is an example in the physics crate. It builds a benchmark scene of 50 000 walkers and 50 000 items on 32 x 32 tiles and prints a hash of the scene after N ticks:

Terminal window
cargo run --release -p mcrs_minecraft_engine_physics --example state_hash -- <threads> <ticks>

Run it with 1 thread and then with 8 threads, and the hash at the end of the line is the same. The example also prints the architecture, so you can compare two machines. The code is short:

state_hash: same scene, any number of threads
fn main() {
let mut arguments = std::env::args().skip(1);
let threads: usize = arguments.next().unwrap().parse().unwrap();
let ticks: u32 = arguments.next().unwrap().parse().unwrap();
let pool = TaskPoolBuilder::new().num_threads(threads).build();
let mut scene = TestScene::<()>::new(16, 50_000, 50_000, 11);
let tiles_per_task = (32 * 32_usize).div_ceil(threads * 4);
let mut writes = EntityWrites::default();
for _ in 0..ticks {
scene.step(&pool, tiles_per_task, &mut writes);
}

crates/mcrs_minecraft_engine_physics/examples/state_hash.rs:14-24

The example checks the design and makes no claim about speed. It needs a nightly toolchain and is not part of the install guide, which is WIP.

Area State
Entity pass over tiles with a frozen snapshot, hash equal across thread counts in the code, covered by the example and tests above
Cells of one world ticking at the same time with halo exchange in the code, covered by tick_determinism.rs
Lighting and world generation on threads of their own, applied at a tick boundary in the code
Block simulation: scheduled ticks, block updates, shards partly: kernels exist, shards that merge and split are WIP#160
Redstone not started: WIP#190
One dimension on many machines goal: WIP#181

A thing that does not change costs nothing

Section titled “A thing that does not change costs nothing”

Parallelism is one of two ideas behind the tick. The other is that work is proportional to change. A resting entity waits for an event and is not checked every tick, and a cell that has nothing to tick is skipped. Sparse changes over a large population go through an explicit work queue.4 Mob AI uses waits in place of per-tick checks.

Two costs are not measured yet: an idle server tick and the memory per section.56 The project publishes no mob benchmark numbers on this page. Performance and how we measure shows what PERF.md holds, with the machine and the scenario next to each number.

Next: Cells and the road to many nodes.

  1. Partial#364 The decision that the tick runs in phases with few sync points. ↩

  2. Works#367 The decision that defines the three tiers of determinism. ↩

  3. WIP#190 Tracks redstone circuits that behave as in vanilla. ↩

  4. Partial#365 The decision that work is proportional to change. ↩

  5. WIP#320 Tracks the measurement of the idle server tick. ↩

  6. WIP#324 Tracks fixed component sizes and a count of section memory. ↩

Sources and verification

Verified: 26.4-snapshot-7, 51d45c26a, 2026-11-09

Tracking: #181, #190, #367, #364