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
PerformanceMode.enabled— the master low-perf toggle. Static, so any script reads it asPerformanceMode.enabledwithout a node reference.PerformanceMode.erosion_amount— the wall-erosion shader amount (0.0= off, solid tiles, zero shader cost). Applied interrain.gdshaderand ignored entirely in low-perf flat mode.
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 emittedperf_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).PerfSetupcloses 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 autoload — scenes/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:
- Disables itself immediately if
PerformanceMode.enabledis already true, or if the player has dismissed the suggestion (checked via theSettingsPanelis_perf_suggestion_dismissed()call). - After a
WARMUP_SECONDS(120.0) grace period, it averages FPS over rollingWINDOW_SECONDS(10.0) windows. - If
WINDOWS_REQUIRED(2) consecutive windows both average belowTHRESHOLD_FPS(30.0), it emitsEventBus.low_fps_detectedonce and then disables itself. A single good window resets the streak.
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
scripts/core/PerformanceMode.gd— staticenabled/erosion_amountflags.scripts/core/PerfSetup.gd— autoload that resolves and applies the flag at startup, before renderers read it.scripts/core/PerfMonitor.gd— FPS watchdog that emitslow_fps_detected; instantiated byWorld.gd.scripts/core/EventBus.gd— declaresperf_mode_changed(enabled)andlow_fps_detected.scenes/world/TerrainDisplay.gd,scenes/world/MachineDisplay.gd,scenes/village/VillageBackdrop.gd— representative low-perf renderers.