M1: ihslib, protobuf-c, mbedtls, opus and libc++ cross-built; the title discovers Steam

tools/build-deps.sh builds the streaming stack once into build/deps from explicit
file lists (ihslib's CMake wants pkg-config and SDL2). platform/ps5 holds the
pthread backend for ihslib, ProsperoLight's sceNet socket adapter and entropy
source, and an __assert shim. The app links the SDK's libc++/libc++abi/libunwind
(the linker script now brackets .eh_frame for libunwind; -L satisfies libc++'s
pthread dependent-library note with the SDK's empty libpthread.a).

src/main.cpp drives discovery, PIN pairing and the streaming request from a flow
thread and counts delivered video/audio frames; app_storage keeps the identity and
log under /data/steamlink-ps5.

Tested on the console: discovers the PC in 3 s, unauthorized stream request leads to
the pairing prompt with a PIN on screen.
This commit is contained in:
jmelovich committed 2026-10-08 17:24:28 -07:00
1 parent ae52676cfc
commit d3049b4bc6
11 files changed
+870 -58

No files matched your search

+11 -3
View File
@@ -8,8 +8,11 @@ SHELL := /bin/bash
-include .env
APP_DEFINITIONS ?=
APP_INCLUDE_PATHS ?=
APP_STATIC_ARCHIVES ?=
# The streaming stack from tools/build-deps.sh (archive order: users before providers).
APP_INCLUDE_PATHS ?= third_party/ihslib/include platform/ps5 third_party/opus/include third_party/mbedtls/include
APP_STATIC_ARCHIVES ?= build/deps/libihslib.a build/deps/libprotobuf-c.a build/deps/libopus.a \
build/deps/libmbedtls.a build/deps/libmbedx509.a build/deps/libmbedcrypto.a \
build/deps/libcxx.a build/deps/libcxxabi.a build/deps/libunwind.a
APP_RUNTIME_MODULES ?=
APP_WRAP_SYMBOLS ?=
APP_SOURCE_DIR ?=
@@ -140,7 +143,12 @@ $(RUNTIME): $(RUNTIME_INPUTS)
@printf '%s\n' '==> [libc] Generating the missing or outdated runtime'
@bash tools/rebuild-libc.sh
app: $(RUNTIME)
# The streaming stack (ihslib, protobuf-c, mbedtls, opus, platform/ps5) is built once
# into build/deps by tools/build-deps.sh and relinked into the app from there.
stream-deps:
@bash tools/build-deps.sh
app: $(RUNTIME) stream-deps
@printf '%s\n' '==> [app] Compiling, linking, signing, and assembling the app folder'
@bash tools/build.sh Folder
+117
View File
@@ -0,0 +1,117 @@
/*
* steamlink-ps5 - pthread backend for ihslib's thread abstraction.
* SPDX-License-Identifier: GPL-3.0-or-later
*
* ihslib ships an SDL2 backend; the PS5 has no SDL. Mutexes are recursive to
* match SDL's semantics, which ihslib relies on. Threads get an explicit stack:
* a title's default thread stack is not what a desktop library expects.
*/
#include "ihs_thread.h"
#include <pthread.h>
#include <stdlib.h>
#define IHS_PS5_THREAD_STACK_SIZE (512 * 1024)
struct IHS_Thread {
pthread_t tid;
IHS_ThreadFunction *function;
void *context;
};
struct IHS_Mutex {
pthread_mutex_t mutex;
};
struct IHS_Cond {
pthread_cond_t cond;
};
static void *ThreadEntry(void *arg) {
IHS_Thread *thread = arg;
thread->function(thread->context);
return NULL;
}
IHS_Thread *IHS_ThreadCreate(IHS_ThreadFunction *function, const char *name, void *context) {
(void) name;
IHS_Thread *thread = calloc(1, sizeof(IHS_Thread));
if (thread == NULL) {
return NULL;
}
thread->function = function;
thread->context = context;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, IHS_PS5_THREAD_STACK_SIZE);
int result = pthread_create(&thread->tid, &attr, ThreadEntry, thread);
pthread_attr_destroy(&attr);
if (result != 0) {
free(thread);
return NULL;
}
return thread;
}
void IHS_ThreadJoin(IHS_Thread *thread) {
if (thread == NULL) {
return;
}
pthread_join(thread->tid, NULL);
free(thread);
}
IHS_Mutex *IHS_MutexCreate() {
IHS_Mutex *mutex = calloc(1, sizeof(IHS_Mutex));
if (mutex == NULL) {
return NULL;
}
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex->mutex, &attr);
pthread_mutexattr_destroy(&attr);
return mutex;
}
void IHS_MutexDestroy(IHS_Mutex *mutex) {
if (mutex == NULL) {
return;
}
pthread_mutex_destroy(&mutex->mutex);
free(mutex);
}
bool IHS_MutexLock(IHS_Mutex *mutex) {
return pthread_mutex_lock(&mutex->mutex) == 0;
}
bool IHS_MutexUnlock(IHS_Mutex *mutex) {
return pthread_mutex_unlock(&mutex->mutex) == 0;
}
IHS_Cond *IHS_CondCreate() {
IHS_Cond *cond = calloc(1, sizeof(IHS_Cond));
if (cond == NULL) {
return NULL;
}
pthread_cond_init(&cond->cond, NULL);
return cond;
}
void IHS_CondDestroy(IHS_Cond *cond) {
if (cond == NULL) {
return;
}
pthread_cond_destroy(&cond->cond);
free(cond);
}
void IHS_CondSignal(IHS_Cond *cond) {
pthread_cond_signal(&cond->cond);
}
bool IHS_CondWait(IHS_Cond *cond, IHS_Mutex *mutex) {
return pthread_cond_wait(&cond->cond, &mutex->mutex) == 0;
}
+13 -21
View File
@@ -1,33 +1,22 @@
/*
* ps5-native-app-boilerplate - ProsperoLight component.
* ps5-native-app-boilerplate - ProsperoLight component, trimmed for steamlink-ps5.
* Copyright (C) 2026 BlackBearReloaded
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Force-included (-include) when compiling third-party networking code: a PS5
* socket is a libSceNet handle, so the descriptor calls below must go through
* the adapter in ps5_sockets.c rather than libc.
*/
#pragma once
#include <fcntl.h>
#include <pthread.h>
#include <poll.h>
#include <sys/select.h>
/* The SDK's <arpa/inet.h> does not pull these in; ihslib's POSIX backends expect it to. */
#include <netinet/in.h>
#include <sys/socket.h>
#include "ps5_thread_placement.h"
/*
* The generic FreeBSD path in moonlight-common-c renames every new thread.
* Calling the PS5 pthread_rename_np export from a newly started thread hangs
* before its entry point runs, so the name is never applied. The hook runs on
* the new thread itself, so it places that thread by name instead.
*/
static inline int ps5_pthread_setname_place(pthread_t thread, const char *name) {
(void)thread;
ps5_thread_placement_apply_name(name);
return 0;
}
#define pthread_setname_np ps5_pthread_setname_place
/* Portable by default; optional CPUID + XCR0 checked nanors dispatch. */
#include "ps5_fec_cpu.h"
/* PS5 network handles must be closed/configured through libSceNet. */
#ifndef PS5_SOCKET_ADAPTER_IMPLEMENTATION
#define close ps5_socket_close
#define fcntl ps5_socket_fcntl
@@ -37,4 +26,7 @@ static inline int ps5_pthread_setname_place(pthread_t thread, const char *name)
int ps5_socket_close(int socket);
int ps5_socket_fcntl(int socket, int command, ...);
int ps5_socket_ioctl(int socket, unsigned long request, ...);
int ps5_socket_poll(struct pollfd *descriptors, nfds_t count, int timeout_ms);
int ps5_socket_select(int descriptor_count, fd_set *read_set, fd_set *write_set,
fd_set *error_set, struct timeval *timeout);
#endif
+23
View File
@@ -0,0 +1,23 @@
/*
* steamlink-ps5 - libc entry points the title's runtime does not provide.
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Third-party C code (ihslib, mbedtls) reaches these through libc macros; the
* clean-room runtime shim resolves them to nothing, so they are defined here.
*/
#include <stdio.h>
#include <stdlib.h>
int sceKernelDebugOutText(int channel, const char *text);
/* assert(): report to the kernel log, then stop. */
__attribute__((noreturn)) void __assert(const char *function, const char *file, int line,
const char *expression)
{
char text[512];
snprintf(text, sizeof text, "[SLP5] assertion failed: %s (%s:%d, %s)\n", expression, file, line,
function ? function : "?");
sceKernelDebugOutText(0, text);
abort();
}
+16
View File
@@ -0,0 +1,16 @@
/*
* steamlink-ps5 - Logging: a file under the data directory plus the kernel log.
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <string>
namespace slp
{
// printf-style; one line per call, timestamped. Thread-safe.
void log(const char *format, ...) __attribute__((format(printf, 1, 2)));
// The most recent line, for the status screen.
std::string log_last_line();
} // namespace slp
+152
View File
@@ -0,0 +1,152 @@
/*
* steamlink-ps5 - Data directory, log file and client identity.
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "app_storage.hpp"
#include "app_log.hpp"
#include <cerrno>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <fcntl.h>
#include <mutex>
#include <string>
#include <sys/stat.h>
#include <unistd.h>
extern "C"
{
int sceKernelDebugOutText(int channel, const char *text);
int ps5_get_random(void *output, size_t length); // platform/ps5/ps5_entropy.c
}
namespace slp
{
namespace
{
std::string g_data_dir;
Identity g_identity{};
int g_log_fd = -1;
std::mutex g_log_mutex;
std::string g_last_line;
bool writable_directory(const std::string &path)
{
mkdir(path.c_str(), 0777);
const std::string probe = path + "/.probe";
const int fd = open(probe.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
return false;
close(fd);
unlink(probe.c_str());
return true;
}
void load_or_create_identity()
{
const std::string path = g_data_dir + "/identity.bin";
int fd = open(path.c_str(), O_RDONLY);
if (fd >= 0)
{
const ssize_t got = read(fd, &g_identity, sizeof g_identity);
close(fd);
if (got == static_cast<ssize_t>(sizeof g_identity) && g_identity.device_id != 0)
{
log("identity loaded, device id %llu", static_cast<unsigned long long>(g_identity.device_id));
return;
}
}
random_bytes(&g_identity, sizeof g_identity);
g_identity.device_id |= 1; // never zero
fd = open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd >= 0)
{
write(fd, &g_identity, sizeof g_identity);
close(fd);
log("identity created, device id %llu", static_cast<unsigned long long>(g_identity.device_id));
}
else
{
log("identity could not be saved (%s); pairing will not persist", std::strerror(errno));
}
}
} // namespace
void storage_init()
{
// ShadowMount+ 1.7beta4 and newer mount /data into the sandbox; /download0 is
// always there when param.json declares downloadDataSize.
for (const char *candidate : {"/data/steamlink-ps5", "/download0/steamlink-ps5", "/app0/steamlink-ps5"})
{
if (writable_directory(candidate))
{
g_data_dir = candidate;
break;
}
}
if (!g_data_dir.empty())
g_log_fd = open((g_data_dir + "/log.txt").c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
log("data directory: %s", g_data_dir.empty() ? "(none writable)" : g_data_dir.c_str());
if (!g_data_dir.empty())
load_or_create_identity();
else
random_bytes(&g_identity, sizeof g_identity);
}
const char *data_dir()
{
return g_data_dir.c_str();
}
const Identity &identity()
{
return g_identity;
}
void random_bytes(void *destination, std::size_t length)
{
if (ps5_get_random(destination, length) != 0)
{
// Last resort: never leave the buffer untouched.
auto *bytes = static_cast<unsigned char *>(destination);
std::uint64_t seed = static_cast<std::uint64_t>(time(nullptr)) ^ reinterpret_cast<std::uintptr_t>(destination);
for (std::size_t i = 0; i < length; i++)
{
seed = seed * 6364136223846793005ull + 1442695040888963407ull;
bytes[i] = static_cast<unsigned char>(seed >> 56);
}
}
}
void log(const char *format, ...)
{
char text[1024];
va_list args;
va_start(args, format);
const int length = std::vsnprintf(text, sizeof text, format, args);
va_end(args);
if (length < 0)
return;
// Seconds since the title started: the console's clock is not worth a localtime_r.
char line[1100];
static const std::time_t started = time(nullptr);
const long elapsed = static_cast<long>(time(nullptr) - started);
const int line_length = std::snprintf(line, sizeof line, "[SLP5] %5lds %s\n", elapsed, text);
std::lock_guard lock{g_log_mutex};
sceKernelDebugOutText(0, line);
if (g_log_fd >= 0)
write(g_log_fd, line, static_cast<size_t>(line_length));
g_last_line = text;
}
std::string log_last_line()
{
std::lock_guard lock{g_log_mutex};
return g_last_line;
}
} // namespace slp
+29
View File
@@ -0,0 +1,29 @@
/*
* steamlink-ps5 - Where the app keeps its files, and the client identity.
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <cstddef>
#include <cstdint>
namespace slp
{
// The pairing identity Steam remembers: a device id and the 32-byte secret the
// device token is derived from. Generated once, then persisted.
struct Identity
{
std::uint64_t device_id;
std::uint8_t secret_key[32];
};
// Picks the data directory (/data/steamlink-ps5 when ShadowMount+ mounts /data into
// the sandbox, otherwise /download0), opens the log, loads or creates the identity.
// Call first thing in main, on the main thread.
void storage_init();
const char *data_dir();
const Identity &identity();
// Console entropy (sysctl kern.rng_pseudo). Not for keys that leave the console.
void random_bytes(void *destination, std::size_t length);
} // namespace slp
+354 -33
View File
@@ -1,59 +1,380 @@
/*
* ps5-native-app-boilerplate - Minimal native application example.
* Copyright (C) 2026 BlackBearReloaded
* steamlink-ps5 - Steam Remote Play client for the PS5: M1 protocol bring-up.
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Draws a small CPU-rendered scene and keeps the process alive for normal
* shell-mediated closure.
* No controller input yet. A flow thread drives ihslib: discover the PC running
* Steam, pair with the PIN shown on screen when Steam asks for one, request a
* stream, then count the video and audio frames the session delivers. The main
* thread draws the state on the CPU renderer.
*/
#include "app_log.hpp"
#include "app_storage.hpp"
#include "demo_renderer.hpp"
extern "C"
{
#include "ihslib.h"
}
#include <array>
#include <span>
#include <atomic>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <mutex>
#include <pthread.h>
#include <string>
#include <unistd.h>
namespace
{
std::array<char, 48> banner{};
std::array<char, 48> build_label{};
constexpr char device_name[] = "PS5";
struct Status
{
std::mutex mutex;
std::string state = "STARTING";
std::string host;
std::string pin;
std::string video;
std::string audio;
std::atomic<std::uint64_t> video_frames{0};
std::atomic<std::uint64_t> video_bytes{0};
std::atomic<std::uint64_t> audio_frames{0};
std::atomic<std::uint64_t> audio_bytes{0};
};
Status status;
void set_state(const char *text)
{
std::lock_guard lock{status.mutex};
if (status.state == text)
return;
status.state = text;
slp::log("state: %s", text);
}
// The demo font has capitals only; everything shown goes through here.
std::string upper(std::string text)
{
for (char &c : text)
if (c >= 'a' && c <= 'z')
c = static_cast<char>(c - 'a' + 'A');
return text;
}
void draw_scene(ps5::demo::Canvas &canvas) noexcept
{
using ps5::demo::Color;
canvas.clear(Color::background);
canvas.rectangle(120, 430, 500, 470, Color::panel);
canvas.rectangle(710, 430, 500, 470, Color::panel);
canvas.rectangle(1300, 430, 500, 470, Color::panel);
canvas.rectangle(120, 375, 1680, 8, Color::white);
canvas.text(120, 80, "STEAM REMOTE PLAY", 10, Color::white);
canvas.rectangle(120, 200, 1680, 6, Color::white);
canvas.text(120, 90, "HELLO WORLD", 14, Color::white);
canvas.text(120, 245, "MODERN CXX20 RAII", 6, Color::white);
canvas.text(120, 315, banner.data(), 5, Color::cyan);
std::array<char, 96> line{};
{
std::lock_guard lock{status.mutex};
canvas.text(120, 260, upper(status.state), 5, Color::cyan);
if (!status.host.empty())
canvas.text(120, 340, upper("HOST " + status.host), 4, Color::white);
if (!status.pin.empty())
{
canvas.text(120, 440, "ENTER THIS PIN IN STEAM ON YOUR PC", 4, Color::yellow);
canvas.text(120, 500, status.pin, 12, Color::yellow);
}
if (!status.video.empty())
canvas.text(120, 700, upper(status.video), 4, Color::white);
if (!status.audio.empty())
canvas.text(120, 760, upper(status.audio), 4, Color::white);
}
std::snprintf(line.data(), line.size(), "VIDEO FRAMES %llu BYTES %llu",
static_cast<unsigned long long>(status.video_frames.load()),
static_cast<unsigned long long>(status.video_bytes.load()));
canvas.text(120, 840, line.data(), 4, Color::white);
std::snprintf(line.data(), line.size(), "AUDIO FRAMES %llu BYTES %llu",
static_cast<unsigned long long>(status.audio_frames.load()),
static_cast<unsigned long long>(status.audio_bytes.load()));
canvas.text(120, 900, line.data(), 4, Color::white);
canvas.text(120, 1000, upper(slp::log_last_line()), 3, Color::magenta);
}
canvas.circle(370, 665, 130, Color::cyan);
canvas.rectangle(840, 535, 240, 240, Color::yellow);
canvas.triangle(1550, 520, 170, 285, Color::magenta);
void ihs_log(IHS_LogLevel level, const char *tag, const char *message)
{
if (level > IHS_LogLevelInfo)
return;
slp::log("[IHS.%s %s] %s", tag, IHS_LogLevelName(level), message);
}
canvas.text(250, 830, "CIRCLE", 5, Color::white);
canvas.text(870, 830, "SQUARE", 5, Color::white);
canvas.text(1420, 830, "TRIANGLE", 5, Color::white);
// ---- client phase: discovery, authorization, streaming request -------------
// A build that is not a release says which one it is (docs/PULL_REQUEST_BUILDS.md).
if (build_label[0] != '\0')
canvas.text(120, 960, build_label.data(), 4, Color::yellow);
struct ClientFlow
{
IHS_ClientConfig config{};
IHS_HostInfo host{};
bool host_chosen = false;
bool authorization_tried = false;
bool have_session = false;
IHS_SessionInfo session{};
char pin[8] = {};
};
void request_stream(IHS_Client *client, ClientFlow &flow)
{
IHS_StreamingRequest request{};
request.streamingEnable.video = true;
request.streamingEnable.audio = true;
request.streamingEnable.input = true;
request.maxResolution.x = 1920;
request.maxResolution.y = 1080;
request.audioChannelCount = 2;
request.streamingInterface = IHS_StreamInterfaceBigPicture;
set_state("REQUESTING STREAM");
if (!IHS_ClientStreamingRequest(client, &flow.host, &request))
slp::log("IHS_ClientStreamingRequest failed: %s", IHS_ClientError(client));
}
void on_discovered(IHS_Client *client, const IHS_HostInfo *host, void *context)
{
auto &flow = *static_cast<ClientFlow *>(context);
char *address = IHS_IPAddressToString(&host->address.ip);
slp::log("discovered %s at %s:%u (games running: %d)", host->hostname, address, host->address.port,
host->gamesRunning);
if (flow.host_chosen)
{
free(address);
return;
}
flow.host_chosen = true;
flow.host = *host;
{
std::lock_guard lock{status.mutex};
status.host = std::string{host->hostname} + " " + address;
}
free(address);
request_stream(client, flow);
}
void on_authorization_progress(IHS_Client *, const IHS_HostInfo *, void *)
{
set_state("WAITING FOR PIN ENTRY ON THE PC");
}
void on_authorization_success(IHS_Client *client, const IHS_HostInfo *, std::uint64_t steam_id, void *context)
{
auto &flow = *static_cast<ClientFlow *>(context);
slp::log("authorized, steam id %llu", static_cast<unsigned long long>(steam_id));
{
std::lock_guard lock{status.mutex};
status.pin.clear();
}
request_stream(client, flow);
}
void on_authorization_failed(IHS_Client *client, const IHS_HostInfo *, IHS_AuthorizationResult result, void *)
{
slp::log("authorization failed: %d", static_cast<int>(result));
set_state("PAIRING FAILED");
{
std::lock_guard lock{status.mutex};
status.pin.clear();
}
IHS_ClientStop(client);
}
void on_streaming_progress(IHS_Client *, const IHS_HostInfo *, void *)
{
set_state("STEAM IS STARTING THE STREAM");
}
void on_streaming_success(IHS_Client *client, const IHS_HostInfo *, const IHS_SocketAddress *address,
const std::uint8_t *session_key, std::size_t session_key_length, void *context)
{
auto &flow = *static_cast<ClientFlow *>(context);
flow.session.address = *address;
flow.session.sessionKeyLen = session_key_length;
std::memcpy(flow.session.sessionKey, session_key, session_key_length);
flow.session.steamId = 0;
flow.have_session = true;
char *text = IHS_IPAddressToString(&address->ip);
slp::log("stream granted at %s:%u, session key %zu bytes", text, address->port, session_key_length);
free(text);
IHS_ClientStop(client);
}
void on_streaming_failed(IHS_Client *client, const IHS_HostInfo *, IHS_StreamingResult result, void *context)
{
auto &flow = *static_cast<ClientFlow *>(context);
slp::log("streaming request failed: %d", static_cast<int>(result));
if ((result == IHS_StreamingUnauthorized || result == IHS_StreamingPINRequired) && !flow.authorization_tried)
{
flow.authorization_tried = true;
std::uint8_t random[2];
slp::random_bytes(random, sizeof random);
std::snprintf(flow.pin, sizeof flow.pin, "%04u", (random[0] | (random[1] << 8)) % 10000u);
{
std::lock_guard lock{status.mutex};
status.pin = flow.pin;
}
set_state("PAIRING: STEAM > SETTINGS > REMOTE PLAY > PAIR STEAM LINK");
if (!IHS_ClientAuthorizationRequest(client, &flow.host, flow.pin))
{
slp::log("IHS_ClientAuthorizationRequest failed: %s", IHS_ClientError(client));
IHS_ClientStop(client);
}
return;
}
set_state("STREAM REQUEST FAILED");
IHS_ClientStop(client);
}
// ---- session phase: count what the host sends ------------------------------
int on_video_start(IHS_Session *, const IHS_StreamVideoConfig *config, void *)
{
slp::log("video start: %ux%u codec %d, %zu bytes codec data", config->width, config->height,
static_cast<int>(config->codec), config->codecDataLen);
std::lock_guard lock{status.mutex};
status.video = "video " + std::to_string(config->width) + "x" + std::to_string(config->height) + " codec " +
std::to_string(static_cast<int>(config->codec));
return 0;
}
IHS_StreamVideoSubmitResult on_video_submit(IHS_Session *, IHS_Buffer *data, IHS_StreamVideoFrameFlag, void *)
{
status.video_frames.fetch_add(1);
status.video_bytes.fetch_add(data->size);
return IHS_StreamVideoSubmitOK;
}
void on_video_stop(IHS_Session *, void *)
{
slp::log("video stop");
}
int on_audio_start(IHS_Session *, const IHS_StreamAudioConfig *config, void *)
{
slp::log("audio start: %u Hz, %u channels, codec %d", config->frequency, config->channels,
static_cast<int>(config->codec));
std::lock_guard lock{status.mutex};
status.audio = "audio " + std::to_string(config->frequency) + " hz " + std::to_string(config->channels) +
" ch codec " + std::to_string(static_cast<int>(config->codec));
return 0;
}
int on_audio_submit(IHS_Session *, IHS_Buffer *data, void *)
{
status.audio_frames.fetch_add(1);
status.audio_bytes.fetch_add(data->size);
return 0;
}
void on_audio_stop(IHS_Session *, void *)
{
slp::log("audio stop");
}
void on_session_configuring(IHS_Session *, IHS_SessionConfig *config, void *)
{
config->enableAudio = true;
config->enableHevc = false;
}
void on_session_connected(IHS_Session *, void *)
{
set_state("STREAMING (COUNTING FRAMES)");
}
void on_session_disconnected(IHS_Session *, void *)
{
set_state("SESSION ENDED");
}
bool set_cursor(IHS_Session *, std::uint64_t, void *)
{
return true;
}
void *flow_thread(void *)
{
ClientFlow flow;
const slp::Identity &identity = slp::identity();
flow.config.deviceId = identity.device_id;
flow.config.secretKey = identity.secret_key;
flow.config.deviceName = device_name;
IHS_Init();
for (;;)
{
flow.host_chosen = false;
flow.have_session = false;
set_state("LOOKING FOR A PC RUNNING STEAM");
IHS_Client *client = IHS_ClientCreate(&flow.config);
IHS_ClientSetLogFunction(client, ihs_log);
IHS_ClientDiscoveryCallbacks discovery{};
discovery.discovered = on_discovered;
IHS_ClientAuthorizationCallbacks authorization{};
authorization.progress = on_authorization_progress;
authorization.success = on_authorization_success;
authorization.failed = on_authorization_failed;
IHS_ClientStreamingCallbacks streaming{};
streaming.progress = on_streaming_progress;
streaming.success = on_streaming_success;
streaming.failed = on_streaming_failed;
IHS_ClientSetDiscoveryCallbacks(client, &discovery, &flow);
IHS_ClientSetAuthorizationCallbacks(client, &authorization, &flow);
IHS_ClientSetStreamingCallbacks(client, &streaming, &flow);
if (!IHS_ClientStartDiscovery(client, 0))
slp::log("discovery failed to start: %s", IHS_ClientError(client));
IHS_ClientThreadedJoin(client);
IHS_ClientDestroy(client);
if (flow.have_session)
{
IHS_Session *session = IHS_SessionCreate(&flow.config, &flow.session);
IHS_SessionSetLogFunction(session, ihs_log);
IHS_StreamSessionCallbacks session_callbacks{};
session_callbacks.configuring = on_session_configuring;
session_callbacks.connected = on_session_connected;
session_callbacks.disconnected = on_session_disconnected;
IHS_StreamVideoCallbacks video_callbacks{};
video_callbacks.start = on_video_start;
video_callbacks.submit = on_video_submit;
video_callbacks.stop = on_video_stop;
IHS_StreamAudioCallbacks audio_callbacks{};
audio_callbacks.start = on_audio_start;
audio_callbacks.submit = on_audio_submit;
audio_callbacks.stop = on_audio_stop;
IHS_StreamInputCallbacks input_callbacks{};
input_callbacks.setCursor = set_cursor;
IHS_SessionSetSessionCallbacks(session, &session_callbacks, nullptr);
IHS_SessionSetVideoCallbacks(session, &video_callbacks, nullptr);
IHS_SessionSetAudioCallbacks(session, &audio_callbacks, nullptr);
IHS_SessionSetInputCallbacks(session, &input_callbacks, nullptr);
set_state("CONNECTING SESSION");
if (IHS_SessionConnect(session))
IHS_SessionThreadedJoin(session);
else
slp::log("IHS_SessionConnect failed");
IHS_SessionDestroy(session);
}
sleep(5);
}
return nullptr;
}
} // namespace
int main()
{
ps5::demo::read_asset_text("/app0/assets/banner.txt", std::span{banner}, "APP0 ASSET FAILED");
// Written by the build when BUILD_LABEL is set; the demo font has capitals only.
ps5::demo::read_asset_text("/app0/build-label.txt", std::span{build_label}, "");
for (char &character : build_label)
{
if (character >= 'a' && character <= 'z')
character = static_cast<char>(character - 'a' + 'A');
}
ps5::demo::run(draw_scene, banner.data());
slp::storage_init();
slp::log("Steam Remote Play for PS5 starting; data in %s", slp::data_dir());
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 1024 * 1024);
pthread_t thread;
if (pthread_create(&thread, &attr, flow_thread, nullptr) != 0)
slp::log("could not start the flow thread");
pthread_attr_destroy(&attr);
ps5::demo::run_frames(draw_scene, "Steam Remote Play");
}
+8 -1
View File
@@ -26,10 +26,17 @@ SECTIONS {
*(.plt .plt.*)
} : text
/* The bracket symbols are what libunwind (pulled in by libc++abi) looks up. */
.eh_frame_hdr : ALIGN(CONSTANT(MAXPAGESIZE)) {
PROVIDE_HIDDEN(__eh_frame_hdr_start = .);
KEEP(*(.eh_frame_hdr))
PROVIDE_HIDDEN(__eh_frame_hdr_end = .);
} : ro
.eh_frame : {
PROVIDE_HIDDEN(__eh_frame_start = .);
KEEP(*(.eh_frame))
PROVIDE_HIDDEN(__eh_frame_end = .);
} : ro
.eh_frame : { KEEP(*(.eh_frame)) } : ro
.gcc_except_table : { *(.gcc_except_table .gcc_except_table.*) } : ro
.rodata : { *(.rodata .rodata.*) } : ro
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env bash
# steamlink-ps5 - Cross-build the streaming dependencies once, into build/deps.
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Everything the app links besides its own sources: protobuf-c's runtime, mbedtls
# (the crypto ihslib's authorization and session need), opus (the audio decoder),
# ihslib itself (built from an explicit file list: its CMake wants pkg-config and
# SDL2, neither of which exists for the console) and the PS5 platform layer under
# platform/ps5 (sceNet socket adapter, entropy, pthread backend). Each archive is
# rebuilt only when its inputs change.
#
# tools/build-deps.sh build what is missing or stale
# tools/build-deps.sh --clean remove build/deps first
set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
bash "$root/tools/setup-native-dependencies.sh" >/dev/null
sdk=${PS5_PAYLOAD_SDK:-$root/.deps/native/ps5-payload-sdk}
cc="$sdk/bin/prospero-clang"
ar="$sdk/bin/llvm-ar"
ranlib="$sdk/bin/llvm-ranlib"
out="$root/build/deps"
jobs=${BUILD_JOBS:-$(nproc 2>/dev/null || echo 2)}
ihslib="$root/third_party/ihslib"
protobuf_c="$root/third_party/protobuf-c"
mbedtls="$root/third_party/mbedtls"
opus="$root/third_party/opus"
platform="$root/platform/ps5"
[[ ${1:-} == --clean ]] && rm -rf -- "$out"
[[ -x $cc && -x $ar ]] || { echo "build-deps.sh: PS5 payload SDK not found at $sdk" >&2; exit 2; }
[[ -f $ihslib/include/ihslib.h && -f $protobuf_c/protobuf-c/protobuf-c.c && -f $mbedtls/Makefile && -f $opus/CMakeLists.txt ]] || {
echo "build-deps.sh: submodules missing; run: git submodule update --init --recursive" >&2
exit 2
}
mkdir -p "$out/obj"
# An archive is up to date when the hash of its inputs matches the one recorded
# beside it. Hash file contents, not timestamps: git checkouts reset the latter.
stamp_of() { sha256sum "$@" | sha256sum | cut -d' ' -f1; }
fresh() {
local archive=$1 stamp=$2
[[ -f $archive && -f $archive.stamp && $(<"$archive.stamp") == "$stamp" ]]
}
cflags=(-O2 -ffunction-sections -fdata-sections -fPIC)
mbedtls_config=(-DMBEDTLS_CONFIG_FILE='"mbedtls_ps5_config.h"' "-I$platform")
# ---- protobuf-c runtime ----------------------------------------------------
archive="$out/libprotobuf-c.a"
stamp=$(stamp_of "$protobuf_c/protobuf-c/protobuf-c.c" "$protobuf_c/protobuf-c/protobuf-c.h" "$0")
if ! fresh "$archive" "$stamp"; then
echo "==> [deps] protobuf-c"
"$cc" "${cflags[@]}" -std=c11 -w -I"$protobuf_c" -c "$protobuf_c/protobuf-c/protobuf-c.c" -o "$out/obj/protobuf-c.o"
rm -f "$archive" && "$ar" rcs "$archive" "$out/obj/protobuf-c.o" && echo "$stamp" > "$archive.stamp"
fi
# ---- mbedtls ---------------------------------------------------------------
archive="$out/libmbedcrypto.a"
stamp=$(stamp_of "$platform/mbedtls_ps5_config.h" "$platform/psa_crypto_ps5_config.h" "$mbedtls/include/mbedtls/build_info.h" "$0" \
$(find "$mbedtls/library" -type f -name '*.c' | sort))
if ! fresh "$archive" "$stamp"; then
echo "==> [deps] mbedtls (library only)"
work="$out/mbedtls-build"
rm -rf "$work" && cp -a "$mbedtls" "$work"
# The checkout already holds its generated sources; the generators would need perl
# and python jsonschema on the host. A git checkout gives the templates arbitrary
# mtimes, so mark the generated files newer than them and turn generation off.
touch "$work/library/psa_crypto_driver_wrappers.h" "$work/library/psa_crypto_driver_wrappers_no_static.c" \
"$work/library/error.c" "$work/library/version_features.c" "$work/library/ssl_debug_helpers_generated.c"
make -C "$work" lib -j "$jobs" CC="$cc" AR="$ar" GEN_FILES= \
CFLAGS="${cflags[*]} -DMBEDTLS_CONFIG_FILE=\\\"mbedtls_ps5_config.h\\\" -I$platform" \
WARNING_CFLAGS='-w' >"$out/mbedtls.log" 2>&1 || { tail -30 "$out/mbedtls.log" >&2; exit 1; }
cp "$work/library/libmbedcrypto.a" "$work/library/libmbedx509.a" "$work/library/libmbedtls.a" "$out/"
rm -rf "$work"
echo "$stamp" > "$archive.stamp"
fi
# ---- opus (decoder only is not an option upstream; the whole library is small) ----
archive="$out/libopus.a"
stamp=$(stamp_of "$opus/CMakeLists.txt" "$0" $(find "$opus/src" "$opus/celt" "$opus/silk" -maxdepth 1 -type f -name '*.c' | sort))
if ! fresh "$archive" "$stamp"; then
echo "==> [deps] opus"
work="$out/opus-build"
cmake -S "$opus" -B "$work" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER="$cc" -DCMAKE_AR="$ar" -DCMAKE_RANLIB="$ranlib" \
-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \
-DCMAKE_C_FLAGS_RELEASE="${cflags[*]} -DNDEBUG" \
-DOPUS_BUILD_SHARED_LIBRARY=OFF -DOPUS_BUILD_TESTING=OFF -DOPUS_BUILD_PROGRAMS=OFF \
-DOPUS_DISABLE_INTRINSICS=ON -DOPUS_X86_MAY_HAVE_AVX2=OFF -DOPUS_X86_PRESUME_AVX2=OFF \
-DOPUS_X86_PRESUME_SSE4_1=OFF -DOPUS_STACK_PROTECTOR=OFF -DOPUS_FORTIFY_SOURCE=OFF \
>"$out/opus.log" 2>&1 || { tail -30 "$out/opus.log" >&2; exit 1; }
cmake --build "$work" --target opus --parallel "$jobs" >>"$out/opus.log" 2>&1 || { tail -30 "$out/opus.log" >&2; exit 1; }
cp "$work/libopus.a" "$archive"
rm -rf "$work"
echo "$stamp" > "$archive.stamp"
fi
# ---- ihslib + PS5 platform layer ------------------------------------------
# Sources: everything under src/ except the SDL backends (threads, UDP, HID) and the
# OpenSSL crypto; the console gets the pthread backend from platform/ps5.
mapfile -t ihslib_sources < <(find "$ihslib/src" -type f -name '*.c' \
! -path '*/hid/sdl/*' ! -name 'ihs_thread_sdl.c' ! -name 'ihs_udp_sdl.c' ! -name 'impl_openssl.c' | sort)
platform_sources=("$platform/ihs_thread_posix.c" "$platform/ps5_sockets.c" "$platform/ps5_entropy.c" "$platform/ps5_runtime_shims.c")
archive="$out/libihslib.a"
stamp=$(stamp_of "${ihslib_sources[@]}" "${platform_sources[@]}" "$platform/ps5_compat.h" "$platform/mbedtls_ps5_config.h" "$0" \
$(find "$ihslib/include" "$ihslib/src" -type f -name '*.h' | sort))
if ! fresh "$archive" "$stamp"; then
echo "==> [deps] ihslib + platform (${#ihslib_sources[@]} + ${#platform_sources[@]} files)"
objects=()
# Socket descriptor calls inside ihslib route through the sceNet adapter (ps5_compat.h);
# the adapter itself and the rest of the platform layer see the real libc names.
common=("${cflags[@]}" -std=gnu11 -w "-I$ihslib/include" "-I$ihslib/src" "-I$protobuf_c" "-I$mbedtls/include" "-I$platform" "${mbedtls_config[@]}")
i=0
for source in "${ihslib_sources[@]}"; do
object="$out/obj/ihs-$(printf '%03d' "$i")-$(basename "${source%.c}").o"
"$cc" "${common[@]}" -include "$platform/ps5_compat.h" -c "$source" -o "$object" &
objects+=("$object"); i=$((i + 1))
(( i % jobs == 0 )) && wait
done
wait
# set -e does not see a failed background compile: check the objects exist.
for object in "${objects[@]}"; do
[[ -f $object ]] || { echo "build-deps.sh: compile failed: $object" >&2; exit 1; }
done
for source in "${platform_sources[@]}"; do
object="$out/obj/platform-$(basename "${source%.c}").o"
extra=()
[[ $(basename "$source") == ps5_sockets.c ]] && extra=(-DPS5_SOCKET_ADAPTER_IMPLEMENTATION)
"$cc" "${common[@]}" "${extra[@]}" -c "$source" -o "$object"
objects+=("$object")
done
rm -f "$archive" && "$ar" rcs "$archive" "${objects[@]}" && echo "$stamp" > "$archive.stamp"
fi
# ---- libc++ ---------------------------------------------------------------
# The app uses std::string, std::mutex and friends, whose non-inline parts live in
# the SDK's libc++. Copied under names the app build's path check accepts.
cp "$sdk/target/lib/libc++.a" "$out/libcxx.a"
cp "$sdk/target/lib/libc++abi.a" "$out/libcxxabi.a"
cp "$sdk/target/lib/libunwind.a" "$out/libunwind.a"
echo "==> [deps] ready: $(cd "$out" && ls *.a | tr '\n' ' ')"
+3
View File
@@ -257,7 +257,10 @@ if [[ -n ${pacbrew_root:-} ]]; then
ninja_inputs+=("$input")
done < <(find "$pacbrew_root" -type f \( -name '*.a' -o -name '*.so' \) -print0 | sort -z)
fi
# -L: libc++'s objects carry a dependent-library note for pthread; the SDK's
# libpthread.a is empty (threads come from libkernel) and satisfies it.
ninja_edge LINK "$build/llvm-pie.elf" "$sdk_root/bin/prospero-lld" -T "$native/ps5-pie.ld" --eh-frame-hdr \
-L "$sdk_root/target/lib" \
"${wrap_options[@]}" --version-script "$native/app-symbols.map" \
-e _start -o "$build/llvm-pie.elf" "${link_inputs[@]}" \
--as-needed "$sdk_root"/target/lib/*.so