# Show HN: WiPhone Lab – Doom on an Open ESP32 Wi-Fi Phone

> Source: <https://github.com/mulfyx/wiphone-lab>
> Published: 2026-07-30 19:10:48+00:00

An unofficial fork of the **WiPhone** open-source phone firmware (v0.8.30) that
keeps the full phone working and adds a reproducible **System Benchmark**, an
on-device **test harness**, and optional **Doom** and **Gravity Defied** ports.

Not affiliated with or endorsed by MZJ Technology / HackEDA. The very first
commit, *"WiPhone 0.8.30 original firmware"*, is their pristine upstream
release; every commit after it is this fork's work.

How this was built.Most of this fork was written by AI agents — the game ports, the shared display pipeline, the benchmark and the harness itself.The work was hardware-in-the-loop rather than code review on faith: the agents built firmware, flashed it to the phone, drove it over UART through the

`@WP1`

harness, and read back telemetry and frame CRCs from the running device. Most of the numbers in this README were measured that way, and more than one conclusion came from watching the panel disagree with what the firmware believed it had drawn — seeDisplay clockbelow for the worst of them.

[WiPhone](https://wiphone.io) is an open-source, hackable mobile phone built
around an **ESP32**. It places calls over Wi‑Fi using **SIP/VoIP**, and the
firmware is a single Arduino sketch (`WiPhone.ino`

).

Target hardware / toolchain:

| SoC | ESP32‑D0WDQ6‑V3, dual‑core Xtensa LX6 @ 240 MHz |
| RAM | 520 KiB internal SRAM + 4 MiB PSRAM (mapped, 40 MHz) |
| Display | ST7789 240×320 over VSPI, driven by `TFT_eSPI` |
| Audio | WM8750 codec over I²S |
| Radios | Wi‑Fi (SIP/VoIP), optional LoRa messaging |
| Build | Arduino‑ESP32 1.0.5 (pinned), ESP‑IDF 3.3.4 |

Stock firmware features (telephony, SIP, address book, messaging, settings,
etc.) are unchanged and documented in `CHANGELOG.txt`

.

Everything below sits **on top of** the stock 0.8.30 firmware; telephony is left
fully intact. The additions are reachable from the phone's menus.

A port of the classic J2ME motorcycle physics game.

- Deterministic fixed‑point (Q16.16) physics, level loader and renderer.
- Runs at
**~22 FPS**(a fixed 45 ms frame period) using the shared raw‑VSPI async DMA display pipeline. - Includes an in‑app deterministic benchmark.

A ride with the in-game FPS/LCD overlay enabled, on the device:

## gravity-defied-ride.mp4

A playable Doom port based on **FPDoom**.

- Full‑screen
**320×240** landscape output via the async DMA pipeline (**~13.6 FPS** at full quality with sound — see*Performance notes*). **Sound:** a real SFX mixer plus**OPL2 music**(DOSBox`dbopl`

emulator with GENMIDI instruments, MUS→MIDI conversion). SFX and music are mixed and played through the shared WM8750 codec/I²S using a snapshot/restore audio**lease** that restores the exact telephony audio state on exit.- The Doom zone and most engine buffers live in PSRAM to fit alongside the phone firmware without running the internal heap out of memory.

Gameplay recorded off the device, with sound for the OPL music:

## doom-gameplay.mp4

A raw‑VSPI worker (core 0) that converts indexed frames to RGB565 and streams
them in ping‑pong DMA chunks, overlapping the game's next‑frame render. Used by
both Gravity Defied and Doom; it leases the VSPI bus from `TFT_eSPI`

and returns
it cleanly on exit.

A production diagnostics app that takes a **reproducible** performance and
correctness snapshot of the whole environment (CPU on both cores; integer,
fixed‑point, `float`

and `double`

kernels; internal RAM and PSRAM; audio codecs;
renderer; completed display transfers; flash reads). Deterministic workloads are
guarded by frozen CRC32 values and scored against an explicitly saved baseline,
so an optimization can be told apart from a regression or a broken computation.

**On‑device harness**(`TestHarness.cpp/.h`

, compiled only in harness builds): a line‑based`@WP1`

UART protocol for automated testing, driven by the Python host client`tools/harness/wiphone_harness.py`

.**Reproducible builds**(`tools/system_benchmark/build.py`

): produces`regular`

/`harness`

firmware with a content‑addressed**Build ID**(`B1-<sha256[:16]>`

) and a full manifest, for provenance and stack‑trace‑able releases.

```
WiPhone.ino, *.cpp, *.h       Stock WiPhone firmware (GUI, SIP, audio, storage…)
src/gravity_defied/           Gravity Defied game + in-app benchmark
src/doom/                     Doom port; src/doom/engine/ is the FPDoom engine
src/display/                  Shared raw-VSPI async DMA display pipeline
src/system_benchmark/         System Benchmark app (kernels, engine, storage, UI)
src/audio/, src/drivers/,     Codecs (G.711/G.722), device drivers,
src/TFT_eSPI/, src/…          vendored/adapted libraries
tools/                        Host-side tooling: build, harness, asset generation,
                              screenshots, icon/font converters
TestHarness.cpp/.h            Harness-only UART automation (@WP1 protocol)
CHANGELOG.txt                 Upstream WiPhone changelog
```

**Arduino‑ESP32** core (pinned) — newer cores are`1.0.5`

*not*compatible.**arduino‑cli**(used by the build script) or the Arduino IDE.`1.5.1`

- Board:
**ESP32 Wrover Module**(`esp32wrover`

) — PSRAM must be enabled. - Library:
**RadioHead**(`1.120`

, LoRa in`lora.cpp`

).`TFT_eSPI`

is vendored in`src/TFT_eSPI/`

; other libraries come with the ESP32 core. - For flashing:
`esptool.py`

(`3.0.0`

used here) and a USB‑UART link to the phone (CP2104), e.g.`/dev/ttyUSB0`

.

Board / compiler identity (FQBN):

```
esp32:esp32:esp32wrover:FlashFreq=80,FlashMode=dio,PartitionScheme=default,DebugLevel=none
```

The project also compiles with `-fsigned-char -fno-common -fwrapv -fno-strict-overflow`

, and **Doom** additionally requires the PSRAM linker
script `src/doom/wiphone_doom_psram.ld`

(via `-Wl,--wrap=esp_spiram_add_to_heapalloc`

)
plus `-DWIPHONE_DOOM_EMBEDDED_SFX`

. These are wired automatically by the build
script below.

`tools/system_benchmark/build.py`

applies the Doom PSRAM linker script, pins the
toolchain, and emits a content‑addressed **Build ID** and a full manifest
(`.elf`

/`.map`

for later stack traces):

Phone firmware only, without the game ports:

```
python3 tools/system_benchmark/build.py --output <artifact-dir> --flavor regular
#   --flavor harness   also build the @WP1 UART test firmware
#   --flavor both      build regular + harness
#   --no-distrobox     run the host arduino-cli directly
#                      (default runs it inside a distrobox container "wi-phone")
```

With Doom and Gravity Defied, and with Doom's sound:

```
python3 tools/system_benchmark/build.py --output <artifact-dir> --flavor regular \
    --games --doom-sfx-wad /path/to/doom-with-audio.wad
```

They are an optional proof of concept guarded by`--games`

is required for the game ports.`BUILD_GAMES`

, so a default build contains none of their code — about 1.2 MiB smaller, 59% of the app partition against 98%.

, and only works together with`--doom-sfx-wad`

is what gives Doom sound`--games`

. It generates the embedded SFX and OPL music tables from a WAD that still contains its audio lumps. Without it`WIPHONE_DOOM_EMBEDDED_SFX`

stays undefined, the sound table is empty, and the mixer task never starts — Doom then runs completely silent, with no error. It also costs about 780 KiB of flash, so a sound build sits near the app size limit (seeDoom IWADbelow).

Result: `<artifact-dir>/regular/WiPhone.ino.bin`

(+ `.elf`

, `.map`

,
`build-manifest.json`

) and a Build ID like `B1-EA6254DAB017912D`

.

Open `WiPhone.ino`

, select **ESP32 Wrover Module**, set Flash 80 MHz / DIO,
default partition scheme, PSRAM **enabled**, then Verify/Upload. This builds the
phone firmware alone. The game ports need `BUILD_GAMES`

(uncomment it in
`config.h`

), and a correct **Doom** build additionally needs the custom
linker/flags above — so use Option A when you want the games.

Write the app image to `app0 @ 0x10000`

. The bootloader, partition table, NVS
and other partitions are left untouched:

```
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 \
  --before default_reset --after hard_reset write_flash -z \
  --flash_mode dio --flash_freq 80m --flash_size 16MB \
  0x10000 <artifact-dir>/regular/WiPhone.ino.bin
```

The app image always ends before `0x300000`

, so flashing the firmware never
touches the Doom IWAD region below. That bound is enforced at build time:
`build.py`

passes `upload.maximum_size`

= `0x300000 - 0x10000`

, so an app that
would reach the WAD fails to link instead of silently overwriting it. A sound
build currently uses about 98–99% of that budget.

For licensing reasons no IWAD ships in this repository. The firmware expects a
**specific** IWAD (fixed size and SHA‑256, pinned in `src/doom/DoomBuildConfig.h`

)
and reads it from flash at ** 0x300000** — inside the

`app0`

region, after the
app. Provide that IWAD yourself and flash it there:

```
esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 \
  write_flash 0x300000 YOUR_IWAD.wad
```

Without a matching WAD the phone and Gravity Defied still work; Doom simply reports that it cannot mount the WAD.

The toolchain is intentionally pinned to Arduino‑ESP32 1.0.5. Its precompiled SDK is a hard constraint for some optimizations (see

Performance notes).

Full‑screen Doom is bound by writing the framebuffer, which lives in PSRAM
clocked at **40 MHz** (`CONFIG_SPIRAM_SPEED_40M`

in the frozen 1.0.5 SDK). The
two obvious speed‑ups are both blocked by that SDK:

- an
**internal‑SRAM framebuffer** doesn't fit — Wi‑Fi's static RX buffers are never freed (`esp_wifi_deinit`

is commented out in Arduino‑ESP32 1.0.5) and the largest contiguous internal block stays below the ~77 KiB needed; **80 MHz PSRAM** is not selectable without recompiling the SDK.

Measured full‑screen FPS, full detail:

| Configuration | FPS |
|---|---|
| Shipped: 40 MHz display clock, sound on | ~13.6 |
| Same build with sound disabled | ~15.3 |

The shipped default is **full quality at ~13.6 FPS**; the SFX/OPL mixer task
costs the difference.

Both games drive the panel through the shared raw‑VSPI pipeline at **40 MHz**.
An earlier revision used 80 MHz, which is outside what this ST7789 tolerates
here: it corrupted the panel's address state mid‑frame, producing torn and
duplicated scanlines and, in the worst case, a display that stayed white while
DMA still reported every transfer as completed — the engine, CRCs and telemetry
all looked healthy. Both Doom and Gravity Defied were affected. Dropping to
40 MHz cleared it in both, and cost no frame rate: Gravity is bound by its fixed
45 ms frame period, and Doom measured slightly *faster* than it had at 80 MHz.
`FullScreenDmaTransfer`

now rejects any request above 40 MHz.

The phone firmware and the optional game ports are separate bodies of code under
different licenses. The games are a proof of concept: they link to the firmware's
interfaces but are not part of it, and they are excluded from the build unless
`BUILD_GAMES`

is defined (see `config.h`

).

**Phone firmware**

- © MZJ Technology / HackEDA, under the
**WiPhone Public License v1.0**(`WiPhone_Public_License_v1.0.txt`

, also at[https://wiphone.io/WiPhone_Public_License_v1.0.txt](https://wiphone.io/WiPhone_Public_License_v1.0.txt)). Stock files modified by this fork carry a notice to that effect, as clause 3.0.4 requires. `src/TFT_eSPI/`

— TFT_eSPI by Bodmer, FreeBSD licence (`src/TFT_eSPI/license.txt`

).`FairyMax.h`

— Fairy-Max by H.G. Muller, released into the public domain.`src/audio/`

— G.711 (ITU-T reference) and G.722 by Steve Underwood.

**Doom — src/doom/, GPL‑2.0**

- Engine: id Software's
, released 1997‑12‑23 and relicensed by id under the`linuxdoom-1.10`

**GNU GPL v2**; ZeniMax applied GPL‑2.0 to the official repository on 2024‑01‑16, which settles the older Doom Source License headers still present in the 1997 files. Pinned copy in`tools/fpdoom/vendor/id-doom/`

. - Port:
**FPDoom**(`tools/fpdoom/vendor/fpdoom/`

), released into the public domain under the Unlicense. - Music:
`opl.c`

,`opl_queue.c`

,`oplplayer.c`

,`midifile.c`

from**Chocolate Doom**© Simon Howard, GPL‑2.0;`mus2mid.c`

and`lprintf.h`

from**PrBoom**, GPL‑2.0;`dbopl.c`

is the**DOSBox** OPL2/OPL3 emulator © The DOSBox Team, GPL‑2.0 or later. - No Doom game data ships here — you supply your own IWAD.

**Gravity Defied — src/gravity_defied/, GPL‑2.0**

- Port derived from
by rgimad, AntonEvmenenko and Max Logaev, GPL‑2.0.[gravity_defied_cpp](https://github.com/rgimad/gravity_defied_cpp) - The original
*Gravity Defied*(J2ME, 2004) and its name, logo, artwork and levels belong to**Codebrew Software**. This project is not associated with Codebrew Software, and the GPL‑2.0 above covers the port's code, not the original game's assets.

**Tooling**

`tools/prboom/`

is a standalone reference benchmark of Retro‑Go's PrBoom 2.5.0 (GPL‑2.0). It is not compiled into the firmware.
