Menu ▾ ▴

Logic Thread

Rufus Pearce

Source: engine/logic_thread.py
Role: Central simulation and game-logic scheduler
Simulation rate: 60 Hz
Threading model: Dedicated logic thread with separate Monster AI worker

The Logic Thread is Fio's central runtime simulation thread. It provides a fixed-rate execution loop shared by both editor mode and play mode, handling gameplay state, physics, entity interaction, I/O, movers, doors, triggers, portals, pickups, collision preparation, plugin events, and other time-dependent systems.

Unlike the renderer, which is concerned with producing frames, the Logic Thread is responsible for advancing the simulated world.

The implementation is deliberately independent of the Qt rendering loop. This keeps simulation timing separate from GPU presentation and allows the game state to continue progressing at a controlled rate regardless of rendering workload.


1. Fixed 60 Hz Simulation

LogicThread runs at:

60 ticks per second

with a tick duration of:

1 / 60 = 0.016666... seconds

The thread uses a high-resolution time.perf_counter() clock and an accumulator rather than simply sleeping for 1/60 second after every update.

Conceptually:

elapsed time
     │
     ▼
 accumulator
     │
     ├── >= 1 tick ──► simulation tick
     │
     ├── >= 1 tick ──► simulation tick
     │
     └── remainder retained

This means that elapsed wall-clock time is converted into discrete simulation steps.

The accumulator can execute multiple ticks when necessary, preventing small timing variations from permanently changing the simulation rate.

Each completed tick also updates the thread's performance counter, allowing Fio to report the actual logic throughput independently of the nominal 60 Hz target.


2. One Thread, Two Operating Modes

LogicThread is shared between:

  • Editor mode
  • Play mode

The main loop selects the appropriate update path depending on play_mode.

                    LogicThread
                         │
                  fixed 60 Hz loop
                         │
                ┌────────┴────────┐
                │                 │
          Editor Mode        Play Mode
                │                 │
       _tick_editor_mode    _tick_play_mode

This avoids maintaining separate timing infrastructure for the editor and game runtime.

The editor therefore has access to the same continuously running logic infrastructure used by the game.


3. Editor Mode

When Fio is not playing a level, LogicThread runs _tick_editor_mode().

Editor-mode processing includes the real-time state required by the editor viewport, including camera movement and mouse-look handling.

The editor camera is maintained directly by LogicThread rather than being tied exclusively to Qt's paint/update cycle.

This is important because viewport rendering and editor simulation are separate concerns:

Qt / Renderer
     │
     │ displays
     ▼
current editor state

LogicThread
     │
     │ updates
     ▼
editor camera / interaction state

The editor can therefore maintain continuous camera movement while the renderer consumes the resulting state.


4. Play Mode

When play mode is enabled, LogicThread switches to _tick_play_mode().

Play-mode processing is the primary simulation path and coordinates the active game systems.

The Logic Thread owns or coordinates state for:

  • player movement
  • player physics
  • Player 2
  • collision
  • triggers
  • entity interactions
  • pickups
  • level changes
  • movers
  • doors
  • lights
  • portals
  • water interactions
  • weapons
  • projectile state
  • damage and death
  • speakers
  • cinematic cameras
  • game I/O
  • plugin events
  • model collision
  • dynamic collision state
  • gameplay timers

The result is a single authoritative place through which the majority of time-dependent game behaviour advances.


5. Play-Mode Initialisation

Entering play mode performs considerably more than simply setting a boolean.

set_play_mode(True) prepares the runtime world before the first simulation tick.

Among other things it:

  1. Reads runtime control configuration.
  2. Initialises movers.
  3. Initialises doors.
  4. Initialises parented lights.
  5. Initialises parented portals.
  6. Prepares collision for angled brushes.
  7. Builds model collision geometry.
  8. Refreshes the collision-brush cache.
  9. Resets player health and death state.
  10. Clears pickup and key state.
  11. Resets timers and transient interaction state.
  12. Rebuilds runtime entity lookup information.

This means expensive preparation work is performed at the play-mode boundary, rather than repeatedly in the 60 Hz hot path.


6. Runtime Entity Caches

One of the important performance characteristics of LogicThread is that it does not repeatedly search the entire level for every operation.

When play begins, _build_entity_caches() constructs O(1) lookup dictionaries for entity names and IDs.

name ─────────► entity
id   ─────────► entity

It also creates pre-filtered collections for frequently accessed entity types:

  • trigger brushes
  • pickups
  • level changers
  • monsters

This is particularly important for large maps because the hot path does not need to repeatedly perform:

for every brush
for every thing
    determine whether this is a monster/trigger/pickup...

Instead, the relevant collections are prepared once and reused.

The monster collection additionally receives an ID lookup so MonsterAI does not need to repeatedly scan the complete level entity set.


7. Collision Preparation

LogicThread is also responsible for preparing runtime collision data.

Angled Brushes

Normal box brushes can use the fast AABB collision path.

