Files
aego/ARCHITECTURE.md
2026-09-26 16:32:40 +03:00

1790 lines
52 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Aego — Architecture
> **Status:** active architecture specification
> **Language:** Go 1.27+
> **Repository model:** monorepo, single root Go module `aego`
> **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:
1. **A real native engine, not a wrapper around another game engine.**
2. **A built-in editor** with a Unity/Godot-like workflow.
3. **A custom gameplay language** with hot reload and editor integration.
4. **Native Go extensions** for users who need to extend the engine itself.
5. **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](#4-dependency-rules) and enforced by `tools/depcheck`.
```mermaid
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/input` is a leaf-style input model. `platform/...` may import it; `platform/input` never imports the platform backend.
- `render/...` does **not** import `platform`, `engine`, `ui`, `scene` or `editor`.
- `render/module` is the single integration package allowed to connect renderer registration with `engine`.
- `schema/...` depends only on `core/...`. Asset and node references are recognized through schema-facing interfaces, not by importing high-level packages.
- `scene/...` never imports `editor/...` or `asset/pipeline/...`.
- `scene/...` resolves assets only through `asset.Resolver` / runtime loader-facing contracts.
- `sceneio/...` owns scene source serialization and may use `scene`, `schema`, `format` and runtime asset-reference contracts; `scene` itself does not import `sceneio`.
- `asset/loader/...` is runtime-safe.
- `asset/pipeline/...` and `project/...` are development-only and never enter exported runtime dependency graphs.
- `script/...` may use `schema`, `scene` and stable runtime services, but never `editor/...`.
- User project `native/` code imports only `aego/sdk`.
- Nothing below `editor/...` imports `editor/...`.
- `core/stats` remains a leaf utility package; renderer-specific statistics remain in `render`.
- 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 from `platform/input` into `platform`.
- Event-to-state conversion is performed by `platform.ApplyInput`.
- `render` and its normal subpackages do not import `platform`, `engine` or `ui`.
- `render/module` is the only exception for engine integration.
- `ui/...` may import `render/...` and `platform/input`, but not the full `platform` backend and not `engine`.
- `engine/res` is mounted as `engine://` during `Build()`.
- `core/stats` does not absorb renderer counters; `render.Stats` remains owned by `render`.
### 4.3 Stage 3 clarifications
- `schema/...` depends only on `core/...`.
- Asset and node references are recognized through `schema.AssetRef` and `schema.NodeRef`-style contracts implemented by `asset` and `scene`.
- `scene/...` knows assets only through `asset.Resolver`; it never imports `asset/pipeline`.
- `asset/loader/...` never imports `asset/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 only `aego/sdk` from the engine.
- `render` remains scene-agnostic; scene-to-render extraction lives in `scene/systems`.
### 4.4 Stage 4 clarifications
- `ui/...` may import `format/...` 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`, `project` and `engine`. **No package imports `editor/...` from below.**
- `app/run`, `runtime/...`, and `examples/**` when built **without** the `aego_editor` tag are forbidden from importing `editor/...`; `tools/depcheck` treats this as an error.
- `render/text` exposes `Font` as 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 same `Font` API.
- Editor-only project Go code is isolated by the `aego_editor` build tag.
### 4.5 Enforcement
`tools/depcheck` enforces at least:
- forbidden package-family edges;
- runtime → editor/dev-only imports;
- `app/run`, `runtime/...`, and non-editor `examples/**` importing `editor/...`;
- `examples/**` bypassing `aego/sdk`;
- `os.*` outside `vfs`, `platform`, `tools`, `cmd`, `project`, `asset/pipeline`, `engine/bootstrap`, and `app/run`;
- `import "C"` outside approved native bridges such as `platform/sdl` and `gpu/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.
```text
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.
```text
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:
```bash
aego new MyGame
```
```text
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:
```text
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
```go
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:
```text
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:
```text
Sprite2D
Camera2D
AudioSource2D
```
are **editor creation templates**, not serialized node types.
For example:
```text
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:
```text
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)`:
1. marks the node logically dead immediately;
2. `IsAlive(h)` becomes false immediately;
3. queries skip it immediately;
4. physical component/tree removal happens during the structural flush;
5. 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:
```go
type TypeDescriptor struct {
ID TypeID
FullName string
Version uint32
Fields []FieldDescriptor
FormerNames []string
}
```
```go
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 `reflect` work.
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:
```text
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:
```text
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:
```text
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
```text
player.png
player.png.meta
```
Moving both files preserves `AssetID`.
### 12.4 Import cache key
Conceptually:
```text
hash(
source bytes,
normalized import settings,
importer version,
pipeline version,
dependency artifact hashes,
)
```
### 12.5 Transactional reload
```text
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:
```text
res://
engine://
user://
cache://
```
Rules:
- `res://` and `engine://` support overlay mounts; latest mount wins reads.
- `user://` and `cache://` have exactly one writable mount.
- runtime code never assumes `res://` is an OS directory.
- `.aepak` later 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
```text
scene/systems
↓ extracts
ViewFrame / SpriteDraw
↓
render
↓
gpu
↓
backend
```
Renderer never receives Scene or Node types.
### 14.2 Sprite order
Visual order:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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:
```text
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
```text
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:
```text
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 `NodeID`s 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:
```text
native/
└── module.go
```
and import:
```go
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:
```text
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:
```text
modal
↓
focused widget
↓
hovered panel shortcuts
↓
global shortcuts
↓
Scene View
↓
Game View
```
Draw list streams:
```text
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:
```go
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:
```go
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:
```text
channel = asset
level = error
message = "texture import failed"
asset = 0x...
path = res://sprites/player.png
cause = ...
```
Typical channels:
```text
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:
```go
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:
```text
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:
```text
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
```text
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:
```text
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:
```text
50k world sprites
~10k visible
one atlas/material
0 steady-state renderer allocations
minimal draw calls
```
Stage 3 baseline adds:
```text
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:
```text
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:
```text
render/view
asset/loader
asset/pipeline
sceneio
platform/input
```
Avoid generic dumping grounds:
```text
utils
helpers
common
manager
misc
```
### Stable IDs
Engine-owned systems/services/passes use namespace-qualified IDs:
```text
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:
1. A project can be created, run and edited through Aego tooling.
2. A scene can be built from nodes/components and saved canonically.
3. Assets survive moves through persistent IDs.
4. Native Go can add components/systems without changing engine source.
5. The editor automatically exposes registered schema.
6. Edit mode and Play mode are isolated worlds.
7. Renderer accepts extracted render data rather than scene objects.
8. Headless tests use the same core engine model.
9. Runtime packages remain independent from editor/import pipeline code.
10. Dependency and OS/cgo boundaries are machine-checked.
11. Hot reload failure is non-destructive.
12. 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:
```text
aego
sandbox
aego-editor
aego-runtime
```
`aego` is the user-facing CLI.
Expected command family:
```text
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:
```text
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.