mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-21 16:12:27 -05:00
Home screen forwarders start a game with --rom and --core
From mihawk-99/PS5_RetroArch#34 (Martin Pham). A forwarder, a small app
with a home screen tile of its own, starts the title with --rom <file>
[--core <core>] [--exit-after-game] (each also as --x=<value>). On a
launch that names no mode, src/frontend_mode_ps5.cpp takes it as game
mode takes a frontend's request: the content (absolute, or inside
/app0/content/ and never above it) and the core are checked by
ps5_game_check, RetroArch starts with -L, and afterwards the title
opens as from the home screen, or closes with --exit-after-game.
Without --core the playlists choose, else the one core whose info file
lists the extension (Genesis Plus GX for the Mega Drive extensions both
Sega cores list) or, for a folder, the one core that opens folders. A
refusal goes to trace.txt and the launch goes on as from the home
screen; a handover's --ps5-mode wins over --rom.
Changed from the pull request: its README examples and test fixtures no
longer name games and use the ES-DE folder names content/ has now; one
header comment is moved to its declaration. Its line-ending rewrites of
the PowerShell scripts are dropped (they are gone, 8f14452), and its
GitHub workflow is parked in parked/github-build-workflow, with what
would finish it at the top of build.yml: it cannot run here, and it
does not fetch PS5_MihawkGen, which the frontend compiles now.
To prove it on a console, whose control payload launches without
arguments: the relaunch test takes its restarts' arguments from the
lines after the arm line, and tools/run-title.sh --relaunch-arg writes
them.
Evidence: tests/frontend_mode_ps5_test.cpp (parsing, paths, cores by
playlist, extension and folder, refusals, after the game);
tests/relaunch_ps5_test.cpp (arguments carried; a control character or
a ninth argument arms nothing). Gates: format, unit (204), build
(staged cores reused), integration, evidence PASS. Console: with this
series' deploy.
This commit is contained in:
1 parent
6f03aa224a
commit
20dd7fe082
15 files changed
+1551
-24
No files matched your search
@@ -250,6 +250,35 @@ the title folder. With [ShadowMountPlus](https://github.com/drakmor/ShadowMountP
|
||||
`/mnt/ext1`), only when a drive is mounted there. Without it, the sandbox hides them
|
||||
and **EXTERNAL** (`/mnt`) may be empty.
|
||||
|
||||
<a id="forwarders"></a>
|
||||
|
||||
## <img src="webui/assets/fluent/game-chat.svg" width="26" height="26" alt=""> Start a game from a home screen forwarder
|
||||
|
||||
A forwarder is a small app with its own home screen tile that launches `PPSA99169` with
|
||||
launch arguments. The title then starts that game in RetroArch with the core given, instead of
|
||||
opening the picker or a frontend.
|
||||
|
||||
| Argument | Meaning |
|
||||
| --- | --- |
|
||||
| `--rom <file>` or `--rom=<file>` | The content to start: an absolute path (`/mnt/usb0/roms/snes/Game.zip`), or a path inside the title's `content/` folder (`/app0/content/`), such as `snes/Game.zip`. A folder is content for a core that opens folders, one whose info file lists `/` among its extensions: DOSBox Pure (`--rom /mnt/usb0/roms/dos/w95 --core dosbox_pure`), PUAE, VICE. A relative path may not contain `..`. |
|
||||
| `--core <core>` or `--core=<core>` | The core to run it with, one of the title's: `snes9x`, `snes9x_libretro`, `snes9x_libretro.so` and `/app0/cores/snes9x_libretro.so` all name `/app0/cores/snes9x_libretro.so`. Without it, the core RetroArch's playlists associate with the content, or else the one core whose info file lists the file's extension (`.sfc` is Snes9x's; a Mega Drive `.md` or `.gen` is Genesis Plus GX's). An extension several cores take (`.cue`, `.chd`, `.iso`, `.zip`, `.bin`) needs `--core`. |
|
||||
| `--exit-after-game` | When RetroArch quits (Close Content or Quit), the title closes and the console returns to the home screen. Without it, the title then opens as from the home screen. |
|
||||
|
||||
```text
|
||||
--rom "snes/Game (USA).zip" --core snes9x
|
||||
--rom /mnt/usb0/roms/psx/Game.chd --core mednafen_psx_hw --exit-after-game
|
||||
```
|
||||
|
||||
The game runs as a frontend's game does in game mode (`src/ps5_game.h`): the content and core
|
||||
are checked the same way, RetroArch starts with `-L <core> <content>` and its own configuration,
|
||||
overrides and saves apply, and its log is `retroarch-game.log`. If the content is missing, the
|
||||
core is not one of the title's, or no core is named and none can be chosen, the reason is
|
||||
written to `trace.txt` (`forwarder: ... not run: ...`) and the title opens as from the home
|
||||
screen. A launch that names a mode (`--ps5-mode=...`) wins over `--rom`. The title's own restarts
|
||||
carry no arguments, so the forwarded game never starts twice. Arguments only reach a new
|
||||
process: if the title is already running, a forwarder should close it first. Parsing is in
|
||||
`src/frontend_mode_ps5.cpp`, checked by `tests/frontend_mode_ps5_test.cpp`.
|
||||
|
||||
<a id="what-has-been-verified"></a>
|
||||
|
||||
## <img src="webui/assets/fluent/checkmark-circle.svg" width="26" height="26" alt=""> Compatibility and known limits
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
# PS5 RetroArch - the title (README's "Build from source") and a ready-to-install PPSA99169 bundle,
|
||||
# on GitHub-hosted runners.
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# PARKED (mihawk-99/PS5_RetroArch#34, Martin Pham): not run, and not accepted as working. It
|
||||
# was written against the tree of 2026-10-08. The frontend now compiles frame generation from
|
||||
# PS5_MihawkGen beside this repository, which this file does not fetch; PS5_VULKAN_REVISION is
|
||||
# the beta.1 one; scripts/core-plan.py has to be re-checked against tools/build-title.sh's
|
||||
# optional cores and every seed script against the current pins. Done means: moved back to
|
||||
# .github/workflows/ (its scripts to .github/scripts/), a run on a fork whose zip passes
|
||||
# tools/check-manifest.sh and tools/check-notices.py, and that zip loading on a console.
|
||||
#
|
||||
# README's workspace, made from public sources at their pins, with this checkout as PS5_RetroArch:
|
||||
#
|
||||
# PS5_Vulkan mihawk-99/PS5_Vulkan at PS5_VULKAN_REVISION below: its RADV release archive
|
||||
# (tools/build-radv.sh release), its link recipe (tools/radv-link.sh)
|
||||
# PS5_Mesa the Mesa fork at the revision PS5_Vulkan pins, which RADV is built from
|
||||
# PS5_PayloadSDK the payload SDK fork at the revision each of the two projects pins
|
||||
#
|
||||
# The jobs:
|
||||
#
|
||||
# plan the cores tools/build-title.sh builds (.github/scripts/core-plan.py), but for those
|
||||
# CI_SKIP_CORES names
|
||||
# radv RADV, as PS5_Vulkan builds it (LLVM 18, Mesa's host needs), cached by its pins
|
||||
# core one job per core: its tools/build-<core>.sh, as build-title.sh runs it. A core is
|
||||
# cached with its stamp (tools/core-stamp.sh: the script, its patches, the SDK and the
|
||||
# loader), so a core whose inputs did not change is not built again; MAME, the longest
|
||||
# build, also keeps its ccache, so a build cut short by the time limit carries on
|
||||
# title tools/build-title.sh, with every core in place and up to date (each script finds its
|
||||
# stamp and skips), RADV from the radv job, and its output folder zipped as
|
||||
# dist/PPSA99169.zip: the PPSA99169 folder, ready for /data/homebrew or a folder
|
||||
# ShadowMountPlus scans
|
||||
#
|
||||
# What a development build here leaves out, as tools/build-title.sh does without PS5_RELEASE_TAG:
|
||||
# the frontends (EmulationStation and the picker, PS5_FRONTENDS), which need PS5_OpenGL's SDK,
|
||||
# its SDL2 build and PS5_VulkanTemplate. Without them the title opens RetroArch, as it always has,
|
||||
# and a forwarder (--rom, --core) works the same.
|
||||
#
|
||||
# The cores, the frontend and the title are built in Ubuntu 26.04 with LLVM 22, the toolchain
|
||||
# README's /usr/bin/clang is: the core ABI check (tools/check-core.py) wants FreeBSD/PS5
|
||||
# libraries, which the SDK's clang links from LLVM 20, and the title's link takes
|
||||
# -z nodynamic-undefined-weak, which LLD has from 21 (.github/scripts/install-host-tools.sh).
|
||||
# Host steps README's environment had from elsewhere:
|
||||
# - RetroArch's configure links a vkCreateInstance test with the console's compiler, against a
|
||||
# libvulkan the SDK does not carry: a stub archive answers it (the driver is RADV, linked
|
||||
# into eboot.bin later; nothing links the stub)
|
||||
# - tools/fetch-retroarch.sh checks for libretro-common/include/retroarch.h, which the pinned
|
||||
# tag does not have; it takes the tree pinned in place instead, a clone at the pinned commit
|
||||
# - tools/build-title.sh checks ps5vk's four archives are there before it links either
|
||||
# driver; a RADV link does not read them, so empty archives stand in for them
|
||||
# - zlib (zlib.net), Mesa's zlib wrap (wrapdb) and libmicrohttpd (ftp.gnu.org) from GitHub or
|
||||
# Ubuntu's archive, checked against each script's own pin (.github/scripts/seed-*.sh)
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
paths-ignore: ['**.md', 'evidence/**', 'releases/**']
|
||||
pull_request:
|
||||
paths-ignore: ['**.md', 'evidence/**', 'releases/**']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
# Cores left out of this build (space-separated libretro names, as in tools/build-title.sh's
|
||||
# core_names): no job builds them and the title does not carry them. MAME, the longest build by
|
||||
# far, is left out for now; empty this to build it again.
|
||||
CI_SKIP_CORES: ''
|
||||
# PS5_Vulkan main when the beta.1 release was built (radv-link.sh binds strcasestr, mkstemp,
|
||||
# readlink, link and symlink, which tools/build-title.sh's wraps expect)
|
||||
PS5_VULKAN_REVISION: 5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
name: Plan the cores
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- id: plan
|
||||
run: |
|
||||
# shellcheck disable=SC2086 # a list of names
|
||||
python3 .github/scripts/core-plan.py skip $CI_SKIP_CORES
|
||||
matrix=$(python3 .github/scripts/core-plan.py matrix)
|
||||
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||
python3 -c 'import json, sys; [print(c["core"], c["script"]) for c in json.loads(sys.argv[1])]' "$matrix"
|
||||
|
||||
radv:
|
||||
name: RADV (PS5_Vulkan)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
container:
|
||||
image: ubuntu:24.04
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Install the build tools
|
||||
run: |
|
||||
apt-get update
|
||||
# PS5_Vulkan's: LLVM 18 with Clang, its runtime builtins and development files, libclc, the
|
||||
# SPIR-V translator, glslangValidator, meson from pip (Mesa needs 1.4; Ubuntu 24.04 has 1.3),
|
||||
# rsync for build-radv.sh's source tree, rev and flock for the SDK's scripts
|
||||
apt-get install --yes --no-install-recommends \
|
||||
ca-certificates git curl wget unzip zip xz-utils file make build-essential pkgconf patch \
|
||||
bsdextrautils util-linux rsync ccache binutils cmake ninja-build bison flex glslang-tools \
|
||||
spirv-tools python3 python3-pip \
|
||||
clang-18 lld-18 llvm-18 libclang-rt-18-dev llvm-18-dev libclang-18-dev libclc-18-dev \
|
||||
libllvmspirvlib-18-dev llvm-spirv-18 zlib1g-dev
|
||||
python3 -m pip install --break-system-packages --no-cache-dir meson mako packaging pyyaml
|
||||
git config --global --add safe.directory '*'
|
||||
for tool in clang-18 ld.lld-18 llvm-config-18 meson ninja rsync rev flock; do
|
||||
command -v "$tool" >/dev/null || { echo "missing $tool" >&2; exit 2; }
|
||||
done
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: PS5_RetroArch
|
||||
|
||||
- name: Fetch the pinned sources
|
||||
run: |
|
||||
fetch_at() { # fetch_at <dir> <url> <commit>: just that commit, checked out
|
||||
rm -rf "$1"
|
||||
git init -q "$1"
|
||||
git -C "$1" remote add origin "$2"
|
||||
git -C "$1" fetch -q --depth 1 origin "$3"
|
||||
git -C "$1" checkout -q --detach FETCH_HEAD
|
||||
}
|
||||
fetch_at PS5_Vulkan https://github.com/mihawk-99/PS5_Vulkan.git "$PS5_VULKAN_REVISION"
|
||||
sdk=$(sed -n 's/^sdk_revision=\([0-9a-f]\{40\}\).*/\1/p' PS5_Vulkan/tools/setup-native-dependencies.sh)
|
||||
mesa=$(sed -n 's/^mesa_revision=\([0-9a-f]\{40\}\).*/\1/p' PS5_Vulkan/tools/build-radv.sh)
|
||||
[[ -n $sdk && -n $mesa ]] || { echo "PS5_Vulkan's SDK or Mesa pin was not found" >&2; exit 2; }
|
||||
fetch_at PS5_PayloadSDK https://github.com/mihawk-99/PS5_PayloadSDK.git "$sdk"
|
||||
fetch_at PS5_Mesa https://github.com/mihawk-99/PS5_Mesa.git "$mesa"
|
||||
{
|
||||
echo "RADV_KEY=ps5ra-radv-$mesa-$sdk-$PS5_VULKAN_REVISION"
|
||||
echo "CI_DOWNLOADS=$GITHUB_WORKSPACE/ci-downloads"
|
||||
} >> "$GITHUB_ENV"
|
||||
echo "PS5_Vulkan ${PS5_VULKAN_REVISION:0:12}: SDK fork ${sdk:0:12}, Mesa ${mesa:0:12}"
|
||||
|
||||
- name: Restore the RADV driver
|
||||
id: radv
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: PS5_Vulkan/.deps/native/radv-release
|
||||
key: ${{ env.RADV_KEY }}
|
||||
|
||||
- name: Build RADV (PS5_Vulkan tools/build-radv.sh release)
|
||||
if: steps.radv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
bash PS5_RetroArch/.github/scripts/seed-zlib.sh PS5_Vulkan
|
||||
BUILD_JOBS=$(nproc) bash PS5_Vulkan/tools/setup-native-dependencies.sh
|
||||
bash PS5_RetroArch/.github/scripts/seed-mesa-wrap.sh PS5_Vulkan PS5_Mesa
|
||||
if ! bash PS5_Vulkan/tools/build-radv.sh release; then
|
||||
for log in PS5_Vulkan/.deps/work/*.log; do
|
||||
[[ -f $log ]] && { echo "::group::$log"; tail -n 60 "$log"; echo "::endgroup::"; }
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Save the RADV driver
|
||||
if: steps.radv.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: PS5_Vulkan/.deps/native/radv-release
|
||||
key: ${{ env.RADV_KEY }}
|
||||
|
||||
- run: cat PS5_Vulkan/.deps/native/radv-release/PROVENANCE.txt
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: radv-release
|
||||
path: PS5_Vulkan/.deps/native/radv-release
|
||||
retention-days: 3
|
||||
|
||||
core:
|
||||
name: Core ${{ matrix.core }}
|
||||
needs: plan
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 355
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(needs.plan.outputs.matrix) }}
|
||||
container:
|
||||
image: ubuntu:26.04
|
||||
# SDKs preinstalled on the runner, mounted so the job can delete them for disk space
|
||||
volumes:
|
||||
- /usr/share/dotnet:/host-free/dotnet
|
||||
- /usr/local/lib/android:/host-free/android
|
||||
- /opt/ghc:/host-free/ghc
|
||||
- /usr/local/.ghcup:/host-free/ghcup
|
||||
- /opt/hostedtoolcache:/host-free/toolcache
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
CORE: ${{ matrix.core }}
|
||||
CORE_CACHE: ps5ra-core-${{ matrix.core }}
|
||||
# ccache for MAME alone (tools/build-mame.sh sets its base directory): the other cores
|
||||
# are cached whole by their stamps, and every ccache would not fit the repository's cache
|
||||
PS5_DISABLE_CCACHE: ${{ matrix.core == 'mame' && '0' || '1' }}
|
||||
CCACHE_MAXSIZE: 5G
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Free disk space
|
||||
run: |
|
||||
find /host-free -mindepth 2 -maxdepth 2 -exec rm -rf {} + 2>/dev/null || true
|
||||
df -h "$GITHUB_WORKSPACE"
|
||||
|
||||
# git before the checkout, so it is a repository (tools/stage-notices.py reads its revision)
|
||||
- name: Install git
|
||||
run: apt-get update && apt-get install --yes --no-install-recommends ca-certificates git
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: PS5_RetroArch
|
||||
|
||||
- name: Install the build tools
|
||||
run: bash PS5_RetroArch/.github/scripts/install-host-tools.sh
|
||||
|
||||
- name: The payload SDK and zlib (tools/setup-native-dependencies.sh)
|
||||
run: |
|
||||
echo "CI_DOWNLOADS=$GITHUB_WORKSPACE/ci-downloads" >> "$GITHUB_ENV"
|
||||
export CI_DOWNLOADS=$GITHUB_WORKSPACE/ci-downloads
|
||||
bash PS5_RetroArch/.github/scripts/fetch-sdk-fork.sh
|
||||
bash PS5_RetroArch/.github/scripts/seed-zlib.sh PS5_RetroArch
|
||||
BUILD_JOBS=$(nproc) bash PS5_RetroArch/tools/setup-native-dependencies.sh
|
||||
|
||||
- name: Restore the core
|
||||
id: restore
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ci-core
|
||||
key: ${{ env.CORE_CACHE }}-${{ github.run_id }}
|
||||
restore-keys: ${{ env.CORE_CACHE }}-
|
||||
|
||||
- name: Restore ccache
|
||||
if: matrix.core == 'mame'
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ci-ccache
|
||||
key: ps5ra-ccache-mame-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
restore-keys: ps5ra-ccache-mame-
|
||||
|
||||
- name: Fetch the fork's pinned commit
|
||||
if: matrix.fork
|
||||
run: python3 PS5_RetroArch/.github/scripts/core-plan.py prefetch "$CORE"
|
||||
|
||||
- name: Build ${{ matrix.core }} (tools/${{ matrix.script }})
|
||||
timeout-minutes: 330
|
||||
working-directory: PS5_RetroArch
|
||||
run: |
|
||||
# the last build of this core, which its script keeps when its stamp still matches
|
||||
[[ ! -d ../ci-core ]] || cp -a ../ci-core/. .
|
||||
JOBS=$(nproc)
|
||||
export CCACHE_DIR=$GITHUB_WORKSPACE/ci-ccache JOBS
|
||||
[[ $PS5_DISABLE_CCACHE == 1 ]] || ccache --zero-stats
|
||||
bash "tools/${{ matrix.script }}"
|
||||
# a cache entry from before the collected set grew (LRPS2's build report) passes the
|
||||
# stamp but lacks what the title needs: that core is built again, once
|
||||
if ! python3 .github/scripts/core-plan.py check "$CORE"; then
|
||||
echo "::warning::$CORE: the cached build lacks what the title needs; building it again"
|
||||
PS5_FORCE_CORES=1 bash "tools/${{ matrix.script }}"
|
||||
python3 .github/scripts/core-plan.py check "$CORE"
|
||||
fi
|
||||
[[ $PS5_DISABLE_CCACHE == 1 ]] || ccache --show-stats
|
||||
|
||||
- name: Save ccache
|
||||
if: always() && matrix.core == 'mame'
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: ci-ccache
|
||||
key: ps5ra-ccache-mame-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
|
||||
- name: Collect what the title needs
|
||||
id: collect
|
||||
run: |
|
||||
rm -rf ci-core
|
||||
python3 PS5_RetroArch/.github/scripts/core-plan.py collect "$CORE" ci-core
|
||||
stamp=$(<"PS5_RetroArch/build/cores/stamps/${{ matrix.stamp }}")
|
||||
# the stamp, and the version of what collect keeps (a new one saves an entry again)
|
||||
echo "key=$CORE_CACHE-$stamp-c2" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Save the core
|
||||
if: steps.collect.outputs.key != steps.restore.outputs.cache-matched-key
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: ci-core
|
||||
key: ${{ steps.collect.outputs.key }}
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: core-${{ matrix.core }}
|
||||
path: ci-core
|
||||
include-hidden-files: true
|
||||
retention-days: 3
|
||||
|
||||
title:
|
||||
name: Title (PPSA99169)
|
||||
needs: [radv, core]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
container:
|
||||
image: ubuntu:26.04
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
# a development build: no release tag, and no frontends (see the top of this file)
|
||||
PS5_FRONTENDS: ''
|
||||
CCACHE_MAXSIZE: 1G
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
# git before the checkout, so it is a repository (tools/stage-notices.py reads its revision)
|
||||
- name: Install git
|
||||
run: apt-get update && apt-get install --yes --no-install-recommends ca-certificates git
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: PS5_RetroArch
|
||||
|
||||
- name: Install the build tools
|
||||
run: bash PS5_RetroArch/.github/scripts/install-host-tools.sh
|
||||
|
||||
- name: Fetch the pinned sources
|
||||
run: |
|
||||
fetch_at() { # fetch_at <dir> <url> <commit>: just that commit, checked out
|
||||
rm -rf "$1"
|
||||
git init -q "$1"
|
||||
git -C "$1" remote add origin "$2"
|
||||
git -C "$1" fetch -q --depth 1 origin "$3"
|
||||
git -C "$1" checkout -q --detach FETCH_HEAD
|
||||
}
|
||||
fetch_at PS5_Vulkan https://github.com/mihawk-99/PS5_Vulkan.git "$PS5_VULKAN_REVISION"
|
||||
bash PS5_RetroArch/.github/scripts/fetch-sdk-fork.sh
|
||||
# PS5_Mesa's licence texts, which tools/stage-notices.py copies for RADV
|
||||
mesa=$(sed -n 's/^mesa_revision=\([0-9a-f]\{40\}\).*/\1/p' PS5_Vulkan/tools/build-radv.sh)
|
||||
git init -q PS5_Mesa
|
||||
git -C PS5_Mesa remote add origin https://github.com/mihawk-99/PS5_Mesa.git
|
||||
git -C PS5_Mesa sparse-checkout set --no-cone /docs/license.rst '/licenses/'
|
||||
git -C PS5_Mesa fetch -q --depth 1 --filter=blob:none origin "$mesa"
|
||||
git -C PS5_Mesa checkout -q --detach FETCH_HEAD
|
||||
# LLVM's licence, for the SDK's LLVM runtime (stage-notices.py looks in ../PS5_LLVM)
|
||||
mkdir -p PS5_LLVM/llvm
|
||||
curl --fail --location --retry 3 --silent --show-error -o PS5_LLVM/llvm/LICENSE.TXT \
|
||||
https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-18.1.8/llvm/LICENSE.TXT
|
||||
# RetroArch at tools/fetch-retroarch.sh's pin, as a clone it pins in place
|
||||
revision=$(sed -n 's/^revision="\([0-9a-f]\{40\}\)".*/\1/p' PS5_RetroArch/tools/fetch-retroarch.sh)
|
||||
fetch_at PS5_RetroArch/vendor/retroarch https://github.com/libretro/RetroArch.git "$revision"
|
||||
bash PS5_RetroArch/tools/fetch-retroarch.sh
|
||||
# ES-DE at tools/build-esde.sh's pin, for the certificates the WebUI's daemon verifies
|
||||
# HTTPS against (resources/certificates/curl-ca-bundle.crt)
|
||||
version=$(sed -n 's/^esde_version=\(v[0-9.]*\)$/\1/p' PS5_RetroArch/tools/build-esde.sh)
|
||||
commit=$(sed -n 's/^esde_commit=\([0-9a-f]\{40\}\)$/\1/p' PS5_RetroArch/tools/build-esde.sh)
|
||||
git clone -q --depth 1 --branch "$version" https://gitlab.com/es-de/emulationstation-de.git \
|
||||
PS5_RetroArch/.deps/es-de
|
||||
[[ $(git -C PS5_RetroArch/.deps/es-de rev-parse HEAD) == "$commit" ]] ||
|
||||
{ echo "ES-DE $version is not $commit" >&2; exit 2; }
|
||||
|
||||
- name: Restore downloads
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
ci-downloads
|
||||
PS5_RetroArch/.deps/downloads
|
||||
key: ps5ra-downloads-${{ hashFiles('PS5_RetroArch/tools/video-assets.py', 'PS5_RetroArch/assets/**/source.json', 'PS5_RetroArch/tools/build-curl.sh') }}
|
||||
restore-keys: ps5ra-downloads-
|
||||
|
||||
- name: The payload SDK, zlib and libmicrohttpd
|
||||
run: |
|
||||
export CI_DOWNLOADS=$GITHUB_WORKSPACE/ci-downloads
|
||||
bash PS5_RetroArch/.github/scripts/seed-zlib.sh PS5_RetroArch
|
||||
BUILD_JOBS=$(nproc) bash PS5_RetroArch/tools/setup-native-dependencies.sh
|
||||
bash PS5_RetroArch/.github/scripts/seed-microhttpd.sh
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: radv-release
|
||||
path: PS5_Vulkan/.deps/native/radv-release
|
||||
|
||||
- name: Stand-ins for ps5vk's archives
|
||||
run: |
|
||||
# build-title.sh requires them before it links either driver; RADV's link replaces them
|
||||
for archive in build/driver/ps5/libps5vk.ps5.a .deps/native/vulkan-runtime/lib/libvk_runtime.ps5.a \
|
||||
build/driver/ps5/libpsbc_driver.ps5.a .deps/native/psbc/lib/libpsbc_support.ps5.a; do
|
||||
mkdir -p "PS5_Vulkan/$(dirname "$archive")"
|
||||
printf '!<arch>\n' > "PS5_Vulkan/$archive"
|
||||
done
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: core-*
|
||||
merge-multiple: true
|
||||
path: PS5_RetroArch
|
||||
|
||||
- name: Check the cores are the ones built
|
||||
working-directory: PS5_RetroArch
|
||||
run: |
|
||||
# shellcheck disable=SC2086 # a list of names
|
||||
python3 .github/scripts/core-plan.py skip $CI_SKIP_CORES
|
||||
# every core's library, stamp, build report and licence texts, before anything is built
|
||||
python3 .github/scripts/core-plan.py check all
|
||||
# each script finds its stamp and outputs and skips; one that would build again means
|
||||
# the core jobs and this one differ in an input, which this stops before it builds
|
||||
python3 .github/scripts/core-plan.py matrix |
|
||||
python3 -c 'import json, sys; [print(c["script"]) for c in json.load(sys.stdin)]' |
|
||||
while read -r script; do
|
||||
output=$(PS5_DISABLE_CCACHE=1 timeout 120 bash "tools/$script" 2>&1 | head -n 5) || true
|
||||
grep -q 'up to date' <<<"$output" ||
|
||||
{ echo "tools/$script did not find its core up to date:" >&2; echo "$output" >&2; exit 1; }
|
||||
echo "$output"
|
||||
done
|
||||
|
||||
# What the title builds besides the cores, kept from the last run: RetroArch's configured tree
|
||||
# and objects (build-retroarch.sh rebuilds what its fingerprint and patch stamps say moved),
|
||||
# the WebUI's libmicrohttpd, updater and curl (each stamped by its script), and the title's
|
||||
# own compiles (ccache)
|
||||
- name: Restore the title's builds
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: |
|
||||
ci-ccache
|
||||
PS5_RetroArch/build/ra
|
||||
PS5_RetroArch/build/ra-conf
|
||||
PS5_RetroArch/build/webui-mhd-ps5
|
||||
PS5_RetroArch/build/webui-update-ps5
|
||||
PS5_RetroArch/.deps/native/curl-ps5
|
||||
key: ps5ra-title-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
restore-keys: ps5ra-title-
|
||||
|
||||
- name: Build the frontend (tools/build-retroarch.sh)
|
||||
working-directory: PS5_RetroArch
|
||||
run: |
|
||||
sdk=$PWD/.deps/native/ps5-payload-sdk
|
||||
stub=$GITHUB_WORKSPACE/ci-vulkan-stub
|
||||
mkdir -p "$stub"
|
||||
# configure's vkCreateInstance link test (see the top of this file)
|
||||
printf 'int vkCreateInstance(void *a, void *b, void *c) { (void)a; (void)b; (void)c; return -9; }\n' \
|
||||
> "$stub/vulkan_stub.c"
|
||||
"$sdk/bin/prospero-clang" -c "$stub/vulkan_stub.c" -o "$stub/vulkan_stub.o"
|
||||
"$sdk/bin/prospero-ar" rcs "$stub/libvulkan.a" "$stub/vulkan_stub.o"
|
||||
PS5_PAYLOAD_SDK=$sdk PS5_CLANG=/usr/bin/clang LDFLAGS="-L$stub" bash tools/build-retroarch.sh
|
||||
|
||||
- name: Build the title (tools/build-title.sh)
|
||||
working-directory: PS5_RetroArch
|
||||
env:
|
||||
# the WebUI's ScreenScraper developer account, when the repository has one
|
||||
PS5_SCREENSCRAPER_DEVID: ${{ secrets.PS5_SCREENSCRAPER_DEVID }}
|
||||
PS5_SCREENSCRAPER_DEVPASSWORD: ${{ secrets.PS5_SCREENSCRAPER_DEVPASSWORD }}
|
||||
run: |
|
||||
export PS5_PAYLOAD_SDK=$PWD/.deps/native/ps5-payload-sdk PS5_CLANG=/usr/bin/clang
|
||||
JOBS=$(nproc)
|
||||
export CCACHE_DIR=$GITHUB_WORKSPACE/ci-ccache JOBS
|
||||
bash tools/build-title.sh
|
||||
ccache --show-stats || true
|
||||
|
||||
- name: Save the title's builds
|
||||
if: always()
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: |
|
||||
ci-ccache
|
||||
PS5_RetroArch/build/ra
|
||||
PS5_RetroArch/build/ra-conf
|
||||
PS5_RetroArch/build/webui-mhd-ps5
|
||||
PS5_RetroArch/build/webui-update-ps5
|
||||
PS5_RetroArch/.deps/native/curl-ps5
|
||||
key: ps5ra-title-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
|
||||
- name: Stage the bundle (dist/PPSA99169.zip)
|
||||
working-directory: PS5_RetroArch
|
||||
run: |
|
||||
# tools/build-title.sh's package: the PPSA99169 folder, checked after extraction
|
||||
mv dist/PS5_RetroArch.zip dist/PPSA99169.zip
|
||||
python3 - dist/PPSA99169.zip <<'PY'
|
||||
import json, sys, zipfile
|
||||
names = set(zipfile.ZipFile(sys.argv[1]).namelist())
|
||||
for required in ("PPSA99169/eboot.bin", "PPSA99169/sce_sys/param.json",
|
||||
"PPSA99169/sce_module/libc.prx", "PPSA99169/retroarch.cfg"):
|
||||
if required not in names:
|
||||
sys.exit(f"{required} is not in the bundle")
|
||||
cores = sorted(n for n in names if n.startswith("PPSA99169/cores/") and n.endswith(".so"))
|
||||
print(f"{len(names)} files, {len(cores)} cores")
|
||||
PY
|
||||
(cd dist && sha256sum PPSA99169.zip > PPSA99169.zip.sha256 && cat PPSA99169.zip.sha256)
|
||||
echo "SHORT_SHA=${GITHUB_SHA:0:7}" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
# the artifact downloads as a zip itself: PPSA99169.zip is inside it
|
||||
name: PS5_RetroArch-PPSA99169-${{ env.SHORT_SHA }}
|
||||
path: |
|
||||
PS5_RetroArch/dist/PPSA99169.zip
|
||||
PS5_RetroArch/dist/PPSA99169.zip.sha256
|
||||
retention-days: 14
|
||||
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""The cores tools/build-title.sh builds, for the workflow's per-core jobs.
|
||||
|
||||
core-plan.py matrix the core_names list of tools/build-title.sh as a JSON
|
||||
matrix: [{"core", "script", "stamp", "fork", "revision",
|
||||
"source"}]; fork and revision only for a core built from a
|
||||
mihawk-99 fork checkout (tools/core-fork.sh, LRPS2's own)
|
||||
core-plan.py prefetch CORE that fork's pinned commit alone, fetched and checked out in
|
||||
the tree the build script uses, so it does not clone the whole
|
||||
history (MAME's is gigabytes); the script still checks it out
|
||||
at the pin and cleans it when it builds
|
||||
core-plan.py skip CORE... leave those cores out of this checkout's title (CI_SKIP_CORES
|
||||
in the workflow): their names out of tools/build-title.sh's
|
||||
core_names and tools/generate-core-metadata.py's CORES, so the
|
||||
matrix has no job for them and the title neither builds nor
|
||||
stages them. An edit to the CI's working tree, never committed
|
||||
core-plan.py check CORE...|all every one of those cores has what the title needs from it
|
||||
(stamp, library, info file, build report, licence texts);
|
||||
exit 1 naming what is missing. A core job rebuilds a core its
|
||||
cache restored without them; the title job checks them all
|
||||
before it builds anything
|
||||
core-plan.py collect CORE OUT what the title job needs from a built core, copied into OUT
|
||||
at the same paths: build/cores/stage (the library, its info
|
||||
file, its system/ assets), the stamp that lets the title's
|
||||
own run of the script skip it, the build and ABI reports
|
||||
stage-notices.py reads, and the core option sources
|
||||
tools/generate-core-metadata.py extracts the WebUI's
|
||||
catalogs from
|
||||
|
||||
Each core's script is the one whose core_stamp_skip names <core>_libretro.so, the file
|
||||
build-title.sh stages.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TOOLS = ROOT / "tools"
|
||||
|
||||
|
||||
def core_names():
|
||||
text = (TOOLS / "build-title.sh").read_text()
|
||||
found = re.search(r"^core_names=\(([^)]*)\)", text, re.M)
|
||||
if not found:
|
||||
sys.exit("core-plan: no core_names=(...) in tools/build-title.sh")
|
||||
return found.group(1).split()
|
||||
|
||||
|
||||
def plan():
|
||||
scripts = sorted(TOOLS.glob("build-*.sh"))
|
||||
cores = []
|
||||
for core in core_names():
|
||||
output = f"/build/cores/stage/cores/{core}_libretro.so"
|
||||
matches = []
|
||||
for script in scripts:
|
||||
text = script.read_text()
|
||||
stamp = re.search(r"core_stamp_skip\s+([\w.-]+)((?:\s*\\?\n?\s*\"[^\"]*\")+)", text)
|
||||
if stamp and output in stamp.group(2):
|
||||
matches.append((script, text, stamp.group(1)))
|
||||
if len(matches) != 1:
|
||||
sys.exit(f"core-plan: {len(matches)} scripts stage {core}_libretro.so")
|
||||
script, text, stamp = matches[0]
|
||||
entry = {"core": core, "script": script.name, "stamp": stamp}
|
||||
fork = re.search(r"core_fork_checkout\s+(PS5_\w+)\s", text) or \
|
||||
re.search(r"github\.com/mihawk-99/(PS5_\w+)\.git", text)
|
||||
revision = re.search(r"^revision=([0-9a-f]{40})\b", text, re.M)
|
||||
if fork and revision:
|
||||
name = re.search(r"^core_name=([\w.-]+)", text, re.M)
|
||||
source = re.search(r'source_dir="\$root/(\.deps/[\w.-]+-src)"', text)
|
||||
entry.update(fork=fork.group(1), revision=revision.group(1),
|
||||
source=source.group(1) if source else f".deps/{name.group(1)}-src")
|
||||
cores.append(entry)
|
||||
return cores
|
||||
|
||||
|
||||
def find(core):
|
||||
for entry in plan():
|
||||
if entry["core"] == core:
|
||||
return entry
|
||||
sys.exit(f"core-plan: {core} is not in tools/build-title.sh's core_names")
|
||||
|
||||
|
||||
def git(*args, cwd=None):
|
||||
subprocess.run(["git", *args], cwd=cwd, check=True)
|
||||
|
||||
|
||||
def prefetch(core):
|
||||
entry = find(core)
|
||||
if "fork" not in entry:
|
||||
print(f"==> [{core}] {entry['script']} fetches its own source")
|
||||
return
|
||||
tree = ROOT / entry["source"]
|
||||
if (tree / ".git").is_dir():
|
||||
print(f"==> [{core}] {entry['source']} is already there")
|
||||
return
|
||||
git("init", "-q", str(tree))
|
||||
# origin is the published fork: a relative submodule URL (../PS5_Dynarmic) resolves against it
|
||||
git("remote", "add", "origin", f"https://github.com/mihawk-99/{entry['fork']}.git", cwd=tree)
|
||||
git("fetch", "-q", "--depth", "1", "--no-recurse-submodules", "origin", entry["revision"], cwd=tree)
|
||||
# checked out too: a core its stamp finds up to date is not checked out by its script, and
|
||||
# collect still takes its licence texts and option sources from the tree
|
||||
git("checkout", "-q", "--force", "--detach", entry["revision"], cwd=tree)
|
||||
print(f"==> [{core}] {entry['fork']} {entry['revision'][:12]} in {entry['source']}")
|
||||
|
||||
|
||||
def metadata_sources(core):
|
||||
"""The files generate-core-metadata.py reads for CORE: its source and that folder's others."""
|
||||
spec = __import__("importlib.util").util.spec_from_file_location(
|
||||
"core_metadata", TOOLS / "generate-core-metadata.py")
|
||||
module = __import__("importlib.util").util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
files = []
|
||||
for stem, _name, source, _flags in module.CORES:
|
||||
if stem == core:
|
||||
folder = (ROOT / source).parent
|
||||
files += [path for path in sorted(folder.iterdir()) if path.is_file()]
|
||||
return files
|
||||
|
||||
|
||||
def notice_components(entry):
|
||||
"""The core's entries in tooling/notices/components.json: those whose artifacts name its
|
||||
library. Their build folder (build/cores/<build>/build.json) is not always the stamp's name:
|
||||
LRPS2's is lrps2, its stamp pcsx2."""
|
||||
table = json.loads((ROOT / "tooling/notices/components.json").read_text(encoding="utf-8"))
|
||||
library = f"cores/{entry['core']}_libretro.so"
|
||||
return [component for component in table["components"]
|
||||
if component.get("source", {}).get("kind") == "core" and library in component.get("artifacts", [])]
|
||||
|
||||
|
||||
def licence_texts(entry, missing=None):
|
||||
"""The licence texts stage-notices.py copies for this core, from its source tree; a text
|
||||
none of whose paths exists goes to missing (or stops the script)."""
|
||||
paths = []
|
||||
for component in notice_components(entry):
|
||||
for text in component.get("texts", []):
|
||||
if "from" not in text:
|
||||
continue
|
||||
choices = text["from"] if isinstance(text["from"], list) else [text["from"]]
|
||||
found = next((ROOT / choice for choice in choices if (ROOT / choice).exists()), None)
|
||||
if found:
|
||||
paths.append(found)
|
||||
elif missing is None:
|
||||
sys.exit(f"core-plan: {component['id']}'s licence text {choices} is missing")
|
||||
else:
|
||||
missing.append(choices[0])
|
||||
return paths
|
||||
|
||||
|
||||
def required(entry):
|
||||
"""What the title cannot be made without, for this core: the stamp its script skips by, the
|
||||
library and its info file, and the build report stage-notices.py reads
|
||||
(build/cores/<build>/build.json)."""
|
||||
stage = ROOT / "build/cores/stage"
|
||||
files = [ROOT / "build/cores/stamps" / entry["stamp"],
|
||||
stage / "cores" / f"{entry['core']}_libretro.so", stage / "info" / f"{entry['core']}_libretro.info"]
|
||||
files += [ROOT / "build/cores" / component["source"]["build"] / "build.json"
|
||||
for component in notice_components(entry)]
|
||||
return files
|
||||
|
||||
|
||||
def missing_of(entry):
|
||||
missing = [str(path.relative_to(ROOT)) for path in required(entry) if not path.is_file()]
|
||||
licence_texts(entry, missing)
|
||||
return missing
|
||||
|
||||
|
||||
def check(cores):
|
||||
"""Every core's required files and licence texts are here; exit 1 naming what is not."""
|
||||
bad = 0
|
||||
for core in cores:
|
||||
missing = missing_of(find(core))
|
||||
if missing:
|
||||
bad += 1
|
||||
print(f"core-plan: {core} is missing {', '.join(missing)}", file=sys.stderr)
|
||||
if bad:
|
||||
sys.exit(1)
|
||||
print(f"==> [cores] {len(cores)} cores have their libraries, stamps, build reports and licences")
|
||||
|
||||
|
||||
def collect(core, out):
|
||||
entry = find(core)
|
||||
out = Path(out).resolve()
|
||||
stage = ROOT / "build/cores/stage"
|
||||
files = required(entry) + sorted((stage / "cores").glob(f"{core}_libretro.*")) + \
|
||||
sorted((stage / "info").glob(f"{core}_libretro.*"))
|
||||
# the ABI reports beside the build reports, in the folder a script names after its stamp, its
|
||||
# core or its notice entry
|
||||
builds = {component["source"]["build"] for component in notice_components(entry)}
|
||||
for folder in sorted({entry["stamp"], core} | builds):
|
||||
files += [ROOT / "build/cores" / folder / "abi.json"]
|
||||
files = [path for path in dict.fromkeys(files) if path.is_file() or path in required(entry)]
|
||||
files += metadata_sources(core)
|
||||
folders = [stage / "system"] if (stage / "system").is_dir() else []
|
||||
for path in licence_texts(entry):
|
||||
(folders if path.is_dir() else files).append(path)
|
||||
for path in files:
|
||||
if not path.is_file():
|
||||
sys.exit(f"core-plan: {core} did not leave {path.relative_to(ROOT)}")
|
||||
target = out / path.relative_to(ROOT)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(path, target)
|
||||
# folders: assets a core stages under build/cores/stage/system (PPSSPP, Dolphin; only this
|
||||
# job's core is there), and licence folders
|
||||
for path in folders:
|
||||
shutil.copytree(path, out / path.relative_to(ROOT), dirs_exist_ok=True)
|
||||
print(f"==> [{core}] {len(files)} files and {len(folders)} folders collected in {out}")
|
||||
|
||||
|
||||
def skip(cores):
|
||||
known = core_names()
|
||||
for core in cores:
|
||||
if core not in known:
|
||||
sys.exit(f"core-plan: {core} is not in tools/build-title.sh's core_names")
|
||||
title = TOOLS / "build-title.sh"
|
||||
text = title.read_text()
|
||||
found = re.search(r"^core_names=\(([^)]*)\)", text, re.M)
|
||||
kept = [core for core in found.group(1).split() if core not in cores]
|
||||
title.write_text(text[:found.start(1)] + " ".join(kept) + text[found.end(1):])
|
||||
metadata = TOOLS / "generate-core-metadata.py"
|
||||
lines = metadata.read_text().splitlines(keepends=True)
|
||||
entry = re.compile(r"^\s*\('(" + "|".join(map(re.escape, cores)) + r")',")
|
||||
metadata.write_text("".join(line for line in lines if not entry.match(line)))
|
||||
print(f"==> [cores] left out of this build: {' '.join(cores)}; {len(kept)} cores")
|
||||
|
||||
|
||||
def main(argv):
|
||||
if argv[:1] == ["matrix"] and len(argv) == 1:
|
||||
print(json.dumps(plan()))
|
||||
elif argv[:1] == ["prefetch"] and len(argv) == 2:
|
||||
prefetch(argv[1])
|
||||
elif argv[:1] == ["skip"]:
|
||||
if argv[1:]:
|
||||
skip(argv[1:])
|
||||
elif argv[:1] == ["check"] and len(argv) >= 2:
|
||||
check(argv[1:] if argv[1:] != ["all"] else [entry["core"] for entry in plan()])
|
||||
elif argv[:1] == ["collect"] and len(argv) == 3:
|
||||
collect(argv[1], argv[2])
|
||||
else:
|
||||
sys.exit("usage: core-plan.py matrix | prefetch CORE | skip CORE... | check CORE...|all | collect CORE OUT")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# ../PS5_PayloadSDK, the payload SDK fork, at the commit tools/setup-native-dependencies.sh
|
||||
# pins: just that commit, fetched and checked out beside this checkout. The setup exports it with
|
||||
# git archive; tools/stage-notices.py copies its licence texts from the working tree (LICENSE,
|
||||
# platform/src/regex/COPYRIGHT.musl).
|
||||
#
|
||||
# .github/scripts/fetch-sdk-fork.sh
|
||||
set -euo pipefail
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
fork=$root/../PS5_PayloadSDK
|
||||
revision=$(sed -n 's/^sdk_revision=\([0-9a-f]\{40\}\).*/\1/p' "$root/tools/setup-native-dependencies.sh")
|
||||
[[ -n $revision ]] || { echo "fetch-sdk-fork: no sdk_revision in tools/setup-native-dependencies.sh" >&2; exit 2; }
|
||||
if ! git -C "$fork" cat-file -e "$revision^{commit}" 2>/dev/null; then
|
||||
[[ -d $fork/.git ]] || {
|
||||
git init -q "$fork"
|
||||
git -C "$fork" remote add origin https://github.com/mihawk-99/PS5_PayloadSDK.git
|
||||
}
|
||||
git -C "$fork" fetch -q --depth 1 origin "$revision"
|
||||
fi
|
||||
git -C "$fork" checkout -q --force --detach "$revision"
|
||||
[[ -f $fork/LICENSE ]] || { echo "fetch-sdk-fork: no LICENSE in $fork at $revision" >&2; exit 2; }
|
||||
echo "==> [sdk] PS5_PayloadSDK ${revision:0:12} beside $root"
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# The host tools of the core and title jobs, in an ubuntu:26.04 container: README's (git, wget,
|
||||
# unzip, make, patch, pkg-config, readelf, host compilers), what the core builds run (CMake,
|
||||
# Ninja, autotools, Python 3), rev and flock for the SDK's scripts, and LLVM 22 as the unversioned
|
||||
# clang, clang++ and llvm-config: /usr/bin/clang is the compiler tools/build-title.sh names
|
||||
# (PS5_CLANG), and the SDK's wrappers run the LLVM its prospero-llvm-config finds, llvm-config
|
||||
# when no llvm-config-15 to -21 is installed. LLVM 22 links what the project's checks expect:
|
||||
# FreeBSD/PS5 core libraries (tools/check-core.py) and -z nodynamic-undefined-weak for the
|
||||
# title's weak references. Its bin directory goes on PATH for the unversioned tools (llvm-objdump).
|
||||
set -euo pipefail
|
||||
apt-get update
|
||||
apt-get install --yes --no-install-recommends \
|
||||
ca-certificates git curl wget unzip zip xz-utils bzip2 file make build-essential pkgconf \
|
||||
patch rsync ccache binutils cmake ninja-build autoconf automake libtool bison flex gperf \
|
||||
bsdextrautils util-linux xxd python3 python3-pip python3-yaml python3-jsonschema \
|
||||
clang-22 lld-22 llvm-22 libclang-rt-22-dev
|
||||
git config --global --add safe.directory '*'
|
||||
for tool in clang clang++ llvm-config; do
|
||||
ln -sf "../lib/llvm-22/bin/$tool" "/usr/bin/$tool"
|
||||
done
|
||||
for version in 15 16 17 18 19 20 21; do
|
||||
! command -v "llvm-config-$version" >/dev/null || { echo "llvm-config-$version would be the SDK's" >&2; exit 2; }
|
||||
done
|
||||
llvm=/usr/lib/llvm-22/bin
|
||||
[[ -n ${GITHUB_PATH:-} ]] && echo "$llvm" >> "$GITHUB_PATH"
|
||||
for tool in "$llvm/llvm-objdump" "$llvm/ld.lld" \
|
||||
"$(clang --print-resource-dir)/lib/linux/libclang_rt.builtins-x86_64.a"; do
|
||||
[[ -e $tool ]] || { echo "missing $tool" >&2; exit 2; }
|
||||
done
|
||||
clang --version | head -n 1
|
||||
llvm-config --version
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Mesa's zlib subproject (its subprojects/zlib.wrap, which RADV's PS5 build takes with
|
||||
# --force-fallback-for=zlib) for PS5_Vulkan's recipe: the wrap's source and patch put in the tree's
|
||||
# subprojects/packagecache, where meson looks before it downloads. The wrap names zlib.net, and
|
||||
# wrapdb.mesonbuild.com for the patch with no fallback; WrapDB's GitHub releases carry both files,
|
||||
# checked here against the wrap's own hashes. The recipe keeps packagecache when it refreshes its tree.
|
||||
#
|
||||
# .github/scripts/seed-mesa-wrap.sh <PS5_Vulkan checkout> <PS5_Mesa checkout>
|
||||
set -euo pipefail
|
||||
vulkan=${1:?usage: seed-mesa-wrap.sh <PS5_Vulkan> <PS5_Mesa>}
|
||||
mesa=${2:?usage: seed-mesa-wrap.sh <PS5_Vulkan> <PS5_Mesa>}
|
||||
revision=$(sed -n 's/^mesa_revision=\([0-9a-f]\{40\}\).*/\1/p' "$vulkan/tools/build-radv.sh")
|
||||
[[ -n $revision ]] || { echo "seed-mesa-wrap: no mesa_revision in $vulkan/tools/build-radv.sh" >&2; exit 2; }
|
||||
wrap=$(git -C "$mesa" show "$revision:subprojects/zlib.wrap")
|
||||
field() { sed -n "s/^$1 *= *//p" <<<"$wrap" | head -n 1; }
|
||||
release=$(field wrapdb_version)
|
||||
[[ -n $release ]] || { echo "seed-mesa-wrap: the wrap at ${revision:0:12} has no wrapdb_version" >&2; exit 2; }
|
||||
cache=$vulkan/.deps/work/radv-src/subprojects/packagecache
|
||||
downloads=${CI_DOWNLOADS:-$HOME/.cache/ps5-retroarch-ci}/mesa-wrap
|
||||
mkdir -p "$cache" "$downloads"
|
||||
for kind in source patch; do
|
||||
name=$(field "${kind}_filename")
|
||||
hash=$(field "${kind}_hash")
|
||||
[[ -n $name && $hash =~ ^[0-9a-f]{64}$ ]] || { echo "seed-mesa-wrap: no ${kind}_filename or ${kind}_hash" >&2; exit 2; }
|
||||
if ! printf '%s %s\n' "$hash" "$downloads/$name" | sha256sum --check --strict --status 2>/dev/null; then
|
||||
curl --fail --location --retry 3 --silent --show-error -o "$downloads/$name.download" \
|
||||
"https://github.com/mesonbuild/wrapdb/releases/download/zlib_$release/$name"
|
||||
mv -- "$downloads/$name.download" "$downloads/$name"
|
||||
fi
|
||||
printf '%s %s\n' "$hash" "$downloads/$name" | sha256sum --check --strict
|
||||
cp -- "$downloads/$name" "$cache/$name"
|
||||
done
|
||||
echo "==> [mesa-wrap] zlib $release's source and patch in $cache"
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# libmicrohttpd's release tarball for tools/build-webui-http.sh, put where it looks
|
||||
# (.deps/webui), so the build does not depend on ftp.gnu.org answering a CI runner. GNU's
|
||||
# server first, then Ubuntu's archive, whose orig tarball is the same file; either is checked
|
||||
# against the script's own pin (version, hash) and kept in CI_DOWNLOADS.
|
||||
#
|
||||
# .github/scripts/seed-microhttpd.sh
|
||||
set -euo pipefail
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
script=$root/tools/build-webui-http.sh
|
||||
version=$(sed -n 's/^version=\([0-9.]*\)$/\1/p' "$script")
|
||||
hash=$(sed -n 's/^hash=\([0-9a-f]\{64\}\)$/\1/p' "$script")
|
||||
[[ -n $version && -n $hash ]] || { echo "seed-microhttpd: no version or hash in $script" >&2; exit 2; }
|
||||
target=$root/.deps/webui/libmicrohttpd-$version.tar.gz
|
||||
check() { printf '%s %s\n' "$hash" "$1" | sha256sum --check --strict --status 2>/dev/null; }
|
||||
if check "$target"; then
|
||||
echo "==> [microhttpd] $target is libmicrohttpd $version"
|
||||
exit 0
|
||||
fi
|
||||
cache=${CI_DOWNLOADS:-$HOME/.cache/ps5-retroarch-ci}/libmicrohttpd-$version.tar.gz
|
||||
mkdir -p "$(dirname -- "$cache")" "$(dirname -- "$target")"
|
||||
if ! check "$cache"; then
|
||||
for url in "https://ftp.gnu.org/gnu/libmicrohttpd/libmicrohttpd-$version.tar.gz" \
|
||||
"http://archive.ubuntu.com/ubuntu/pool/universe/libm/libmicrohttpd/libmicrohttpd_$version.orig.tar.gz"; do
|
||||
if curl --fail --location --retry 3 --connect-timeout 30 --silent --show-error \
|
||||
-o "$cache.download" "$url" && check "$cache.download"; then
|
||||
mv -- "$cache.download" "$cache"
|
||||
echo "==> [microhttpd] from $url"
|
||||
break
|
||||
fi
|
||||
rm -f -- "$cache.download"
|
||||
done
|
||||
fi
|
||||
printf '%s %s\n' "$hash" "$cache" | sha256sum --check --strict
|
||||
cp -- "$cache" "$target"
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# zlib for a checkout whose tools/setup-native-dependencies.sh builds it (this one, PS5_Vulkan):
|
||||
# the tarball put where that script looks for it, so it does not download it from
|
||||
# zlib.net, which can answer a CI runner with something other than the tarball. zlib's GitHub release
|
||||
# carries the same file; it is checked against the script's own pin (zlib_version, zlib_hash), and
|
||||
# kept in CI_DOWNLOADS.
|
||||
#
|
||||
# .github/scripts/seed-zlib.sh <checkout>
|
||||
set -euo pipefail
|
||||
checkout=${1:?usage: seed-zlib.sh <checkout>}
|
||||
script=$checkout/tools/setup-native-dependencies.sh
|
||||
[[ -f $script ]] || { echo "seed-zlib: no $script" >&2; exit 2; }
|
||||
version=$(sed -n 's/^zlib_version="\([0-9.]*\)".*/\1/p' "$script")
|
||||
hash=$(sed -n 's/^zlib_hash="\([0-9a-f]\{64\}\)".*/\1/p' "$script")
|
||||
[[ -n $version && -n $hash ]] || { echo "seed-zlib: no zlib_version or zlib_hash in $script" >&2; exit 2; }
|
||||
target=$checkout/.deps/native/zlib/zlib-$version.tar.gz
|
||||
mkdir -p "$(dirname -- "$target")"
|
||||
if printf '%s %s\n' "$hash" "$target" | sha256sum --check --strict --status 2>/dev/null; then
|
||||
echo "==> [zlib] $target is zlib $version"
|
||||
exit 0
|
||||
fi
|
||||
cache=${CI_DOWNLOADS:-$HOME/.cache/ps5-retroarch-ci}/zlib-$version.tar.gz
|
||||
mkdir -p "$(dirname -- "$cache")"
|
||||
if ! printf '%s %s\n' "$hash" "$cache" | sha256sum --check --strict --status 2>/dev/null; then
|
||||
curl --fail --location --retry 3 --silent --show-error -o "$cache.download" \
|
||||
"https://github.com/madler/zlib/releases/download/v$version/zlib-$version.tar.gz"
|
||||
mv -- "$cache.download" "$cache"
|
||||
fi
|
||||
printf '%s %s\n' "$hash" "$cache" | sha256sum --check --strict
|
||||
cp -- "$cache" "$target"
|
||||
echo "==> [zlib] $target: zlib $version from its GitHub release"
|
||||
@@ -15,6 +15,9 @@
|
||||
* --ps5-mode=quit a frontend quit (EmulationStation, or RetroArch the
|
||||
* picker started): the picker, or, when a frontend is
|
||||
* remembered, the title closes
|
||||
* --rom <file> [--core <core>] [--exit-after-game]
|
||||
* a home screen forwarder's game (no mode): run as a frontend's
|
||||
* game, then the title as from the home screen, or closed
|
||||
* no mode a launch from the home screen: the frontend remembered
|
||||
* in config/frontend.cfg (src/ps5_frontend_choice.h), or
|
||||
* the picker when none is or L1 is held as the title
|
||||
@@ -46,10 +49,14 @@
|
||||
*/
|
||||
#include "frontend_mode_ps5.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "ps5_frontend_choice.h"
|
||||
@@ -67,9 +74,26 @@ std::string launch_mode;
|
||||
struct ps5_game game;
|
||||
bool game_running = false;
|
||||
std::time_t game_started = 0;
|
||||
/* The game is a forwarder's (--rom), not a frontend's: nothing to hand a result back to */
|
||||
bool game_forwarded = false;
|
||||
bool forward_exit_after_game = false;
|
||||
|
||||
extern "C" void ps5_permissions_settle(); /* src/permissions_ps5.cpp */
|
||||
|
||||
std::string joined(const std::vector<std::string> &names)
|
||||
{
|
||||
std::string text;
|
||||
for (const std::string &name : names)
|
||||
text += (text.empty() ? "" : ", ") + name;
|
||||
return text;
|
||||
}
|
||||
|
||||
bool is_folder(const std::string &path)
|
||||
{
|
||||
struct stat status;
|
||||
return stat(path.c_str(), &status) == 0 && S_ISDIR(status.st_mode);
|
||||
}
|
||||
|
||||
bool exists(const std::string &path)
|
||||
{
|
||||
std::FILE *file = std::fopen(path.c_str(), "rb");
|
||||
@@ -172,6 +196,7 @@ bool take_game(const Paths &paths, unsigned replaced_wait_seconds)
|
||||
}
|
||||
game = request;
|
||||
game_running = true;
|
||||
game_forwarded = false;
|
||||
game_started = std::time(nullptr);
|
||||
char line[PS5_GAME_PATH_MAX * 3 + 64];
|
||||
std::snprintf(line, sizeof line, "game mode: %s with %s, for %s", game.content, game.core,
|
||||
@@ -179,6 +204,113 @@ bool take_game(const Paths &paths, unsigned replaced_wait_seconds)
|
||||
ps5::debug::mark(line);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* A forwarder's game: true when RetroArch is to run it in this process. Not runnable,
|
||||
* the reason goes to the trace and the launch goes on as one from the home screen. */
|
||||
bool take_forward(const Paths &paths, const Forward &forward)
|
||||
{
|
||||
struct ps5_game request = {};
|
||||
const std::string content = forward_content(forward.rom, paths.content);
|
||||
const std::string core = forward_core(forward.core, PS5_GAME_CORES);
|
||||
std::snprintf(request.content, sizeof(request.content), "%s", content.c_str());
|
||||
std::snprintf(request.core, sizeof(request.core), "%s", core.c_str());
|
||||
/* game mode's check wants a frontend to come back to: the title itself */
|
||||
std::snprintf(request.frontend, sizeof(request.frontend), "%s", paths.eboot.c_str());
|
||||
const auto refused = [&](const char *why)
|
||||
{
|
||||
char line[PS5_GAME_PATH_MAX + 400];
|
||||
std::snprintf(line, sizeof line, "forwarder: --rom %s --core %s not run: %s",
|
||||
forward.rom.c_str(), forward.core.empty() ? "(none)" : forward.core.c_str(),
|
||||
why);
|
||||
ps5::debug::mark(line);
|
||||
return false;
|
||||
};
|
||||
if (content.empty())
|
||||
return refused("the content is not a path inside the content folder (no \"..\")");
|
||||
if (core.size() >= sizeof(request.core) || content.size() >= sizeof(request.content))
|
||||
return refused("a path is too long");
|
||||
/* A folder is content for a core that opens folders, as DOSBox Pure opens a DOS or Windows
|
||||
* installation: its .info lists "/" in supported_extensions. Game mode's check wants a file,
|
||||
* so a folder's request is checked with the title in its place, for the core and frontend. */
|
||||
const bool folder = is_folder(content);
|
||||
const std::vector<std::string> folder_cores =
|
||||
folder ? forward_folder_cores(paths.info) : std::vector<std::string>();
|
||||
const auto check = [&](struct ps5_game &candidate)
|
||||
{
|
||||
if (!folder)
|
||||
return ps5_game_check(&candidate);
|
||||
struct ps5_game probe = candidate;
|
||||
std::snprintf(probe.content, sizeof(probe.content), "%s", paths.eboot.c_str());
|
||||
const int result = ps5_game_check(&probe);
|
||||
std::snprintf(candidate.error, sizeof(candidate.error), "%s", probe.error);
|
||||
if (result != 0 || !candidate.core[0])
|
||||
return result;
|
||||
std::string name = candidate.core;
|
||||
name = name.substr(name.rfind('/') + 1);
|
||||
name.erase(name.size() - std::strlen("_libretro.so"));
|
||||
if (std::find(folder_cores.begin(), folder_cores.end(), name) != folder_cores.end())
|
||||
return 0;
|
||||
std::snprintf(candidate.error, sizeof(candidate.error),
|
||||
"the content is a folder, and %s does not open folders (its .info lists no "
|
||||
"\"/\"%s%s)",
|
||||
name.c_str(),
|
||||
folder_cores.empty() ? "" : "; these do: ", joined(folder_cores).c_str());
|
||||
return -1;
|
||||
};
|
||||
if (check(request) != 0)
|
||||
return refused(request.error);
|
||||
/* --core is the user's choice for this tile; without it, RetroArch's playlists choose,
|
||||
* as they do for a frontend's game. */
|
||||
if (!request.core[0])
|
||||
{
|
||||
struct ps5_game associated = request;
|
||||
if (ps5_game_playlist_core(paths.playlists.c_str(), request.content, associated.core,
|
||||
sizeof(associated.core)) &&
|
||||
check(associated) == 0)
|
||||
std::snprintf(request.core, sizeof(request.core), "%s", associated.core);
|
||||
/* nor a playlist: the core whose .info file lists the content's extension, when one
|
||||
* does (or a shared cartridge extension's own core) */
|
||||
std::vector<std::string> runnable;
|
||||
for (const std::string &name :
|
||||
folder ? folder_cores : forward_extension_cores(paths.info, request.content))
|
||||
{
|
||||
struct ps5_game candidate = request;
|
||||
const std::string path = forward_core(name, PS5_GAME_CORES);
|
||||
std::snprintf(candidate.core, sizeof(candidate.core), "%s", path.c_str());
|
||||
if (path.size() < sizeof(candidate.core) && check(candidate) == 0)
|
||||
runnable.push_back(name);
|
||||
}
|
||||
std::string chosen =
|
||||
runnable.size() == 1 ? runnable[0] : forward_preferred_core(request.content, runnable);
|
||||
if (!request.core[0] && !chosen.empty())
|
||||
std::snprintf(request.core, sizeof(request.core), "%s",
|
||||
forward_core(chosen, PS5_GAME_CORES).c_str());
|
||||
if (!request.core[0] && runnable.empty())
|
||||
return refused(folder
|
||||
? "no core: the content is a folder, and no core of the title "
|
||||
"opens folders"
|
||||
: "no core: neither --core nor RetroArch's playlists name one, and "
|
||||
"no core lists its extension");
|
||||
if (!request.core[0])
|
||||
{
|
||||
const std::string why = "no core: neither --core nor RetroArch's playlists name one, "
|
||||
"and several cores " +
|
||||
std::string(folder ? "open folders" : "list its extension") +
|
||||
" (" + joined(runnable) + "); --core names the one";
|
||||
return refused(why.c_str());
|
||||
}
|
||||
}
|
||||
game = request;
|
||||
game_running = true;
|
||||
game_forwarded = true;
|
||||
forward_exit_after_game = forward.exit_after_game;
|
||||
game_started = std::time(nullptr);
|
||||
char line[PS5_GAME_PATH_MAX * 2 + 64];
|
||||
std::snprintf(line, sizeof line, "forwarder: %s with %s%s", game.content, game.core,
|
||||
forward_exit_after_game ? ", closing after it" : "");
|
||||
ps5::debug::mark(line);
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string mode_argument(int argc, char **argv)
|
||||
@@ -190,6 +322,175 @@ std::string mode_argument(int argc, char **argv)
|
||||
return "";
|
||||
}
|
||||
|
||||
Forward forward_arguments(int argc, char **argv)
|
||||
{
|
||||
Forward forward;
|
||||
const auto value = [&](int &i, const char *flag, std::string &out)
|
||||
{
|
||||
const std::size_t length = std::strlen(flag);
|
||||
if (std::strcmp(argv[i], flag) == 0)
|
||||
{
|
||||
if (i + 1 < argc && argv[i + 1])
|
||||
out = argv[++i];
|
||||
return true;
|
||||
}
|
||||
if (std::strncmp(argv[i], flag, length) == 0 && argv[i][length] == '=')
|
||||
{
|
||||
out = argv[i] + length + 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (int i = 0; i < argc && argv && argv[i]; i++)
|
||||
{
|
||||
if (std::strcmp(argv[i], "--exit-after-game") == 0)
|
||||
forward.exit_after_game = true;
|
||||
else if (!value(i, "--rom", forward.rom))
|
||||
value(i, "--core", forward.core);
|
||||
}
|
||||
return forward;
|
||||
}
|
||||
|
||||
std::string forward_content(const std::string &rom, const std::string &content)
|
||||
{
|
||||
std::string path = rom;
|
||||
while (!path.empty() && (path.back() == ' ' || path.back() == '\r' || path.back() == '\n'))
|
||||
path.pop_back();
|
||||
while (path.size() > 1 && path.back() == '/') /* a folder given as "dos/w95/" */
|
||||
path.pop_back();
|
||||
if (path.empty())
|
||||
return "";
|
||||
if (path[0] == '/')
|
||||
return path;
|
||||
for (std::string::size_type at = 0; at <= path.size();)
|
||||
{
|
||||
const std::string::size_type slash = path.find('/', at);
|
||||
const std::string::size_type end = slash == std::string::npos ? path.size() : slash;
|
||||
if (path.compare(at, end - at, "..") == 0 && end - at == 2)
|
||||
return "";
|
||||
at = end + 1;
|
||||
}
|
||||
return content + path;
|
||||
}
|
||||
|
||||
std::string forward_core(const std::string &core, const std::string &cores)
|
||||
{
|
||||
if (core.empty() || core.find('/') != std::string::npos)
|
||||
return core;
|
||||
static const char suffix[] = "_libretro.so";
|
||||
const std::string::size_type suffix_length = sizeof(suffix) - 1;
|
||||
std::string name = core;
|
||||
if (name.size() > 3 && name.compare(name.size() - 3, 3, ".so") == 0)
|
||||
name.erase(name.size() - 3);
|
||||
if (name.size() >= suffix_length - 3 &&
|
||||
name.compare(name.size() - (suffix_length - 3), suffix_length - 3, "_libretro") == 0)
|
||||
name.erase(name.size() - (suffix_length - 3));
|
||||
return name.empty() ? "" : cores + name + suffix;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string lower(std::string text)
|
||||
{
|
||||
for (char &c : text)
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
return text;
|
||||
}
|
||||
|
||||
/* The content's extension, lower case, without the dot; empty for none */
|
||||
std::string extension_of(const std::string &content)
|
||||
{
|
||||
const std::string::size_type slash = content.rfind('/');
|
||||
const std::string::size_type dot = content.rfind('.');
|
||||
if (dot == std::string::npos || (slash != std::string::npos && dot < slash) ||
|
||||
dot + 1 == content.size())
|
||||
return "";
|
||||
return lower(content.substr(dot + 1));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace
|
||||
{
|
||||
/* The cores whose .info lists token in supported_extensions ("sfc", or "/" for a folder) */
|
||||
std::vector<std::string> cores_listing(const std::string &info, const std::string &extension)
|
||||
{
|
||||
std::vector<std::string> cores;
|
||||
if (extension.empty())
|
||||
return cores;
|
||||
DIR *folder = opendir(info.c_str());
|
||||
if (!folder)
|
||||
return cores;
|
||||
static const char suffix[] = "_libretro.info";
|
||||
const std::string::size_type suffix_length = sizeof(suffix) - 1;
|
||||
for (struct dirent *entry; (entry = readdir(folder)) != nullptr;)
|
||||
{
|
||||
const std::string file = entry->d_name;
|
||||
if (file.size() <= suffix_length ||
|
||||
file.compare(file.size() - suffix_length, suffix_length, suffix) != 0)
|
||||
continue;
|
||||
std::FILE *input = std::fopen((info + file).c_str(), "r");
|
||||
if (!input)
|
||||
continue;
|
||||
char line[4096];
|
||||
bool listed = false;
|
||||
while (!listed && std::fgets(line, sizeof line, input))
|
||||
{
|
||||
/* supported_extensions = "sfc|smc|swc" */
|
||||
const char *at = line;
|
||||
while (*at == ' ' || *at == '\t')
|
||||
at++;
|
||||
if (std::strncmp(at, "supported_extensions", 20) != 0)
|
||||
continue;
|
||||
const char *open = std::strchr(at, '"');
|
||||
const char *close = open ? std::strchr(open + 1, '"') : nullptr;
|
||||
if (!close)
|
||||
break;
|
||||
const std::string list = lower(std::string(open + 1, close));
|
||||
for (std::string::size_type start = 0; start <= list.size();)
|
||||
{
|
||||
std::string::size_type bar = list.find('|', start);
|
||||
if (bar == std::string::npos)
|
||||
bar = list.size();
|
||||
if (list.compare(start, bar - start, extension) == 0 &&
|
||||
bar - start == extension.size())
|
||||
listed = true;
|
||||
start = bar + 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
std::fclose(input);
|
||||
if (listed)
|
||||
cores.push_back(file.substr(0, file.size() - suffix_length));
|
||||
}
|
||||
closedir(folder);
|
||||
std::sort(cores.begin(), cores.end());
|
||||
return cores;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<std::string> forward_extension_cores(const std::string &info,
|
||||
const std::string &content)
|
||||
{
|
||||
return cores_listing(info, extension_of(content));
|
||||
}
|
||||
|
||||
std::vector<std::string> forward_folder_cores(const std::string &info)
|
||||
{
|
||||
return cores_listing(info, "/");
|
||||
}
|
||||
|
||||
std::string forward_preferred_core(const std::string &content,
|
||||
const std::vector<std::string> &cores)
|
||||
{
|
||||
static const char *const sega[] = {"md", "smd", "gen", "sms", "gg", "sg", "68k", "sgd"};
|
||||
const std::string extension = extension_of(content);
|
||||
for (const char *shared : sega)
|
||||
if (extension == shared &&
|
||||
std::find(cores.begin(), cores.end(), "genesis_plus_gx") != cores.end())
|
||||
return "genesis_plus_gx";
|
||||
return "";
|
||||
}
|
||||
|
||||
bool session_start(int argc, char **argv)
|
||||
{
|
||||
for (int i = 0; i < argc && argv && argv[i]; i++)
|
||||
@@ -290,6 +591,14 @@ void run(const Paths &paths, int argc, char **argv, unsigned replaced_wait_secon
|
||||
if (launch.mode.empty() && !launch.test_run && !launch.picker_test && launch.choice != "ask" &&
|
||||
launch.picker_present && paths.reopen_held)
|
||||
launch.reopen_held = paths.reopen_held();
|
||||
/* A forwarder's game, on a launch that names no mode (a handover's mode wins). */
|
||||
const Forward forward = forward_arguments(argc, argv);
|
||||
if (!forward.rom.empty() && launch.mode.empty())
|
||||
{
|
||||
if (take_forward(paths, forward))
|
||||
return; /* RetroArch runs the game */
|
||||
/* not runnable (the trace says why): the launch goes on as from the home screen */
|
||||
}
|
||||
const Next next = decide(launch);
|
||||
char line[256];
|
||||
std::snprintf(line, sizeof line,
|
||||
@@ -336,6 +645,23 @@ void after_retroarch(const Paths &paths, const std::string &mode, int status,
|
||||
game_running = false;
|
||||
return;
|
||||
}
|
||||
if (game_running && game_forwarded)
|
||||
{
|
||||
/* A forwarder's game: no frontend to hand a result to. The title opens as from
|
||||
* the home screen (the picker, or the frontend remembered), or closes. */
|
||||
game_running = game_forwarded = false;
|
||||
char line[128];
|
||||
std::snprintf(line, sizeof line, "forwarder: RetroArch quit (status %d) after %ld s; %s",
|
||||
status, static_cast<long>(std::time(nullptr) - game_started),
|
||||
forward_exit_after_game ? "the title closes"
|
||||
: "the title as from the home screen");
|
||||
ps5::debug::mark(line);
|
||||
if (forward_exit_after_game)
|
||||
return;
|
||||
restart_as(paths.eboot, replaced_wait_seconds,
|
||||
"forwarder: LoadExec did not replace the process; the title closes; result");
|
||||
return;
|
||||
}
|
||||
if (game_running)
|
||||
{
|
||||
game_running = false;
|
||||
@@ -374,6 +700,8 @@ const ps5::frontend_mode::Paths title_paths{"/app0/picker/picker.bin",
|
||||
PS5_GAME_REQUEST_PATH,
|
||||
PS5_GAME_RESULT_PATH,
|
||||
PS5_GAME_PLAYLISTS,
|
||||
"/app0/content/",
|
||||
"/app0/info/",
|
||||
PS5_FRONTEND_CHOICE_PATH,
|
||||
"/app0/trace.txt",
|
||||
"/app0/retroarch.log",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ps5::frontend_mode
|
||||
{
|
||||
@@ -37,6 +38,8 @@ struct Paths
|
||||
{
|
||||
std::string picker, es_de, test_run, picker_test, eboot;
|
||||
std::string request, result, playlists; /* game mode's (src/ps5_game.h) */
|
||||
std::string content; /* a forwarder's relative --rom is in it */
|
||||
std::string info; /* the cores' .info files (supported_extensions) */
|
||||
std::string choice; /* config/frontend.cfg */
|
||||
std::string trace, retroarch_log, game_log;
|
||||
/* Reads the pad as the title starts: true when L1 is held. Asked only when a
|
||||
@@ -47,6 +50,44 @@ struct Paths
|
||||
/* The mode the process arguments name with --ps5-mode=, from argv[0] on (LoadExec's
|
||||
* arguments are the whole argv), or an empty string. */
|
||||
std::string mode_argument(int argc, char **argv);
|
||||
|
||||
/* A home screen forwarder's launch: a small app with a home screen tile of its own that
|
||||
* starts the title with
|
||||
*
|
||||
* --rom <file> the content: an absolute path, or one inside /app0/content/
|
||||
* --core <core> the core: snes9x, snes9x_libretro.so or /app0/cores/snes9x_libretro.so;
|
||||
* without it, the one RetroArch's playlists associate with the content,
|
||||
* else the one core whose .info lists its extension (or folders)
|
||||
* --exit-after-game when RetroArch quits, the title closes rather than opening as from
|
||||
* the home screen
|
||||
*
|
||||
* (each --x <value> also as --x=<value>). The game runs as a frontend's does in game mode
|
||||
* (src/ps5_game.h): checked, then RetroArch with -L, Close Content quitting it. */
|
||||
struct Forward
|
||||
{
|
||||
std::string rom, core;
|
||||
bool exit_after_game = false;
|
||||
};
|
||||
Forward forward_arguments(int argc, char **argv);
|
||||
/* The content's path: an absolute one as it is; a relative one inside content (a folder
|
||||
* ending in '/'), never above it: empty for a path with a ".." part. A trailing '/' goes. */
|
||||
std::string forward_content(const std::string &rom, const std::string &content);
|
||||
/* The core's path inside cores (a folder ending in '/'), from a name with or without
|
||||
* _libretro.so; a path with a '/' as it is (ps5_game_check decides). Empty for none. */
|
||||
std::string forward_core(const std::string &core, const std::string &cores);
|
||||
/* The cores whose .info file in info (a folder ending in '/') lists the content's extension in
|
||||
* supported_extensions, by name (snes9x), sorted: a forwarder without --core whose content no
|
||||
* playlist names takes the one core that runs it. */
|
||||
std::vector<std::string> forward_extension_cores(const std::string &info,
|
||||
const std::string &content);
|
||||
/* The cores whose .info lists "/" in supported_extensions: they open a folder as content
|
||||
* (DOSBox Pure, PUAE, VICE). A forwarder's --rom may name a folder for them. */
|
||||
std::vector<std::string> forward_folder_cores(const std::string &info);
|
||||
/* Of several such cores, the one a cartridge extension two cores share is meant for (the Mega
|
||||
* Drive and Master System extensions: Genesis Plus GX before PicoDrive); empty when there is
|
||||
* no such choice and --core has to name one. */
|
||||
std::string forward_preferred_core(const std::string &content,
|
||||
const std::vector<std::string> &cores);
|
||||
/* A launch that starts a session: no --ps5- argument at all, as from the home screen
|
||||
* or a test run. Every handover names a mode, and a test's restarts their generation. */
|
||||
bool session_start(int argc, char **argv);
|
||||
|
||||
+30
-4
@@ -14,7 +14,9 @@
|
||||
*
|
||||
* /app0/relaunch-test.txt ("<count> <run id> [image]", written by tools/run-title.sh
|
||||
* --relaunch-test, with --relaunch-image naming another image of the title in /app0)
|
||||
* arms it. Each process the test starts appends one line to
|
||||
* arms it; each line after the first (--relaunch-arg) is one more argument the restarts
|
||||
* carry, as a home screen forwarder's --rom and --core do (src/frontend_mode_ps5.cpp).
|
||||
* Each process the test starts appends one line to
|
||||
* /app0/relaunch-test.jsonl: its generation, which is the number of this run's
|
||||
* lines already in the file (so a restart that lost its arguments still counts,
|
||||
* and cannot loop), the generation its arguments name, argc and argv, the
|
||||
@@ -118,7 +120,9 @@ bool parse_arm(const std::string &text, Arm &arm)
|
||||
unsigned count = 0;
|
||||
char run[48] = {};
|
||||
char image[256] = {};
|
||||
const int fields = std::sscanf(text.c_str(), "%u %47s %255s", &count, run, image);
|
||||
const std::size_t first_end = text.find('\n');
|
||||
const int fields =
|
||||
std::sscanf(text.substr(0, first_end).c_str(), "%u %47s %255s", &count, run, image);
|
||||
if (fields < 2 || count == 0 || count > max_count)
|
||||
return false;
|
||||
for (const char *at = run; *at; at++)
|
||||
@@ -129,9 +133,28 @@ bool parse_arm(const std::string &text, Arm &arm)
|
||||
if (fields == 3 && (std::strncmp(image, "/app0/", 6) != 0 || std::strstr(image, "..") ||
|
||||
std::strpbrk(image, "\"\\")))
|
||||
return false;
|
||||
/* The lines after the first are arguments for the restarts, one a line. */
|
||||
std::vector<std::string> arguments;
|
||||
for (std::size_t at = first_end; at != std::string::npos && at + 1 < text.size();)
|
||||
{
|
||||
const std::size_t end = text.find('\n', at + 1);
|
||||
std::string argument = text.substr(at + 1, end == std::string::npos ? end : end - at - 1);
|
||||
at = end;
|
||||
if (!argument.empty() && argument.back() == '\r')
|
||||
argument.pop_back();
|
||||
if (argument.empty())
|
||||
continue;
|
||||
if (arguments.size() == 8 || argument.size() > 255)
|
||||
return false;
|
||||
for (const char c : argument)
|
||||
if (static_cast<unsigned char>(c) < 0x20)
|
||||
return false;
|
||||
arguments.push_back(argument);
|
||||
}
|
||||
arm.count = count;
|
||||
arm.run = run;
|
||||
arm.image = fields == 3 ? image : "";
|
||||
arm.arguments = arguments;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -240,11 +263,14 @@ bool run_test(const Paths &paths, int argc, char **argv, unsigned replaced_wait_
|
||||
}
|
||||
|
||||
const std::string next = std::string(token) + std::to_string(current + 1);
|
||||
const char *const arguments[] = {next.c_str(), nullptr};
|
||||
std::vector<const char *> arguments{next.c_str()};
|
||||
for (const std::string &argument : arm.arguments)
|
||||
arguments.push_back(argument.c_str());
|
||||
arguments.push_back(nullptr);
|
||||
std::fflush(nullptr);
|
||||
const std::string &image = arm.image.empty() ? paths.image : arm.image;
|
||||
ps5_permissions_settle();
|
||||
const int result = sceSystemServiceLoadExec(image.c_str(), arguments);
|
||||
const int result = sceSystemServiceLoadExec(image.c_str(), arguments.data());
|
||||
std::string event = line_start(arm.run, current) +
|
||||
",\"event\":\"loadexec\",\"result\":" + std::to_string(result);
|
||||
if (result >= 0)
|
||||
|
||||
+4
-1
@@ -8,17 +8,20 @@
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ps5::relaunch
|
||||
{
|
||||
/* "<count> <run id> [image]": restart the title <count> times (1 to 20) under one
|
||||
* run id (letters, digits, '-' and '_'), through the image named, a path in /app0,
|
||||
* or the title's own eboot.bin when none is. */
|
||||
* or the title's own eboot.bin when none is. Each line after the first is one more
|
||||
* argument for the restarts (at most 8, printable, under 256 bytes). */
|
||||
struct Arm
|
||||
{
|
||||
unsigned count = 0;
|
||||
std::string run;
|
||||
std::string image;
|
||||
std::vector<std::string> arguments;
|
||||
};
|
||||
|
||||
struct Paths
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* directory for the files the decision reads. */
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
@@ -184,19 +185,12 @@ int main(int argc, char **argv)
|
||||
}
|
||||
|
||||
/* The launches, against files in the scratch directory. */
|
||||
const ps5::frontend_mode::Paths paths{dir + "/picker.bin",
|
||||
dir + "/es-de.bin",
|
||||
dir + "/test-run.txt",
|
||||
dir + "/picker-test.txt",
|
||||
dir + "/eboot.bin",
|
||||
dir + "/game-request.txt",
|
||||
dir + "/game-result.txt",
|
||||
dir + "/playlists",
|
||||
dir + "/frontend.cfg",
|
||||
dir + "/trace.txt",
|
||||
dir + "/retroarch.log",
|
||||
dir + "/retroarch-game.log",
|
||||
read_l1};
|
||||
const ps5::frontend_mode::Paths paths{
|
||||
dir + "/picker.bin", dir + "/es-de.bin", dir + "/test-run.txt",
|
||||
dir + "/picker-test.txt", dir + "/eboot.bin", dir + "/game-request.txt",
|
||||
dir + "/game-result.txt", dir + "/playlists", dir + "/content/",
|
||||
dir + "/info/", dir + "/frontend.cfg", dir + "/trace.txt",
|
||||
dir + "/retroarch.log", dir + "/retroarch-game.log", read_l1};
|
||||
touch(paths.picker, true);
|
||||
touch(paths.es_de, true);
|
||||
touch(paths.test_run, false);
|
||||
@@ -413,8 +407,173 @@ int main(int argc, char **argv)
|
||||
!ps5::frontend_mode::running_game());
|
||||
assert(marks.back() == "game mode: no request; RetroArch runs");
|
||||
|
||||
/* A home screen forwarder: --rom and --core, run as a game; afterwards the title as from
|
||||
* the home screen, or closed with --exit-after-game. */
|
||||
{
|
||||
using ps5::frontend_mode::forward_arguments;
|
||||
using ps5::frontend_mode::forward_content;
|
||||
using ps5::frontend_mode::forward_core;
|
||||
char rom[] = "--rom", rom_value[] = "snes/Game (USA).zip", core_flag[] = "--core=snes9x",
|
||||
exit_flag[] = "--exit-after-game", dangling[] = "--core";
|
||||
char *full[] = {rom, rom_value, core_flag, exit_flag, nullptr};
|
||||
const ps5::frontend_mode::Forward parsed = forward_arguments(4, full);
|
||||
assert(parsed.rom == "snes/Game (USA).zip" && parsed.core == "snes9x" &&
|
||||
parsed.exit_after_game);
|
||||
char *missing[] = {dangling, nullptr};
|
||||
assert(forward_arguments(1, missing).core.empty() &&
|
||||
forward_arguments(0, nullptr).rom.empty());
|
||||
assert(forward_content("snes/Game.zip", "/app0/content/") == "/app0/content/snes/Game.zip");
|
||||
assert(forward_content("/mnt/usb0/Game.zip\r\n", "/c/") == "/mnt/usb0/Game.zip");
|
||||
assert(forward_content("../Game.zip", "/c/").empty() &&
|
||||
forward_content("a/../../x", "/c/").empty());
|
||||
assert(forward_content("a/..b.zip", "/c/") == "/c/a/..b.zip" &&
|
||||
forward_content("", "/c/").empty());
|
||||
for (const char *name : {"snes9x", "snes9x_libretro", "snes9x_libretro.so"})
|
||||
assert(forward_core(name, "/app0/cores/") == "/app0/cores/snes9x_libretro.so");
|
||||
assert(forward_core("/app0/cores/mgba_libretro.so", "/x/") ==
|
||||
"/app0/cores/mgba_libretro.so");
|
||||
assert(forward_core("", "/app0/cores/").empty() &&
|
||||
forward_core("_libretro.so", "/c/").empty());
|
||||
}
|
||||
mkdir((dir + "/content").c_str(), 0777);
|
||||
mkdir((dir + "/content/snes").c_str(), 0777);
|
||||
const std::string forwarded = dir + "/content/snes/Game.zip";
|
||||
touch(forwarded, true);
|
||||
touch(paths.eboot, true); /* the title, which game mode's check takes as the frontend */
|
||||
touch(paths.picker, true);
|
||||
std::remove(paths.choice.c_str());
|
||||
exec_mode = Exec::replace;
|
||||
/* --core named: the game, with that core; the playlists do not override it */
|
||||
assert(launch(paths, {"--rom", "snes/Game.zip", "--core", "snes9x"}) == "retroarch");
|
||||
const struct ps5_game *forward_game = ps5::frontend_mode::running_game();
|
||||
assert(forward_game && forwarded == forward_game->content && core == forward_game->core);
|
||||
assert(marks.back() == "forwarder: " + forwarded + " with " + core);
|
||||
assert(ps5::frontend_mode::retroarch_log(paths) == paths.game_log);
|
||||
/* RetroArch quit: the title as from the home screen, with no result for a frontend */
|
||||
std::remove(paths.result.c_str());
|
||||
assert(quit("") == paths.eboot && exec_argument.empty());
|
||||
assert(!ps5::frontend_mode::exists(paths.result) && !ps5::frontend_mode::running_game());
|
||||
/* --exit-after-game: the title closes (catchReturnFromMain's LoadExec("exit")) */
|
||||
{
|
||||
const std::string absolute = "--rom=" + forwarded;
|
||||
assert(launch(paths, {absolute.c_str(), "--core=snes9x_libretro.so",
|
||||
"--exit-after-game"}) == "retroarch");
|
||||
assert(ps5::frontend_mode::running_game() && quit("") == "closes");
|
||||
assert(marks.back().find("the title closes") != std::string::npos);
|
||||
}
|
||||
/* no --core: RetroArch's playlists choose, as for a frontend's game */
|
||||
playlist = std::fopen((paths.playlists + "/SNES.lpl").c_str(), "w");
|
||||
assert(playlist);
|
||||
std::fprintf(playlist, "{\"items\":[{\"path\":\"%s\",\"core_path\":\"%s\"}]}",
|
||||
forwarded.c_str(), other.c_str());
|
||||
std::fclose(playlist);
|
||||
assert(launch(paths, {"--rom", "snes/Game.zip"}) == "retroarch" &&
|
||||
other == ps5::frontend_mode::running_game()->core);
|
||||
assert(quit("") == paths.eboot);
|
||||
std::remove((paths.playlists + "/SNES.lpl").c_str());
|
||||
/* not runnable: the trace says why, and the launch goes on as from the home screen */
|
||||
assert(launch(paths, {"--rom", "snes/Game.zip"}) == paths.picker &&
|
||||
!ps5::frontend_mode::running_game());
|
||||
assert(std::find_if(marks.begin(), marks.end(), [](const std::string &m)
|
||||
{ return m.find("no core: neither --core") != std::string::npos; }) !=
|
||||
marks.end());
|
||||
/* nor a playlist: the core whose .info lists the extension (a hack no playlist has) */
|
||||
mkdir(paths.info.c_str(), 0777);
|
||||
write_file(paths.info + "snes9x_libretro.info",
|
||||
"display_name = \"Snes9x\"\nsupported_extensions = \"smc|SFC|swc\"\n");
|
||||
write_file(paths.info + "bsnes_libretro.info", "supported_extensions = \"sfc|bs\"\n");
|
||||
write_file(paths.info + "genesis_plus_gx_libretro.info",
|
||||
" supported_extensions = \"md|gen|cue\"\n");
|
||||
write_file(paths.info + "picodrive_libretro.info", "supported_extensions = \"md|32x|cue\"\n");
|
||||
write_file(paths.info + "notes.txt", "supported_extensions = \"sfc\"\n");
|
||||
const std::string hack = dir + "/content/snes/Hack (patched).SFC";
|
||||
touch(hack, true);
|
||||
assert((ps5::frontend_mode::forward_extension_cores(paths.info, hack) ==
|
||||
std::vector<std::string>{"bsnes", "snes9x"}));
|
||||
assert(ps5::frontend_mode::forward_extension_cores(paths.info, dir + "/content/snes/x.smc") ==
|
||||
std::vector<std::string>{"snes9x"});
|
||||
assert(ps5::frontend_mode::forward_extension_cores(paths.info, dir + "/content/a.b/noext")
|
||||
.empty());
|
||||
assert(ps5::frontend_mode::forward_extension_cores(paths.info, dir + "/content/x.sf").empty());
|
||||
assert(ps5::frontend_mode::forward_preferred_core(
|
||||
"/x/Racer.md", {"genesis_plus_gx", "picodrive"}) == "genesis_plus_gx");
|
||||
assert(
|
||||
ps5::frontend_mode::forward_preferred_core("/x/Game.cue", {"genesis_plus_gx", "picodrive"})
|
||||
.empty());
|
||||
/* bsnes lists .sfc but is not one of the title's cores (no library): snes9x runs it */
|
||||
std::remove(other.c_str());
|
||||
assert(launch(paths, {"--rom", "snes/Hack (patched).SFC"}) == "retroarch" &&
|
||||
core == ps5::frontend_mode::running_game()->core);
|
||||
assert(quit("") == paths.eboot);
|
||||
/* both are cores of the title: --core has to choose, and the trace names them */
|
||||
touch(other, true);
|
||||
assert(launch(paths, {"--rom", "snes/Hack (patched).SFC"}) == paths.picker &&
|
||||
!ps5::frontend_mode::running_game());
|
||||
assert(std::find_if(marks.begin(), marks.end(),
|
||||
[](const std::string &m)
|
||||
{
|
||||
return m.find("several cores list its extension (bsnes, snes9x)") !=
|
||||
std::string::npos;
|
||||
}) != marks.end());
|
||||
/* a Mega Drive cartridge both Sega cores list: Genesis Plus GX */
|
||||
const std::string genesis = std::string(PS5_GAME_CORES) + "genesis_plus_gx_libretro.so";
|
||||
touch(genesis, true);
|
||||
touch(std::string(PS5_GAME_CORES) + "picodrive_libretro.so", true);
|
||||
touch(dir + "/content/Racer.md", true);
|
||||
assert(launch(paths, {"--rom", "Racer.md"}) == "retroarch" &&
|
||||
genesis == ps5::frontend_mode::running_game()->core);
|
||||
assert(quit("") == paths.eboot);
|
||||
/* a folder, for a core that opens folders (its .info lists "/"): DOSBox Pure's DOS or
|
||||
* Windows installation */
|
||||
write_file(paths.info + "dosbox_pure_libretro.info",
|
||||
"supported_extensions = \"zip|dosz|exe|/\"\n");
|
||||
const std::string dosbox = std::string(PS5_GAME_CORES) + "dosbox_pure_libretro.so";
|
||||
touch(dosbox, true);
|
||||
mkdir((dir + "/content/dos").c_str(), 0777);
|
||||
mkdir((dir + "/content/dos/w95").c_str(), 0777);
|
||||
assert(ps5::frontend_mode::forward_folder_cores(paths.info) ==
|
||||
std::vector<std::string>{"dosbox_pure"});
|
||||
assert(ps5::frontend_mode::forward_content("dos/w95/", "/c/") == "/c/dos/w95");
|
||||
{
|
||||
const std::string w95 = "--rom=" + dir + "/content/dos/w95";
|
||||
assert(launch(paths, {w95.c_str(), "--core", "dosbox_pure_libretro",
|
||||
"--exit-after-game"}) == "retroarch" &&
|
||||
dosbox == ps5::frontend_mode::running_game()->core &&
|
||||
dir + "/content/dos/w95" == ps5::frontend_mode::running_game()->content);
|
||||
assert(quit("") == "closes");
|
||||
}
|
||||
/* without --core: the one core that opens folders */
|
||||
assert(launch(paths, {"--rom", "dos/w95/"}) == "retroarch" &&
|
||||
dosbox == ps5::frontend_mode::running_game()->core);
|
||||
assert(quit("") == paths.eboot);
|
||||
/* a core that does not open folders is refused, naming those that do */
|
||||
assert(launch(paths, {"--rom", "dos/w95", "--core", "snes9x"}) == paths.picker &&
|
||||
!ps5::frontend_mode::running_game());
|
||||
const auto marked = [&](const char *text)
|
||||
{
|
||||
return std::find_if(marks.begin(), marks.end(), [&](const std::string &m)
|
||||
{ return m.find(text) != std::string::npos; }) != marks.end();
|
||||
};
|
||||
assert(
|
||||
marked("snes9x does not open folders (its .info lists no \"/\"; these do: dosbox_pure)"));
|
||||
/* a folder that is not there is still "not a file" */
|
||||
assert(launch(paths, {"--rom", "dos/w98", "--core", "dosbox_pure"}) == paths.picker &&
|
||||
marked("--rom dos/w98 --core dosbox_pure not run: the content is not a file"));
|
||||
for (const std::vector<const char *> &args :
|
||||
std::vector<std::vector<const char *>>{{"--rom", "snes/Missing.zip", "--core", "snes9x"},
|
||||
{"--rom", "../Game.zip", "--core", "snes9x"},
|
||||
{"--rom", "snes/Game.zip", "--core", "nosuchcore"}})
|
||||
assert(launch(paths, args) == paths.picker && !ps5::frontend_mode::running_game());
|
||||
/* a handover's mode wins over --rom */
|
||||
assert(launch(paths, {"--ps5-mode=es-de", "--rom", "snes/Game.zip", "--core", "snes9x"}) ==
|
||||
paths.es_de &&
|
||||
!ps5::frontend_mode::running_game());
|
||||
|
||||
std::puts(
|
||||
"frontend_mode_ps5: decisions, mode argument, LoadExec targets, test runs and "
|
||||
"frontend_mode_ps5: forwarder --rom/--core launches, cores by extension, folders, refusals "
|
||||
"and "
|
||||
"after-game PASS; "
|
||||
"decisions, mode argument, LoadExec targets, test runs and "
|
||||
"refused or ignored LoadExec PASS; the remembered frontend, L1, quit and closing PASS; "
|
||||
"session and per-mode logs PASS; back to the picker after RetroArch, no handover after "
|
||||
"an update; game mode requests, cores, refusals and results PASS");
|
||||
|
||||
@@ -24,6 +24,7 @@ enum class Exec
|
||||
Exec exec_mode = Exec::replace;
|
||||
int exec_calls = 0;
|
||||
std::string exec_path, exec_argument;
|
||||
std::vector<std::string> exec_arguments;
|
||||
std::vector<std::string> marks;
|
||||
|
||||
std::string slurp(const std::string &path)
|
||||
@@ -79,7 +80,10 @@ extern "C" int sceSystemServiceLoadExec(const char *path, const char *const *arg
|
||||
exec_calls++;
|
||||
exec_path = path;
|
||||
exec_argument = argv && argv[0] ? argv[0] : "";
|
||||
assert(argv && argv[0] && !argv[1]);
|
||||
assert(argv && argv[0]);
|
||||
exec_arguments.assign(argv, argv + 1);
|
||||
for (const char *const *at = argv + 1; *at; at++)
|
||||
exec_arguments.emplace_back(*at);
|
||||
if (exec_mode == Exec::replace)
|
||||
throw Replaced{};
|
||||
return exec_mode == Exec::refuse ? static_cast<int>(0x80020002u) : 0;
|
||||
@@ -128,7 +132,7 @@ int main(int argc, char **argv)
|
||||
arm(paths, "3 chain-1\n");
|
||||
assert(launch(paths, {""}, continued));
|
||||
assert(exec_calls == 1 && exec_path == "/app0/eboot.bin" &&
|
||||
exec_argument == "--ps5-relaunch=1");
|
||||
exec_argument == "--ps5-relaunch=1" && exec_arguments.size() == 1);
|
||||
assert(launch(paths, {"--ps5-relaunch=1"}, continued) && exec_argument == "--ps5-relaunch=2");
|
||||
// A restart whose arguments were lost still counts from the file; the last
|
||||
// one finds its token after another argument.
|
||||
@@ -193,6 +197,23 @@ int main(int argc, char **argv)
|
||||
std::string::npos);
|
||||
assert(!launch(paths, {"--ps5-relaunch=1"}, continued) && !continued);
|
||||
|
||||
// Arguments for the restarts, one a line after the first: each restart carries them
|
||||
// after its generation (a home screen forwarder's here). A bad line arms nothing.
|
||||
arm(paths, "1 forward\n--rom\r\nsnes/Game (USA).zip\n\n--core\nsnes9x\n");
|
||||
assert(launch(paths, {""}, continued));
|
||||
assert((exec_arguments == std::vector<std::string>{"--ps5-relaunch=1", "--rom",
|
||||
"snes/Game (USA).zip", "--core", "snes9x"}));
|
||||
assert(!launch(paths, {"--ps5-relaunch=1", "--rom", "snes/Game (USA).zip", "--core", "snes9x"},
|
||||
continued) &&
|
||||
!continued && !exists(paths.arm));
|
||||
const int before_bad_arguments = exec_calls;
|
||||
for (const char *bad : {"1 r\nok\nbad\ttab", "1 r\n1\n2\n3\n4\n5\n6\n7\n8\n9"})
|
||||
{
|
||||
arm(paths, bad);
|
||||
assert(!launch(paths, {""}, continued) && !continued && !exists(paths.arm));
|
||||
}
|
||||
assert(exec_calls == before_bad_arguments);
|
||||
|
||||
// A record that cannot be written never restarts.
|
||||
const int calls = exec_calls;
|
||||
ps5::relaunch::Paths unwritable = paths;
|
||||
|
||||
+13
-3
@@ -10,6 +10,9 @@
|
||||
# tools/run-title.sh --relaunch-test=5 --watch 60 restart the title 5 times, then RetroArch
|
||||
# tools/run-title.sh --relaunch-test=1 --relaunch-image=probe/eboot-copy.bin --watch 45
|
||||
# restart through a copy of eboot.bin at /app0/probe/...
|
||||
# tools/run-title.sh --relaunch-test=1 --relaunch-arg=--rom --relaunch-arg=snes/Game.zip \
|
||||
# --relaunch-arg=--core --relaunch-arg=snes9x --watch 45
|
||||
# restart with a home screen forwarder's arguments
|
||||
# tools/run-title.sh --display-modes-test=180 --watch 60 every display size, then RetroArch
|
||||
# tools/run-title.sh --relaunch-test=1 --relaunch-image=es-de/es-de.bin --frontend-capture=20,40 --watch 60
|
||||
# hand over to EmulationStation, picture its frames at 20 and 40 s
|
||||
@@ -81,6 +84,7 @@ core_test=none
|
||||
relaunch_test=0
|
||||
display_modes_test=0
|
||||
relaunch_image=
|
||||
relaunch_args=
|
||||
frontend_capture=
|
||||
frontend_scroll=
|
||||
picker_test=
|
||||
@@ -103,6 +107,7 @@ while (( $# )); do
|
||||
--relaunch-test) relaunch_test=5 ;;
|
||||
--relaunch-test=*) relaunch_test=${1#*=} ;;
|
||||
--relaunch-image=*) relaunch_image=${1#*=} ;;
|
||||
--relaunch-arg=*) relaunch_args+="${1#*=}"$'\n' ;;
|
||||
--display-modes-test) display_modes_test=180 ;;
|
||||
--display-modes-test=*) display_modes_test=${1#*=} ;;
|
||||
--frontend-capture=*) frontend_capture=${1#*=} ;;
|
||||
@@ -119,7 +124,7 @@ while (( $# )); do
|
||||
--menu-capture=*) menu_capture=${1#*=} ;;
|
||||
--home-launch) home_launch=1 ;;
|
||||
--retroarch-arg=*) retroarch_args+="${1#*=}"$'\n' ;;
|
||||
*) echo "usage: ${0##*/} [--no-build] [--no-deploy] [--watch SECONDS] [--gpu-profile 1..60] [--audio-test] [--core-test[=fceumm|mgba|snes9x|fbneo|genesis_plus_gx|ppsspp]] [--relaunch-test[=1..20] [--relaunch-image=PATH]] [--display-modes-test[=frames]] [--frontend-capture=SECONDS[,SECONDS...] [--frontend-scroll] [--frontend-profile] [--frontend-quit] [--frontend-launch]] [--picker-test=FRAMES:retroarch|es-de|none] [--retroarch-frames=N] [--game=CORE:PATH] [--pad-script=FILE] [--pad-monitor] [--menu-capture=FRAMES] [--home-launch] [--retroarch-arg=ARG ...]" >&2; exit 2 ;;
|
||||
*) echo "usage: ${0##*/} [--no-build] [--no-deploy] [--watch SECONDS] [--gpu-profile 1..60] [--audio-test] [--core-test[=fceumm|mgba|snes9x|fbneo|genesis_plus_gx|ppsspp]] [--relaunch-test[=1..20] [--relaunch-image=PATH] [--relaunch-arg=ARG ...]] [--display-modes-test[=frames]] [--frontend-capture=SECONDS[,SECONDS...] [--frontend-scroll] [--frontend-profile] [--frontend-quit] [--frontend-launch]] [--picker-test=FRAMES:retroarch|es-de|none] [--retroarch-frames=N] [--game=CORE:PATH] [--pad-script=FILE] [--pad-monitor] [--menu-capture=FRAMES] [--home-launch] [--retroarch-arg=ARG ...]" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
@@ -140,6 +145,9 @@ if (( relaunch_test && watch < 30 )); then
|
||||
exit 2
|
||||
fi
|
||||
relaunch_run=relaunch-$(date +%Y%m%d-%H%M%S)
|
||||
# Arguments the restarts carry, one a line after the arm line (a forwarder's --rom and --core).
|
||||
[[ -z $relaunch_args ]] || (( relaunch_test )) ||
|
||||
{ echo "--relaunch-arg needs --relaunch-test" >&2; exit 2; }
|
||||
# Another image of the title, a path under /app0: a copy of this build's eboot.bin.
|
||||
if [[ -n $relaunch_image ]]; then
|
||||
(( relaunch_test )) || { echo "--relaunch-image needs --relaunch-test" >&2; exit 2; }
|
||||
@@ -294,7 +302,7 @@ fi
|
||||
# appeared. A run that does not ask for extras must not inherit them, so the file
|
||||
# is removed on every run - before the launch, because deleting it afterwards
|
||||
# would leave it for the next one if this run dies.
|
||||
python3 - "$title_id" "$profile" "$audio_test" "$core_test" "$relaunch_test" "$relaunch_run" "$display_modes_test" "$relaunch_image" "$frontend_capture" "$capture_run" "$frontend_scroll" "$picker_test" "$retroarch_frames" "$pad_script" "$pad_monitor" "$home_launch" "$game" "$menu_capture" "$retroarch_args" <<'PY'
|
||||
python3 - "$title_id" "$profile" "$audio_test" "$core_test" "$relaunch_test" "$relaunch_run" "$display_modes_test" "$relaunch_image" "$frontend_capture" "$capture_run" "$frontend_scroll" "$picker_test" "$retroarch_frames" "$pad_script" "$pad_monitor" "$home_launch" "$game" "$menu_capture" "$retroarch_args" "$relaunch_args" <<'PY'
|
||||
import importlib.util, sys
|
||||
spec = importlib.util.spec_from_file_location("dt", "tools/deploy-title.py")
|
||||
dt = importlib.util.module_from_spec(spec); spec.loader.exec_module(dt)
|
||||
@@ -339,8 +347,10 @@ with connect(**dt.load_settings()) as ftp:
|
||||
else:
|
||||
upload_atomic(ftp, Path(f"dist/{sys.argv[1]}/eboot.bin"), f"/data/homebrew/{sys.argv[1]}/{sys.argv[8]}")
|
||||
print(f" uploaded a copy of this build's eboot.bin to /app0/{sys.argv[8]}")
|
||||
ftp.storbinary(f"STOR {control}", io.BytesIO(f"{sys.argv[5]} {sys.argv[6]}{image}\n".encode()))
|
||||
ftp.storbinary(f"STOR {control}", io.BytesIO(f"{sys.argv[5]} {sys.argv[6]}{image}\n{sys.argv[20]}".encode()))
|
||||
print(f" armed relaunch test: {sys.argv[5]} restarts, run {sys.argv[6]}{image}")
|
||||
if sys.argv[20]:
|
||||
print(f" each restart carries: {sys.argv[20].splitlines()}")
|
||||
control = f"/data/homebrew/{sys.argv[1]}/display-modes-test.txt"
|
||||
remove_if_present(ftp, control)
|
||||
if int(sys.argv[7]):
|
||||
|
||||
Reference in new issue
Block a user