mirror of
https://github.com/MrLowPr/overdrive.git
synced 2026-10-17 14:01:04 +02:00
742 lines
34 KiB
C++
742 lines
34 KiB
C++
// Copyright (C) 2026 half144 and PS5 React contributors
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Additional attribution term: see LICENSE-ATTRIBUTION.
|
|
// Independent React proof: PS5 lifecycle/display/input, software UI and GL texture.
|
|
|
|
#include "network.hpp"
|
|
#include "archives.hpp"
|
|
#include "packages.hpp"
|
|
#include "removals.hpp"
|
|
#include "browser_capture.hpp"
|
|
#include "lan.hpp"
|
|
#include "render_workers.hpp"
|
|
#include "thread_name.hpp"
|
|
#include "app_config.hpp"
|
|
#include "async_log.hpp"
|
|
#include "platform/ps5/display_egl.hpp"
|
|
#include "platform/ps5/pad.hpp"
|
|
#include "platform/ps5/system.hpp"
|
|
#include "core/input.hpp"
|
|
#include "damage_tracker.hpp"
|
|
#include "frame_stats.hpp"
|
|
#include "gl_presenter.hpp"
|
|
#include "image_loader.hpp"
|
|
#include "host_api.hpp"
|
|
#include "host_platform.hpp"
|
|
#include "actions.hpp"
|
|
#include "input_script.hpp"
|
|
#include "js_heap.hpp"
|
|
#include "js_profiler.hpp"
|
|
#include "payload_loader.hpp"
|
|
#include "screenshot.hpp"
|
|
#include "text_shaper.hpp"
|
|
#include "video_api.hpp"
|
|
#include "layouts.h"
|
|
#include "video_output.hpp"
|
|
#include "video_frame.hpp"
|
|
#include "filesystem_access.hpp"
|
|
#include <unistd.h>
|
|
#include <EGL/egl.h>
|
|
#include <ps5_opengl_display_modes.h>
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cerrno>
|
|
#include <climits>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <cstdint>
|
|
#include <iterator>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <pthread.h>
|
|
#include <sys/resource.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
extern "C" {
|
|
#include "er_runtime.h"
|
|
#include "er_scene.h"
|
|
#include "native_renderer.h"
|
|
#include "software_backend.h"
|
|
std::uint64_t sceKernelReadTsc(void);
|
|
std::uint64_t sceKernelGetTscFrequency(void);
|
|
void hui_heap_capacity(std::size_t* size, std::size_t* flexible_before, std::size_t* flexible_after);
|
|
void hui_heap_stats(std::size_t* live_bytes, std::size_t* peak_bytes, std::size_t* blocks, std::size_t* failures);
|
|
}
|
|
|
|
// Extraction steps go to the console log, so the crash log keeps the last one before a fault.
|
|
bool archives::list_directory(const char* path, void (*visit)(const char*, void*), void* user) { return host::read_dir(path, visit, user); }
|
|
void archives::trace(const char* step, const char* detail) { async_log::write("[PS5-REACT] archive %s %s", step, detail); }
|
|
// The extraction's write ring: larger on larger heaps, and none (inline writes, as before) unless the
|
|
// heap still has room for the 64 MiB decoder budget the preflight admits, the ring, and a margin.
|
|
std::size_t archives::pipeline_bytes() {
|
|
constexpr std::size_t MiB = 1024 * 1024;
|
|
std::size_t size = 0, live = 0, peak = 0, blocks = 0, failures = 0;
|
|
hui_heap_capacity(&size, nullptr, nullptr);
|
|
hui_heap_stats(&live, &peak, &blocks, &failures);
|
|
const std::size_t ring = size >= 256 * MiB ? 32 * MiB : size >= 192 * MiB ? 16 * MiB : 8 * MiB;
|
|
return live + 64 * MiB + ring + 16 * MiB <= size ? ring : 0;
|
|
}
|
|
// The image caches drop to their low budgets for an in-process extraction (the render thread trims
|
|
// within a frame or two; images on screen stay), then the heap must still hold the decoder budget and
|
|
// the margin. pipeline_bytes() sizes the ring from what is left after the trim.
|
|
bool archives::hold_memory(bool hold) {
|
|
images::hold_memory(hold);
|
|
if (!hold) return true;
|
|
for (int i = 0; i < 25 && images::memory().decoded > images::kLowCacheBytes; ++i) usleep(10'000);
|
|
constexpr std::size_t MiB = 1024 * 1024;
|
|
std::size_t size = 0, live = 0, peak = 0, blocks = 0, failures = 0;
|
|
hui_heap_capacity(&size, nullptr, nullptr);
|
|
hui_heap_stats(&live, &peak, &blocks, &failures);
|
|
const bool room = !size || live + 64 * MiB + 16 * MiB <= size;
|
|
async_log::write("[PS5-REACT] extraction memory: heap=%zu/%zuMiB img decoded=%zuKiB room=%d", live >> 20, size >> 20,
|
|
images::memory().decoded >> 10, room);
|
|
return room;
|
|
}
|
|
bool archives::downloading() { return network::downloading(); }
|
|
// rar-extract.elf goes to the payload loader as the package installer does, and reports on the socket.
|
|
int archives::start_rar_worker(const std::string& request, std::string& error) {
|
|
const int socket = payload_loader::send("rar-extract.elf", request, 5'000'000, "Archives.extract", error);
|
|
if (socket >= 0) payload_loader::receive_timeout(socket, 250'000);
|
|
return socket;
|
|
}
|
|
long archives::read_rar_worker(int stream, char* data, std::size_t size) {
|
|
const int count = payload_loader::receive(stream, data, size);
|
|
return count < 0 ? -1 : count;
|
|
}
|
|
void archives::cancel_rar_worker(int stream) { payload_loader::write(stream, "cancel\n", 7); }
|
|
void archives::close_rar_worker(int stream) { payload_loader::close(stream); }
|
|
|
|
namespace {
|
|
// Startup steps log their time since main(), to show where launch time goes.
|
|
std::int64_t launch_us = 0;
|
|
long long since_launch_ms() { return (hui::sys::monotonic_us() - launch_us) / 1000; }
|
|
|
|
void log_memory() {
|
|
std::size_t live = 0, peak = 0, blocks = 0, failures = 0, size = 0;
|
|
hui_heap_stats(&live, &peak, &blocks, &failures);
|
|
hui_heap_capacity(&size, nullptr, nullptr);
|
|
const images::Memory image = images::memory();
|
|
async_log::write("[PS5-REACT] memory: heap=%zu/%zuMiB peak=%zuMiB failed=%zu img encoded=%zuKiB decoded=%zuKiB",
|
|
live >> 20, size >> 20, peak >> 20, failures, image.encoded >> 10, image.decoded >> 10);
|
|
async_log::write("[PS5-REACT] images: %s", images::stats().c_str());
|
|
}
|
|
|
|
// A frozen screen on a tester's console says where the render loop hangs: past three seconds without
|
|
// a turn, the log names the loop's phase, the native call it is in and how far script got meanwhile
|
|
// (QuickJS polls its interrupt handler while it runs, so rising js_ticks with no call is a script loop).
|
|
std::atomic<std::int64_t> loop_beat{0};
|
|
std::atomic<const char*> loop_phase{"start"};
|
|
std::atomic<std::uint64_t> js_ticks{0};
|
|
std::atomic<bool> watching{true};
|
|
|
|
void beat(const char* phase) {
|
|
loop_phase.store(phase, std::memory_order_relaxed);
|
|
loop_beat.store(hui::sys::monotonic_us(), std::memory_order_relaxed);
|
|
}
|
|
|
|
void* watch_loop(void*) {
|
|
name_thread("stall-watch");
|
|
std::int64_t stalled_at = 0, next_report = 0;
|
|
std::uint64_t ticks = 0;
|
|
while (watching) {
|
|
usleep(500'000);
|
|
const std::int64_t now = hui::sys::monotonic_us(), last = loop_beat.load(std::memory_order_relaxed);
|
|
if (stalled_at && last != stalled_at) {
|
|
async_log::write("[PS5-REACT] stall: loop turned again after %lldms", static_cast<long long>((last-stalled_at)/1000));
|
|
stalled_at = 0;
|
|
}
|
|
if (now - last < 3'000'000) continue;
|
|
if (!stalled_at) { stalled_at = last; next_report = now; ticks = js_ticks; }
|
|
if (now < next_report) continue;
|
|
const char* call = host::current_call.load();
|
|
const std::uint64_t seen = js_ticks;
|
|
async_log::write("[PS5-REACT] stall: loop stuck %llds phase=%s call=%s js_ticks=+%llu",
|
|
static_cast<long long>((now-last)/1000000), loop_phase.load(std::memory_order_relaxed),
|
|
call ? call : "none", static_cast<unsigned long long>(seen-ticks));
|
|
ticks = seen; next_report = now + 5'000'000;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Unused decoded images kept for scrolling back. Browsing peaks near 110 MiB live with 24 MiB of them
|
|
// and a 64-connection download, so a 128 MiB heap keeps 24 MiB; a 256 MiB heap holds 64 MiB (about 160
|
|
// grid covers, or a hero and 140 covers) and still leaves over 80 MiB free at that peak.
|
|
std::size_t image_cache_bytes() {
|
|
std::size_t heap = 0;
|
|
hui_heap_capacity(&heap, nullptr, nullptr);
|
|
constexpr std::size_t MiB = 1024 * 1024;
|
|
return heap >= 256 * MiB ? 64 * MiB : heap >= 192 * MiB ? 40 * MiB : 24 * MiB;
|
|
}
|
|
|
|
// A file in the app folder's dev/ directory. After console filesystem elevation /app0 is only a
|
|
// logical path, so it is resolved like the native API's paths.
|
|
std::string dev_path(const char* name) {
|
|
char path[PATH_MAX];
|
|
return host::resolve_path((std::string("/app0/dev/") + name).c_str(), path, sizeof path) ? path : "";
|
|
}
|
|
|
|
constexpr std::int64_t duration_us = static_cast<std::int64_t>(PS5_REACT_TIMEOUT) * 1000000;
|
|
|
|
void react_log(const char* line) { async_log::write("[REACT] %s", line); }
|
|
|
|
// The smallest layout at least as tall as the console's output, else the largest. An unreadable
|
|
// output picks the smallest.
|
|
const Ps5ReactLayout& choose_layout() {
|
|
if (ps5_react_layout_count == 1) return ps5_react_layouts[0];
|
|
const int lines = video_output::output_lines();
|
|
for (unsigned i = 0; i < ps5_react_layout_count; ++i)
|
|
if (ps5_react_layouts[i].height >= lines) return ps5_react_layouts[i];
|
|
return ps5_react_layouts[ps5_react_layout_count - 1];
|
|
}
|
|
|
|
// An app folder may carry dev/input-script.txt (InputScript syntax) for unattended profiling; it is
|
|
// never part of a build, only added to a test deploy.
|
|
void load_input_script(InputScript& script) {
|
|
FILE* file = std::fopen(dev_path("input-script.txt").c_str(), "rb");
|
|
if (!file) return;
|
|
char text[4096] = {};
|
|
text[std::fread(text, 1, sizeof text - 1, file)] = '\0';
|
|
std::fclose(file);
|
|
char error[160];
|
|
if (script.parse(text, error, sizeof error)) async_log::write("[PS5-REACT] input script loaded");
|
|
else async_log::write("[PS5-REACT] input script ignored: %s", error);
|
|
}
|
|
|
|
// Live commands for a running title: a test machine overwrites dev/commands.txt with lines
|
|
// "<sequence> <steps>" (InputScript syntax); about once a second the host runs the steps of every line
|
|
// whose sequence number is new. Active only when the app folder has a dev directory, which a build never
|
|
// creates. Lines present at launch are skipped.
|
|
class LiveCommands {
|
|
public:
|
|
void start() {
|
|
struct stat info;
|
|
enabled_ = stat(dev_path("").c_str(), &info) == 0 && S_ISDIR(info.st_mode);
|
|
if (enabled_) read(false);
|
|
}
|
|
|
|
// Appends new commands to `script` once it has finished its previous steps.
|
|
void poll(std::int64_t now_us, InputScript& script) {
|
|
if (!enabled_ || now_us < next_poll_us_) return;
|
|
next_poll_us_ = now_us + 1000000;
|
|
read(true);
|
|
if (pending_.empty() || !script.done()) return;
|
|
char error[160];
|
|
if (!script.parse(pending_.c_str(), error, sizeof error)) async_log::write("[PS5-REACT] command ignored: %s", error);
|
|
pending_.clear();
|
|
}
|
|
|
|
private:
|
|
void read(bool queue) {
|
|
FILE* file = std::fopen(dev_path("commands.txt").c_str(), "rb");
|
|
if (!file) return;
|
|
char text[4096] = {};
|
|
text[std::fread(text, 1, sizeof text - 1, file)] = '\0';
|
|
std::fclose(file);
|
|
for (char* line = std::strtok(text, "\n"); line; line = std::strtok(nullptr, "\n")) {
|
|
char* steps = nullptr;
|
|
const unsigned long long sequence = std::strtoull(line, &steps, 10);
|
|
if (steps == line || sequence <= last_sequence_) continue;
|
|
steps += std::strspn(steps, " \t");
|
|
last_sequence_ = sequence;
|
|
if (!queue) continue;
|
|
async_log::write("[PS5-REACT] command: %s", steps);
|
|
pending_ += steps;
|
|
pending_ += ' ';
|
|
}
|
|
}
|
|
|
|
bool enabled_ = false;
|
|
unsigned long long last_sequence_ = 0;
|
|
std::int64_t next_poll_us_ = 0;
|
|
std::string pending_;
|
|
};
|
|
|
|
// The engine reads this clock around every blit, thousands of times in a full-screen frame:
|
|
// clock_gettime made the frame log cost about 10 ms there, the time-stamp counter costs nothing.
|
|
std::uint64_t tsc_per_us = 1;
|
|
std::uint32_t perf_clock() { return static_cast<std::uint32_t>(sceKernelReadTsc() / tsc_per_us); }
|
|
|
|
// Indexed like kButtonNames.
|
|
constexpr hui::Action kButtonActions[] = {
|
|
hui::Action::up, hui::Action::down, hui::Action::left, hui::Action::right,
|
|
hui::Action::confirm, hui::Action::back, hui::Action::north, hui::Action::west,
|
|
hui::Action::page_prev, hui::Action::page_next, hui::Action::jump_prev, hui::Action::jump_next,
|
|
hui::Action::l3, hui::Action::r3, hui::Action::menu, hui::Action::touch,
|
|
};
|
|
static_assert(std::size(kButtonActions) == std::size(kButtonNames));
|
|
|
|
// Buttons delivered to the app as actions; the D-pad and left stick come through input.nav.
|
|
constexpr std::pair<hui::Action, HostAction> kExtraActions[] = {
|
|
{hui::Action::confirm, HostAction::confirm}, {hui::Action::back, HostAction::back},
|
|
{hui::Action::page_prev, HostAction::l1}, {hui::Action::page_next, HostAction::r1},
|
|
{hui::Action::jump_prev, HostAction::l2}, {hui::Action::jump_next, HostAction::r2},
|
|
{hui::Action::north, HostAction::triangle}, {hui::Action::west, HostAction::square},
|
|
{hui::Action::menu, HostAction::options},
|
|
};
|
|
|
|
GamepadState gamepad_state(const hui::InputFrame& input) {
|
|
GamepadState state;
|
|
state.connected = input.connected;
|
|
state.left_x = input.stick_x; state.left_y = input.stick_y;
|
|
state.right_x = input.stick2_x; state.right_y = input.stick2_y;
|
|
state.l2 = input.trigger_l; state.r2 = input.trigger_r;
|
|
for (std::size_t i = 0; i < std::size(kButtonActions); ++i)
|
|
if (input.is_held(kButtonActions[i])) state.buttons |= 1u << i;
|
|
return state;
|
|
}
|
|
|
|
const char* nav_action(hui::Direction direction) {
|
|
switch (direction) {
|
|
case hui::Direction::up: return action_name(HostAction::up);
|
|
case hui::Direction::down: return action_name(HostAction::down);
|
|
case hui::Direction::left: return action_name(HostAction::left);
|
|
case hui::Direction::right: return action_name(HostAction::right);
|
|
default: return nullptr;
|
|
}
|
|
}
|
|
|
|
GcScheduler gc_scheduler(hui::sys::monotonic_us);
|
|
JsProfiler profiler(hui::sys::monotonic_us);
|
|
|
|
// Calls the runtime's input entry `name` with `argc` arguments, which it frees.
|
|
bool call_input(const char* name, int argc, JSValue* argv) {
|
|
gc_scheduler.input(hui::sys::monotonic_us());
|
|
er_perf_phase_begin(ER_PERF_PHASE_JS);
|
|
JSContext* ctx = er_runtime_context();
|
|
JSValue global = JS_GetGlobalObject(ctx);
|
|
JSValue fn = JS_GetPropertyStr(ctx, global, name);
|
|
JSValue result = JS_Call(ctx, fn, global, argc, argv);
|
|
const bool ok = !JS_IsException(result);
|
|
if (!ok) {
|
|
JSValue error = JS_GetException(ctx);
|
|
const char* message = JS_ToCString(ctx, error);
|
|
async_log::write("[PS5-REACT] input exception: %s", message ? message : "unknown");
|
|
if (message) JS_FreeCString(ctx, message);
|
|
JS_FreeValue(ctx, error);
|
|
}
|
|
JS_FreeValue(ctx, result);
|
|
for (int i = 0; i < argc; ++i) JS_FreeValue(ctx, argv[i]);
|
|
JS_FreeValue(ctx, fn); JS_FreeValue(ctx, global);
|
|
er_perf_phase_end(ER_PERF_PHASE_JS);
|
|
return ok;
|
|
}
|
|
|
|
bool dispatch(const char* action) {
|
|
JSValue value = JS_NewString(er_runtime_context(), action);
|
|
return call_input("__ps5ReactDispatch", 1, &value);
|
|
}
|
|
|
|
// The touchpad's finger at (x, y) 0..1, or lifted.
|
|
bool touch(bool down, float x, float y) {
|
|
JSContext* ctx = er_runtime_context();
|
|
JSValue args[2] = {down ? JS_NewFloat64(ctx, x) : JS_NULL, down ? JS_NewFloat64(ctx, y) : JS_NULL};
|
|
return call_input("__ps5ReactTouch", 2, args);
|
|
}
|
|
|
|
// Display.setRefreshRate: the rate can change only while EGL is terminated (ps5_opengl_display_modes.h),
|
|
// so the presenter's GL objects go with the context and are made again. The engine framebuffer is CPU
|
|
// memory and survives; the presenter's first draw uploads all of it.
|
|
bool switch_refresh_rate(hui::ps5::Display& display, GlPresenter& presenter, const Ps5ReactLayout& layout, int hz) {
|
|
const std::int64_t start = hui::sys::monotonic_us();
|
|
presenter.release();
|
|
display.close();
|
|
if (!eglSetDisplayRefreshPS5(eglGetDisplay(EGL_DEFAULT_DISPLAY), hz))
|
|
async_log::write("[PS5-REACT] refresh %d Hz refused: 0x%x", hz, eglGetError());
|
|
const bool ok = display.open(layout.surface_width, layout.surface_height) && presenter.init(layout.width, layout.height);
|
|
async_log::write("[PS5-REACT] refresh switch to %d Hz ok=%d in %lldms", hz, ok,
|
|
static_cast<long long>((hui::sys::monotonic_us() - start) / 1000));
|
|
return ok;
|
|
}
|
|
|
|
// Owned resources are released while the context is valid; closing the title
|
|
// happens after this scope, through the platform's lifecycle API.
|
|
bool run_proof() {
|
|
hui::ps5::Display display;
|
|
hui::ps5::Pad pad;
|
|
GlPresenter presenter;
|
|
hui::InputTracker tracker;
|
|
hui::PadSample samples[64];
|
|
FrameStats stats;
|
|
unsigned summaries = 0;
|
|
bool runtime = false, software = false;
|
|
// Accepted only before Display::open initializes EGL. A display without 120 Hz stays at 60.
|
|
if (PS5_REACT_REFRESH_RATE != 60 && !eglSetDisplayRefreshPS5(eglGetDisplay(EGL_DEFAULT_DISPLAY), PS5_REACT_REFRESH_RATE))
|
|
async_log::write("[PS5-REACT] refresh %d Hz refused: 0x%x", PS5_REACT_REFRESH_RATE, eglGetError());
|
|
const Ps5ReactLayout& layout = choose_layout();
|
|
const int width = layout.width, height = layout.height;
|
|
bool ok = display.open(layout.surface_width, layout.surface_height);
|
|
async_log::write("[PS5-REACT] display=%d at %lldms", ok, since_launch_ms());
|
|
if (ok) {
|
|
ok = presenter.init(width, height);
|
|
async_log::write("[PS5-REACT] presenter=%d", ok);
|
|
}
|
|
if (ok) {
|
|
software = er_software_backend_init(width, height);
|
|
ok = software && damage_tracker_install(width, height);
|
|
// The presenter draws qualifying <Image layer> images itself (er_set_layers_enabled). A test deploy may
|
|
// rasterize them all with a dev/layers.txt holding 0, for comparisons.
|
|
bool layers = true;
|
|
if (FILE* file = std::fopen(dev_path("layers.txt").c_str(), "rb")) {
|
|
char text[16] = {};
|
|
layers = !std::fgets(text, sizeof text, file) || std::atoi(text) != 0;
|
|
std::fclose(file);
|
|
}
|
|
er_set_layers_enabled(ok && layers && presenter.layers_supported());
|
|
// A test deploy may upload video as textures instead of storage buffers with dev/video-upload.txt "textures".
|
|
if (FILE* file = std::fopen(dev_path("video-upload.txt").c_str(), "rb")) {
|
|
char text[16] = {};
|
|
if (std::fgets(text, sizeof text, file) && !std::strncmp(text, "textures", 8)) presenter.set_video_upload(false);
|
|
std::fclose(file);
|
|
}
|
|
// A test deploy may dump the upscaler's input and output at its Nth frame with dev/upscale-dump.txt "N".
|
|
if (FILE* file = std::fopen(dev_path("upscale-dump.txt").c_str(), "rb")) {
|
|
char text[16] = {};
|
|
if (std::fgets(text, sizeof text, file) && std::atoi(text) > 0)
|
|
presenter.video().dump_upscale_at(dev_path("upscale"), std::uint64_t(std::atoi(text)));
|
|
std::fclose(file);
|
|
}
|
|
async_log::write("[PS5-REACT] video upload=%s",
|
|
presenter.video().upload_mode() == GlVideo::Upload::storage ? "storage" : "textures");
|
|
async_log::write("[PS5-REACT] software framebuffer=%d %dx%d", ok, width, height);
|
|
}
|
|
if (ok) {
|
|
// A test deploy may pick the count with dev/render-workers.txt (1 renders single-core).
|
|
const int cpus = render_workers::available_cpus();
|
|
int wanted = cpus;
|
|
if (FILE* file = std::fopen(dev_path("render-workers.txt").c_str(), "rb")) {
|
|
char text[16] = {};
|
|
if (std::fgets(text, sizeof text, file) && std::atoi(text) > 0) wanted = std::atoi(text);
|
|
std::fclose(file);
|
|
}
|
|
async_log::write("[PS5-REACT] render workers=%d of %d, cpus=%d", render_workers::start(wanted),
|
|
render_workers::max_workers(), cpus);
|
|
}
|
|
// The pad opens before the bundle runs: it also initializes the user service
|
|
// that the users, notification and browser calls need.
|
|
if (ok) {
|
|
async_log::write("[PS5-REACT] pad=%d at %lldms", pad.open(), since_launch_ms());
|
|
host_platform_set_pad(&pad);
|
|
}
|
|
if (ok) {
|
|
images::set_cache_bytes(image_cache_bytes());
|
|
images::set_heap_probe([] {
|
|
images::Heap heap;
|
|
std::size_t peak = 0, blocks = 0, failures = 0;
|
|
hui_heap_stats(&heap.live, &peak, &blocks, &failures);
|
|
hui_heap_capacity(&heap.size, nullptr, nullptr);
|
|
return heap;
|
|
});
|
|
if (network::start()) ps5_react_start_images();
|
|
async_log::write("[PS5-REACT] sound=%d at %lldms", ps5_react_start_sound(), since_launch_ms());
|
|
ErRuntimeConfig config = {};
|
|
config.screen_width = width; config.screen_height = height;
|
|
config.screen_scale = 2;
|
|
config.log = react_log;
|
|
config.max_stack_size = 1024 * 1024;
|
|
config.memory_limit = kJsMemoryLimit;
|
|
config.malloc_functions = js_heap_functions();
|
|
config.install_host_globals = ps5_react_install_host_api;
|
|
runtime = er_runtime_init(&config);
|
|
ok = runtime;
|
|
async_log::write("[PS5-REACT] runtime=%d at %lldms", ok, since_launch_ms());
|
|
// A test deploy samples JavaScript with dev/profile.txt holding the interval in microseconds.
|
|
if (FILE* file = ok ? std::fopen(dev_path("profile.txt").c_str(), "rb") : nullptr) {
|
|
char text[16] = {};
|
|
const int interval = std::fgets(text, sizeof text, file) ? std::atoi(text) : 0;
|
|
std::fclose(file);
|
|
profiler.start(JS_GetRuntime(er_runtime_context()), interval > 0 ? interval : 1000);
|
|
async_log::write("[PS5-REACT] profiling JavaScript every %d us", interval > 0 ? interval : 1000);
|
|
}
|
|
}
|
|
if (ok) {
|
|
layout.register_assets();
|
|
char fonts[256];
|
|
if (host::resolve_path("/app0/fonts", fonts, sizeof fonts))
|
|
text_shaper::install(fonts, [](const char* line) { async_log::write("[PS5-REACT] %s", line); });
|
|
ok = er_runtime_load_source(layout.bundle, layout.bundle_length, "app.jsx.bundle");
|
|
async_log::write("[PS5-REACT] bundle=%d gc_accounting=%d at %lldms", ok, er_runtime_gc_accounting_ok(), since_launch_ms());
|
|
}
|
|
if (ok) {
|
|
// Input failure is logged; timeout still allows the display-only proof to end.
|
|
async_log::write("[PS5-REACT] timeout=%ds after first frame", PS5_REACT_TIMEOUT);
|
|
std::int64_t first_present = 0, previous = hui::sys::monotonic_us(), last_vblank_us = previous;
|
|
// Replaced by the accepted mode after the first frame at a rate: a display without 120 Hz stays at 60.
|
|
int refresh_hz = PS5_REACT_REFRESH_RATE;
|
|
bool mode_known = false;
|
|
double tick_carry_ms = 0;
|
|
std::uint64_t frames = 0;
|
|
InputScript script;
|
|
load_input_script(script);
|
|
bool touched = false;
|
|
VirtualPad virtual_pad;
|
|
LiveCommands commands;
|
|
commands.start();
|
|
beat("start");
|
|
JS_SetInterruptHandler(JS_GetRuntime(er_runtime_context()),
|
|
[](JSRuntime*, void*) { js_ticks.fetch_add(1, std::memory_order_relaxed); return 0; }, nullptr);
|
|
pthread_t watcher;
|
|
const bool watcher_started = pthread_create(&watcher, nullptr, watch_loop, nullptr) == 0;
|
|
// A test deploy may lower the slow-frame threshold (milliseconds) with dev/slow-frame-ms.txt.
|
|
std::uint32_t slow_frame_us = 33000;
|
|
if (FILE* file = std::fopen(dev_path("slow-frame-ms.txt").c_str(), "rb")) {
|
|
char text[16] = {};
|
|
if (std::fgets(text, sizeof text, file) && std::atoi(text) > 0) slow_frame_us = std::atoi(text) * 1000u;
|
|
std::fclose(file);
|
|
}
|
|
// And report every slow frame instead of the first few per window, with dev/slow-frame-lines.txt.
|
|
if (FILE* file = std::fopen(dev_path("slow-frame-lines.txt").c_str(), "rb")) {
|
|
char text[16] = {};
|
|
if (std::fgets(text, sizeof text, file) && std::atoi(text) > 0) stats.set_slow_line_limit(std::atoi(text));
|
|
std::fclose(file);
|
|
}
|
|
tsc_per_us = std::max<std::uint64_t>(sceKernelGetTscFrequency() / 1000000, 1);
|
|
er_perf_set_clock(perf_clock);
|
|
// A test deploy may let idle collections run during motion with a dev/gc-still.txt holding 0.
|
|
if (FILE* file = std::fopen(dev_path("gc-still.txt").c_str(), "rb")) {
|
|
char text[16] = {};
|
|
if (std::fgets(text, sizeof text, file)) gc_scheduler.wait_for_still(std::atoi(text) != 0);
|
|
std::fclose(file);
|
|
}
|
|
while (ok) {
|
|
const std::int64_t now = hui::sys::monotonic_us();
|
|
beat("input");
|
|
if (now <= 0 || (duration_us > 0 && first_present && now - first_present >= duration_us)) break;
|
|
if (const char* line = stats.start_frame(now)) {
|
|
async_log::write("[PS5-REACT] %s", line);
|
|
// Every fifth summary, about ten seconds apart.
|
|
if (++summaries % 5 == 1) log_memory();
|
|
if (const std::string videos = video_layers::summary(); !videos.empty()) {
|
|
const std::uint64_t bytes = presenter.video().take_upload_bytes();
|
|
const std::uint32_t us = presenter.video().take_upload_us();
|
|
// The whole process's CPU time since the last line, as cores busy: decoding threads included.
|
|
static std::int64_t cpu_before = 0, wall_before = 0;
|
|
rusage usage{};
|
|
getrusage(RUSAGE_SELF, &usage);
|
|
const std::int64_t cpu_us = (usage.ru_utime.tv_sec + usage.ru_stime.tv_sec) * std::int64_t{1000000} +
|
|
usage.ru_utime.tv_usec + usage.ru_stime.tv_usec;
|
|
const std::int64_t wall_us = hui::sys::monotonic_us();
|
|
const double cores = wall_before && wall_us > wall_before ? double(cpu_us - cpu_before) / (wall_us - wall_before) : 0;
|
|
cpu_before = cpu_us;
|
|
wall_before = wall_us;
|
|
async_log::write("[PS5-REACT] video upload %.1f MiB in %.1f ms, process cpu %.2f cores", bytes / 1048576.0,
|
|
us / 1000.0, cores);
|
|
if (const std::string upscale = presenter.video().take_upscale_log(); !upscale.empty())
|
|
async_log::write("[PS5-REACT] video upscale %s", upscale.c_str());
|
|
std::size_t start = 0;
|
|
while (start < videos.size()) {
|
|
const std::size_t end = videos.find('\n', start);
|
|
async_log::write("[PS5-REACT] %s", videos.substr(start, end - start).c_str());
|
|
if (end == std::string::npos) break;
|
|
start = end + 1;
|
|
}
|
|
}
|
|
}
|
|
// Input and JavaScript commit layout only; the commit after them paints the frame once.
|
|
er_set_raster_deferred(true);
|
|
const auto count = pad.read(samples);
|
|
const auto input = tracker.update(std::span<const hui::PadSample>(samples, count), now);
|
|
pad.tick(static_cast<float>(now - previous) / 1000000.0f);
|
|
host_platform_tick(now);
|
|
if (const char* direction = nav_action(input.nav)) ok = dispatch(direction);
|
|
for (const auto& [button, action] : kExtraActions) {
|
|
if (ok && input.is_pressed(button)) ok = dispatch(action_name(action));
|
|
}
|
|
// Each frame the finger is down, and once when it lifts.
|
|
float touch_x = 0, touch_y = 0;
|
|
const bool touch_down = host_platform_read_touch(&touch_x, &touch_y);
|
|
if (ok && (touch_down || touched)) ok = touch(touch_down, touch_x, touch_y);
|
|
touched = touch_down;
|
|
commands.poll(now, script);
|
|
if (const char* action = ok ? script.next(static_cast<std::uint64_t>(now / 1000)) : nullptr) {
|
|
async_log::write("[PS5-REACT] script: %s", action);
|
|
if (!std::strcmp(action, "quit")) break;
|
|
if (VirtualPad::is_step(action)) {
|
|
char error[160];
|
|
if (!virtual_pad.apply(action, static_cast<std::uint64_t>(now / 1000), error, sizeof error))
|
|
async_log::write("[PS5-REACT] script: %s", error);
|
|
} else if (!std::strncmp(action, "shot:", 5)) {
|
|
const std::string path = dev_path((std::string(action + 5) + ".bmp").c_str());
|
|
async_log::write("[PS5-REACT] shot %s: %s", path.c_str(), save_screenshot(path.c_str()) ? "saved" : std::strerror(errno));
|
|
} else if (!std::strncmp(action, "touch:", 6)) {
|
|
float x = 0, y = 0;
|
|
ok = touch(std::sscanf(action + 6, "%f,%f", &x, &y) == 2, x, y);
|
|
} else {
|
|
ok = dispatch(action);
|
|
}
|
|
}
|
|
GamepadState state = gamepad_state(input);
|
|
// A scripted hold presses like the controller: the button's action, once.
|
|
for (std::uint32_t pressed = virtual_pad.merge(state, static_cast<std::uint64_t>(now / 1000)), i = 0; ok && pressed;
|
|
pressed >>= 1, ++i) {
|
|
if (const char* held = pressed & 1 ? VirtualPad::action(i) : nullptr) ok = dispatch(held);
|
|
}
|
|
ps5_react_set_gamepad(state);
|
|
if (!ok) break;
|
|
stats.lap(FrameStats::input, hui::sys::monotonic_us());
|
|
er_perf_phase_begin(ER_PERF_PHASE_JS);
|
|
beat("js");
|
|
er_runtime_pump();
|
|
// The display shows a new frame every vblank, so motion advances by whole vblanks, not by the
|
|
// loop's jittery wall-clock interval.
|
|
const std::int64_t vblank_us = 1000000 / refresh_hz;
|
|
const std::int64_t vblanks = std::max<std::int64_t>(1, (now - previous + vblank_us / 2) / vblank_us);
|
|
const double elapsed_ms = vblanks * 1000.0 / refresh_hz;
|
|
ok = ps5_react_frame(er_runtime_context(), elapsed_ms);
|
|
er_perf_phase_end(ER_PERF_PHASE_JS);
|
|
if (!ok) {
|
|
async_log::write("[PS5-REACT] frame callback exception");
|
|
break;
|
|
}
|
|
er_set_raster_deferred(false);
|
|
beat("commit");
|
|
er_commit();
|
|
if (*er_runtime_last_error()) { ok = false; break; }
|
|
stats.lap(FrameStats::update, hui::sys::monotonic_us());
|
|
er_perf_phase_begin(ER_PERF_PHASE_PRESENT);
|
|
beat("present");
|
|
int layer_count = 0;
|
|
const ERLayer* layers = er_get_layers(&layer_count);
|
|
presenter.set_layers({layers, static_cast<std::size_t>(layer_count)});
|
|
video_layers::update(hui::sys::monotonic_us(), 1000000 / refresh_hz);
|
|
// A frame that changed nothing is not presented: the screen keeps showing the last one, and the
|
|
// loop sleeps to the next vblank instead of waiting in the driver, which costs CPU.
|
|
const bool still = first_present && presenter.unchanged(damage_tracker_rects(), damage_tracker_moves(),
|
|
display.width(), display.height());
|
|
if (!still)
|
|
ok = presenter.draw(er_software_framebuffer(), damage_tracker_rects(), damage_tracker_moves(),
|
|
display.width(), display.height());
|
|
er_perf_phase_end(ER_PERF_PHASE_PRESENT);
|
|
{
|
|
const GlPresenter::Timing& t = presenter.last_timing();
|
|
stats.note_present(t.upload_us, t.draw_us, t.upload_bytes);
|
|
}
|
|
const std::size_t repainted = damage_tracker_area();
|
|
damage_tracker_clear();
|
|
stats.lap(FrameStats::present, hui::sys::monotonic_us());
|
|
beat("swap");
|
|
if (still) {
|
|
const std::int64_t wake = last_vblank_us + (hui::sys::monotonic_us() - last_vblank_us + 1000000 / refresh_hz) /
|
|
(1000000 / refresh_hz) * (1000000 / refresh_hz);
|
|
const std::int64_t left = wake - hui::sys::monotonic_us();
|
|
if (left > 0) usleep(static_cast<useconds_t>(left));
|
|
} else {
|
|
ok = ok && display.swap();
|
|
last_vblank_us = hui::sys::monotonic_us();
|
|
}
|
|
stats.lap(FrameStats::swap, hui::sys::monotonic_us());
|
|
if (!ok) break;
|
|
beat("gc");
|
|
if (const char* line = gc_scheduler.frame(JS_GetRuntime(er_runtime_context()), hui::sys::monotonic_us(),
|
|
repainted, static_cast<std::size_t>(width) * height))
|
|
async_log::write("[PS5-REACT] %s", line);
|
|
if (!first_present) {
|
|
first_present = hui::sys::monotonic_us();
|
|
hui::sys::hide_splash_screen();
|
|
async_log::write("[PS5-REACT] first frame presented at %lldms", since_launch_ms());
|
|
}
|
|
beat("embedded");
|
|
const bool first_at_mode = !mode_known;
|
|
if (first_at_mode) {
|
|
// The accepted mode is known only once a frame has been presented.
|
|
mode_known = true;
|
|
EGLint mode_width = 0, mode_height = 0, mode_hz = 0;
|
|
if (eglGetDisplayModePS5(eglGetDisplay(EGL_DEFAULT_DISPLAY), &mode_width, &mode_height, &mode_hz)) {
|
|
async_log::write("[PS5-REACT] display mode %dx%d at %d Hz", mode_width, mode_height, mode_hz);
|
|
if (mode_hz > 0) refresh_hz = mode_hz;
|
|
}
|
|
ps5_react_set_refresh_rate(refresh_hz);
|
|
}
|
|
// The engine clock takes whole milliseconds; carrying the remainder keeps 8.33 ms frames at speed.
|
|
tick_carry_ms += std::min(elapsed_ms, 50.0);
|
|
const auto tick_ms = static_cast<std::uint32_t>(tick_carry_ms);
|
|
tick_carry_ms -= tick_ms;
|
|
embedded_renderer_tick(tick_ms);
|
|
previous = now;
|
|
// The first frame at a mode may wait out the TV's resync in its swap; that wait is not motion.
|
|
if (first_at_mode) previous = hui::sys::monotonic_us();
|
|
++frames;
|
|
if (const char* line = stats.end_frame(slow_frame_us)) async_log::write("[PS5-REACT] %s", line);
|
|
if (ps5_react_exit_requested()) break;
|
|
if (const int hz = ps5_react_take_refresh_request()) {
|
|
ok = switch_refresh_rate(display, presenter, layout, hz);
|
|
refresh_hz = hz;
|
|
mode_known = false;
|
|
// Nor is the reopen itself.
|
|
previous = hui::sys::monotonic_us();
|
|
}
|
|
}
|
|
async_log::write("[PS5-REACT] loop ended ok=%d frames=%llu", ok, static_cast<unsigned long long>(frames));
|
|
watching = false;
|
|
if (watcher_started) pthread_join(watcher, nullptr);
|
|
if (profiler.running()) {
|
|
profiler.stop();
|
|
const bool saved = profiler.write(dev_path("profile.folded").c_str());
|
|
async_log::write("[PS5-REACT] profile: %llu samples, saved=%d", static_cast<unsigned long long>(profiler.samples()), saved);
|
|
}
|
|
}
|
|
if (!ok && runtime) async_log::write("[PS5-REACT] error=%s", er_runtime_last_error());
|
|
video_layers::stop();
|
|
ps5_react_stop_images();
|
|
archives::stop();
|
|
browser_capture::stop();
|
|
lan::stop();
|
|
packages::stop();
|
|
removals::stop();
|
|
network::stop();
|
|
if (runtime) er_runtime_shutdown();
|
|
text_shaper::shutdown();
|
|
ps5_react_stop_sound();
|
|
render_workers::stop();
|
|
if (software) er_software_backend_destroy();
|
|
host_platform_set_pad(nullptr);
|
|
pad.close();
|
|
presenter.release();
|
|
display.close();
|
|
return ok;
|
|
}
|
|
|
|
void* render_thread(void*) {
|
|
name_thread("render");
|
|
hui::sys::log("[PS5-REACT] render thread started, 8 MiB stack, at %lldms", since_launch_ms());
|
|
if (!async_log::start()) hui::sys::log("[PS5-REACT] log writer thread failed; logging synchronously");
|
|
const bool ok = run_proof();
|
|
async_log::stop();
|
|
hui::sys::log("[PS5-REACT] cleanup complete, result=%d; requesting title closure", ok);
|
|
hui::sys::quit();
|
|
return nullptr;
|
|
}
|
|
} // namespace
|
|
|
|
int main() {
|
|
hui::sys::log("[PS5-REACT] %s (%s)", PS5_REACT_NAME, PS5_REACT_TITLE);
|
|
std::size_t heap = 0, flexible_before = 0, flexible_after = 0;
|
|
hui_heap_capacity(&heap, &flexible_before, &flexible_after);
|
|
if (heap)
|
|
hui::sys::log("[PS5-REACT] heap: %zu MiB (flexible memory free %zu MiB before it, %zu MiB after)",
|
|
heap >> 20, flexible_before >> 20, flexible_after >> 20);
|
|
else
|
|
hui::sys::log("[PS5-REACT] heap: none, system malloc (flexible memory free %zu MiB)", flexible_before >> 20);
|
|
launch_us = hui::sys::monotonic_us();
|
|
#if PS5_REACT_VIDEO
|
|
video::preload_platform_decoder();
|
|
#endif
|
|
initialize_filesystem_access();
|
|
pthread_attr_t attributes;
|
|
int result = pthread_attr_init(&attributes);
|
|
if (!result) {
|
|
result = pthread_attr_setstacksize(&attributes, 8 * 1024 * 1024);
|
|
pthread_t thread;
|
|
if (!result) result = pthread_create(&thread, &attributes, render_thread, nullptr);
|
|
pthread_attr_destroy(&attributes);
|
|
}
|
|
if (result) {
|
|
hui::sys::log("[PS5-REACT] thread creation failed=%d", result);
|
|
hui::sys::quit();
|
|
}
|
|
// Native titles must not return to CRT exit(); the render thread owns closure.
|
|
hui::sys::park();
|
|
return 0;
|
|
}
|