52 KiB
Aego — Architecture
Status: active architecture specification
Language: Go 1.27+
Repository model: monorepo, single root Go moduleaego
Scope: 2D-first engine with a planned 2.5D path, native desktop/mobile targets, built-in editor, custom scripting language, and compile-time native Go extensions.
1. Purpose
Aego is a 2D/2.5D game engine written in Go.
The engine is designed around five long-term goals:
- A real native engine, not a wrapper around another game engine.
- A built-in editor with a Unity/Godot-like workflow.
- A custom gameplay language with hot reload and editor integration.
- Native Go extensions for users who need to extend the engine itself.
- Portable runtime architecture that can target desktop and mobile without making the renderer, scene system, editor, or gameplay code depend on a specific OS or graphics API.
Aego deliberately uses a small number of low-level external pieces where reimplementing platform plumbing provides little engine value:
- SDL3 for OS integration;
- generated OpenGL bindings;
- build-time tooling such as the font generator.
The engine itself owns scene management, rendering, batching, UI, assets, scripting, editor tooling, physics, audio mixing, networking, persistence, animation, materials, and export.
2. Architectural principles
2.1 Core rules
- No global engine singletons.
- Low-level packages never import high-level packages.
- Runtime code must not depend on editor/dev-only packages.
- Persistent identity is never represented by a runtime handle.
- Runtime handles are generational and never serialized.
- Source assets are not runtime assets.
- Source file paths are not asset identity.
- Scene data is text and Git-friendly.
- Export/runtime data is binary and optimized.
- Renderer does not know about scenes.
- Scene does not know about the editor.
- User Go code imports only
aego/sdk. - Native extensions are compiled into the project executable; they are not dynamically loaded with
plugin.Open. - Every stage ends in a runnable, testable result. Calendar deadlines do not define architectural completion.
2.2 Engine layers
This diagram shows architectural ownership and the main dependency direction, not every legal Go import.
The exact package rules are defined in §4 Dependency rules and enforced by tools/depcheck.
flowchart BT
core["core"]
pinput["platform/input"]
platform["platform"]
gpu["gpu"]
vfs["vfs"]
schema["schema"]
format["format"]
engine["engine / services"]
render["render"]
ui["ui"]
loader["asset/loader"]
scene["scene"]
sceneio["sceneio"]
gameplay["script · physics · audio · anim · net · store"]
sdk["aego/sdk<br/>public native-extension surface"]
runtime["runtime"]
editor["editor"]
project["project"]
pipeline["asset/pipeline"]
apprun["app/run"]
entry["cmd/* · demos · examples · tools · templates"]
core --> pinput
core --> platform
core --> gpu
core --> vfs
core --> schema
core --> format
pinput --> platform
platform --> engine
gpu --> engine
vfs --> engine
gpu --> render
pinput --> ui
render --> ui
format --> ui
vfs --> loader
schema --> scene
loader --> scene
format --> sceneio
schema --> sceneio
loader --> sceneio
scene --> sceneio
engine --> gameplay
schema --> gameplay
scene --> gameplay
scene --> sdk
schema --> sdk
gameplay --> sdk
sdk --> runtime
sdk --> editor
vfs -. dev-only .-> project
format -. dev-only .-> project
project -. dev-only .-> pipeline
loader -. dev-only .-> pipeline
project -. dev-only .-> apprun
pipeline -. dev-only .-> apprun
engine -. dev-only .-> apprun
project -. editor-only .-> editor
pipeline -. editor-only .-> editor
runtime --> entry
editor --> entry
apprun --> entry
Layer meaning
| Layer | Responsibility |
|---|---|
| Foundation | core: handles, math, IDs, clocks, logging, errors, statistics, deterministic utilities. |
| Platform contracts | platform/input, platform, gpu, vfs: OS/GPU/filesystem boundaries and their backend implementations. |
| Metadata contracts | schema, format: type metadata and serialization primitives. They remain independent from scene/editor code. |
| Orchestration | engine: lifecycle, worlds, phases, modules, services, main-thread work and frame execution. |
| Presentation | render, ui: scene-agnostic rendering and immediate-mode UI. |
| Runtime content | asset/loader, scene, sceneio: runtime assets, Node+Components world model and scene serialization. |
| Gameplay systems | script, physics, audio, anim, net, store: feature systems layered over the stable runtime model. |
| Extension surface | aego/sdk: the only supported API imported by user native/ code. It is a facade, not an internal storage layer. |
| Applications | runtime, editor: executable-facing composition of the lower layers. |
| Development-only | project, asset/pipeline, app/run, CLI/tooling: source-project scanning, importing, rebuilding and supervision. These never enter exported runtime dependency graphs. |
The important boundary is not that every box imports only the box directly below it; the important boundary is that the graph remains a DAG and lower/runtime layers never acquire reverse dependencies on editor/dev-only code.
3. Decision registry
Decision IDs are stable references for design discussions, code reviews, migrations, and ADR-style changes.
3.1 Foundation and runtime
| ID | Decision |
|---|---|
| D01 | Go 1.27+; the exact toolchain is pinned in go.mod. |
| D02 | SDL3 is pinned and used only for windowing, GL context creation, events, timing, text input, clipboard, drag/drop, lifecycle and audio device output. SDL_Renderer and SDL_GPU are not used. |
| D03 | OpenGL 3.3 Core is the first GPU backend. Public gpu APIs remain backend-neutral. |
| D04 | No global engine singletons. Construction is Builder → Engine. |
| D05 | All runtime resources use generational handles. Public engine APIs do not expose long-lived pointers to internal storage. |
| D06 | Main thread owns platform, GPU and authoritative game/editor state. Workers compute and return results through queues. |
| D07 | Headless is a first-class engine configuration, not a special build hack. |
| D08 | 2D world: +X right, +Y down, radians internally, positive rotation clockwise, UV (0,0) top-left. |
| D09 | One world unit is not defined as one pixel. Camera scale controls world-to-screen size. |
| D10 | Shader math is linear; color textures and backbuffer use sRGB semantics. |
| D11 | Fixed timestep uses an accumulator with interpolation and explicit MaxFrameDelta / MaxSteps. |
| D12 | Logs are structured and channel-based. Errors are typed. panic is reserved for violated programming invariants. |
| D13 | File-format versions are independent from engine versions. |
| D14 | Dependency direction is strictly bottom-up. Low-level packages never import high-level packages. |
| D15 | Stages close by acceptance criteria, not by a fixed calendar duration. |
3.2 Coordinates, targets and GPU
| ID | Decision |
|---|---|
| D16 | Projection matrices in mathx use GL clip-space convention z ∈ [-1,1]. Backend differences are neutralized through gpu.Capabilities.ClipZeroToOne and a fixup in render/view, never in mathx. |
| D17 | Offscreen targets render in native backend orientation. For GL, window viewport/scissor is flipped at the backend boundary; offscreen targets stay GL-native. render/view is the only place allowed to compensate target Y orientation. Render-target textures are exposed top-down with UV (0,0) top-left. |
| D18 | Texture color is premultiplied once during upload. Runtime shaders do not multiply alpha again. Standard blending is ONE / ONE_MINUS_SRC_ALPHA. |
| D19 | Transparent visual order is painter order: layer → order → z → submission. Batching only merges adjacent compatible draws and never reorders by texture/sampler/blend/clip. |
| D20 | Sprite backend uses an instanced unit quad generated from gl_VertexID. This is an implementation detail of render/sprite, not public API. Sprite z exists only in sorting metadata. |
| D21 | GL 3.3 buffer streaming uses orphaning (BufferData(NULL) + update). Persistent mapping is not part of the baseline. |
| D22 | SamplerDesc is independent from Texture. Renderer caches samplers by descriptor. |
| D23 | TextureRegion is the drawable texture unit: full texture, spritesheet frame, atlas cell or glyph. |
| D24 | Renderer is an ordered list of Pass objects with PassDesc{ID, Before, After}. Pass order is topologically resolved and frozen once rendering starts. Built-ins initially include sprites and debug. |
| D25 | Camera2D.Zoom means pixels per world unit. |
| D26 | Screen/UI coordinates are Y-down logical pixels. PushClip is expressed in logical target pixels and never rotates with the camera. |
| D27 | Stage 2 text is a built-in bitmap font mounted from engine://. Full editor text support arrives later. |
| D28 | Golden rendering tests use exact comparison for nearest/pixel-stable scenes and tolerance comparison otherwise. Linux reference rendering uses Mesa llvmpipe with SDL offscreen video. |
3.3 Scene, schema and identity
| ID | Decision |
|---|---|
| D29 | Scene model is Node + Components. Transform, hierarchy, name, enabled state and visibility layers are built into the node. Everything else is a component. |
| D30 | Components use an independent sparse-set store per type. Aego does not use archetype ECS storage. |
| D31 | A node can contain at most one component of a given type. |
| D32 | A typed *T obtained from ComponentStore[T] is valid only for the current phase/query window. Adds enter a pending store and are visible through direct lookup immediately but not through active iteration until flush. Removes become logically invisible immediately and are physically compacted at flush. |
| D33 | Structural mutations are deferred to phase flush. Destroy is logically immediate and idempotent; physical removal is deferred. |
| D34 | NodeID is 128-bit persistent identity. NodeHandle is runtime-only and never serialized. |
| D35 | Files store full type/field names. Runtime uses hashes. FormerName / FormerFieldName support schema renames. Hash collisions are build errors during Freeze(). |
| D36 | Source formats use canonical aetext and contain no comments. Runtime/export artifacts use binary formats. |
| D37 | Unknown components are preserved as parsed semantic blocks in engine.Unknown and survive load/save even when the defining extension is missing. |
| D38 | Unresolved NodeRef / AssetRef preserve their persistent IDs and produce diagnostics; they do not silently become null. |
| D39 | Nodes store LocalEnabled; EffectiveEnabled is derived during the hierarchy/transform pass. |
| D40 | Node names are unique among siblings, interned, and indexed by (parent, name). |
| D41 | Visibility uses Node.VisibilityLayers and Camera.Mask. |
| D48 | Built-in scene systems have stable IDs. engine.transform performs flush/hierarchy/transform/effective-enabled work. engine.sprite_extract extracts render commands. View construction belongs to Prepare rather than a separate camera phase. |
| D49 | FieldDescriptor may use direct offset fast-path only for POD-like field kinds. Reference/GC-sensitive fields use typed accessors. |
| D50 | Scenes are assets. project.aego references the main scene by AssetID. |
3.4 Assets, VFS and development pipeline
| ID | Decision |
|---|---|
| D42 | Assets are split into runtime asset/loader and dev-only asset/pipeline. Scene depends only on asset.Resolver. |
| D43 | Sidecars are named <filename>.<ext>.meta; moving source + sidecar preserves identity. |
| D44 | Import cache is content-addressed by source bytes, settings, importer version, pipeline version and dependency artifact hashes. |
| D45 | Hot reload is transactional: import → validate → runtime/GPU preparation → atomic swap. Failure preserves the previous working resource. |
| D52 | Initial watcher is stdlib polling over (path, size, mtime) behind a Watcher interface. Known limitation: a same-size rewrite inside filesystem mtime granularity may be missed. |
| D54 | Raw os filesystem access is allowed only in vfs, platform, tools, cmd, project, asset/pipeline, engine/bootstrap and app/run. Runtime-path packages (engine, render, scene, sceneio, asset/loader, ui, sdk) use VFS so the same code can run over .aepak. |
3.5 Native Go extensions and services
| ID | Decision |
|---|---|
| D46 | aego/sdk is the only supported import surface for project-native Go code. tools/depcheck enforces this in examples. |
| D47 | Native project builds use generated bootstrap code under .aego/generated. Build cache keys include engine/SDK/Go versions, target and native source state. |
| D51 | Engine services are accessed through typed service registration keyed by stable string IDs. This allows upper layers to attach Scene, Renderer, Loader and editor services without reverse imports. |
| D55 | aego run supervises the dev executable. Native source changes trigger a rebuild. Compile errors leave the current process alive. Successful rebuilds use a stdin restart protocol; the child serializes restart state and exits, then the new binary launches. |
| D57 | engine.World is a service container. Systems declare scope in {Play, Editor, Frame}. Engine.RunPhase(world, phase, scope) executes a phase for a specific world. |
3.6 Editor
| ID | Decision |
|---|---|
| D56 | Play mode clones the edit scene into a separate play world through scene serialization. Edit scene is immutable during Play. Stop destroys the play world. |
| D58 | Editor UI fonts are pre-baked by tools/genfont; runtime TTF rasterization is not required for the Stage 4 editor. render/text exposes the common Font API (UTF-8 + kerning), BitmapFont.Font() adapts the Stage 2 debug font to that API, and editor font selection uses text.FontSet.Closest(px, scale). |
| D59 | Stage 4 uses one OS window. Docking exists entirely inside it. DockTree is serializable and does not know about native windows. |
| D60 | Editor is a UI over scene/schema/assets/render. Nothing below editor/ imports editor/. Runtime and app/run do not link editor code. |
| D61 | All editor mutations go through history commands: property/transform/create/delete/reparent/rename/enabled/layers/component changes. A drag gesture is one transaction. Delete stores a serialized fragment preserving NodeIDs. |
| D62 | Document dirty state is derived from History state vs saved History state. Scene.Revision exists only for cache invalidation/change tracking. |
| D63 | Selection stores persistent NodeID, never NodeHandle. |
| D64 | Scene editor camera is editor state, not a scene node. |
| D65 | Scene View and Game View render into pooled render targets in 64-pixel buckets. Runner target/view overrides redirect extraction without changing scene ownership. |
| D66 | Reparent preserves world transform by default. Non-invertible parent transform yields SceneBadReparent. Shear created by non-uniform parent scale is not preserved. |
| D67 | Editor-only project Go code is compiled with build tag aego_editor. |
| D68 | Local editor state lives under .aego/user, .aego/session, .aego/recovery, plus .aego/editor.lock; none is committed. |
| D69 | Input routing order: modal → focused widget → hovered-panel shortcuts → global shortcuts → Scene View → Game View. Active widgets/gizmos own pointer capture. |
| D70 | Text entry uses SDL text input/editing events. Text input starts on text-field focus and stops on blur. Enter/blur commit; Escape cancels. |
| D71 | Editor extension callbacks are protected by recover. A panicking extension is disabled and reported instead of taking down the editor. |
| D72 | Editor UI is not required to be allocation-free. Stage 4 idle budget is ≤64 KiB/frame and the target is 60 FPS with a 10k-node Hierarchy. ui/draw.List has independent base/popup/modal/top streams. |
4. Dependency rules
The import graph must remain acyclic. The layer diagram in §2.2 is intentionally conceptual; this section contains the rules that are actually enforced.
4.1 Global rules
core/...is the lowest engine layer and uses only the standard library.platform/inputis a leaf-style input model.platform/...may import it;platform/inputnever imports the platform backend.render/...does not importplatform,engine,ui,sceneoreditor.render/moduleis the single integration package allowed to connect renderer registration withengine.schema/...depends only oncore/.... Asset and node references are recognized through schema-facing interfaces, not by importing high-level packages.scene/...never importseditor/...orasset/pipeline/....scene/...resolves assets only throughasset.Resolver/ runtime loader-facing contracts.sceneio/...owns scene source serialization and may usescene,schema,formatand runtime asset-reference contracts;sceneitself does not importsceneio.asset/loader/...is runtime-safe.asset/pipeline/...andproject/...are development-only and never enter exported runtime dependency graphs.script/...may useschema,sceneand stable runtime services, but nevereditor/....- User project
native/code imports onlyaego/sdk. - Nothing below
editor/...importseditor/.... core/statsremains a leaf utility package; renderer-specific statistics remain inrender.- cgo and direct OS access are restricted to explicitly approved packages.
4.2 Stage 1–2 clarifications
core → platform/input → platform; there is no reverse import fromplatform/inputintoplatform.- Event-to-state conversion is performed by
platform.ApplyInput. renderand its normal subpackages do not importplatform,engineorui.render/moduleis the only exception for engine integration.ui/...may importrender/...andplatform/input, but not the fullplatformbackend and notengine.engine/resis mounted asengine://duringBuild().core/statsdoes not absorb renderer counters;render.Statsremains owned byrender.
4.3 Stage 3 clarifications
schema/...depends only oncore/....- Asset and node references are recognized through
schema.AssetRefandschema.NodeRef-style contracts implemented byassetandscene. scene/...knows assets only throughasset.Resolver; it never importsasset/pipeline.asset/loader/...never importsasset/pipeline/....project/...owns source-project metadata (project.aego), project roots and native-build inputs; it is dev-only.examples/**represents user-facing SDK projects and may import onlyaego/sdkfrom the engine.renderremains scene-agnostic; scene-to-render extraction lives inscene/systems.
4.4 Stage 4 clarifications
ui/...may importformat/...for serialization/deserialization of docking and UI layout state.editor/...may import the runtime/dev package families it composes:core,platform/input,gpu,render,ui,schema,scene,asset/...,format,projectandengine. No package importseditor/...from below.app/run,runtime/..., andexamples/**when built without theaego_editortag are forbidden from importingeditor/...;tools/depchecktreats this as an error.render/textexposesFontas the common text-facing abstraction with UTF-8 and kerning support.BitmapFont.Font()converts/adapts the Stage 2 built-in bitmap debug font to the sameFontAPI.- Editor-only project Go code is isolated by the
aego_editorbuild tag.
4.5 Enforcement
tools/depcheck enforces at least:
- forbidden package-family edges;
- runtime → editor/dev-only imports;
app/run,runtime/..., and non-editorexamples/**importingeditor/...;examples/**bypassingaego/sdk;os.*outsidevfs,platform,tools,cmd,project,asset/pipeline,engine/bootstrap, andapp/run;import "C"outside approved native bridges such asplatform/sdlandgpu/gl33/gl;- obvious mutable global engine/service singletons.
5. Repository layout
The number in parentheses is the stage where the package first becomes real. Empty placeholder packages are not created.
aego/
├── go.mod
├── ARCHITECTURE.md
├── ROADMAP.md
├── Taskfile.yml
├── .github/workflows/ci.yml
│
├── core/ # stdlib-only
│ ├── handle/ (1)
│ ├── mathx/ (1)
│ ├── clock/ (1)
│ ├── log/ (1)
│ ├── errs/ (1)
│ ├── stats/ (1)
│ ├── bits/ (2)
│ ├── ids/ (3)
│ └── rng/ (3)
│
├── engine/ (1)
│ ├── res/ (2) embedded engine:// resources
│ ├── bootstrap/ (3) dev/native bootstrap integration
│ └── ...
│
├── platform/ (1)
│ ├── input/
│ ├── sdl/
│ ├── headless/
│ └── android/ (14)
│
├── gpu/ (1)
│ ├── gl33/
│ │ ├── gl/ generated, DO NOT EDIT
│ │ └── internal/glstate/
│ ├── null/
│ ├── gles30/ (14)
│ └── shaderc/ (12)
│
├── vfs/ (1)
│ ├── osfs/
│ ├── pakfs/ (13)
│ └── assetfs/ (14)
│
├── render/ (2)
│ ├── view/
│ ├── texture/
│ ├── sprite/
│ ├── culling/
│ ├── debug/
│ ├── text/ (2 BitmapFont, 4 Font UTF-8+kerning / FontSet)
│ ├── gpuprof/
│ ├── module/
│ ├── material/ (12)
│ ├── postfx/ (12)
│ └── light/ (12)
│
├── ui/ (2 core, 4 editor-complete)
│ ├── draw/
│ ├── core/
│ ├── widgets/
│ ├── overlay/
│ ├── layout/ (3)
│ ├── textedit/ (4)
│ ├── dock/ (4)
│ └── theme/ (4)
│
├── schema/ (3)
│ └── reflectgo/
│
├── format/ (3)
│ ├── aetext/ source parser/writer + canonicalizer
│ ├── binary/ import/export artifact primitives
│ └── pak/ (13) .aepak index/container format
│
├── project/ (3) project.aego, source project model, native build inputs
│
├── asset/ (3)
│ ├── loader/ runtime AssetID → artifact/resource
│ ├── pipeline/ dev-only scan/meta/import/watch/cache
│ ├── importer/
│ │ ├── png/
│ │ ├── atlas/
│ │ ├── font/ (4)
│ │ ├── audio/ (9)
│ │ └── shader/ (12)
│ └── pak/ (13)
│
├── scene/ (3)
│ ├── components/
│ ├── systems/
│ └── prefab/
│
├── sceneio/ (3)
│
├── script/ (5+)
│ ├── token/
│ ├── ast/
│ ├── parser/
│ ├── types/
│ ├── checker/
│ ├── ir/
│ ├── compiler/
│ ├── bytecode/
│ ├── vm/
│ ├── runtime/
│ ├── bind/
│ ├── stdlib/
│ ├── reload/
│ ├── lang/
│ └── docgen/ (7)
│
├── physics/ (8)
├── audio/ (9)
├── anim/ (11)
├── net/ (13)
├── store/ (13)
│
├── sdk/ (1 boundary, populated from 3)
│
├── editor/ (4)
│ ├── app/
│ ├── project/
│ ├── document/
│ ├── command/
│ ├── history/
│ ├── selection/
│ ├── playmode/
│ ├── gizmo/
│ ├── ext/
│ ├── build/
│ └── panels/
│ ├── hierarchy/
│ ├── inspector/
│ ├── sceneview/
│ ├── gameview/
│ ├── assets/
│ ├── console/
│ ├── profiler/
│ ├── scripteditor/ (6)
│ ├── physicsdebug/ (8)
│ ├── audiomixer/ (9)
│ ├── pixel/ (10)
│ ├── spritesheet/ (10)
│ ├── animation/ (11)
│ ├── shader/ (12)
│ ├── settings/
│ ├── export/ (13)
│ └── help/ (7)
│
├── runtime/ (13)
│ └── app/
│
├── app/
│ └── run/ (3) dev runner/supervisor; never imports editor
│
├── cmd/
│ ├── sandbox/ (1)
│ ├── aego/ (3)
│ ├── aego-editor/ (4)
│ └── aego-runtime/ (13)
│
├── tools/
│ ├── bootstrap-native.sh (1)
│ ├── gen-gl.sh (1)
│ ├── depcheck/ (1)
│ ├── genfont/ (2, nested module)
│ ├── golden/ (2)
│ ├── bench/ (2)
│ └── docsite/ (7)
│
├── templates/ (13)
│ ├── project/
│ ├── desktop/
│ └── android/ (14)
│
├── examples/ (3) user-facing projects; engine imports only via aego/sdk
│
├── demos/
│ ├── basic input resize headless (1)
│ ├── sprites camera layers text
│ │ offscreen stress (2)
│ ├── scene hotreload native (3)
│ ├── editor (4)
│ ├── platformer (5+)
│ └── physics audio anim shaders net later stages
│
├── tests/
│ ├── integration/
│ ├── gl/
│ ├── golden/
│ └── bench/
│
├── third_party/
│ └── sdl/version.txt
│
├── docs/ (7)
└── .cache/ ignored
5.1 Important correction: source vs binary format
Source scene/material/animation/prefab/project files are not chunked binary containers.
project.aego
*.aescene
*.aeprefab
*.aemat
*.aeanim
use canonical aetext.
Chunk tables such as string/reference/node/property tables belong only to runtime/export/import artifacts, primarily .aepak and binary cache formats.
6. User project layout
Created by:
aego new MyGame
MyGame/
├── project.aego
├── res/ mounted as res://
│ ├── scenes/
│ ├── scripts/
│ ├── sprites/
│ ├── audio/
│ └── shaders/
│
├── native/ optional Go extensions
│ ├── module.go imports only aego/sdk
│ └── editor_*.go optional; `//go:build aego_editor`
│
├── .aego/ ignored
│ ├── cache/
│ ├── generated/
│ ├── build/
│ ├── user/
│ ├── session/
│ └── recovery/
│
└── exports/
The engine treats res:// as project content, not as a direct OS path.
7. File formats
| Extension | Purpose | Representation |
|---|---|---|
project.aego |
Project metadata and main scene reference | canonical aetext |
.aescene |
Scene source | canonical aetext |
.aeprefab |
Prefab source | canonical aetext |
.aemat |
Material source | canonical aetext |
.aeanim |
Animation source | canonical aetext |
.aes |
Aego gameplay script | text |
.aeshader |
User shader source | text |
.meta |
Source asset sidecar | canonical aetext |
.aepak |
Export package | binary |
Every format owns its own version.
Example:
engine_version = 0.8.0
native_sdk = 3
scene_format = 4
material_format = 2
dsl_version = 5
pak_format = 2
These values are intentionally independent.
8. Handles and lifetime
8.1 Handle rules
type Handle[T any] struct {
Index uint32
Gen uint32
}
- Generation starts at
1. - Zero handle is always invalid.
- Reusing an index increments generation.
- Persistent data never stores a runtime handle.
- A pointer returned by an arena/store is a temporary fast-path view, not ownership.
8.2 Arena pointer lifetime
A pointer returned by Arena.Get is valid only until an operation that may grow/move the arena.
Callers that need stable identity keep the handle and re-resolve it.
8.3 GPU lifetime
- GPU destruction during a frame is deferred until a safe point at
EndFrame. - Backend state caches key resources by full
{Index, Gen}, never raw index. - Recreated resources with reused indices must invalidate cached backend bindings.
9. Scene model
9.1 Node contents
Every node owns:
NodeID persistent 128-bit identity
Name interned StringID
LocalEnabled
EffectiveEnabled derived
VisibilityLayers
Transform2D local
WorldTransform derived
Parent
FirstChild
NextSibling
PrevSibling
Transform2D is built into the node and cannot be removed.
9.2 Node templates are editor conveniences
Names such as:
Sprite2D
Camera2D
AudioSource2D
are editor creation templates, not serialized node types.
For example:
Sprite2D template
→ create Node
→ attach SpriteRenderer
The scene file stores only the node and its components.
9.3 Components
A component type has an independent sparse-set:
denseNodes[]
denseData[]
sparse[]
The model intentionally avoids archetype/chunk ECS.
Rules:
- one component of each type per node;
- direct typed iteration for systems;
- schema metadata exists independently from hot-path storage;
- structural changes are deferred.
9.4 Structural mutation semantics
Destroy(h):
- marks the node logically dead immediately;
IsAlive(h)becomes false immediately;- queries skip it immediately;
- physical component/tree removal happens during the structural flush;
- repeated
Destroy(h)is harmless.
Adds/removes follow D32 semantics so a system cannot accidentally iterate newly-added instances and loop over its own structural writes.
9.5 Hierarchy traversal
The scene stores a derived pre-order index list.
Structural flush rebuilds it only when hierarchy structure changes.
Transform/effective-enabled evaluation is a single linear pass where a parent always appears before its children.
No recursive transform traversal is required per frame.
10. Schema and reflection
schema is the single metadata source for:
- serialization;
- editor Inspector;
- undo/history;
- DSL bindings;
- native extension discovery;
- property validation;
- references;
- future replication metadata.
Conceptually:
type TypeDescriptor struct {
ID TypeID
FullName string
Version uint32
Fields []FieldDescriptor
FormerNames []string
}
type FieldDescriptor struct {
ID FieldID
Name string
Kind FieldKind
Flags FieldFlags
Offset uintptr
Size uintptr
FastPath bool
FormerNames []string
}
Reflection may be used while registering Go types.
After Registry.Freeze():
- full-name uniqueness is validated;
- type/field hash collisions are rejected;
- lookup tables are fixed;
- hot paths use IDs and precomputed descriptors rather than repeated
reflectwork.
POD-like fields may use offset-based fast access.
GC-sensitive/reference fields use typed accessors so the runtime does not bypass Go write barriers.
11. References and identity
11.1 Node reference
A serialized NodeRef stores NodeID.
Runtime may cache a NodeHandle, but that cache is derived.
Missing target:
ID remains present
runtime handle is invalid
diagnostic is emitted
Saving the scene does not erase the unresolved reference.
11.2 Asset reference
AssetRef[T] follows the same model:
persistent AssetID
+
derived runtime resource handle
Source file paths never appear in scene identity.
12. Asset architecture
12.1 Runtime loader
asset/loader exists in editor, dev run and exported games.
Responsibilities:
AssetID
↓
artifact index
↓
runtime resource
↓
residency / hot swap
It knows nothing about:
- source
.png; .meta;- filesystem watcher;
- import settings editor;
- source folder scans.
12.2 Development pipeline
asset/pipeline exists only in editor/dev tools.
Responsibilities:
- source scan;
- sidecar management;
- source → AssetID mapping;
- importers;
- dependency graph;
- content-addressed cache;
- watcher;
- batched reimport;
- generation of runtime artifacts.
12.3 Sidecars
player.png
player.png.meta
Moving both files preserves AssetID.
12.4 Import cache key
Conceptually:
hash(
source bytes,
normalized import settings,
importer version,
pipeline version,
dependency artifact hashes,
)
12.5 Transactional reload
source changed
↓
debounced dirty set
↓
dependency closure
↓
import temporary artifact
↓
validate
↓
prepare runtime/GPU resource
↓
success?
├─ yes → atomic swap
└─ no → old resource stays active
Watcher events are coalesced before import, so editor saves and large operations such as git checkout do not trigger pathological reimport storms.
13. VFS
Engine-facing code uses URI-like schemes:
res://
engine://
user://
cache://
Rules:
res://andengine://support overlay mounts; latest mount wins reads.user://andcache://have exactly one writable mount.- runtime code never assumes
res://is an OS directory. .aepaklater mounts into the same VFS surface.
Allowed raw OS filesystem users are limited to vfs, platform, tools, cmd, project, asset/pipeline, engine/bootstrap, and app/run.
14. Renderer architecture
14.1 Separation
scene/systems
↓ extracts
ViewFrame / SpriteDraw
↓
render
↓
gpu
↓
backend
Renderer never receives Scene or Node types.
14.2 Sprite order
Visual order:
layer
↓
order
↓
z
↓
submission
Batching is a post-order optimization only.
A texture/material/sampler change may split a batch but must never change visual order.
14.3 Texture semantics
CPU image decoding may begin as straight alpha.
Uploader converts to premultiplied representation.
Runtime blending expects premultiplied color.
14.4 Render targets
Window and offscreen targets share a common render target abstraction.
Backend orientation differences are contained by render/view and backend conversion rules.
ReadPixels always returns:
RGBA8
top row first
no row padding
regardless of backend.
14.5 UI/editor rendering
Scene View and Game View render into pooled render targets.
Targets are bucketed to 64-pixel boundaries to avoid constant reallocation while splitters are dragged.
15. GPU contracts
15.1 Uniforms and samplers
Engine convention:
UBO blocks: ub0 .. ub15
samplers: tex0 .. tex14
texture unit 15 reserved for engine upload/internal work
Stage 12 shader tooling emits these conventions automatically.
15.2 GPU input memory
Data passed through cgo-backed GPU functions must live in pointer-free byte allocations when needed.
Do not pass a subfield whose containing Go object includes pointers and assume cgo checks only the field.
15.3 Queries
Only one active GPU query is allowed per device in the baseline implementation.
Nested query scopes are forbidden.
EndQuery must occur in the same render pass in which the query began.
16. Engine construction and modules
Construction:
Builder
↓ register modules / systems / services
Build()
↓ initialize platform, VFS, workers, GPU
↓ freeze registries
ModuleIniter.Init(*Engine)
↓
Engine
Modules register systems during Builder.Use.
Runtime resource creation happens during module initialization after low-level backends are available.
16.1 Services
Services are stable-ID entries inside a world/service registry.
They allow high-level packages to attach:
- renderer;
- asset resolver/loader;
- scene;
- editor context;
- other world-local services;
without creating reverse package imports.
17. Worlds and execution scopes
A World is a set of world-local services and state.
System scopes:
Play
Editor
Frame
Examples:
- transform system: Editor + Play;
- sprite extraction: Editor + Play;
- user gameplay AI: Play;
- editor maintenance system: Editor;
- platform/frame statistics: Frame.
The engine can run the same phase graph for different worlds without conflating edit state and play state.
18. Frame lifecycle
High-level order:
FrameBegin
drain OnMain
stats.begin
PollEvents
platform.PollEvents
input.Apply
resize / DPI metrics
app event dispatch
Input
FixedBegin
× zero or more fixed steps
PrePhysics
Physics
app.fixed
PostPhysics
FixedEnd
PreUpdate
app.update
PostUpdate
Animation
RenderPrepare
structural flush as required
transforms
effective enabled
views
sprite extraction
Render
gpu.BeginFrame
renderer begin
app.render
render systems/passes
RenderUI
UI / editor overlays
renderer end
gpu.EndFrame
window swap
FrameEnd
stats.end
Reserved system IDs include:
app.fixed
app.update
app.render
engine.transform
engine.sprite_extract
Extension systems order themselves with stable Before / After IDs.
19. Input model
Input has separate concepts for:
- physical scan code;
- layout-dependent key code;
- Unicode text input;
- pointer state;
- recorded
InputFrame.
Focus loss clears held button/key state.
Gameplay/editor code never synthesizes text by translating KeyDown.
Text fields use platform text/IME events.
Recorded InputFrame exists to support replay, tests and future networking/debugging.
20. Editor architecture
20.1 Editor is a client of the engine
Editor does not own a parallel scene/render/asset model.
It uses:
scene
schema
asset/pipeline
asset/loader
render
ui
history
sdk extension registry
20.2 Documents
Open scene files are documents.
A scene document owns:
- loaded edit scene;
- history state;
- persistent selection;
- Scene View state;
- file path/AssetID;
- dirty state.
20.3 Play mode
Edit Scene
↓ serialize/clone through sceneio
Play Scene / Play World
↓ mutable gameplay
Stop
↓ destroy Play World
The edit scene is not rolled back because it is never mutated by gameplay.
20.4 Undo/redo
Editor changes use commands rather than direct mutation.
Core command families:
SetProperty
SetTransform
CreateNode
DeleteNode
ReparentNode
RenameNode
SetEnabled
SetLayers
AddComponent
RemoveComponent
AssignAsset
Continuous gestures begin one history transaction and commit once.
Delete stores a semantic serialized fragment with original NodeIDs so undo restores references.
20.5 Selection
Selection stores persistent IDs.
It survives scene reload/native rebuild whenever the corresponding object still exists.
20.6 Docking
Stage 4 provides docking inside one OS window.
Dock tree is editor state and can later be extended toward multi-window support without changing panel identity.
20.7 Editor extensions
Project-native Go may add:
- panels;
- commands;
- inspectors;
- gizmos;
- creation templates;
- editor tools.
Editor-only code is compiled under aego_editor.
Extension callbacks are panic-isolated.
21. Native Go project extensions
A project may contain:
native/
└── module.go
and import:
import "aego/sdk"
only.
Native extensions may provide:
- components;
- systems;
- asset importers;
- native DSL bindings;
- renderer features through stable extension points;
- editor tools under
aego_editor.
They are statically compiled into the project build.
Aego does not rely on Go dynamic plugins.
21.1 Dev rebuild
aego run acts as supervisor:
watch native/*.go
↓
go build
↓
failure?
├─ yes → keep old process alive
└─ no → ask child to serialize restart state
↓
child exits
↓
start new binary
Stage 4 editor reuses the same idea but persists richer editor/session state.
22. UI architecture
UI is immediate-mode with retained internal state keyed by stable UI IDs.
State includes:
- focus;
- text-edit buffer;
- scroll;
- active item;
- pointer capture;
- dock tree;
- popup/modal state.
Input routing priority:
modal
↓
focused widget
↓
hovered panel shortcuts
↓
global shortcuts
↓
Scene View
↓
Game View
Draw list streams:
base
popup
modal
top
Each stream maintains its own clip balance.
23. Fonts and text
Stage 2 uses a built-in bitmap debug font.
render/text exposes a common Font abstraction. From Stage 4 that contract guarantees the text layer expected by the editor:
- UTF-8 decoding;
- glyph lookup;
- kerning;
- metrics needed for layout and cursor placement.
The Stage 2 BitmapFont remains usable through:
font := bitmapFont.Font()
BitmapFont.Font() adapts the built-in debug font to the common Font API rather than creating a parallel text path.
Stage 4 editor fonts are pre-generated by tools/genfont and collected into text.FontSet; DPI-aware selection uses:
font := fontSet.Closest(px, displayScale)
The editor requires:
- UTF-8;
- Cyrillic coverage;
- kerning;
- DPI-aware font selection;
- stable golden-test fonts.
Runtime TTF rasterization is intentionally not required by the Stage 4 architecture.
More advanced shaping can be introduced later without changing the Font abstraction.
24. Logging and errors
24.1 Logging
Structured example:
channel = asset
level = error
message = "texture import failed"
asset = 0x...
path = res://sprites/player.png
cause = ...
Typical channels:
engine
platform
gpu
render
asset
scene
script
physics
audio
network
editor
game
Logging/formatting in hot frame paths is prohibited.
24.2 Errors
Errors are typed through errs.Error.
Fatal termination is reserved for conditions where the application cannot continue, such as failing to initialize the required platform/graphics backend.
Shader compile, asset import, editor extension and scene load errors are recoverable diagnostics wherever possible.
25. Allocation and performance policy
Runtime hot paths
Targets:
- zero steady-state engine allocations/frame where practical;
- no frame-path logging;
- no accidental slice growth in steady-state renderer/scene loops;
- explicit capacity overflow statistics rather than panics.
Bulk sprite submission uses slice APIs.
Single-item convenience APIs may copy values.
Editor
Editor is allowed to allocate.
Stage 4 targets:
- ≤64 KiB/frame while idle;
- no unbounded per-frame garbage growth;
- 60 FPS with a 10k-node Hierarchy on the project baseline machine.
26. Build modes
Normal native build
Includes SDL and graphics backend.
aego_nonative
Build tag excludes SDL/OpenGL-native packages and is used for pure-Go CI/tests/tools.
This is distinct from runtime:
Config.Headless = true
A normal native binary may still run headless.
Hidden GPU window
WindowDesc.Hidden creates a GPU-capable hidden window/context for golden and benchmark runs.
27. SDL/native dependency policy
SDL version is pinned in:
third_party/sdl/version.txt
Native bootstrap prepares per-target artifacts into cache/build locations.
SDL is not built implicitly as an opaque side-effect of ordinary package compilation.
Generated OpenGL bindings live under:
gpu/gl33/gl
and are excluded from manual editing and selected static-analysis checks.
28. Testing strategy
28.1 Unit tests
Cover:
- handles/generation;
- math;
- fixed stepper;
- sparse sets;
- hierarchy ordering;
- schema registration/freeze;
- canonical serialization;
- VFS;
- asset dependency graph;
- editor history;
- UI layout/input state.
28.2 Contract tests
Real-GL tests verify the public GPU contract against the GL backend.
Headless/null-backend tests verify engine behavior without platform graphics.
28.3 Golden rendering
tests/golden/
Examples:
- alpha blending;
- sprite rotation;
- nearest sampling;
- clipping;
- offscreen target orientation;
- sRGB behavior;
- camera transforms;
- editor gizmos/UI.
28.4 Scene/format tests
Required:
load → save → canonical same representation
load → save → load semantic equivalence
format 0 → format 1 migration
unknown component survives round-trip
moved asset with same AssetID does not break scene
missing NodeRef keeps its NodeID
28.5 Performance tests
Stage 2 baseline:
50k world sprites
~10k visible
one atlas/material
0 steady-state renderer allocations
minimal draw calls
Stage 3 baseline adds:
50k nodes
sparse component stores
transform pass
sprite extraction
The scene layer must not materially destroy Stage 2 renderer behavior.
28.6 Dependency tests
tools/depcheck runs in CI.
29. CI expectations
The CI pipeline should eventually cover:
go test ./...
go vet ./...
staticcheck
depcheck
aego_nonative build
native desktop builds
Linux GL contract tests
golden tests via llvmpipe
bench regression check
Generated bindings may have explicit static-analysis exclusions.
30. Naming conventions
Packages
Prefer capability/domain names over implementation-role names.
Good:
render/view
asset/loader
asset/pipeline
sceneio
platform/input
Avoid generic dumping grounds:
utils
helpers
common
manager
misc
Stable IDs
Engine-owned systems/services/passes use namespace-qualified IDs:
engine.transform
engine.sprite_extract
render.sprites
render.debug
editor.save
editor.undo
Project extensions should use project/plugin namespaces.
31. Architecture invariants checklist
A code review changing engine architecture should be able to answer yes to all relevant items:
- Does runtime remain independent from editor/dev-only packages?
- Is persistent identity separated from runtime handles?
- Can the same code path work when
res://comes from.aepak? - Is backend-specific behavior confined to backend/view boundaries?
- Does render remain independent from scene?
- Are structural scene writes safe during iteration?
- Are unknown extension components preserved?
- Are native extensions using only
aego/sdk? - Does a failed hot reload keep the previous working state?
- Is the file/source representation canonical and versioned?
- Does an edit participate in history rather than mutate editor state invisibly?
- Can the feature run in headless/tests where appropriate?
- Is the public API not accidentally exposing backend/runtime-storage details?
32. Stage ownership summary
| Stage | Architectural ownership |
|---|---|
| 1 | Core, platform, SDL bridge, input, VFS, Builder/Engine, phases, handles, headless, GPU/RHI, logging/errors, worker/main-thread boundary |
| 2 | 2D renderer, camera/views, sprite batching, culling, targets, debug draw, GPU profiling, UI foundation, golden rendering |
| 3 | Schema, Node+Components scene model, sparse sets, project, asset loader/pipeline, aetext source formats, scene IO, native Go extensions, app/run, aego run/build |
| 4 | Editor v1, docking, hierarchy, inspector, Scene/Game views, gizmos, undo/redo, asset browser, play world, editor extensions |
| 5–7 | DSL core, editor integration/hot reload, docs/playground |
| 8–12 | Physics, audio, pixel/sprite tools, animation, materials/shaders/lighting |
| 13+ | Runtime packaging/export, networking/store, Android, 2.5D path |
33. Definition of architectural stability
The architecture is considered ready for feature growth after Stage 4 when all of the following are true:
- A project can be created, run and edited through Aego tooling.
- A scene can be built from nodes/components and saved canonically.
- Assets survive moves through persistent IDs.
- Native Go can add components/systems without changing engine source.
- The editor automatically exposes registered schema.
- Edit mode and Play mode are isolated worlds.
- Renderer accepts extracted render data rather than scene objects.
- Headless tests use the same core engine model.
- Runtime packages remain independent from editor/import pipeline code.
- Dependency and OS/cgo boundaries are machine-checked.
- Hot reload failure is non-destructive.
- Golden, serialization and performance regression tests are part of CI.
At that point later systems—DSL, physics, audio, animation, materials, networking and 2.5D—plug into established boundaries rather than redefining them.
34. Binary names
Development/tool binaries:
aego
sandbox
aego-editor
aego-runtime
aego is the user-facing CLI.
Expected command family:
aego new
aego edit
aego run
aego build
aego export
aego bootstrap
aego dump
aego test
aego bench
aego docs
Commands become functional at the stage that owns their implementation.
35. Final rule
When choosing between a locally convenient shortcut and preserving a boundary that later stages depend on, preserve the boundary.
Aego is deliberately structured so that:
platform/GPU can change
without rewriting render,
render can change
without rewriting scene,
scene can change
without rewriting editor data ownership,
editor can evolve
without entering runtime,
and user projects can extend the engine
without forking the engine core.
That separation is the primary architectural constraint of the project.