mirror of
https://github.com/MyLuxy/overdrive.git
synced 2026-10-18 18:03:47 -05:00
feat: linux desktop preview and npm scripts
This commit is contained in:
1 parent
c742e20f89
commit
bf3689a5ec
16 files changed
+352
-33
No files matched your search
@@ -3,6 +3,8 @@
|
||||
.build/
|
||||
dist/
|
||||
node_modules/
|
||||
/Overdrive.exfat
|
||||
/update.zip
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
|
||||
@@ -226,6 +226,52 @@ What a bundler throws away could not come back: most comments, the JSX syntax (i
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Developing</b></summary>
|
||||
<br>
|
||||
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Building</b></summary>
|
||||
<br>
|
||||
@@ -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=<its bin directory>` 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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Executable
+51
@@ -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=<its bin directory> 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=<directory holding its clang and llvm-config>" >&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"
|
||||
Executable
+50
@@ -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."
|
||||
@@ -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 <x> <y>` 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
|
||||
|
||||
|
||||
+3
-3
@@ -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 | — |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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=<file> 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 <x> <y>` 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;
|
||||
|
||||
@@ -8,14 +8,19 @@
|
||||
#include "audio/mixer.hpp"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <mach/mach.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/wait.h>
|
||||
#ifdef __APPLE__
|
||||
#include <mach/mach.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/sysctl.h>
|
||||
#else
|
||||
#include <mntent.h>
|
||||
#include <sys/sysinfo.h>
|
||||
#endif
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -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<std::int64_t>(info.freeram + info.bufferram) * static_cast<std::int64_t>(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<host_info64_t>(&stats), &count) != KERN_SUCCESS)
|
||||
return -1;
|
||||
return static_cast<std::int64_t>(stats.free_count + stats.inactive_count) * static_cast<std::int64_t>(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<std::int64_t>(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<char*>(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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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):
|
||||
|
||||
@@ -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 <key|@ref> move focus by focusKey or by a ref from the last snapshot
|
||||
ps5_drive.py click <x> <y> a left mouse click at logical pixels: focuses the element
|
||||
there and confirms it
|
||||
ps5_drive.py shot <path.bmp> the frame on screen, 960×540
|
||||
ps5_drive.py script "<steps>" 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 <dir>")
|
||||
|
||||
@@ -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"])
|
||||
@@ -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"))
|
||||
|
||||
Reference in new issue
Block a user