A fork of hchunhui/tiny386 (a tiny,
from-scratch x86 PC emulator in C) that turns it into a headless retro PC you
reach from any web browser over Wi-Fi — no LCD panel required. The emulated
screen streams to an HTML <canvas> over a WebSocket, and you type and click
straight from the page. The board can even be its own Wi-Fi access point with a
captive portal, so a laptop or phone connects directly (no router) and the
browser pops open on the emulator automatically.
It runs DOS, EGA/CGA games (Commander Keen, Pac-Man, Digger), Windows 9x and Linux on an ESP32-S3 (8 MB PSRAM) — streamed live to your browser.
The original tiny386 documentation is preserved below, under Upstream: Tiny386.
A from-scratch streaming backend for boards with no display: an HTTP server
ships a small JS client, then pushes framebuffer deltas over a WebSocket to a
<canvas>; keyboard and pointer-lock mouse go back the other way. Open
http://<board>/ and you have the machine.
- Dirty-tile detection — a per-16×16-tile FNV hash; only changed tiles are re-encoded and sent, so a static screen costs ~0.
- Native-resolution rasterization (
STREAM_NATIVE) — render low-res modes at the guest's true size (e.g. 320×200) and let the browser upscale: ~4× less to rasterize, hash and send. - 4-bit palette encoding (
STREAM_PALETTE) — for ≤16-colour EGA/CGA, send packed 4-bit indices + a 16-colour palette instead of RGB565 (~4× fewer bytes); the browser expands them. - CGA native path — mode 04h (320×200 4-colour, used by Pac-Man/Digger) now takes the same native + 4-bit fast path as EGA (2-bpp decode + CGA interleaved scanline addressing), instead of falling back to full-panel RGB565.
- Scroll copy-rect BLIT (
STREAM_BLIT) — detect a CRTC start-address scroll and tell the browser todrawImage-shift the moved pixels instead of resending them (vertical, horizontal, diagonal, and 1-px pel-pan). - RLE run-length coding of tiles, adaptive frame pacing, a WS heartbeat, latest-wins frame drop under backpressure, and a double-buffered canvas so every frame presents atomically (no tile-by-tile tearing).
IN 0x3DAretrace-poll fast-path — the VGA status poll dominated scroll instructions; reading it directly (bypassing the I/O dispatch) gave +65% emulator throughput.- Retrace clock on core 1 — advancing the VGA retrace at the guest's instruction rate lifted the guest-perceived vsync from ~20 Hz to ~132 Hz, unthrottling scroll fps and smoothing motion.
- EGA display-address aperture-wrap and double-scan-width fixes (smooth scroll, no stripes / half-screen).
- x87 FPU fixes (tag word, reset/environment, 80→64-bit significand rounding).
- SoftAP "TINY386-WIFI" + captive portal (
-DWIFI_AP=on) — the board is its own open access point at192.168.4.1with a DNS-hijack + HTTP-redirect captive portal, so joining the network auto-opens the browser on the stream. - STA mode with a no-IP reconnect watchdog, open-auth support, and a pre-connect RSSI scan log; Wi-Fi tuned (modem-sleep off) for streaming throughput.
- Guest networking bridged to Wi-Fi via the NE2000 ↔ lwIP packet filter.
- USB Host Mass-Storage (pen drive) support (
-DUSB_MSC) for disk images. - PSRAM handed to the ESP heap (
CONFIG_SPIRAM_USE_MALLOC), fixing the internal-DRAM starvation that was the real streaming bottleneck. - "reset guest" web button — reboot the emulated PC from the browser (works even when a hung DOS game has halted the CPU), no power-cycle.
GET /benchlink-throughput probe, and serial perf logs (emulated-CPU Msteps/s, tiles/frame, KB/s).
# ESP-IDF 5.2.x, ESP32-S3 with 8 MB PSRAM
cd esp
idf.py -DBOARD=stream -DSTREAM_PALETTE=on -DSTREAM_BLIT=on -DUSB_MSC=on -DWIFI_AP=on build
idf.py -p <PORT> flashThen join Wi-Fi TINY386-WIFI (the captive portal opens the stream), or browse to the board's IP in STA mode.
Tiny386 is an x86 PC emulator written in C99. The highlight of the project is its portability. It now boots Windows 9x/NT on MCU such as ESP32-S3.
The core of the project is a built-from-scratch, simple and stupid i386 cpu emulator. Some features are missing, e.g. debugging, hardware tasking and some permission checks, but it should be able to run most 16/32 bit software. To boot modern linux kernel and windows, some 486 and 586 instrutions are added. The cpu emulator is kept in ~6K LOC. There is also an optional x87 fpu emulator.
To assemble a complete PC system, we have ported many peripherals from TinyEMU and QEMU, it now includes:
- 8259 PIC
- 8254 PIT
- 8042 Keyboard Controller
- CMOS RTC
- ISA VGA with Bochs VBE
- IDE Disk Controller
- NE2000 ISA Network Card
- 8257 ISA DMA
- PC Speaker
- Adlib OPL2 (optional)
- SoundBlaster 16
For firmware, the BIOS/VGABIOS comes from seabios. Tiny386 also supports booting linux kernel directly, without traditional BIOS. The idea comes from JSLinux, and it uses a small stub code called linuxstart.
See here
Linux (with rawdraw): You need to install libslirp libx11 and libasound2 first, then type make.
Linux (with SDL): You need to install libslirp SDL1.2 (or sdl12-compat) first, then type make USE_SDL=y.
For other platforms, please refer to .github/workflows/build.yml.
Pre-built binaries: here
- Prepare an ini file
[pc]
; set path to BIOS and VGA BIOS
bios = bios.bin
vga_bios = vgabios.bin
; set memory size and VGA memory size
mem_size = 32M
vga_mem_size = 2M
; fda/fdb for floppy disks (optional)
fda = floppy.img
; hda/hdb/hdc/hdd for hard disks (optional)
; cda/cdb/cdc/cdd for CD-ROM disks (optional)
hda = win95.img
cdb = win95_cd.iso
; "fill_cmos" fixes "MS-DOS compatibility mode" in win9x, but it breaks winNT...
fill_cmos = 1
; force 8-dot mode (640 pixel wide in text mode) if set to 1
vga_force_8dm = 0
[display]
width = 720
height = 480
[cpu]
; gen = 3/4/5/6, for 386/486/586/686
gen = 3
; fpu = 0/1, to disable/enable x87
fpu = 0- Run
./tiny386 config.ini
./tiny386 -kvm config.ini # run with KVM (build with `make USE_CPUABS=y`)For rawdraw and SDL port: Press "Ctrl + ]" to grab/ungrab the keyboard and mouse. Press "Ctrl + [" to show/hide OSD (On Screen Display). In OSD mode, the floppy/CD-ROM disk can be changed on the fly.
Supported boards:
With ESP-IDF 5.2.x:
- JC3248W535 (ESP32-S3, 480x320)
- Elecrow CrowPanel Advance 7.0" HMI (ESP32-S3, 800x480)
With ESP-IDF 6.0.x (experimental):
- JC4880P443 (ESP32-P4 Rev1.3 360MHz, 800x480)
You can find the pre-built flash image esp/flash_image_JC3248W535.bin from here.
The pre-built image can be flashed directly to offset 0.
Online flasher for esp chips: https://espressif.github.io/esptool-js
To build and flash manually:
scripts/build.sh patch_idf # apply patches to ESP-IDF 5.2.x
#scripts/build.sh patch_idf_60 # apply patches to ESP-IDF 6.0.x
make prepare
cd esp
idf.py -DBOARD=jc3248w535 update-dependencies build # or -DBOARD=elecrow7s3
idf.py flashAll files should be put in a SD card with FAT/exFAT file system. The ini file should be tiny386.ini and put in the root directory.
Please refer to esp/tiny386.ini.
Alternative usage: bios.bin vgabios.bin vmlinux.bin and linuxstart.bin can be put in corresponding flash partition. Other files can be put in the storage flash partition. Please refer to esp/partition.csv.
- Forward over WIFI
wifikbd is used to forward keyboard/mouse events to the dev board over WIFI:
(ESP32-S3 board: listen on TCP port 9999) <--- WIFI ---> AP <--- WIFI/Wire ---> (PC: ./wifikbd esp_board_addr 9999)
- USB hid (WIP)
See here.
Use "setup /im" to bypass memory check.
Use patcher9x.
Manually set the IRQ to 9(or 2).
Use fill_cmos = 0 in the config ini file.
The cpu emulator and the project as a whole are both licensed under the BSD-3-Clause license.
Adlib emulation is an optional part of the project, and it requires the library fmopl which is licensed under the LGPL.
Use make USE_FMOPL=n to build without adlib emulation.
SeaBIOS is distributed under the GNU LGPL-3 license.
Some parts ported from QEMU/TinyEMU are under the MIT license.