3 Commits
Author SHA1 Message Date
Gary Riches e01f5db170 README: nothing left of the builder kit
Claude-Session: https://claude.ai/code/session_016xMYBaR9GfxtETyVf6er1P
2026-10-14 10:28:35 +01:00
Gary Riches 768a56cc43 0.10.0: a built title instead of a builder kit; the HD pack halved on the console
The game's code is compiled from the decompilation, as in the other
decompilation ports this workspace releases, so the release is the built
title folder (ps5/tools/package-release.sh) with SOURCE.txt and the
licences. The builder kit and the PC texture tools are gone.

SMS_TEXTURE_PACK_MAX_SIZE (3072 on the PS5) halves a pack image longer
than that until it fits, from the file's own levels where it has them,
else by nextBlockLevel, kept on disk as <name>.top so each large file is
read once. The UHD pack now goes in as published: Delfino Plaza loads in
8.9 s the first time and 5.8 s after, at 60 fps.

Claude-Session: https://claude.ai/code/session_016xMYBaR9GfxtETyVf6er1P
2026-10-14 10:22:13 +01:00
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
19 changed files with 771 additions and 932 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 -15
View File
@@ -9,7 +9,7 @@ header, and may also be used under that licence alone.
| --- | --- | --- |
| `ps5/tools/png-to-title-dds.py` (encoder from PS5 RetroArch, by Mihawk) | GPL-3.0-or-later | [`licenses/GPL-3.0.txt`](licenses/GPL-3.0.txt) |
| `ps5/src/platform.c`, `ps5/src/platform.h` and the tools taken from the PS5 Vulkan Template (by Mihawk) | MIT | [`licenses/MIT.txt`](licenses/MIT.txt) |
| The rest of `ps5/src/`, `ps5/tools/`, `ps5/cmake/`, `ps5/kit/`, `ps5/opengl/`, `ps5/probe/` and `ps5/art/` (written for this port) | MIT | [`licenses/MIT.txt`](licenses/MIT.txt) |
| The rest of `ps5/src/`, `ps5/tools/`, `ps5/cmake/`, `ps5/opengl/`, `ps5/probe/` and `ps5/art/` (written for this port) | MIT | [`licenses/MIT.txt`](licenses/MIT.txt) |
| `ps5/sdl/` (SDL2 drivers for the console) and `ps5/patches/sdl.patch` | Zlib, as SDL | [`licenses/Zlib.txt`](licenses/Zlib.txt) |
| `ps5/patches/sms-pc-port.patch` | MIT, as sms-pc-port | [`licenses/MIT.txt`](licenses/MIT.txt) |
| `ps5/art/kenney/` and the button prompts in `ps5/res/prompts/` made from it (Kenney's input prompts) | CC0-1.0 | in `ps5/art/kenney/` |
@@ -29,26 +29,21 @@ project, under that project's licence.
| 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 |
## A built title is not for distribution
## The release
A built `eboot.bin` holds the game's own code, compiled from the
decompilation. Whatever the licences above say about the rest, and whatever
licence the decompilation is published under, that code is a reconstruction of
Nintendo's program: a built title stays on the machine that made it.
The builder kit (`ps5/tools/package-kit.sh`) is what is shared: the same build
without that part. It holds compiled forms of the projects above, under their
licences, with `SOURCES.txt` naming each one's repository and revision, and
nothing of the game. Whoever uses it compiles the game's code on their own
machine, from the public sources, and plays it with their own disc image.
A release (`ps5/tools/package-release.sh`) is the built title folder: its
`eboot.bin` links the parts above and is distributed under GPL-3.0 as a whole,
with `SOURCE.txt` naming the repository and revision of each part and
`licenses/` holding their licence texts. The game's code in it is compiled from
the decompilation; nothing from a disc is in it.
## What is not covered
- **The game.** Super Mario Sunshine, its disc image and everything on it
belong to Nintendo. None of it is in this repository or in a kit, and no
licence above covers it.
belong to Nintendo. None of it is in this repository or in a release, and no
licence above covers it; the title plays your own disc image.
- **Texture packs.** The port can load Dolphin-format texture packs. None is in
this repository or in a kit; each belongs to its authors.
this repository or in a release; each belongs to its authors.
- **Artwork.** `ps5/art/icon0-source.png` and `ps5/art/pic-source.png` (a harbour
town on a sunny island), and the `ps5/sce_sys/icon0.png`, `pic0.dds` and
`pic1.dds` made from them, are the port's own: made for it by Gary Riches with
+127 -30
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).**
@@ -24,14 +24,15 @@ drawn with OpenGL through the [PS5 OpenGL SDK](https://github.com/blackbearreloa
(Mesa over the console's own GPU interface).
It needs a PS5 that already runs homebrew, and **your own disc image of the
game** (North America, `GMSE01`). No game code, game data, Nintendo artwork,
Sony software or keys are included here.
game** (North America, `GMSE01`): the title is compiled from the
decompilation and reads everything else (models, levels, sound, movies) from
your disc. No game data, Nintendo artwork, Sony software or keys are included.
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.10.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 +40,118 @@ 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.
## How to get it: the builder kit
New in 0.10.0: a ready-made title (copy it over and add your disc), and the HD
texture pack goes in as its authors publish it, with the console sizing its
largest images itself.
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.
## Installing
1. Download `sunshine-ps5-kit-<version>.zip` from the
You need a PS5 that runs homebrew, with etaHEN (or an equivalent),
ShadowMountPlus (which shows folders in `/data/homebrew` as games) and an FTP
server, and **your own disc image** of the North American release (`GMSE01`,
revision 0) as `.iso`, `.gcm` or Dolphin's `.ciso`. Other regions are not
supported.
1. Download `Sunshine-PS5-v<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`.
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`.
2. With an FTP program (FileZilla, Cyberduck, WinSCP), copy the **whole
`PPSA99312` folder** into `/data/homebrew/` on the console.
3. Copy your disc image 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.
```text
/data/homebrew/PPSA99312/eboot.bin
/data/homebrew/PPSA99312/sce_sys/
/data/homebrew/PPSA99312/sce_module/
/data/homebrew/PPSA99312/res/
/data/homebrew/PPSA99312/licenses/
/data/homebrew/PPSA99312/GMSE01.iso (your disc image; any name)
```
**Never share the folder the kit makes, or its `eboot.bin`.**
"Super Mario Sunshine" appears on the home screen. If it does not, check the
folder is exactly `/data/homebrew/PPSA99312/` with `eboot.bin` directly inside
it (not `PPSA99312/PPSA99312/`), and that ShadowMountPlus is running. If it
starts and closes at once, the disc image is missing or is not the North
American one.
To update, copy the new release's `PPSA99312` folder over the old one. Your
saves, settings, disc image and texture packs are left alone.
## Playing
| DualSense | In the game |
| --- | --- |
| Cross | A (jump, confirm) |
| Circle | B (dive, back) |
| 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** |
| Sticks, D-pad | as on a GameCube |
The prompts the game shows on screen are the DualSense's own.
**Settings.** Press the touch pad's button at any time. The D-pad moves and
changes, Cross steps through a row's choices, Circle or the touch pad closes it.
| Row | What it does |
| --- | --- |
| Invert FLUDD Aim X / Y | flips the left stick while you aim FLUDD |
| Invert Camera X / Y | flips the right stick's camera |
| Camera Shake | OFF, HALF or FULL: the game's earthquakes (off to begin with) |
| Performance Overlay | OFF, MINIMAL (the frame rate alone) or FULL |
| Heat Haze | the heat shimmer |
| Resolution \* | 960p, 1440p, 1920p or 2160p |
| Frame Rate \* | 30 or 60 |
| Picture \* | 4:3 or 16:9 |
| Smoothing (FXAA) \* | on or off |
Rows marked \* take effect the next time you start the game. Below them are
the PC port's cheats (god mode, infinite lives and so on). Saves and settings
are kept 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 in `userdata/savestates`. A state loads
exactly only in the release that saved it; with a newer release, older states put
Mario back where he was in that level and keep your progress, but the level
starts afresh.
## HD textures (optional)
The port loads texture packs made for the Dolphin emulator. The one it was
tested with is the
[Super Mario Sunshine UHD Texture Pack](https://github.com/qashto/Super_Mario_Sunshine_UHD_Texture_Pack)
(by qashto and razius), with chasem-dev's
[HD texture extras](https://github.com/chasem-dev/sms-hd-texture-extras):
1. Download `GMS.7z` from the UHD pack's
[release 2.1.1](https://github.com/qashto/Super_Mario_Sunshine_UHD_Texture_Pack/releases/tag/2.1.1)
and `sms-hd-texture-extras-1.1.1.zip` from the extras'
[release 1.1.1](https://github.com/chasem-dev/sms-hd-texture-extras/releases/tag/v1.1.1).
2. Unpack both on your computer (7-Zip on Windows; a Mac opens the `.7z` with
The Unarchiver or `tar -xf GMS.7z`).
3. Copy them to the console so that you have:
```text
/data/homebrew/PPSA99312/mods/textures/GMS/ (the GMS folder from GMS.7z's Textures folder)
/data/homebrew/PPSA99312/mods/textures/sms-hd-texture-extras/ (the extras' zip, unpacked)
```
The packs are not part of this project and are not stored by it: they belong
to their authors.
**The first visit to each area is slower.** The UHD pack's largest images
(4096 to 16384 pixels) are more than the console's graphics driver can hold,
so the title halves each one to 2048 pixels or less the first time it is used
and keeps the result in `userdata/.cache`. After that, loads read the small
copy. Deleting that folder only means the work is done again.
## 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.
@@ -70,27 +161,28 @@ read in the title's log. Nothing else is claimed.
| Start-up | The logos, the opening movie, the title screen, file select; a saved file loaded | A new game from an empty memory card |
| Rendering | 3840x2160; Delfino Plaza and Bianco Hills episode 1 look right; the plaza's gates show their movie of the level beyond | Everything past Bianco Hills episode 1; a comparison with a GameCube |
| Frame rate | A locked 60 frames a second (16.7 ms) in the plaza and Bianco Hills, at 2160p, with the HD texture pack | Later, heavier areas; a long session |
| Loading | Delfino Plaza in about 5 seconds with the console-sized HD pack; a "LOADING..." notice during long loads | |
| Loading | Delfino Plaza in about 6 seconds with the HD pack (9 the first time, while its largest images are halved); a "LOADING..." notice during long loads | |
| 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 |
| Texture packs | The UHD pack and the HD extras as published, their largest images halved on the console (see below) | Other packs |
| Memory | Two levels and back without running out, with the HD pack at the sizes the console uses | Bianco Hills since the console does the halving itself; many levels in one session |
### Known problems
- **Only the start of the game has been played.** Anything from Bianco Hills
episode 2 onward is untested on a PS5.
- **Large texture packs must be shrunk.** The console's driver needs about ten
times a texture's size in memory. The UHD pack as published made loads take
17 seconds and ran the console out of memory in the second level. The kit
makes a console-sized copy for you.
- **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 release 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 release that saved it: in a newer one 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 +208,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,11 +226,11 @@ 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 # the revision SOURCE.txt names
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/
ps5/tools/package-kit.sh # the builder kit, dist/kit/
ps5/tools/build.sh # applies the patch, builds dist/PPSA99312/
ps5/tools/package-release.sh # the release archive, dist/Sunshine-PS5-v<version>.zip
```
`ps5/tools/setup-source.sh --save` writes changes made under
+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
-29
View File
@@ -1,29 +0,0 @@
# Super Mario Sunshine for the PlayStation 5 - the builder kit's share of the build: the game's code alone.
#
# kit/build-title.sh configures the PC port (fetched and patched on the machine
# that runs it) with the payload SDK's toolchain and this file as
# CMAKE_PROJECT_sms_port_INCLUDE, then builds the one target sms_game: the
# archive of the game's own code, which is the part a kit cannot bring. The
# flags are the ones ps5-game.cmake gives the full build, so the archive is the
# one the kit's other objects were linked with when it was tested.
#
# SPDX-License-Identifier: MIT
if(NOT PS5_KIT_DIR)
message(FATAL_ERROR "ps5-kit-game.cmake: PS5_KIT_DIR is not set (use the kit's build-title.sh)")
endif()
add_compile_options(-ffunction-sections -fdata-sections -fno-omit-frame-pointer)
set(CMAKE_C_COMPILER_LAUNCHER sh ${PS5_KIT_DIR}/ps5/tools/cc-init-array.sh)
set(CMAKE_CXX_COMPILER_LAUNCHER sh ${PS5_KIT_DIR}/ps5/tools/cc-init-array.sh)
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
set(BUILD_SHARED_LIBS OFF)
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
set(SMS_ARCH 64 CACHE STRING "" FORCE)
set(SMS_GX_BUILD_TESTS OFF CACHE BOOL "" FORCE)
# The port's other targets are configured but never built here: SDL2 only has
# to exist by name for them.
if(NOT TARGET SDL2::SDL2)
add_library(SDL2::SDL2 INTERFACE IMPORTED GLOBAL)
endif()
-25
View File
@@ -1,25 +0,0 @@
# Super Mario Sunshine for the PlayStation 5 - the builder kit's container: a Linux with what
# build-title.sh needs (LLVM 22, CMake, Ninja, git, Python 3, a host C++ compiler) and the kit.
#
# docker build -t sunshine-ps5-kit .
# docker run --rm -v "$PWD/out":/out sunshine-ps5-kit /out
#
# make-title.sh does both. Nothing of the game is in the image: its source code
# is fetched when the container runs, and what it makes goes to /out.
#
# SPDX-License-Identifier: MIT
FROM ubuntu:24.04
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates wget git patch python3 g++ zlib1g-dev cmake ninja-build \
python3-pip python3-pil 7zip \
&& pip3 install --break-system-packages --no-cache-dir etcpak \
&& wget -qO /etc/apt/trusted.gpg.d/apt.llvm.org.asc https://apt.llvm.org/llvm-snapshot.gpg.key \
&& echo "deb https://apt.llvm.org/noble/ llvm-toolchain-noble-22 main" > /etc/apt/sources.list.d/llvm.list \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends clang-22 lld-22 llvm-22 \
&& rm -rf /var/lib/apt/lists/*
COPY . /kit
RUN bash /kit/build-title.sh --tools
WORKDIR /out
ENTRYPOINT ["bash", "/kit/build-title.sh"]
-243
View File
@@ -1,243 +0,0 @@
# Super Mario Sunshine for the PlayStation 5: builder kit
This kit builds a native PS5 version of Super Mario Sunshine (GameCube) on
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.
> **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
> it on your machine, and you supply the disc image. What the kit makes is
> yours alone: **never share the folder it produces, or its `eboot.bin`.**
---
## Before you start
You need three things.
**1. A PS5 that runs homebrew**, with:
- etaHEN (or an equivalent)
- ShadowMountPlus, which shows folders in `/data/homebrew` as games
- an FTP server running on the console
**2. Your disc image of the game.** The North American release (`GMSE01`,
revision 0), as `.iso`, `.gcm` or Dolphin's `.ciso`. Other regions are not
supported.
**3. A computer** (Windows, macOS or Linux) with:
- [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
---
## Step 1: Unzip the kit
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
Open Docker Desktop and wait until it says it is running.
## Step 3: Build the title
Open a terminal **in the kit's folder** and run the command for your system.
**macOS or Linux**
```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.)
### 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 |
| `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 |
| `linking` | Joins it to the kit's parts and makes `eboot.bin` | under a minute |
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).
With an FTP program (FileZilla, Cyberduck, WinSCP), connect to your PS5 and
copy that **whole folder** into `/data/homebrew/`. Then copy **your disc
image** into it, beside `eboot.bin`, so that you end up with:
```text
/data/homebrew/PPSA99312/eboot.bin
/data/homebrew/PPSA99312/sce_sys/
/data/homebrew/PPSA99312/sce_module/
/data/homebrew/PPSA99312/res/
/data/homebrew/PPSA99312/licenses/
/data/homebrew/PPSA99312/GMSE01.iso (your disc image; any name)
```
## Step 5: Play
"Super Mario Sunshine" appears on the PS5's home screen. Start it like any game.
| DualSense | In the game |
| --- | --- |
| Cross | A (jump, confirm) |
| Circle | B (dive, back) |
| Square | X |
| Triangle | Y (first-person view) |
| R1 | Z |
| L2, R2 | L, R (R sprays FLUDD) |
| Options | Start (pause, map) |
| Touch pad button | **Settings** |
| Sticks, D-pad | as on a GameCube |
The prompts the game shows on screen are the DualSense's own.
### The settings menu
Press the touch pad's button at any time. The D-pad moves and changes, Cross
steps through a row's choices, Circle or the touch pad closes it.
| Row | What it does |
| --- | --- |
| Invert FLUDD Aim X / Y | flips the left stick while you aim FLUDD |
| Invert Camera X / Y | flips the right stick's camera |
| Camera Shake | OFF, HALF or FULL: the game's earthquakes (off to begin with) |
| Performance Overlay | OFF, MINIMAL (the frame rate alone) or FULL |
| Heat Haze | the heat shimmer |
| Resolution \* | 960p, 1440p, 1920p or 2160p |
| Frame Rate \* | 30 or 60 |
| Picture \* | 4:3 or 16:9 |
| Smoothing (FXAA) \* | on or off |
Rows marked \* take effect the next time you start the game. Below them are
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/`.
---
## Optional: HD textures
The port loads texture packs made for the Dolphin emulator. One command
downloads the [Super Mario Sunshine UHD Texture Pack](https://github.com/qashto/Super_Mario_Sunshine_UHD_Texture_Pack)
(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
```
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:
```text
/data/homebrew/PPSA99312/mods/textures/
```
The packs are not part of this kit and are not stored with it: they belong to
their authors, and the copy you make is for your own use.
**Why a console-sized copy?** The console's graphics driver needs about ten
times a texture's size in memory and takes texture data slowly. With the pack
as published (images up to 8192 pixels), Delfino Plaza took 17 seconds to load
and a second level ran the console out of memory. With nothing larger than
2048 pixels the plaza loads in about 5 seconds. Any other Dolphin pack can be
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.
**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:
```sh
JOBS=4 ./make-title.sh
```
On Windows, add `-e JOBS=4` after `docker run --rm`.
**It was interrupted**
Run the same command again. It carries on from where it stopped.
**The game does not appear on the PS5**
Check the folder is exactly `/data/homebrew/PPSA99312/` with `eboot.bin`
directly inside it (not `PPSA99312/PPSA99312/`), and that ShadowMountPlus is
running.
**It starts and closes at once**
The disc image is missing, or is not the North American release. It must be
in `/data/homebrew/PPSA99312/`, beside `eboot.bin`.
## Updating to a newer kit
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)
If you already have LLVM 22 (clang, lld, llvm tools), CMake, Ninja, git,
Python 3, `patch`, a C++ compiler and the zlib development headers:
```sh
./build-title.sh
```
---
## What is in the kit
| Path | What |
| --- | --- |
| `make-title.sh`, `Dockerfile` | Runs the build in a container. |
| `build-title.sh` | The build itself: fetch, patch, compile, link. |
| `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). |
| `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. |
| `REVISIONS`, `SOURCES.txt`, `LICENSING.md`, `licenses/` | What the build fetches, where each part's source is, and its licence. |
The project, with what works, what does not, and how the port is made:
<https://github.com/griches/PS5_Sunshine>
This project is not affiliated with, endorsed or sponsored by Nintendo or Sony
Interactive Entertainment.
-140
View File
@@ -1,140 +0,0 @@
#!/usr/bin/env bash
# Super Mario Sunshine for the PlayStation 5 - the builder kit: make the title on your own machine.
#
# build-title.sh [OUT] OUT/PPSA99312/ (OUT defaults to ./out)
# build-title.sh --tools only build the kit's host tools (the container's image does)
#
# The kit is a build of the port without the game's own code. This script
# fetches the two public source trees that code is compiled from (the PC port,
# chasem-dev/sms-pc-port, and the decompilation it builds, chasem-dev/sms-english,
# both at the revisions SOURCES.txt names), applies the console's patch,
# compiles the game's code with the kit's SDK and links it with the kit's
# objects, by the recipe the kit was tested with. Nothing of the game's data is
# 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.
#
# JOBS compilers at once (default: one a processor)
# PS5_CLANG the clang to use (default: /usr/lib/llvm-22/bin/clang, or clang-22)
#
# SPDX-License-Identifier: MIT
set -euo pipefail
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; }
build_tools() {
local host_cxx=${CXX:-c++} native="$vulkan/tooling/native"
if [[ ! -x $vulkan/build/host/ps5-native-tool ]]; then
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"
fi
}
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
out=${1:-$PWD/out}
mkdir -p "$out"
out=$(cd -- "$out" && pwd)
work="$out/.work"
jobs=${JOBS:-$(nproc)}
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")
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
export LLVM_CONFIG="$llvm_bin/llvm-config"
cat > "$work/bin/clang" <<WRAPPER
#!/bin/sh
if [ "\$#" -eq 1 ] && [ "\$1" = "--print-resource-dir" ]; then echo "$kit/clang-rt"; exit 0; fi
exec "$real_clang" "\$@"
WRAPPER
chmod +x "$work/bin/clang"
export PS5_CLANG="$work/bin/clang"
for tool in git cmake ninja patch python3; do
command -v "$tool" > /dev/null || fail "$tool was not found"
done
build_tools
# ---- the sources of the game's code, at the revisions the kit was built from
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)"
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"
git init -q "$source"
git -C "$source" remote add origin "$port_url"
git -C "$source" fetch -q --depth 1 origin "$port_rev"
git -C "$source" -c advice.detachedHead=false checkout -q FETCH_HEAD
git -C "$source" submodule update -q --init --depth 1 decomp
[[ $(git -C "$source/decomp" rev-parse HEAD) == "$decomp_rev" ]] ||
fail "the decompilation is at $(git -C "$source/decomp" rev-parse HEAD), not $decomp_rev"
say "applying the console's patch"
git -C "$source" apply --whitespace=nowarn "$kit/ps5/patches/sms-pc-port.patch"
echo "$stamp" > "$work/.source-stamp"
fi
# ---- the game's code
build="$work/game"
if [[ ! -f $build/build.ninja ]]; then
say "configuring"
mkdir -p "$build"
cmake -S "$source" -B "$build" -G Ninja \
-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"; }
fi
say "compiling the game's code, $jobs files at a time (this is the long step)"
ninja -C "$build" -j "$jobs" sms_game
[[ -f $build/libsms_game.a ]] || fail "the game's code did not build"
# ---- the link: the kit's objects in the order they were tested in, with that archive
say "linking"
inputs=()
while IFS= read -r line; do
case $line in
@GAMECODE@) inputs+=("$build/libsms_game.a") ;;
-*) inputs+=("$line") ;;
*) inputs+=("$kit/lib/$line") ;;
esac
done < "$kit/lib/link.txt"
PS5_DIST_DIR="$out" PS5_OPENGL_PREFIX="$kit/opengl/sdk" \
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
owner=$(stat -c '%u:%g' "$out")
[[ $owner == 0:0 ]] || chown -R "$owner" "$out" 2>/dev/null || true
fi
say "$app"
echo " copy the folder $title_id to /data/homebrew/ on the console (FTP), and your own"
echo " disc image (USA, GMSE01: .iso, .gcm or .ciso) into it, beside eboot.bin."
echo " The title you built holds the game's code: it is yours to play, not to share."
-23
View File
@@ -1,23 +0,0 @@
#!/usr/bin/env bash
# Super Mario Sunshine for the PlayStation 5 - an HD texture pack for the console, made in the kit's container.
#
# make-texture-pack.sh [OUT] OUT/textures/ (OUT defaults to ./out)
#
# 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.
#
# SPDX-License-Identifier: MIT
set -euo pipefail
kit=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
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")
"$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
-24
View File
@@ -1,24 +0,0 @@
#!/usr/bin/env bash
# Super Mario Sunshine for the PlayStation 5 - the builder kit, run in a container.
#
# make-title.sh [OUT] OUT/PPSA99312/ (OUT defaults to ./out)
#
# 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.
#
# JOBS compilers at once (default: one a processor)
#
# SPDX-License-Identifier: MIT
set -euo pipefail
kit=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
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; }
tag=sunshine-ps5-kit:$(<"$kit/VERSION")
"$container" image inspect "$tag" > /dev/null 2>&1 || "$container" build -t "$tag" "$kit"
exec "$container" run --rm ${JOBS:+-e JOBS="$JOBS"} -v "$out":/out "$tag" /out
+517 -48
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
@@ -678,10 +706,215 @@ index 393b8f9..16f82e6 100644
signalFlag(*ready);
uint64_t tail = s_tail.load();
diff --git a/platform/gx/src/gx_hires.cpp b/platform/gx/src/gx_hires.cpp
index bcd4174..3887f46 100644
index bcd4174..937d1c2 100644
--- a/platform/gx/src/gx_hires.cpp
+++ b/platform/gx/src/gx_hires.cpp
@@ -1049,6 +1049,9 @@ static bool beginUpload(Upload& u) {
@@ -348,6 +348,8 @@ static void queryFormats() {
// file's own mip levels, or 32-bit RGBA/BGRA. Blocks the GL cannot take are
// decoded here; the levels a file leaves out are made by completeChain.
enum { DDS_BC1 = BLOCK_BC1, DDS_BC2 = BLOCK_BC2, DDS_BC3 = BLOCK_BC3, DDS_BC7 = BLOCK_BC7, DDS_RGBA, DDS_BGRA };
+static const GLenum kGl[] = {0, 0x83F1, 0x83F2, 0x83F3, 0x8E8C}; // S3TC DXT1/3/5 RGBA, BPTC UNORM
+static bool gpuTakes(int fmt) { return (fmt >= DDS_BC1 && fmt <= DDS_BC3 && s_s3tc) || (fmt == DDS_BC7 && s_bptc); }
static bool readFile(const std::string& path, std::vector<uint8_t>& out) {
FILE* f = fopen(path.c_str(), "rb");
@@ -392,8 +394,7 @@ static bool decodeDds(const std::string& path, Loaded* L) {
for (int m = std::max(w, h); m > 1; m >>= 1) full++;
// Supported blocks stay compressed whatever levels the file has: the
// missing ones are encoded in the same format (completeChain).
- bool gpu = (fmt <= DDS_BC3 && s_s3tc) || (fmt == DDS_BC7 && s_bptc);
- static const GLenum kGl[] = {0, 0x83F1, 0x83F2, 0x83F3, 0x8E8C}; // S3TC DXT1/3/5 RGBA, BPTC UNORM
+ bool gpu = gpuTakes(fmt);
L->w = w;
L->h = h;
L->compressed = gpu ? kGl[fmt] : 0;
@@ -660,6 +661,23 @@ static std::vector<uint8_t> nextBlockLevel(int fmt, const std::vector<uint8_t>&
return dst;
}
+// The next level of an RGBA one (lw x lh), by the same 2x2 box filter.
+static std::vector<uint8_t> nextRgbaLevel(const std::vector<uint8_t>& src, int lw, int lh) {
+ int nw = std::max(1, lw / 2), nh = std::max(1, lh / 2);
+ std::vector<uint8_t> dst(size_t(nw) * nh * 4);
+ for (int y = 0; y < nh; y++)
+ for (int x = 0; x < nw; x++) {
+ int x0 = std::min(2 * x, lw - 1), x1 = std::min(2 * x + 1, lw - 1);
+ int y0 = std::min(2 * y, lh - 1), y1 = std::min(2 * y + 1, lh - 1);
+ for (int c = 0; c < 4; c++) {
+ int sum = src[(size_t(y0) * lw + x0) * 4 + c] + src[(size_t(y0) * lw + x1) * 4 + c] +
+ src[(size_t(y1) * lw + x0) * 4 + c] + src[(size_t(y1) * lw + x1) * 4 + c];
+ dst[(size_t(y) * nw + x) * 4 + c] = uint8_t((sum + 2) / 4);
+ }
+ }
+ return dst;
+}
+
// Complete every replacement's mip chain down to 1x1 with 2x2 box filtering,
// whatever levels the pack supplied: the sampler minifies a replacement as far
// as its original (samplerFor), and without the lower levels a far wall
@@ -683,22 +701,9 @@ static void completeChain(Loaded* L, bool idle = false) {
return;
}
while (lw > 1 || lh > 1) {
- int nw = std::max(1, lw / 2), nh = std::max(1, lh / 2);
- const std::vector<uint8_t>& src = L->levels.back();
- std::vector<uint8_t> dst(size_t(nw) * nh * 4);
- for (int y = 0; y < nh; y++)
- for (int x = 0; x < nw; x++) {
- int x0 = std::min(2 * x, lw - 1), x1 = std::min(2 * x + 1, lw - 1);
- int y0 = std::min(2 * y, lh - 1), y1 = std::min(2 * y + 1, lh - 1);
- for (int c = 0; c < 4; c++) {
- int sum = src[(size_t(y0) * lw + x0) * 4 + c] + src[(size_t(y0) * lw + x1) * 4 + c] +
- src[(size_t(y1) * lw + x0) * 4 + c] + src[(size_t(y1) * lw + x1) * 4 + c];
- dst[(size_t(y) * nw + x) * 4 + c] = uint8_t((sum + 2) / 4);
- }
- }
- L->levels.push_back(std::move(dst));
- lw = nw;
- lh = nh;
+ L->levels.push_back(nextRgbaLevel(L->levels.back(), lw, lh));
+ lw = std::max(1, lw / 2);
+ lh = std::max(1, lh / 2);
}
}
@@ -783,18 +788,20 @@ static void loadCachedLevels(const std::string& name, const PackFile& f, Loaded*
for (auto& lv : levels) L->levels.push_back(std::move(lv));
}
-// Keeps the levels a completion made (L->levels from L->first on).
-static void storeCachedLevels(const std::string& name, const PackFile& f, const Loaded& L) {
+// Keeps the levels a completion made (L->levels from L->first on); with
+// ".top", a shrunk replacement's top level (shrinkToLimit).
+static const char kTopMagic[8] = {'S', 'M', 'S', 'T', 'O', 'P', '0', '1'};
+static void storeCachedLevels(const std::string& name, const PackFile& f, const Loaded& L, const char* ext = ".mips") {
const std::string& dir = mipCacheDir();
MipsHeader hd{};
if (dir.empty() || L.levels.empty() || !sourceStamp(f.path, &hd.size, &hd.mtime)) return;
- memcpy(hd.magic, kMipsMagic, 8);
+ memcpy(hd.magic, strcmp(ext, ".top") ? kMipsMagic : kTopMagic, 8);
hd.w = uint32_t(L.w);
hd.h = uint32_t(L.h);
hd.blocks = uint32_t(L.blocks);
hd.first = uint32_t(L.first);
hd.count = uint32_t(L.levels.size());
- const std::string path = dir + "/" + name + ".mips", part = path + ".part";
+ const std::string path = dir + "/" + name + ext, part = path + ".part";
FILE* out = fopen(part.c_str(), "wb");
if (!out) return;
bool ok = fwrite(&hd, sizeof hd, 1, out) == 1;
@@ -822,6 +829,85 @@ static void storeCachedLevels(const std::string& name, const PackFile& f, const
if (!ok || ec) std::filesystem::remove(part, ec);
}
+// ------------------------------------------------------------------ replacements too large to hold
+// SMS_TEXTURE_PACK_MAX_SIZE=n halves a replacement longer than n pixels until
+// it is not, for a GPU that cannot hold a pack's largest images (the PS5's
+// driver: the UHD pack's 4096 to 16384 pixel files ran it out of memory, and
+// took 17 seconds to send in one load). The file's own lower levels serve
+// where it has them; otherwise the top level is made from the largest, by
+// nextBlockLevel, and kept on disk as <name>.top beside the completed levels,
+// so each large file is read once. Unset or 0 keeps every replacement whole.
+static int maxSize() {
+ static const int n = getenv("SMS_TEXTURE_PACK_MAX_SIZE") ? std::max(0, atoi(getenv("SMS_TEXTURE_PACK_MAX_SIZE"))) : 0;
+ return n;
+}
+
+static int halvings(int w, int h) {
+ int k = 0;
+ while (maxSize() > 0 && std::max(w, h) > maxSize()) {
+ w = std::max(1, w / 2);
+ h = std::max(1, h / 2);
+ k++;
+ }
+ return k;
+}
+
+static bool ddsSize(const std::string& path, int* w, int* h) {
+ uint8_t d[20];
+ FILE* f = fopen(path.c_str(), "rb");
+ if (!f) return false;
+ bool ok = fread(d, sizeof d, 1, f) == 1 && !memcmp(d, "DDS ", 4);
+ fclose(f);
+ if (!ok) return false;
+ *h = int(rd32(d + 12));
+ *w = int(rd32(d + 16));
+ return *w > 0 && *h > 0;
+}
+
+// The top level an earlier shrinkToLimit kept, for a file of w x h halved k times.
+static bool loadShrunk(const std::string& name, const PackFile& f, int w, int h, int k, Loaded* L) {
+ const std::string& dir = mipCacheDir();
+ uint64_t size;
+ int64_t mtime;
+ if (dir.empty() || !sourceStamp(f.path, &size, &mtime)) return false;
+ FILE* in = fopen((dir + "/" + name + ".top").c_str(), "rb");
+ if (!in) return false;
+ MipsHeader hd;
+ std::vector<uint8_t> top;
+ const int sw = std::max(1, w >> k), sh = std::max(1, h >> k);
+ if (fread(&hd, sizeof hd, 1, in) == 1 && !memcmp(hd.magic, kTopMagic, 8) && hd.size == size && hd.mtime == mtime &&
+ hd.w == uint32_t(sw) && hd.h == uint32_t(sh) && hd.first == 0 && hd.count == 1 && gpuTakes(int(hd.blocks))) {
+ top.resize(blockLevelBytes(int(hd.blocks), sw, sh));
+ if (fread(top.data(), top.size(), 1, in) != 1) top.clear();
+ }
+ fclose(in);
+ if (top.empty()) return false;
+ L->w = sw;
+ L->h = sh;
+ L->blocks = int(hd.blocks);
+ L->compressed = kGl[L->blocks];
+ L->levels.push_back(std::move(top));
+ return true;
+}
+
+// Halves L k times, from the deepest of the levels it has that is still too
+// large. True if that made a level (rather than only dropping the file's own).
+static bool shrinkToLimit(Loaded* L, int k) {
+ const int drop = std::min(k, int(L->levels.size()) - 1);
+ L->levels.erase(L->levels.begin(), L->levels.begin() + drop);
+ L->w = std::max(1, L->w >> drop);
+ L->h = std::max(1, L->h >> drop);
+ for (int i = drop; i < k; i++) {
+ std::vector<uint8_t> next = L->compressed ? nextBlockLevel(L->blocks, L->levels[0], L->w, L->h, false)
+ : nextRgbaLevel(L->levels[0], L->w, L->h);
+ L->levels.clear();
+ L->levels.push_back(std::move(next));
+ L->w = std::max(1, L->w / 2);
+ L->h = std::max(1, L->h / 2);
+ }
+ return drop < k;
+}
+
// `whole`: also the compressed levels completeChain makes, else only the
// pack's and the cached ones (RGBA images are always completed: they are
// decoded anyway).
@@ -830,6 +916,13 @@ static Loaded* decode(const std::string& name, bool whole, bool cached = true) {
Loaded* L = new Loaded;
L->arbitrary = f.arbitrary;
if (endsWith(f.path, ".dds") || endsWith(f.path, ".DDS")) {
+ int fw = 0, fh = 0;
+ const int k = maxSize() > 0 && ddsSize(f.path, &fw, &fh) ? halvings(fw, fh) : 0;
+ if (k && loadShrunk(name, f, fw, fh, k, L)) {
+ if (cached) loadCachedLevels(name, f, L);
+ if (whole) completeChain(L);
+ return L;
+ }
if (!decodeDds(f.path, L)) {
logmsg("texture pack: cannot read %s (not a DDS of a supported format)", f.path.c_str());
L->failed = true;
@@ -847,6 +940,10 @@ static Loaded* decode(const std::string& name, bool whole, bool cached = true) {
L->levels.push_back(std::move(mip.levels.front()));
}
}
+ if (k && !L->failed && shrinkToLimit(L, k)) {
+ logmsg("texture pack: %s is %dx%d, used at %dx%d", f.path.c_str(), fw, fh, L->w, L->h);
+ if (L->compressed) storeCachedLevels(name, f, *L, ".top");
+ }
if (cached && !L->failed && L->compressed) loadCachedLevels(name, f, L);
if (whole || !L->compressed) completeChain(L);
return L;
@@ -1049,6 +1146,9 @@ static bool beginUpload(Upload& u) {
// Sends u's levels, at least one piece and then while `sent` is under
// `allowance`; true once they are all in. A completion's levels already have
// their storage (finishUpload), so they only fill it.
@@ -691,7 +924,7 @@ index bcd4174..3887f46 100644
static bool sendLevels(Upload& u, int unit, size_t allowance, size_t* sent) {
Loaded* L = u.item.image.get();
const bool fill = u.item.request.kind == REST;
@@ -1062,7 +1065,7 @@ static bool sendLevels(Upload& u, int unit, size_t allowance, size_t* sent) {
@@ -1062,7 +1162,7 @@ static bool sendLevels(Upload& u, int unit, size_t allowance, size_t* sent) {
const int n = L->first + int(u.level);
const int lw = std::max(1, L->w >> n), lh = std::max(1, L->h >> n);
size_t bytes = lv.size();
@@ -700,7 +933,7 @@ index bcd4174..3887f46 100644
if (L->compressed)
glCompressedTexImage2D(GL_TEXTURE_2D, n, L->compressed, lw, lh, 0, GLsizei(bytes), lv.data());
else
@@ -1212,13 +1215,23 @@ static bool takeDecoded(Upload& u, bool pack) { // with s_mu held
@@ -1212,13 +1312,23 @@ static bool takeDecoded(Upload& u, bool pack) { // with s_mu held
// Sends decoded textures into GL: all of the ones resources asked for when
// `wait` (a load: until none is left to decode), else up to a frame's
// allowance, or 2 ms. The whole pack's wait for loads (uploadPack).
@@ -724,7 +957,7 @@ index bcd4174..3887f46 100644
if (s_uploads.empty()) {
Upload u;
{
@@ -1226,10 +1239,14 @@ static void uploadPending(bool wait) {
@@ -1226,10 +1336,14 @@ static void uploadPending(bool wait) {
if (wait) lk.lock();
else if (!lk.try_lock()) break;
auto loaded = [] { return s_decodedDemand == 0 && s_queue.empty() && !s_busy; };
@@ -739,7 +972,7 @@ index bcd4174..3887f46 100644
continue;
}
Upload& u = s_uploads.front();
@@ -1237,11 +1254,14 @@ static void uploadPending(bool wait) {
@@ -1237,11 +1351,14 @@ static void uploadPending(bool wait) {
s_uploads.pop_front();
continue;
}
@@ -755,7 +988,7 @@ index bcd4174..3887f46 100644
if (!wait && (sent >= allowance || Clock::now() - start >= std::chrono::milliseconds(2))) break;
}
}
@@ -1325,20 +1345,32 @@ static void touchFresh() {
@@ -1325,20 +1442,32 @@ static void touchFresh() {
glcInvalidate();
}
@@ -790,23 +1023,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 +1130,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 +1149,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 +1236,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 +1264,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 +1291,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 +1624,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 +1652,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 +1694,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 +1767,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 +1806,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 +1823,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 +1836,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 +1847,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 +1881,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 +1984,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 +2096,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.010.000",
"downloadDataSize": 256,
"gameIntent": {
"permittedIntents": [
+4
View File
@@ -149,6 +149,10 @@ ps5_startup(void)
PC's budget of 1536 cannot fit: the title died at about 1,070. From 512
the oldest are let go, which keeps about 3 GiB spare through a load. */
set("SMS_TEXTURE_PACK_MB", "512", 0);
/* ... and the largest images it can take: the UHD pack's 4096 to 16384
pixel files are halved to 2048 (or less) the first time each is used, and
kept so in userdata/.cache, so the pack goes in as its authors publish it */
set("SMS_TEXTURE_PACK_MAX_SIZE", "3072", 0);
if (stat("/app0/mods/hd-cutscenes", &status) == 0)
set("SMS_HD_CUTSCENES", "/app0/mods/hd-cutscenes", 0);
find_disc();
-108
View File
@@ -1,108 +0,0 @@
#!/usr/bin/env python3
"""Super Mario Sunshine for the PlayStation 5 - an HD texture pack sized for the console.
get-texture-pack.py [OUT] OUT/textures/ (OUT defaults to ./out)
Downloads the Super Mario Sunshine UHD Texture Pack (qashto, razius) and the HD
texture extras (chasem-dev) from where their authors publish them, checks each
against the release this was tested with, and makes a copy with nothing larger
than 2048 pixels (shrink-texture-pack.py: the console's driver cannot hold the
4096 and 8192 pixel images). Copy OUT/textures to the console as
/data/homebrew/PPSA99312/mods/textures.
Neither pack is part of this project or stored by it; each belongs to its authors.
Needs Python with Pillow and etcpak, and 7-Zip (7z, 7zz or 7za) or bsdtar to
unpack. About 1 GB to download, 5 GB of disk while it works, 1.3 GB at the end.
SPDX-License-Identifier: MIT
"""
import hashlib, os, shutil, subprocess, sys, urllib.request, zipfile
HERE = os.path.dirname(os.path.abspath(__file__))
PACKS = [
{"name": "Super Mario Sunshine UHD Texture Pack v2.1.1 (qashto, razius)",
"page": "https://github.com/qashto/Super_Mario_Sunshine_UHD_Texture_Pack",
"url": "https://github.com/qashto/Super_Mario_Sunshine_UHD_Texture_Pack/releases/download/2.1.1/GMS.7z",
"file": "GMS.7z", "md5": "274985dba9214b857b3e5605d2ee49b2", "inside": "GMS/Textures/GMS", "as": "GMS"},
{"name": "Super Mario Sunshine HD texture extras v1.1.1 (chasem-dev)",
"page": "https://github.com/chasem-dev/sms-hd-texture-extras",
"url": "https://github.com/chasem-dev/sms-hd-texture-extras/releases/download/v1.1.1/sms-hd-texture-extras-1.1.1.zip",
"file": "sms-hd-texture-extras-1.1.1.zip", "md5": "a658c43cce0751e585e991f8563e5f41", "inside": "", "as": "sms-hd-texture-extras"},
]
def say(text):
print("==> " + text, flush=True)
def md5_of(path):
digest = hashlib.md5()
with open(path, "rb") as source:
for chunk in iter(lambda: source.read(1 << 20), b""):
digest.update(chunk)
return digest.hexdigest()
def fetch(pack, folder):
path = os.path.join(folder, pack["file"])
if os.path.isfile(path) and md5_of(path) == pack["md5"]:
return path
say("downloading %s" % pack["name"])
request = urllib.request.Request(pack["url"], headers={"User-Agent": "PS5_Sunshine texture pack installer"})
with urllib.request.urlopen(request) as reply, open(path + ".part", "wb") as out:
shutil.copyfileobj(reply, out, 1 << 20)
os.replace(path + ".part", path)
got = md5_of(path)
if got != pack["md5"]:
os.remove(path)
sys.exit("get-texture-pack.py: %s is not the release this was tested with (MD5 %s, expected %s); see %s"
% (pack["file"], got, pack["md5"], pack["page"]))
return path
def unpack(archive, folder):
shutil.rmtree(folder, ignore_errors=True)
os.makedirs(folder)
if archive.endswith(".zip"):
with zipfile.ZipFile(archive) as source:
source.extractall(folder)
return
for tool in ("7z", "7zz", "7za"):
if shutil.which(tool):
subprocess.run([tool, "x", "-y", "-o" + folder, archive], check=True, stdout=subprocess.DEVNULL)
return
for tool in ("bsdtar", "tar"): # libarchive reads 7z; GNU tar does not, and says so
if shutil.which(tool) and subprocess.run([tool, "-xf", archive, "-C", folder], stderr=subprocess.DEVNULL).returncode == 0:
return
sys.exit("get-texture-pack.py: 7-Zip (7z, 7zz or 7za) or bsdtar is needed to unpack " + os.path.basename(archive))
def main():
out = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else "out")
downloads, original, result = (os.path.join(out, name) for name in (".texture-downloads", ".texture-original", "textures"))
os.makedirs(downloads, exist_ok=True)
shutil.rmtree(original, ignore_errors=True)
for pack in PACKS:
archive = fetch(pack, downloads)
say("unpacking %s" % pack["file"])
scratch = os.path.join(out, ".texture-unpack")
unpack(archive, scratch)
os.makedirs(original, exist_ok=True)
shutil.move(os.path.join(scratch, pack["inside"]) if pack["inside"] else scratch, os.path.join(original, pack["as"]))
shutil.rmtree(scratch, ignore_errors=True)
# only the textures: the UHD pack also carries an archive of its sources
for root, _, files in os.walk(original):
for name in files:
if not name.lower().endswith((".dds", ".png")):
os.remove(os.path.join(root, name))
say("making the console's copy (nothing larger than 2048 pixels)")
shutil.rmtree(result, ignore_errors=True)
subprocess.run([sys.executable, os.path.join(HERE, "shrink-texture-pack.py"), original, result, "2048"], check=True)
shutil.rmtree(original, ignore_errors=True)
say(result)
print(" copy it to the console as /data/homebrew/PPSA99312/mods/textures (FTP)")
print(" the packs belong to their authors: this copy is for your own use")
if __name__ == "__main__":
main()
+20 -5
View File
@@ -12,7 +12,7 @@
# console's own GPU interface, EGL for the screen), a static archive group and
# its two import stubs. The title folder, dist/<TITLE_ID>/, gets the signed
# eboot.bin, sce_sys/ and the licences. OBJECT may be an archive.
# PS5_DIST_DIR is where the title folder goes instead of dist/ (the builder kit).
# PS5_DIST_DIR is where the title folder goes instead of dist/.
# PS5_OPENGL_PREFIX is the OpenGL SDK's sdk/ directory.
#
# Copyright (C) 2026 Mihawk
@@ -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, which a Mac does not have)
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"
-155
View File
@@ -1,155 +0,0 @@
#!/usr/bin/env bash
# Super Mario Sunshine for the PlayStation 5 - the builder kit: everything of a build but the game.
#
# ps5/tools/package-kit.sh [VERSION] dist/kit/sunshine-ps5-kit-<VERSION>/ and its .zip
#
# A built title is not shared: eboot.bin holds the game's code, compiled from
# the decompilation. The kit is the rest of one, as it stands after
# tools/build.sh: the port's own objects (the platform layer, the graphics
# layer, SDL2), the OpenGL driver, the SDK's headers, libraries and link
# scripts, and kit/build-title.sh, which on somebody else's machine fetches the
# PC port and the decompilation, compiles the game's code and links the same
# title with the same recipe (tools/link-title.sh). What goes in is listed
# here, file by file; the game's code (libsms_game.a) and anything from a disc
# never do, and the script checks that before it finishes.
#
# The link line is the build's own (ninja's), so the kit links what was tested.
#
# SPDX-License-Identifier: MIT
set -euo pipefail
ps5=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
root=$(dirname "$ps5")
vulkan=$(cd -- "${PS5_VULKAN_DIR:-$root/../PS5_Vulkan}" && pwd)
opengl=${PS5_OPENGL_PREFIX:?set PS5_OPENGL_PREFIX to the sdk/ directory of the PS5 OpenGL SDK}
sdk=$(bash "$ps5/tools/setup-sdk.sh")
build=${PS5_BUILD_DIR:-$root/build/game}
ninja=$(command -v ninja || echo "$HOME/.local/bin/ninja")
export PS5_CLANG=${PS5_CLANG:-$(command -v clang || true)}
version=${1:-$(python3 -c 'import json,sys; v=json.load(open(sys.argv[1]))["contentVersion"].split("."); print("%d.%d.%d" % tuple(int(x) for x in v))' "$ps5/sce_sys/param.json")}
name=sunshine-ps5-kit-$version
kit="$root/dist/kit/$name"
source="$root/vendor/sms-pc-port"
# the kit is the build as the sources stand now: bring it up to date first
bash "$ps5/tools/build.sh"
rm -rf -- "$kit"
mkdir -p "$kit/lib" "$kit/ps5/tools"
# ---- the title's link inputs, in the build's order, without the game's code
# (ninja's command: ... link-title.sh BUILD VULKAN SDK INPUT... && :)
mapfile -t inputs < <("$ninja" -C "$build" -t commands sms | tail -1 |
sed -e 's/.*link-title\.sh //' -e 's/ && :$//' | tr ' ' '\n' | sed -n '4,$p' | sed '/^$/d')
strip="$sdk/bin/llvm-strip"
: > "$kit/lib/link.txt"
game_code=0
for input in "${inputs[@]}"; do
case $input in
-*) echo "$input" >> "$kit/lib/link.txt"; continue ;;
libsms_game.a | */libsms_game.a) echo "@GAMECODE@" >> "$kit/lib/link.txt"; game_code=$((game_code + 1)); continue ;;
esac
file=$input
[[ $file == /* ]] || file="$build/$input"
[[ -f $file ]] || { echo "package-kit.sh: link input $input is missing" >&2; exit 2; }
relative=${file#"$build"/}
[[ $relative != /* ]] || { echo "package-kit.sh: link input $input is outside the build tree" >&2; exit 2; }
if [[ ! -f $kit/lib/$relative ]]; then
mkdir -p "$(dirname "$kit/lib/$relative")"
# no debug information: it names this machine's folders, and is most of the size
"$strip" --strip-debug -o "$kit/lib/$relative" "$file"
fi
echo "$relative" >> "$kit/lib/link.txt"
done
[[ $game_code -eq 1 ]] || { echo "package-kit.sh: expected the game's code once in the link line, found it $game_code times" >&2; exit 2; }
# ---- the port: the link, the title's own files, and what the game's code is built with
cp "$ps5/tools/link-title.sh" "$ps5/tools/cc-init-array.sh" "$kit/ps5/tools/"
cp -R "$ps5/sce_sys" "$kit/ps5/sce_sys"
cp -R "$ps5/res" "$kit/ps5/res"
mkdir -p "$kit/ps5/stubs" "$kit/ps5/cmake" "$kit/ps5/patches" "$kit/ps5/opengl"
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/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/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
port_rev=$(git -C "$source" rev-parse HEAD)
decomp_rev=$(git -C "$source/decomp" rev-parse HEAD)
{
echo "port_url=$(git -C "$source" remote get-url origin)"
echo "port_rev=$port_rev"
echo "decomp_rev=$decomp_rev"
} > "$kit/REVISIONS"
# ... and the patch must be the tree's whole difference from that revision
bash "$ps5/tools/setup-source.sh" > /dev/null
# ---- the payload SDK: compiler wrappers, link scripts, the CMake toolchain, headers, libraries
mkdir -p "$kit/sdk/target"
cp -R "$sdk/bin" "$sdk/ldscripts" "$sdk/toolchain" "$kit/sdk/"
cp -R "$sdk/target/include" "$sdk/target/lib" "$kit/sdk/target/"
cp "$sdk/.ps5-sdk-revision" "$kit/sdk/"
# ---- PS5_Vulkan: the link recipe, the CRT, the fake-SELF tool's source, libc.prx
mkdir -p "$kit/vulkan/tools" "$kit/vulkan/tooling/psbc" "$kit/vulkan/tooling/native" "$kit/vulkan/runtime"
cp "$vulkan/LICENSE" "$vulkan/NOTICE.md" "$kit/vulkan/"
cp "$vulkan/tools/radv-link.sh" "$kit/vulkan/tools/"
cp "$vulkan/tooling/prospero-clang18" "$kit/vulkan/tooling/"
cp "$vulkan/tooling/psbc/ps5-pie-unwind.ld" "$kit/vulkan/tooling/psbc/"
cp "$vulkan"/tooling/native/*.cpp "$vulkan"/tooling/native/*.hpp "$vulkan"/tooling/native/*.map \
"$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 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/"
# (as released: libPS5OpenGL.a is a linker script naming the others)
cp -R "$opengl"/lib/. "$kit/opengl/sdk/lib/"
rm -rf "$kit/opengl/sdk/lib/cmake" "$kit/opengl/sdk/lib/pkgconfig"
[[ -f $opengl/sunshine-runtime-defines.txt ]] && cp "$opengl/sunshine-runtime-defines.txt" "$kit/opengl/"
# ---- Clang's builtins for the console's processor: a Linux on arm64 does not install x86-64's
builtins="$("$PS5_CLANG" --print-resource-dir)/lib/linux/libclang_rt.builtins-x86_64.a"
[[ -f $builtins ]] || { echo "package-kit.sh: missing $builtins" >&2; exit 2; }
mkdir -p "$kit/clang-rt/lib/linux"
cp "$builtins" "$kit/clang-rt/lib/linux/"
# ---- licences, and where each part's source is
mkdir -p "$kit/licenses"
cp "$root/LICENSE" "$kit/LICENSE"
cp "$root/LICENSING.md" "$kit/"
cp "$root"/licenses/* "$kit/licenses/"
cp "$source/LICENSE" "$kit/licenses/sms-pc-port-LICENSE.txt"
sdl_revision=$(git -C "$root/.deps/SDL2" describe --tags --always 2>/dev/null || echo "release-2.32.10")
own_revision=$(git -C "$root" rev-parse HEAD 2>/dev/null || echo "(this kit's own files: ps5/ in the kit)")
{
echo "The builder kit $version of Super Mario Sunshine for the PlayStation 5 was built from:"
echo
echo "PS5_Sunshine https://github.com/griches/PS5_Sunshine $own_revision"
echo "sms-pc-port https://github.com/chasem-dev/sms-pc-port $port_rev (+ ps5/patches/sms-pc-port.patch)"
echo "sms-english https://github.com/chasem-dev/sms-english $decomp_rev (fetched by build-title.sh, not in the kit)"
echo "PS5 OpenGL SDK https://github.com/blackbearreloaded/ps5-opengl release 1.0.2, with three runtime objects rebuilt (ps5/opengl/build-runtime.sh; defines in opengl/sunshine-runtime-defines.txt)"
echo "PS5_PayloadSDK https://github.com/mihawk-99/PS5_PayloadSDK $(<"$sdk/.ps5-sdk-revision")"
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
echo "LICENSING.md says which licence covers which part."
} > "$kit/SOURCES.txt"
echo "$version" > "$kit/VERSION"
# ---- nothing of the game: no game code archive or object, no decompilation, no disc image or save
leaks=$(find "$kit" \( -name 'libsms_game*' -o -path '*/sms_game.dir/*' -o -path '*/decomp/*' -o -path '*/patched/*' \
-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")
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"
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# Super Mario Sunshine for the PlayStation 5 - pack the built title folder as a release archive.
#
# ps5/tools/package-release.sh [VERSION] (through tools/vm.sh package-release)
#
# Writes dist/Sunshine-PS5-vVERSION.zip: the title folder dist/<TITLE_ID>/ as the
# console takes it (eboot.bin, sce_sys/, sce_module/, res/, licenses/), with
# SOURCE.txt beside eboot.bin naming the revision of every part the build
# linked. A built title is under GPL-3.0: that file is how a release says where
# its source is. No disc image, save or texture pack goes in, and it refuses if
# one is in the folder. VERSION defaults to param.json's contentVersion.
#
# After PS5_SwitchWaker's package-release.sh.
# SPDX-License-Identifier: MIT
set -euo pipefail
ps5=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
root=$(dirname "$ps5")
vulkan=$(cd -- "${PS5_VULKAN_DIR:-$root/../PS5_Vulkan}" && pwd)
opengl=${PS5_OPENGL_PREFIX:?set PS5_OPENGL_PREFIX to the sdk/ directory of the PS5 OpenGL SDK}
source="$root/vendor/sms-pc-port"
param="$ps5/sce_sys/param.json"
version=${1:-$(python3 -c 'import json,sys; v=json.load(open(sys.argv[1]))["contentVersion"].split("."); print("%d.%d.%d" % tuple(int(x) for x in v))' "$param")}
title_id=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["titleId"])' "$param")
app="$root/dist/$title_id"
for file in eboot.bin sce_sys/param.json sce_sys/icon0.png sce_sys/pic0.dds sce_sys/pic1.dds sce_module/libc.prx \
res/prompts/playstation.png; do
[[ -e $app/$file ]] || { echo "missing $app/$file: run build.sh first" >&2; exit 2; }
done
cmp -s "$param" "$app/sce_sys/param.json" || { echo "the title's param.json is not this tree's: run build.sh first" >&2; exit 2; }
# a console's own files, a disc, a save or a pack are not for a release
stray=$(find "$app" \( -iname '*.iso' -o -iname '*.ciso' -o -iname '*.gcm' -o -iname '*.rvz' -o -iname '*.gci' \
-o -iname '*.dol' -o -iname '*.szs' -o -path "$app/userdata" -o -path "$app/mods" -o -name env.txt \
-o -name settings.txt -o -name bindings.txt \) -print -quit)
[[ -z $stray ]] || { echo "game data or a save in the title folder ($stray): a release never carries it" >&2; exit 2; }
# the submodule's working tree carries the console's patch: that is not a change here
[[ -z $(git -C "$root" status --porcelain --ignore-submodules=dirty) ]] ||
{ echo "uncommitted changes: a release is built from a commit" >&2; exit 2; }
sdk=$(bash "$ps5/tools/setup-sdk.sh")
sdl_revision=$(git -C "$root/.deps/SDL2" describe --tags --always 2>/dev/null || echo "release-2.32.10")
mkdir -p "$app/licenses"
cp "$root/licenses/GPL-3.0.txt" "$root/licenses/MIT.txt" "$root/licenses/Zlib.txt" "$app/licenses/"
cp "$source/LICENSE" "$app/licenses/sms-pc-port-LICENSE.txt"
cp "$source/decomp/LICENSE" "$app/licenses/sms-english-LICENSE.txt"
cp "$root/LICENSING.md" "$app/licenses/LICENSING.md"
cat > "$app/SOURCE.txt" <<SOURCE
Super Mario Sunshine for PlayStation 5, version $version
An unofficial homebrew port of chasem-dev's sms-pc-port. Not affiliated with
Nintendo or Sony Interactive Entertainment.
This title is distributed under the GNU General Public License, version 3
(licenses/GPL-3.0.txt). Its source is these projects at these revisions:
The port (patch, SDL2 drivers, platform layer, build)
$(git -C "$root" remote get-url origin 2>/dev/null || echo "this repository")
$(git -C "$root" rev-parse HEAD)
The PC port: sms-pc-port (MIT), with ps5/patches/sms-pc-port.patch
https://github.com/chasem-dev/sms-pc-port
$(git -C "$source" rev-parse HEAD)
The game's code: the sms-english decompilation it builds (CC0-1.0)
https://github.com/chasem-dev/sms-english
$(git -C "$source/decomp" rev-parse HEAD)
OpenGL: the PS5 OpenGL SDK (GPL-3.0-or-later), release 1.0.2, with three runtime
objects rebuilt by ps5/opengl/build-runtime.sh
https://github.com/blackbearreloaded/ps5-opengl
SDL2 (Zlib), with ps5/patches/sdl.patch and the drivers in ps5/sdl
https://github.com/libsdl-org/SDL
$sdl_revision
The link recipe, CRT, libc.prx and packaging: PS5_Vulkan (GPL-3.0)
https://github.com/mihawk-99/PS5_Vulkan
$(git -C "$vulkan" rev-parse HEAD)
The platform layer and SDK: PS5_PayloadSDK (GPL-3.0), over ps5-payload-dev/sdk
https://github.com/mihawk-99/PS5_PayloadSDK
$(<"$sdk/.ps5-sdk-revision")
Compiler: $(${PS5_CLANG:-clang} --version | head -1)
No game data is included. Super Mario Sunshine belongs to Nintendo; this title
does nothing without a GameCube disc image of the USA disc (GMSE01, revision 0,
.iso, .gcm or .ciso) that you provide, placed beside eboot.bin. The pictures in
sce_sys/ are the port's own, and res/prompts holds button symbols drawn from
Kenney's CC0 Input Prompts.
eboot.bin sha256: $(sha256sum "$app/eboot.bin" | cut -d' ' -f1)
SOURCE
out="$root/dist/Sunshine-PS5-v$version.zip"
rm -f "$out"
(cd "$root/dist" && zip -q -r -X "$out" "$title_id" -x '*/.DS_Store')
printf '==> %s (%s bytes)\n' "$out" "$(stat -c %s "$out")"
sha256sum "$out"
-84
View File
@@ -1,84 +0,0 @@
#!/usr/bin/env python3
"""Super Mario Sunshine for the PlayStation 5 - a texture pack sized for the console.
shrink-texture-pack.py SOURCE DEST [MAX]
Copies a Dolphin texture pack, making every BC7 .dds wider or taller than MAX
(2048) that size with a full mipmap chain. The PS5's OpenGL driver takes
texture data slowly (about 35 MiB a second) and a title's graphics memory is
limited: the UHD pack's 4096 and 8192 pixel images are two thirds of its 3 GB,
cost Delfino Plaza a 17 second load, and ran Bianco Hills out of memory.
Needs Python with Pillow and etcpak.
SPDX-License-Identifier: MIT
"""
import os, shutil, struct, sys
from multiprocessing import Pool
from PIL import Image
import etcpak
Image.MAX_IMAGE_PIXELS = None
def blocks(image):
w, h = image.size
pw, ph = (w + 3) // 4 * 4, (h + 3) // 4 * 4
if (pw, ph) != (w, h): # the small levels: repeat the edge into the 4x4 block
image = image.resize((pw, ph), Image.NEAREST)
# the parameters are not optional: without them etcpak crashes
return etcpak.compress_bc7(image.tobytes(), pw, ph, etcpak.BC7CompressBlockParams())
def convert(job):
source, dest, limit = job
header = bytearray(open(source, "rb").read(148))
height, width = struct.unpack_from("<II", header, 12)
dxgi = struct.unpack_from("<I", header, 128)[0] if header[84:88] == b"DX10" else 0
if max(width, height) <= limit or dxgi not in (98, 99):
shutil.copyfile(source, dest)
return 0
image = Image.open(source).convert("RGBA")
scale = limit / max(width, height)
w, h = max(4, round(width * scale)), max(4, round(height * scale))
levels = []
while True:
level = image.resize((w, h), Image.LANCZOS)
levels.append(blocks(level))
if w == 1 and h == 1:
break
w, h = max(1, w // 2), max(1, h // 2)
w, h = max(4, round(width * scale)), max(4, round(height * scale))
struct.pack_into("<I", header, 8, struct.unpack_from("<I", header, 8)[0] | 0x20000 | 0x80000) # mip count, linear size
struct.pack_into("<II", header, 12, h, w)
struct.pack_into("<I", header, 20, len(levels[0]))
struct.pack_into("<I", header, 28, len(levels))
struct.pack_into("<I", header, 108, 0x401008) # complex, mipmapped texture
with open(dest, "wb") as out:
out.write(header)
for level in levels:
out.write(level)
return 1
def main():
source, dest = sys.argv[1], sys.argv[2]
limit = int(sys.argv[3]) if len(sys.argv) > 3 else 2048
jobs = []
for root, _, files in os.walk(source):
target = os.path.join(dest, os.path.relpath(root, source))
os.makedirs(target, exist_ok=True)
for name in files:
if name.startswith("."):
continue
path = os.path.join(root, name)
if name.endswith(".dds"):
jobs.append((path, os.path.join(target, name), limit))
else:
shutil.copyfile(path, os.path.join(target, name))
with Pool() as pool:
made = sum(pool.imap_unordered(convert, jobs, chunksize=4))
print("%d images, %d made smaller" % (len(jobs), made))
if __name__ == "__main__":
main()
+1 -1
View File
@@ -6,7 +6,7 @@
# ps5/tools/vm.sh console-files ps5/tools/console-files.sh (your sm64.o2r, over FTP)
# ps5/tools/vm.sh deploy ps5/tools/deploy.sh (FTP to the console)
# ps5/tools/vm.sh run ps5/tools/run.sh (launch and follow klog)
# ps5/tools/vm.sh package-kit [V] ps5/tools/package-kit.sh (the builder kit)
# ps5/tools/vm.sh package-release [V] ps5/tools/package-release.sh (the release archive)
#
# The stack (PS5_Vulkan with its RADV archive, PS5_Mesa, PS5_PayloadSDK) lives in
# the Lima VM "ps5" under ~/ps5, built with LLVM 22 (~/ps5/env.sh); this folder is