Android¶
The normal Android workflow starts with a standalone game created by the packaged Snowpulse SDK. From the extracted SDK directory, create the game first:
That command generates /path/to/projects/MyGame/android/ along with the
game's CMake project, source, assets, and platform metadata. This guide assumes
that generated project layout. If you have not created a game yet, start with
Getting Started.
For GameAnalytics/ByteBrew selection, generated-project dependencies, R8 rules, and the optional redistribution-approved ByteBrew AAR path, see Analytics.
Snowpulse Android games use a native C++17 shared library hosted by AndroidX GameActivity. The shipping renderer is OpenGL ES 3 through EGL; Android Frame Pacing presents frames through Swappy when vsync is enabled. Games target touch-first phones and tablets on Android 9/API 28 or newer.
Vulkan, controllers/Android TV, Chromebook certification, soft-keyboard game text input, Play Asset Delivery, Play Games cloud saves, billing, ads, and Android video recording are not part of the v1 backend. The presentation and asset-provider boundaries are designed so Vulkan and Play Asset Delivery can be added later without changing game-facing APIs.
Pinned Toolchain¶
Android upgrades are validated as a complete set. Do not update one item in isolation in a release branch.
| Component | Version |
|---|---|
| JDK | 17 |
| Gradle wrapper | 9.5.0 |
| Android Gradle Plugin | 9.3.0 |
compileSdk / targetSdk |
36 |
minSdk |
28 |
| Build Tools | 36.0.0 |
| Android NDK | 29.0.14206865 (r29) |
| CMake | 3.22.1 |
| GameActivity | 4.4.2 |
| Android Frame Pacing | 2.1.3 |
| Oboe | 1.10.0 |
| Media3 | 1.10.1 |
| OkHttp | 5.4.0 |
The pins follow the official AGP 9.3 compatibility matrix, NDK revision history, AndroidX Games releases, Oboe 1.10.0 release, and Media3 releases. API 36 also meets the Google Play target requirement taking effect on August 31, 2026; see Target API level requirements.
Developer Installation¶
Install the current stable Android Studio or Android command-line tools, then use SDK Manager to install:
- SDK Platform 36 and Build Tools 36.0.0
- Platform Tools (
adb) - NDK 29.0.14206865
- CMake 3.22.1, Ninja, and LLDB
- Android Emulator
- API 28 and API 36 emulator images
Install JDK 17 separately if Android Studio does not provide the required JDK.
Set JAVA_HOME to that JDK and ANDROID_SDK_ROOT to the SDK. Accept SDK
licenses with sdkmanager --licenses. Install bundletool for local AAB
validation and split-APK testing.
Standalone SDK and game projects¶
From the extracted SDK directory, diagnose the environment without changing
it. Keep using this SDK launcher when maintaining the generated game; there is
no supported ./snowpulse launcher in the client project's root directory.
Engine contributors¶
The repository tool is a separate maintainer alternative. Use it only from a full Snowpulse engine source checkout when working on engine-tree examples:
The doctor reports a non-17 active Java, missing or mismatched SDK packages,
unaccepted licenses, missing emulator images, and missing bundletool. A
system Java 25 or CMake 4.x does not replace the pinned JDK 17/CMake 3.22.1
pair used by Gradle.
Generated Project¶
snowpulse new creates a complete android/ project beside the CMake game:
project/
.snowpulse/ # managed-file hashes and project metadata
android/
app/
build.gradle.kts
proguard-rules.pro
src/main/AndroidManifest.xml
src/main/res/
gradle/libs.versions.toml
gradle/verification-metadata.xml
gradle/wrapper/
build.gradle.kts
settings.gradle.kts
gradlew
keystore.properties.example
verify-artifact.sh
assets/
src/
CMakeLists.txt
The app module owns the application ID, display name, version code/name,
icons, orientation, assets, ABI filters, shrinking, and signing. Its
externalNativeBuild points at the game root and requests only the game's
shared-library target. :snowpulseRuntime is an engine-owned Gradle library
containing the reusable GameActivity, OkHttp, and Media3 bridge.
For a standalone project created by an older SDK, run the current SDK's updater from the SDK directory:
No selector means --all, so a missing android/ project and a missing
assets/asset-packs.json are added while the SDK and unchanged generated
infrastructure are refreshed. Use --android, --asset-packs, or
--sdk-only for a targeted update. The updater never replaces gameplay
source or existing assets; it only migrates recognized generated AppConfig
and stock portal setup to target-config defaults. Custom code is preserved with
a warning. Customized generated
files remain in place, with current candidates staged under
.snowpulse/pending/; deliberate replacement requires --force-managed.
For an already generated game with a vendored engine, use the default update
or --sdk-only to receive runtime fixes such as system volume-key handling,
then rebuild and reinstall the APK. --android alone refreshes scaffolding but
does not replace the vendored engine source.
Open <project>/android in Android Studio. Do not open the repository root as an
Android project. The default orientation is unspecified; change
android:screenOrientation in the app manifest to portrait, landscape, or
another Android-supported value without changing native code.
App Entry and CMake¶
Define the application once:
std::unique_ptr<snowpulse::App> createApplication() {
return std::make_unique<Application>();
}
SNOWPULSE_DEFINE_APP(createApplication)
Create its target with:
snowpulse_add_game(application
SOURCES ${APPLICATION_SOURCES}
DISPLAY_NAME "Application"
BUNDLE_ID "com.example.application"
ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets"
ASSIMP ON
SPINE_VERSION "4.3"
WEB_MODE GAME
WEB_PLATFORM SNOWBLINK_PLAIN
)
snowpulse_add_game creates a shared library on Android, an app-bundle
executable on iOS, and a normal executable elsewhere. It also selects the
engine variant and owns web/iOS packaging; generated projects use this common
declaration.
Android configuration defines SNOW_PLATFORM_ANDROID and excludes GLFW,
GLAD, desktop tools/tests, desktop libcurl, and unrelated example targets.
Engine-tree examples created or adopted with snowpulse-engine own an
android/ project and one unconditional root CMake registration; see
Engine-Tree Example Projects.
Build and Deploy¶
Run from the generated android/ directory:
Debug builds contain arm64-v8a and x86_64, native debug symbols, and the
normal Android debug signature. Use adb logcat for native and Java logs:
Release builds require explicit upload-key credentials:
export SNOWPULSE_ANDROID_KEYSTORE_FILE=/secure/path/upload.jks
export SNOWPULSE_ANDROID_KEYSTORE_PASSWORD='...'
export SNOWPULSE_ANDROID_KEY_ALIAS='upload'
export SNOWPULSE_ANDROID_KEY_PASSWORD='...'
./gradlew :app:assembleRelease
./gradlew :app:bundleRelease
The release APK and AAB are under app/build/outputs/apk/release/ and
app/build/outputs/bundle/release/. Release contains only arm64-v8a and
uses -O3, ThinLTO, hidden visibility, section garbage collection, R8,
resource shrinking, and stripped shipped libraries. Full native debug symbols
and the R8 mapping.txt remain separate build outputs for Play Console crash
symbolication.
Release tasks fail if credentials are absent; they never fall back to the
debug key. Instead of environment variables, copy
keystore.properties.example to the ignored keystore.properties and fill
the same four names. Never commit a keystore or real secrets.
Use Play App Signing with a separately protected upload key. An AAB is a Play publishing artifact, not an installable application; see Android App Bundles.
AAB and Release Verification¶
Validate and locally install an AAB:
bundletool validate --bundle app/build/outputs/bundle/release/app-release.aab
bundletool build-apks \
--bundle app/build/outputs/bundle/release/app-release.aab \
--output app-release.apks \
--ks "$SNOWPULSE_ANDROID_KEYSTORE_FILE" \
--ks-key-alias "$SNOWPULSE_ANDROID_KEY_ALIAS"
bundletool install-apks --apks app-release.apks
Verify the universal APK or signed release APK directly:
apksigner verify --verbose --print-certs app/build/outputs/apk/release/app-release.apk
./verify-artifact.sh app/build/outputs/apk/release/app-release.apk --release
./verify-artifact.sh app/build/outputs/bundle/release/app-release.aab --release
The helper checks signatures, APK 16 KB ZIP alignment, each native ELF LOAD segment, and the arm64-only release policy. NDK r29 emits 16 KB-aligned shared libraries by default; the check prevents an incompatible prebuilt library from slipping into the package. See Android's 16 KB page-size guide.
Runtime Architecture¶
SnowpulseGameActivity extends GameActivity. The native glue owns the app
factory, lifecycle/input poll loop, EGL context and window surface, and render
loop. It handles window replacement, pause/resume, configuration and density
changes, low-memory notifications, process/activity recreation, and
destruction. Java network/video callbacks use token registries or atomic
handoffs, so callbacks cannot call deleted C++ requests or application
objects.
The EGL context is retained while only the window surface is unavailable. An
EGL_CONTEXT_LOST event recreates EGL and restores registered shaders,
asset-backed and dynamic textures, ImGui device objects, and Effekseer device
objects. Renderer command execution remains separate from EGL/Swappy
presentation, preserving the seam for a future Vulkan presenter.
The surface requests RGBA8, depth 24, stencil 8, and GLES 3. Vsync uses
SwappyGL_swap; disabling vsync uses direct eglSwapBuffers.
Subscribe to Android lifecycle events through the normal event bus:
auto subscription = context()->events()->on<snowpulse::AppLifecycleEvent>(
[](const snowpulse::AppLifecycleEvent& event) {
// Suspended, Resumed, or LowMemory
});
Override App::onBackRequested() and return true to remain in game. Return
false to let Android finish the Activity through system/predictive Back.
Touch, Safe Areas, and Resizing¶
GameActivity pointer IDs remain stable for each touch. Input::touches()
exposes all active pointers; the first active touch is mirrored to
MouseLeft and mousePosition() for existing scene/UI code. Rotation,
density changes, surface resize, Escape/Back, and standard hardware keys are
handled. Volume Up, Volume Down, Volume Mute, and the camera button remain
system-owned and are not exposed through Input; Android displays its normal
volume UI and adjusts the media stream. Controllers, Android TV, and
Chromebook-specific mappings are deferred.
The Activity suggests AudioManager.STREAM_MUSIC while it is in the
foreground. Calls, accessibility services, and other higher-priority system
audio states may still override the selected stream normally. Audio focus is
separate: focus loss temporarily mutes/suspends engine output without changing
the user's device volume.
Rendering is edge-to-edge and immersive. Insets are reported in logical game coordinates:
Games may apply these values to UIRoot padding; world rendering is not
automatically inset. The Activity restores immersive mode after transient
system bars or focus changes and accepts tablet/multi-window surface changes.
Assets¶
All normal assets stay in the base app module and are read directly from the
APK/AAB with AAssetManager; Snowpulse does not perform first-run extraction.
Logical paths are UTF-8, case-sensitive, relative to assets/, and reject
absolute paths, backslashes, drive prefixes, and .. segments.
IAssetStream provides read, seek, tell, and size; IAssetSource
provides exists, open, readAll, and directory enumeration.
AppContext::assetSource() exposes the active provider. Texture, font, JSON,
atlas, particle, audio, Spine, Effekseer, Assimp, and Tiled paths use this boundary.
The *Rooted methods remain filesystem-only compatibility APIs and should not
be used for packaged Android assets.
Keep Tiled .tmj, .tsj, .tj, and referenced images in their authored
relative directory layout. References are resolved relative to the declaring
JSON file, remain case-sensitive, and load through AAssetManager; do not turn
them into /assets/... filesystem paths. Native arrays and base64 tile data
using zlib, gzip, or zstd are decoded inside the engine and do not need Android
Java or Maven dependencies.
Base assets report Ready through the existing asset-pack API. Play Asset
Delivery can later supply another IAssetSource, but is not implemented.
The Gradle app prevents compression for seek-sensitive audio, MP4, FBX, Spine
binary, and Effekseer formats. Add a custom extension to noCompress if an
application introduces another long-form seekable format.
Effekseer Effects¶
Android builds include the vendored Effekseer runtime and its OpenGL ES 3
renderer; no separate Java or Maven effect dependency is required. Effect
assets use the same AssetManager-backed logical paths as other packaged
assets, and the runtime restores its GLES device objects after
EGL_CONTEXT_LOST.
#if SNOWPULSE_HAS_EFFEKSEER
// Include <runtime/components/effekseer_renderer.h> explicitly.
auto* effect = node->addComponent<snowpulse::EffekseerRenderer>();
effect->sortOrder = 10;
if (effect->load("effects/effect.efkefc")) {
const auto handle = effect->playPrimary();
if (handle != snowpulse::EffekseerRenderer::kInvalidHandle) {
effect->setSpeed(handle, 0.9f);
}
}
#endif
Keep referenced textures, models, and materials under assets/ using the
relative layout expected by the authored effect. Effekseer sound modules and
distortion/background capture are disabled. See
Particles and Trails for handle controls and
cross-platform usage.
Platform Services¶
- Audio: statically linked vendored OpenAL Soft with the Maven/Prefab Oboe backend. It supports short buffers, seekable streamed music, focus and lifecycle muting; Oboe handles device/Bluetooth/headset route reopening.
- HTTPS: asynchronous OkHttp over JNI, with verbs, query parameters, headers, redirects, per-call timeout, cancellation, response status/body/headers, and callbacks dispatched from the game thread. Android's trust store is used and cleartext HTTP is rejected by the manifest/network policy.
- Saves: atomic slot writes below app-private
filesDir; no storage permission is required. Saves survive updates and are included in Android backup rules; cache directories are excluded. - MP4: Media3 ExoPlayer renders
asset:///...in a control-free overlay above GLES and follows Activity lifecycle cleanup. - ImGui: Snowpulse input plus
imgui_impl_opengl3GLES3. - Spine, Assimp, Box2D, and Effekseer: vendored native sources; Effekseer uses its GLES3 renderer.
- MIDI, portal integrations, and cloud saves: existing null implementations.
Dependency Policy¶
Engine-owned C/C++ libraries remain source-vendored and compile as static PIC
libraries: OpenAL Soft, Assimp, Box2D, Spine, Effekseer, stb, GLM, JSON, zlib,
zstd, and the audio decoders. They link into the game .so with hidden visibility,
section garbage collection, and ThinLTO. GameActivity and Swappy use their
static Prefab variants. The official Oboe 1.10.0 Prefab release is a shared
library and all three official native packages declare c++_shared, so the APK
also contains liboboe.so and one matching libc++_shared.so; Snowpulse never
mixes static and shared libc++ runtimes.
Do not vendor the Android SDK, NDK, JDK, CMake, Android Studio, Gradle
distribution, general third-party AARs, or Maven caches. The explicitly
redistribution-approved ByteBrew AAR is the documented exception. GameActivity,
Frame Pacing, Oboe, Media3, OkHttp, and GameAnalytics are pinned Maven/Prefab
dependencies because they track Android platform behavior, codecs, networking
security, device compatibility, or provider releases. Generated settings
configure google(), Maven Central, and GameAnalytics's pinned release
repository (https://maven.gameanalytics.com/release). Dynamic versions are
banned, and Gradle dependency-verification checksums are committed. Android
does not use vcpkg or libcurl.
Profiling and Test Matrix¶
Use Android Studio CPU/Memory profilers, System Trace, GPU Inspector, and
adb shell dumpsys gfxinfo <application-id> as appropriate. Profile Swappy
presentation at 60, 90, and 120 Hz on representative Adreno and Mali devices.
Before a platform release, test API 28 and API 36 emulators, an API 28 physical phone, a current phone, a tablet, and a 16 KB-page device/emulator. Exercise asset loaders, every HTTP verb and failure mode, multi-touch identity, Back, rotation/resizing/insets, saves across process death and signed update, Home/resume, lock/unlock, focus loss, surface/context recreation, and repeated video show/hide. Rebuild representative desktop, web, and iOS targets after changes to common asset, input, audio, or GLES code.