A port of GEM — the VDI graphics layer, then the AES — to an Atari XL/XE fitted with VBXE (video), a 65C816 accelerator with linear RAM (Rapidus, and in principle Antonia), and Ultimate 1MB.
The target surface is 640 × 240, 16 colours — VBXE's HR overlay, 4bpp chunky. That is a better GEM surface than the Atari ST's medium resolution.
Current version 0.1 — VERSION at the top of the tree is where it
lives; make dist stamps a build with it and the commit, and make release is the same for the public, without the floppies (their DOS is
not gem4xe's to give away) and named by the version alone.
A 65C816 with linear RAM is required. VBXE is not: one GEM.COM carries
both display drivers and chooses at start-up, so a machine without a VBXE gets
320×168 on ANTIC mode F instead — make test-m26 boots the shipped binary three
ways to prove it. The 6502 path is still deferred.
The desktop first came up on a real board on 2026-09-13 — a 130XE with a
Rapidus, an Ultimate 1MB, a VBXE and a SIDE 2 — after two reports that did
not get there: a white screen (docs/phase39.md), then a boot screen that
stopped at its Vectors line (docs/phase40.md). That line says
what the machine found under its OS ROM, how the Rapidus was set up and which
RAM under the ROM took the interrupt vectors, and make diag builds GEMDIAG.COM — GEM.COM with a digit and a tone per start-up
step and OPTION/SELECT/START to skip and single-step — for a machine with no
emulator bridge to ask.
The product booted and used, photographed under AltirraSDL by make shots
(tests/emu/shots.py, which reads its coordinates out of the running
desktop's own object trees). Every picture is 640 × 480: the 640 × 240
overlay with its rows doubled, which is what a monitor shows.
The rest are in docs/shots/: the bare desk, the Desk and
File menus, the About box, a folder in icon and text view, Show Info, and
the clock accessory over the desktop.
VBXE sits on the 1.79 MHz chip bus no matter how fast the CPU runs. Measured on target, a full-screen fill costs 0.51 of a frame through the blitter and a full-screen copy 0.88; the same work through the MEMAC window is roughly twenty times slower. So the VDI emits blitter control blocks, it does not plot pixels, and the window manager will use dirty rectangles rather than full-screen repaints.
| Gate | ||
|---|---|---|
make test-host |
170/170 | pointer device layer — the ST, Amiga and CX80 models walked through the target's C in the compiler's simulator — .xex far-code staging, and the application bindings: every one of them called in the simulator with the three call gates replaced by recorders, and the parameter block each builds compared with the VDI and AES contracts; the application kit, assembled and built out of a copy of itself in a directory of its own; the far allocator, asked for the blocks that used to straddle a bank; and the distribution, built both ways, with the release checked for the floppies it must not carry and for what its page says instead |
make test-emu |
5/5 | VBXE FX 1.26 / Rapidus / MEMAC A / CPU switch |
make test-m1 |
5/5 | Calypsi C on the 65C816 |
make test-m2 |
PASS | 640×240×4bpp HR overlay, 153,600/153,600 pixels |
make test-m3 |
86/86 | VDI conformance — pixels and return values |
make test-m4 |
13/13 | AES object library: draw, find, change, edit, centre, icons |
make test-m5 |
PASS | linear RAM probed: banks $02-$EF, 14.9 MB |
make test-m6 |
PASS | far code copied up and running from the banks the linker chose — bank $01, and $01+$02 in a forced-spill link; bank $00 on the fast bus |
make test-m7 |
10/10 | evnt_*, form_do, form_dial, graf_watchbox under host-driven input |
make test-m8 |
12/12 | the window manager and the control manager: rectangle lists, moves, gadgets, WM_* |
make test-m9 |
4/4 | menus: the bar, drop-downs, MN_SELECTED, screenshotted inside the wait |
make test-m10 |
27/27 | native-mode interrupts: the OS shadowed into SRAM byte for byte, the VBI, a ~4 kHz timer, the keyboard, a trak-ball counted under interrupt, and a clean return to DOS |
make test-m11 |
PASS | the application ABI: a separately linked program loaded, relocated and run, calling the VDI and the AES through COP — its records, the loader's, and the screen against the reference |
make test-m12 |
PASS | the file layer: CIO through the OS in emulation mode, rsrc_load/rsrc_obfix, shel_*, and the file selector driven over two disks — its listings, its scrolling and its returned strings against the reference, pixel for pixel |
make test-m13 |
19/19 | alerts, icons and the pointer: form_alert parsed, laid out and drawn against the reference; every mouse form graf_mouse owns, and the caller's own |
make test-m14 |
PASS | SpartaGEM on SpartaDOS 3.2: the DOS identified behind CIO, paths mapped into its > syntax, files read through subdirectories, and the file selector walked into a folder and back out -- listings and strings against the reference, pixel for pixel |
make test-m14x |
PASS | the same on SpartaDOS X 4.50, the cartridge -- with the application pool and the test stage moved out of its way, into the banked window it services calls from |
make test-m15 |
PASS | GEMDOS: the ST's trap #1 as gem4xe's third COP face, answered from CIO and the DOS seam -- directory searches, paths, files, far memory, attributes and errors, every answer against the disk image; and the four calls that used to be holes (docs/phase16.md): Dfree exact from the file system's own count rather than three characters of a listing, Fseek through all three modes, Fdatime without disturbing a search, Tgetdate/Tgettime off the Ultimate 1MB's DS1305 — test-m15x on SpartaDOS X, test-m15d on DOS 2, test-m15u on the U1MB machine, where the clock is compared with the host's own |
make test-m16 |
PASS | the shell loop: DESKTOP.G4A loaded and run, a program run from it and the desktop back, a missing program's alert, shutdown -- the screen against the reference at each stop, the pool and the far heap back where they were, the stack's low-water mark (1199 of 2048 bytes); the VDI's virtual workstations (one per program) under it. GEM.COM, the product, does the same from the DOS prompt and returns to it |
make test-m17 |
PASS | the GEM Desktop: DESKTOP.G4A's menu bar, drive icons and trash, an icon clicked, Desk -> About and its dialog, a drive opened into a folder window, a folder opened in it and closed back out, the fuller, the arrows, the closer, File -> Quit -- driven at the mouse and checked against tools/deskref.py, the desktop itself transcribed against the AES model, its directory listings answered from the disk image: thirteen screens, the desktop's 1984 bytes of globals byte for byte at nine of them, 252 calls on both sides, the pool and far heap back, the runner's and the desktop's stack low-water marks (1223 of 2048, 292 of 640) |
make test-m18 |
PASS | a program run from the desktop: drive A opened, the window full, M11.G4A double-clicked -- the desktop puts its window's place in the shell buffer as DESKTOP.INF text and exits, the shell runs the program, the desktop comes back and opens the window where it was, File -> Quit -- the desktop transcribed twice against one AES model with the program's calls counted between: six screens, G at four waits, 275 calls over the three programs, the pool and far heap back, each run's stack low-water mark (425 and 268 of 640) |
make test-m19 |
PASS | the desktop's writes to a disk: File -> New folder, the name typed into its dialog, Dcreate, the folder in the listing; the same name again, refused, and the alert -- text and all -- out of DESKTOP.RSC's free strings; an item dragged into the new folder and copied there, the walk a DTA deep per level; the window fulled and File -> Show info on the folder -- what it holds, counted -- and then on a file, whose extension is edited in place and whose OK renames it (Frename); the SUB tree selected and File -> Delete, counted first, confirmed in a dialog whose counts tick down, and the tree gone; then Options -> Save desktop and Options -> Read .INF file, the layout written to DESKTOP.INF and read straight back with the windows closed and opened again from it -- seventeen screens, G at ten waits, 605 calls on both sides, and the disk image itself read back afterwards, the INF included |
make test-m20 |
PASS | what the system says comes off the disk: form_error on three disks — the product's LANG.RSC, a German translation of it, and no file at all — each compared with the model given the strings that disk carries, so the first and third draw the same screen and the second draws the translation |
make test-m21 |
PASS | a loadable font: the system font inverted so every glyph differs, read as SYSTEM.FNT at start-up off one disk and absent from another, with vqt_name, vst_font and GDOS's vst_load_fonts/vst_unload_fonts answering for the right face either way |
make test-m22 |
PASS | two programs that are not tests: a calculator driven at its keypad and a clock left to tick, both through the shell loop |
make test-m23 |
PASS | a program opened from a folder and used — the path a person took that eight gates had not |
make test-m24 |
PASS | the ANTIC surface: mode F at 320×168 in the region the VBXE's MEMAC window would have had, 53,760/53,760 pixels against tools/anticref.py, two colours and no palette hardcoded |
make test-m25 |
PASS | the VDI and the AES object library on that surface — the same src/vdi/vdi.c the VBXE build links, on Atari's condensed 6×6 face, with an OUTLINED dialog and its DEFAULT button drawn by ob_draw: 53,760/53,760 pixels against tools/vdiref.py and tools/aesref.py themselves, run on the ANTIC device with one argument changed; and the geometry gsx_start derived, checked by doing the AES's own arithmetic rather than by comparing numbers somebody wrote down |
make test-m26 |
PASS | one binary, two screens: the shipped GEM.COM booted three ways with nothing typed — with a VBXE, without one, and with VIDEO=ANTIC in GEM4XE.CFG against a VBXE that works — checking which device the VDI ended on, what the AES laid out for, what the file parsed to, and the desk itself pixel for pixel against tools/deskref.py run on the ANTIC device — the same desktop model test-boot compares the VBXE desks against |
make test-m27 |
PASS | two contexts taking turns on one engine stack — the turns in the order they were asked for, and a context that recurses six frames and yields from the deepest one finding every local intact on the way back out, which is the whole claim the design rests on: a parked extent goes back to the same addresses. Prints what a park cost: 90 bytes at the deepest |
make test-m28 |
PASS | a desk accessory resident beside the desktop — loaded before it, registered in the Desk menu, and its own timer still advancing while the desktop owns the mouse. The drop-down is checked against the object tree rather than a screenshot; the menu is then pulled down with the mouse and the AC_OPEN read word for word out of the accessory, and File → Quit delivers the AC_CLOSE — after which the accessory is still there, which is the whole difference between an accessory and a program |
make test-m29 |
PASS | an application with more data than bank $00 has — linked --data-model=large, its variables in a far bank of their own, loaded by the shell and run: the far bss zeroed, the far initialisers copied up by the crt's own table, and a 40 KB array walked end to end. The .G4A header's far-bank count comes from the linker's map, because a far bss carries no bytes and sizing it from the image would ask for one bank too few |
make test-m30 |
PASS | the VDI on a printer — the third device through the seam and the first that is not a screen: 640×800 dots in one far bank, drawn by the same vdi.c, written out by v_updwk as PCL 5 and as PostScript, both files read back out of the disk image. The PCL is decoded back to a page and compared with vdiref on devref.Printer — all 512,000 dots — and the Atari's own PostScript is rendered by Ghostscript and compared with the same page, which is what checks the y-flip and the DeviceGray inversion |
make test-boot |
PASS | the product disks booting into the desktop with nothing typed and nothing poked — the loader finds the Rapidus behind the 6502 the machine came up as and switches it itself: build/gem-sp.atr (SpartaDOS, STARTUP.BAT for 3.2 and AUTOEXEC.BAT for X), build/gem-boot.atr (a double-density DOS 2 with DUP.SYS, the system named AUTORUN.SYS, 122 sectors free since the far image travels packed) and build/gem-sdx.atr (a double-sided SDFS disk with no DOS on it, the one the release carries, booted under the SpartaDOS X cartridge fixture); the 6502 boot runs GEM by itself and ends in the loader's refusal; COLDST and the Rapidus switch bring the machine up cold as a 65C816, the DOS starts GEM again, the boot screen is read back off E: while it is held and every line checked against the machine that wrote it, and the far image is spot-checked against the linker's output before the desk is compared pixel for pixel with the desktop model at its first wait |
make test-cf |
PASS | the product CF card booting into the desktop: build/gem-cf.img, an APT table and two SDFS partitions, on a SIDE 2's IDE bus, with SpartaDOS X and the PBI BIOS that mounts those partitions coming from a real Ultimate 1MB flash image. The gate walks the U1MB BIOS setup itself (PBI BIOS on, hard disk on, an ID that is not the Rapidus's) from a fresh profile of its own, keeps the SIDE's SDX bank unmapped so the PBI BIOS will touch the disk, and then runs the same boot as test-boot -- refusal, switch, desk against the model. Needs the U1MB fixture and the patched emulator, so not in make test. make test-cf-dosclock boots the same card with CLOCK=DOS in its GEM4XE.CFG, so that the SpartaDOS X kernel -- kd_gettd, the clock of a machine with no U1MB and no SIDE, an Antonia with an IDE Plus 2 say -- answers instead of the chip, and its answer is compared with the host's clock |
make test-m14u test-m15u |
PASS | the same two on SpartaDOS X 4.49b booted from a real Ultimate 1MB flash image, U1MB switched on -- needs the patched emulator in tools/altirra/, so not in make test |
make check-cc |
PASS | the ten compiler bugs worked around, in the vendor's simulator |
make movie |
PASS | a session with the AES itself, filmed frame by frame and checked as a gate: build/movie/gem4xe.mp4 |
make bench |
— | GEMBench's tests on this machine, in milliseconds, not a gate (docs/bench.md) |
make test runs them all. Per-phase notes, including the bugs and what caught
them, are in docs/ — one document per phase, with an index.
The 37 VDI opcodes the AES and the GEM Desktop actually use are complete. The
AES object library draws, hit-tests and edits; form_do runs a dialog under
keyboard and pointer input; the window manager keeps dirty-rectangle lists
and blits a window across the screen (x snapped to even: the blitter has no
shifter); the control manager turns a press on a frame into WM_* messages
and holds the mouse until the button is up, as the ROM does; menus drop, are
saved and restored through a VRAM form, and report MN_SELECTED — all
compared call for call and pixel for pixel against the host model
(docs/phase8.md). make movie runs all of that as one session on the
emulated machine — About from the Desk menu, a window opened by
double-click, dragged, sized, covered by the dialog, fulled and closed —
screenshotting every frame, with every returned word and every shot
checked against the model. Making it pass found that a pixel plotted
through the MEMAC window costs 60–80 µs, so the pointer and vrt_cpyfm
now go through the blitter like text does (docs/phase8b.md); reading
the compiler's listing then cut the strip builder and the line stepper
to a third of their instructions, and a blitter mode written off in
Phase 2b turned out to do the whole icon in one blit — 60 ms an icon in
Phase 8, 2 ms now (docs/phase8c.md). make bench then put GEMBench's
headings on the machine — absolute milliseconds, since GEMBench's source
is private and there is no ST here — and its first profile named the
control-block builder and the VRAM upload: the dialog went from 78 to
51 ms and a 40-character line from 27 to 15 ms, and a seventh compiler
defect turned up under the rewrite (docs/bench.md). Next is
form_alert and the desktop, with the multiply-per-glyph and the upload
loop still on the benchmark's list.
Phase 7 also found that the Rapidus resets with all of bank $00 on the
1.79 MHz bus, and that gem4xe had run its data, stack and direct page there
for six phases without a gate noticing (docs/phase7.md, Step 4).
src/sys/rapidus.c derives the speed map from the linker's placement and
the MEMAC window rather than restating either, and make test-m6 reads the
registers back.
Code lives above bank $00, in the accelerator's first megabyte. A .xex
segment header is two 16-bit addresses, so a DOS loader cannot place anything
above $FFFF; the far image therefore travels as chunks aimed at a staging
buffer and DOS copies it up through INITAD as it reads the file. That took
the code ceiling from ~28 KB to a bank at a time, and freed the $4000-$7FFF
scaffold the test runner had been borrowing from U1MB. Since phase 38 the
chunks are LZ-packed -- 131 KB of far image in 88 KB of file, unpacked
by src/farload.s as each chunk arrives -- which is what gave the floppy
its DUP.SYS back (docs/phase38.md).
Bank $01 was 90% full by the time the benchmark landed, so the far code is
now linked into one linker memory per bank, $01 through $0F, filled in
order: the image spills into the next bank only when the current one cannot
hold the next whole function, and no function ever straddles a bank boundary
(the 65816 program counter wraps within its bank, and a single memory spanning
banks let the linker place a function across the seam — tried, and it did).
The far heap starts above the highest address the loader actually wrote,
_fl_top, recorded chunk by chunk, since the linker has no operator for the
end of a section that lives in several memories. Because the real build still
fits in bank $01, make test-m6 also links the same objects with bank $01
cut to 16 KB and boots that: the code runs from bank $02, the heap starts at
$03, and the mechanism is proved today rather than on the day the code
outgrows the bank (docs/phase6.md, the follow-up).
Running this on a machine without a 65C816 would corrupt memory rather than fail — the long store the copier needs is an unstable undocumented opcode on an NMOS 6502 — so the loader identifies the CPU and probes for linear RAM before its first store, and prints a line and returns to DOS if either is missing.
Interrupts run in native mode (docs/phase9.md). The 65C816's native
vectors sit at $FFE4-$FFEF, inside the OS ROM, which the Atari OS never
fills — so from Phase 0 to Phase 8 gem4xe ran with NMI and IRQ off and
polled everything. src/sys/irq.c copies the OS ROM into the Rapidus's
SRAM under it, page by page through write-through, patches the six vectors
to point at bank-$00 stubs, and switches the window in; the handlers
count frames, run a POKEY timer at ~4 kHz that samples the joystick port
and decodes a quadrature or trak-ball device into two counters, and put
keys into a ring. The pointer layer consumes the counters, so an ST mouse,
an Amiga mouse and a CX80 trak-ball now work as well as the tablet did —
in Altirra; no real hardware has been near this. On exit the ROM is
switched back, $0000-$3FFF is written back to the motherboard, and DOS's
own keyboard IRQ echoes the next key typed at its prompt, which is what
the gate checks. Getting the gate green also found that the emulator was
reading a phantom joystick: the host keyboard's "System Control" HID
interface, which SDL enumerates as a joystick with one out-of-range axis,
held PORTA's left line low through AltirraSDL's input maps. The rig now
keeps SDL's joystick subsystem off every host input device it can find in
sysfs.
Applications call in through COP (docs/phase10.md). COP #$73 is
a VDI call and COP #$C8 an AES call, the parameter block's address in
X:C — the ST's trap #2 on a 65C816. The handler is the saveds entry
point of the plan: it takes gem4xe's direct page, data bank and stack for
the duration and gives the caller's back, and the shim behind it keeps
DRI's copy-in/copy-out discipline, so an application's arrays can be
anywhere in the 16 MB. A program is linked on its own rules
(src/app/gemapp.scm) and packed as a .g4a by tools/mkg4a.py, which
derives the fixups Calypsi's linker does not emit by linking the same
objects three times and diffing; the loader puts the near part in a
bank-$00 pool and the code in a far bank. The gate application makes
eighteen VDI and AES calls and the harness checks what each returned,
from the application's own memory, against the reference.
More than one thing at a time (docs/phase26.md). Every file
operation could already work on a set — the delete, the copy and the move
all walk the window's entries and act on each one flagged — but a click
chose exactly one. Now SHIFT-click adds and removes, and a press on a
window's background draws a rubber band whose box is the new
selection. A press is read in a new order: the drag is recognised
before the click semantics, because SHIFT means "move" to a drag and
"add to the selection" to a click and the same press cannot be both.
The band is what test-m19 drives, and not by preference: no harness
here can hold SHIFT down through a mouse click, which is the same block
that has kept the SHIFT-drag move ungated since phase 18.
The desktop remembers (docs/phase25.md). Save desktop writes the
window layout to DESKTOP.INF on the boot drive and Read .INF file
reads it back, closing what is open first; at start-up the desktop looks
in the shell buffer, then the file, then its built-in default — which is
the donor's order, and the difference between surviving a program and
surviving the power being switched off. Most of it was already there:
the format, the writer and the parser have carried the windows between
programs since milestone 6. What was missing was a file.
The loader switches the machine (docs/phase23.md). A Rapidus always
cold-boots as a 6502 — Altirra's device does it in ColdReset() and the
card does the same — so gem4xe used to refuse a machine that could have
run it, and every gate got past that by poking $D1FF/$D191 over the
test bridge. Thirty-five bytes in src/farload.s now probe the eight PBI
slots for a card that answers on both of the registers measured at a
DOS prompt ($D190 reads $00, $D191 bit 6 set; open bus reads $FF
at each), set COLDST so the restart is a cold one, and switch. The CPU
resets inside that write. test-boot now does nothing to the machine
after power, and test-m6 boots the same image with the Rapidus taken
out, which is where the refusal is still right — and gates that the probe
leaves such a machine exactly as it found it.
⚠ And it found a real one (docs/phase24.md). Those thirty-five
bytes turned test-m14x red, and thirty-five bytes of dead padding
did the same, at some sizes and not others — a fault with no clean
boundary. It was not the near memory at all: far_alloc was a bump
allocator with nothing stopping a block from straddling a bank
boundary, and Calypsi's __far pointer arithmetic is 16 bits within
a bank (carrying into the bank byte is what __huge is for). A buffer
that straddles one wraps to the bottom of its own bank the moment it is
indexed past the edge — and the bottom of a far bank is the far code
image. It is the phase 6 corruption by another road, the file selector's
900-byte name list was the block, and master was green by luck.
Eight lines start the next bank rather than straddle, and
tests/host/test_farmem.py asks the allocator for the blocks that used
to break it.
And then the room (same notebook). Bank $00 had four bytes free
and seven; the 608 bytes of fill patterns — 23% of all the near memory
there is — went to cfar with the far code, and the boundary moved to
share what that bought: 360 and 259 bytes free now. The workstation
holds the pattern as a source and a first row rather than a pointer,
because a pointer that could name either a far table or the user's own
near array has to be far, and then "is this the user's?" is a near-to-far
comparison on the workstation switch. That version passed test-m3
86/86 and crashed the desktop — the conformance suite drives the
physical workstation and never reaches that code.
make dist is what a tester is handed (docs/phase22.md): the
bootable disks, the system's files loose for a disk of their own, the
kit, and a page that says how to try it — half of which is generated
from the desktop's own menu and from the disk images, so it cannot
claim something the resource greys out. It carries 816.COM, which
is the piece that was actually missing: a Rapidus always cold-boots as
a 6502, so a tester's first screen is the loader's refusal, and until
now only the test harness could make the switch. Fourteen bytes set
COLDST and write $D1FF/$D191; test-boot now types 816 at the
SpartaDOS prompt rather than poking those registers, so what the page
says to do is what the gate does.
There is a kit (docs/phase21.md). make sdk packs eleven files —
the header, the bindings and the start-up as source, the linker's rules,
the packer and a commented example — with no part of gem4xe itself in
them, because an application links against none of it. make in the
kit turns example/hello.c into hello.g4a; make APP=mine.c turns
yours into mine.g4a. Its gate builds it out of a copy of itself in a
directory of its own, and then rebuilds the Phase 10 gate application
with it and compares the bytes: they are identical, so what test-m11
proves about that binary on real hardware is what the kit inherits.
And there is a binding for every one of them (docs/phase20.md).
src/app/gem.h declares the whole surface the system serves — the VDI
in the names it has had since 1984, the GDPs as v_bar/v_arc/… rather
than a raw v_gdp, every vq* inquiry, the raster calls with a real
MFDB, the file selector, objc_edit, the shel_* family, and the
GEMDOS calls phase 16 added — so GEM source written for an ST compiles
against it. The four opcodes the driver answers with v_nop are
deliberately absent: a binding that silently does nothing is worse than
a name that is not there. tests/host/test_bind.py calls every binding
in the compiler's simulator with the three call gates replaced by
recorders, and compares the parameter block each one builds with the
contract — which is how a vqt_width that put its deltas in the wrong
words was found, in a gap the conformance gate cannot see.
SpartaGEM: gem4xe runs on SpartaDOS (docs/phase13.md), 3.2 from
disk and X 4.50 from its cartridge, as well as on DOS 2 -- the same
binary, which asks the DOS what it is at start-up (src/sys/dos.h) and
reads its answer for the shape of a path and the mark on a folder in a
listing; a GEM application sees A:\DIR\NAME.EXT on all three. Making
room for the cartridge moved the application pool and the test stage
into $4000-$7FFF, the banked window the port had kept out of for
twelve phases: a DOS banks there only inside its own call, and puts it
back. Two things bit on the way. SpartaDOS 3.2 keeps 7 KB of itself
under the OS ROM, where the interrupt layer's ROM copy had overwritten
it, so the copy now goes to the Rapidus's SRAM alone -- gem4xe on
SpartaDOS 3.2 needs the accelerator. And the bridge reads what the CPU
sees, SpartaDOS X's bank included, so the one word the harness polls
during a call moved out of the window. Every call costs more than on
DOS 2 (a directory read about twice as long a record), measured in
docs/phase13.md, and everything is Altirra: no real SpartaDOS machine
has been near this.
GEMDOS, and two emulator bugs (docs/phase14.md). The ST's trap #1
is gem4xe's third COP face: a call block laid out as the ST's stack
frame with the result in front, so an ST binding's picture of the
arguments is the block's picture four bytes along, and the donor's
desktop code can read unchanged above it. Building it found two places
where Altirra's 65C816 is not a 65C816 in native mode -- a taken branch
that does the 6502's page-crossing dummy read, in bank $00, which
puts $D5xx on the cartridge bus from code running at $xxD5xx; and
SEI with an IRQ pending, whose "one more interrupt" shadow the native
vector states never clear, an IRQ storm that wraps the stack. Both are
fixed in tools/altirra/ and sent upstream; until then the linker map
leaves the $D5 page of every far bank empty. The Ultimate 1MB, which
the SDL emulator could not switch on headlessly, now can be: the same
patches add the switches, and the two *u gates boot SpartaDOS X from
the machine's own flash.
The desktop (docs/phase14.md, milestones 3 to 7). The AES's shell
loop runs DESKTOP.G4A, then whatever it asks for, then the desktop
again; GEM.COM is that loop from the DOS prompt, back to it at
shutdown. The desktop is the donor's deskmain.c, deskobj.c and
deskwin.c cut to what shows so far -- the bar, an icon for each drive
GEMDOS reports and the trash, About, Quit, folder windows, and a
program run from its icon, and now a folder made and a folder tree
deleted: a drive or a folder double-clicked lists through
Fsfirst/Fsnext into a window, folders first, with an icon per
entry, the name and information lines, the fuller, the arrows and the
closer; a program double-clicked goes to shel_write, and the desktop
leaves its windows' places in the shell buffer as the text of
DESKTOP.INF and opens them again when the shell brings it back; File
-> New folder and File -> Delete (deskfun.c) put up the donor's
dialogs, and the delete counts what it will do before it does it,
walking folders inside folders with a DTA per level -- our GEMDOS
keeps a search by the DTA that owns it, as the ST does. It loads its
resource from the disk beside it, its icons EmuTOS's, checked in like
the font. Its gate is a new kind: the desktop is a program, not a
script, and which calls it makes depends on what the AES answers, so
tools/deskref.py is the desktop transcribed against the model, and
the harness syncs to the ABI's own call counter rather than a record
count. A tenth compiler defect (parameters clamped in place, then read
from a slot never written) put two of three icons off the screen on
the first run; an eleventh (a near-to-far struct copy over 8 bytes is
an internal error) is copied around byte by byte. The first window
open ran the application's 256-byte stack out, and a .G4A's stack
is sized per link now; and the hourglass that stayed over an opened
folder was the donor's gsx_mfset hide-and-show, missing on both
sides of the gate. The transcription found the next one on the host
before the emulator ran: the desktop's loop ends on do_open's
answer, and a drive's window opening had been answering TRUE -- as it
found, a milestone later, that a New folder was answering the same way
and quitting the desktop. The delete's dialog broke one of the
compiler's own rules (a 16-bit load through a local pointer with the
arithmetic in the same expression, tools/ccbug rule 5) and filled
25 KB with spaces, through GTIA space, which soft-resets VBXE, and
through POKEY's IRQEN, which froze the machine in an interrupt storm:
the gate's post-mortem -- which call the target is inside, what the
delete had counted, the CPU's last thirty-two instructions -- is how
that was read back.
What is not built yet, and is written down so it shapes what is
(docs/shipping.md). The system is 121 KB — more than a single-density
floppy holds, and an enhanced-density one leaves it three sectors of
room. Double density is where the DOS 2 product disk lives now, and
tools/atr.py writes it: 253-byte sectors take the DOS, its shell, GEM
and the desktop with 6 KB to spare — enough for a DESKTOP.INF and
not for a program. Since GEM.COM carries both display drivers
(docs/phase34.md) that floppy is the system and nothing else; an
application goes on the SpartaDOS install disk, which has 123 KB free,
or on the card. The volume gem4xe belongs on is a CF card or a hard disk with APT
partitions, and make writes one: build/gem-cf.img is a 16 MB image
with an APT table, two 8 MB SDFS partitions and the install layout on
the first — the system in \GEM\, an application in \APPS\, an
AUTOEXEC.BAT that runs it. make test-cf boots that card into the
desktop, on the machine the project is for: an Ultimate 1MB whose
flash holds SpartaDOS X and the PBI BIOS, a SIDE 2 with the card on
its IDE bus. Nothing on the card is a driver — the PBI BIOS reads the
APT table and mounts the partitions as D1: and D2: before any DOS
runs. Three things about that machine had to be read out of its own
firmware first, including the wait at $D803 that stops the disk dead
while the SIDE still claims the cartridge window (docs/shipping.md
§3). make sd writes the same card in the shape a SubCart / AVGCART
wants -- a FAT32 partition first, the APT after it -- and make test-sd
boots that one. The floppy is now the bootstrap, not the ceiling.
Both product disks now come up in the desktop rather than at a prompt:
the SpartaDOS one from a STARTUP.BAT and an AUTOEXEC.BAT (3.2 runs
the first, X the second), the DOS 2 one from AUTORUN.SYS — which
DOS II+/D, the disk's old DOS, turns out not to have at all. Nothing is
typed on the way in: a Rapidus always cold-boots as a 6502, and the
loader switches it (docs/phase23.md). No string a person
reads belongs in the C, and none does now: the desktop's eleven alerts are nine free strings of DESKTOP.RSC,
asked for by index (fun_alert, the donor's shape), and the gate puts
one on the screen and compares it. The one exception is the alert that
says the resource is missing, which cannot come from the resource.
A translation is two files (docs/phase15.md). LANG.RSC is what
the system says — form_error's alerts, the shell's failures — kept
far-resident and copied a string at a time into a near buffer, since bank
$00 is the scarce thing, with the same bytes linked in as the English a
disk without the file falls back on. SYSTEM.FNT is the character set it
says it in: the 8x8 strip is loadable, so Latin-2, Cyrillic, Greek or
Turkish (all of which EmuTOS ships, and make fonts writes out) replace
the Atari ST set the system links. make test-m20 boots three disks —
the English, a German one, and no file — and make test-m21 two, with
and without a font; both are compared with the models given what that
disk carries. The calls are GDOS's own (vst_load_fonts,
vst_unload_fonts, vst_font, vqt_name) and none of the rest of GDOS:
with 14 MB the memory was never the constraint, the AES's fixed character
cell is. An application's own .RSC stays the third file, because a GEM
dialog's geometry travels with its text.
The applications this is for (docs/gacs.md). GACS — the GURPS Autoduel
Construction Set — and RetroWP are why gem4xe was started: a portable C89
engine each, a shell per platform, the Atari ST first. make gacs-check
compiles GACS's engine with Calypsi for the 65C816 in both data models, links
it with its working set in far memory, and runs it in Calypsi's simulator
against the six tables GACS ships: 27 chassis, 13 engines, 10 tires, 20
weapons parsed, a design computed, ad_compute answering AD_OK, and the
line GACS's own CLI prints. It wants 24 KB of code and ~56 KB of data, all
far — and 84 bytes of bank $00. Every AES call its GEM shell makes is
implemented here, its resource is the format we read, and its dialogs are laid
out in character cells that fit 640×240. What is missing is a shell, one
linker line, and — for both programs, eventually — a printer.
Every gate compares the target against a host reference model —
tools/vbxeref.py, tools/vdiref.py, tools/aesref.py — which is treated as
the specification. Both pixels and returned values are compared, byte for byte,
running on emulated hardware with all three boards fitted.
Nothing here is asserted by eye. Two of the bugs found so far were invisible on
screen and only a pixel diff caught them — and one went the other way: the file
selector listed a file the reference did not, every returned value agreed, and
only the screenshots disagreed (docs/phase11.md).
Calypsi cc65816 5.18 has ten defects this tree has met — eight in code
generation, one crash and one in the front end's constant arithmetic — each
reproduced in the vendor's own simulator (the crash, in the compiler itself)
and worked around at the source (or, for the divide flags, with a linker
override). tools/ccbug/README.md lists
them and the rules the sources follow; make check-cc reports when one is
fixed upstream so its workaround can go.
Needs Calypsi 5.18+ for the
65816 and Python 3. There is no Atari target in Calypsi, so this tree carries
its own board support: src/crt_atari.s, src/gem4xe.scm and
tools/mkxex.py. The C itself is compiler-neutral: every word Calypsi adds
to the language — __far, __attribute__((tiny)), __simple_call, the
interrupt intrinsics — is mapped once in src/portab.h (FAR, TINY,
SIMPLE_CALL, cpu_sei()…) and appears nowhere else, which
tests/host/test_portab.py enforces; a second 65816 compiler needs a second
block in that header and its own assembly sources, not a sweep through 26,000
lines. The GEM system font is extracted from an
EmuTOS checkout at build time rather than
committed here, so point EMUTOS= at one; CALYPSI= finds the tool chain.
Both default to ~/dev/….
make # build
make sdk # the application kit, for writing a program that runs on it
make dist # what a tester is handed: the disks, the kit, and how to try it
make release # the same for the public, named by VERSION: tarball, zip, the DOS-less floppy, checksums
make test-host # the host tests: no emulator, no fixtures
The emulated gates need AltirraSDL,
and the ones that put an Ultimate 1MB in the machine need the patches in
tools/altirra/ as well — two are open pull requests upstream, so run those
with ALTIRRASDL=/path/to/patched/AltirraSDL.
They also need Atari disk images, and none is distributed here: a DOS 2
disk, a double-density DOS 2 disk, a SpartaDOS 3.2 disk, an SDX cartridge
image, a U1MB flash image. Copy fixtures.toml.example to fixtures.toml
(gitignored) and put your own paths in it; the harness copies an image into
build/ before it touches it, and a gate whose fixture is missing says so and
stops.
make test # host tests + the emulated gates
make test-cf # the CF card on the U1MB machine (needs [u1mb].flash)
make test-cf-dosclock # the same, with the DOS as the clock
GPLv2 or later — see COPYING, and docs/licence.md for the position in
full, including the one thing still outstanding. The lineage is EmuTOS, which
is the Caldera-GPL'd Digital Research GEM source carried forward in C, so the
licence is inherited rather than chosen.
It is v2-or-later throughout, including the AES, and the reason is in GPLv2
itself rather than in an assumption: EmuTOS's vdi/*.c say "version 2 or at
your option any later version", its aes/*.c name no version at all, and
section 9 says that when a program does not specify a version "you may choose
any version ever published by the Free Software Foundation". Silence is the
recipient's choice, not a v2-only grant.
No Apache-2.0 object is linked — src/sys/clib.c supplies the eight ISO C
functions Calypsi took from NuttX — and the 815 bytes of GEM.COM, 0.6% of it,
that is the compiler's own runtime falls under the GPL's System Library
carve-out for "a compiler used to produce the work" (GPLv3 §1; GPLv2 §3 more
loosely), so the binaries are wholly distributable as they stand. Calypsi's own
licence expressly permits "producing application software for vintage and
retro computing systems". tests/host/test_licence.py reads the linker maps
and fails if the Apache-2.0 half regresses or the runtime set grows.
Where a file follows EmuTOS, its header names the donor file it follows, and the two trees are read side by side deliberately — this is a port, not a clean room. Two provenance rules hold everywhere else:
- The Atari Corp VDI/AES corpus is a specification only. It settles what a real ROM does; no line of it appears here.
- Nothing that is not ours to give is in the tree. No ROM images, no disk
images, no firmware: the disk images are the user's own (
fixtures.toml), and the Altirra patches intools/altirra/are diffs against a GPLv2 project that are also filed upstream. What is here from elsewhere is GPL'd and says so in its own header: the GEM 8×8 font and the standard fill patterns, extracted from EmuTOS bytools/fontconv.pyandtools/patconv.pyand checked in — so the host reference reads the same bytes the target links, and an EmuTOS checkout is needed only to regenerate them.


