Phones, tablets and browsers: what was measured
The research behind the mobile-web skill, the layout runner, export -Run for Web presets and doctor's Web and
Android checks, gathered on 2026-10-21 with Godot 4.7.2 (mono and standard builds) on Linux. Android exports were built
and inspected here; nothing ran on a phone (no device, and no KVM for an emulator), and iOS needs macOS.
Sources
| Source | What it gave |
|---|---|
| Godot docs 4.7: Exporting for the Web | No C# on the web; single-threaded export the default since 4.3; COOP/COEP for threads; hosts that compress (GitHub Pages) and do not (itch.io); WebGL 2 only; audio, fullscreen, gamepad and background-tab limits |
| Godot docs 4.7: Exporting for Android, Gradle builds | SDK and JDK versions, keystore variables (keystore and key passwords must match), folders starting with _ skipped by Gradle builds |
Godot 4.7.2 source: platform/web/export/export_plugin.cpp, platform/android/export/export_plugin.cpp, os_web.cpp, base_button.cpp, ProjectSettings.xml, DisplayServer.xml |
The C# web refusal, net9.0 for prebuilt C# Android, minSdk 24 and targetSdk 36, the asynchronous IndexedDB sync, multitouch buttons, touch emulation defaults, which platforms implement safe areas and cutouts |
| Android: page sizes (updated 2026-10-16), target API | 16 KB pages required for updates from 1 February 2027; target API 36 for new apps and updates since 31 August 2026 |
| Android accessibility, Apple Human Interface Guidelines (Buttons), WCAG 2.2 SC 2.5.8 | 48x48 dp, 44x44 pt, 24x24 CSS px |
| Apple: upcoming requirements | iOS 26 SDK for uploads since April 2026 (search summary; the page was not read directly) |
| CrazyGames technical requirements | 50 MB initial download, 20 MB for the mobile homepage |
| coi-serviceworker (MIT) | The other way to get cross-origin isolation on hosts without headers |
| Virtual Joystick for Godot 4 (MIT) | An on-screen stick add-on |
| "Controlling Carnage: Effective Side-Scroller Controls on Mobile" (Roadhouse Interactive, GDC; notes) | Hit pads larger than the drawn buttons, no dead spot under a thumb, sliding between pads, the largest pad for jump, fading art, a tighter layout under 7 inches; the C# example's Controls/TouchPads.cs and TouchControls.cs |
| Zach Gage, "Controls You Can Feel" (GDC talk on touch controls; notes) | 75 x 75 pt for buttons pressed without looking (Super Crate Box), 44 pt for buttons looked at, nothing under 40 pt; sticks as the weak option; swipe up, not down. The small-control check (minControlDp) and the skill's thumb-control rules |
Measured here
| What | Result |
|---|---|
| C# Web export (mono editor, the C# example) | exit 1: "Exporting to Web is currently not supported in Godot 4 when using C#/.NET"; the mono templates have no web zips |
| Single-threaded web build | starts in Chromium (SwiftShader WebGL 2) in 1.5 to 3.5 s, with or without COOP/COEP headers |
| Threads build without headers | never starts: "missing features: Cross-Origin Isolation, SharedArrayBuffer"; with headers it starts |
| PWA option "Ensure Cross Origin Isolation Headers" | works, but the first load stalled ("Service worker already exists") until a second reload |
| Web size (GDScript example) | index.wasm 37.7 MB, 9.7 MB gzip, 6.8 MB brotli; the whole folder 38 MB, 9.9 MB gzip |
Exporting into res://build/web |
the next export packed the exported icons: index.pck grew from 2.7 KB to 26.8 KB |
A call on null in _ready |
release build: silent, the function carries on; debug build: SCRIPT ERROR with a backtrace |
user:// on the web |
/userfs, IndexedDB; a counter survived a reload; the sync starts on the next frame after the file closes |
| Touch in Chromium (Playwright, 412x915 at 2.625x) | InputEventScreenTouch plus an emulated mouse button; positions in device pixels; ScreenGetDpi() 252 |
| Safe area on the web | the whole window; no cutouts |
| Headless Firefox on a GitHub Linux runner (no GPU) | the engine never starts: "missing features: WebGL2", also with webgl.force-enabled; Chromium (SwiftShader) and the runner's Edge start it. With HYPERDOT_WEB_HEADED=1 and a window under Xvfb (libgl1-mesa-dri), Firefox passes every check |
| Audio unlock | could not be reproduced: headless and headed Chromium started the AudioContext without a gesture |
| Android APK (GDScript) | 26.4 MB, 14 s; minSdk 24, targetSdk 36, arm64-v8a; zipalign -c -P 16 passed; every .so aligned to 16 KB |
Android export without import_etc2_astc |
refused: "ETC2/ASTC texture compression is required for Android export" |
| Android export without an icon | succeeds, but logs ERROR: No project icon specified (the kit now ignores the line; doctor warns) |
ANDROID_HOME/JAVA_HOME with an existing editor settings file |
ignored ("Invalid Android SDK path"); a fresh XDG_CONFIG_HOME makes them apply |
| C# Android, prebuilt template, net8.0 | refused: "the export template only supports 'net9.0'"; with net9.0 and .NET 9: a 102 MB APK with about 60 MB of static .a files |
| C# Android, Gradle AAB, net8.0 | 39 MB, no .a files, signed; 34 s with a warm Gradle cache (the first Gradle build hit HTTP 429 from Maven Central once) |
| Godot headless | exit 134 (abort) after finished exports or imports, 4 times in 23 runs: Parameter "singleton" is null at shutdown |
layout runner, headless and with a display |
identical output; 0.4 s for a menu |
TouchScreenButton in 4.7.2 |
a Node2D (ClassDB.get_parent_class), so the runner measures it apart from Controls. Touches hit its shape, centred on the texture when shape_centered is on and there is a texture, around the node's origin when there is none; without a shape, the texture's rectangle (checked with InputEventScreenTouch at points inside and outside) |
C# safe area
The SafeArea autoload in the skill, and the container that applies it (verified with the layout runner: the fixture's
fixed menu passes on every profile only when SetInsets reaches it).
public partial class SafeMargins : MarginContainer // anchor it full rect; children stay inside the safe area
{
[Export] public int Extra { get; set; }
private SafeArea _safeArea = null!;
public override void _Ready()
{
_safeArea = GetNode<SafeArea>("/root/SafeArea");
_safeArea.Changed += Apply;
Apply();
}
public override void _ExitTree() => _safeArea.Changed -= Apply;
private void Apply()
{
Rect2 view = GetViewportRect();
Rect2 safe = _safeArea.CanvasRect();
AddThemeConstantOverride("margin_left", Mathf.CeilToInt(safe.Position.X - view.Position.X) + Extra);
AddThemeConstantOverride("margin_top", Mathf.CeilToInt(safe.Position.Y - view.Position.Y) + Extra);
AddThemeConstantOverride("margin_right", Mathf.CeilToInt(view.End.X - safe.End.X) + Extra);
AddThemeConstantOverride("margin_bottom", Mathf.CeilToInt(view.End.Y - safe.End.Y) + Extra);
}
}
The autoload also connects GetTree().Root.SizeChanged to its Changed signal and reads --safe-area=L,T,R,B from
OS.GetCmdlineUserArgs() in _Ready, so a desktop run can fake a notch.
Mistakes an agent is likely to make
- Trying to export a C# game to the web, or "fixing" it with settings.
- A threads build hosted on itch.io or GitHub Pages: a blank "missing features" page.
- Exporting into the project, so the next export packs the last one.
- Smoke-testing a release web build, which hides script errors.
- Expecting
ANDROID_HOME/JAVA_HOMEto configure a headless export when editor settings exist. - Forgetting
import_etc2_astc(the export refuses) and the icon. - C# on Android with the prebuilt template on net8.0; and the .NET first-run banner read as the target framework (
DOTNET_NOLOGO=1). - A portrait game left in the default landscape orientation, or with an unswapped viewport.
- Reading
GetDisplaySafeArea()as canvas coordinates, or expecting it on web and desktop, or cutouts on iOS. - Treating
ScreenGetDpi()on Android as exact, or the emulated mouse as multitouch. - Expecting
low_processor_modeto help a game that redraws every frame. - Believing an automated browser proves the audio unlock.
- Trusting Godot's exit code alone: 4.7.2 headless can abort with 134 after finishing.
exportruns a crashed export again, up to twice, after deleting what it left (exports[].retried); a crash on every run still fails.