DOOM now opens on a game list. The package bundles id's freely distributable shareware DOOM1.WAD; players add The Ultimate DOOM, DOOM II, TNT and Plutonia (or Freedoom) from their own files: - copied into /data/homebrew/PPSA99666/wads, as WADs or inside ZIP, 7Z or RAR archives, which are imported once at the next start; - downloaded from an http:// or https:// link typed on the system keyboard: a WAD, an archive, or a folder listing to pick from. Archives are read in place over HTTP range requests, so a large solid 7Z needs no room on the console beyond the game WADs it holds. The console uses sceHttp/sceSsl and the IME dialog; the PC build uses libcurl. Archives go through libarchive, liblzma and zlib, fetched pinned and built for both targets, with large allocations taken from mmap. Engine fixes that came with it: IdentifyVersion takes the launcher's choice and sets gamemission, so TNT and Plutonia get their own level names and finale texts; the sky check no longer gives The Ultimate DOOM the DOOM II skies; saves are per game; widescreen full-screen pictures from the 2024 re-release are drawn centred instead of rejected. The console test runner receives doom.log over the capture connection instead of reading the kernel log.
13 KiB
DOOM for PS5 (native)
id Software's original linuxdoom-1.10 source running as a native PS5 title: its own home-screen
icon, launched like a game, drawing through Sony's VideoOut and AGC (GPU) drivers, reading the
DualSense through ScePad and playing sound through AudioOut. Title ID PPSA99666.
All PS5, platform, audio, launcher and test code here is written for this project. The external code is id's game source, the archive libraries the importer is built from (libarchive, xz's liblzma, zlib) and the build tools listed at the end.
Layout
| Path | What |
|---|---|
src/doom/ |
id's original game code with the 64-bit and portability fixes listed below |
src/port/ |
Doom's i_* layer (main, system, video, sound, network, controller mapping) on top of the platform API |
src/launcher/ |
The game list shown before the engine starts: IWAD discovery, settings, Doom-style drawing from the bundled WAD, HTML folder listings, and the importer (HTTP or local file, WAD or ZIP/7Z/RAR) |
src/platform/platform.h |
The platform API: time, log, files and folder listing, video present, pad, rumble, audio output, HTTP, text input |
src/audio/ |
Sound engine: SFX mixer, MUS sequencer with DMX-style voice allocation, OPL FM synth |
src/ps5/ |
PS5 backend: startup (crt0.c), system, VideoOut, AGC compute presenter, pad, AudioOut, sceHttp, the IME keyboard |
src/ps5/present.cl |
GPU kernel: palette lookup, sharp-bilinear scaling to 1080p, tiled scanout writes (gfx1010, wave64) |
src/host/ |
Headless Linux backend for testing on the PC: PNG frames, scripted pad, WAV audio, libcurl HTTP |
third_party/ |
Build configuration for libarchive, liblzma and zlib (config.h files and third_party.mk); the sources are fetched, not copied |
test/ |
render_music (MUS lump to WAV), present_preview (runs the GPU kernel's math on the CPU) and the console test plans |
title/ |
param.json and the icon |
tools/ |
Tool, library and shareware WAD fetch, kernel embedding, deploy, console test runner |
Build
Everything builds in the ps5-doom-build image (docker/Dockerfile: clang/lld/llvm 18, ninja, gdb,
libcurl for the PC build).
docker build -t ps5-doom-build docker/
MSYS_NO_PATHCONV=1 docker run --rm -v "$(cygpath -w "$PWD"):/src" -w /src ps5-doom-build make -j16 ps5 host
make ps5 fetches the pinned tools, the archive libraries (tools/fetch-archive-libs.sh) and the
shareware WAD on first use, then:
- compiles the game, the launcher and the PS5 layer for
x86_64-sie-ps5against the PS5 payload SDK headers, and the reading half of libarchive (zip, 7z, rar, rar5), liblzma's decoders and zlib's inflate with the same compiler; - compiles
present.clforgfx1010(wave64), links it and embeds it withtools/embed-kernel.py, which reads the register values from the compiler's kernel descriptor and fails the build if the kernel needs anything the presenter does not set up; - generates import stubs the SDK lacks (
libSceAgc,libSceAgcDriver,libSceCommonDialog) fromsrc/ps5/stubs/*.txt; - links a PIE, converts it to a PS5 module, signs the FSELF and assembles
dist/PPSA99666/with the sharewaredoom1.wadinwads/.
The libraries' malloc, calloc, realloc, free and strdup are renamed at compile time to
src/launcher/import_memory.c, which takes blocks of 256 KB and more from mmap: a native app's
libc heap cannot hold a 64 MB LZMA dictionary. The PC build uses the same wrapper.
Testing on the PC
build/host/bin/doom is the same game on the headless backend. Environment variables:
DOOM_WADDIR (the user's WAD folder), DOOM_SAVEDIR, DOOM_OUT (frame output dir),
DOOM_CAPTURE (frame numbers to save as PNG plus raw indices and palette), DOOM_FRAMES (exit
after N frames), DOOM_AUDIO (absolute WAV path), DOOM_INPUT (pad script:
frame:BUTTON+BUTTON+LX=-32000:frames;...), DOOM_IWAD (skip the launcher and start this game
file, e.g. doom1.wad) and DOOM_TEXT (the answer to the keyboard prompt).
DOOM_IWAD=doom1.wad DOOM_OUT=out DOOM_CAPTURE=120,900 build/host/bin/doom -timedemo demo1
Without DOOM_IWAD the launcher runs and the pad script drives it. For the importer, serve files
from inside the container with python3 -m http.server (no range support) or reach a server on
the Windows host as host.docker.internal (docker run --add-host=host.docker.internal:host-gateway).
For memory errors, build with AddressSanitizer:
make host HOST_DIR=build/asan HOST_FLAGS="-O1 -g -fsanitize=address" HOST_CC="clang -fsanitize=address".
The launcher
I_ChooseIwad (in src/port/i_system.c) runs the launcher before the engine identifies its IWAD
and maps the chosen game to Doom's game mode, mission and language. The launcher:
- scans the WAD folders (
/app0/wads, then/download0) for the known IWAD names, checks each file's header and directory, and tells The Ultimate DOOM from DOOM by the presence ofE4M1; - imports any ZIP, 7Z or RAR found there that it has not imported before (recorded in
/download0/launcher.cfgby size and path), extracting only known IWAD names; - downloads from a typed link with
plat_http_get: an HTML answer is parsed as a folder listing (links under the folder that end in.wad,.zip,.7z,.raror/), a WAD is copied, an archive goes through libarchive. libarchive seeks with HTTP range requests; a server without them is read through or re-read from the start instead; - writes into the first writable WAD folder (
/app0/wadson the console), through a.partfile that is checked as an IWAD before it is renamed; - shows a one-time notice with a checkbox before the first download.
Saves are per game: <save dir>/<iwad name>sav<slot>.dsg (for example doom2sav0.dsg).
On the console
uv run --no-project python tools/deploy.py uploads dist/PPSA99666 to /data/homebrew/PPSA99666
(PS5Upload helper must be running), where ShadowMountPlus registers it. Uploading adds and replaces
files but never deletes, so files dropped from the package stay on the console until removed.
make release writes dist/PPSA99666.zip and its .sha256.
The app is sandboxed: it reads and writes /app0 (its folder; imports land in /app0/wads) and
writes /download0 (config, saves, launcher.cfg and doom.log). Log lines also go to the kernel
log (sceKernelDebugOutText). Fatal errors show as a system notification.
Presentation uses the AGC compute path by default. Hold L1+R1 while the game starts to force the CPU scaler. If a GPU frame does not complete within 500 ms, the game switches to the CPU scaler by itself and logs why.
Console testing
tools/console_test.py <plan> runs the deployed title hands-free: it writes the plan as test.cfg
into the title folder, launches through the doomlaunch payload (tools/launcher, put into
/data/pldmgr/payloads/doomlaunch/), pulls captured frames over TCP from the running game (the
app listens on 9119; the PC firewall blocks the other direction), de-tiles them to PNG in
build/test/run/, receives doom.log over the same connection when the game exits, prints it and
deletes test.cfg. Plan lines: input <steps>, capture <frames>, frames <N> (clean exit),
present cpu, game <file> (skip the launcher), text <value> (answer the keyboard prompt
without opening it). Plans: console-play.cfg (menus, play, save, load), console-cpu.cfg,
console-soak.cfg (6 minutes), console-import.cfg (launcher, notice, folder listing and a 7Z
import from http://192.168.0.10:8666/), console-autoimport.cfg, console-tnt.cfg,
console-ultimate.cfg.
PS5 facts learned on hardware (FW 13.00)
downloadDataSize64 is rejected (0x80a40087, launch fails); 256 works./download0is a fixed-size image of that size (/user/download/PPSA99666/download0.dat), not a folder the PC can read; large data belongs in/app0.- The libc heap of a native app is small: large blocks must come from
mmap(the zone and the archive libraries do). - The sandbox refuses
access,chdir,opendir,dup,dup2(EPERM).open,renameandunlinkwork,/app0is writable, and folders list throughsceKernelOpenwithO_DIRECTORYplussceKernelGetdents(8-byte records: 32-bit inode, 16-bit length, type, name length). Saves use explicit/download0/...paths; output is captured by pointingstdout/stderrat a pipe (fdopen), and klog wants one line persceKernelDebugOutText. sceHttpwithsceSslworks in the sandboxed title, plain and HTTPS, includingRangerequest headers added withsceHttpAddRequestHeader(answers 206).sceHttpReadDatareturns only when the buffer is full or the body ends. About 12 MB/s from a PC on the same wired network.- The SDK's FreeBSD headers do not match the console's C library everywhere:
MB_CUR_MAXexpands to___mb_cur_max(the library has_Getmbcurmax), and there is nolocaltime_r,gmtime_rortimegm.assertneeds__assert, which is missing too. - Scanout must be tiled (
sceVideoOutSetBufferAttribute2tiling 0; 1 is0x80290007), 16 MB per 1080p buffer. Pixel layout: 512x128 tiles with the bit interleave insrc/ps5/tiling.h. - GPU memory: direct memory type 12, protection 0x33, CPU writes flushed with
clflushbefore GPU use and invalidated before CPU reads of GPU output (ps5_cache_flush). - Compute runs as wave64 whatever the dispatch flag says, and the GPU mishandles the gfx10.3
nullcarry-out operand (writes are dropped). The kernel is built for gfx1010, wave64;embed-kernel.pyrejects wave32. GPU page faults appear in klog asGPU Protection fault ... addr(VA). - After rest mode ShadowMountPlus can lose its install hook ("TitleDir bridge unavailable"); restarting the ShadowMountPlus payload through the Payload Manager fixes it without a reboot.
Controls
| Input | In game | In menus |
|---|---|---|
| Left stick | Move and strafe (analog) | Navigate |
| Right stick | Turn (analog; speed follows the Mouse Sensitivity slider) | |
| D-pad | Move and turn (digital) | Navigate (repeats when held) |
| R2 | Fire | |
| Cross / Square | Use | Select (Cross), answers yes to prompts |
| Circle | Strafe modifier | Back, answers no to prompts |
| L1 / R1 | Previous / next weapon (zoom on the automap) | |
| L2 | Run | |
| Triangle / touchpad | Automap (Square toggles follow mode on the map) | |
| Options | Menu | Close menu |
Saving needs no keyboard: an empty slot is pre-filled with the level name, Cross saves.
Changes to id's code
- 64-bit: pointers stored in
int(config string defaults, save game pointer fields, thecolumndirectoryfield of the on-disk texture struct), pointer arrays sizedcount*4(r_data.c,p_setup.c), table alignment throughintcasts, 16-byte zone alignment. - Undefined behaviour and latent bugs: the unterminated
sprnameslist, unsequenced event queue updates, button array resets that cleared 8 bytes of pointers, save games written into the screen buffers (now a dedicated 4 MB buffer), the level-load sky check that compared the game mode with mission values (pack_plutequalsretail, so The Ultimate DOOM got DOOM II skies). - Portability: no
values.h/alloca.h/sound server; config in the working directory (the port changes into the save directory at start). - Game selection:
IdentifyVersiontakes the launcher's choice (I_ChooseIwad) instead of probing file names, setsgamemission, and the TNT and Plutonia level names and finale texts that id left behindFIXMEcomments are used. - Behaviour: 1.9 demos play (the WADs' attract demos); empty save slots get a default name; saves are per game; default SFX channels 8 (the DOS default) instead of 3; Freedoom IWADs recognised; static limits raised (visplanes, drawsegs, sprites, openings, intercepts, plats, ceilings, buttons, scrollers) so limit-removing maps such as Freedoom's run; full-screen pictures wider than 320 pixels (the 2024 re-release's widescreen title and intermission screens) are drawn centred and clipped instead of being rejected.
External code and tools (pinned)
- ps5-native-app-boilerplate
b1315a9: itsps5-native-tool(PIE to PS5 module, FSELF signing), its clean-roomlibc.prx, its intermediate PIE linker script, and the PS5 payload SDK v0.42 it fetches. Built in.deps/native-appbytools/fetch-native-tools.sh; nothing from it is copied intosrc/. - libarchive 3.8.9, xz 5.8.4 (liblzma)
and zlib 1.3.2, fetched and checked by
tools/fetch-archive-libs.shinto.deps/archiveand compiled into the game for both targets. - clang/lld/llvm 18 (x86_64-sie-ps5 and amdgcn targets).