DOOM feasibility experiment
Measured on 2026-09-15. Real Freedoom E1M1 completed a scripted 700-tic host run with a 2 MiB DOOM zone. A Q port still needs a qualified mutable memory strategy and a combined firmware size measurement. DOOM is not included in the accepted 0.1.7GQ firmware.
The experiment used the native
doomgeneric engine
at dcb7a8dbc7a16ce3dda29382ac9aae9d77d21284 and
Freedoom 0.13.0,
the release linked by the official download page.
All source, assets, executables and captures remain in the ignored local
build/reviews/doom-feasibility/ directory. Nothing was installed on the Q
or written to its SD card by this experiment.
Host result
Section titled “Host result”The macOS ARM64 host adapter rendered 320×200 indexed frames, disabled sound and multiplayer, streamed the WAD through seek/read calls, and supplied a fixed sequence of movement, turning, firing and use events. It ran E1M1 at skill 2 through 700 game tics: 20 seconds of game time at DOOM’s 35 Hz rate. Enemies remained enabled. The player moved from the starting position, fired ammunition and finished with 85 health.
| Zone capacity | Result | Peak allocated zone bytes | Peak non-purgeable zone bytes | WAD bytes read |
|---|---|---|---|---|
| 1 MiB | Startup failed before the first frame; 54,912-byte allocation failed | 1,047,832 | 934,736 | 14,025,538 |
| 2 MiB | Completed | 2,094,192 | 1,531,320 | 15,875,290 |
| 3 MiB | Completed | 3,138,032 | 1,531,464 | 15,373,215 |
| 4 MiB | Completed | 4,189,688 | 1,531,120 | 15,159,068 |
DOOM uses spare zone capacity for purgeable caches, so a nearly full zone does not mean all those bytes must remain resident. The experiment establishes that 2 MiB worked and 1 MiB failed for this sequence; it does not identify the exact minimum between them.
At 2 MiB, the largest zone request was 115,192 bytes. Engine calls to the host allocator peaked at 2,287,849 bytes, including the zone: 190,697 bytes were outside it. This excludes libc’s own internal allocations and the measurement wrappers’ headers. The host has eight-byte pointers; these are host allocation measurements, not ARM allocation sizes.
All successful runs, including a second independent 2 MiB run, produced
identical player position, health, and captured RGB frames at tic 10 and the
end. The final 320×200 RGB frame has SHA-256
c574f154a99b5783bd72fa55b96377334e5c5ef4d35c6430675d3a83c4eec6ff.
The adapter avoids comparing initial wipe frames, which can contain the
uninitialized previous display. Host execution time is not a Q performance
measurement.
The only functional source change was replacing doomgeneric’s output-buffer
allocation of four bytes per pixel with sizeof(pixel_t). CMAP256 then
uses a 64,000-byte output buffer. The separate 64,000-byte I_VideoBuffer
inside the zone remains; buffer sharing is unimplemented. Other scratch
changes measured zone occupancy, engine allocations and WAD reads.
Cortex-M4 link result
Section titled “Cortex-M4 link result”The independent ARM build used GCC 12.2.1, Cortex-M4 hard float, -Os,
function/data sections and linker garbage collection. A minimal headless
adapter retained the actual doomgeneric_Create() / doomgeneric_Tick()
entry points. It linked newlib, math and nosys with mkdir/_fini stubs.
The ARM binary was linked, not executed.
| Standalone ARM component | Bytes |
|---|---|
| Text and read-only data | 267,239 |
| Initialized writable data | 62,028 |
| Zero-initialized data | 239,828 |
| Flash load: text + initialized data | 329,267 |
| Static RAM: initialized + zero-initialized data | 301,856 |
The accepted 0.1.7GQ image uses 1,220,608 of 1,441,792 firmware bytes, leaving 221,184 bytes. The standalone engine’s flash load exceeds that headroom by 108,083 bytes. This is not the exact cost of integration: the Q already contains some libc/math routines, and a combined link may share or discard code differently. Static ARM RAM also excludes the dynamic zone, heap-allocated output buffer, filesystem state and live MicroPython objects.
Remaining integration gates
Section titled “Remaining integration gates”- Mutable PSRAM access. GameQ owns the lower 4 MiB of PSRAM and currently uses explicitly aligned word accesses. DOOM’s ordinary C zone contains byte and halfword stores and cannot be placed there without qualification. The host run uses ordinary host memory and provides no evidence that these writes work on the Q. Its internal heap span is only 389,688 bytes before live Python allocations. A safe external-memory strategy or a substantial allocation redesign is required.
- Combined flash and SRAM layout. Measure the real Q link, buffer sharing, and memory reuse after stopping the Game Boy runtime. Preserve the working firmware updater and resident hub while choosing that layout.
- Q adapter and hardware acceptance. Filesystem, display conversion, bounded engine ticks, input, power handling, failure containment and return to the hub remain unimplemented. No Q FPS, DOOM save/restore, other level, or full-campaign compatibility claim follows from this experiment.
Local reproduction and evidence
Section titled “Local reproduction and evidence”With the retained scratch sources and existing toolchain, the host commands are:
python3 build/reviews/doom-feasibility/host/prepare.pypython3 build/reviews/doom-feasibility/host/run.pyhost/run.py records each exact invocation. Its engine arguments are
-iwad freedoom1.wad -mb N -warp 1 1 -skill 2 -nosound -nomusic.
host/doomgeneric_probe.c records the input schedule. See
host/host-results.json, host/run-*/output.txt, host/run-*/result.json
and host/run-*/first-frame.png / last-frame.png for host evidence.
These ignored experiment helpers are local artifacts, not repository build
dependencies.
ARM commands and limits are preserved in arm/build.sh, arm/Dockerfile,
arm/platform.c and arm/measurement.json; output is
arm/results/out/doom-arm.elf. ELF SHA-256:
4df5ef464089deb2d9194ae0a4b00f32ac236a848ebf60c95e1ffad34135a2f4.
| Input | Bytes | SHA-256 |
|---|---|---|
| doomgeneric source archive at the pin above | 3,090,406 | 1bd3f7f26220494159a38d71f2847ec81b58d6bbd7c7c8d81b08993018001148 |
| Official Freedoom 0.13.0 ZIP | 24,143,781 | 3f9b264f3e3ce503b4fb7f6bdcb1f419d93c7b546f4df3e874dd878db9688f59 |
Extracted freedoom1.wad |
28,795,076 | 7323bcc168c5a45ff10749b339960e98314740a734c30d4b9f3337001f9e703d |
The ZIP hash matches the upstream release’s freedoom-0.13.0-CHECKSUM file.
Archive notices were retained. See the runtime roadmap
for candidate selection and the broader compatibility and dependency work.