Imported from mtojek/syberia_arm64 (
AGENTS.md). Install upstream withnpx skills add mtojek/syberia_arm64. Copyright stays with the author.
Architecture: Android ARM64 .so Wrapper Port
This document describes how the Syberia ARM64 wrapper port works. It is intended for AI agents and developers who want to understand the codebase or create similar ports for other Android games.
To create a new port for a different game, see docs/NEW_PORT.md.
Overview
This project runs an unmodified Android ARM64 shared library (libsyberia1.so) on Linux ARM64 by providing a fake Android environment. No game code is recompiled — the original binary is loaded into memory and executed directly.
The wrapper intercepts all calls the game makes to Android APIs (JNI, OpenSL ES, EGL, bionic libc) and translates them to Linux equivalents (SDL2, glibc, native OpenGL ES).
Execution Flow
main.c
|
+-- mmap(256 MB heap, RWX)
+-- so_load("libsyberia1.so") # Parse ELF, load segments into heap
+-- so_relocate() # Apply ELF relocations
+-- so_resolve(dynlib_functions) # Patch GOT: Android imports -> our shims
+-- so_execute_init_array() # Run .init_array constructors
+-- android_shim_init() # Create fake android_app + SDL window
+-- android_main(fake_app) # Jump into the game
Module Responsibilities
so_util.c — ELF Loader (REUSABLE)
Loads an ARM64 ELF .so into a pre-allocated memory region. Parses program headers, applies relocations (R_AARCH64_RELATIVE, R_AARCH64_GLOB_DAT, R_AARCH64_JUMP_SLOT, R_AARCH64_ABS64), resolves symbols against an import table, and runs .init_array constructors.
Key API:
so_load(filename, base, max_size)— load ELF into memoryso_relocate()— apply all relocationsso_resolve(funcs, count, taint)— patch GOT with our function pointersso_find_addr(symbol)— find symbol address in .soso_find_rel_addr_safe(symbol)— find GOT slot for a symbol (for runtime hooking)hook_arm64(addr, dst)— patch instruction at addr to branch to dst
imports.c — Import Table (GAME-SPECIFIC)
Maps every symbol the .so imports to a real function. This is where Android APIs are redirected to our shims. Contains 370+ entries covering:
- libc/bionic: stdio, stdlib, string, math, errno, locale, etc.
- pthread: Mutex/cond wrappers that translate bionic struct layouts to glibc (ALL mutexes are RECURSIVE — required because the game re-locks from the same thread)
- OpenGL ES: Direct passthrough to native GLES_CM
- OpenSL ES: Redirected to our SDL2 audio bridge
- JNI:
JNI_OnLoadredirected to our fake JavaVM - android_native_app_glue:
android_mainentry point - zlib, libpng, liblog: Direct passthrough or stubs
android_shim.c — Fake Android Environment (PARTIALLY REUSABLE)
Implements android_native_app_glue structs and functions:
struct android_appwith fake activity, window, configALooper_pollAll— processes SDL events, pumps audio callbacksAInputQueue— translates SDL keyboard/gamepad events to AndroidAInputEventAConfiguration— returns hardcoded device config (language, screen density, etc.)ANativeWindow— wraps SDL_WindowANativeActivity— providesinternalDataPath,externalDataPath,assetManagerAAssetManager— translates asset paths to filesystem reads from OBB directory
jni_shim.c — Fake JNI (PARTIALLY REUSABLE)
Creates fake JavaVM and JNIEnv with 512-entry vtables. Most methods are stubs. Key implementations:
FindClass/GetMethodID— return fake handlesCallObjectMethod/CallBooleanMethod— return game-specific values (e.g. package name, OBB path, hasGamePad=true)- The game calls JNI to get the OBB file path and package name
egl_shim.c — EGL to SDL2 (REUSABLE)
Translates EGL calls to SDL2 window management:
eglGetDisplay/eglInitialize— create SDL window with OpenGL ES contexteglSwapBuffers— callsSDL_GL_SwapWindoweglCreateWindowSurface/eglMakeCurrent— return fake handles
opensles_shim.c — OpenSL ES to SDL2 Audio (REUSABLE)
Full OpenSL ES BufferQueue player implementation using SDL2 audio:
- Per-player lock-free SPSC ring buffers (game thread writes, SDL audio thread reads)
- Linear interpolation resampling (any sample rate -> 44100 Hz)
- Mono-to-stereo upmixing
- Volume control (millibel to linear conversion)
- HEADATEND detection: When the game's audio decoder stops feeding data and the ring buffer drains, writes
stopRequestedflag directly into the game'sTeMusicstruct, triggering the native sound completion chain - BufferQueue callbacks fired from
ALooper_pollAll(game thread)
main.c — Entry Point & Crash Recovery (GAME-SPECIFIC)
- Allocates RWX heap, loads .so, resolves imports
- GOT hooks: Patches specific game functions at runtime via GOT slot replacement
- Crash handler: SIGSEGV/SIGBUS handler that decodes ARM64 instructions and skips invalid memory accesses (handles sentinel pointer crashes)
- PadScheme crash recovery: sigsetjmp/siglongjmp wrapper around gamepad functions
util.c / error.c — Helpers (REUSABLE)
Debug printing, fatal error handling.
GOT Hooking Pattern
To intercept a specific game function at runtime:
static void *(*orig_func)(void *, const void *);
static void *hooked_func(void *self, const void *arg) {
void *result = orig_func(self, arg);
if ((uintptr_t)result < 0x1000) // invalid pointer guard
return NULL;
return result;
}
// In main(), after so_resolve():
uintptr_t got_slot = so_find_rel_addr_safe("_ZN8SomeClass10someMethodERK8SomeType");
if (got_slot) {
uintptr_t *slot = (uintptr_t *)got_slot;
orig_func = (void *(*)(void *, const void *))*slot;
*slot = (uintptr_t)hooked_func;
}
Threading Model
- Game thread: Runs
android_main→ game loop →ALooper_pollAll→ SDL events + audio pump - SDL audio thread: Reads from per-player ring buffers, mixes, outputs to hardware
- No mutexes on the audio fast path (lock-free SPSC ring buffers)
- All game API calls happen on the game thread (single-threaded from the game's perspective)
Syberia-Specific Details
These are specific to Syberia and would NOT apply to other games:
- Sentinel pointer 0xb: The game returns 0xb for missing UI layouts. Crash handler skips these.
- BUKA/GOOG string patching: Patches distributor string in .rodata to switch OBB variants.
- TeSFX no-op hooks: TeSFX::stop/close are hooked to no-ops (corrupted sound effect pointers).
- PadScheme sigsetjmp: Gamepad layout functions wrapped with crash recovery.
- TeMusic struct offsets (0x88=mutex, 0x1c0=stopRequested, 0x218=repeat, 0x22b=isPlaying): Hardcoded for this specific binary version.
_exit(0)on quit: The game's destructors crash, so we force-exit.
Build
Docker cross-compilation targeting ARM64 Linux (Trimui Smart Pro sysroot):
make compile # builds via Docker
make shell # interactive shell in build container
make clean # remove artifacts