Clipped or otherwise geometrically complex brushes can instead contain runtime geometry information. At play start, solid angled brushes are converted into world-space collision triangles.

Brush geometry
      │
      ▼
build_collision_mesh()
      │
      ▼
world-space triangles
      │
      ▼
mesh collision

This allows ramps, wedges and other convex geometry to provide proper collision rather than being treated as their enclosing box.

Non-solid brushes such as water, fog, triggers and subtractive geometry are excluded.

The generated collision information is runtime data and is removed when the level is no longer using it.

Model Collision

Things with model_path can also receive runtime collision.

Fio supports:

  • explicit AABB collision sizes
  • mesh-accurate GLB collision
  • model-bounds AABB fallback

The GLB collision path deliberately uses the CPU-only GLBLoader, avoiding OpenGL calls from the Logic Thread.

This is important because OpenGL context ownership belongs to the rendering side of the engine; collision preparation must remain safe on the logic worker.


8. Collision Caching

LogicThread maintains a cached combined collision collection:

brushes
   +
model collision brushes
   │
   ▼
_collision_brushes_cache

Previously, concatenating these collections inside hot paths could create unnecessary allocations.

The cache is rebuilt only when the underlying collision collections actually change.

This is representative of the general LogicThread design: prepare structural information outside the per-tick path and keep the 60 Hz loop focused on changing state.


9. Movers and Doors

Movers and doors are maintained as explicit runtime systems.

LogicThread keeps:

  • mover lists
  • door lists
  • mover state
  • door state
  • cached brush-only views

This allows animated world geometry to be updated without repeatedly reconstructing the relevant collections.

Parented lights and portals are also tracked so that moving geometry can carry associated entities with it.


10. Triggers and Interactions

LogicThread maintains trigger state for the player.

Runtime state includes:

player_in_triggers
fired_once_triggers

This allows trigger volumes to support both continuously active behaviour and one-shot activation.

Trigger brushes are pre-indexed during runtime cache construction, avoiding a full level scan on every tick.


11. I/O System

Fio's entity I/O system is directly connected to LogicThread.

During construction, the Logic Thread creates the IOManager and provides it with access to:

  • the LogicThread
  • the game state
  • entity lookup by name
  • entity lookup by ID
  • registered input handlers

This makes LogicThread the runtime execution environment for entity I/O.

The architecture is therefore approximately:

Entity Output
     │
     ▼
 IOManager
     │
     ▼
LogicThread
     │
     ▼
Target entity / input handler
     │
     ▼
World state change

This is particularly important for Radiant-style entities such as logic timers, doors, movers and other entities whose behaviour is expressed through connections rather than hard-coded relationships.


12. Plugin Integration

The Logic Thread is also the engine's native plugin runtime boundary.

If plugins are available, LogicThread:

  1. Loads the plugin system.
  2. Obtains the plugin manager.
  3. Attaches runtime I/O handlers.
  4. Binds the LogicThread as the engine host.
  5. Dispatches plugin lifecycle and runtime events.

Plugin events pass through _plugin_emit().

This provides a single choke point for the engine's plugin event stream:

LogicThread
     │
     ▼
_plugin_emit()
     │
     ▼
Plugin Manager
     │
     ├── Plugin A
     ├── Plugin B
     ├── Plugin C
     └── ...

Plugin failures are deliberately contained so that a faulty plugin cannot directly bring down the core simulation loop.

Fio also provides an engine-level kill switch through:

FIO_NO_PLUGINS=1

When enabled, the plugin manager is not attached and the per-tick plugin path becomes effectively inactive.


13. Monster AI Is Decoupled

Monster AI is intentionally not executed as a second full simulation loop inside LogicThread.

LogicThread creates:

MonsterAI
MonsterAIThread

and protects shared monster/player state with locks.

The dedicated Monster AI worker can therefore perform AI processing independently of the main gameplay scheduler.

The architecture is:

                    LogicThread
                        │
             ┌──────────┴──────────┐
             │                     │
        Game simulation       shared state
             │                     │
             │              ┌──────┴──────┐
             │              │             │
             ▼              ▼             ▼
         Player         MonsterAI    MonsterAIThread

This prevents potentially expensive AI processing from unnecessarily blocking the central 60 Hz game-logic loop.

The extracted MonsterAI.update() path is explicitly designed to be called from the LogicThread's play-mode processing while the dedicated AI thread handles its own scheduling.


14. Portal Transit

Portal traversal is handled by LogicThread.

Runtime portal state includes:

  • portal lookup cache
  • portal cooldowns
  • previous player position
  • cache-dirty state

The previous player position is retained so portal crossing can be evaluated against the movement segment, rather than merely checking the player's final position.

This prevents fast-moving players from passing through a portal aperture between two simulation samples.

A short transit cooldown also prevents immediate oscillation between two closely positioned portals.


15. Gameplay State

LogicThread owns a significant amount of transient gameplay state.

Examples include:

