# Performance

Board hardware (MediaTek Genio 700, 4 GB RAM, low-end mobile-class GPU) is a fraction as fast as a desktop GPU. Games that run smoothly in the Godot editor can hit 25 fps on the device unless you pay attention to a few specific things. This guide is the checklist.

## Frame Rate

Board’s display runs at 60 Hz, and Godot defaults to matching display refresh — you do **not** need to set `Engine.max_fps = 60` explicitly. If you want physics to run in lockstep with rendering, keep the default `physics_ticks_per_second = 60` in `project.godot`.

```
[physics]
common/physics_ticks_per_second=60
```

If you’ve been tempted to uncap the frame rate (“just run as fast as possible”), don’t — the Genio 700 isn’t going to render faster than 60 fps for any non-trivial scene, and uncapping wastes power and heats the SoC.

## Renderer Choice

Use the **GL Compatibility** renderer on Board, not Vulkan / Forward+. Vulkan and Forward+ aren’t tuned for the Genio 700 SoC and will compile shaders slowly at startup.

In `project.godot`:

```
[rendering]
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
```

This is what every working Board Godot game ships with today. Don’t override it without measurement.

## Cold-Boot Performance: noCompress on PCK

This is a one-time setup step. After installing the Custom Android Build Template ( **Project → Install Android Build Template**), add `noCompress "pck", "sparsepck"` to the `aaptOptions` block in `android/build/build.gradle`, right after the `ignoreAssetsPattern` line:

```
android {
    aaptOptions {
        ignoreAssetsPattern "..."
        noCompress "pck", "sparsepck"
    }
}
```

Godot’s stock template doesn’t set this. Without it, the PCK is deflated inside the APK and the CPU has to inflate it on first launch — cold boot takes ~2 minutes instead of a few seconds. With it set, the PCK can be memory-mapped at boot.

## Parallel Asset Prewarming

Large textures and audio streams block the first frame they’re used unless they’ve already been touched. Spin up a boot scene that uses `WorkerThreadPool` to prewarm them while a splash screen renders.

```
const ASSETS_TO_PREWARM := [\
    "res://art/title.png",\
    "res://audio/theme.ogg",\
    "res://audio/sfx/place.wav",\
]

func _ready() -> void:
    var group_id := WorkerThreadPool.add_group_task(
        _prewarm_one, ASSETS_TO_PREWARM.size(), 4, false, "prewarm")
    while not WorkerThreadPool.is_group_task_completed(group_id):
        await get_tree().process_frame
    get_tree().change_scene_to_file("res://scenes/main.tscn")

func _prewarm_one(index: int) -> void:
    ResourceLoader.load(ASSETS_TO_PREWARM[index])
```

> **Note**: Player avatars decode to `ImageTexture` and are cached after first load. Prewarm the ones you’ll display by calling `await Board.avatar.await_load_avatar(int(player.avatar_id))` for each player from `Board.session.get_players()` in the boot scene — the loader keeps the decoded textures in its own cache, so later lookups are instant.

## Threaded Scene Loads

For scenes too large to load synchronously, use `ResourceLoader.load_threaded_request`:

```
func change_scene_async(path: String) -> void:
    ResourceLoader.load_threaded_request(path)
    while true:
        var progress: Array = []
        match ResourceLoader.load_threaded_get_status(path, progress):
            ResourceLoader.THREAD_LOAD_LOADED:
                get_tree().change_scene_to_packed(
                    ResourceLoader.load_threaded_get(path))
                return
            ResourceLoader.THREAD_LOAD_FAILED, \
            ResourceLoader.THREAD_LOAD_INVALID_RESOURCE:
                return
            _:
                await get_tree().process_frame
```

This keeps your loading screen animating while Godot pulls the scene off disk on a background thread.

## Hot-Path Allocations

`contacts_received` fires up to 60 times per second. Anything you allocate in that handler runs through the garbage collector, and GC pauses are visible on a constrained device.

Reuse buffers rather than allocating per frame:

```
var _by_glyph: Dictionary = {}

func _on_contacts(contacts: Array) -> void:
    _by_glyph.clear()  # reuse, don't reallocate
    for c in contacts:
        _by_glyph[c.glyph_id] = c
    _process_by_glyph(_by_glyph)
```

The entries in `contacts` are raw `Dictionary` values — that’s deliberate, since wrapping each of the up-to-60-per-frame contacts in an object would allocate on the hot path. If you need typed access for a non-hot-path read, wrap a single contact with `BoardContact.from_dict(c)` (which exposes the `BoardContact.Type` / `BoardContact.Phase` enums plus the `facing() -> Vector2` and `is_piece()` helpers) — but don’t convert the per-frame loop itself.

For numeric data, prefer `PackedFloat32Array` / `PackedByteArray` over generic `Array` — typed arrays avoid Variant boxing. Don’t `print()` in hot paths; the Android log pipe takes a mutex and drops frames.

## Texture Settings

Most 2D assets render fine on Board with default import settings. Two cases to override:

- **Large textures**: cap above ~1024×1024 against the Genio 700’s texture budget. Lower the maximum size in the import inspector for non-critical assets.
- **UI atlases**: pack small sprites into a single atlas. The GL Compatibility renderer batches draws sharing a texture, so atlasing turns 30 draw calls into 1.

## Audio

For short SFX (tap, place, lift), preload as `AudioStreamWAV` so it caches as raw PCM:

```
var _sfx_place: AudioStreamWAV = preload("res://audio/sfx/place.wav")
```

For background music and voice lines, `AudioStreamOggVorbis` is fine — decode runs on a separate thread.

## Profiling on Device

Godot’s built-in profiler runs over the editor’s remote debugger. Export the APK ( **Project → Export Project… → Android**, or `godot --headless --export-debug "Android" game.apk`) and install it to the Board — drag the `.apk` onto the Board Connect web UI in your browser (the Board shows its address under **Settings → System**), or have an agent install it with the `board-connect` CLI (`board-connect install game.apk`). Then open Debug → Open Profiler in the editor and point remote debug at the device’s IP. Watch:

- **Frame Time** — should be ≤16.67 ms for 60 fps.
- **Process Frame Time** — your `_process` / `_physics_process` cost.
- **Render Time** — GPU work. High here means draw-call or texture-size pressure.

Always profile on real hardware. A scene at 90 fps in the editor can hit 25 fps on Board because the desktop GPU is 5–50× faster.

## Optimization Checklist

| Concern | Action |
| --- | --- |
| Renderer | `gl_compatibility` set in `project.godot` |
| PCK compression | `noCompress "pck", "sparsepck"` added to `build.gradle``aaptOptions` (one-time setup) |
| Boot | Boot scene with `WorkerThreadPool` prewarms big assets |
| Scene transitions | `ResourceLoader.load_threaded_request` for any scene >2 MB |
| Hot paths | No `Dictionary` allocation or `print()` in `_on_contacts` |
| Audio | SFX as preloaded `AudioStreamWAV` |
| Textures | No texture >1024 unless necessary; UI packed into atlases |
| Profiling | Profiled on real hardware — not the editor |
