AI summary:
The engine now builds and runs on Android arm64, using
the exact same Vulkan 1.3 backend as the PC path. There is no GLES renderer, no cut-down mobile fork, and no separate shader set. The renderer that draws your desktop frame is the renderer that draws the phone's.
It renders, takes touch input, handles rotation, survives being backgrounded, plays positional audio, loads mobile-compressed textures, and composites an HTML/CSS HUD over a streamed 3D world with a playable character.
And rather than claim "it works on Android", I ran it on a device farm.
176 device/OS combinations on real hardware. The full results — including everything that fails, and why — are in this post.
What's in it
- Vulkan 1.3 + descriptor indexing (bindless) as the baseline — the same requirement as PC. WSI via vkCreateAndroidSurfaceKHR.
- Touch input, including a virtual joystick (left thumb moves, right thumb turns).
- Display rotation and resume from background.
- Positional audio — openal-soft is now the Android sound provider, so the whole environmental audio pillar (EFX reverb, HRTF, occlusion) lights up on a phone.
- ASTC / EAC textures — proper mobile formats, with a headless publish step: TitanEditor --publish=android.
- RmlUi — the HTML/CSS UI stack cross-compiles for Android and is fully interactive by touch.
- A real game demo — streamed Game::World, textured terrain, a playable character, RmlUi HUD, in landscape.
The deliverable is a shared
libEsenthelProject.so loaded by a NativeActivity subclass. Editor and tutorials are gated off for Android.
The engine's render defaults are DESKTOP defaults
This was the biggest perf lesson, and it will bite anyone porting:
the engine's defaults are desktop defaults, and on a phone they are ruinous. Six shadow cascades at 2048², SSAO, and an FSR-style upscaler are all perfectly sensible on a GPU with a fan.
Measured with GPU timestamps on an Adreno 740 at 2316x1080:
47 -> 72 fps, from three changes.
- Shadows — 6 cascades @2048² down to 3 @1024². Light phase 6.8 -> 3.9 ms.
- Ambient — SSAO down to AMBIENT_FLAT.
- Density — 75% render scale. Opaque 4.9 -> 3.2 ms, light 3.9 -> 2.4 ms.
And one genuine trap. The default density filter is
FILTER_EASU (FSR-style upscaling), and it
costs more than the pixels it saves. Rendering at 75% with EASU (49 fps) was
slower than rendering at 100% and not upscaling at all (55 fps). Pair any density change with
FILTER_LINEAR, or you will conclude — wrongly — that resolution scaling does nothing.
I did not trust that. It was one GPU.
So I measured it on four
"EASU is a trap" is a claim about one GPU's ALU-to-bandwidth balance. Generalising that into a shipping default without checking is exactly the kind of thing that quietly embarrasses you later. So: same world, same fixed camera, same frame counts, four render presets, swept
inside a single run on each device.
GPU milliseconds. "Shipped" is 75% + LINEAR. "+EASU" changes ONLY the filter, so the difference between those two columns is the cost of EASU and nothing else.
Code:
GPU device shipped +EASU native(100%) EASU costs
-----------------------------------------------------------------------------
Adreno 740 S23 Ultra 5.04 11.32 8.95 +5.5 ms
Adreno 750 Galaxy S24 3.36 5.73 4.89 +2.5 ms
Mali-G710 Pixel 7 Pro 6.11 12.53 8.67 +6.0 ms
Mali-G68 Galaxy A54 18.97 32.95 29.79 +15.1 ms
The trap is universal, and far worse on Mali than on the Adreno where I found it. On every GPU measured, upscaling and then running EASU costs more GPU time than simply rendering every pixel. A Mali-G68 pays
+15.1 ms — on a device whose entire 60 fps budget is 16.7 ms. Generalising the Adreno figure would have understated the problem by 3x.
The reassuring half: the shipped default (75% + LINEAR) is the fastest preset on all four GPUs. The setting was right. The evidence for it was thin, and now it isn't.
Read GPU milliseconds, not fps. The Adreno 750 sits at ~70 fps under
every preset while its GPU time swings 3.4 -> 6.4 ms. It is panel/CPU-capped, so fps there measures the ceiling, not the render cost. Benchmarking a phone by frame rate alone measures the screen.
The device matrix — 176 combos on real hardware
Code:
Runs 94 Vulkan 1.3, bindless, 0 validation errors
Below baseline 80 Vulkan 1.1 — not ONE exposes dynamic_rendering
Fails 2 Mali-G52 MC2 — no descriptor indexing
Measured 176 / 199 catalogue combos
Phones it RUNS on — 0 validation errors
"fps" is filled in only where I ran the perf sweep; it is a property of the GPU, not the phone. "BC" is whether the GPU can read the engine's BC7 textures natively — see further down, this turns out to matter more than the failure column.
Code:
GPU fps BC Devices
------------------------------------------------------------------------
Mali-G715 — no Pixel 8, 8 Pro, 8a, 9, 9 Pro, 9 Pro XL,
9 Pro Fold, 9a, 10a
Adreno 740 88 yes Galaxy S23, S23+, S23 Ultra, Tab S9,
Z Flip5, Z Fold5, Xperia 1 V
Adreno 750 69 yes Galaxy S24, S24+, S24 Ultra,
Z Flip6, Z Fold6
Mali-G710 53 no Pixel 7, 7 Pro, 7a, Pixel Fold, Pixel Tablet
Mali-G68 25 no Galaxy A16, A26, A35, A54, Tab S9 FE
Adreno 830 — yes Galaxy S25, S25+, S25 Edge, S25 Ultra,
Z TriFold
PowerVR DXT-48-1536 — no Pixel 10, 10 Pro, 10 Pro XL, 10 Pro Fold
Samsung Xclipse 940 — no Galaxy S24, S24+, S24 FE (Exynos variants)
Mali-G57 MC2 — no Galaxy A15, XCover7, moto g (2025)
Adreno 840 — yes Galaxy S26, S26+
Adreno 710 — yes Galaxy A36
Mali-G68 MC4 — no Galaxy A34, Lenovo Tab P12
Mali-G57 — no moto g04, realme C53
Samsung Xclipse 540 — no Galaxy A56
Adreno 735 — yes motorola razr plus 2024
Mali-G720 Immortalis — no Galaxy Tab S10+
Samsung Xclipse 530 — no Galaxy A55
Samsung Xclipse 920 — no Galaxy S23 FE (Exynos variant)
Mali-G78 — no Pixel 6
Adreno, Mali, PowerVR and Samsung Xclipse all run it. Every current flagship line — Pixel 6 through 10, Galaxy S23 through S26, the foldables — plus a decent spread of mid-range.
Phones it FAILS on
Code:
Mali-G52 MC2 Galaxy A06, moto g05 driver 49.1.0
no descriptor indexing — 11 validation errors
Two devices, one GPU class. The engine's materials are bindless, so this is a real product decision — a non-bindless fallback path — not a bug.
Phones BELOW the baseline (Vulkan 1.1 — cannot run it)
Code:
GPU Devices
------------------------------------------------------------------------
Adreno 730 Galaxy S22, S22+, S22 Ultra, S23 FE, Tab S8,
Tab S8 Ultra, Z Flip4, Z Fold4, OnePlus 10T
Adreno 619 Galaxy A52, Tab A9+, Tab S7 FE, OnePlus Nord CE 2/3
Lite, Xperia 10 V, moto g34, CPH2195
Adreno 660 Galaxy S21, S21+, S21 FE, S21 Ultra
Adreno 650 Galaxy Note20, Note20 Ultra, S20 FE, S20 Ultra, Tab S7
Adreno 610 Galaxy A05s, Tab A7, Xperia 10 II, moto g(30),
moto g(9) play, moto g play (2021 / 2024)
Mali-G57 MC2 Galaxy A14, A15, A16, A79, moto g (2022)
Mali-G78 Galaxy S21 Ultra, Pixel 6, Pixel 6a
PowerVR Rogue GE8320 Galaxy A03s, A12, A21
Adreno 642L Galaxy A52s, motorola edge 30
Adreno 710 Xperia 10 VI, moto g96
Mali-G52 / G52 MC2 Galaxy Tab A8, Galaxy A32, Motorola G20,
moto g stylus (2023)
Mali-G68 Galaxy A25, Galaxy A53
Mali-G610 MC3 motorola edge 40 neo
Mali-G77 MC9 motorola edge 40
Mali-G72 Galaxy XCover Pro
PowerVR BXM-8-256 moto g54
Adreno 620 / 630 Pixel 5, Pixel 3
Adreno 506 AQUOS sense2 SH-01L
Read that list carefully, because it is stranger than it looks:
several phones appear in BOTH tables. Pixel 6. Galaxy A15, A16. Galaxy S23 FE, Galaxy S24. Same model name, opposite verdict.
There are two separate reasons for that, and they are worth keeping apart.
1. Regional silicon. Galaxy S23 FE and S24 ship a Snapdragon in some markets and an Exynos in others — Adreno 730 vs Xclipse 920, Adreno 750 vs Xclipse 940. Those are genuinely different GPUs behind one marketing name. Nothing surprising, but it does mean "does the S24 work?" has no single answer.
2. The driver. This is the real finding.
The baseline is a DRIVER threshold, not hardware
Pixel 6, Galaxy A15/A16 and the Mali-G68 A-series are not regional variants. They are the
same silicon, passing or failing purely on which driver version the device shipped with:
Code:
GPU PASSES on driver FAILS on driver
--------------------------------------------------------------------
Adreno 710 512.800.35 / 512.800.48 512.615.0 / 512.615.88
Mali-G57 MC2 38.1.0 32.1.0
Mali-G68 38.1.0 32.1.0
Mali-G78 38.1.0 32.1.0 / 38.0.0
The threshold is
Mali ≥38 and
Adreno ≥512.800. A Pixel 6 on Mali driver 38.1.0 runs the engine; a Pixel 6 on 32.1.0 cannot. Nothing about the phone changed but the driver it shipped with.
Below the threshold the device does not expose Vulkan 1.3 — and crucially,
not one below-baseline device exposes dynamic_rendering even as an extension. So supporting them means writing a second render path, not a shim.
And it is not simply a matter of waiting for old phones to age out. I have Galaxy S22s running a current
Android 16 that still ship an Adreno 730 driver reporting Vulkan 1.1. The OS is new; the GPU driver is not.
The interesting finding is not in the failure column. Look at the BC column in the first table:
60 of the 94 devices that DO run us have no BC texture support — every Mali, every PowerVR, every Xclipse. The engine's own
Engine.pak still ships BC7, so on
64% of our supported devices every texture is silently expanded to RGBA at load. Several times the VRAM. Nothing looks wrong on screen. Only a capability probe can see it, and it is now the biggest open item on the Android side.
Also worth naming: a mid-range Mali-G68 (Galaxy A54) runs the demo at
25 fps even on the mobile preset. If that class of device is in scope, it needs its own settings tier.
One caveat on all of the above: this is
the test farm's catalogue, not the market. It over-represents Pixels and Samsungs and whatever Google chose to stock. Read it as a driver/GPU survey, not a market-share estimate.
The farm's PASS column lies
A warning for anyone doing this: a device-farm "pass" on a game-loop test means
the activity finished. Nothing more.
An app that fails to load its data pak, exits immediately, and renders
absolutely nothing also "passes". That happened here, and I believed it for a while — the pak sat at a path that scoped storage made unreadable, the engine logged "Can't load Pak", quit, drew nothing, and the farm reported
Passed across the board.
So every verdict in the tables above comes from the
device's own logcat — a capability line the probe prints
before any bail-out, so that even hardware we refuse to run on still yields a full data row instead of an empty log. That is why the below-baseline devices have driver versions at all.
Summary
- Same Vulkan backend as PC — no GLES, no mobile fork, no separate shaders
- 94 device/OS combos run it clean across Adreno, Mali, PowerVR and Xclipse — 0 validation errors
- The baseline is a driver threshold, not hardware — the same GPU passes on a newer driver
- 47 -> 72 fps from mobile render settings — and the EASU trap is now measured on four GPUs, not one
- Touch, rotation, resume, audio, ASTC/EAC, RmlUi HUD, playable character in a streamed world
- Five latent engine bugs found and fixed — all of which desktop had been hiding
Open: the
Engine.pak BC7 transcode (64% of supported devices are silently decompressing textures), and a settings tier for mid-range Mali.
More info about the device farm, it was Google Cloud's, all of the scripts and apps needed to run the device farm tests are included in the repo:
The scripts
Code:
scripts/android_gpu_matrix.sh one device per GPU architecture — does it render correctly?
scripts/android_perf_matrix.sh 4 render presets per device (the EASU harness) — NEW
scripts/android_survey_resume.sh capability survey, idempotent, launches async
scripts/android_survey_collect.sh pulls the logcats, prints the report
scripts/android_survey_report.py summary + per-device table + survey.json
scripts/android_tutorial_sweep.sh 37 tutorials on-device with the validation layer
scripts/android_publish_pak.sh the ASTC/EAC Project.pak
scripts/android_thirdparty_build.sh one-time arm64 cross-build
scripts/android_game_demo.sh build + deploy the game demo
scripts/android_fetch_validation_layer.sh
The apps they ship to the devices
AndroidProbe.cpp (capability probe), AndroidPerf.cpp (the perf sweep I added), AndroidSweep.cpp (tutorial sweep), AndroidGame.cpp (the demo) — all tracked, all with CMake targets.
in this fork:
https://github.com/DrewGilpin/EsenthelEngine