Doom on an IBM i (AS/400) green screen.
Two backends over one doomgeneric core:
as400doom— compiles in IBM i PASE and renders 24-bit colour Unicode half-blocks to your SSH session. Actually playable.as400doom5250— a TN5250E server. Pointtn5250,tn5250jor IBM ACS at it and play Doom on a real 5250 green screen. No AS/400 required, which is the point: the interesting part is the protocol, and an IBM i is not something most people can borrow.
$ make && make wad
$ ./bin/as400doom # terminal
$ ./bin/as400doom5250 # then aim a 5250 emulator at localhost:2323
A 5250 is block-mode. The received wisdom is that you get roughly one frame per Enter key, which is why green-screen Doom is usually a joke rather than a project.
That is wrong, and it is worth being precise about why. A 5250 client is a pure slave for output: its receive loop drains every record that arrives and applies it immediately, with no "am I invited?" test anywhere in the path. The host can push unlimited unsolicited Write-To-Display records. You can stream video to a terminal designed in 1977.
Better still, the client calls its render function once after draining the whole queue — so a host that outruns the terminal gets automatic frame coalescing instead of an unbounded backlog. Free adaptive frame-skip.
Input is the half that really is block-mode: a 5250 cannot transmit spontaneously. Each AID keypress sends exactly one record. Video streams, keys trickle.
A 5250 attribute byte occupies a screen cell. Two adjacent cells cannot have different colours — every colour change burns a cell, which paints black. Seven colours, minus a cell per transition, sounds like a flat and speckled image. The first implementation here spent 47% of the screen on attribute cells.
Two things fix it.
The attribute picks intensity, not just hue. A normal attribute is a
coloured glyph on black, so a density ramp (' ' '.' '*' '#') gives partial
coverage; a reverse attribute with an EBCDIC space gives a solid block.
That is 7 colours × 4 intensities + black ≈ 29 tones. Glyph changes are free;
only attribute changes cost a cell. And level 0 is black under any normal
attribute, so black — which is most of a Doom frame — never breaks a run.
Where the colour changes go is a shortest-path problem, not a per-pixel choice. At each column you either render under the current attribute or spend the cell on a switch. The key structural point is that the switch cost does not depend on which attribute you switch to, so the inner minimisation is one running minimum shared by all 14 states and the DP collapses from O(states²) to O(states) — 14 operations per column, ~50K per frame.
The result is the provably lowest-error assignment for each row, not a heuristic. Attribute cells per frame dropped 262 → 63, non-black cells rose 1423 → 1926, and frames got smaller (1113 → 851 bytes).
| ANSI / terminal backend | works, verified |
| TN5250E server | works, verified against a protocol-auditing client |
| PASE build wiring | flags verified against IBM's toolchain; not run on real hardware |
| Native ILE C build | written, never compiled — see ile/README.md |
I do not have an AS/400. Everything marked verified was verified on Linux against real protocol implementations; everything else says so plainly.
make # both binaries
make wad # fetch Freedoom (BSD-licensed) into ./wads
make test # unit tests -- run these on the IBM i too
make pase # run ON the IBM i, in a PASE ssh session
yum install gcc12 make-gnu coreutils-gnu
gmake pase && gmake test
Or push and build in one step:
tools/deploy-pase.sh you@your-ibmi
make pase uses gcc-12, not gcc. The plain gcc on IBM i is a
symlink to GCC 6.3.0, which IBM has abandoned and which fails on 7.5+ with
spurious errors out of sys/socket.h. It also passes -Wl,-bbigtoc, without
which AIX's 64KB Table of Contents overflows at link time on a codebase this
size. See docs/PORTING.md.
Terminal — w/s move, a/d strafe, q/e turn, arrows, space to
use, Ctrl-L fire, Esc menu, Tab automap.
5250 — a 5250 only transmits on AID keys, so everything is a function
key: F1/F2 forward/back, F3/F4 turn, F5 fire, F6 use, F7/F8
strafe, F9 automap, F10 menu.
Neither a terminal nor a 5250 can report key release, so held keys are
inferred from autorepeat — see the long comment in src/input.h for why
movement stutters once on first press and why that is inherent.
| variable | |
|---|---|
AS400_DOOM_COLOR |
truecolor (default) / 256 / ascii |
AS400_DOOM_FPS |
draw-rate cap; 35 terminal, 20 for 5250, 0 uncapped |
AS400_DOOM_KEYHOLD_MS |
synthetic key-release window (default 300) |
AS400_DOOM_PORT |
TN5250E listen port (default 2323) |
AS400_DOOM_5250_WIDE |
force 24×80 (0) or 27×132 (1) |
AS400_DOOM_5250_FULLWIDTH |
use all columns; more detail, stretched |
The draw-rate cap is decoupled from the game tick — it drops frames, not tics, so the simulation stays exact. It matters: uncapped truecolour at 100×30 sustains ~1.3MB/s of escape codes, which is fine on a local pty and hopeless over SSH to someone else's datacentre. At 10fps it is 359KB/s.
tools/t5250_probe.py— a TN5250E client that renders the session to PNG and audits the protocol. The reference C client frames purely onIAC EORand never reads the record length field, so a malformed header passes local testing and only fails later against IBM ACS. This checks it.tools/ansi2png.py— replays an ANSI capture into a PNG. The terminal output is diffed, so you cannot eyeball a single frame; you have to replay.tools/deploy-pase.sh— rsync to an IBM i, build, run the tests there.
src/
dgcompat.h pixel extraction, char-signedness assert, geometry
render.c box-filter 320x200 -> character-cell grid (shared)
input.c key queue with synthesised key-release (shared)
wadfind.c IWAD autodetect -- doomgeneric compiles out the engine's
own search, so DOOMWADDIR is silently ignored
ds5250.c the 5250 encoder: DP colour assignment + order emission
backend_ansi.c DG_* for a terminal
backend_5250.c DG_* + the TN5250E server
dgassert.c compile-time sizes for every on-disk WAD struct
vendor/doomgeneric/ engine, with 4 documented patches (docs/PORTING.md)
ile/ native ILE C path -- researched, unverified
GPL-2.0-or-later, inherited from Doom via doomgeneric. LICENSE is the
verbatim GPLv2. Doom is © 1993–1996 id Software; doomgeneric is © ozkl;
Chocolate Doom lineage © Simon Howard.
Freedoom (fetched by make wad, not committed) is BSD-3-Clause and is not
covered by this repo's licence. Commercial Doom IWADs are not redistributable
— if you own Doom, pass -iwad /path/to/doom.wad.