Performance Mode

Performance Mode is Underroot's global low-graphics switch. The CPU draws every terrain tile, machine, and backdrop scene each frame, so on slower hardware (notably Web) the game turns off lighting shaders and throttles redraws to hold a playable frame rate. What follows: where the flag lives, how it's applied at startup, which renderers respect it.

The flag

scripts/core/PerformanceMode.gd is a tiny class_name PerformanceMode holder with two static fields:

class_name PerformanceMode

static var enabled: bool = false
static var erosion_amount: float = 0.0

Because the fields are static, they are readable the instant the script class is loaded — before any node's _ready(). That is what makes the startup ordering below matter.

Startup: PerfSetup applies the flag first

scripts/core/PerfSetup.gd is an autoload (registered as PerfSetup in project.godot) whose only job is to resolve and apply the flag before any world renderer reads it:

func _ready() -> void:
	PerformanceMode.enabled = _resolve_initial()   # must run first — erosion default reads it
	PerformanceMode.erosion_amount = _resolve_erosion()

_resolve_initial() reads user://settings.json for a saved low_perf_mode choice. If the player has made no explicit choice yet, it falls back to the platform default:

# No saved choice yet -> platform default.
return OS.has_feature("web")

So Web exports default low-perf ON (browsers run the CPU-drawn scenes much slower); desktop defaults OFF. _resolve_erosion() similarly reads a saved erosion_amount or falls back to DEFAULT_EROSION (0.12) on normal mode, 0.0 in low-perf.

Why an autoload exists just for this: previously the saved value was applied later, inside SettingsPanel._load_settings(), after the renderers had already cached the static default — and that load never emitted perf_mode_changed. The Settings toggle and the actual rendering could then disagree (the toggle showing ON on Web while the game still drew full graphics). PerfSetup closes that gap by running before the renderers.

Changing the flag at runtime

When the player flips the toggle in scenes/ui/SettingsPanel.gd, the change is broadcast through the EventBus autoload:

signal perf_mode_changed(enabled: bool)

Every renderer that cares connects to EventBus.perf_mode_changed in its _ready(), caches the value locally, and forces a redraw. This is the standard cross-system signal convention — renderers never reach into the Settings panel directly.

Which renderers respect it

Renderer Low-perf behavior
scenes/world/TerrainDisplay.gd Flat tiles, no lighting/erosion shader. Caches _low_perf = PerformanceMode.enabled and re-reads on perf_mode_changed.
scenes/world/MachineDisplay.gd Redraw timer clamps to ~10 fps (0.10 s) while active.
scenes/village/VillageBackdrop.gd Redraw interval 0.10 s (10 fps) in low-perf vs 0.033 s (~30 fps) normal; distant surface scenery.

The pattern in each is the same:

_low_perf = PerformanceMode.enabled
EventBus.perf_mode_changed.connect(func(enabled: bool) -> void:
	...)

Other scenes in the low-perf-aware set (e.g. DiscoveryLayer, DepthRuler, ParticleLayer, MawDisplay) read the same flag; consult each file for its specific concession.

PerfMonitor: suggesting the switch

scripts/core/PerfMonitor.gd watches the frame rate and, if the machine is struggling, nudges the player toward turning low-perf on. It is not an autoloadscenes/world/World.gd instantiates it once and adds it as a child:

var _perf_monitor: Node = load("res://scripts/core/PerfMonitor.gd").new()
add_child(_perf_monitor)

Its logic:

EventBus.low_fps_detected is consumed by scenes/ui/PerfSuggestionModal.gd, which offers the player the one-click switch. PerfMonitor only detects and suggests; it never flips PerformanceMode.enabled itself.

Key files