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

See Also