diff --git a/.gitignore b/.gitignore index f73cbc7..31d7fea 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ .build/ dist/ node_modules/ +/Overdrive.exfat +/update.zip # Python __pycache__/ diff --git a/README.md b/README.md index 57780d2..87b69dc 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,52 @@ What a bundler throws away could not come back: most comments, the JSX syntax (i +
+Developing +
+ +Everything runs from the repository root through npm. Once after cloning, `npm install` installs +every dependency through its `postinstall` (`packaging/setup.sh`): on Linux the apt packages for the +desktop preview, the PS5 build and the tests (it asks for `sudo` once), on macOS the Homebrew +formulae; then Pillow and the framework's npm packages. + +```sh +git clone git@github.com:inglbiwalid/overdrive.git +cd overdrive +npm install # once; asks for sudo on Linux +npm run dev # the app in a window on your PC, rebuilt on every save under app/ +``` + +| Command | What it does | +| --- | --- | +| `npm install` | Installs every dependency, once after cloning | +| `npm run dev` | Opens the app in the desktop preview and rebuilds it on every save (state resets on restart) | +| `npm run preview` | Opens the desktop preview once, without watching | +| `npm test` | The framework's checks; needed after changes under `ps5-react/`, not for `app/` only | +| `npm run build` | The PS5 app, then `Overdrive.exfat` and `update.zip` (see Building) | +| `npm run build:app` | The PS5 app only, in `ps5-react/dist/PPSA99058` | + +The preview is the console's UI on your PC, so it takes a controller or the keyboard, and a mouse +as a developer's shortcut. The first `npm run dev` downloads and compiles the framework's pinned +dependencies, which takes a while; later starts are quick. + +| Action | Keyboard | Mouse | PS5 | +| --- | --- | --- | --- | +| Move focus | Arrow keys | Wheel (up/down) | D-pad | +| Select | Enter or Space | Left click on the element | ✕ | +| Back | Backspace | Right click | ○ | +| Sections | Q / E | — | L1 / R1 | +| Favorite | F | — | □ | +| Quit the preview | Escape | — | PS button | + +The app's code is under `app/` (see "What's in this repository"); a change there needs no rebuild +of the framework. Keep new user-facing strings in every file of `app/i18n/locales/`. On Linux the +preview is compiled with clang (`CC=clang CXX=clang++`, set by the npm scripts), since GCC stops on +a bounds warning in `archive_preflight.cpp`. Linux support for the preview is this fork's addition; +upstream ps5-react supports macOS only. + +
+
Building
@@ -234,21 +280,19 @@ Builds run on `ubuntu-24.04` with clang 18, only from **Run workflow**: a push b Each run puts `Overdrive.exfat` and `update.zip` in a draft release named after `app/version.js`, which only maintainers can see. -The same build locally, on any Linux with the packages the workflow installs (`clang-18`, -`llvm-18`, `cmake`, `ninja-build`, `nodejs`, `npm`, `zlib1g-dev`, `build-essential`, -`pkg-config`, `unzip`, `curl`, `exfatprogs`, and Pillow from `ps5-react/requirements.txt`): +The same build locally, from the repository root, after `npm install` (see Developing): ```sh -cd ps5-react -python3 tools/build_ps5.py --app-dir ../app --llvm /usr/bin -sudo ../packaging/make-exfat.sh dist/PPSA99058 ../Overdrive.exfat +npm run build # the app, then Overdrive.exfat and update.zip here +npm run build:app # the app only, in ps5-react/dist/PPSA99058 ``` -The build downloads its pinned dependencies into `ps5-react/.deps` and writes the app folder, a -`.zip` and a `.receipt.json` into `ps5-react/dist`. It verifies the result itself -(`tools/verify.py`), so a green build means the payload structure and signatures check out. -`--llvm /usr/bin` points it at the system clang; upstream defaults to the Homebrew LLVM path -because macOS is its supported development environment. +`packaging/build.sh` behind them finds LLVM 18 where apt or Homebrew put it (set +`LLVM=` otherwise), builds through `ps5-react/tools/build_ps5.py`, then runs +`packaging/make-exfat.sh` under `sudo`, since the image is mounted to be filled. The build +downloads its pinned dependencies into `ps5-react/.deps` and writes the app folder, a `.zip` and a +`.receipt.json` into `ps5-react/dist`. It verifies the result itself (`tools/verify.py`), so a +green build means the payload structure and signatures check out. The image is a plain exFAT volume whose root is the app folder, with 64 KiB clusters and the label `OVERDRIVE`. To release a new version, raise the version in `app/version.js` and diff --git a/package.json b/package.json new file mode 100644 index 0000000..dfeb862 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "overdrive", + "private": true, + "description": "Overdrive, a PS5 game store. The scripts wrap the ps5-react build for this app.", + "//": [ + "JSON has no comments; npm ignores this key.", + "", + "npm install Run once after cloning. Through postinstall it installs every dependency: the", + " apt or Homebrew packages for the preview, the PS5 build and the tests (asks for", + " sudo on Linux), Pillow, and the framework's npm packages. See packaging/setup.sh.", + "build Builds the PS5 app, then packages Overdrive.exfat and update.zip here, like the", + " GitHub workflow. Needs LLVM 18, cmake, ninja, nasm, exfatprogs; asks for sudo", + " to mount the image while filling it. See packaging/build.sh.", + "build:app The PS5 build only, into ps5-react/dist/PPSA99058. No sudo.", + "preview Opens the app in the desktop preview window on this PC. No PS5 toolchain needed.", + "dev Same window, rebuilt and restarted on every save under app/. State resets.", + "test The framework's checks; run after changing rendering or input in ps5-react/.", + "", + "CC=clang CXX=clang++: the preview is compiled with clang on every platform, since GCC", + "stops on a bounds warning in archive_preflight.cpp." + ], + "scripts": { + "postinstall": "bash packaging/setup.sh", + "build": "bash packaging/build.sh", + "build:app": "bash packaging/build.sh --no-package", + "preview": "cd ps5-react && CC=clang CXX=clang++ python3 tools/cli.py preview --app-dir ../app", + "dev": "cd ps5-react && CC=clang CXX=clang++ python3 tools/cli.py preview --app-dir ../app --watch", + "test": "cd ps5-react && CC=clang CXX=clang++ npm test" + } +} diff --git a/packaging/build.sh b/packaging/build.sh new file mode 100755 index 0000000..4944a35 --- /dev/null +++ b/packaging/build.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Builds the PS5 app and packages it as Overdrive.exfat, the same steps as +# .github/workflows/build.yml, on this machine. Run it through npm from the +# repository root: +# +# npm run build build the app and package the image +# npm run build:app build the app only (ps5-react/dist/PPSA99058) +# +# The build is pinned to LLVM 18. It is looked for where apt and Homebrew put +# it; set LLVM= to point elsewhere. Packaging needs root +# (make-exfat.sh mounts the image to fill it), so it asks through sudo. +set -euo pipefail + +root="$(cd "$(dirname "$0")/.." && pwd)" +package=1 +[ "${1:-}" = "--no-package" ] && package=0 + +llvm="${LLVM:-}" +if [ -z "$llvm" ]; then + for candidate in /usr/lib/llvm-18/bin /opt/homebrew/opt/llvm@18/bin /usr/local/opt/llvm@18/bin; do + if [ -x "$candidate/clang" ]; then llvm="$candidate"; break; fi + done +fi +if [ -z "$llvm" ] || [ ! -x "$llvm/clang" ]; then + echo "error: LLVM 18 not found. Install it (apt: clang-18 llvm-18 llvm-18-dev; brew: llvm@18)" >&2 + echo " or set LLVM=" >&2 + exit 1 +fi + +missing=() +for tool in cmake ninja nasm curl unzip zip; do + command -v "$tool" >/dev/null || missing+=("$tool") +done +if [ "$package" = 1 ] && ! command -v mkfs.exfat >/dev/null; then missing+=("mkfs.exfat (exfatprogs)"); fi +if [ "${#missing[@]}" -gt 0 ]; then + echo "error: missing tools: ${missing[*]}" >&2 + echo " apt: cmake ninja-build nasm curl unzip zip exfatprogs" >&2 + exit 1 +fi + +echo "→ building the PS5 app with LLVM at $llvm" +(cd "$root/ps5-react" && python3 tools/build_ps5.py --app-dir "$root/app" --llvm "$llvm") +[ "$package" = 1 ] || exit 0 + +echo "→ packaging Overdrive.exfat (sudo: the image is mounted to fill it)" +sudo bash "$root/packaging/make-exfat.sh" "$root/ps5-react/dist/PPSA99058" "$root/Overdrive.exfat" OVERDRIVE +sudo chown "$(id -u):$(id -g)" "$root/Overdrive.exfat" +# The in-app updater fetches one name, for an image install: the image alone at the root of update.zip. +rm -f "$root/update.zip" +zip -q -X -j "$root/update.zip" "$root/Overdrive.exfat" +ls -l "$root/Overdrive.exfat" "$root/update.zip" diff --git a/packaging/setup.sh b/packaging/setup.sh new file mode 100755 index 0000000..d0b29eb --- /dev/null +++ b/packaging/setup.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Installs everything a fresh clone needs to preview, test and build Overdrive. +# npm install runs it as postinstall, once, from the repository root: +# +# npm install +# +# Linux (apt): asks for sudo once to install the packages for the desktop +# preview (SDL2, OpenGL, curl, libarchive, OpenSSL, clang), the PS5 build +# (LLVM 18, ninja, nasm, exfatprogs) and the tests (bsdtar), then Pillow for +# the asset baking and the framework's npm packages. +# macOS (Homebrew): the same through brew and pip. +set -euo pipefail + +root="$(cd "$(dirname "$0")/.." && pwd)" + +case "$(uname -s)" in +Linux) + command -v apt-get >/dev/null || { echo "error: only apt-based Linux is scripted; see README 'Build it yourself' for the package list" >&2; exit 1; } + echo "→ apt packages (sudo)" + sudo apt-get update + sudo apt-get install -y \ + build-essential cmake clang ninja-build nasm pkg-config git curl unzip zip zstd \ + libsdl2-dev libgl-dev libcurl4-openssl-dev libarchive-dev libarchive-tools liblzma-dev libssl-dev zlib1g-dev \ + clang-18 llvm-18 llvm-18-dev exfatprogs \ + nodejs npm python3 python3-pil + ;; +Darwin) + command -v brew >/dev/null || { echo "error: Homebrew is needed: https://brew.sh" >&2; exit 1; } + echo "→ Homebrew formulae" + brew install cmake sdl2 llvm@18 nasm ninja curl libarchive xz openssl@3 python node + echo "→ Pillow" + python3 -m pip install -r "$root/ps5-react/requirements.txt" + ;; +*) + echo "error: unsupported OS $(uname -s)" >&2 + exit 1 + ;; +esac + +echo "→ framework npm packages" +(cd "$root/ps5-react" && npm ci) + +echo "→ checking" +python3 -c "import PIL; print('Pillow', PIL.__version__)" +for tool in cmake clang ninja nasm node mkfs.exfat; do + printf '%-10s %s\n' "$tool" "$(command -v "$tool" || echo MISSING)" +done +echo +echo "Ready. Next: npm run preview (desktop window), npm run build (Overdrive.exfat)." +echo "The first preview and the first build download and compile the pinned dependencies; that takes a while." diff --git a/ps5-react/CHANGELOG.md b/ps5-react/CHANGELOG.md index 09db687..c661524 100644 --- a/ps5-react/CHANGELOG.md +++ b/ps5-react/CHANGELOG.md @@ -1,4 +1,26 @@ # Changelog +## Unreleased — desktop preview on Linux, with a mouse + +- The desktop preview builds and runs on Linux: `sysinfo`, `/proc/self/mounts` and `xdg-open` stand + in for the Mach, BSD and Launch Services calls, FFmpeg and `rar-extract` take TLS and SHA-256 from + the system OpenSSL, and the JS stack guard is 4 MiB there. macOS keeps SecureTransport and + VideoToolbox; the PS5 build is unchanged. Build with `CC=clang CXX=clang++`: GCC rejects a + bounds warning in `archive_preflight.cpp`. +- A mouse drives the preview as a developer's pointer: a left click focuses the smallest navigable + element under it and confirms, a right click is `back`, and the wheel is `up`/`down`. + `ps5_drive.py click ` does the same in the snapshot's logical pixels. The click goes + through `globalThis.__ps5ReactHit(nx, ny)`, a hit-test over the focus manager's nodes that takes + the point as fractions of the screen; the desktop host maps the window point through the frame's + letterbox first. +- None of this reaches the console. The Linux stand-ins, the mouse handling and the `click` command + live in `native/desktop/` and `tools/`, which the PS5 build never compiles; `ffmpeg.ps5()` and the + PS5 `rar-extract` link line are untouched. The one shared file is the JS bundle: `__ps5ReactHit` is + defined only when the host sets `globalThis.__ps5ReactPointer` before the bundle loads, which the + desktop host does and the PS5 host does not, so on the console the hook is never created and + nothing calls it. Its source text (about 600 bytes of the 2.4 MB bundle) still ships, as the + existing `__ps5ReactInspect` and `__ps5ReactFocus` dev hooks do; stripping it would need a + target-aware bundle step, a change to the PS5 build path this entry deliberately avoids. + ## Unreleased — far focus scrolls skip ahead diff --git a/ps5-react/README.md b/ps5-react/README.md index 4668479..ee78785 100644 --- a/ps5-react/README.md +++ b/ps5-react/README.md @@ -88,9 +88,9 @@ Refresh.** The first run fetches and builds pinned dependencies when needed. | Action | Desktop preview | PS5 | | --- | --- | --- | -| Move focus | Arrow keys | D-pad | -| Activate focused element | Enter | Cross | -| Navigate back | Backspace | Circle | +| Move focus | Arrow keys, mouse wheel | D-pad | +| Activate focused element | Enter, left click on it | Cross | +| Navigate back | Backspace, right click | Circle | | Options (the `options` action) | O | Options | | Exit application | Escape | PS button | | Stop the development watcher | Ctrl+C in the terminal | — | diff --git a/ps5-react/docs/NAVIGATION.md b/ps5-react/docs/NAVIGATION.md index a307e46..03bf79f 100644 --- a/ps5-react/docs/NAVIGATION.md +++ b/ps5-react/docs/NAVIGATION.md @@ -93,9 +93,9 @@ frames. | Action | Desktop | PS5 | | --- | --- | --- | -| `up`, `down`, `left`, `right` | Arrow keys | D-pad, left stick | -| `confirm` | Enter | Cross (X) | -| `back` | Backspace | Circle | +| `up`, `down`, `left`, `right` | Arrow keys (`up`/`down` also the mouse wheel) | D-pad, left stick | +| `confirm` | Enter, or a left click (it focuses the element under the pointer first) | Cross (X) | +| `back` | Backspace, right click | Circle | | `l1`, `r1` | Q, E | L1, R1 | | `l2`, `r2` | Z, C | L2, R2 (past half travel on the desktop) | | `triangle`, `square` | T, F | Triangle, Square | diff --git a/ps5-react/native/desktop/CMakeLists.txt b/ps5-react/native/desktop/CMakeLists.txt index 5806251..d3c5388 100644 --- a/ps5-react/native/desktop/CMakeLists.txt +++ b/ps5-react/native/desktop/CMakeLists.txt @@ -61,6 +61,9 @@ endforeach() if(APPLE) target_link_libraries(ps5-react-preview PRIVATE "-framework VideoToolbox" "-framework CoreFoundation" "-framework CoreMedia" "-framework CoreVideo" "-framework CoreServices" "-framework Security") +else() + # FFmpeg's https/tls protocols use the system OpenSSL on Linux. + target_link_libraries(ps5-react-preview PRIVATE OpenSSL::SSL OpenSSL::Crypto) endif() # tools/test_video.py: the player in real time on a short clip, with a stand-in mixer. add_executable(ps5-react-video-test EXCLUDE_FROM_ALL ../../tools/tests/video_client.cpp ../shared/video_player.cpp ../shared/video_subtitles.cpp ../shared/audio_sink.cpp video_decoder.cpp) diff --git a/ps5-react/native/desktop/host.cpp b/ps5-react/native/desktop/host.cpp index 02327a0..096827f 100644 --- a/ps5-react/native/desktop/host.cpp +++ b/ps5-react/native/desktop/host.cpp @@ -242,17 +242,24 @@ struct Host { cfg.screen_width = width; cfg.screen_height = height; cfg.screen_scale = 2; cfg.memory_limit = kJsMemoryLimit; cfg.malloc_functions = js_heap_functions(); +#ifdef __APPLE__ cfg.max_stack_size = 1024 * 1024; +#else + // x86-64 Linux builds use deeper native frames per JS call; the main thread has 8 MiB. + cfg.max_stack_size = 4 * 1024 * 1024; +#endif cfg.install_host_globals = ps5_react_install_host_api; runtime_started = er_runtime_init(&cfg); if (!runtime_started) return false; // PS5_REACT_PROFILE= samples JavaScript; the folded stacks are written there on exit. if (std::getenv("PS5_REACT_PROFILE")) profiler.start(JS_GetRuntime(er_runtime_context()), 1000); - if (controlled) { - // Read when the bundle loads: Text reports its content to the inspector only then. + { + // Read when the bundle loads: the pointer hit-test exists only where a mouse can call it, and + // Text reports its content to the inspector only under the control channel. JSContext* ctx = er_runtime_context(); JSValue global = JS_GetGlobalObject(ctx); - JS_SetPropertyStr(ctx, global, "__ps5ReactInspecting", JS_TRUE); + JS_SetPropertyStr(ctx, global, "__ps5ReactPointer", JS_TRUE); + if (controlled) JS_SetPropertyStr(ctx, global, "__ps5ReactInspecting", JS_TRUE); JS_FreeValue(ctx, global); } er_register_assets(); @@ -312,6 +319,39 @@ struct Host { return call_input("__ps5ReactTouch", 2, args); } + // A mouse click at window point (x, y): focuses the element under it, as a developer's pointer. The + // frame is letterboxed in the window, so the point maps through that scale and offset. + bool click(int x, int y) { + int ww = 0, wh = 0; + SDL_GetWindowSize(window, &ww, &wh); + if (ww <= 0 || wh <= 0) return false; + const float scale = std::min(float(ww) / width, float(wh) / height); + return hit((x - (ww - width * scale) / 2) / scale, (y - (wh - height * scale) / 2) / scale); + } + + // Focuses the navigable element under logical point (fx, fy); whether one took focus. + bool hit(float fx, float fy) { + if (fx < 0 || fy < 0 || fx >= width || fy >= height) return false; + gc_scheduler.input(now_us()); + JSContext* ctx = er_runtime_context(); + JSValue global = JS_GetGlobalObject(ctx); + JSValue fn = JS_GetPropertyStr(ctx, global, "__ps5ReactHit"); + // As fractions: the runtime's layout units need not be the render's pixels. + JSValue args[2] = {JS_NewFloat64(ctx, fx / width), JS_NewFloat64(ctx, fy / height)}; + JSValue result = JS_Call(ctx, fn, global, 2, args); + const bool hit = !JS_IsException(result) && JS_ToBool(ctx, result) == 1; + if (JS_IsException(result)) { + JSValue error = JS_GetException(ctx); + const char* message = JS_ToCString(ctx, error); + std::fprintf(stderr, "Click failed: %s\n", message ? message : "JS exception"); + if (message) JS_FreeCString(ctx, message); + JS_FreeValue(ctx, error); + } + JS_FreeValue(ctx, result); JS_FreeValue(ctx, args[0]); JS_FreeValue(ctx, args[1]); + JS_FreeValue(ctx, fn); JS_FreeValue(ctx, global); + return hit; + } + // A development hook's result as text: strings as they are, anything else through JSON. std::string call_js(const char* name, const char* arg) { JSContext* ctx = er_runtime_context(); @@ -369,6 +409,12 @@ struct Host { Control::reply(client, call_js("__ps5ReactInspect", nullptr)); } else if (verb == "focus") { Control::reply(client, call_js("__ps5ReactFocus", arg.c_str()) == "true" ? "ok" : "not found: " + arg); + } else if (verb == "click") { + // `click ` in logical pixels: a left mouse click there. + int x = 0, y = 0; + const bool found = std::sscanf(arg.c_str(), "%d %d", &x, &y) == 2 && hit(float(x), float(y)); + if (found && !dispatch(action_name(HostAction::confirm))) return false; + Control::reply(client, found ? "ok" : "nothing at " + arg); } else if (verb == "script") { // Runs input-script steps from now on, e.g. a touchpad swipe. char error[512] = {}; @@ -438,6 +484,13 @@ struct Host { } else if ((event.type == SDL_CONTROLLERTOUCHPADDOWN || event.type == SDL_CONTROLLERTOUCHPADMOTION || event.type == SDL_CONTROLLERTOUCHPADUP) && event.ctouchpad.touchpad == 0 && event.ctouchpad.finger == 0) { if (!touch(event.type != SDL_CONTROLLERTOUCHPADUP, event.ctouchpad.x, event.ctouchpad.y)) return false; + } else if (event.type == SDL_MOUSEBUTTONDOWN) { + // A developer's mouse stands in for the pad: left selects what is under it, right goes back. + if (event.button.button == SDL_BUTTON_LEFT) { + if (click(event.button.x, event.button.y)) action = action_name(HostAction::confirm); + } else if (event.button.button == SDL_BUTTON_RIGHT) action = action_name(HostAction::back); + } else if (event.type == SDL_MOUSEWHEEL && event.wheel.y != 0) { + action = action_name(event.wheel.y > 0 ? HostAction::up : HostAction::down); } else if (event.type == SDL_CONTROLLERAXISMOTION) { const auto trigger = [&](bool& down, const char* name) { if (crossed(down, event.caxis.value / 32767.0f)) action = name; diff --git a/ps5-react/native/desktop/host_platform.cpp b/ps5-react/native/desktop/host_platform.cpp index cf51746..45641c7 100644 --- a/ps5-react/native/desktop/host_platform.cpp +++ b/ps5-react/native/desktop/host_platform.cpp @@ -8,14 +8,19 @@ #include "audio/mixer.hpp" #include -#include #include -#include #include #include #include -#include #include +#ifdef __APPLE__ +#include +#include +#include +#else +#include +#include +#endif #include #include #include @@ -26,8 +31,12 @@ extern char** environ; namespace { SDL_GameController* controller = nullptr; -void sysctl_string(const char* name, char* out, std::size_t size) { +void sysctl_string([[maybe_unused]] const char* name, char* out, [[maybe_unused]] std::size_t size) { +#ifdef __APPLE__ if (sysctlbyname(name, out, &size, nullptr, 0) != 0) *out = '\0'; +#else + *out = '\0'; +#endif } // "pt_BR.UTF-8" → "pt-BR"; the C and POSIX locales name no language. @@ -43,6 +52,11 @@ void environment_language(char* out, std::size_t size) { } std::int64_t free_memory() { +#ifndef __APPLE__ + struct sysinfo info; + if (sysinfo(&info) != 0) return -1; + return static_cast(info.freeram + info.bufferram) * static_cast(info.mem_unit); +#else vm_statistics64_data_t stats; mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; vm_size_t page = 0; @@ -50,6 +64,7 @@ std::int64_t free_memory() { host_statistics64(mach_host_self(), HOST_VM_INFO64, reinterpret_cast(&stats), &count) != KERN_SUCCESS) return -1; return static_cast(stats.free_count + stats.inactive_count) * static_cast(page); +#endif } } // namespace @@ -70,6 +85,21 @@ bool arrange_relaunch(char*, std::size_t) { } int list_mounts(MountEntry* mounts, int max) { +#ifndef __APPLE__ + FILE* table = setmntent("/proc/self/mounts", "r"); + if (!table) return -1; + int used = 0; + while (used < max) { + const mntent* entry = getmntent(table); + if (!entry) break; + std::snprintf(mounts[used].device, sizeof mounts[used].device, "%s", entry->mnt_fsname); + std::snprintf(mounts[used].path, sizeof mounts[used].path, "%s", entry->mnt_dir); + std::snprintf(mounts[used].type, sizeof mounts[used].type, "%s", entry->mnt_type); + ++used; + } + endmntent(table); + return used; +#else struct statfs* found = nullptr; const int count = getmntinfo(&found, MNT_NOWAIT); if (count <= 0) return -1; @@ -80,6 +110,7 @@ int list_mounts(MountEntry* mounts, int max) { std::snprintf(mounts[i].type, sizeof mounts[i].type, "%s", found[i].f_fstypename); } return used; +#endif } bool disk_usage(const char* real_path, double& total, double& free) { @@ -120,11 +151,13 @@ void device_info(DeviceInfo& info) { char version[24]; sysctl_string("kern.osproductversion", version, sizeof version); if (*version) std::snprintf(info.firmware, sizeof info.firmware, "macOS %s", version); +#ifdef __APPLE__ std::uint64_t frequency = 0; std::size_t size = sizeof frequency; // Apple silicon does not publish a CPU frequency; it stays unknown there. if (sysctlbyname("hw.cpufrequency", &frequency, &size, nullptr, 0) == 0) info.cpu_frequency = static_cast(frequency); +#endif info.free_memory = free_memory(); rusage usage; if (getrusage(RUSAGE_SELF, &usage) == 0) @@ -152,10 +185,14 @@ bool notify(const char* message, const char* sub_message) { bool open_url(const char* url) { if (std::strncmp(url, "http://", 7) && std::strncmp(url, "https://", 8)) return false; +#ifdef __APPLE__ char open[] = "open"; +#else + char open[] = "xdg-open"; +#endif char* argv[] = {open, const_cast(url), nullptr}; pid_t pid = 0; - if (posix_spawnp(&pid, "open", nullptr, nullptr, argv, environ) != 0) return false; + if (posix_spawnp(&pid, open, nullptr, nullptr, argv, environ) != 0) return false; int status = 0; // `open` hands the URL to Launch Services and exits at once. return waitpid(pid, &status, 0) == pid && WIFEXITED(status) && WEXITSTATUS(status) == 0; diff --git a/ps5-react/runtime/js/focus/inspect.js b/ps5-react/runtime/js/focus/inspect.js index 611ded5..aa9cad1 100644 --- a/ps5-react/runtime/js/focus/inspect.js +++ b/ps5-react/runtime/js/focus/inspect.js @@ -41,4 +41,19 @@ export function installInspector(manager) { }; })); globalThis.__ps5ReactFocus = key => manager.focus(key); + // A pointer at (nx, ny), fractions of the screen: focuses the smallest navigable element under it, + // as a mouse click on the desktop preview would. Returns whether one took focus. Only the desktop + // host has a pointer (it sets the flag before the bundle loads); the PS5 gets no hook. + if (!globalThis.__ps5ReactPointer) return; + globalThis.__ps5ReactHit = (nx, ny) => { + const x = nx * screen.width, y = ny * screen.height; + let best = null, bestArea = Infinity; + for (const node of manager.nodes) { + const rect = screenRect(node); + if (!rect || inert(node.scope) || x < rect.x || y < rect.y || x >= rect.x + rect.width || y >= rect.y + rect.height) continue; + const area = rect.width * rect.height; + if (area < bestArea) { best = node; bestArea = area; } + } + return best ? manager.focus(best) : false; + }; } diff --git a/ps5-react/tools/ffmpeg.py b/ps5-react/tools/ffmpeg.py index 44977de..ddc885b 100644 --- a/ps5-react/tools/ffmpeg.py +++ b/ps5-react/tools/ffmpeg.py @@ -10,6 +10,7 @@ import hashlib import os from pathlib import Path import shutil +import sys import tarfile from common import DEPS, LOCK, ROOT, fetch, run @@ -76,10 +77,12 @@ def _build(name, arguments, env=None): def desktop(): - """macOS: TLS through SecureTransport, h264/hevc through VideoToolbox when the decoder asks.""" + """macOS: TLS through SecureTransport, h264/hevc through VideoToolbox when the decoder asks. + Linux: TLS through the system OpenSSL, software decoding only.""" + platform = (["--enable-securetransport", "--enable-videotoolbox", "--enable-hwaccel=h264_videotoolbox,hevc_videotoolbox"] + if sys.platform == "darwin" else ["--enable-openssl"]) return _build("desktop", ["--cc=clang", "--enable-static", "--disable-shared", "--pkg-config=false", - *COMPONENTS, "--enable-securetransport", "--enable-videotoolbox", - "--enable-hwaccel=h264_videotoolbox,hevc_videotoolbox"]) + *COMPONENTS, *platform]) def ps5(sdk, network_ports, env): diff --git a/ps5-react/tools/ps5_drive.py b/ps5-react/tools/ps5_drive.py index 84e4099..9cfef24 100644 --- a/ps5-react/tools/ps5_drive.py +++ b/ps5-react/tools/ps5_drive.py @@ -23,6 +23,8 @@ hidden-layer elements; --json prints the raw records ps5_drive.py diff what changed on screen since the last snapshot or diff ps5_drive.py focus move focus by focusKey or by a ref from the last snapshot + ps5_drive.py click a left mouse click at logical pixels: focuses the element + there and confirms it ps5_drive.py shot the frame on screen, 960×540 ps5_drive.py script "" input-script steps from now on (docs/ANIMATION.md), e.g. "swipe:right:0.3" for a touchpad swipe @@ -175,6 +177,8 @@ def main(): return diff() if args[0] == "focus" and len(args) == 2: return print(send(f"focus {resolve(args[1])}")) + if args[0] == "click" and len(args) == 3: # logical px, as snapshot prints them; the host takes render px + return print(send(f"click {round(float(args[1]) * SCALE)} {round(float(args[2]) * SCALE)}")) print(send(" ".join(args))) except OSError as error: raise SystemExit(f"No preview on {SOCKET} ({error}); run: ps5_drive.py start --app-dir ") diff --git a/ps5-react/tools/rar_worker.py b/ps5-react/tools/rar_worker.py index 8c7f004..ece029c 100644 --- a/ps5-react/tools/rar_worker.py +++ b/ps5-react/tools/rar_worker.py @@ -6,6 +6,7 @@ program of its own. UnRAR's license allows its use to extract RAR archives but i so it is never linked into the engine; the engine starts this program and reads its reports. """ import subprocess +import sys import tarfile from concurrent.futures import ThreadPoolExecutor from pathlib import Path @@ -99,4 +100,5 @@ def build(output, compiler, flags=(), libraries=(), env=None): def build_host(output): """rar-extract for the desktop preview and the archive tests.""" - return build(output, ["clang++"], ["-pthread"]) + # macOS hashes with CommonCrypto; elsewhere the driver uses OpenSSL's EVP. + return build(output, ["clang++"], ["-pthread"], [] if sys.platform == "darwin" else ["-lcrypto"]) diff --git a/ps5-react/tools/test_archives.py b/ps5-react/tools/test_archives.py index 8a12c5b..0ee1ac1 100644 --- a/ps5-react/tools/test_archives.py +++ b/ps5-react/tools/test_archives.py @@ -1,6 +1,6 @@ """Real local archive extraction with the same native worker used on PS5.""" from pathlib import Path -import binascii,hashlib,io,json,lzma,os,random,struct,subprocess,tarfile,tempfile,time,zipfile +import binascii,hashlib,io,json,lzma,os,random,struct,subprocess,sys,tarfile,tempfile,time,zipfile from rar_worker import build_host ROOT=Path(__file__).resolve().parents[1] DATA=bytes(range(256))*1024 @@ -116,9 +116,12 @@ def uudecode(source,destination): def main(): with tempfile.TemporaryDirectory(prefix="ps5-react-archives-") as tmp: root=Path(tmp);binary=root/"archive-client" - prefix=subprocess.check_output(["brew","--prefix","libarchive"],text=True).strip() - xz=subprocess.check_output(["brew","--prefix","xz"],text=True).strip() - crypto=subprocess.check_output(["brew","--prefix","openssl@3"],text=True).strip() + # Homebrew keeps these keg-only on macOS; Linux distributions install them under /usr. + if sys.platform=="darwin": + prefix=subprocess.check_output(["brew","--prefix","libarchive"],text=True).strip() + xz=subprocess.check_output(["brew","--prefix","xz"],text=True).strip() + crypto=subprocess.check_output(["brew","--prefix","openssl@3"],text=True).strip() + else: prefix=xz=crypto="/usr" sources=[ROOT/"tools/tests/archive_client.cpp",ROOT/"native/shared/archives.cpp",ROOT/"native/shared/archive_preflight.cpp",ROOT/"native/shared/rar5_password.cpp",ROOT/"native/shared/archive_writer.cpp",ROOT/"native/desktop/rar_worker_process.cpp"] # RAR volumes go to rar-extract, as in the app; the other formats stay with libarchive. os.environ["PS5_REACT_RAR_WORKER"]=str(build_host(root/"rar-extract"))