player_health
player_dead
player2_health
player2_dead

collected_pickups
collected_keys
respawn_timers

active_speakers
hurt_trigger_timers

mover_states
door_states

bullet_marks
monster_projectiles
gunfire_events

current_hud_message
active_weapon
muzzle_flash_active

Keeping these values in the simulation layer means they are advanced by the same fixed timestep rather than being driven by rendering frequency.


16. Water and Environmental Interaction

Water interaction is also processed by the Logic Thread.

Runtime state tracks:

  • whether the player was previously in water
  • water-walking cadence
  • water transition state

This allows LogicThread to distinguish events such as:

outside water
     │
     ▼
enter water
     │
     ▼
walking in water
     │
     ▼
leave water

The cadence is simulation-time based rather than dependent on how frequently the viewport happens to repaint.


17. Performance Design

LogicThread is deliberately structured around a hot path / cold path distinction.

Cold-path work

Performed when entering play mode or when runtime configuration changes:

  • entity cache construction
  • angled collision baking
  • model collision generation
  • collision cache construction
  • mover/door initialisation
  • portal cache construction
  • runtime state reset

Hot-path work

Performed at 60 Hz:

  • movement
  • physics
  • collision
  • triggers
  • interactions
  • animation state
  • timers
  • projectiles
  • gameplay state
  • AI coordination
  • plugin event dispatch

The implementation repeatedly uses cached collections and precomputed lookup tables to keep the hot path from performing unnecessary allocations or whole-level scans.

This distinction is particularly important for Fio's large-world capabilities.


18. Logic Thread and Render Thread

LogicThread does not exist to render the game.

The division is approximately:

              ┌──────────────────────┐
              │      LogicThread     │
              │                      │
              │  60 Hz simulation    │
              │  physics             │
              │  entities            │
              │  I/O                 │
              │  gameplay            │
              └──────────┬───────────┘
                         │
                    shared state
                         │
                         ▼
              ┌──────────────────────┐
              │   Render / Qt side   │
              │                      │
              │  camera/view         │
              │  culling             │
              │  OpenGL              │
              │  presentation        │
              └──────────────────────┘

This separation is especially important because the rendering path may operate at a different frame rate from the simulation.

A display running at 144 Hz does not cause the simulation to run at 144 Hz, and a temporarily slower renderer does not redefine the simulation timestep.


19. Thread-Safe State Ownership

The Logic Thread is the authoritative owner of simulation state, but some systems operate concurrently.

Monster AI has explicit locks for shared state, including:

_monster_lock
_player_damage_lock

The broader engine also uses ThreadedGameState to mediate state shared between the simulation and rendering systems.

This gives Fio a layered concurrency model rather than allowing arbitrary threads to mutate arbitrary game objects.


20. Failure Containment

The main simulation loop is designed to survive individual tick failures.

A failed tick is handled rather than allowing the timing accumulator to become permanently stuck.

This is important for a fixed-timestep loop: if one simulation operation throws an exception, the scheduler must still consume the failed timestep rather than repeatedly attempting the same accumulated tick forever. Fio has regression coverage specifically for this behaviour.

Plugin callbacks are similarly guarded so plugin exceptions do not directly terminate the core logic scheduler.


21. Why LogicThread Is Central to Fio

The Logic Thread effectively forms the simulation spine of Fio.

It sits between the static map/editor representation and the continuously changing runtime world:

        Editor / Map Data
               │
               ▼
        ┌──────────────┐
        │ LogicThread  │
        └──────┬───────┘
               │
     ┌─────────┼──────────┐
     │         │          │
 Physics     Entity      I/O
     │       Systems      │
     │         │          │
     └─────────┼──────────┘
               │
        Runtime World State
               │
       ┌───────┴────────┐
       ▼                ▼
   Rendering          AI / Plugins

Rather than spreading gameplay timing across the renderer, Qt callbacks, entity classes and individual systems, Fio gives the runtime a common 60 Hz execution authority.

This is what allows movement, physics, doors, triggers, I/O, projectiles, portals and other time-dependent systems to behave as parts of one simulation rather than as unrelated update mechanisms.


22. Design Summary

The Logic Thread follows several core principles:

Principle Implementation
Fixed simulation rate 60 Hz
Independent timing perf_counter() + accumulator
Shared editor/runtime scheduler Editor and play update paths
Minimal hot-path work Cached entity and collision collections
Runtime preparation Expensive collision/entity setup at play start
Deterministic progression Fixed-duration simulation ticks
Renderer independence Simulation does not depend on Qt repaint rate
Extensibility Native plugin event dispatch
Entity communication Integrated I/O manager
Concurrent AI Dedicated Monster AI thread
Runtime safety Locks and guarded subsystem failures
Large-level optimisation O(1) entity lookup and pre-filtered lists

The result is a conventional but robust game-engine architecture: the Logic Thread advances the world; the renderer observes and presents it.


Source

engine/logic_thread.py — Fio 2.3.0.0709_Latest.