1 Commits
Author SHA1 Message Date
Gary Riches 30ff84d01a 0.9.0: save states, a faster map, and a builder kit without Docker
- sms-pc-port moved to c8fb4a3: its save states (L1 + R1) and its one-copy
  map wipe. On the PS5 the game's global ranges come from lld's map in
  link-title.sh and are written at link addresses the game slides by
  (savestate_ranges.py --pie), with tests for lld maps
- The kit builds on macOS (Apple silicon), Linux and WSL: tools.sh finds LLVM
  22, CMake and Ninja or fetches them once, checked; Linux takes LLVM from
  apt.llvm.org; ps5-native-tool ships prebuilt for Linux; the scripts run on
  bash 3.2; the zip holds no symlinks. --docker keeps the container route
- make-texture-pack.sh needs no Docker either (fetch-wheels.py)

Claude-Session: https://claude.ai/code/session_017rd8WaKMMLthMzLqTiaQGA
2026-10-14 09:28:56 +01:00
14 changed files with 953 additions and 138 deletions

No files matched your search

+1
View File
@@ -16,3 +16,4 @@ rom/
*.szs
# never: texture packs (they belong to their authors)
mods/
__pycache__/
+10 -1
View File
@@ -27,7 +27,16 @@ project, under that project's licence.
| Windowing, input and sound | [SDL2](https://github.com/libsdl-org/SDL) | Zlib |
| The link recipe, the C runtime start-up and the fake-SELF tool | [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan) | GPL-3.0 |
| The platform layer and SDK | [PS5_PayloadSDK](https://github.com/mihawk-99/PS5_PayloadSDK), a fork of ps5-payload-dev/sdk | GPL-3.0 |
| Smaller libraries | stb, bcdec, xxHash | each its own, all permissive |
| Smaller libraries | stb, bcdec, xxHash; zlib, in the kit's Linux build of the `eboot.bin` tool | each its own, all permissive |
The builder kit fetches its compiler and build tools the first time it runs,
from their projects' own releases: [LLVM](https://github.com/llvm/llvm-project)
(Apache-2.0 with LLVM exceptions), [CMake](https://github.com/Kitware/CMake)
(BSD-3-Clause) and [Ninja](https://github.com/ninja-build/ninja) (Apache-2.0),
and for the optional texture pack [Pillow](https://github.com/python-pillow/Pillow)
(MIT-CMU), [etcpak](https://github.com/K0lb3/etcpak) (MIT and BSD-3-Clause),
[archspec](https://github.com/archspec/archspec) (Apache-2.0 or MIT) and, on
Linux, [7-Zip](https://github.com/ip7z/7zip) (LGPL-2.1, with its unRAR restriction). None is in the kit.
## A built title is not for distribution
+29 -9
View File
@@ -14,7 +14,7 @@ the native PC port of Super Mario Sunshine by **[chasem-dev](https://github.com/
> platform, sound, input and graphics layers, 60 and 120 frames a second,
> widescreen, texture packs and a great deal more, built on the
> [decompilation](https://github.com/chasem-dev/sms-english) it carries. This
> repository adds only what a PS5 needs on top of it: a patch of about 900
> repository adds only what a PS5 needs on top of it: a patch of about 1,000
> lines, the console's drivers and a build recipe. If you enjoy this, the
> thanks belong there. **Please star and support
> [chasem-dev/sms-pc-port](https://github.com/chasem-dev/sms-pc-port).**
@@ -31,7 +31,7 @@ This is an unofficial port. It is not part of sms-pc-port and is not supported
by it: **please do not take PS5 problems to chasem-dev.** Report them
[here](https://github.com/griches/PS5_Sunshine/issues).
## This is an early release: 0.8.0
## This is an early release: 0.9.0
**It is not fully tested.** It was brought up and played on one console over a
single day (2026-10-13), through the opening, Delfino Plaza and the first
@@ -39,28 +39,37 @@ episode of Bianco Hills. Everything after that is unplayed on a PS5. Expect
faults; several serious ones were found and fixed only by playing, and there
will be more further into the game. Keep a copy of your save.
New in 0.9.0: sms-pc-port's save states (L1 + R1) and its faster wipe into the
map, from its revision of 2026-10-14, and a builder kit that no longer needs
Docker.
## How to get it: the builder kit
A built title holds the game's own code, so none is offered here. The
[release](https://github.com/griches/PS5_Sunshine/releases/latest) is a
**builder kit**: everything of a build except that code. On your own computer
it downloads the public source code, compiles it and links the title. One
command, about 5 to 20 minutes, with Docker.
command, about 5 to 20 minutes, on a Mac with Apple silicon, on Linux, or on
Windows in WSL. No Docker: on Ubuntu and WSL the compiler, LLVM 22, comes from
LLVM's own packages (apt.llvm.org); on a Mac it and the build tools are fetched
from their own releases the first time, checked, and kept for next time.
1. Download `sunshine-ps5-kit-<version>.zip` from the
[releases page](https://github.com/griches/PS5_Sunshine/releases/latest) and unzip it.
2. Start Docker Desktop, open a terminal in the kit's folder and run `./make-title.sh`.
2. Get your computer ready once (a Mac: Apple's Command Line Tools; Windows: WSL
and LLVM 22; the kit's README has the commands), then open a terminal in the
kit's folder and run `./make-title.sh`.
3. Copy the folder it makes, `out/PPSA99312`, to `/data/homebrew/` on the console (FTP).
4. Copy your own disc image (`.iso`, `.gcm` or `.ciso`) into that folder, beside `eboot.bin`.
The kit's own [README](ps5/kit/README.md) has every step, the controls, the
settings menu, Windows commands and what to do when something goes wrong.
settings menu, the steps for Windows and what to do when something goes wrong.
**Never share the folder the kit makes, or its `eboot.bin`.**
## Status
Tested on one console (PS5, firmware 13.60) on 2026-10-13, with the North
Tested on one console (PS5, firmware 13.60) on 2026-10-13 and 2026-10-14, with the North
American disc (`GMSE01`, revision 0). "Seen" means it ran there and was
watched by a person playing, looked at in the console's own screenshots, or
read in the title's log. Nothing else is claimed.
@@ -74,9 +83,12 @@ read in the title's log. Nothing else is claimed.
| Input | Walking, jumping, the camera, FLUDD, menus, the DualSense's own button prompts | Every button in play; rumble; a second controller |
| Settings | The touch pad's menu: inverted FLUDD aim and camera, camera shake, the performance overlay, resolution, frame rate; choices kept across a restart | Every row; the cheats below them |
| Saving | A save made earlier loads | Saving in play, checked after a restart |
| Save states | Saving with L1 + R1 in a level, the pause menu's list with pictures, loading a state, and loading after a restart | States across many levels; a state from another build (loads the position only, by design) |
| The map (R1) | The wipe into it, now quick | |
| Sound | Music, effects and the movies' sound play | A careful listen for faults |
| Texture packs | The UHD pack and the HD extras, sized for the console (see below) | Other packs |
| Memory | Two levels and back without running out, with the console-sized pack | Many levels in one session |
| Builder kit | Built without Docker on a Mac (Apple silicon, macOS's own tools) and on Linux (arm64), and the Mac's title played on the console. On x86-64 Linux: LLVM's build fetched and run, a game file compiled identically, `eboot.bin` made byte for byte the same | A whole build on Windows (WSL) or x86-64 Linux; the `--docker` route |
### Known problems
@@ -89,8 +101,11 @@ read in the title's log. Nothing else is claimed.
- **Camera shake is off to begin with.** The game's earthquakes (a boss
landing, a building rising) are strong on a large screen. Turn them back on
in the settings menu if you want the original feel.
- **No save states.** sms-pc-port has since gained them; this port is built on
the revision before, and has not tried them.
- **Save states belong to the build that saved them.** L1 + R1 saves one in a
level, and loads one from the pause menu (sms-pc-port's own). A state loads
exactly only in the build that saved it: after a rebuild it puts Mario back
where he was and keeps the progress, but the level starts afresh. Keep a
copy of your memory card save as well.
## What the console needed
@@ -116,6 +131,11 @@ For the curious, and for anyone porting something similar. All of it is in
- **Threads.** The game's video decoder runs below its main thread and was
starved whenever a frame had no time to spare; the main thread now gives the
others a turn.
- **Save states.** sms-pc-port finds the game's global variables for a state
in the link map after linking. The console's title is linked by lld and
loaded at an address of its own choosing, so the map is read with lld's
layout and the ranges are written at link addresses, which the game slides
by where it was loaded.
- **The console's own menu.** The touch pad's button opens settings a PS5 has
no keyboard or launcher for.
@@ -129,7 +149,7 @@ tools; no Vulkan driver is linked) and the PS5 OpenGL SDK 1.0.2.
```sh
git clone https://github.com/griches/PS5_Sunshine.git && cd PS5_Sunshine
git clone https://github.com/chasem-dev/sms-pc-port.git vendor/sms-pc-port
git -C vendor/sms-pc-port checkout f3590911967886041f41586596b35ed124e8a43f # see ps5/kit: REVISIONS
git -C vendor/sms-pc-port checkout c8fb4a3e81a2176588574395302ecf862f9a440a # see ps5/kit: REVISIONS
git -C vendor/sms-pc-port submodule update --init decomp
export PS5_VULKAN_DIR=/path/to/PS5_Vulkan PS5_OPENGL_PREFIX=/path/to/ps5-opengl-sdk/sdk
ps5/tools/build.sh # applies the patch, builds dist/PPSA99312/
+2 -1
View File
@@ -79,8 +79,9 @@ set(SMS_GX_BUILD_TESTS OFF CACHE BOOL "" FORCE)
# The link: every object and archive CMake would hand the linker goes to the
# console's recipe instead (the platform layer, the CRT, OpenGL, the fake SELF)
# (and the port's save state tool, which writes the game's data ranges into the linked ELF)
set(CMAKE_CXX_LINK_EXECUTABLE
"bash ${ps5}/tools/link-title.sh ${CMAKE_BINARY_DIR} ${PS5_VULKAN_DIR} ${PS5_PAYLOAD_SDK} --whole-archive ${ps5_platform_archive} --no-whole-archive <LINK_FLAGS> <OBJECTS> <LINK_LIBRARIES>")
"env SMS_SAVESTATE_RANGES=${CMAKE_SOURCE_DIR}/tools/savestate_ranges.py bash ${ps5}/tools/link-title.sh ${CMAKE_BINARY_DIR} ${PS5_VULKAN_DIR} ${PS5_PAYLOAD_SDK} --whole-archive ${ps5_platform_archive} --no-whole-archive <LINK_FLAGS> <OBJECTS> <LINK_LIBRARIES>")
# (the start-up constructor in the platform archive has no caller: hence whole)
# The platform archive is named by its file in the link, so the game does not
+116 -40
View File
@@ -5,6 +5,8 @@ your own computer, for a PS5 that has homebrew enabled. It plays **your own
copy** of the game, from your own disc image, at 4K and 60 frames a second.
You run one command. Your computer does the rest in about 5 to 20 minutes.
Nothing needs installing first on a Mac or Linux beyond what the steps below
say; on Windows it runs in WSL.
> **There is no game in this kit.** No game code, no game data, nothing of
> Nintendo's. The build fetches the public source code it needs and compiles
@@ -27,12 +29,15 @@ You need three things.
revision 0), as `.iso`, `.gcm` or Dolphin's `.ciso`. Other regions are not
supported.
**3. A computer** (Windows, macOS or Linux) with:
**3. A computer**, one of:
- [Docker Desktop](https://www.docker.com/products/docker-desktop/), installed and running
- an internet connection (the build downloads a compiler and the source code)
- about 4 GB of free disk space
- 8 GB of memory or more
- a Mac with Apple silicon (M1 or later), macOS 13 or later
- Windows 10 or 11, with WSL (Windows' own Linux, installed in step 2)
- Linux on x86_64 or arm64 (Ubuntu, Debian, Fedora, ...)
with an internet connection, about 6 GB of free disk space and 8 GB of memory
or more. (An Intel Mac works too if you install LLVM 22 yourself: see
*If something goes wrong*.)
---
@@ -41,34 +46,73 @@ supported.
Unzip `sunshine-ps5-kit-<version>.zip` anywhere. You get a folder with this
`README.md`, `make-title.sh` and the rest of the kit in it.
## Step 2: Start Docker
## Step 2: Get your computer ready (once)
Open Docker Desktop and wait until it says it is running.
**Mac.** Open Terminal and run:
```sh
xcode-select --install
```
and click Install. These are Apple's Command Line Tools (git, Python and a
compiler). If it says they are already installed, you are ready.
**Windows.** Open PowerShell **as administrator** and run:
```powershell
wsl --install
```
Restart when it asks, then open **Ubuntu** from the Start menu and choose a
user name and password. Then, in Ubuntu, install what the build uses and the
compiler, LLVM 22, from [apt.llvm.org](https://apt.llvm.org) (LLVM's own
packages for Ubuntu):
```sh
sudo apt update && sudo apt install -y git python3 patch curl xz-utils unzip lsb-release wget gnupg software-properties-common
curl -fsSL https://apt.llvm.org/llvm.sh -o /tmp/llvm.sh && sudo bash /tmp/llvm.sh 22 && sudo apt install -y llvm-22
```
Move the kit into Ubuntu's own disk (building from the Windows drive is
several times slower). If you unzipped it to your Downloads folder:
```sh
cp -r /mnt/c/Users/<you>/Downloads/sunshine-ps5-kit-<version> ~/
cd ~/sunshine-ps5-kit-<version>
```
**Linux.** You need git, Python 3, `patch` and curl, and LLVM 22 is best
installed from [apt.llvm.org](https://apt.llvm.org). On Ubuntu or Debian:
```sh
sudo apt install -y git python3 patch curl xz-utils lsb-release wget gnupg software-properties-common
curl -fsSL https://apt.llvm.org/llvm.sh -o /tmp/llvm.sh && sudo bash /tmp/llvm.sh 22 && sudo apt install -y llvm-22
```
On another Linux, the build fetches LLVM's own build of LLVM 22 instead, if
the libraries it needs are there (it says what to do if they are not).
## Step 3: Build the title
Open a terminal **in the kit's folder** and run the command for your system.
**macOS or Linux**
Open a terminal **in the kit's folder** (on Windows, the Ubuntu window from
step 2) and run:
```sh
./make-title.sh
```
**Windows (PowerShell)**
```powershell
docker build -t sunshine-ps5-kit .
docker run --rm -v ${PWD}\out:/out sunshine-ps5-kit /out
```
(If you use WSL, the macOS and Linux command works there instead.)
The first time, it fetches what it needs and your computer does not have: on a
Mac the compiler (LLVM 22, about 1.5 GB), and CMake and Ninja (small build
tools), from their projects' own releases. It checks each download and keeps
it for next time (in `~/Library/Caches/sunshine-ps5-kit` on a Mac,
`~/.cache/sunshine-ps5-kit` on Linux). It installs nothing system-wide and
never asks for your password.
### What you will see
| Stage | What it does | How long |
| --- | --- | --- |
| First run only | Builds the container; downloads a compiler (about 1 GB) | a few minutes |
| First run only | Fetches what is missing: on a Mac the compiler, CMake and Ninja (about 1.6 GB) | a few minutes |
| `fetching` | Downloads the PC port and the decompilation from GitHub | a minute |
| `configuring` | Prepares the build | under a minute |
| `compiling the game's code` | Compiles about 600 files for the PS5. **This is the long one.** | 3 to 15 minutes |
@@ -79,6 +123,8 @@ It is finished when it prints the path of a folder ending in `PPSA99312`.
## Step 4: Copy it to the PS5
The result is the folder `out/PPSA99312` inside the kit's folder (about 50 MB).
On Windows, open it from File Explorer at
`\\wsl$\Ubuntu\home\<you>\sunshine-ps5-kit-<version>\out`.
With an FTP program (FileZilla, Cyberduck, WinSCP), connect to your PS5 and
copy that **whole folder** into `/data/homebrew/`. Then copy **your disc
@@ -104,6 +150,7 @@ image** into it, beside `eboot.bin`, so that you end up with:
| Square | X |
| Triangle | Y (first-person view) |
| R1 | Z |
| L1 + R1 | **Save state** (in a level); in the pause menu, the list of save states to load |
| L2, R2 | L, R (R sprays FLUDD) |
| Options | Start (pause, map) |
| Touch pad button | **Settings** |
@@ -134,6 +181,15 @@ the PC port's cheats (god mode, infinite lives and so on).
Your saves and settings are kept on the console in
`/data/homebrew/PPSA99312/userdata/`.
### Save states
Hold L1 and press R1 in a level to save the moment. Pause (Options) and press
L1 + R1 again to see your save states, with a picture of each, and load one.
They are kept beside your memory card, in a `savestates` folder in
`/data/homebrew/PPSA99312/userdata/`. A state loads exactly only in the build
that saved it; after you rebuild, older states put Mario back where he was in
that level and keep your progress, but the level starts afresh.
---
## Optional: HD textures
@@ -143,17 +199,13 @@ downloads the [Super Mario Sunshine UHD Texture Pack](https://github.com/qashto/
(by qashto and razius) and the [HD texture extras](https://github.com/chasem-dev/sms-hd-texture-extras)
(by chasem-dev) from their authors and makes a copy sized for the console:
**macOS or Linux**
```sh
./make-texture-pack.sh
```
**Windows (PowerShell)**
```powershell
docker run --rm --entrypoint python3 -v ${PWD}\out:/out sunshine-ps5-kit /kit/tools/get-texture-pack.py /out
```
(On Windows, in the Ubuntu window, as in step 3.) The first time it fetches
the two Python packages it uses, Pillow and etcpak, and on Linux 7-Zip, into
the same cache as the compiler.
It downloads about 1 GB, needs about 5 GB of free disk space while it works,
and leaves `out/textures` (1.3 GB). Copy that folder to the console as:
@@ -176,25 +228,46 @@ shrunk the same way with `tools/shrink-texture-pack.py SOURCE DEST`.
## If something goes wrong
**"docker was not found"**
Docker Desktop is not installed, or not running. Start it and try again.
**"this Mac needs Apple's Command Line Tools"**
Run `xcode-select --install`, wait for it to finish, and run the command
again.
**"missing: git python3 ..."**
Install what it names (step 2 has the command for Ubuntu and Debian).
**"LLVM's own Linux build would not run here: it needs libxml2.so.2"**
Newer Linux releases (Ubuntu 26.04, Debian 13) no longer have that library.
Install LLVM 22 from apt.llvm.org as in step 2; the build then uses it.
**"LLVM publishes no build for Darwin on x86_64"** (an Intel Mac)
Install LLVM 22 with Homebrew, then point the kit at it:
```sh
brew install llvm@22 lld@22
PS5_CLANG=$(brew --prefix llvm@22)/bin/clang ./make-title.sh
```
**The fetch fails**
The build needs to reach github.com the first time. Check the connection and
run the command again.
**The build stops part way with "Killed", or Docker closes**
It ran out of memory. Either give Docker more (Docker Desktop > Settings >
Resources), or run fewer compilers at once:
**The build stops part way with "Killed"**
It ran out of memory. Run fewer compilers at once:
```sh
JOBS=4 ./make-title.sh
```
On Windows, add `-e JOBS=4` after `docker run --rm`.
On Windows, WSL may also need more memory: see Microsoft's notes on
`.wslconfig`.
**It was interrupted**
Run the same command again. It carries on from where it stopped.
Run the same command again. It carries on from where it stopped, including a
download.
**A download fails or "did not download correctly"**
Run the command again. Every download is checked against the checksum the kit
expects, and a bad one is thrown away.
**The game does not appear on the PS5**
Check the folder is exactly `/data/homebrew/PPSA99312/` with `eboot.bin`
@@ -211,13 +284,15 @@ Unzip the new kit, build again, and copy the new `out/PPSA99312` over the old
one on the console. Your saves, settings, disc image and texture packs are
left alone.
## Without Docker (Linux only)
## With Docker instead
If you already have LLVM 22 (clang, lld, llvm tools), CMake, Ninja, git,
Python 3, `patch`, a C++ compiler and the zlib development headers:
If you would rather build in a container (Docker Desktop or Podman), the kit
still can: `./make-title.sh --docker` and `./make-texture-pack.sh --docker`.
On Windows without WSL, in PowerShell:
```sh
./build-title.sh
```powershell
docker build -t sunshine-ps5-kit .
docker run --rm -v ${PWD}\out:/out sunshine-ps5-kit /out
```
---
@@ -226,11 +301,12 @@ Python 3, `patch`, a C++ compiler and the zlib development headers:
| Path | What |
| --- | --- |
| `make-title.sh`, `Dockerfile` | Runs the build in a container. |
| `make-title.sh` | Builds the title (`--docker`: in a container, with `Dockerfile`). |
| `build-title.sh` | The build itself: fetch, patch, compile, link. |
| `tools.sh` | Finds LLVM 22, CMake and Ninja, or fetches them once, checked. |
| `lib/` | The port's own compiled parts: the platform layer, the graphics layer, SDL2. |
| `opengl/` | The PS5 OpenGL SDK's driver, as this port links it. |
| `vulkan/` | PS5_Vulkan's link recipe and the tool that makes `eboot.bin` (no Vulkan driver). |
| `vulkan/` | PS5_Vulkan's link recipe and the tool that makes `eboot.bin` (no Vulkan driver), built for Linux in `vulkan/prebuilt/`. |
| `sdk/` | The payload SDK's compiler wrappers, headers, libraries and link scripts. |
| `ps5/` | The console's patch to the PC port, the link script, the title's icon and pictures. |
| `make-texture-pack.sh`, `tools/` | Downloads an HD texture pack and sizes it for the console. |
+36 -22
View File
@@ -13,12 +13,16 @@
# involved: the title plays your own disc image, which you copy beside
# eboot.bin on the console.
#
# Needs: bash, git, python3, cmake, ninja, patch, a host C++ compiler with zlib,
# and LLVM 22 (clang, lld, llvm-ar). make-title.sh runs it in a container that
# has them.
# Runs on macOS (Apple silicon; with Apple's Command Line Tools) and on Linux
# (x86_64 or arm64; on Windows, in WSL), with git, python3, patch and curl.
# LLVM 22, CMake and Ninja come from this machine if it has them, and are
# otherwise fetched once into a cache (tools.sh says from where). On Linux the
# kit's own ps5-native-tool is used; elsewhere it is compiled here.
# make-title.sh --docker runs this in a container instead.
#
# JOBS compilers at once (default: one a processor)
# PS5_CLANG the clang to use (default: /usr/lib/llvm-22/bin/clang, or clang-22)
# PS5_CLANG LLVM 22's clang, to use instead of looking for one
# KIT_CACHE where fetched tools are kept
#
# SPDX-License-Identifier: MIT
set -euo pipefail
@@ -26,10 +30,21 @@ kit=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
sdk="$kit/sdk" vulkan="$kit/vulkan"
say() { printf '==> %s\n' "$*"; }
fail() { echo "build-title.sh: $*" >&2; exit 2; }
# shellcheck source=tools.sh
source "$kit/tools.sh"
# ELF -> the console's SELF: the kit's own build for Linux, or compiled here
build_tools() {
local host_cxx=${CXX:-c++} native="$vulkan/tooling/native"
local prebuilt="$vulkan/prebuilt/$kit_os-$kit_arch/ps5-native-tool"
[[ $kit_arch == arm64 && $kit_os == Linux ]] && prebuilt="$vulkan/prebuilt/Linux-aarch64/ps5-native-tool"
if [[ ! -x $vulkan/build/host/ps5-native-tool && -f $prebuilt ]]; then
mkdir -p "$vulkan/build/host"
cp "$prebuilt" "$vulkan/build/host/ps5-native-tool"
chmod +x "$vulkan/build/host/ps5-native-tool"
fi
if [[ ! -x $vulkan/build/host/ps5-native-tool ]]; then
command -v "$host_cxx" > /dev/null || fail "a C++ compiler (c++) is needed to build the kit's ps5-native-tool"
mkdir -p "$vulkan/build/host"
"$host_cxx" -std=c++20 -O2 "$native/native_app_builder.cpp" "$native/self_container.cpp" \
"$native/elf_object.cpp" "$native/sce_module_writer.cpp" -lz -o "$vulkan/build/host/ps5-native-tool"
@@ -39,30 +54,29 @@ if [[ ${1:-} == --tools ]]; then
build_tools
exit 0
fi
case ${1:-} in -h | --help) sed -n '2,24p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
case ${1:-} in -h | --help) sed -n '2,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
out=${1:-$PWD/out}
mkdir -p "$out"
out=$(cd -- "$out" && pwd)
work="$out/.work"
jobs=${JOBS:-$(nproc)}
jobs=${JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)}
title_id=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["titleId"])' "$kit/ps5/sce_sys/param.json")
app="$out/$title_id"
mkdir -p "$work/bin" "$app"
# ---- the compiler: LLVM 22, with the kit's builtins for the console's processor
real_clang=${PS5_CLANG:-}
if [[ -z $real_clang ]]; then
for candidate in /usr/lib/llvm-22/bin/clang clang-22; do
if command -v "$candidate" > /dev/null; then real_clang=$(command -v "$candidate"); break; fi
done
fi
[[ -n $real_clang ]] || fail "clang 22 was not found (set PS5_CLANG, or use make-title.sh)"
real_clang=$(readlink -f "$real_clang")
# ---- the tools: this machine's, or fetched once (tools.sh)
# (an unzip that dropped the executable bits: the SDK's wrappers are run directly)
chmod +x "$sdk"/bin/* 2>/dev/null || true
kit_check_basics || exit 2
kit_find_llvm || exit 2
kit_find_cmake || exit 2
kit_find_ninja || exit 2
real_clang=$kit_clang
llvm_bin=$(dirname "$real_clang")
for tool in ld.lld llvm-ar llvm-nm llvm-config; do
[[ -x $llvm_bin/$tool ]] || fail "$llvm_bin/$tool is missing (LLVM's lld and tools, beside clang)"
done
say "compiler: $real_clang; $(cmake --version | head -1); ninja $(ninja --version)"
# ---- the compiler: LLVM 22, with the kit's builtins for the console's processor
export LLVM_CONFIG="$llvm_bin/llvm-config"
cat > "$work/bin/clang" <<WRAPPER
#!/bin/sh
@@ -82,7 +96,7 @@ port_url=$(sed -n 's/^port_url=//p' "$kit/REVISIONS")
port_rev=$(sed -n 's/^port_rev=//p' "$kit/REVISIONS")
decomp_rev=$(sed -n 's/^decomp_rev=//p' "$kit/REVISIONS")
source="$work/sms-pc-port"
stamp="$port_rev $decomp_rev $(sha256sum "$kit/ps5/patches/sms-pc-port.patch" | cut -d' ' -f1)"
stamp="$port_rev $decomp_rev $(kit_sha256 "$kit/ps5/patches/sms-pc-port.patch")"
if [[ $(cat "$work/.source-stamp" 2>/dev/null) != "$stamp" ]]; then
say "fetching the PC port and the decompilation (about 150 MB)"
rm -rf -- "$source" "$work/game"
@@ -107,7 +121,7 @@ if [[ ! -f $build/build.ninja ]]; then
-DCMAKE_TOOLCHAIN_FILE="$sdk/toolchain/prospero.cmake" \
-DCMAKE_PROJECT_sms_port_INCLUDE="$kit/ps5/cmake/ps5-kit-game.cmake" \
-DPS5_KIT_DIR="$kit" > "$build/configure.log" 2>&1 ||
{ tail -40 "$build/configure.log" >&2; fail "the configuration failed"; }
{ tail -40 "$build/configure.log" >&2; mv "$build" "$build.failed-$$"; rm -rf "$build.failed-$$"; fail "the configuration failed"; }
fi
say "compiling the game's code, $jobs files at a time (this is the long step)"
ninja -C "$build" -j "$jobs" sms_game
@@ -123,14 +137,14 @@ while IFS= read -r line; do
*) inputs+=("$kit/lib/$line") ;;
esac
done < "$kit/lib/link.txt"
PS5_DIST_DIR="$out" PS5_OPENGL_PREFIX="$kit/opengl/sdk" \
PS5_DIST_DIR="$out" PS5_OPENGL_PREFIX="$kit/opengl/sdk" SMS_SAVESTATE_RANGES="$source/tools/savestate_ranges.py" \
bash "$kit/ps5/tools/link-title.sh" "$work" "$vulkan" "$sdk" "${inputs[@]}"
mkdir -p "$app/res"
cp -R "$kit/ps5/res/." "$app/res/"
cp "$kit/LICENSING.md" "$app/licenses/"
# a container runs as root: hand the result to whoever owns the output folder
if [[ $(id -u) -eq 0 ]]; then
if [[ $(id -u) -eq 0 && $kit_os == Linux ]]; then
owner=$(stat -c '%u:%g' "$out")
[[ $owner == 0:0 ]] || chown -R "$owner" "$out" 2>/dev/null || true
fi
+50 -9
View File
@@ -1,23 +1,64 @@
#!/usr/bin/env bash
# Super Mario Sunshine for the PlayStation 5 - an HD texture pack for the console, made in the kit's container.
# Super Mario Sunshine for the PlayStation 5 - an HD texture pack for the console.
#
# make-texture-pack.sh [OUT] OUT/textures/ (OUT defaults to ./out)
# make-texture-pack.sh [OUT] OUT/textures/ (OUT defaults to ./out), made on this machine
# make-texture-pack.sh --docker [OUT] the same, in the kit's container (Docker, or Podman: CONTAINER=podman)
#
# Optional. Downloads the UHD texture pack and the HD extras from their
# authors, makes a copy sized for the console (tools/get-texture-pack.py) and
# leaves it in OUT/textures: copy that to the console as
# /data/homebrew/PPSA99312/mods/textures. Needs Docker (or Podman:
# CONTAINER=podman), an internet connection and about 5 GB of free disk space.
# /data/homebrew/PPSA99312/mods/textures. Needs Python 3, an internet
# connection and about 5 GB of free disk space. The Python packages it uses
# (Pillow, etcpak) and, on Linux, 7-Zip are fetched once into the kit's cache
# (tools.sh); a Mac's own tar opens the pack.
#
# SPDX-License-Identifier: MIT
set -euo pipefail
kit=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
if [[ ${1:-} == --docker ]]; then
shift
out=${1:-$PWD/out}
mkdir -p "$out"
out=$(cd -- "$out" && pwd)
container=${CONTAINER:-docker}
command -v "$container" > /dev/null || { echo "make-texture-pack.sh: $container was not found: install Docker Desktop (or Podman), or run without --docker" >&2; exit 2; }
tag=sunshine-ps5-kit:$(<"$kit/VERSION")
"$container" image inspect "$tag" > /dev/null 2>&1 || "$container" build -t "$tag" "$kit"
exec "$container" run --rm --entrypoint python3 -v "$out":/out "$tag" /kit/tools/get-texture-pack.py /out
fi
out=${1:-$PWD/out}
mkdir -p "$out"
out=$(cd -- "$out" && pwd)
container=${CONTAINER:-docker}
command -v "$container" > /dev/null || { echo "make-texture-pack.sh: $container was not found: install Docker Desktop (or Podman)" >&2; exit 2; }
tag=sunshine-ps5-kit:$(<"$kit/VERSION")
# shellcheck source=tools.sh
source "$kit/tools.sh"
command -v python3 > /dev/null || { echo "make-texture-pack.sh: Python 3 is needed" >&2; exit 2; }
if [[ $kit_os == Darwin ]] && ! xcode-select -p > /dev/null 2>&1; then
echo "make-texture-pack.sh: this Mac needs Apple's Command Line Tools (for Python): run xcode-select --install" >&2
exit 2
fi
"$container" image inspect "$tag" > /dev/null 2>&1 || "$container" build -t "$tag" "$kit"
exec "$container" run --rm --entrypoint python3 -v "$out":/out "$tag" /kit/tools/get-texture-pack.py /out
# Pillow and etcpak, for this Python
packages=$(python3 "$kit/tools/fetch-wheels.py" "$kit_cache/python") || exit 2
export PYTHONPATH="$packages${PYTHONPATH:+:$PYTHONPATH}"
# 7-Zip on Linux (GNU tar cannot open a .7z; a Mac's tar can)
if [[ $kit_os == Linux ]] && ! command -v 7z > /dev/null && ! command -v 7zz > /dev/null && ! command -v 7za > /dev/null; then
case $kit_arch in
x86_64) asset=7z2604-linux-x64.tar.xz sha=fc0327ba27e89bd086cf426dff17d77de582953cdbbc10a6576540a06853ffcd ;;
aarch64 | arm64) asset=7z2604-linux-arm64.tar.xz sha=5b0ac3aa91c1e3499f011d6af69d7f6b4de3ef2f011d9e2ff44c58ebd04aa388 ;;
*) echo "make-texture-pack.sh: 7-Zip is needed (7z, 7zz or 7za)" >&2; exit 2 ;;
esac
dest="$kit_cache/7zip-26.04"
if [[ ! -x $dest/7zz ]]; then
echo "==> fetching 7-Zip 26.04"
kit_fetch "https://github.com/ip7z/7zip/releases/download/26.04/$asset" "$sha" "$kit_cache/$asset" || exit 2
mkdir -p "$dest"
tar -xJf "$kit_cache/$asset" -C "$dest" 7zz
rm -f "$kit_cache/$asset"
fi
PATH=$dest:$PATH
fi
exec python3 "$kit/tools/get-texture-pack.py" "$out"
+14 -7
View File
@@ -1,23 +1,30 @@
#!/usr/bin/env bash
# Super Mario Sunshine for the PlayStation 5 - the builder kit, run in a container.
# Super Mario Sunshine for the PlayStation 5 - the builder kit: make the title.
#
# make-title.sh [OUT] OUT/PPSA99312/ (OUT defaults to ./out)
# make-title.sh [OUT] OUT/PPSA99312/ (OUT defaults to ./out), built on this machine
# make-title.sh --docker [OUT] the same, built in a container (Docker, or Podman: CONTAINER=podman)
#
# Needs Docker (or Podman: CONTAINER=podman) and an internet connection: the
# first run builds the container's image, which downloads a compiler, and each
# title build fetches the game's source code from GitHub the first time (it is
# kept in OUT/.work afterwards). See README.md.
# On this machine (macOS on Apple silicon, Linux, or WSL on Windows) it runs
# build-title.sh, which uses the compiler and build tools this machine has, or
# fetches them once (LLVM 22 is about 1.5 GB). Each title build fetches the
# game's source code from GitHub the first time (it is kept in OUT/.work
# afterwards). See README.md.
#
# JOBS compilers at once (default: one a processor)
#
# SPDX-License-Identifier: MIT
set -euo pipefail
kit=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
if [[ ${1:-} != --docker ]]; then
exec bash "$kit/build-title.sh" "$@"
fi
shift
out=${1:-$PWD/out}
mkdir -p "$out"
out=$(cd -- "$out" && pwd)
container=${CONTAINER:-docker}
command -v "$container" > /dev/null || { echo "make-title.sh: $container was not found: install Docker Desktop (or Podman)" >&2; exit 2; }
command -v "$container" > /dev/null || { echo "make-title.sh: $container was not found: install Docker Desktop (or Podman), or build without --docker" >&2; exit 2; }
tag=sunshine-ps5-kit:$(<"$kit/VERSION")
"$container" image inspect "$tag" > /dev/null 2>&1 || "$container" build -t "$tag" "$kit"
+243
View File
@@ -0,0 +1,243 @@
# Super Mario Sunshine for the PlayStation 5 - the builder kit's tools: found on this machine, or fetched once.
#
# Sourced by build-title.sh. Sets kit_clang (LLVM 22's clang, with lld and the
# LLVM tools beside it), and puts CMake and Ninja on PATH. Each comes from this
# machine when it has a version that works:
#
# LLVM 22 PS5_CLANG, /usr/lib/llvm-22 (apt.llvm.org), clang-22 on PATH, or
# Homebrew's llvm@22 with lld beside it
# CMake 3.20 to 3.x on PATH (CMake 4 drops what SDL's build still asks for)
# Ninja on PATH
#
# and otherwise from its project's own release for this kind of machine, checked
# against the SHA-256 below and kept in the kit's cache (KIT_CACHE; by default
# ~/Library/Caches/sunshine-ps5-kit on a Mac, ~/.cache/sunshine-ps5-kit on Linux),
# so it is fetched once for every kit. LLVM's release is the large one: about
# 1.5 GB to fetch (1.9 GB for Linux), of which about 450 MB is kept.
#
# On Ubuntu and Debian (and WSL), apt.llvm.org's packages are the better way to
# LLVM 22, and the kit's README says so: LLVM's own Linux build needs
# libxml2.so.2, which Ubuntu 26.04 and Debian 13 no longer have (they moved to
# libxml2.so.16). It is fetched only where the libraries it needs are there.
#
# Bash 3.2 (a Mac's) runs this: no associative arrays.
#
# SPDX-License-Identifier: MIT
kit_llvm_version=22.1.8
kit_cmake_version=3.31.12
kit_ninja_version=1.13.2
kit_os=$(uname -s)
kit_arch=$(uname -m)
case $kit_os-$kit_arch in
Darwin-arm64)
kit_llvm_asset=LLVM-$kit_llvm_version-macOS-ARM64.tar.xz
kit_llvm_sha=f260f4f7c0d430828a81ae8a3826a1d63fc0963ec2459489308cc23b1f7eab4f
;;
Linux-x86_64)
kit_llvm_asset=LLVM-$kit_llvm_version-Linux-X64.tar.xz
kit_llvm_sha=df0e1ecf16caf3489a272a5eea4eec9b0d82878f6477fa309504f918a0006384
;;
Linux-aarch64 | Linux-arm64)
kit_llvm_asset=LLVM-$kit_llvm_version-Linux-ARM64.tar.xz
kit_llvm_sha=805efad2bb91cb4967fa569e0881d10c0f69c04461cf671cccbae19f547acc34
;;
*) kit_llvm_asset= ;; # (an Intel Mac: LLVM publishes no build for it)
esac
case $kit_os-$kit_arch in
Darwin-*)
kit_cmake_asset=cmake-$kit_cmake_version-macos-universal.tar.gz
kit_cmake_sha=799af7fd545db9bf1b9cfe72f8095880e727a2d4e0df0e3dffc3bc7b95c2d3b0
kit_cmake_bin=CMake.app/Contents/bin
kit_ninja_asset=ninja-mac.zip
kit_ninja_sha=c99048673aa765960a99cf10c6ddb9f1fad506099ff0a0e137ad8960a88f321b
;;
Linux-x86_64)
kit_cmake_asset=cmake-$kit_cmake_version-linux-x86_64.tar.gz
kit_cmake_sha=0dc2e9a6860f06bf10bd8fadc03e35d9eeb4df46e33763a7e480e987758f385c
kit_cmake_bin=bin
kit_ninja_asset=ninja-linux.zip
kit_ninja_sha=5749cbc4e668273514150a80e387a957f933c6ed3f5f11e03fb30955e2bbead6
;;
Linux-aarch64 | Linux-arm64)
kit_cmake_asset=cmake-$kit_cmake_version-linux-aarch64.tar.gz
kit_cmake_sha=83f8fd91d2038a56556e1400390fcfe42f79602940c494f6c6f1cdae7f9e7f40
kit_cmake_bin=bin
kit_ninja_asset=ninja-linux-aarch64.zip
kit_ninja_sha=fd2cacc8050a7f12a16a2e48f9e06fca5c14fc4c2bee2babb67b58be17a607fc
;;
*) kit_cmake_asset= kit_ninja_asset= ;;
esac
if [[ -n ${KIT_CACHE:-} ]]; then
kit_cache=$KIT_CACHE
elif [[ $kit_os == Darwin ]]; then
kit_cache=$HOME/Library/Caches/sunshine-ps5-kit
else
kit_cache=${XDG_CACHE_HOME:-$HOME/.cache}/sunshine-ps5-kit
fi
kit_sha256() { python3 -c 'import hashlib,sys; h=hashlib.sha256(); f=open(sys.argv[1],"rb"); [h.update(b) for b in iter(lambda: f.read(1<<20), b"")]; print(h.hexdigest())' "$1"; }
# kit_fetch URL SHA256 FILE: download, check, and keep it only if it matches
kit_fetch() {
local url=$1 sha=$2 file=$3
mkdir -p "$(dirname "$file")"
# (one already here, from a run stopped while unpacking it)
[[ -f $file && $(kit_sha256 "$file") == "$sha" ]] && return 0
if command -v curl > /dev/null; then
curl -fL --retry 3 --progress-bar -o "$file.part" "$url" || { rm -f "$file.part"; return 1; }
elif command -v wget > /dev/null; then
wget -q --show-progress -O "$file.part" "$url" || { rm -f "$file.part"; return 1; }
else
echo "tools.sh: curl or wget is needed to download $url" >&2
return 1
fi
if [[ $(kit_sha256 "$file.part") != "$sha" ]]; then
rm -f "$file.part"
echo "tools.sh: $url did not download correctly (its SHA-256 is not the expected one); try again" >&2
return 1
fi
mv "$file.part" "$file"
}
kit_is_llvm22() {
[[ -x $1 ]] && "$1" --version 2>/dev/null | head -1 | grep -q "version 22\." || return 1
local bin
bin=$(dirname "$(kit_realpath "$1")")
local tool
for tool in ld.lld llvm-ar llvm-nm llvm-config llvm-objcopy llvm-strip; do
[[ -x $bin/$tool ]] || return 1
done
# (lld needs a library clang does not: it has to run too)
"$bin/ld.lld" --version > /dev/null 2>&1
}
# The shared libraries LLVM's own Linux build needs, that this machine lacks
kit_llvm_missing_libraries() {
[[ $kit_os == Linux ]] || return 0
local cache library
cache=$( (ldconfig -p || /sbin/ldconfig -p) 2>/dev/null)
for library in libz.so.1 libstdc++.so.6 libgcc_s.so.1 libxml2.so.2; do
grep -q "^[[:space:]]*$library " <<< "$cache" || printf '%s ' "$library"
done
}
kit_llvm_advice() {
echo "tools.sh: install LLVM 22 from apt.llvm.org (about 150 MB), then run this again:" >&2
echo " sudo apt install -y lsb-release wget gnupg software-properties-common" >&2
echo " curl -fsSL https://apt.llvm.org/llvm.sh -o /tmp/llvm.sh && sudo bash /tmp/llvm.sh 22 && sudo apt install -y llvm-22" >&2
}
kit_realpath() { python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$1"; }
kit_find_llvm() {
local candidate
if [[ -n ${PS5_CLANG:-} ]]; then
kit_is_llvm22 "$PS5_CLANG" && { kit_clang=$(kit_realpath "$PS5_CLANG"); return 0; }
echo "tools.sh: PS5_CLANG ($PS5_CLANG) is not LLVM 22's clang with lld, llvm-ar, llvm-nm, llvm-config, llvm-objcopy and llvm-strip beside it" >&2
return 1
fi
for candidate in /usr/lib/llvm-22/bin/clang "$(command -v clang-22 || true)" \
/opt/homebrew/opt/llvm@22/bin/clang /usr/local/opt/llvm@22/bin/clang \
"$kit_cache/llvm-$kit_llvm_version/bin/clang"; do
[[ -n $candidate ]] && kit_is_llvm22 "$candidate" && { kit_clang=$(kit_realpath "$candidate"); return 0; }
done
[[ -n $kit_llvm_asset ]] || {
echo "tools.sh: LLVM 22 was not found, and LLVM publishes no build for $kit_os on $kit_arch to fetch." >&2
echo "tools.sh: install LLVM 22 with lld (Homebrew: brew install llvm@22 lld@22) and set PS5_CLANG to its clang." >&2
return 1
}
local missing
missing=$(kit_llvm_missing_libraries)
if [[ -n $missing ]]; then
echo "tools.sh: LLVM 22 was not found, and LLVM's own Linux build would not run here: it needs ${missing% }." >&2
kit_llvm_advice
return 1
fi
local dest="$kit_cache/llvm-$kit_llvm_version" top=${kit_llvm_asset%.tar.xz}
echo "==> fetching LLVM $kit_llvm_version, the compiler (about 1.5 GB, once: it is kept in $kit_cache)"
kit_fetch "https://github.com/llvm/llvm-project/releases/download/llvmorg-$kit_llvm_version/$kit_llvm_asset" \
"$kit_llvm_sha" "$kit_cache/$kit_llvm_asset" || return 1
echo "==> unpacking the parts of it the build uses"
rm -rf "$dest.part"
mkdir -p "$dest.part"
local members=() name
for name in clang clang-22 clang++ lld ld.lld llvm-ar llvm-ranlib llvm-nm llvm-config llvm-objcopy llvm-strip \
llvm-readobj llvm-readelf; do
members+=("$top/bin/$name")
done
members+=("$top/lib/clang/22/include")
tar -xJf "$kit_cache/$kit_llvm_asset" -C "$dest.part" "${members[@]}" || return 1
rm -rf "$dest"
mv "$dest.part/$top" "$dest"
rm -rf "$dest.part" "$kit_cache/$kit_llvm_asset"
kit_is_llvm22 "$dest/bin/clang" || {
echo "tools.sh: the LLVM fetched into $dest does not run here" >&2
[[ $kit_os == Linux ]] && kit_llvm_advice
return 1
}
kit_clang=$dest/bin/clang
}
kit_cmake_works() {
local version
version=$("$1" --version 2>/dev/null | head -1 | sed -n 's/^cmake version \([0-9]*\)\.\([0-9]*\).*/\1 \2/p')
[[ -n $version ]] || return 1
set -- $version
[[ $1 -eq 3 && $2 -ge 20 ]]
}
kit_find_cmake() {
local found
found=$(command -v cmake || true)
[[ -n $found ]] && kit_cmake_works "$found" && return 0
local dest="$kit_cache/cmake-$kit_cmake_version"
if [[ ! -x $dest/$kit_cmake_bin/cmake ]]; then
[[ -n $kit_cmake_asset ]] || { echo "tools.sh: CMake 3.20 or later (but not 4) is needed" >&2; return 1; }
echo "==> fetching CMake $kit_cmake_version (about 60 MB, once)"
kit_fetch "https://github.com/Kitware/CMake/releases/download/v$kit_cmake_version/$kit_cmake_asset" \
"$kit_cmake_sha" "$kit_cache/$kit_cmake_asset" || return 1
rm -rf "$dest" "$dest.part"
mkdir -p "$dest.part"
tar -xzf "$kit_cache/$kit_cmake_asset" -C "$dest.part" || return 1
mv "$dest.part/${kit_cmake_asset%.tar.gz}" "$dest"
rm -rf "$dest.part" "$kit_cache/$kit_cmake_asset"
fi
PATH=$dest/$kit_cmake_bin:$PATH
}
kit_find_ninja() {
ninja --version > /dev/null 2>&1 && return 0
local dest="$kit_cache/ninja-$kit_ninja_version"
if [[ ! -x $dest/ninja ]]; then
[[ -n $kit_ninja_asset ]] || { echo "tools.sh: Ninja is needed" >&2; return 1; }
echo "==> fetching Ninja $kit_ninja_version"
kit_fetch "https://github.com/ninja-build/ninja/releases/download/v$kit_ninja_version/$kit_ninja_asset" \
"$kit_ninja_sha" "$kit_cache/$kit_ninja_asset" || return 1
mkdir -p "$dest"
python3 -m zipfile -e "$kit_cache/$kit_ninja_asset" "$dest" && chmod +x "$dest/ninja" || return 1
rm -f "$kit_cache/$kit_ninja_asset"
fi
PATH=$dest:$PATH
}
# What no download supplies: say what to install, for this machine
kit_check_basics() {
local missing=() tool
for tool in git python3 patch tar; do
command -v "$tool" > /dev/null || missing+=("$tool")
done
# a Mac's python3 and git are stand-ins until the Command Line Tools are installed
if [[ $kit_os == Darwin ]] && ! xcode-select -p > /dev/null 2>&1; then
echo "build-title.sh: this Mac needs Apple's Command Line Tools (git, Python and a C++ compiler)." >&2
echo "build-title.sh: run xcode-select --install and start this again once they are installed." >&2
return 1
fi
if [[ ${#missing[@]} -gt 0 ]]; then
echo "build-title.sh: missing: ${missing[*]}" >&2
[[ $kit_os == Linux ]] && echo "build-title.sh: on Ubuntu or Debian (and WSL): sudo apt install git python3 patch xz-utils curl" >&2
return 1
fi
}
+305 -41
View File
@@ -1,5 +1,5 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2ad719a..8a74c1b 100644
index 091476d..3fcb2a8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -90,6 +90,18 @@ include(${CMAKE_BINARY_DIR}/decomp_sources.cmake)
@@ -21,17 +21,17 @@ index 2ad719a..8a74c1b 100644
set(SMS_GAME_FLAGS
-w -fno-strict-aliasing -fwrapv -fno-omit-frame-pointer
# Retail MSL has no _FORTIFY_SOURCE checks (Ubuntu g++ enables them at -O1+;
@@ -118,7 +130,8 @@ check_cxx_compiler_flag("-fno-unreachable-traps" SMS_HAVE_NO_UNREACHABLE_TRAPS)
if(SMS_HAVE_NO_UNREACHABLE_TRAPS)
list(APPEND SMS_GAME_FLAGS -fno-unreachable-traps)
endif()
-if(SMS_ARCH STREQUAL "64")
@@ -121,7 +133,8 @@ endif()
# Non-PIE: the game's code and data sit at the same addresses every run, which
# 64-bit builds need for pointers in u32 slots and save states need everywhere
# (a state holds pointers to the game's vtables and globals).
-if(SMS_ARCH STREQUAL "64" OR CMAKE_SYSTEM_NAME STREQUAL "Linux")
+# (The PS5 only runs position-independent code; its titles load at 0x400000.)
+if(SMS_ARCH STREQUAL "64" AND NOT PS5)
+if((SMS_ARCH STREQUAL "64" OR CMAKE_SYSTEM_NAME STREQUAL "Linux") AND NOT PS5)
list(APPEND SMS_GAME_FLAGS -fno-pie)
endif()
# Clang rejects the game's pointer-to-int truncations ((int)ptr, (u32)ptr) as
@@ -128,6 +141,13 @@ endif()
@@ -131,6 +144,13 @@ endif()
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
list(APPEND SMS_GAME_FLAGS -fms-extensions)
endif()
@@ -45,7 +45,7 @@ index 2ad719a..8a74c1b 100644
# macOS: native arm64 reserves the low 4 GiB as PAGEZERO, so port_low_alloc
# cannot satisfy pointer-in-u32. Build x86_64 (Rosetta) with a 4 KiB pagezero.
@@ -294,6 +314,10 @@ elseif(WIN32)
@@ -300,6 +320,10 @@ elseif(WIN32)
set(_object_prefix "")
endif()
set(SMS_OBJCOPY objcopy)
@@ -56,7 +56,7 @@ index 2ad719a..8a74c1b 100644
else()
set(_object_prefix "")
set(SMS_OBJCOPY objcopy)
@@ -315,7 +339,7 @@ list(FILTER SMS_PLATFORM_SOURCES EXCLUDE REGEX "/platform/gx/")
@@ -321,7 +345,7 @@ list(FILTER SMS_PLATFORM_SOURCES EXCLUDE REGEX "/platform/gx/")
# platform/mods/eclipse is built by cmake/eclipse.cmake, and only with SMS_ECLIPSE.
list(FILTER SMS_PLATFORM_SOURCES EXCLUDE REGEX "/platform/mods/eclipse/")
list(FILTER SMS_PLATFORM_SOURCES EXCLUDE REGEX "/tests/")
@@ -65,7 +65,7 @@ index 2ad719a..8a74c1b 100644
list(FILTER SMS_PLATFORM_SOURCES EXCLUDE REGEX "/platform/trace/trace\\.cpp$")
endif()
add_executable(sms ${SMS_PORT_SRC}/port_main.cpp ${SMS_PLATFORM_SOURCES})
@@ -366,13 +390,14 @@ if(WIN32)
@@ -373,13 +397,14 @@ if(WIN32)
# data. Keep the image below 4 GiB, as on the Linux non-PIE build.
target_link_options(sms PRIVATE -Wl,--image-base,0x400000 -Wl,--disable-dynamicbase -Wl,--disable-high-entropy-va)
endif()
@@ -77,11 +77,39 @@ index 2ad719a..8a74c1b 100644
else()
target_link_options(sms PRIVATE -Wl,--defsym=lbl_80004600=0x80004600)
endif()
-if(SMS_ARCH STREQUAL "64")
+if(SMS_ARCH STREQUAL "64" AND NOT PS5)
-if(SMS_ARCH STREQUAL "64" OR CMAKE_SYSTEM_NAME STREQUAL "Linux")
+if((SMS_ARCH STREQUAL "64" OR CMAKE_SYSTEM_NAME STREQUAL "Linux") AND NOT PS5)
target_compile_options(sms PRIVATE -fno-pie)
if(NOT APPLE AND NOT WIN32)
target_link_options(sms PRIVATE -no-pie)
@@ -398,16 +423,21 @@ if(APPLE)
endif()
# Save states copy the game's own global variables: the link map says where
# they are, and tools/savestate_ranges.py writes that into the executable.
-if(APPLE)
+# (The PS5's link makes its title from the executable in the link itself, so
+# the console's link script does this there.)
+if(PS5)
+elseif(APPLE)
target_link_options(sms PRIVATE "LINKER:-map,${CMAKE_BINARY_DIR}/sms.map")
else()
target_link_options(sms PRIVATE "LINKER:-Map=${CMAKE_BINARY_DIR}/sms.map")
endif()
-add_custom_command(TARGET sms POST_BUILD
- COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/savestate_ranges.py
- --map ${CMAKE_BINARY_DIR}/sms.map --exe $<TARGET_FILE:sms>
- --archive libsms_game.a --archive libsms_eclipse.a
- COMMENT "Recording the game's data for save states")
+if(NOT PS5)
+ add_custom_command(TARGET sms POST_BUILD
+ COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/savestate_ranges.py
+ --map ${CMAKE_BINARY_DIR}/sms.map --exe $<TARGET_FILE:sms>
+ --archive libsms_game.a --archive libsms_eclipse.a
+ COMMENT "Recording the game's data for save states")
+endif()
find_package(Threads REQUIRED)
if(WIN32 AND SMS_ARCH STREQUAL "64")
add_executable(sms_windows_stack_test EXCLUDE_FROM_ALL
diff --git a/decomp-patches/modhook-zz-macos-data-exports.patch b/decomp-patches/modhook-zz-macos-data-exports.patch
index 6bb5f59..bdd84c6 100644
--- a/decomp-patches/modhook-zz-macos-data-exports.patch
@@ -339,7 +367,7 @@ index c85815a..956cdf3 100644
// a headless run, so the process would ignore them. (No effect if the GX
// layer initialised SDL first; the user's own setting wins.)
diff --git a/platform/gx/CMakeLists.txt b/platform/gx/CMakeLists.txt
index 50a25d1..d97dde6 100644
index 3a31075..bf0b8eb 100644
--- a/platform/gx/CMakeLists.txt
+++ b/platform/gx/CMakeLists.txt
@@ -55,8 +55,10 @@ target_include_directories(sms_gx
@@ -790,23 +818,24 @@ index bcd4174..3887f46 100644
// Once a display frame: over the memory budget (SMS_TEXTURE_PACK_MB, 1536
diff --git a/platform/gx/src/gx_internal.h b/platform/gx/src/gx_internal.h
index 88b4c57..d204cf6 100644
index 1b0c961..77410da 100644
--- a/platform/gx/src/gx_internal.h
+++ b/platform/gx/src/gx_internal.h
@@ -284,7 +284,12 @@ void shaderPrepareStage(int stage); // while a stage loads: its programs (the w
// ---------------------------------------------------------------- overlay panels
// Each slot keeps its own texture, so panels drawn in the same frame keep their
// cached bitmaps. GXPC_DrawOverlay draws the debug overlay's slot.
-enum OverlaySlot { OVERLAY_DEBUG, OVERLAY_CHEAT_MENU, OVERLAY_WARP_MENU, OVERLAY_SLOTS };
+enum OverlaySlot { OVERLAY_DEBUG, OVERLAY_CHEAT_MENU, OVERLAY_WARP_MENU, OVERLAY_LOADING, OVERLAY_SLOTS };
@@ -290,8 +290,14 @@ enum OverlaySlot {
OVERLAY_WARP_MENU,
OVERLAY_STATE_MENU,
OVERLAY_STATE_TOAST,
+ OVERLAY_LOADING,
OVERLAY_SLOTS
};
+// A black frame saying LOADING, shown from inside a long load (gx_platform.cpp);
+// `step` animates its dots.
+void loadingScreen(int step);
+size_t hiresResidentBytes(); // the texture pack's replacements held, for the statistics
+void drawLoadingFrame(int step, int winW, int winH); // gx_render.cpp, on the GL thread
void drawOverlayPanel(OverlaySlot slot, const uint8_t* rgba, int w, int h, int x, int y, int scale, int winW, int winH);
// Panel bitmaps (gx_overlay.cpp): RGBA8, w x h, row 0 at the top. Text shares
// one scratch buffer, so panels are rasterised on the GL thread only.
// SMS_WINDOW_SHOTS: the window as shown, menus included, at GX frame `frame` (GL thread, before the swap).
void captureWindow(uint32_t frame, int w, int h);
diff --git a/platform/gx/src/gx_overlay.cpp b/platform/gx/src/gx_overlay.cpp
index d4cb8da..add238b 100644
--- a/platform/gx/src/gx_overlay.cpp
@@ -896,10 +925,10 @@ index d4cb8da..add238b 100644
GXPC_DrawOverlay(upload, w, h, 8, 8, scale, winW, winH);
}
diff --git a/platform/gx/src/gx_platform.cpp b/platform/gx/src/gx_platform.cpp
index 23778b5..e39c1f3 100644
index f2c1fc4..baa3ab8 100644
--- a/platform/gx/src/gx_platform.cpp
+++ b/platform/gx/src/gx_platform.cpp
@@ -37,6 +37,15 @@
@@ -38,6 +38,15 @@
#include <EGL/egl.h>
#include <EGL/eglext.h>
#endif
@@ -915,7 +944,7 @@ index 23778b5..e39c1f3 100644
namespace gx {
extern void (*g_displayCopyHook)(const void* xfb);
@@ -598,10 +607,86 @@ void dumpFrame(const void* xfb) {
@@ -599,10 +608,86 @@ void dumpFrame(const void* xfb) {
fclose(f);
}
@@ -1002,7 +1031,7 @@ index 23778b5..e39c1f3 100644
const bool skip = s_skipPresent && s_presentCost >= kSkipPresentCost;
s_skipPresent = false;
s_lastPresent = 0;
@@ -737,6 +822,27 @@ int GXPC_InitAuto(float efbScale) {
@@ -738,6 +823,27 @@ int GXPC_InitAuto(float efbScale) {
return 0;
}
@@ -1030,7 +1059,7 @@ index 23778b5..e39c1f3 100644
void GXPC_Present(const void* xfb) {
#ifdef SMS_GX_HAVE_SDL2
if (s_mode == MODE_WINDOW && s_window) {
@@ -864,6 +970,25 @@ void sms_gx_pump_events(void) {
@@ -867,6 +973,25 @@ void sms_gx_pump_events(void) {
}
continue;
}
@@ -1057,7 +1086,7 @@ index 23778b5..e39c1f3 100644
// the debug overlay is open, and closes it; a press that does either
// is kept from the pad layer, release included. Otherwise they reach
diff --git a/platform/gx/src/gx_render.cpp b/platform/gx/src/gx_render.cpp
index dae542e..b985d17 100644
index bd96a35..4e7c559 100644
--- a/platform/gx/src/gx_render.cpp
+++ b/platform/gx/src/gx_render.cpp
@@ -14,9 +14,25 @@
@@ -1390,9 +1419,9 @@ index dae542e..b985d17 100644
const GLsizeiptr bytes = GLsizeiptr(s_efbW) * EFB_H * 4;
int cur = s_frameNo & 1, t = depth ? 1 : 0;
if (!(s_peekIssued & (1u << t))) {
@@ -2999,6 +3213,27 @@ void GXPC_DrawOverlay(const uint8_t* rgba, int w, int h, int x, int y, int scale
} // extern "C"
@@ -3030,6 +3244,27 @@ void gx::captureWindow(uint32_t frame, int w, int h) {
}
}
+// The window black, with LOADING and its dots at the bottom right.
+void gx::drawLoadingFrame(int step, int winW, int winH) {
@@ -1418,7 +1447,7 @@ index dae542e..b985d17 100644
void gx::drawOverlayPanel(OverlaySlot slot, const uint8_t* rgba, int w, int h, int x, int y, int scale, int winW, int winH) {
if (!s_ready || w <= 0 || h <= 0) return;
if (!rgba && (!s_overlayTex[slot] || w != s_overlayW[slot] || h != s_overlayH[slot])) return;
@@ -3093,7 +3328,7 @@ void main() { o_color = texture(u_color, v_uv); }
@@ -3124,7 +3359,7 @@ void main() { o_color = texture(u_color, v_uv); }
float window[2] = {float(winW), float(winH)};
glUniform4fv(s_overlayRect, 1, rect);
glUniform2fv(s_overlayWindow, 1, window);
@@ -1460,7 +1489,7 @@ index 56ff7cc..7d3826b 100644
// The kernel checks the source and fails with EFAULT instead of faulting.
struct iovec local = {to, size}, remote = {(void*)from, size};
diff --git a/platform/os/os_thread.cpp b/platform/os/os_thread.cpp
index e676983..335167f 100644
index 193c3a7..a1f253d 100644
--- a/platform/os/os_thread.cpp
+++ b/platform/os/os_thread.cpp
@@ -328,6 +328,19 @@ double mono_seconds()
@@ -1533,7 +1562,7 @@ index e676983..335167f 100644
Host* h = new Host();
pthread_cond_init(&h->cv, NULL);
h->irq_enabled = true;
@@ -854,3 +901,37 @@ extern "C" void OSWaitCond(OSCond* c, OSMutex* m)
@@ -871,3 +918,37 @@ extern "C" void OSWaitCond(OSCond* c, OSMutex* m)
extern "C" void OSSignalCond(OSCond* c) { OSWakeupThread(&c->queue); }
extern "C" double port_idle_seconds(void) { return g_idle_seconds; }
@@ -1572,10 +1601,10 @@ index e676983..335167f 100644
+ OSRestoreInterrupts(lvl);
+}
diff --git a/platform/pad/pad.cpp b/platform/pad/pad.cpp
index ecc299e..2a64638 100644
index c05f07f..930473b 100644
--- a/platform/pad/pad.cpp
+++ b/platform/pad/pad.cpp
@@ -68,7 +68,13 @@ __attribute__((weak)) int GXPC_GameInputBlocked(void);
@@ -69,7 +69,13 @@ __attribute__((weak)) int GXPC_GameInputBlocked(void);
// SMS_CAMERA_SPEED=percent scales manual camera rotation (100 = retail)
// SMS_FREE_CAMERA=1 the camera stays where it is put (no auto swing-back)
// SMS_MOUSE_SENSITIVITY=% mouse look speed, when the GX layer captures the mouse
@@ -1589,7 +1618,7 @@ index ecc299e..2a64638 100644
int port_free_camera = 0;
f32 port_camera_speed_x = 1.0f;
f32 port_camera_speed_y = 1.0f;
@@ -664,6 +670,12 @@ void init()
@@ -739,6 +745,12 @@ void init()
parse_bindings(kDefaultBindings, "defaults");
g_invert_cx = env_on("SMS_CAMERA_INVERT_X");
g_invert_cy = env_on("SMS_CAMERA_INVERT_Y");
@@ -1602,7 +1631,7 @@ index ecc299e..2a64638 100644
if (g_invert_cx || g_invert_cy)
port_log("[pad] camera inverted:%s%s\n", g_invert_cx ? " X" : "", g_invert_cy ? " Y" : "");
const char* path = getenv("SMS_BINDINGS");
@@ -736,8 +748,10 @@ void autopress_init()
@@ -811,8 +823,10 @@ void autopress_init()
g_auto.push_back(a);
}
// Headless runs have no SDL event loop: set the key state directly.
@@ -1613,7 +1642,7 @@ index ecc299e..2a64638 100644
port_log("[pad] SMS_AUTOPRESS: %u scripted presses via %s\n", (unsigned)g_auto.size(),
g_push ? "SDL_PushEvent (keyboard path)" : "direct key state");
}
@@ -909,6 +923,20 @@ extern "C" u32 PADRead(PADStatus* status)
@@ -1045,6 +1059,20 @@ extern "C" u32 PADRead(PADStatus* status)
return PAD_CHAN0_BIT;
}
@@ -1647,7 +1676,7 @@ index e3838ba..57faea6 100644
int port_vi_deterministic(void);
s64 port_vi_virtual_ticks(void);
diff --git a/platform/port_runtime.cpp b/platform/port_runtime.cpp
index e2ccfa4..30ad8c1 100644
index cecfc98..1951eca 100644
--- a/platform/port_runtime.cpp
+++ b/platform/port_runtime.cpp
@@ -73,6 +73,7 @@ int port_heat_haze = 1;
@@ -1750,6 +1779,80 @@ index e2ccfa4..30ad8c1 100644
if (p != MAP_FAILED)
return p;
#endif
diff --git a/platform/savestate/savestate.cpp b/platform/savestate/savestate.cpp
index 33a62af..fa5c45c 100644
--- a/platform/savestate/savestate.cpp
+++ b/platform/savestate/savestate.cpp
@@ -6,7 +6,9 @@
// after linking, so tools/savestate_ranges.py reads the link map and writes
// them, with a fingerprint of the build, into kRanges below (it finds the
// placeholder in the executable by its magic). A build without that table
-// saves and loads the portable part only.
+// saves and loads the portable part only. A position-independent build (the
+// PS5's) has the ranges at link addresses, and the table's own link address
+// beside them, so they slide by where the table was loaded.
//
// A state is saved and loaded between two frames (patch zzz-savestate-01), on
// the game's thread while it owns the CPU, with no disc read or interrupt
@@ -60,10 +62,12 @@ extern "C" __attribute__((weak)) const void* port_button_glyph(int chr);
// Filled in after linking (tools/savestate_ranges.py; keep the layout in
// step): the build's fingerprint and the game's writable data as [start, end)
// address ranges. count 0: this build cannot save or load exactly.
+// linkAddress: where the linker put this table, for a position-independent
+// build (savestate_ranges.py --pie); 0, the ranges are where they are.
struct PortSaveStateRanges {
char magic[16];
uint8_t fingerprint[32];
- uint32_t count, reserved;
+ uint32_t count, linkAddress;
uint64_t ranges[2048][2];
};
__attribute__((used)) static const volatile PortSaveStateRanges kRanges = { "SMSSTATE-RANGES", {}, 0, 0, {} };
@@ -120,11 +124,19 @@ uint32_t range_count()
return n <= sizeof kRanges.ranges / sizeof kRanges.ranges[0] ? n : 0;
}
+// Where range i is in this run.
+u8* range_start(uint32_t i)
+{
+ const uintptr_t slide = kRanges.linkAddress ? (uintptr_t)&kRanges - kRanges.linkAddress : 0;
+ return (u8*)(uintptr_t)(kRanges.ranges[i][0] + slide);
+}
+size_t range_size(uint32_t i) { return (size_t)(kRanges.ranges[i][1] - kRanges.ranges[i][0]); }
+
size_t globals_size()
{
size_t n = 0;
for (uint32_t i = 0; i < range_count(); i++)
- n += (size_t)(kRanges.ranges[i][1] - kRanges.ranges[i][0]);
+ n += range_size(i);
return n;
}
@@ -575,9 +587,8 @@ int port_savestate_save(const PortSaveStatePortable* portable)
job->aram.assign(aram, aram + aramSize);
job->globals.reserve(info.globalsSize);
for (uint32_t i = 0; i < range_count(); i++) {
- const u8* a = (const u8*)(uintptr_t)kRanges.ranges[i][0];
- const u8* b = (const u8*)(uintptr_t)kRanges.ranges[i][1];
- job->globals.insert(job->globals.end(), a, b);
+ const u8* a = range_start(i);
+ job->globals.insert(job->globals.end(), a, a + range_size(i));
}
}
port_log("[savestate] saving %s (%s)\n", job->path.c_str(), exact ? "exact" : "position only");
@@ -661,9 +672,8 @@ int port_savestate_load_exact(const char* path, PortSaveStatePortable* portable,
}
size_t at = 0;
for (uint32_t i = 0; i < range_count(); i++) {
- u8* start = (u8*)(uintptr_t)kRanges.ranges[i][0];
- const size_t len = (size_t)(kRanges.ranges[i][1] - kRanges.ranges[i][0]);
- memcpy(start, globals.data() + at, len);
+ const size_t len = range_size(i);
+ memcpy(range_start(i), globals.data() + at, len);
at += len;
}
for (int i = 0; i < n; i++)
diff --git a/platform/trace/trace_windows.cpp b/platform/trace/trace_windows.cpp
index 4067508..19ad775 100644
--- a/platform/trace/trace_windows.cpp
@@ -1788,3 +1891,164 @@ index 3a13700..239f209 100644
const s32 now = (s32)VIGetRetraceCount();
if (now - due > max_behind)
due = now;
diff --git a/tools/savestate_ranges.py b/tools/savestate_ranges.py
index 73aa40a..db2f7df 100644
--- a/tools/savestate_ranges.py
+++ b/tools/savestate_ranges.py
@@ -5,12 +5,16 @@
A save state copies the game's own global variables (platform/savestate), and
only the link map says where the linker put them. This reads the map (GNU ld
-on Linux and Windows, ld64 on macOS), keeps the writable sections that came
+on Linux and Windows, ld64 on macOS, lld on the PS5), keeps the writable sections that came
from the game's archives, merges the neighbours, and writes them with a
fingerprint of the build into the placeholder table in the executable (found by
its magic; kRanges in platform/savestate/savestate.cpp). The fingerprint is a
SHA-256 of the executable with the table blank, so two identical builds share
it and any other build differs. Running it again gives the same result.
+
+A position-independent executable (the PS5's) has its ranges at link
+addresses: --pie also records where the linker put the table, and the game
+slides the ranges by where it finds the table at run time.
"""
import argparse
@@ -118,6 +122,42 @@ def ld64_ranges(lines, archives):
return out
+LLD_LINE = re.compile(r"^\s*([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(\d+)( +)(\S.*)$")
+
+
+def lld_ranges(lines, archives):
+ """lld -Map: "VMA LMA Size Align Out In Symbol", the three last columns told
+ apart by their indent (an output section, then 8 more spaces for an input
+ section, 16 for a symbol)."""
+ out = []
+ section = None
+ for line in lines:
+ m = LLD_LINE.match(line.rstrip("\n"))
+ if not m:
+ continue
+ indent, text = len(m.group(5)), m.group(6)
+ if indent == 1:
+ section = text.split()[0]
+ elif indent == 9 and section and GNU_WRITABLE.match(section):
+ start, size = int(m.group(1), 16), int(m.group(3), 16)
+ if size and from_game(text, archives):
+ out.append([start, start + size])
+ return out
+
+
+def link_address(image, offset):
+ """The address an ELF64 file offset is loaded at (its PT_LOAD segment)."""
+ if image[:4] != b"\x7fELF" or image[4] != 2:
+ sys.exit("savestate_ranges: --pie needs a 64-bit ELF executable")
+ phoff, = struct.unpack_from("<Q", image, 0x20)
+ phentsize, phnum = struct.unpack_from("<HH", image, 0x36)
+ for i in range(phnum):
+ kind, _, p_offset, p_vaddr, _, p_filesz = struct.unpack_from("<IIQQQQ", image, phoff + i * phentsize)
+ if kind == 1 and p_offset <= offset < p_offset + p_filesz:
+ return p_vaddr + offset - p_offset
+ sys.exit("savestate_ranges: the range table is in no loaded segment")
+
+
def merge(ranges):
ranges = sorted(ranges)
merged = []
@@ -134,13 +174,16 @@ def main():
parser.add_argument("--map", required=True)
parser.add_argument("--exe", required=True)
parser.add_argument("--archive", action="append", default=[])
+ parser.add_argument("--pie", action="store_true", help="record the table's link address, to slide by at run time")
args = parser.parse_args()
archives = args.archive or ["libsms_game.a"]
with open(args.map, encoding="utf-8", errors="replace") as f:
lines = f.readlines()
ld64 = any(line.startswith("# Object files:") for line in lines[:50])
- ranges = merge(ld64_ranges(lines, archives) if ld64 else gnu_ranges(lines, archives))
+ lld = any(re.match(r"\s+VMA\s+LMA\s+Size\s+Align\s+Out\s+In\s+Symbol", line) for line in lines[:5])
+ ranges = merge(ld64_ranges(lines, archives) if ld64 else
+ lld_ranges(lines, archives) if lld else gnu_ranges(lines, archives))
if len(ranges) > MAX_RANGES:
sys.exit(f"savestate_ranges: {len(ranges)} ranges, more than the table's {MAX_RANGES}")
@@ -152,7 +195,10 @@ def main():
blank = bytes(TABLE_SIZE - len(MAGIC))
image[at + len(MAGIC):at + TABLE_SIZE] = blank
fingerprint = hashlib.sha256(image).digest()
- table = fingerprint + struct.pack("<II", len(ranges), 0)
+ table_address = link_address(image, at) if args.pie else 0
+ if table_address >= 1 << 32:
+ sys.exit("savestate_ranges: the range table's link address does not fit its field")
+ table = fingerprint + struct.pack("<II", len(ranges), table_address)
table += b"".join(struct.pack("<QQ", start, end) for start, end in ranges)
image[at + len(MAGIC):at + len(MAGIC) + len(table)] = table
diff --git a/tools/tests/test_savestate_ranges.py b/tools/tests/test_savestate_ranges.py
index 046a189..c1f72cb 100644
--- a/tools/tests/test_savestate_ranges.py
+++ b/tools/tests/test_savestate_ranges.py
@@ -1,7 +1,8 @@
#!/usr/bin/env python3
-"""tools/savestate_ranges.py: the game's data found in GNU ld and ld64 link maps."""
+"""tools/savestate_ranges.py: the game's data found in GNU ld, ld64 and lld link maps."""
import importlib.util
import pathlib
+import struct
import unittest
spec = importlib.util.spec_from_file_location(
@@ -52,6 +53,36 @@ LD64 = """\
<<dead>> \t0x00000008\t[ 2] _unused
"""
+# lld (the PS5's link): an output section, input sections 8 spaces further in,
+# symbols 16; sizes in hex without 0x
+LLD = """\
+ VMA LMA Size Align Out In Symbol
+ 0 0 ed7b30 16384 .text
+ 0 0 40 16 libsms_game.a(Main.cpp.o):(.text.main)
+ 0 0 0 1 main
+ 1adc000 1adc000 100 16 .data.rel.ro
+ 1adc000 1adc000 100 16 libsms_game.a(Vtables.cpp.o):(.data.rel.ro._ZTV1T)
+ 1da4240 1da4240 400 32 .data
+ 1da4240 1da4240 4 4 /b/libps5_platform.a(platform.c.o):(.data.mouse_handle)
+ 1da4240 1da4240 4 1 mouse_handle
+ 1da4244 1da4244 3c 4 libsms_game.a(Application.cpp.o):(.data.gpApplication)
+ 1da4244 1da4244 4 1 gpApplication
+ 1da4280 1da4280 20 8 libsms_game.a(Mario.cpp.o):(.data._ZN5TMario5sDataE)
+ 1df6658 1df6658 100 8 .bss
+ 1df6658 1df6658 30 8 libsms_game.a(J2DPrint.cpp.o):(.bss._ZN8J2DPrint6mMutexE)
+ 1df6688 1df6688 8 8 libsms_gx.a(gx_api.cpp.o):(.bss.s_state)
+"""
+
+
+def elf_with_segment(vaddr, p_offset):
+ """A 64-bit ELF header with one PT_LOAD segment, for link_address."""
+ image = bytearray(0x200)
+ image[:5] = b"\x7fELF\x02"
+ struct.pack_into("<Q", image, 0x20, 0x40) # e_phoff
+ struct.pack_into("<HH", image, 0x36, 56, 1) # e_phentsize, e_phnum
+ struct.pack_into("<IIQQQQ", image, 0x40, 1, 4, p_offset, vaddr, vaddr, 0x100)
+ return image
+
class RangesTest(unittest.TestCase):
archives = ['libsms_game.a', 'libsms_eclipse.a']
@@ -64,6 +95,14 @@ class RangesTest(unittest.TestCase):
found = ranges.merge(ranges.ld64_ranges(LD64.splitlines(True), self.archives))
self.assertEqual(found, [[0x200040, 0x200070], [0x300000, 0x300100]])
+ def test_lld_keeps_game_input_sections_in_writable_data(self):
+ found = ranges.merge(ranges.lld_ranges(LLD.splitlines(True), self.archives))
+ self.assertEqual(found, [[0x1da4244, 0x1da42a0], [0x1df6658, 0x1df6688]])
+
+ def test_link_address_of_a_file_offset(self):
+ image = elf_with_segment(0x1ec0000, 0x100)
+ self.assertEqual(ranges.link_address(image, 0x150), 0x1ec0050)
+
def test_archive_members_in_either_linker_spelling(self):
self.assertTrue(ranges.from_game('/b/libsms_game.a(X.o)', self.archives))
self.assertTrue(ranges.from_game('/b/libsms_game.a[3](X.o)', self.archives))
+1 -1
View File
@@ -15,7 +15,7 @@
"conceptId": "99312",
"contentBadgeType": 1,
"contentId": "UP9000-PPSA99312_00-SUNSHINE00000000",
"contentVersion": "00.008.000",
"contentVersion": "00.009.000",
"downloadDataSize": 256,
"gameIntent": {
"permittedIntents": [
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Fetches the texture tools' Python packages for this Python, without pip.
fetch-wheels.py DIR
Downloads Pillow and etcpak (and archspec, which etcpak imports) as wheels from PyPI, the ones built for this
Python and this machine, checks each against PyPI's SHA-256, and unpacks them
into DIR/<python version>, which it prints: put that on PYTHONPATH. A fresh
Ubuntu (or WSL) has neither pip nor venv, and a Mac's Command Line Tools have
Python 3.9, which newer Pillows no longer support. Run again, it does nothing.
SPDX-License-Identifier: MIT
"""
import hashlib, json, os, platform, re, sys, urllib.request, zipfile
# (name, version): Pillow 12 needs Python 3.10, so 3.9 gets the last that supports it
PACKAGES = [("archspec", "0.2.6"), ("etcpak", "0.9.15"), ("pillow", "12.3.0" if sys.version_info >= (3, 10) else "11.3.0")]
def compatible(filename):
"""A wheel this Python can import here: its CPython tag (or abi3 from an
older one) and its platform; or pure Python."""
if filename.endswith("-py3-none-any.whl"):
return True
m = re.match(r"[^-]+-[^-]+-(cp(\d)(\d+))-(cp\d+m?|abi3)-(.+)\.whl$", filename)
if not m:
return False
major, minor, abi, plats = int(m.group(2)), int(m.group(3)), m.group(4), m.group(5).split(".")
here = sys.version_info
if abi == "abi3":
if (major, minor) > (here.major, here.minor):
return False
elif (major, minor) != (here.major, here.minor):
return False
machine = platform.machine().lower()
for plat in plats:
if sys.platform == "darwin":
want = "arm64" if machine == "arm64" else "x86_64"
if plat.startswith("macosx_") and (plat.endswith("_" + want) or plat.endswith("_universal2")):
return True
elif sys.platform.startswith("linux"):
want = {"arm64": "aarch64"}.get(machine, machine)
g = re.match(r"manylinux_(\d+)_(\d+)_(.+)$", plat) or re.match(r"manylinux(2014)_(.+)$", plat)
if not g:
continue
if g.re.pattern.startswith("manylinux(2014)"):
need, arch = (2, 17), g.group(2)
else:
need, arch = (int(g.group(1)), int(g.group(2))), g.group(3)
libc = os.confstr("CS_GNU_LIBC_VERSION").split()[-1].split(".") if hasattr(os, "confstr") else ["0", "0"]
if arch == want and (int(libc[0]), int(libc[1])) >= need:
return True
return False
def main():
root = os.path.join(sys.argv[1], "python%d.%d" % sys.version_info[:2])
done = os.path.join(root, ".done-" + "-".join(n + v for n, v in PACKAGES))
if not os.path.exists(done):
os.makedirs(root, exist_ok=True)
for name, version in PACKAGES:
with urllib.request.urlopen(f"https://pypi.org/pypi/{name}/{version}/json") as r:
files = json.load(r)["urls"]
wheel = next((f for f in files if compatible(f["filename"])), None)
if not wheel:
sys.exit(f"fetch-wheels.py: no {name} {version} for Python {platform.python_version()} on "
f"{sys.platform} {platform.machine()}")
print(f"==> fetching {wheel['filename']}", file=sys.stderr)
with urllib.request.urlopen(wheel["url"]) as r:
data = r.read()
if hashlib.sha256(data).hexdigest() != wheel["digests"]["sha256"]:
sys.exit(f"fetch-wheels.py: {wheel['filename']} did not download correctly; try again")
path = os.path.join(root, wheel["filename"])
with open(path, "wb") as f:
f.write(data)
with zipfile.ZipFile(path) as z:
for member in z.infolist():
z.extract(member, root)
# a zip keeps the executable bit in its Unix attributes; extract() drops it
mode = member.external_attr >> 16
if mode:
os.chmod(os.path.join(root, member.filename), mode)
os.remove(path)
open(done, "w").close()
print(root)
if __name__ == "__main__":
main()
+19 -4
View File
@@ -123,7 +123,7 @@ done
radv_link_flags+=(--version-script "$work/link/game-local.map")
"$sdk/bin/prospero-lld" "${radv_linker_script[@]}" --eh-frame-hdr "${radv_link_flags[@]}" \
--version-script "$native/app-symbols.map" --exclude-libs=ALL \
-e _start -o "$work/link/llvm-pie.elf" \
-e _start -o "$work/link/llvm-pie.elf" -Map="$work/link/sms.map" \
-u ps5_agc_gate2_run \
"$work/link/obj/app_crt.o" --start-group "${objects[@]}" "${experimental[@]}" \
-L "$opengl/lib" -lPS5OpenGL --end-group \
@@ -148,7 +148,8 @@ null_imports=$(comm -23 \
awk '$1 == "U" { sub(/@.*/, "", $2); print $2 }' | sort -u) \
<(for library in "$sdk"/target/lib/*.so "$work/link/stubs/libSceAgc.so" "$work/link/stubs/libSceAgcDriver.so" \
"$work/link/stubs/libSceMouse.so"; do
case ${library##*/} in libkernel_sys.so | libScePosixForWebKit.so) continue ;; esac
# (an if, not a case: a Mac's bash 3.2 misreads a case pattern's ")" inside $( ))
if [[ ${library##*/} == libkernel_sys.so || ${library##*/} == libScePosixForWebKit.so ]]; then continue; fi
"$sdk/bin/llvm-nm" -D --defined-only "$library" 2>/dev/null | awk '{ print $NF }'
done | sort -u))
if [[ -n $null_imports ]]; then
@@ -156,6 +157,14 @@ if [[ -n $null_imports ]]; then
echo "link-title.sh: bind them to the platform layer (PS5_Vulkan's tools/radv-link.sh)" >&2
exit 1
fi
# Save states copy the game's own global variables: the port's tool reads where
# the link put them from the map and writes them into the ELF, at link addresses
# the game slides by where it was loaded (--pie). Without it, states keep only
# the position.
if [[ -n ${SMS_SAVESTATE_RANGES:-} ]]; then
python3 "$SMS_SAVESTATE_RANGES" --pie --map "$work/link/sms.map" --exe "$work/link/llvm-pie.elf" \
--archive libsms_game.a --archive libsms_eclipse.a
fi
"$tool" link --in "$work/link/llvm-pie.elf" --out "$work/eboot.elf.new" \
--stub-dir "$sdk/target/lib" --stub "$work/link/stubs/libSceAgc.so" \
--stub "$work/link/stubs/libSceAgcDriver.so" --stub "$work/link/stubs/libSceMouse.so" --module-sdk 0x02000009 \
@@ -171,14 +180,20 @@ cp "$param" "$app/sce_sys/param.json"
for asset in icon0.png pic0.dds pic1.dds snd0.at9; do
[[ -f $ps5/sce_sys/$asset ]] && cp "$ps5/sce_sys/$asset" "$app/sce_sys/$asset"
done
(cd "$vulkan/runtime" && sha256sum --check --strict --quiet libc.prx.sha256)
# (Python, not sha256sum: the builder kit runs this on a Mac too)
python3 - "$vulkan/runtime/libc.prx" "$vulkan/runtime/libc.prx.sha256" <<'CHECK'
import hashlib, sys
want = open(sys.argv[2]).read().split()[0]
if hashlib.sha256(open(sys.argv[1], "rb").read()).hexdigest() != want:
sys.exit("link-title.sh: libc.prx does not match libc.prx.sha256")
CHECK
cp "$vulkan/runtime/libc.prx" "$app/sce_module/libc.prx"
mv "$work/eboot.elf.new" "$work/eboot.elf"
# The linked ELF of the title last packaged, for tools/run.sh to symbolise a crash with
# (the game's build tree may be on the VM's own disk)
mkdir -p "$root/build"
cp "$work/link/llvm-pie.elf" "$root/build/last-title.elf"
printf '==> %s: %s (eboot.bin %s bytes)\n' "$title_id" "$app" "$(stat -c %s "$app/eboot.bin")"
printf '==> %s: %s (eboot.bin %s bytes)\n' "$title_id" "$app" "$(wc -c < "$app/eboot.bin" | tr -d ' ')"
# The licences beside eboot.bin
mkdir -p "$app/licenses"
+37 -3
View File
@@ -72,11 +72,11 @@ cp "$ps5/stubs/libSceMouse_stub.c" "$kit/ps5/stubs/"
cp "$ps5/cmake/ps5-kit-game.cmake" "$kit/ps5/cmake/"
cp "$ps5/patches/sms-pc-port.patch" "$ps5/patches/sdl.patch" "$kit/ps5/patches/"
cp "$ps5/opengl/build-runtime.sh" "$kit/ps5/opengl/"
cp "$ps5/kit/build-title.sh" "$ps5/kit/make-title.sh" "$ps5/kit/Dockerfile" "$kit/"
cp "$ps5/kit/build-title.sh" "$ps5/kit/make-title.sh" "$ps5/kit/tools.sh" "$ps5/kit/Dockerfile" "$kit/"
cp "$ps5/kit/README.md" "$kit/README.md"
chmod +x "$kit/build-title.sh" "$kit/make-title.sh"
mkdir -p "$kit/tools"
cp "$ps5/tools/shrink-texture-pack.py" "$ps5/tools/get-texture-pack.py" "$kit/tools/"
cp "$ps5/tools/shrink-texture-pack.py" "$ps5/tools/get-texture-pack.py" "$ps5/tools/fetch-wheels.py" "$kit/tools/"
cp "$ps5/kit/make-texture-pack.sh" "$kit/"
chmod +x "$kit/make-texture-pack.sh"
# the revisions build-title.sh fetches: the ones this build was made from
@@ -106,6 +106,37 @@ cp "$vulkan"/tooling/native/*.cpp "$vulkan"/tooling/native/*.hpp "$vulkan"/tooli
"$vulkan"/tooling/native/*.ld "$kit/vulkan/tooling/native/"
cp "$vulkan/runtime/libc.prx" "$vulkan/runtime/libc.prx.sha256" "$vulkan/runtime/README.md" "$kit/vulkan/runtime/"
# ---- the fake-SELF tool built for Linux, static, so a Linux (or WSL) needs no C++
# compiler: x86_64 and arm64, with zlib compiled in. A Mac builds its own from the
# source above (its Command Line Tools have a compiler and zlib).
zlib_version=1.3.2
zlib="$root/.deps/zlib-$zlib_version"
if [[ ! -f $zlib/zlib.h ]]; then
mkdir -p "$root/.deps"
curl -fsSL -o "$zlib.tar.gz" "https://github.com/madler/zlib/releases/download/v$zlib_version/zlib-$zlib_version.tar.gz"
echo "bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16 $zlib.tar.gz" | sha256sum --check --quiet
tar -xzf "$zlib.tar.gz" -C "$root/.deps"
rm -f "$zlib.tar.gz"
fi
for target in x86_64:x86_64-linux-gnu- aarch64:aarch64-linux-gnu-; do
arch=${target%%:*} prefix=${target#*:}
# the host's own compiler for its own processor
[[ $(uname -m) == "$arch" ]] && prefix=
command -v "${prefix}g++" > /dev/null ||
{ echo "package-kit.sh: ${prefix}g++ is missing (apt install g++-${arch//_/-}-linux-gnu)" >&2; exit 2; }
objects="$build/native-tool-$arch"
rm -rf "$objects"
mkdir -p "$objects" "$kit/vulkan/prebuilt/Linux-$arch"
for file in adler32 crc32 deflate inflate inffast inftrees trees zutil; do
"${prefix}gcc" -O2 -c "$zlib/$file.c" -o "$objects/$file.o"
done
"${prefix}g++" -std=c++20 -O2 -static -I"$zlib" "$vulkan"/tooling/native/{native_app_builder,self_container,elf_object,sce_module_writer}.cpp \
"$objects"/*.o -o "$kit/vulkan/prebuilt/Linux-$arch/ps5-native-tool"
"${prefix}strip" "$kit/vulkan/prebuilt/Linux-$arch/ps5-native-tool"
rm -rf "$objects"
done
cp "$zlib/LICENSE" "$kit/vulkan/prebuilt/zlib-LICENSE.txt"
# ---- the PS5 OpenGL SDK: the driver's archive and the console's two GPU import stubs
mkdir -p "$kit/opengl/sdk/lib"
cp "$opengl/../LICENSE" "$opengl/../THIRD_PARTY_NOTICES.md" "$kit/opengl/"
@@ -139,6 +170,7 @@ own_revision=$(git -C "$root" rev-parse HEAD 2>/dev/null || echo "(this kit's ow
echo "PS5_Vulkan https://github.com/mihawk-99/PS5_Vulkan $(git -C "$vulkan" rev-parse HEAD) (its link recipe, CRT and native tool; no Vulkan driver is linked)"
echo "SDL2 https://github.com/libsdl-org/SDL $sdl_revision (+ ps5/patches/sdl.patch and the console's drivers)"
echo "Clang builtins https://github.com/llvm/llvm-project $("$PS5_CLANG" --version | head -1)"
echo "zlib https://github.com/madler/zlib $zlib_version (in vulkan/prebuilt's Linux tools)"
echo
echo "LICENSING.md says which licence covers which part."
} > "$kit/SOURCES.txt"
@@ -149,7 +181,9 @@ leaks=$(find "$kit" \( -name 'libsms_game*' -o -path '*/sms_game.dir/*' -o -path
-o -iname '*.iso' -o -iname '*.gcm' -o -iname '*.ciso' -o -iname '*.rvz' -o -iname '*.gci' -o -iname '*.szs' -o -iname '*.thp' \
-o -iname '*.dol' -o -iname '*.arc' \) -print)
[[ -z $leaks ]] || { echo "package-kit.sh: these must not be in a kit:" >&2; echo "$leaks" >&2; exit 1; }
(cd "$root/dist/kit" && rm -f "$name.zip" && zip -qry "$name.zip" "$name")
# (the files symlinks point to, not the links: Windows' own unzip, and Python's,
# make a link a file holding its target's name, and the SDK has header links)
(cd "$root/dist/kit" && rm -f "$name.zip" && zip -qr "$name.zip" "$name")
printf '==> %s (%s unpacked, %s zipped)\n' "$root/dist/kit/$name.zip" \
"$(du -sh "$kit" | cut -f1)" "$(du -sh "$root/dist/kit/$name.zip" | cut -f1)"
sha256sum "$root/dist/kit/$name.zip"