Game Forwarder tiles: the tile app and its launch payload, built into the title

A home screen tile is a title of its own (PPSA99170 to PPSA99269) that starts one
game in RetroArch. Titles cannot launch titles (sceSystemServiceLaunchApp returns
0x8094000F from one, sceLncUtilLaunchApp is a null import), so the tile hands a
payload to the console's ELF loader on 127.0.0.1:9021 and exits:

- forwarder/tile.c: the tile's eboot.bin. Reads its title id from
  /app0/sce_sys/param.json, stamps it into /app0/launch.elf's slot, sends it to
  the loader, logs /app0/tile.log, and leaves through LoadExec("exit") (a return
  from main ends in a SIGSYS error notice).
- forwarder/launch.c: the payload. Reads /data/homebrew/<tile>/forward.txt (one
  argument a line: --rom, --core, --exit-after-game, src/frontend_mode_ps5.cpp),
  waits for the tile to leave, and starts PPSA99169 with those arguments through
  sceLncUtilLaunchApp (sceSystemServiceLaunchApp as the fallback).
- tools/build-forwarder.sh builds both; tools/build-title.sh puts them in
  dist/PPSA99169/forwarder/ and counts them in the title's identity; the notices
  cover both files.

Evidence:
- host: python3 -m unittest tests.test_forwarder (the tile stamps and sends the
  payload byte for byte, refuses a lowercase id, a missing or a doubled slot; the
  payload passes the five arguments, falls back to SystemService, and an
  unstamped payload starts nothing) - OK.
- console (base PS5, 13.40, ShadowMountPlus 1.7-beta5fix1), the test tile
  PPSA99170 registered through ShadowMountPlus's manual list:
  tile.log "tile PPSA99170: launch payload 110832 bytes, 110832 sent to
  127.0.0.1:9021 (errno 0)"; launch.log five arguments, sceLncUtilLaunchApp
  00002018; RetroArch's trace "forwarder: /app0/content/nes/<game> with
  /app0/cores/fceumm_libretro.so, closing after it"; the tile exited cleanly
  (no error notice), back to the home screen after the game.
- gates: format, unit (206 tests), build (tile 11,123 B, payload 110,832 B),
  integration (206 tests; a first run timed out starting two scraper test
  servers under load, both passed alone and on the rerun), evidence - PASS.
