Ported Linux Emulators — Software Guests as ImGui Layers
The broadest, lowest-effort way to run another system on the Ant64: take an existing Linux emulator, point its OS calls at AntOS, hand its frames to the ImGui canvas, and let FireStorm dress the result up at scanout. It runs as an AntOS application on DeMon's CM — software-accurate, not cycle-accurate — and it exists precisely because the AntOS libraries were kept standard-compatible.
This is a distinct path from a personality cartridge. A personality is the machine — an HDL chipset plus a fabric softcore (or a hard-core emulator) that displaces FireStorm and aims at silicon accuracy. A ported Linux emulator is a guest program running on the CM under AntOS, whose output FireStorm composites and upscales like any other layer. Both "run another system"; they are different products, and this page is about the software one.
Why it's mostly a link job, not a rewrite
DeMon is a Raspberry Pi CM5 running real Linux, and the AntOS libraries deliberately keep os and io byte-for-byte compatible with standard Lua/POSIX where they overlap. So a mature Linux emulator (a console core, a home-computer emulator, a DOS box) already makes the calls AntOS understands. Porting is therefore mostly substituting a few implementations under signatures that already match, not chasing hundreds of call sites.
The work concentrates at three seams, in rough order of effort.
Seam 1 — OS calls (mostly free, occasionally a shim)
Because the host is Linux, the emulator's libc fopen / clock_gettime / mmap calls simply work. You only redirect the calls that should hit AntOS semantics instead of raw Linux:
- Storage — ROM, disk-image, and save-state paths routed through the drive-letter VFS so "open my ROM" lands on
C:(cartridge) orD:(DBFS) and is catalogued automatically (see AntOS Libraries §7). A guest save-state becomes a tagged DBFS file, queryable and shadow-backed, for free. - Time — most guests want stock wall-clock (already fine); a guest that reads a hardware RTC can be pointed at the MCP79410.
- Config / logs — into DBFS rather than scattered dotfiles.
Mechanically this is a thin libantos_compat the emulator links ahead of libc, or an LD_PRELOAD shim — a small surface, because most calls don't need redirecting at all. The shim is a binding-style adapter: it calls the same libantos_fs / libantos_io cores AntOS uses, so a ported emulator and a native AntOS tool see one filesystem.
Seam 2 — Video (the frame-present boundary, and the big win)
Almost every Linux emulator already renders a guest frame into a framebuffer/texture and hands it to SDL or GL to present. The port intercepts exactly that present call — one function — and instead drops the frame into the AntOS scene as a layer beneath the ImGui UI:
emulator core → guest framebuffer (native res, e.g. 320×240)
→ present shim → AntOS canvas layer (under the ImGui overlay)
→ HDMI → FireStorm compositor → scanout
The emulator core is untouched — you replace its presenter, not its renderer. Crucially, the emulator keeps rendering at native / integer guest resolution and stays cheap; it never pays for upscaling.
FireStorm does the dress-up at scanout
Whatever arrives over HDMI, FireStorm scales and styles at scanout: integer/anamorphic upscaling to 4K, CRT masks, scanlines, curvature, colour-bleed, NTSC artefacts. The guest stays a small, dumb, low-res image; the FPGA makes it look right, and does so on the output side where it's free. The ImGui UI (menus, overlays, debug) composites on top as its own layer — the emulator frame is just one of FireStorm's 16 layers.
Present modes — a setting, not a build option
Where the guest frame goes is an AntOS setting the shim reads at frame time, not a per-emulator build decision — so one ported binary switches modes without recompiling. The emulator core never knows; it still just calls "present this framebuffer," and the shim's presenter routes the bytes.
| Mode | Path | Upscale by | Trade |
|---|---|---|---|
canvas (default) |
guest frame → ImGui canvas layer → 720p HDMI overlay | CM GPU to 720p, then FireStorm 720p→4K | simplest; one HDMI feed; guest + UI share a surface; double-resample |
native |
guest frame → 2nd HDMI (data channel) at native res → FireStorm | FireStorm, native→4K + CRT on the real pixels | best quality & latency; no CM resample; guest is its own FireStorm layer |
pre720 |
guest frame → CM scales to exactly 720p → HDMI | FireStorm 720p→4K only | predictable fixed feed; deliberate CM scale; UI kept separate |
canvas and pre720 differ on purpose: canvas composites the guest into the ImGui scene (guest and UI on one surface); pre720 is a clean, deliberately-scaled 720p guest feed with the UI kept elsewhere.
The setting. emu.present_mode is a normal sys / Settings value — editable in the Settings overlay or from a command, persisted in DBFS. A per-emulator override falls out for free: a global default plus an optional key in the emulator's DBFS map file (twitchy title → native, everything else → canvas). It can be switched live — flip the key and the next present goes the other way, one frame's blip as FireStorm re-homes the layer.
Presenter backend abstraction. The shim exposes one present(framebuffer, geometry) contract with a backend per mode, selected by the config key — the same core/binding-style split the libraries use: one contract, swappable implementations. Adding a fourth target later (a capture-to-DBFS recorder, a remote / netImgui present) is another backend, not a fork.
Two things native mode requires (pinned in fpga):
nativeis the second consumer of the 2nd HDMI data channel. That channel normally carries DeMon's in-stream data; innativemode it carries the guest framebuffer while an emulator is foregrounded, and DeMon's data falls back to the PCIe link — one claimant at a time, so the two uses can't collide.- FireStorm must be told the guest geometry. A native-res layer isn't a fixed 720p, so the shim hands FireStorm the guest width / height / pixel-aspect over the register window (PCIe) as a small sideband, so the compositor knows how to integer-scale it.
Seam 3 — Audio & input (the "playable vs runs" seam)
Small, but skip them and the guest runs without being usable.
- Audio — another present-boundary swap: the SDL audio callback (or ALSA sink) streams into Pulse/Tempest for mixing with the chipset's own voices, rather than out a Linux sound device.
- Input — remap SDL/evdev events from the real controls: joypads via Sticky, keyboard/mouse via DeMon's USB host, performance controls via Pulse. A per-emulator map file (in DBFS) turns guest buttons into Ant64 inputs.
Frame pacing & latency (vsync-lock)
A guest frame travels CM → HDMI → FireStorm compositor → scanout, which adds roughly a frame of pipeline. Fine for most software; noticeable for twitch play. Two rules keep it honest:
- Lock the emulator's present to the system vsync pulse — the same frame-locked Composer contract the supervisor feeds use — so frames promote deterministically instead of tearing or drifting against the output.
- Let the guest run at its own rate (50 Hz PAL, 60 Hz NTSC, 59.94, …) and have FireStorm handle the cadence to the real display, rather than resampling time in the emulator. The guest's clock stays authentic; the FPGA reconciles it to the panel.
Latency here is inherent to the software route — it is one of the reasons the fabric personality path exists for cases that can't tolerate it.
How this differs from a fabric personality
| Ported Linux emulator (this page) | Fabric personality | |
|---|---|---|
| What it is | a guest program on the CM, under AntOS | the machine — HDL chipset + fabric/hard-core CPU |
| Accuracy | software-accurate (implementation-dependent) | aims at cycle / silicon accuracy |
| Effort to bring up | a port: shim + present hooks (days) | HDL / core design (much larger) |
| FireStorm's role | composites + upscales the guest's layer | is the chipset the guest drives |
| Breadth | the whole Linux emulator ecosystem | one carefully-built machine at a time |
| Runs during | normal AntOS operation (FireStorm native) | a loaded personality image (FireStorm displaced) |
Neither replaces the other. The emulator route gives broad compatibility fast; the personality route gives accuracy for the machines that deserve it. It's worth being explicit so nobody expects cycle-exactness from a software port, or a two-day turnaround from a fabric core.
Where it sits — the fourth emulation tier
personality_cpu lays out the ways to recreate a guest CPU — fabric softcore, A25 hard-core emulation, CM emulation, and hybrids. This tier is the pragmatic form of CM emulation: rather than writing a bespoke emulator, you reuse an existing, mature Linux emulator and adapt it through the compat shim and the present hooks. It is the AntOS libraries' first big external customer — the clearest demonstration that keeping os/io standard, and the core/binding split clean, buys real leverage.
Honest tradeoffs
- Accuracy is the emulator's, not the Ant64's — as good (or as loose) as the upstream core.
- A frame of present latency — mitigated by vsync-lock, not eliminated.
- Per-emulator glue — the shim is generic, but audio/input maps and the present hook are per-emulator (small, but not zero).
- Licensing travels with the core — a ported GPL emulator stays GPL; ship accordingly.
- Best-quality upscale needs the native-res-layer path (above); the easy canvas path double-resamples.
What you get for that: the Ant64 can run an enormous back-catalogue quickly, with FireStorm's scanout effects making software cores look better than they do on a plain Linux box — and every guest's files living in DBFS like any other AntOS data.
Related documents
- AntOS Libraries — the standard-compatible
os/ioand core/binding split that make the port a link job - Personality CPUs — the fabric / hard-core / hybrid recreation options this tier sits beside
- Composer — the frame-locked layer contract the guest frame promotes under
- FireStorm — scanout scaling, effects, and the system vsync pulse
- Pulse / audio — where guest audio is mixed