4 Commits
Author SHA1 Message Date
rijads ba6e305bd3 contentVersion 01.001.001 for v1.1.1 2026-10-06 10:09:13 +02:00
rijads 0134717023 README: screenshots from the console 2026-10-06 10:09:13 +02:00
rijads 78a22de43d Screenshots from the console; upload server writes whole chunks and stops at once
screenshots/: the game list, the send screen during and after an upload and its page in a
browser, the DOOM II title, MAP01 and shooting the first zombies, taken on the console.

Testing the send screen with a browser and the 643 MB collection 7Z found two faults. The body
was written as recv handed it over, about 20 KB per write, and writes to /app0 cost more the
larger the file: the upload fell to 0.4 MB/s past 400 MB (1.6 ms per write at first, 47 ms past
480 MB). It now writes whole 256 KB chunks, which stay at 4 ms each; the whole 7Z arrived at
11 MB/s and its games were added 8 s later. Leaving the screen during an upload froze the game
until the upload ended, because the stop was only checked when recv timed out; it is now checked
before every recv, and the partial file is deleted.

console_test.py launches through the PS5Upload helper when the Payload Manager's port is dead
after rest mode. TODO: an unreproduced crash on The Ultimate DOOM's attract demos, games already
installed counted as added, and the page's status waiting for a running upload.
2026-10-06 10:09:13 +02:00
rijads c0b13e5efc Upload server: serve browsers' connections in any order, without Nagle
The send screen's server handled one connection at a time and waited up to 30 s for its
request, and browsers open connections before they need them and may leave them quiet, which
stalled the page and every upload. It now keeps up to eight waiting and serves whichever sends a
request first. Replies are two sends; with Nagle the second waited for Windows' delayed
acknowledgement (up to 200 ms), so the server sets TCP_NODELAY.

The same changes were measured on the console in the OpenRCT2 port; built here for the PS5 and
the PC, not run on the console.
2026-10-05 17:19:26 +02:00
14 changed files with 157 additions and 43 deletions

No files matched your search