This commit is contained in:
Mihawk committed 2026-10-18 00:23:06 -04:00
1 parent 459e194dc0
commit a0ea678ee2
9 files changed
+544 -6

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
/* PS5 RetroArch - what a Game Forwarder tile (forwarder/tile.c) and its launch payload
* (forwarder/launch.c) share.
*
* Copyright (C) 2026 Mihawk
* SPDX-License-Identifier: GPL-3.0-or-later
*
* A tile tells the payload which tile it is by writing its title id over
* FORWARDER_SLOT_MARKER in the payload's bytes before it sends them; the payload then reads
* that tile's forward.txt. No file is written for the hand-over, so two tiles cannot race.
*/
#ifndef PS5_RETROARCH_FORWARDER_H
#define PS5_RETROARCH_FORWARDER_H
#include <stddef.h>
/* The payload's slot holds this until a tile writes its title id over it. */
#define FORWARDER_SLOT_MARKER "PS5RA-FORWARDER-TILE-ID-SLOT"
#define FORWARDER_SLOT_SIZE 32
/* A title id and its NUL: four capitals and five digits (PPSA99170). */
#define FORWARDER_ID_SIZE 10
/* The arguments a tile's forward.txt may hold, one a line, and the longest line. */
#define FORWARDER_MAX_ARGUMENTS 16
#define FORWARDER_MAX_ARGUMENT 512
static inline int forwarder_valid_id(const char *id, size_t length)
{
if (!id || length != 9)
return 0;
for (size_t i = 0; i < 9; i++)
if (i < 4 ? !(id[i] >= 'A' && id[i] <= 'Z') : !(id[i] >= '0' && id[i] <= '9'))
return 0;
return 1;
}
int forwarder_title_id(const char *param, char id[FORWARDER_ID_SIZE]);
int forwarder_stamp(char *payload, long size, const char *id);
#endif
+123
View File
@@ -0,0 +1,123 @@
/* PS5 RetroArch - the Game Forwarder's launch payload.
*
* Copyright (C) 2026 Mihawk
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Run by the console's ELF loader, sent there by a tile (forwarder/tile.c) with that tile's
* title id written into tile_slot. A payload may start a title where the tile may not, so
* this waits for the tile to close (up to 15 s), then starts RetroArch (FORWARDER_TARGET)
* with the arguments in /data/homebrew/<tile>/forward.txt, one a line, through the launch
* calls the console tooling's control payload uses: sceLncUtilLaunchApp as the foreground
* user, then sceSystemServiceLaunchApp. Each step is written to the tile's launch.log.
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "forwarder.h"
#ifndef FORWARDER_HOMEBREW
#define FORWARDER_HOMEBREW "/data/homebrew"
#endif
#ifndef FORWARDER_TARGET
#define FORWARDER_TARGET "PPSA99169"
#endif
#ifndef FORWARDER_WAIT_STEP_US
#define FORWARDER_WAIT_STEP_US 250000
#endif
typedef struct
{
uint32_t size;
int user_id;
uint32_t app_opt;
uint64_t crash_report;
uint64_t check_flag;
} lnc_app_param_t;
int sceSystemServiceLaunchApp(const char *title_id, const char *argv[], lnc_app_param_t *param);
int sceLncUtilLaunchApp(const char *title_id, const char *argv[], lnc_app_param_t *param);
uint32_t sceLncUtilGetAppIdOfRunningBigApp(void);
int sceLncUtilGetAppTitleId(uint32_t app_id, char *title_id);
int sceUserServiceInitialize(void *param);
int sceUserServiceGetForegroundUser(int *user_id);
/* The tile writes its title id over the marker in these bytes (forwarder.h). */
__attribute__((used)) char tile_slot[FORWARDER_SLOT_SIZE] = FORWARDER_SLOT_MARKER;
/* The running big app's title id, or "" when none runs. */
static void running_title(char title[16])
{
const uint32_t app = sceLncUtilGetAppIdOfRunningBigApp();
memset(title, 0, 16);
if (app != 0 && app != 0xffffffffu && sceLncUtilGetAppTitleId(app, title) != 0)
memset(title, 0, 16);
title[15] = '\0';
}
int main(void)
{
char tile[FORWARDER_ID_SIZE];
memcpy(tile, (const char *)tile_slot, sizeof(tile) - 1);
tile[sizeof(tile) - 1] = '\0';
if (!forwarder_valid_id(tile, strlen(tile)))
{
printf("forwarder payload: no tile's title id in the slot; nothing launched\n");
return 1;
}
char path[256];
snprintf(path, sizeof(path), FORWARDER_HOMEBREW "/%s/launch.log", tile);
FILE *log = fopen(path, "w");
char lines[FORWARDER_MAX_ARGUMENTS][FORWARDER_MAX_ARGUMENT];
const char *arguments[FORWARDER_MAX_ARGUMENTS + 1];
int count = 0;
snprintf(path, sizeof(path), FORWARDER_HOMEBREW "/%s/forward.txt", tile);
FILE *input = fopen(path, "r");
while (input && count < FORWARDER_MAX_ARGUMENTS && fgets(lines[count], sizeof lines[count], input))
{
lines[count][strcspn(lines[count], "\r\n")] = '\0';
if (lines[count][0])
{
arguments[count] = lines[count];
count++;
}
}
if (input)
fclose(input);
arguments[count] = NULL;
if (log)
fprintf(log, "launch %s: %d argument(s) from forward.txt%s\n", tile, count,
input ? "" : " (none: the file could not be read)");
const int init = sceUserServiceInitialize(NULL);
int user = -1;
const int foreground = sceUserServiceGetForegroundUser(&user);
char title[16];
int waited = 0;
for (running_title(title); strcmp(title, tile) == 0 && waited < 60; waited++)
{
usleep(FORWARDER_WAIT_STEP_US);
running_title(title);
}
if (log)
{
fprintf(log, "launch %s: user service %08x, foreground user %d (%08x); waited %d step(s) for the tile, "
"running now: '%s'\n",
tile, (unsigned)init, user, (unsigned)foreground, waited, title);
fflush(log);
}
lnc_app_param_t param = {sizeof(param), user, 0, 0, 0};
int result = sceLncUtilLaunchApp(FORWARDER_TARGET, arguments, &param);
if (log)
fprintf(log, "launch %s: sceLncUtilLaunchApp(%s) = %08x\n", tile, FORWARDER_TARGET, (unsigned)result);
if (result < 0)
{
result = sceSystemServiceLaunchApp(FORWARDER_TARGET, arguments, &param);
if (log)
fprintf(log, "launch %s: sceSystemServiceLaunchApp(%s) = %08x\n", tile, FORWARDER_TARGET,
(unsigned)result);
}
if (log)
fclose(log);
return result < 0;
}
+163
View File
@@ -0,0 +1,163 @@
/* PS5 RetroArch - a Game Forwarder tile: the app each home screen tile the WebUI makes runs.
*
* Copyright (C) 2026 Mihawk
* SPDX-License-Identifier: GPL-3.0-or-later
*
* A tile is a title of its own (/data/homebrew/<its id>), and a title cannot start another:
* sceSystemServiceLaunchApp answered a tile 0x8094000f, and sceLncUtilLaunchApp is a null
* import for a title. So the tile hands the launch to a payload: it writes its own title id
* into forwarder/launch.c's slot, sends that payload to the console's ELF loader on
* 127.0.0.1:9021 as eboot.bin sends the WebUI's daemon (src/webui_link.cpp), and closes as
* eboot.bin closes. The payload waits for it to go and starts RetroArch with the arguments
* in the tile's forward.txt (--rom, --core, --exit-after-game: src/frontend_mode_ps5.cpp).
* What happened is written to the tile's tile.log.
*/
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include "forwarder.h"
#ifndef FORWARDER_APP0
#define FORWARDER_APP0 "/app0"
#endif
#ifndef FORWARDER_LOADER_PORT
#define FORWARDER_LOADER_PORT 9021
#endif
int sceSystemServiceHideSplashScreen(void);
int sceSystemServiceLoadExec(const char *path, const char *const *argv);
/* The whole of a small file, NUL-terminated; NULL when it cannot be read. */
static char *read_file(const char *path, long *size)
{
FILE *file = fopen(path, "rb");
char *data = NULL;
long length = -1;
if (file && fseek(file, 0, SEEK_END) == 0 && (length = ftell(file)) >= 0 &&
fseek(file, 0, SEEK_SET) == 0 && (data = malloc((size_t)length + 1)) &&
fread(data, 1, (size_t)length, file) != (size_t)length)
{
free(data);
data = NULL;
}
if (file)
fclose(file);
if (data)
{
data[length] = '\0';
*size = length;
}
return data;
}
/* The "titleId" of this tile's param.json into id; 0 when there is no valid one. */
int forwarder_title_id(const char *param, char id[FORWARDER_ID_SIZE])
{
const char *key = param ? strstr(param, "\"titleId\"") : NULL;
const char *value = key ? strchr(key + 9, '"') : NULL;
if (!value || !forwarder_valid_id(value + 1, 9) || value[10] != '"')
return 0;
memcpy(id, value + 1, 9);
id[9] = '\0';
return 1;
}
/* Writes id over the payload's slot, which must be there exactly once; 0 otherwise. */
int forwarder_stamp(char *payload, long size, const char *id)
{
const size_t marker = strlen(FORWARDER_SLOT_MARKER);
char *slot = NULL;
for (long at = 0; at + (long)marker <= size; at++)
if (memcmp(payload + at, FORWARDER_SLOT_MARKER, marker) == 0)
{
if (slot)
return 0;
slot = payload + at;
}
if (!slot)
return 0;
memset(slot, 0, marker);
memcpy(slot, id, strlen(id));
return 1;
}
/* Sends the payload, whole, to the loader; the bytes sent. */
static long send_to_loader(const char *payload, long size, int *error)
{
long sent = 0;
const int fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in loader;
memset(&loader, 0, sizeof(loader));
loader.sin_family = AF_INET;
loader.sin_port = htons(FORWARDER_LOADER_PORT);
loader.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
*error = 0;
if (fd < 0 || connect(fd, (struct sockaddr *)&loader, sizeof(loader)) != 0)
*error = errno;
else
while (sent < size)
{
const ssize_t n = send(fd, payload + sent, (size_t)(size - sent), 0);
if (n <= 0)
{
*error = errno;
break;
}
sent += n;
}
if (fd >= 0)
close(fd);
return sent;
}
int main(int argc, char **argv)
{
(void)argc;
(void)argv;
FILE *log = fopen(FORWARDER_APP0 "/tile.log", "w");
sceSystemServiceHideSplashScreen();
long param_size = 0, payload_size = 0;
char *param = read_file(FORWARDER_APP0 "/sce_sys/param.json", &param_size);
char *payload = read_file(FORWARDER_APP0 "/launch.elf", &payload_size);
char id[FORWARDER_ID_SIZE] = "";
if (!forwarder_title_id(param, id))
{
if (log)
fprintf(log, "tile: no title id in sce_sys/param.json\n");
}
else if (!payload || !forwarder_stamp(payload, payload_size, id))
{
if (log)
fprintf(log, "tile %s: launch.elf is missing or has no slot for the title id\n", id);
}
else
{
int error = 0;
const long sent = send_to_loader(payload, payload_size, &error);
if (log)
fprintf(log, "tile %s: launch payload %ld bytes, %ld sent to 127.0.0.1:%d (errno %d)\n", id,
payload_size, sent, FORWARDER_LOADER_PORT, error);
}
free(param);
free(payload);
if (log)
fflush(log);
/* A title closes as eboot.bin does (src/main.cpp): returning from main ended one with
* SIGSYS, which the console reports as an error in the game. */
const int result = sceSystemServiceLoadExec("exit", NULL);
if (log)
{
fprintf(log, "tile %s: LoadExec(exit) = %08x\n", id, (unsigned)result);
fclose(log);
}
if (result >= 0)
for (;;)
usleep(100000); /* the shell ends the process asynchronously */
return 0;
}
+151
View File
@@ -0,0 +1,151 @@
"""The Game Forwarder's tile app and launch payload (forwarder/), built for the host with the
console's calls stubbed: the tile stamps its title id into the payload and sends it to the
loader; the payload reads that tile's arguments, waits for the tile to close and starts
RetroArch with them."""
import shutil
import socket
import subprocess
import tempfile
import threading
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
MARKER = b"PS5RA-FORWARDER-TILE-ID-SLOT"
TILE_STUBS = r'''
int sceSystemServiceHideSplashScreen(void) { return 0; }
int sceSystemServiceLoadExec(const char *path, const char *const *argv)
{ (void)path; (void)argv; return -1; }
'''
LAUNCH_DRIVER = r'''
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct { uint32_t size; int user_id; uint32_t app_opt; uint64_t crash_report; uint64_t check_flag; } param_t;
extern char tile_slot[];
int launch_main(void);
static int polls, lnc_result;
static FILE *record;
uint32_t sceLncUtilGetAppIdOfRunningBigApp(void) { return polls-- > 0 ? 0x2018u : 0xffffffffu; }
int sceLncUtilGetAppTitleId(uint32_t app, char *title) { (void)app; strcpy(title, "PPSA99170"); return 0; }
int sceUserServiceInitialize(void *p) { (void)p; return 0; }
int sceUserServiceGetForegroundUser(int *user) { *user = 7; return 0; }
static int launch(const char *call, const char *title, const char *argv[], param_t *param, int result)
{
fprintf(record, "%s %s user=%d size=%u", call, title, param->user_id, param->size);
for (int i = 0; argv[i]; i++)
fprintf(record, " [%s]", argv[i]);
fprintf(record, "\n");
return result;
}
int sceLncUtilLaunchApp(const char *title, const char *argv[], param_t *param)
{ return launch("lnc", title, argv, param, lnc_result); }
int sceSystemServiceLaunchApp(const char *title, const char *argv[], param_t *param)
{ return launch("system", title, argv, param, 0); }
/* polls the tile is still running for, the first call's result (hex), the tile id stamped
* into the slot ("-" leaves the marker), the record file */
int main(int argc, char **argv)
{
if (argc != 5 || strlen(argv[3]) >= 32)
return 2;
polls = atoi(argv[1]);
lnc_result = (int)strtol(argv[2], NULL, 16);
if (strcmp(argv[3], "-") != 0)
memcpy(tile_slot, argv[3], strlen(argv[3]) + 1);
record = fopen(argv[4], "w");
const int status = launch_main();
fclose(record);
return status;
}
'''
@unittest.skipUnless(shutil.which('cc'), 'requires a host C compiler')
class Forwarder(unittest.TestCase):
def build(self, folder, name, sources, defines):
binary = folder / name
subprocess.run(['cc', '-std=gnu11', '-Wall', '-Wextra', '-Werror', *defines,
'-I' + str(ROOT / 'forwarder'), *map(str, sources), '-o', str(binary)], check=True)
return binary
def test_the_tile_stamps_its_id_and_sends_the_payload(self):
with tempfile.TemporaryDirectory() as temporary:
folder = Path(temporary)
app0 = folder / 'app0'
(app0 / 'sce_sys').mkdir(parents=True)
stubs = folder / 'stubs.c'
stubs.write_text(TILE_STUBS)
with socket.socket() as server:
server.bind(('127.0.0.1', 0))
server.listen(1)
port = server.getsockname()[1]
tile = self.build(folder, 'tile', [ROOT / 'forwarder/tile.c', stubs],
[f'-DFORWARDER_APP0="{app0}"', f'-DFORWARDER_LOADER_PORT={port}'])
payload = b'\x7fELF' + b'\x01' * 100 + MARKER + b'\0' * 4 + b'tail'
(app0 / 'launch.elf').write_bytes(payload)
(app0 / 'sce_sys/param.json').write_text('{"contentId": "x", "titleId": "PPSA99170"}')
received = bytearray()
def accept():
connection, _ = server.accept()
with connection:
while chunk := connection.recv(4096):
received.extend(chunk)
thread = threading.Thread(target=accept)
thread.start()
subprocess.run([str(tile)], check=True, timeout=10)
thread.join(10)
expected = payload.replace(MARKER, b'PPSA99170' + b'\0' * (len(MARKER) - 9))
self.assertEqual(bytes(received), expected)
log = (app0 / 'tile.log').read_text()
self.assertIn(f'tile PPSA99170: launch payload {len(payload)} bytes, {len(payload)} sent', log)
# Nothing is sent without a title id, or without exactly one slot.
(app0 / 'sce_sys/param.json').write_text('{"titleId": "ppsa99170"}')
subprocess.run([str(tile)], check=True, timeout=10)
self.assertIn('no title id in sce_sys/param.json', (app0 / 'tile.log').read_text())
(app0 / 'sce_sys/param.json').write_text('{"titleId": "PPSA99170"}')
for bad in (b'\x7fELF no slot', b'\x7fELF' + MARKER + MARKER):
(app0 / 'launch.elf').write_bytes(bad)
subprocess.run([str(tile)], check=True, timeout=10)
self.assertIn('has no slot for the title id', (app0 / 'tile.log').read_text())
def test_the_payload_waits_and_starts_retroarch_with_the_tiles_arguments(self):
with tempfile.TemporaryDirectory() as temporary:
folder = Path(temporary)
homebrew = folder / 'homebrew'
(homebrew / 'PPSA99170').mkdir(parents=True)
driver = folder / 'driver.c'
driver.write_text(LAUNCH_DRIVER)
payload = folder / 'launch.o'
subprocess.run(['cc', '-std=gnu11', '-Wall', '-Wextra', '-Werror', '-c', f'-DFORWARDER_HOMEBREW="{homebrew}"',
'-DFORWARDER_TARGET="PPSA99169"', '-DFORWARDER_WAIT_STEP_US=1000', '-Dmain=launch_main',
'-I' + str(ROOT / 'forwarder'), str(ROOT / 'forwarder/launch.c'), '-o', str(payload)],
check=True)
launch = self.build(folder, 'launch', [driver, payload], [])
(homebrew / 'PPSA99170/forward.txt').write_bytes(
b'--rom\r\nsnes/Game (USA).zip\n\n--core\nsnes9x\n--exit-after-game\n')
record = folder / 'record.txt'
subprocess.run([str(launch), '3', '4018', 'PPSA99170', str(record)], check=True, timeout=10)
self.assertEqual(record.read_text(), 'lnc PPSA99169 user=7 size=32 [--rom] [snes/Game (USA).zip] '
'[--core] [snes9x] [--exit-after-game]\n')
log = (homebrew / 'PPSA99170/launch.log').read_text()
self.assertIn('launch PPSA99170: 5 argument(s) from forward.txt', log)
self.assertIn("waited 3 step(s) for the tile, running now: ''", log)
# The first call refused: the second is asked.
subprocess.run([str(launch), '0', '80940031', 'PPSA99170', str(record)], timeout=10)
self.assertEqual([line.split(' ')[0] for line in record.read_text().splitlines()], ['lnc', 'system'])
# Not stamped (the marker is still there): nothing is launched.
result = subprocess.run([str(launch), '0', '0', '-', str(record)], capture_output=True, text=True,
timeout=10)
self.assertEqual(result.returncode, 1)
self.assertEqual(record.read_text(), '')
if __name__ == '__main__':
unittest.main()
+3 -2
View File
@@ -142,7 +142,8 @@
"name": "PS5 RetroArch (this port: title code, platform integration, build scripts and patches)",
"kind": "code",
"always": true,
"artifacts": ["eboot.bin", "retroarch.cfg", "LEGAL.txt", "sce_sys/param.json", "es-de/es-de.bin", "picker/picker.bin"],
"artifacts": ["eboot.bin", "retroarch.cfg", "LEGAL.txt", "sce_sys/param.json", "es-de/es-de.bin", "picker/picker.bin",
"forwarder/eboot.bin", "forwarder/launch.elf"],
"licence": "GPL-3.0-or-later",
"copyright": [
"Copyright (C) 2026 Mihawk",
@@ -180,7 +181,7 @@
"name": "PS5 payload SDK fork and its platform layer (libps5platform.a, headers, import stubs)",
"kind": "code",
"always": true,
"artifacts": ["eboot.bin", "cores/*.so", "es-de/es-de.bin"],
"artifacts": ["eboot.bin", "cores/*.so", "es-de/es-de.bin", "forwarder/eboot.bin", "forwarder/launch.elf"],
"licence": "GPL-3.0-or-later",
"copyright": ["Copyright (C) 2022-2026 John Törnblom and ps5-payload-dev contributors", "Copyright (C) 2026 Mihawk (the platform layer)"],
"texts": [
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# PS5 RetroArch - build the Game Forwarder's two programs (forwarder/): the app every home
# screen tile runs (eboot.bin, linked as tools/build.sh links the title's, with nothing of
# RetroArch in it) and the launch payload a tile sends to the console's ELF loader
# (launch.elf, built with the payload SDK as the WebUI's daemon is). The WebUI copies both
# into each tile it makes. Run after the title's build, whose native tool this uses.
# Prints the folder holding them.
set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
sdk="$root/.deps/native/ps5-payload-sdk"
native="$root/tooling/native"
tool="$root/build/host/ps5-native-tool"
out="$root/build/forwarder"
[[ -x $sdk/bin/prospero-clang ]] || { echo "error: no SDK at $sdk" >&2; exit 2; }
[[ -x $tool ]] || { echo "error: no $tool; build the title first (tools/build.sh)" >&2; exit 2; }
target=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["titleId"])' "$root/sce_sys/param.json")
[[ $target =~ ^[A-Z]{4}[0-9]{5}$ ]] || { echo "error: no title id in sce_sys/param.json" >&2; exit 2; }
rm -rf -- "$out"
mkdir -p "$out/obj"
# The launch payload: what it starts is this title.
PS5_PAYLOAD_SDK="$sdk" PS5_CLANG=${PS5_CLANG:-clang} "$sdk/bin/prospero-clang" -std=c11 -O2 -Wall -Wextra -Werror \
-DFORWARDER_TARGET="\"$target\"" -o "$out/launch.elf" "$root/forwarder/launch.c" \
-lSceSystemService -lSceUserService
grep -q "PS5RA-FORWARDER-TILE-ID-SLOT" "$out/launch.elf" ||
{ echo "error: launch.elf has no slot for a tile's title id" >&2; exit 1; }
# The tile's app.
cc() { PS5_PAYLOAD_SDK="$sdk" sh "$root/tooling/prospero-clang18" "$@"; }
cc -std=c11 -O2 -Wall -Wextra -Werror -ffunction-sections -fdata-sections \
-c "$root/forwarder/tile.c" -o "$out/obj/tile.o"
for part in app_crt app_cpp_runtime; do
cc -std=c++20 -O2 -Wall -Wextra -fno-exceptions -fno-rtti -ffunction-sections -fdata-sections \
-c "$native/$part.cpp" -o "$out/obj/$part.o"
done
"$sdk/bin/prospero-lld" -T "$native/ps5-pie.ld" -L "$native" --eh-frame-hdr --error-limit=0 \
--version-script "$native/app-symbols.map" --exclude-libs=ALL -e _start -o "$out/tile.elf" \
"$out/obj/app_crt.o" "$out/obj/app_cpp_runtime.o" "$out/obj/tile.o" --as-needed "$sdk"/target/lib/*.so
# As tools/build.sh: an import only libkernel_sys or libScePosixForWebKit defines is null at
# run time (the spike's tile called sceLncUtilLaunchApp, null for a title, and faulted at 0).
mapfile -t null_imports < <(comm -23 \
<("$sdk/bin/llvm-nm" -D --undefined-only "$out/tile.elf" | awk '$1 == "U" { sub(/@.*/, "", $2); print $2 }' |
sort -u) \
<(for stub in "$sdk"/target/lib/*.so; do
case $(basename "$stub") in libkernel_sys.so | libScePosixForWebKit.so) continue ;; esac
"$sdk/bin/llvm-nm" -D --defined-only "$stub" 2>/dev/null | awk '{ print $NF }'
done | sort -u))
((${#null_imports[@]} == 0)) || { echo "error: the tile imports what no module exports: ${null_imports[*]}" >&2; exit 1; }
"$tool" link --in "$out/tile.elf" --out "$out/tile.module.elf" --stub-dir "$sdk/target/lib" \
--module-sdk 0x02000009 --companion-sdk 0x08050001 --file-name eboot.elf >&2
"$tool" self --sign --in "$out/tile.module.elf" --out "$out/eboot.bin" --magic 0x1D3D154F >&2
printf '==> [forwarder] tile app %s bytes, launch payload %s bytes (starts %s)\n' \
"$(stat -c %s "$out/eboot.bin")" "$(stat -c %s "$out/launch.elf")" "$target" >&2
printf '%s\n' "$out"
+7
View File
@@ -331,6 +331,8 @@ inputs += sorted(p for p in (root / "third_party/qrcodegen").rglob("*") if p.is_
inputs += sorted(p for p in (root / "webui").rglob("*") if p.is_file())
inputs += sorted(p for p in (root / "daemon").rglob("*") if p.is_file())
inputs.append(root / "tools/build-webui-daemon.sh")
inputs += sorted(p for p in (root / "forwarder").rglob("*") if p.is_file())
inputs.append(root / "tools/build-forwarder.sh")
inputs.append(root / "tools/xmb-system-icons.py")
inputs.append(root / "build/lapy/build/owned_root_daemon-service-verified-client/lapy-root-daemon.elf")
inputs += sorted(p for p in (root / "build/webui-core-metadata").rglob("*") if p.is_file())
@@ -500,6 +502,11 @@ mkdir -p "$dist/webui"
cp -a "$root/webui/." "$dist/webui/"
cp "$webui_daemon" "$dist/webui/ps5-retroarch-webui.elf"
cp "$lapy_daemon" "$dist/lapy-root-daemon.elf"
# The Game Forwarder's programs (forwarder/, tools/build-forwarder.sh): the app every home
# screen tile runs and the payload it sends. The WebUI copies both into each tile it makes.
forwarder=$(bash "$root/tools/build-forwarder.sh")
mkdir -p "$dist/forwarder"
cp "$forwarder/eboot.bin" "$forwarder/launch.elf" "$dist/forwarder/"
# XMB's playlist tabs show the systems' hardware from the WebUI's artwork, named by
# database (issue 35); game rows keep XMB's own content icon.
python3 "$root/tools/xmb-system-icons.py" "$dist/assets/xmb/monochrome/png"
+2 -2
View File
@@ -26,8 +26,8 @@ import sys
from pathlib import Path
COVERED = ("lapy-root-daemon.elf", "eboot.bin", "es-de/*.bin", "picker/*.bin", "sce_module/*.prx", "cores/*.so", "cores/*.info",
"info/*.info")
EXECUTABLE = ("eboot.bin", "es-de.bin", "picker.bin", "lapy-root-daemon.elf", "*.prx", "*.so")
"info/*.info", "forwarder/eboot.bin", "forwarder/launch.elf")
EXECUTABLE = ("eboot.bin", "es-de.bin", "picker.bin", "lapy-root-daemon.elf", "launch.elf", "*.prx", "*.so")
def sha256(path):
+3 -2
View File
@@ -33,8 +33,9 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
TABLE = ROOT / "tooling/notices/components.json"
# The title's executables: eboot.bin, the frontends beside it, modules and cores.
EXECUTABLE = ("eboot.bin", "es-de.bin", "picker.bin", "lapy-root-daemon.elf", "*.prx", "*.so")
# The title's executables: eboot.bin, the frontends beside it, the Game Forwarder's two
# programs, modules and cores.
EXECUTABLE = ("eboot.bin", "es-de.bin", "picker.bin", "lapy-root-daemon.elf", "launch.elf", "*.prx", "*.so")
class NoticeError(Exception):