"Which parts of the code are safe to run in parallel?"
DoSingleTick()) is fundamentally single-threaded
with a fixed, deterministic order of 22 steps. The global Rand state (seed + iterations) has
no locks, no atomics, no thread-local isolation — any parallel Rand call will cause a desync.
RimWorld 1.6 uses 5 distinct parallelism mechanisms. Each is carefully isolated from the game simulation loop.
| Mechanism | Where Used | When |
|---|---|---|
Unity Burst Jobs BURST |
PathFinderJob (A* search) | Every frame, scheduled & completed async |
GenThreading.ParallelFor/Each |
DefDatabase, XmlCrossRef, ThingFilter | Loading only (startup) |
Parallel.ForEach |
PathFinderMapData, DefOfHelper, ShortHashGiver, PlayDataLoader, SaveFileList | Loading + per-tick data gathering |
PLINQ (.AsParallel()) |
GenTypes (type caching), DirectXmlLoader | Loading only (one-time) |
Explicit Thread |
DirectXmlLoader (2 threads), AudioClipLoader | Loading only |
In RimWorld 1.6, pathfinding was moved to Unity Burst Jobs. The PathFinderJob struct implements
IJob with [BurstCompile] and runs A* on NativeArrays (unmanaged memory).
Multiple path requests can be batched and scheduled in parallel.
Why it's safe: Burst jobs operate on copied data (NativeArrays). They cannot access managed C# objects, static fields, or the Rand state. The grid data is gathered beforehand on the main thread.
Verse/PathFinderJob.cs Verse/PathFinder.cs
The 13 data sources (Cost, Area, Connectivity, Water, Fence, Building, Faction, Fog, Danger, Darkness, Perceptual...)
are updated via Parallel.ForEach(sources, ...) — each source computes independently on its own NativeArray slice.
Why it's safe: Each IPathFinderDataSource writes to its own array. No shared mutable state.
No Rand calls. Incremental updates (cellDeltas) are collected on the main thread first.
All parallelism during loading happens before the game simulation starts. No ticks are running, no Rand is active.
| System | Mechanism | File |
|---|---|---|
| XML Asset Loading | 2 explicit Threads + ConcurrentBag |
Verse/DirectXmlLoader.cs |
| XML Cross-References | GenThreading.ParallelForEach |
Verse/DirectXmlCrossRefLoader.cs:475 |
| DefDatabase Population | Parallel.ForEach per Def subclass |
Verse/PlayDataLoader.cs:110 |
| DefOf Binding | Parallel.ForEach |
RimWorld/DefOfHelper.cs:20 |
| ShortHash Assignment | Parallel.ForEach |
Verse/ShortHashGiver.cs:16 |
| Type System Caching | AllTypes.AsParallel() |
Verse/GenTypes.cs:163-210 |
| Static Constructors | Parallel.ForEach |
Verse/StaticConstructorOnStartupUtility.cs:35 |
| ThingFilter Init | GenThreading.ParallelFor + lock |
Verse/ThingFilter.cs:557-558 |
| DefDatabase Actions | GenThreading.ParallelForEach |
Verse/DefDatabase.cs:150 |
| Audio Loading | Background Thread + lock |
RuntimeAudioClipLoader/Manager.cs |
| Save File Metadata | Parallel.ForEach |
RimWorld/Dialog_SaveFileList.cs:26 |
Fleck (particle) drawing is batched across threads using ManualResetEvent for synchronization.
Each batch operates on a slice of the fleck array with its own DrawBatch.
Pure rendering — no game state mutation, no Rand.
ConcurrentDictionary<Thing, float> immutableStatCache is used for stat values that never change.
Thread-safe by design. The mutable stat cache is a regular Dictionary (single-threaded only).
DoSingleTick() — is single-threaded and must stay that way.
22 steps execute in a fixed order. Each step can read/write any game state freely because nothing else runs concurrently.
Ludeon built their own parallel framework instead of using System.Threading.Tasks.Parallel everywhere.
It uses ThreadPool.QueueUserWorkItem with AutoResetEvent synchronization.
| Method | Signature | Used For |
|---|---|---|
ParallelForEach<T> |
(List<T>, Action<T>, maxDegree) |
DefDatabase, XmlCrossRef |
ParallelFor |
(from, to, Action<int>, maxDegree) |
ThingFilter initialization |
Verse/GenThreading.cs:1-145
| Pattern | Where Used | Purpose |
|---|---|---|
lock (object) |
Log, ThingFilter, AudioLoader | Critical section protection |
ConcurrentDictionary |
StatWorker (immutable cache) | Thread-safe read/write cache |
ConcurrentBag |
DirectXmlLoader | Thread-safe collection for XML files |
Interlocked.Add/Read |
GenThreading, Log, DeepProfiler | Atomic counter operations |
volatile |
DeepProfiler.enabled | Thread-safe flag visibility |
ManualResetEvent |
FleckParallelizationInfo | Draw batch completion signal |
AutoResetEvent |
GenThreading, ParallelDeflateOutputStream | Task completion synchronization |
Thread-local storage |
DeepProfiler (per-thread instances) | Avoid contention on profiler stacks |
Rand.ValueAsync(seed) |
Available but rarely used | Stateless RNG — safe from any thread |
Rand is a global static with two fields: seed (uint) and iterations (uint).
Every Rand.Value call increments iterations by 1. There is no lock, no atomic operation, no thread-local state.
If two threads call Rand.Value concurrently, the iterations counter gets corrupted.
Even if you could prevent corruption, the call order would be non-deterministic — breaking savegame reproducibility.
Rand is called in: Pawn.Tick, Thing.PostMake, Kill(), DamageInfo, MTBEventOccurs, HediffSet, Storyteller, equalizeCells.Shuffle, combat verbs, social interactions, need calculations — virtually everywhere.
Rand.ValueAsync(int seed) calls MurmurHash.GetInt(seed, 0) directly.
No global state touched. Safe from any thread. But almost nothing in the codebase uses it.
| Category | Safety | Examples | Reason |
|---|---|---|---|
| Burst Jobs | SAFE | PathFinderJob | NativeArrays, no managed access |
| Data Source Gathering | SAFE | PathFinderMapData sources | Each source writes own array |
| Loading / Startup | SAFE | XML, Defs, Types, Audio | No simulation running |
| Fleck Rendering | SAFE | FleckSystemBase draw batches | Pure rendering, no game state |
| Immutable Caches | SAFE | StatWorker immutable cache | ConcurrentDictionary, read-heavy |
| Pure Hash Functions | SAFE | MurmurHash, Rand.ValueAsync | No state, pure function |
| Read-Only Queries | CAUTION | Reachability, Region traversal | Safe if no rebuild during query |
| Map Grid Reads | CAUTION | ThingGrid.ThingsAt, CellIndices | Safe if no spawn/despawn during read |
| Tick Simulation | UNSAFE | DoSingleTick, all 22 steps | Global Rand + shared mutable state |
| Pawn Operations | UNSAFE | Tick, Kill, Spawn, Job dispatch | Rand + 20 trackers + Map registration |
| Thing Lifecycle | UNSAFE | PostMake, SpawnSetup, DeSpawn | Rand + 15 Map subsystem registrations |
Safe to parallelize:
Rand (e.g., distance calculations, stat lookups)MurmurHash.GetInt(seed, counter) or Rand.ValueAsync(seed) for stateless RNGNever parallelize:
Rand.Value/Bool/Range/ChanceDoSingleTick() or called from itReachabilityCache during ticksPathFinderJob is Burst-compiled native code. Harmony cannot patch it.
To modify pathfinding costs, patch PathFinderMapData.ParameterizeGridJob or
PathFinderCostTuning.For(Pawn) instead — these are managed C# and run on the main thread.
These systems are directly related to parallelism decisions:
The threading analysis is comprehensive because parallelism patterns are concentrated in a few key files.
All Parallel.For, GenThreading, AsParallel, Thread, lock,
volatile, ConcurrentDictionary, and Burst usages have been found via full-text search.
| Namespace | Files | Analyzed |
|---|---|---|
| Verse | 1,747 | 326 |
| Verse.AI | ~278 | 118 |
| Verse.AI.Group | ~129 | 19 |
| RimWorld | 5,913 | 1,114 |
| RimWorld.QuestGen | 381 | 306 |
| RimWorld.Planet | ~200 | 12 |
FleckSystemBase.cs — full fleck parallelization pipeline (how draw batches are scheduled)FastTileFinder.cs — world map tile finding with Parallel.For + Unity JobsParallelDeflateOutputStream — multi-threaded Zlib compression (save games)