# 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:

```gdscript
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 as `PerformanceMode.enabled` without a node reference.
- `PerformanceMode.erosion_amount` — the wall-erosion shader amount (`0.0` = off, solid tiles, zero shader cost). Applied in `terrain.gdshader` and 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**:

```gdscript
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:

```gdscript
# 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:

```gdscript
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:

```gdscript
_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:

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

Its logic:

- Disables itself immediately if `PerformanceMode.enabled` is already true, or if the player has dismissed the suggestion (checked via the `SettingsPanel` `is_perf_suggestion_dismissed()` call).
- After a `WARMUP_SECONDS` (`120.0`) grace period, it averages FPS over rolling `WINDOW_SECONDS` (`10.0`) windows.
- If `WINDOWS_REQUIRED` (`2`) **consecutive** windows both average below `THRESHOLD_FPS` (`30.0`), it emits `EventBus.low_fps_detected` once 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` — static `enabled` / `erosion_amount` flags.
- `scripts/core/PerfSetup.gd` — autoload that resolves and applies the flag at startup, before renderers read it.
- `scripts/core/PerfMonitor.gd` — FPS watchdog that emits `low_fps_detected`; instantiated by `World.gd`.
- `scripts/core/EventBus.gd` — declares `perf_mode_changed(enabled)` and `low_fps_detected`.
- `scenes/world/TerrainDisplay.gd`, `scenes/world/MachineDisplay.gd`, `scenes/village/VillageBackdrop.gd` — representative low-perf renderers.

## Related

- [EventBus and the Signal Convention](/docs/underroot/eventbus-and-the-signal-convention)
- [The Autoload Model](/docs/underroot/the-autoload-model)
- [The Scene Tree](/docs/underroot/the-scene-tree)
- [Save System and Migration](/docs/underroot/save-system-and-migration)