diff --git a/Makefile b/Makefile index ae2d774..fb9e64a 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/platform/ps5/ihs_thread_posix.c b/platform/ps5/ihs_thread_posix.c new file mode 100644 index 0000000..9326415 --- /dev/null +++ b/platform/ps5/ihs_thread_posix.c @@ -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 +#include + +#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; +} diff --git a/platform/ps5/ps5_compat.h b/platform/ps5/ps5_compat.h index 61acc23..6e10eb3 100644 --- a/platform/ps5/ps5_compat.h +++ b/platform/ps5/ps5_compat.h @@ -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 -#include +#include +#include +/* The SDK's does not pull these in; ihslib's POSIX backends expect it to. */ +#include +#include -#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 diff --git a/platform/ps5/ps5_runtime_shims.c b/platform/ps5/ps5_runtime_shims.c new file mode 100644 index 0000000..da1db95 --- /dev/null +++ b/platform/ps5/ps5_runtime_shims.c @@ -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 +#include + +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(); +} diff --git a/src/app_log.hpp b/src/app_log.hpp new file mode 100644 index 0000000..72e944a --- /dev/null +++ b/src/app_log.hpp @@ -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 + +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 diff --git a/src/app_storage.cpp b/src/app_storage.cpp new file mode 100644 index 0000000..4a1e808 --- /dev/null +++ b/src/app_storage.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(sizeof g_identity) && g_identity.device_id != 0) + { + log("identity loaded, device id %llu", static_cast(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(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(destination); + std::uint64_t seed = static_cast(time(nullptr)) ^ reinterpret_cast(destination); + for (std::size_t i = 0; i < length; i++) + { + seed = seed * 6364136223846793005ull + 1442695040888963407ull; + bytes[i] = static_cast(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(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(line_length)); + g_last_line = text; +} + +std::string log_last_line() +{ + std::lock_guard lock{g_log_mutex}; + return g_last_line; +} +} // namespace slp diff --git a/src/app_storage.hpp b/src/app_storage.hpp new file mode 100644 index 0000000..1a5084e --- /dev/null +++ b/src/app_storage.hpp @@ -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 +#include + +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 diff --git a/src/main.cpp b/src/main.cpp index e880b5d..e528b6d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 -#include +#include +#include +#include +#include +#include +#include +#include +#include namespace { -std::array banner{}; -std::array 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 video_frames{0}; + std::atomic video_bytes{0}; + std::atomic audio_frames{0}; + std::atomic 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(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 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(status.video_frames.load()), + static_cast(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(status.audio_frames.load()), + static_cast(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(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(context); + slp::log("authorized, steam id %llu", static_cast(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(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(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(context); + slp::log("streaming request failed: %d", static_cast(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(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(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(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(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(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"); } diff --git a/tooling/native/ps5-pie.ld b/tooling/native/ps5-pie.ld index 91fd169..6abc7df 100644 --- a/tooling/native/ps5-pie.ld +++ b/tooling/native/ps5-pie.ld @@ -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 diff --git a/tools/build-deps.sh b/tools/build-deps.sh new file mode 100755 index 0000000..90791ba --- /dev/null +++ b/tools/build-deps.sh @@ -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' ' ')" diff --git a/tools/build.sh b/tools/build.sh index 8330f37..35261d7 100644 --- a/tools/build.sh +++ b/tools/build.sh @@ -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