+17 -2
View File
@@ -25,6 +25,7 @@ the end.
| `test/` | `render_music` (MUS lump to WAV), `present_preview` (runs the GPU kernel's math on the CPU) and the console test plans |
| `sce_sys/` | `param.json` (raise `contentVersion` in every release) and the icon (the shareware WAD's `M_DOOM` logo, scaled 3x on black) |
| `tools/` | Tool, library and shareware WAD fetch, kernel and page embedding, deploy, console test runner, `send.py` (sends files to the send screen like its page) |
| `screenshots/` | For showing the port, taken on the console (2026-10-06): the game list, the send screen during and after an upload and its page in a browser, the DOOM II title, MAP01 and shooting the first zombies. Console captures cut to the 4:3 picture (1440x1080, the side bars dropped), the page to 768x576 |
## Build
@@ -98,7 +99,16 @@ and maps the chosen game to Doom's game mode, mission and language. The launcher
as `<name>.upload` in the WAD folder. The launcher moves game WADs into place, runs archives
through the importer and deletes them afterwards, and reports each result on the screen and to the
page, which polls `GET /status`. The address shown is the one the route to the internet leaves
from (a connected UDP socket's local address); the QR code is drawn with qrcodegen.
from (a connected UDP socket's local address); the QR code is drawn with qrcodegen. The server
handles one request at a time but keeps up to eight connections waiting and serves whichever
sends a request first: browsers open connections before they need them and may leave them quiet,
and waiting on one (up to 30 s) stalled the page and every upload. Replies go out with
`TCP_NODELAY`: they are two sends, and with Nagle the second waited for Windows' delayed
acknowledgement (up to 200 ms). Both came from the OpenRCT2 port. The body is written in whole
256 KB chunks (see the write cost under PS5 facts), and leaving the screen stops an upload in
progress at once, deleting the partial file. On the console (2026-10-06) a browser sent a WAD and
a 643 MB 7Z at 11 MB/s, the network's speed, and the 7Z's games were added 8 s later. While an
upload runs the page's status polls wait for it, so its *On the console* list fills in afterwards.
Saves are per game: `<save dir>/<iwad name>sav<slot>.dsg` (for example `doom2sav0.dsg`).
@@ -123,7 +133,8 @@ itself and logs why.
`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
`/data/pldmgr/payloads/doomlaunch/`; when the Payload Manager's port is dead after rest mode, through
the PS5Upload helper's app launch instead), 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),
@@ -147,6 +158,10 @@ import from `http://192.168.0.10:8666/`), `console-autoimport.cfg`, `console-tnt
plus `sceKernelGetdents` (8-byte records: 32-bit inode, 16-bit length, type, name length). Saves
use explicit `/download0/...` paths; output is captured by pointing `stdout`/`stderr` at a pipe
(`fdopen`), and klog wants one line per `sceKernelDebugOutText`.
- Writes to `/app0` cost per call, and more the larger the file. Writing a 643 MB upload as `recv`
handed it over (about 20 KB per call) took 1.6 ms per write at first, 9 ms past 384 MB and
47 ms past 480 MB, down to 0.4 MB/s; in 256 KB writes it stayed at 4 ms each to the end (about
60 MB/s). `recv` on a title's socket returns about 20 KB at a time.
- A title can listen on TCP ports, but the sandbox refuses some with `EACCES` (8666 and 50000 of
those tried; 8000, 9000, 9090, 9666, 18666 and 30000 work). BSD sockets come from `libkernel`.
- `sceHttp` with `sceSsl` works in the sandboxed title, plain and HTTPS, including `Range` request
+14
View File
@@ -9,6 +9,15 @@ It ships with the free shareware episode of DOOM and plays The Ultimate DOOM, DO
DOOM from your own game files, which you can send from a PC or phone browser, download from a
link or copy to the console, including straight out of ZIP, 7Z and RAR archives.
<p align="center">
<img src="screenshots/shooting.png" width="49%" alt="DOOM II, MAP01: firing the pistol at a zombieman">
<img src="screenshots/shooting-2.png" width="49%" alt="DOOM II, MAP01: the next zombieman, one already down">
</p>
<p align="center">
<img src="screenshots/launcher.png" width="49%" alt="The game list with The Ultimate DOOM, DOOM II, both Final DOOM games and the shareware episode">
<img src="screenshots/title.png" width="49%" alt="DOOM II title screen">
</p>
## Requirements
A jailbroken PS5 with kstuff (fake-signed executables) and
@@ -52,6 +61,11 @@ WADs it recognises and skips everything else.
`http://` or `https://` address on the console keyboard and choose *Download*. The link can point
at a WAD, an archive, or a folder; for a folder DOOM lists the files and you pick one or all.
<p align="center">
<img src="screenshots/send-screen.png" width="49%" alt="The send screen on the TV, receiving the DOOM + DOOM II collection 7Z">
<img src="screenshots/upload-page.png" width="49%" alt="The same transfer on the page in a PC browser">
</p>
To download from your own PC instead, open a terminal in the folder that holds the files and run a
small web server, then use the address it prints with your PC's local IP (for example
`http://192.168.1.20:8000/`):
+15
View File
@@ -24,3 +24,18 @@ Loading itself exists: id's `-file` still works. Order of work:
- [ ] *Legacy of Rust* and most community WADs (Boom, MBF21, `DEHACKED`) need far more than id's
engine: decide between extending it and moving the game core to a modern port (for example
DSDA-Doom or Woof!) behind our PS5 layer.
## Found in console testing (2026-10-06)
- [ ] A crash on the console with no trace: The Ultimate DOOM (the 2024 `doom.wad`) on its attract
demos with the main menu open, within a second after frame 3300 (about 95 s after the
launcher). ShadowMountPlus saw an exception stop (`flags=0x008a4102`); `doom.log` ends at the
capture of frame 3300, and the kernel log's crash lines were read past before they were kept.
The same run again went 9000 frames without a crash, and the PC build (with AddressSanitizer,
with and without audio) plays the same demos cleanly. Keep a kernel log recording during
console runs (klogsrv, or polling the helper's `/api/ps5/klog`) to catch the next one.
- [ ] Games already installed count as added: sending the collection 7Z again reports "Added
DOOM.WAD DOOM2.WAD PLUTONIA.WAD TNT.WAD" and the game list "Added 5 games" when nothing new
arrived. Report only new games, and name them by title.
- [ ] The page's *On the console* list waits for a running upload (the server takes one request at a
time), so a WAD added before a long archive shows up only after the archive is sent.
+1 -1
View File
@@ -10,7 +10,7 @@
"conceptId": "99666",
"contentBadgeType": 1,
"contentId": "UP9000-PPSA99666_00-DOOMNATIVEPS5000",
"contentVersion": "01.001.000",
"contentVersion": "01.001.001",
"downloadDataSize": 256,
"gameIntent": {
"permittedIntents": [
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+97 -37
View File
@@ -2,6 +2,8 @@
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <poll.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
@@ -10,6 +12,7 @@
#include <strings.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include "platform.h"
@@ -26,6 +29,9 @@
#define RECEIVE_TIMEOUT_S 1
#define SEND_TIMEOUT_S 5
#define IDLE_LIMIT_S 30
#define MAX_WAITING 8
#define WAITING_LIMIT_S 15
#define POLL_MS 250
#ifdef MSG_NOSIGNAL
#define SEND_FLAGS MSG_NOSIGNAL
@@ -94,21 +100,23 @@ static void respond_text(int fd, const char *status, const char *text)
respond(fd, status, "text/plain; charset=utf-8", text, strlen(text));
}
// recv that gives up when the server stops or the browser stays quiet for too long.
// recv that gives up when the server stops or the browser stays quiet for too long. The stop is
// checked before every recv: data that keeps arriving must not hold up leaving the screen.
static ssize_t receive(int fd, void *buffer, size_t size)
{
int idle = 0;
for (;;)
while (!stopping)
{
ssize_t got = recv(fd, buffer, size, 0);
if (got >= 0)
return got;
if ((errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) || stopping ||
if ((errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) ||
++idle >= IDLE_LIMIT_S / RECEIVE_TIMEOUT_S)
return -1;
}
return -1;
}
// Reads the request head into head[]; returns its length including the blank line, with the
@@ -209,6 +217,7 @@ static void put_file(int fd, const char *target, int head_length, int used)
char name[UPLOAD_NAME], part[UPLOAD_PATH];
const char *value = header_value("Content-Length");
long long length = value ? strtoll(value, NULL, 10) : -1, done = 0;
size_t filled = 0;
int out, full, lost = 0, error = 0;
if (clean_name(name, sizeof(name), target))
@@ -244,24 +253,35 @@ static void put_file(int fd, const char *target, int head_length, int used)
{
long long extra = used - head_length < length ? used - head_length : length;
if (write_all(out, (const uint8_t *)head + head_length, (size_t)extra))
error = errno ? errno : EIO;
memcpy(chunk, head + head_length, (size_t)extra);
filled = (size_t)extra;
done = extra;
}
// Writes go out in whole chunks. recv hands over about 20 KB at a time, and the console
// charges per write, more the larger the file: written as received, a 643 MB archive fell
// to 0.4 MB/s past 400 MB.
while (!error && !lost && done < length)
{
ssize_t got = receive(fd, chunk, length - done < CHUNK ? (size_t)(length - done) : CHUNK);
size_t room = CHUNK - filled;
ssize_t got = receive(fd, chunk + filled, length - done < (long long)room ? (size_t)(length - done) : room);
if (got <= 0)
lost = 1;
else if (write_all(out, chunk, (size_t)got))
error = errno ? errno : EIO;
else
{
filled += (size_t)got;
done += got;
set_progress(1, name, done, length);
if (filled == CHUNK)
{
if (write_all(out, chunk, filled))
error = errno ? errno : EIO;
filled = 0;
}
}
}
if (!error && !lost && filled && write_all(out, chunk, filled))
error = errno ? errno : EIO;
close(out);
set_progress(0, "", 0, 0);
@@ -331,34 +351,85 @@ static void handle(int fd)
respond_text(fd, "404 Not Found", "Not found.");
}
static void *serve(void *arg)
static void prepare_client(int client)
{
struct timeval receive_timeout = {RECEIVE_TIMEOUT_S, 0}, send_timeout = {SEND_TIMEOUT_S, 0};
int yes = 1;
#ifdef SO_NOSIGPIPE
setsockopt(client, SOL_SOCKET, SO_NOSIGPIPE, &yes, sizeof(yes));
#endif
setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &receive_timeout, sizeof(receive_timeout));
setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &send_timeout, sizeof(send_timeout));
// A reply is two sends (head, body). With Nagle the body waits until the sender acknowledges
// the head, which Windows delays by up to 200 ms per request.
setsockopt(client, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes));
}
// Browsers open connections before they need them and may leave them quiet, so the server keeps
// several waiting and serves whichever sends a request first, one request at a time; waiting for
// the next request on a single connection blocked everyone else for up to IDLE_LIMIT_S.
static void *serve(void *arg)
{
int waiting[MAX_WAITING], count = 0, kept, i;
time_t since[MAX_WAITING], now;
struct pollfd fds[1 + MAX_WAITING];
(void)arg;
while (!stopping)
{
int client = accept(listener, NULL, NULL);
if (client < 0)
fds[0].fd = listener;
fds[0].events = POLLIN;
for (i = 0; i < count; i++)
{
if (!stopping)
plat_sleep_us(100000);
fds[1 + i].fd = waiting[i];
fds[1 + i].events = POLLIN;
}
if (poll(fds, (nfds_t)(1 + count), POLL_MS) < 0)
{
plat_sleep_us(100000);
continue;
}
if (!stopping)
{
#ifdef SO_NOSIGPIPE
int yes = 1;
setsockopt(client, SOL_SOCKET, SO_NOSIGPIPE, &yes, sizeof(yes));
#endif
setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, &receive_timeout, sizeof(receive_timeout));
setsockopt(client, SOL_SOCKET, SO_SNDTIMEO, &send_timeout, sizeof(send_timeout));
handle(client);
// Connections that sent something (or closed) are served and closed, quiet ones dropped
// after WAITING_LIMIT_S.
now = time(NULL);
kept = 0;
for (i = 0; i < count; i++)
{
int ready = fds[1 + i].revents != 0;
if (ready && !stopping)
handle(waiting[i]);
if (ready || stopping || now - since[i] > WAITING_LIMIT_S)
{
close(waiting[i]);
continue;
}
waiting[kept] = waiting[i];
since[kept++] = since[i];
}
count = kept;
if ((fds[0].revents & POLLIN) && !stopping)
{
int client = accept(listener, NULL, NULL);
if (client < 0)
continue;
if (count == MAX_WAITING)
{
close(waiting[0]);
memmove(waiting, waiting + 1, (size_t)--count * sizeof(waiting[0]));
memmove(since, since + 1, (size_t)count * sizeof(since[0]));
}
prepare_client(client);
waiting[count] = client;
since[count++] = now;
}
close(client);
}
for (i = 0; i < count; i++)
close(waiting[i]);
return NULL;
}
@@ -472,23 +543,12 @@ int upload_start(const char *dir)
void upload_stop(void)
{
struct sockaddr_in self;
int fd, i;
int i;
if (!running)
return;
stopping = 1;
// accept() does not return on its own; a connection to ourselves wakes the thread up.
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) >= 0)
{
memset(&self, 0, sizeof(self));
self.sin_family = AF_INET;
self.sin_port = htons((uint16_t)port);
self.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
connect(fd, (struct sockaddr *)&self, sizeof(self));
close(fd);
}
// The server thread sees it within POLL_MS.
pthread_join(server, NULL);
close(listener);
listener = -1;
+13 -3
View File
@@ -2,8 +2,9 @@
uv run --no-project --with pillow python tools/console_test.py test/console-play.cfg [--timeout S]
Writes the plan to the title folder as test.cfg (plus a capture port), launches PPSA99666 with
the doomlaunch payload (tools/launcher, loaded through the Payload Manager), pulls the frames the
Writes the plan to the title folder as test.cfg (plus a capture port), launches PPSA99666 (with
the doomlaunch payload from tools/launcher, loaded through the Payload Manager, or through the
PS5Upload helper when the Payload Manager does not answer), pulls the frames the
plan captures from the running title over TCP, converts them to PNG under build/test/run/,
receives the title's log over the same connection when it exits (build/test/run/doom.log),
prints it and removes test.cfg again. The PS5Upload helper must be running.
@@ -61,6 +62,15 @@ def install_launcher():
write_remote(f"{LAUNCHER_DIR}/{LAUNCHER.name}", data)
def launch():
try:
print("launch:", get(f"{PLDMGR}/loadpayload:{LAUNCHER.name}", timeout=30).strip()[:200], flush=True)
except OSError as e:
# The Payload Manager's port can stay dead after rest mode; the helper launches the same way.
print(f"Payload Manager unreachable ({e}), launching through the PS5Upload helper", flush=True)
print("launch:", push.http("POST", "/api/ps5/app/launch", {"addr": push.XFER, "title_id": TITLE}), flush=True)
def receive_exactly(sock, size):
data = bytearray()
while len(data) < size:
@@ -134,7 +144,7 @@ def main():
plan = Path(args.plan).read_text().rstrip("\n") + f"\nserve {CAPTURE_PORT}\n"
write_remote(TEST_PLAN, plan.encode())
print("launch:", get(f"{PLDMGR}/loadpayload:{LAUNCHER.name}", timeout=30).strip()[:200], flush=True)
launch()
seen = set()
deadline = time.time() + args.timeout
try: