Aqualead Engine Core
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.
| Service | Class | Used by game? |
|---|---|---|
| Heap | ALHeap / ALHeapBaseImp | yes — all game allocations |
| Cooperative fibers | ALFiber | yes — the entire frame |
| Update objects | ALUpdaterImp | yes — tasks, fades, pads |
| Message queues | ALMessageQueue | yes — deferred commands |
| Textures/sprites | ALTexture/ALSprite (+ImpGLES) | yes — all 2D |
| Sound | ALSound/ALVoice (+ImpOAL) | yes — all audio |
| Scene graph | ALSceneImp | engine-internal only |
| Widgets / FSM / collision / particles / tilemap | ALWidgetBase / ALFsm / ALCollision3D / ALParticleEmitter / ALTileMap | dead code — never instantiated |
| Scripting (Lua 5.1.5) | ALLuaImp / ALScriptImp | dead — 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);
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 | |
|---|---|
| +0x00 | vtable |
| +0x04 | child/next fiber |
| +0x08 | saved stack pointer |
| +0x0c | stack size (0x100) |
| +0x10/+0x14 | flags |
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);
+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:
ttStartTask(id, fn, arg, stack, prio)enqueues a 24-byte start command (layout in Chapter 2);- at the frame boundary,
SetAfterUpdateFuncdrains the queue viaDALUpdaterExecCommand, creating/destroying/sleepingALUpdaterImpobjects; - task ids resolve through
TaskList[id](base0x6c8760), each holding a generation-tagged handle; ttCountTask(id)validates low-16-bit index / high-16-bit generation before reuse (the classic ABA guard).
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
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:
- Game side:
ALSpriteDrawGLES::Draw → ALVertexList::AddRectetc.: appends to the packet buffer. - Sync:
ALSystemImp::DrawThreadMutex+DrawThreadSema/DrawWaitSema: mutex-protected packet replay, not double buffering. - Frame end:
WinSwapDB()inmotherTaskpublishes the frame.
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:
| Abstract | Implementation | Backed by |
|---|---|---|
| ALSystemImp | ALSystemImpGLES | EGL + GLES2 (Mali blob on device) |
| ALScreenImp | ALScreenImpGLES | EGL surface |
| ALTextureImp / ALMeshImp | *GLES variants | GLES2 textures/VBOs |
| ALPadImp | ALPadImpUnix | evdev EVIOCGKEY |
| ALSoundImp | ALSoundImpOAL | OpenAL + 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.