Aebiten — a 2D game engine written in
Aether, rendering through
aether-ui's canvas
(GTK4 / AppKit / Win32). Games are .ae programs: a fixed-60-TPS
on_update, a per-frame on_draw against a software-composited
framebuffer, sprite images with affine transforms, color matrices, blend
modes, paths, bitmap text, and audio. This branch is Aether end to end —
there is no Go on it, and no GPU dependency.
The engine's design and API lineage come from
Ebitengine ("Ebiten"), Hajime Hoshi's Go engine.
License: Apache 2.0, portions copyright Hajime Hoshi and The Ebiten
Authors — see NOTICE.md. Architecture, status table, and
what is deliberately not carried over:
AE-MIGRATION.md.
The complete, unmodified Go implementation lives on
legacy_golang,
which tracks upstream
hajimehoshi/ebiten. It is the
porting oracle: behaviour questions are settled by reading (or running)
the Go, and anything not yet in the status table is found there.
The two branches are not mirror images, and the gaps are deliberate:
- API shape. This branch is idiomatic Aether, not a transliteration.
The concepts and names carry over (GeoM, DrawImage, ColorM, DebugPrint,
just-pressed input, fixed-TPS update), but where Go has interfaces and
methods, Aether has handles plus the trailing-block builder DSL
(
aebiten.game { },core.draw { }). Code does not port line-for-line between the branches; behaviour does. - Feature coverage. Some upstream subsystems are deliberately absent
here — Kage shaders, gamepad/touch/mobile/JS targets, TTF text so far —
and some things exist only here (the software compositor itself); TTF
text renders through aether-ui's
vg.fontrather than go-text. The authoritative list is the status table inAE-MIGRATION.md. - Time.
legacy_golangfollows upstream, which keeps moving;mainforked at a fixed upstream commit and adopts later upstream changes case by case, not automatically (upstream'sScreenSizeaddition, for example, postdates the fork and is not yet ported). A behaviour difference against the tip of the Go branch may simply be an upstream change the port hasn't chosen to chase yet.
aebiten/core.ae— pure drawing core: GeoM, ColorScale, ColorM, blend table, Image + DrawImage/DrawTriangles, and the per-pixel loops. Headless, no ui dependency — the whole engine is Aether.aebiten/png.ae— PNG decoder (std.zlib inflate).aebiten/util.ae— ebitenutil: DebugPrint bitmap font (embedded), scaled text.aebiten/vector.ae— vector: primitives + scanline path fill.aebiten/audio.ae— audio players over std.audio.aebiten/text.ae— TrueType text into images (aether-ui's pure-Aethervg.fontparser + the scanline rasterizer;lib/vgsymlinks the sibling checkout so headlessae runresolves it).aebiten/module.ae— the engine (import aebiten): game loop, input, window/canvas present.examples/<name>/— ported examples, one aeb node each (examples/resources/keeps the original assets).tests/— headless unit tests.
aeb all.ae # build everything
aeb examples/snake # or one example
./target/build/examples/snake/bin/snake
# tests (headless, from the repo root):
ae run tests/test_core.ae
./ci.sh # the whole gate: tests + builds + driver smokeNeeds a sibling ../aether-ui checkout (or AETHER_UI_ROOT) at or after
commit ad960a8 (canvas_on_key_release + canvas_draw_image_scaled_ptr),
and an aether toolchain with the inlined std.mem accessors (the post-0.578
build; aether#1733). The repo's aether.toml pins -O2 — the pixel loops
need the optimizer to exploit the inlined accessors.
The surface is Aether's trailing-block builder DSL ("config IS code"):
the game block registers the loop, the draw block builds a DrawImage's
options — no handles threaded, no option objects created or freed by hand.
import aebiten
import aebiten.core
main() {
aebiten.game("Title", 320, 240) { // logical size; runs on block end
window_scale(2.0) // optional window upscale
update() callback |e: ptr| { // 60 TPS fixed step
if aebiten.is_key_just_pressed(e, aebiten.KEY_SPACE) == 1 { ... }
}
draw() callback |e: ptr, screen: ptr| { // per frame
core.image_fill(screen, 0, 0, 0, 255)
core.draw(screen, sprite) { // options as a block
translate(0.0 - 8.0, 0.0 - 8.0)
rotate(theta)
translate(x, y)
linear()
opacity(0.5)
}
aebiten.debug_print(e, screen, "FPS: ${aebiten.actual_fps(e)}")
}
}
}
The explicit-handle API (game_new / on_update / on_draw / run,
opts_new + geom_*) remains — six of the nine examples still use it;
snake, flappy, and rotate show the DSL form. One naming rule: inside
aebiten.* / core.* trailing blocks, bare names resolve to the module's
DSL setters before your file's functions — name your own tick/paint
functions something other than update/draw (the ports use
step/render).
Driver testing: run with AETHER_UI_TEST_PORT=<port> and use the standard
AetherUIDriver routes — POST /canvas/1/key?name=Left, /keyup, /click,
/move, /release, GET /screenshot, POST /shutdown. A driver-injected
key press must span an engine tick (sleep ~20ms between key and keyup).