Let an AI agent, or your test suite, play, test and debug software on a vintage PC emulated by 86Box, with emulated time frozen between moves.
86box-mcp is an MCP server that drives an emulated PC at the hardware level: keys on the emulated keyboard, the emulated mouse, frames from the emulated video card, physical memory. Nothing runs inside the guest, so it works from BIOS POST on, in any video mode, under any OS. Its main targets are DOS games and tools. It runs headless (good for servers and CI), or with the normal 86Box window if you want to watch.
It is built on an opt-in automation socket in 86Box itself, merged upstream
in #8365. It's off by default:
build 86Box with -D AUTOMATION=ON to get it.
Automating Windows 9x? Use V9x Remote Agent instead. See how they compare.
An agent takes seconds to decide what to do; a DOS game runs at 35 or 70 frames per second. In held mode no emulated time passes between tool calls, so a real-time game becomes turn-based:
act(hold_keys=["up", "ctrl"], ms=200) # run forward, firing, for exactly 0.2 s of game time
# → returns the frame at the end of those 0.2 s
The agent thinks as long as it needs, and the game never moves without it. The same control makes runs repeatable: two cold boots driven by the same inputs produce byte-identical frames. Steps run as fast as the host allows (about 2× real time for a 486DX2-66 on an older Xeon).
Needs Linux and Python 3.11+. Get the code, and 86Box:
git clone https://github.com/dafugg/86box-mcp.git && cd 86box-mcp
git clone https://github.com/86Box/86Box.git 86boxInstall 86Box's build dependencies. On Debian 13:
sudo apt install build-essential cmake ninja-build pkg-config \
qt6-base-dev qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \
libqt6opengl6-dev qt6-qpa-plugins libvulkan-dev libgl-dev libsdl3-dev libpng-dev \
libfreetype-dev libopenal-dev librtmidi-dev libfluidsynth-dev libsndfile1-dev \
libslirp-dev libserialport-dev libevdev-dev libxkbcommon-x11-dev libx11-dev \
libwayland-dev libzstd-dev libedit-dev libxcb-cursor0 mtoolsThen build with the automation socket enabled:
cmake -G Ninja -S 86box -B 86box/build-mcp -D CMAKE_BUILD_TYPE=Release \
-D QT=ON -D USE_QT6=ON -D AUTOMATION=ON -D DISCORD=OFF -D MUNT=OFF -D SOUNDCANVAS=OFF
cmake --build 86box/build-mcpOn Ubuntu 24.04, which has no SDL3, install libsdl2-dev instead of
libsdl3-dev and add -D SDL2=ON. We've tested that build outside any
container: the test suite and the MCPDEMO driver's 34 checks pass. That test
unpacked Ubuntu's packages into a private prefix rather than installing them,
and it had no display, so it didn't try vm_start(window=True).
The server uses 86box/build-mcp/src/86Box automatically when that build has
the automation socket; set BOX86_BINARY to use a build somewhere else.
If you can't install those packages (no root, or a distribution without Qt6 or SDL3), Docker does the build and runs 86Box in a Debian container:
docker/build.sh # builds the image and 86Box into .cache/buildThe server uses the Docker wrapper docker/86box.sh whenever there's no
native build. One constraint: VM directories and media must be under $HOME
or /tmp, because those are the only paths mounted into the container.
This is how we develop it. Our build machine has no root access, so we can't
install the packages, and upstream's AppImage packaging was broken when we
tried it. The container gives a reproducible Debian toolchain on any host.
Any 86Box built with -D AUTOMATION=ON works; official releases are built
without it.
docker/windows/build.sh cross-compiles 86Box for 64-bit Windows in a
Fedora container (MinGW, Qt6, SDL3; details) and collects the DLLs into
.cache/win64/86Box-automation-win64/. Start it on Windows with
--automation tcp:7086. It writes a token file and serves on 127.0.0.1
unless you also pass --automation-allow-remote, so read
Choosing a transport first. Then connect from the MCP
server with vm_attach("tcp:<windows-ip>:7086", "<token>").
The MCP server also runs natively on Windows, next to a Windows 86Box. A
user has run it on Windows 11, without WSL, through its own VM class and
through Claude Code over MCP: boot, held steps, typing, the text screen,
screenshots, memory, registers and window=True. We haven't tested it on
Windows ourselves yet.
- 86Box with the automation socket: cross-compile it with
docker/windows/build.shas above, or build it in MSYS2 (MinGW64):cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DQT=ON -DAUTOMATION=ON. - The server:
py -3.12 -m venv server\.venv server\.venv\Scripts\python -m pip install -e server
- Register it with your agent, with
BOX86_BINARYpointing at86Box.exe, and with the folder holding its DLLs onPATH(for an MSYS2 build,C:\msys64\mingw64\bin). For example:claude mcp add 86box -e BOX86_BINARY=C:\86Box\86Box.exe ^ -e "PATH=C:\msys64\mingw64\bin;C:\Windows\System32;C:\Windows" ^ -- C:\86box-mcp\server\.venv\Scripts\box86mcp.exe
On Windows the server talks to 86Box over TCP on 127.0.0.1, with a token
(there are no UNIX sockets). vm_start shows the normal 86Box window by
default there; pass window=False to run headless. The
DOS disk tools (dos_create_vm, dos_install_files, dos_put_floppy) need
mtools (mformat, mcopy, mmd, mattrib) on PATH or in
BOX86_MTOOLS_DIR; MSYS2 has a mtools package. Recordings need ffmpeg
on PATH. python -m box86mcp.ctl listens on loopback TCP, with a token in
an owner-only file. The debugger tools need 86Box built with -DGDBSTUB=ON.
python3 -m venv server/.venv && server/.venv/bin/pip install -e 'server[test]'
claude mcp add 86box -- "$PWD/server/.venv/bin/box86mcp"The server's Python package is called box86mcp, not 86box_mcp, because
Python names can't start with a digit. The package, its command and its
BOX86_* settings all use that name. Agents see the server as 86box.
You also need the 86Box ROMs. Put them where
86Box looks for them, such as ~/.local/share/86Box/roms, or point
BOX86_ROMS at them.
For the DOS helpers you also need mtools on your PATH, and a bootable DOS
hard disk image to take the system files from: pass it to dos_create_vm or
set BOX86_DOS_BASE_IMG. Without one, install FreeDOS from its floppies as in
the examples.
The tests that boot DOS need that image too, as BOX86_TEST_BASE_IMG, and are
skipped without it.
Give your agent the skill. .claude/skills/86box
teaches an agent how to use the server well: when to run held or in real
time, how to install software, play real-time programs, compare 86Box builds,
use the debugger and work alongside V9x, and the usual pitfalls. Claude Code
picks it up by itself in this repo. Elsewhere, copy it into your skills:
cp -r .claude/skills/86box ~/.claude/skills/Then ask your agent something like:
Create a DOS VM in ~/vms/test, install ./build/GAME.EXE to C:\GAME, boot it, start the game and get to the main menu.
Everything below was driven through the MCP tools and recorded by 86Box
itself. The GIFs are short excerpts; python -m box86mcp.ctl render-timeline turns a
recording into a full video captioned with the MCP call in progress.
Claude's first real game in Windows 3.11. The yellow line narrates its genuine
mistakes as they happen. It aims at a menu inside the corner where mouse_to
parks the pointer. A drag gets thrown off the window by Windows' mouse
acceleration. mouse_to chases the game clock. Finally, a strategy mistake
loses the game (score 8).
After researching Klondike strategy (uncover hidden cards first; only empty a column for a king that unlocks something), it wins with a score of 2600. Most of that is Solitaire's time bonus. In held mode the game clock only runs while the agent acts, not while it thinks.
Next, Claude was asked for the highest score it could get. It wrote a bot,
examples/solitaire, on top of this project's Python
client, and it scored 24,024. The best possible is about 24,072. A human
needs minutes to play a game, so typical scores are a few thousand.
How the score works: Windows 3.1 Solitaire adds a bonus of 700,000 ÷ seconds for a win, but only for wins of 30 s or more. Each move scores a few points. So the bot plays all 52 cards up as fast as the mouse allows, then holds the last card until Solitaire's own clock reads 30. Recycling the waste costs 100 points, so the best games never recycle.
The bot builds on two parts of this project:
- Held time: the emulator stops between actions, so recognising cards, checking the screen and choosing a move take no game time.
- Exact input: pointer acceleration is off in the VM, so one mouse count moves the pointer exactly one pixel. The bot can jump straight to a card, sending moves in pieces no faster than Windows reads the mouse.
The bot also:
- recognises cards from their corners (all 52 labelled once from contact sheets);
- keeps a model of the table, and checks it against the screen after every move;
- reads the score and clock off the status bar.
Building it took a few rounds of fixes. One fix was for a double-click that nobody meant: a click to turn a card over, then a grab at the same spot. Then it played 50 recorded games, won 19 and kept the two best. Two wins never recycled the waste, and both beat 24,000:
Run it with python examples/solitaire/play.py <games> [--minutes N]. The bot
needs a Windows 3.11 VM at 640×480 with Solitaire set to Draw One, the status
bar on, and the mouse at its slowest speed. The labelled card corners are in
examples/solitaire/lib. If your cards look different, solbot.py collect
gathers new corners on a contact sheet for labelling.
examples/skifree plays the original 16-bit SkiFree 1.0
in Windows 3.11. The bot reads the game's state from guest memory every tick
and plays with the keyboard while time is held.
In Freestyle it scored 32,767, the largest value SkiFree's 16-bit score can hold, and skied across the finish line into the High Scores box:
- Finding the rules: a decompilation of the later 32-bit version, checked against 1.0 in memory.
- The trick: a backscratcher scores 8 points for every tick in the air, and you can hop from a standstill. So the bot hops in place all the way down the course, mixing in uphill steps so the hops don't drift it off the end.
- The finish: it chose each tick's trick to land exactly on the limit.
Another run outran the Abominable Snowman. Turbo skiing is faster than he is, so the bot plans its lane a screen ahead to avoid crashing. It kept going until the distance wrapped round (y is a 16-bit number, and past 2,047 m the distance shows as negative), and came back onto the course for a second lap. Here it crashes just after the wrap, and gets up and away before he reaches it:
examples/arkanoid plays the 1989 DOS version of
Arkanoid II: Revenge of DoH on MS-DOS 6.22. Every frame (about 14 ms), the
bot reads the paddle, the balls, the falling capsules and the score from guest
memory, then steers with the keyboard while time is held. Its best game scored
386,920 and reached round 10:
- Prediction: it works out where each falling ball will meet the paddle, bouncing off the walls, and gets there early. The paddle is only about as fast as the ball.
- Aiming: where the ball hits the paddle sets its new angle. The bot cycles through eight hit positions, so successive bounces sweep the field and reach the last bricks.
- Capsules: these are worth 1,000 points each. The bot catches them while the ball is heading up, which more than tripled its scoring rate.
Two lessons from getting it going:
- The demo: after a game ends, the game's attract-mode demo takes over and plays itself, which is easy to mistake for the bot playing.
- The keys: the game only reads the keypad keys, not the extended arrow keys.
examples/mcpdemo is a small DOS game. The GIF shows
its driver testing keys, mouse_to, write_memory and timing, then a
~20-line bot playing with one act() call per frame. Every tick it publishes its
state to a block in conventional memory: player, enemy, coin and pointer
positions, score, lives, keys held. A pointer at 0000:04F0 says where the
block is. Tests read it with read_memory and wait_for_memory instead of
guessing from pixels, and can even post commands back into the block with
write_memory. The driver checks 34 exact results this way, including
movement per key press, mouse placement, COM1 logs and real-time vs held
timing. Publish a block like this in your own program and the same tools work
for it.
From a blank 512 MB disk: the installer partitions it; Award BIOS setup is
changed to boot from A: first; then format, six floppies, and a boot from C:.
Afterwards FreeDOS's CD driver reads a disc built by cdrom_insert.
On MS-DOS 6.22: Setup's DOS stage, then its GUI with the buttons clicked by
mouse_to. Three dialogs hide the pointer, so those took keys. Then WIN,
and a quick Solitaire.
examples/v9x installs Windows 98 and
V9x Remote Agent into one
VM, and drives it through both MCP servers at once.
crossover.py checks 17 combinations, such as
V9x opening a file in Notepad while find_memory finds its text in RAM. All
17 behaved as expected.
hwview.py takes two inventories of the PC and
matches them. 86box-mcp scans the PCI bus through the debugger, from outside.
V9x exports Windows' Plug and Play tree (HKLM\Enum), from inside. All four
PCI functions matched a Windows device at the same bus, device and function,
and the ISA PnP NE2000 was found as an RTL8019. The SB16, which isn't Plug and
Play, was never detected: Device Manager has no sound category. That's the
kind of mismatch to look for when you emulate a device. See
the example's README for the setup and
driver development for the debugger.
bench/ is a tool for 86Box developers. It runs one or more 86Box
builds through the same workload: Windows 98 boots, then 3DMark 99 MAX's full
benchmark runs on a 3dfx Voodoo 2. Each run starts from a pristine copy of a
prepared disk image, and the tool reports three things.
- Reliability: every phase finished, with no resets.
- Speed: emulated seconds per wall second. The boot and the benchmark run held, stepping as fast as the host allows, so the number isn't capped by real-time pacing.
- 3DMark's own results, which it measures in emulated time, so they reflect the emulation.
pytest bench/ turns a run into pass/fail checks for CI.
Comparing current master with a build from before the old recompiler's removal, it found:
- The older build crashes (
fatal("Over limit!")) when 3DMark's Result Browser starts, and runs the benchmark about 20 % slower. - On a fatal error the automation socket went silent instead of closing (86Box#8392).
- The emulated Voodoo 2 has no speed of its own: it runs as fast as 86Box's render threads draw on the host. Fill rate doubles with twice the threads, while CPU 3DMarks stays within 0.2 %. So the Voodoo-bound results benchmark 86Box's renderer on the host, rather than the emulated card.
See bench/README.md.
These work with any guest OS:
| Tool | What it does |
|---|---|
vm_create, vm_start, vm_stop, vm_status, vm_info |
New VM with a blank disk (512 MB CompactFlash geometry by default), as a 486DX2-66 or a copy of any 86box.cfg you pass as config, with an optional saved CMOS (nvr); boot (held by default; window=True to watch; without vm_dir, the last VM started, or $BOX86_VM_DIR); power off, or press the power button and let the guest shut down (soft=True); status; machine, CPU, RAM, media, speed and reset counts (a triple fault shows up there) |
hold, release, step(ms), fast_forward |
Freeze time, run in real time, or run exactly ms of emulated time; fast_forward(True) lets a released machine run as fast as the host allows |
measure_speed(seconds, paced) |
Emulated seconds per wall second: paced (does it keep up with real time?) or unpaced (throughput, to compare 86Box builds, each started with vm_start(binary=...)) |
act(hold_keys, ms) |
Hold keys for ms, release, return the frame: one game turn |
press_keys, type_text |
Keyboard |
mouse, mouse_to(x, y), mouse_drag |
Relative motion; put the pointer at a screen position (closing the loop on where it lands); drag |
screenshot, screen_text, wait_for_text |
The frame; text-mode screen as text; run until text appears |
read_memory, write_memory, wait_for_memory, registers |
Physical memory, limited to the emulated RAM (vm_info reports mem_kb), and CPU registers |
find_memory, snapshot_memory, diff_memory |
Search memory for bytes, text or a number; snapshot it, then keep only the addresses that changed the way you expect (increased, decreased, equal to a value) to find a game's variables |
serial_read, wait_for_serial |
Guest output on COM1 |
floppy_insert, floppy_eject, cdrom_insert, cdrom_eject |
Swap media while running; cdrom_insert can build an ISO from host files |
record_start, record_stop, reset |
Record a frame every N ms of emulated time; hard reset or Ctrl+Alt+Del |
breakpoint_set, run_until_break, step_instructions, debug_continue |
With vm_start(debugger=True) and 86Box built with -D GDBSTUB=ON: execution breakpoints and memory watchpoints, single instructions, all while held |
io_read, io_write, pci_scan, pci_config_read |
Also with the debugger: port I/O as the CPU would do it, and PCI configuration space |
DOS helpers assume DOS and FAT disks: dos_create_vm (VM with a bootable DOS
disk; takes config and nvr like vm_create), dos_install_files (copy files of any size onto C: while stopped),
dos_put_floppy (hot-insert a floppy with some files) and dos_run (type a
command and wait for the prompt).
The debugger tools are for developing drivers and device emulation; see docs/driver-development.md, which also lists what else would help and what it would take in 86Box.
python -m box86mcp.ctl drives the server from a shell, one call at a time,
and renders recordings into captioned videos; that's how the examples were
made.
Learned the hard way, as the first Solitaire game shows:
- Turn off mouse acceleration first: Control Panel > Mouse > Mouse Tracking Speed to Slow (with the keyboard: End, not Home). Otherwise the pointer gain depends on speed and drags overshoot.
- Keep the screen still while
mouse_tomeasures. It finds the pointer by comparing frames and ignores blinking carets. A clock ticking once a second (Solitaire's status bar) can still fool it, so hide it. - Use the keyboard for the top-left 32 px, where
mouse_toparks the pointer, and for dialogs that hide the pointer (Windows Setup has some). - Double-click with two quick
mouse(left=True, hold_ms=40)calls.
Both projects let an agent drive a vintage PC over MCP. They sit at different layers, and each is strong where the other is weak.
| 86box-mcp | V9x Remote Agent | |
|---|---|---|
| Where it runs | Outside the machine, inside 86Box | Agent program inside the guest OS |
| Guest software needed | None | Agent installed in the guest, plus a network card |
| Guest OS | Any, including bare DOS, games, BIOS setup and OS installers | Windows 95/98/Me/2000 (DOS build: commands and files only) |
| Real retro hardware | No (patched 86Box only) | Yes |
| Emulated time | Hold, exact steps, turn-based play | Runs in real time |
| Screenshots and input | Any video mode, hardware-level input | Windows desktop (needs Explorer) |
| Commands, exit codes, file transfer | Typed at the DOS prompt; files via disk images | Native: run with exit codes, CRC-verified transfer, reboot handling |
| Memory and registers | Yes | No |
| Host | MCP server on Linux; 86Box on Linux, or on Windows driven over TCP; macOS untested | Windows CLI; MCP server on Windows/macOS/Linux |
| License | GPL-2.0-or-later | BSD-2-Clause |
Rule of thumb: to develop or automate Windows 9x software, use V9x Remote Agent. For DOS games and programs, real-time or graphics-mode software, deterministic tests, OS installs, or anything that happens before an OS is up, use 86box-mcp.
They do combine: one Windows 98 VM runs the V9x agent, and an agent uses both
MCP servers side by side. examples/v9x has the setup steps
and crossover.py, which runs 17 crossover checks. All 17 behaved as
expected.
- Works together: V9x opens a file in Notepad and
find_memoryfinds its text in physical RAM.type_texttypes into a window and V9x saves and fetches the file.v9x_moveplaces the pointer,mousemoves it, and V9x reads it back.cdrom_insert(files=...)builds a disc and V9x lists it.reset(kind="hard")reboots andv9x_wait_desktopsees the new boot. The two screenshots agree, except for the pointer. - The catch is time. V9x is a TCP server inside the guest. While
86box-mcp holds the machine, nothing in the guest runs, so V9x calls time
out.
releasewhile you use V9x, or send a V9x call while astepruns: the call completes inside the step, and the machine stays held after it. - Pace V9x calls. The agent takes 4 connections at most, and calls sent while the machine is held pile up. See pacing.
- Two views of one machine:
hwview.pymatches 86box-mcp's PCI scan with Windows' own device tree, fetched by V9x. Every PCI function matched; the non-PnP SB16 was never detected. Details. - Don't overlap: 86box-mcp's disk tools need the VM stopped. Use
v9x_put_filewhile Windows runs, andscreen_textonly reads text modes.
If you're on DOSBox rather than 86Box, look at dosbox-mcp, which pairs an MCP bridge with a DOSBox fork. 86Box emulates specific period hardware (real chipsets, BIOSes, video and sound cards), which matters when you're testing software meant to run on an actual machine.
flowchart LR
agent["Agent<br/>(Claude Code, scripts)"] -- "MCP over stdio" --> server["box86mcp<br/>(Python)"]
server -- "UNIX socket,<br/>one line per command" --> socket["86Box automation socket<br/>(runs between 1 ms<br/>emulation quanta)"]
server -. "launches 86Box,<br/>builds disk, floppy<br/>and CD images" .-> socket
The automation socket (86Box --automation <path> [--automation-hold]) is a
line protocol you can also use from scripts or socat. See
doc/specifications/automation.md
in 86Box. docs/automation-code-guide.md walks
through the code, and TODO.md lists what's still open.
86Box serves the automation protocol on a UNIX domain socket or on TCP:
86Box --automation unix:/tmp/86box.sock ... # Linux, macOS
86Box --automation tcp:7086 --automation-token-file /tmp/86box.token ... # TCP on 127.0.0.1The MCP server picks for you: a UNIX socket on Linux and macOS, TCP on
Windows. Set BOX86_TRANSPORT=tcp or unix to override.
Use the UNIX socket unless you can't. It's owner-only, so only your user
can connect, and it isn't on the network at all. Any process on the machine
can connect to a TCP port, so TCP always requires a per-run random token,
written to an owner-only file, which the client sends first. It also binds
only to 127.0.0.1. 86Box refuses other addresses unless you add
--automation-allow-remote. Even then, think twice: the protocol is
unencrypted, and whoever reaches the port with the token fully controls the
emulated machine, memory included. The design follows
dosbox-automation's.
Details are in the protocol doc.
By default the MCP server talks to its client over stdio, as a child process.
For clients that connect over HTTP instead, set BOX86_MCP_HTTP_PORT: the
server then serves MCP's streamable HTTP transport at
http://127.0.0.1:PORT/mcp. For HTTPS, also set BOX86_MCP_TLS_CERT and
BOX86_MCP_TLS_KEY to a PEM certificate and its key:
BOX86_MCP_HTTP_PORT=8443 BOX86_MCP_TLS_CERT=cert.pem BOX86_MCP_TLS_KEY=key.pem box86mcp
# -> https://localhost:8443/mcpEither way it listens only on 127.0.0.1, with the MCP library's DNS-rebinding
protection (only localhost hosts and origins). There is no authentication:
any local process can connect and drive the VM, and through vm_create and
the disk tools, read and write files as you.
For a machine and network you trust, BOX86_INSECURE=1 drops the
protections that get in the way of simple setups, the way 86Box's GDB stub
already works:
- 86Box's automation socket over TCP (
BOX86_TRANSPORT=tcp, the default on Windows) runs without its token: the server starts 86Box with--automation-insecure, andvm_attachconnects to any 86Box started that way without one. Any local process can then drive the VM. - The MCP server's HTTP transport may listen on any address, set with
BOX86_MCP_HTTP_HOST(for example0.0.0.0), without the DNS-rebinding checks. Anyone who can reach that port can drive the VM and, through the server's tools, read and write files on this machine as you. HTTPS (above) encrypts the connection, but doesn't authenticate the client.
Without BOX86_INSECURE=1, a non-loopback BOX86_MCP_HTTP_HOST is refused.
--automation-insecure needs an 86Box with
86Box#8397.
This is an early release, built for developing our own DOS games and tools.
- 86Box must be built with
-D AUTOMATION=ON. The automation socket is in upstream 86Box, but official releases are built without it. - Windows: best effort, untested by us. 86Box with the automation socket
has been built for 64-bit Windows and driven over TCP from Linux with
vm_attach. The MCP server itself has been run natively on Windows 11 by a user (see Or run everything on Windows), and its Windows code paths (stopping 86Box,window=True, thectlCLI) follow that report. The disk tools, the debugger and the stop fallback haven't run on Windows. macOS hasn't been tried at all. - Mouse input is relative. (See Tips for Windows 3.x.) DOS and Windows 3.x drivers only understand
relative mice, so
mouse_tocloses the loop. It reads the pointer from memory if your program publishes it, or finds it on screen. On screen it needs the pointer to be visible: some programs hide it (Windows 3.11 Setup does in its keyboard-driven dialogs), and busy animation can mask it. 86Box's absolute devices (tablets, touchscreens) need guest drivers that these systems don't ship. - The debugger needs 86Box fixes that aren't upstream yet: 86Box#8390 and 86Box#8391. Without them, the GDB stub can't answer while the machine is held, and memory watchpoints on RAM don't fire. See docs/driver-development.md.
- COM1 output only. The guest can't read serial input yet. Capturing COM1
(
vm_start(com1_log=True)) is off by default. It used to look like it made runs occasionally non-reproducible (about 2 in 6). A likelier cause has since turned up: disk images that recorded the host's clock. With that fixed, 40 of 40 runs with COM1 capture on were identical.
GPL-2.0-or-later, the same as 86Box.












