03 · Foundations

Aqualead Engine Core

The proprietary C++ middleware under the game: a fixed-point-of-truth heap, cooperative fibers over update objects, a handle system with generation counters, and the screen/scene layer that turns one fiber's output into a frame.

3.1What Aqualead is, andand what the game uses of it

Aqualead is a Japanese game middleware library (its classes all carry the AL prefix) that this build links statically. The symtab shows 3,273 vtables (a large class library) but decompiled call-graph analysis shows the game only uses the low-level services: heap, fibers, update objects, message queues, textures, sprites, sound, archives. The higher-level frameworks (scene graphs, widgets, FSMs, collision, particles, tile maps) are compiled in and never instantiated by gameplay. Gameplay code is procedural C-style functions poked into raw structs.

ServiceClassUsed by game?
HeapALHeap / ALHeapBaseImpyes — all game allocations
Cooperative fibersALFiberyes — the entire frame
Update objectsALUpdaterImpyes — tasks, fades, pads
Message queuesALMessageQueueyes — deferred commands
Textures/spritesALTexture/ALSprite (+ImpGLES)yes — all 2D
SoundALSound/ALVoice (+ImpOAL)yes — all audio
Scene graphALSceneImpengine-internal only
Widgets / FSM / collision / particles / tilemapALWidgetBase / ALFsm / ALCollision3D / ALParticleEmitter / ALTileMapdead code — never instantiated
Scripting (Lua 5.1.5)ALLuaImp / ALScriptImpdead — fiber creation stubbed

3.2The heap

Everything allocates from a single Aqualead heap created in main: if ALSystemImp::UseMemorySize is 0, a malloc-backed heap (ALHeapBaseImp::CreateMallocHeap); otherwise a fixed region (ALHeap::Create(ptr,size)). On the original board the game runs with the malloc-backed heap over the process's address space. The heap is current-thread global (ALHeap::SetCurrent): allocation sites in decompiled code appear as plain operator_new with a source-file/string tag:

this = operator_new(0x48,
    "/jenkins/workspace/dgf/Proj/Linux_OGLES/../../../Aqualead/Src/ALUpdaterImp.h",
    "", 0x106);
Allocation tags: every engine allocation carries its header file and line: the reason the RE work can attribute engine internals so precisely.

3.3Fibers: ALFiber

A fiber is a tiny coroutine: vtable, child-fiber link, saved stack pointer, and a 256-byte stack (this is the fiber frame; the big stacks live in the underlying pthreads). main installs a shared 120,000-byte scratch stack (ALFiber::InitStack) that fibers use for their frames.

ALFiber layout
+0x00vtable
+0x04child/next fiber
+0x08saved stack pointer
+0x0cstack size (0x100)
+0x10/+0x14flags

The live scheduler is ALSystemImp::UpdateAll (@ 0x226a08), called every frame from the engine's Process (SystemState = 1; UpdateAll(); SystemState = 2;). It walks the update-object priority list; most objects update synchronously, but objects flagged with “has own fiber” get a real context switch through ALUpdaterImp::SwitchToFiber. Outside the dispatch loop the yield function is FailYield, a deliberate no-op that makes stray yields harmless.

/* ALSystemImp::UpdateAll (0x226a08) — one frame's update pass */
ALFiber::SetCurrentYieldFunc(DefaultYieldFunc);
CurrentUpdateObject = ALPrioList::GetTop(...);
(*UpdateFiber->vtbl[0])(UpdateFiber);
while (NextUpdateObject != 0) {
    while (CurrentUpdateObject->flags & 0x40) {         // owns a fiber?
        ALUpdaterImp::SwitchToFiber(CurrentUpdateObject); // true context switch
        (*UpdateFiber->vtbl[0])();
        if (NextUpdateObject == 0) goto done;
    }
    ALUpdaterImp::SetFiber(CurrentUpdateObject, UpdateFiber);
    CurrentUpdateObject = NextUpdateObject;
    NextUpdateObject = *(NextUpdateObject + 0xc);
    UpdateFiber = ALFiber::Create(UpdateAllFunc, 0);    // lazy per-object fiber
    (*UpdateFiber->vtbl[0])();
}
done:
ALFiber::SetCurrentYieldFunc(FailYield);
Exhibit: the two-tier scheduler: synchronous fast path, promoted fibers for objects that yield mid-update (sleep counting, awaken callbacks via vtable +0xd8 = RefreshControllerState).

3.4Update objects & the deferred task layer

ALUpdaterImp (72 bytes) is the unit of schedulable work: it carries an update callback, a priority, a sleep/awake flag pair, and a handle. The game's own task API (ttStartTask/ttStopTask/ttCountTask, from the PS2-era code) is a thin producer of commands into this system: nothing takes effect immediately:

Handles themselves are managed by ALHandleManagerImp: a growable slot array with a free-list threaded through the slots, and a single global 16-bit MagicCounter bumped on every allocation:

slot = { uint16 generation; uint16 next_free_or_unused; void* ptr; }   // 8 bytes
Why this matters for reimplementation The observable contract is: (a) task lifecycle changes are deferred to frame boundaries, (b) handles stay valid across recycle because of generation checks, (c) every update object has a priority. You can reproduce this with plain coroutines and a sorted list; the mechanism (real ucontext-style fibers) is an implementation detail.

3.5Screens, scenes, and the frame pipeline

ALScreenImp is the render target abstraction (one per display); the engine keeps a screen list (ALScreenList::DrawAll) and a draw-order counter. Each frame the game's fibers record what to draw (sprites, models, fade boxes) into draw packets; the render thread (Chapter 12) owns the GL context and interprets the packets. The boundary is explicit in the code:

3.6Property sets and engine singletons

Engine-wide state lives in a handful of singletons reached through fixed offsets: ALSystem::_Imp (system: tick rate at +0x10f, exit code at +0x440), ALFadeImp::DefaultScreen (the screen fader used by every transition), ALTextureImp statics (default filters, UV offset 0xbf000000). The game layers its own globals on top at fixed addresses: the full map is in the Appendix.

3.7The Imp backend pattern

Every platform-facing class is an abstract ALX with per-platform ALXImp* implementations selected at init. The shipped build uses:

AbstractImplementationBacked by
ALSystemImpALSystemImpGLESEGL + GLES2 (Mali blob on device)
ALScreenImpALScreenImpGLESEGL surface
ALTextureImp / ALMeshImp*GLES variantsGLES2 textures/VBOs
ALPadImpALPadImpUnixevdev EVIOCGKEY
ALSoundImpALSoundImpOALOpenAL + ALSA
ALStreamImp(unix)stdio + save registry

This is exactly where the port shim attaches (Chapter 1). Any reimplementation should internalize this: a fact worth internalizing for any reimplementation: the engine's backend seams are the port's API.

← 02 · Hardware & OS Platform