mirror of
https://github.com/griches/PS5_Sunshine.git
synced 2026-10-17 18:02:14 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e01f5db170 | ||
|
|
768a56cc43 | ||
|
|
30ff84d01a |
No files matched your search
@@ -16,3 +16,4 @@ rom/
|
||||
*.szs
|
||||
# never: texture packs (they belong to their authors)
|
||||
mods/
|
||||
__pycache__/
|
||||
+10
-15
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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
@@ -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))
|
||||
@@ -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": [
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
Executable
+88
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user