Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba6e305bd3 | ||
|
|
0134717023 | ||
|
|
78a22de43d | ||
|
|
c0b13e5efc |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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/`):
|
||||
|
||||
@@ -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.
|
||||
@@ -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": [
|
||||
|
||||
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||