{"slug": "show-hn-wiphone-lab-doom-on-an-open-esp32-wi-fi-phone", "title": "Show HN: WiPhone Lab – Doom on an Open ESP32 Wi-Fi Phone", "summary": "An unofficial fork of the WiPhone open-source phone firmware (v0.8.30) adds a System Benchmark, test harness, and ports of Doom and Gravity Defied to the ESP32-based Wi-Fi phone, with most of the fork written by AI agents using hardware-in-the-loop testing. The fork keeps full telephony intact and runs on the ESP32-D0WDQ6-V3 with 520 KiB SRAM and 4 MiB PSRAM, achieving ~22 FPS for Gravity Defied and ~13.6 FPS for Doom at 320×240 resolution.", "body_md": "An unofficial fork of the **WiPhone** open-source phone firmware (v0.8.30) that\nkeeps the full phone working and adds a reproducible **System Benchmark**, an\non-device **test harness**, and optional **Doom** and **Gravity Defied** ports.\n\nNot affiliated with or endorsed by MZJ Technology / HackEDA. The very first\ncommit, *\"WiPhone 0.8.30 original firmware\"*, is their pristine upstream\nrelease; every commit after it is this fork's work.\n\nHow 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\n\n`@WP1`\n\nharness, 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.\n\n[WiPhone](https://wiphone.io) is an open-source, hackable mobile phone built\naround an **ESP32**. It places calls over Wi‑Fi using **SIP/VoIP**, and the\nfirmware is a single Arduino sketch (`WiPhone.ino`\n\n).\n\nTarget hardware / toolchain:\n\n| SoC | ESP32‑D0WDQ6‑V3, dual‑core Xtensa LX6 @ 240 MHz |\n| RAM | 520 KiB internal SRAM + 4 MiB PSRAM (mapped, 40 MHz) |\n| Display | ST7789 240×320 over VSPI, driven by `TFT_eSPI` |\n| Audio | WM8750 codec over I²S |\n| Radios | Wi‑Fi (SIP/VoIP), optional LoRa messaging |\n| Build | Arduino‑ESP32 1.0.5 (pinned), ESP‑IDF 3.3.4 |\n\nStock firmware features (telephony, SIP, address book, messaging, settings,\netc.) are unchanged and documented in `CHANGELOG.txt`\n\n.\n\nEverything below sits **on top of** the stock 0.8.30 firmware; telephony is left\nfully intact. The additions are reachable from the phone's menus.\n\nA port of the classic J2ME motorcycle physics game.\n\n- Deterministic fixed‑point (Q16.16) physics, level loader and renderer.\n- Runs at\n**~22 FPS**(a fixed 45 ms frame period) using the shared raw‑VSPI async DMA display pipeline. - Includes an in‑app deterministic benchmark.\n\nA ride with the in-game FPS/LCD overlay enabled, on the device:\n\n## gravity-defied-ride.mp4\n\nA playable Doom port based on **FPDoom**.\n\n- Full‑screen\n**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`\n\nemulator 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.\n\nGameplay recorded off the device, with sound for the OPL music:\n\n## doom-gameplay.mp4\n\nA raw‑VSPI worker (core 0) that converts indexed frames to RGB565 and streams\nthem in ping‑pong DMA chunks, overlapping the game's next‑frame render. Used by\nboth Gravity Defied and Doom; it leases the VSPI bus from `TFT_eSPI`\n\nand returns\nit cleanly on exit.\n\nA production diagnostics app that takes a **reproducible** performance and\ncorrectness snapshot of the whole environment (CPU on both cores; integer,\nfixed‑point, `float`\n\nand `double`\n\nkernels; internal RAM and PSRAM; audio codecs;\nrenderer; completed display transfers; flash reads). Deterministic workloads are\nguarded by frozen CRC32 values and scored against an explicitly saved baseline,\nso an optimization can be told apart from a regression or a broken computation.\n\n**On‑device harness**(`TestHarness.cpp/.h`\n\n, compiled only in harness builds): a line‑based`@WP1`\n\nUART protocol for automated testing, driven by the Python host client`tools/harness/wiphone_harness.py`\n\n.**Reproducible builds**(`tools/system_benchmark/build.py`\n\n): produces`regular`\n\n/`harness`\n\nfirmware with a content‑addressed**Build ID**(`B1-<sha256[:16]>`\n\n) and a full manifest, for provenance and stack‑trace‑able releases.\n\n```\nWiPhone.ino, *.cpp, *.h       Stock WiPhone firmware (GUI, SIP, audio, storage…)\nsrc/gravity_defied/           Gravity Defied game + in-app benchmark\nsrc/doom/                     Doom port; src/doom/engine/ is the FPDoom engine\nsrc/display/                  Shared raw-VSPI async DMA display pipeline\nsrc/system_benchmark/         System Benchmark app (kernels, engine, storage, UI)\nsrc/audio/, src/drivers/,     Codecs (G.711/G.722), device drivers,\nsrc/TFT_eSPI/, src/…          vendored/adapted libraries\ntools/                        Host-side tooling: build, harness, asset generation,\n                              screenshots, icon/font converters\nTestHarness.cpp/.h            Harness-only UART automation (@WP1 protocol)\nCHANGELOG.txt                 Upstream WiPhone changelog\n```\n\n**Arduino‑ESP32** core (pinned) — newer cores are`1.0.5`\n\n*not*compatible.**arduino‑cli**(used by the build script) or the Arduino IDE.`1.5.1`\n\n- Board:\n**ESP32 Wrover Module**(`esp32wrover`\n\n) — PSRAM must be enabled. - Library:\n**RadioHead**(`1.120`\n\n, LoRa in`lora.cpp`\n\n).`TFT_eSPI`\n\nis vendored in`src/TFT_eSPI/`\n\n; other libraries come with the ESP32 core. - For flashing:\n`esptool.py`\n\n(`3.0.0`\n\nused here) and a USB‑UART link to the phone (CP2104), e.g.`/dev/ttyUSB0`\n\n.\n\nBoard / compiler identity (FQBN):\n\n```\nesp32:esp32:esp32wrover:FlashFreq=80,FlashMode=dio,PartitionScheme=default,DebugLevel=none\n```\n\nThe project also compiles with `-fsigned-char -fno-common -fwrapv -fno-strict-overflow`\n\n, and **Doom** additionally requires the PSRAM linker\nscript `src/doom/wiphone_doom_psram.ld`\n\n(via `-Wl,--wrap=esp_spiram_add_to_heapalloc`\n\n)\nplus `-DWIPHONE_DOOM_EMBEDDED_SFX`\n\n. These are wired automatically by the build\nscript below.\n\n`tools/system_benchmark/build.py`\n\napplies the Doom PSRAM linker script, pins the\ntoolchain, and emits a content‑addressed **Build ID** and a full manifest\n(`.elf`\n\n/`.map`\n\nfor later stack traces):\n\nPhone firmware only, without the game ports:\n\n```\npython3 tools/system_benchmark/build.py --output <artifact-dir> --flavor regular\n#   --flavor harness   also build the @WP1 UART test firmware\n#   --flavor both      build regular + harness\n#   --no-distrobox     run the host arduino-cli directly\n#                      (default runs it inside a distrobox container \"wi-phone\")\n```\n\nWith Doom and Gravity Defied, and with Doom's sound:\n\n```\npython3 tools/system_benchmark/build.py --output <artifact-dir> --flavor regular \\\n    --games --doom-sfx-wad /path/to/doom-with-audio.wad\n```\n\nThey are an optional proof of concept guarded by`--games`\n\nis required for the game ports.`BUILD_GAMES`\n\n, so a default build contains none of their code — about 1.2 MiB smaller, 59% of the app partition against 98%.\n\n, and only works together with`--doom-sfx-wad`\n\nis what gives Doom sound`--games`\n\n. It generates the embedded SFX and OPL music tables from a WAD that still contains its audio lumps. Without it`WIPHONE_DOOM_EMBEDDED_SFX`\n\nstays 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).\n\nResult: `<artifact-dir>/regular/WiPhone.ino.bin`\n\n(+ `.elf`\n\n, `.map`\n\n,\n`build-manifest.json`\n\n) and a Build ID like `B1-EA6254DAB017912D`\n\n.\n\nOpen `WiPhone.ino`\n\n, select **ESP32 Wrover Module**, set Flash 80 MHz / DIO,\ndefault partition scheme, PSRAM **enabled**, then Verify/Upload. This builds the\nphone firmware alone. The game ports need `BUILD_GAMES`\n\n(uncomment it in\n`config.h`\n\n), and a correct **Doom** build additionally needs the custom\nlinker/flags above — so use Option A when you want the games.\n\nWrite the app image to `app0 @ 0x10000`\n\n. The bootloader, partition table, NVS\nand other partitions are left untouched:\n\n```\nesptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 \\\n  --before default_reset --after hard_reset write_flash -z \\\n  --flash_mode dio --flash_freq 80m --flash_size 16MB \\\n  0x10000 <artifact-dir>/regular/WiPhone.ino.bin\n```\n\nThe app image always ends before `0x300000`\n\n, so flashing the firmware never\ntouches the Doom IWAD region below. That bound is enforced at build time:\n`build.py`\n\npasses `upload.maximum_size`\n\n= `0x300000 - 0x10000`\n\n, so an app that\nwould reach the WAD fails to link instead of silently overwriting it. A sound\nbuild currently uses about 98–99% of that budget.\n\nFor licensing reasons no IWAD ships in this repository. The firmware expects a\n**specific** IWAD (fixed size and SHA‑256, pinned in `src/doom/DoomBuildConfig.h`\n\n)\nand reads it from flash at ** 0x300000** — inside the\n\n`app0`\n\nregion, after the\napp. Provide that IWAD yourself and flash it there:\n\n```\nesptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 \\\n  write_flash 0x300000 YOUR_IWAD.wad\n```\n\nWithout a matching WAD the phone and Gravity Defied still work; Doom simply reports that it cannot mount the WAD.\n\nThe toolchain is intentionally pinned to Arduino‑ESP32 1.0.5. Its precompiled SDK is a hard constraint for some optimizations (see\n\nPerformance notes).\n\nFull‑screen Doom is bound by writing the framebuffer, which lives in PSRAM\nclocked at **40 MHz** (`CONFIG_SPIRAM_SPEED_40M`\n\nin the frozen 1.0.5 SDK). The\ntwo obvious speed‑ups are both blocked by that SDK:\n\n- an\n**internal‑SRAM framebuffer** doesn't fit — Wi‑Fi's static RX buffers are never freed (`esp_wifi_deinit`\n\nis 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.\n\nMeasured full‑screen FPS, full detail:\n\n| Configuration | FPS |\n|---|---|\n| Shipped: 40 MHz display clock, sound on | ~13.6 |\n| Same build with sound disabled | ~15.3 |\n\nThe shipped default is **full quality at ~13.6 FPS**; the SFX/OPL mixer task\ncosts the difference.\n\nBoth games drive the panel through the shared raw‑VSPI pipeline at **40 MHz**.\nAn earlier revision used 80 MHz, which is outside what this ST7789 tolerates\nhere: it corrupted the panel's address state mid‑frame, producing torn and\nduplicated scanlines and, in the worst case, a display that stayed white while\nDMA still reported every transfer as completed — the engine, CRCs and telemetry\nall looked healthy. Both Doom and Gravity Defied were affected. Dropping to\n40 MHz cleared it in both, and cost no frame rate: Gravity is bound by its fixed\n45 ms frame period, and Doom measured slightly *faster* than it had at 80 MHz.\n`FullScreenDmaTransfer`\n\nnow rejects any request above 40 MHz.\n\nThe phone firmware and the optional game ports are separate bodies of code under\ndifferent licenses. The games are a proof of concept: they link to the firmware's\ninterfaces but are not part of it, and they are excluded from the build unless\n`BUILD_GAMES`\n\nis defined (see `config.h`\n\n).\n\n**Phone firmware**\n\n- © MZJ Technology / HackEDA, under the\n**WiPhone Public License v1.0**(`WiPhone_Public_License_v1.0.txt`\n\n, 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/`\n\n— TFT_eSPI by Bodmer, FreeBSD licence (`src/TFT_eSPI/license.txt`\n\n).`FairyMax.h`\n\n— Fairy-Max by H.G. Muller, released into the public domain.`src/audio/`\n\n— G.711 (ITU-T reference) and G.722 by Steve Underwood.\n\n**Doom — src/doom/, GPL‑2.0**\n\n- Engine: id Software's\n, released 1997‑12‑23 and relicensed by id under the`linuxdoom-1.10`\n\n**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/`\n\n. - Port:\n**FPDoom**(`tools/fpdoom/vendor/fpdoom/`\n\n), released into the public domain under the Unlicense. - Music:\n`opl.c`\n\n,`opl_queue.c`\n\n,`oplplayer.c`\n\n,`midifile.c`\n\nfrom**Chocolate Doom**© Simon Howard, GPL‑2.0;`mus2mid.c`\n\nand`lprintf.h`\n\nfrom**PrBoom**, GPL‑2.0;`dbopl.c`\n\nis the**DOSBox** OPL2/OPL3 emulator © The DOSBox Team, GPL‑2.0 or later. - No Doom game data ships here — you supply your own IWAD.\n\n**Gravity Defied — src/gravity_defied/, GPL‑2.0**\n\n- Port derived from\nby rgimad, AntonEvmenenko and Max Logaev, GPL‑2.0.[gravity_defied_cpp](https://github.com/rgimad/gravity_defied_cpp) - The original\n*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.\n\n**Tooling**\n\n`tools/prboom/`\n\nis a standalone reference benchmark of Retro‑Go's PrBoom 2.5.0 (GPL‑2.0). It is not compiled into the firmware.", "url": "https://wpnews.pro/news/show-hn-wiphone-lab-doom-on-an-open-esp32-wi-fi-phone", "canonical_source": "https://github.com/mulfyx/wiphone-lab", "published_at": "2026-07-30 19:10:48+00:00", "updated_at": "2026-07-30 19:22:19.865263+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-tools", "developer-tools"], "entities": ["WiPhone", "ESP32", "MZJ Technology", "HackEDA", "FPDoom", "TFT_eSPI", "WM8750", "Arduino-ESP32"], "alternates": {"html": "https://wpnews.pro/news/show-hn-wiphone-lab-doom-on-an-open-esp32-wi-fi-phone", "markdown": "https://wpnews.pro/news/show-hn-wiphone-lab-doom-on-an-open-esp32-wi-fi-phone.md", "text": "https://wpnews.pro/news/show-hn-wiphone-lab-doom-on-an-open-esp32-wi-fi-phone.txt", "jsonld": "https://wpnews.pro/news/show-hn-wiphone-lab-doom-on-an-open-esp32-wi-fi-phone.jsonld"}}