mirror of
https://github.com/griches/PS5_SwitchWaker.git
synced 2026-10-17 13:00:40 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c0f213f2d | ||
|
|
83b5a62161 | ||
|
|
fc1563da67 | ||
|
|
2f91becaa4 | ||
|
|
5d47aa54c2 | ||
|
|
3c816ae645 | ||
|
|
aa745f8c4d | ||
|
|
6bc6d7641d | ||
|
|
2953584da5 | ||
|
|
14603c312e | ||
|
|
242f0a1ef1 | ||
|
|
e8730bb86c |
No files matched your search
@@ -1,5 +1,9 @@
|
||||
# Wind Waker (GameCube) for PlayStation 5
|
||||
|
||||
[](https://github.com/griches/PS5_SwitchWaker/stargazers)
|
||||
[](https://github.com/griches/PS5_SwitchWaker/releases/latest)
|
||||
[](LICENSING.md)
|
||||
|
||||
A PS5 homebrew port of [SwitchWaker](https://github.com/centollOS/SwitchWaker), the
|
||||
native port of the 2002 GameCube game (USA disc `GZLE01`, revision 0) compiled
|
||||
from its decompilation and drawn with [Aurora](https://github.com/encounter/aurora).
|
||||
@@ -16,8 +20,17 @@ image when the title starts.
|
||||
**Runs on the console.** First run on 2026-10-12 on a PS5 at 3840x2160: the
|
||||
title screen, name entry, the prologue and Outset, at 60 images a second
|
||||
(frame interpolation; the game's logic stays at 30), with the DualSense's
|
||||
symbols on the HUD. Played for minutes, not hours: nothing past Outset has been
|
||||
seen on the console.
|
||||
symbols on the HUD. Played on the console as far as the forest on Outset, the
|
||||
scenes that follow it, and the boss room of Dragon Roost Cavern; nothing else
|
||||
has been seen there.
|
||||
|
||||
**[Version 1.0.3](https://github.com/griches/PS5_SwitchWaker/releases/tag/v1.0.3)** fixes a crash in Dragon Roost Cavern, in the rooms with
|
||||
the hanging platforms. [Version 1.0.2](https://github.com/griches/PS5_SwitchWaker/releases/tag/v1.0.2) fixed a crash when the boss of Dragon
|
||||
Roost Cavern appears, and stopped the whole class of fault behind it (an
|
||||
integer division by zero) from ending the game anywhere else. [Version 1.0.1](https://github.com/griches/PS5_SwitchWaker/releases/tag/v1.0.1)
|
||||
fixed a stop on a black screen at the end of the forest scene. See
|
||||
[Development](#development) for what
|
||||
they were. Copy the [new release](https://github.com/griches/PS5_SwitchWaker/releases/latest) over the old folder; saves carry on.
|
||||
|
||||
A release is the title's folder with no game in it: the player copies their own `GZLE01` disc image (`.iso`, `.gcm` or `.ciso`) into
|
||||
`/data/homebrew/PPSA99309/` beside `eboot.bin`, and the title finds it (the
|
||||
@@ -25,14 +38,14 @@ first one in its folder, then on a USB drive).
|
||||
|
||||
| Area | State |
|
||||
| --- | --- |
|
||||
| Disc | Found in the title's folder; a `.iso` was used on the console, `.ciso` only on the Mac build. A missing or wrong disc closes the title with a console notification (the close is seen working; the notification's text has not been read on a television yet) |
|
||||
| Disc | Found in the title's folder; a `.iso` and a `.ciso` have both been run on the console; `.gcm` has not. A missing or wrong disc closes the title with a console notification (the close is seen working; the notification's text has not been read on a television yet) |
|
||||
| Picture | 16:9. The output is 3840x2160 unless the options menu's "Output resolution" says 2560x1440 or 1920x1080 (applied at the next start); the console scales it to the television. "Internal resolution" follows it ("Window size") or is fixed, which dynamic resolution needs |
|
||||
| 60 fps | Held at 3840x2160 in the scenes above, by the frame counter on screen. Not measured over a long session |
|
||||
| Shaders | A first start with no cache compiled 3,056 render pipelines in about 12 minutes. The title now ships the caches a console produced (`ps5/shader-cache`) and copies them into place when the console has none: the same start then compiles nothing. They must be taken from a console again after a change to Dawn, the driver or the shaders |
|
||||
| Button prompts | The HUD shows Cross (A), Square (B), Circle (X), Triangle (Y), R1 for Z, R2 for R and a right stick for the C stick (`ps5/art/make-glyphs.py`, Kenney's CC0 Input Prompts). Not done: the button pictures inside message text (they are in the game's font), and the options menu's own hints, which still name the GameCube's buttons |
|
||||
| Button prompts | The HUD shows Cross (A), Square (B), Circle (X), Triangle (Y), R1 for Z, R2 for R and a right stick for the C stick (`ps5/art/make-glyphs.py`, [Kenney's CC0 Input Prompts](https://kenney.nl/assets/input-prompts)). The options menu's hints name the DualSense's buttons. Not done: the button pictures inside message text (they are in the game's font). With an HD texture pack the pack's buttons are drawn instead: see [Button symbols with a pack](#hd-textures-optional) |
|
||||
| Options menu | The touch pad's click opens it |
|
||||
| Saves, settings | `/data/homebrew/PPSA99309/user/`; the first start asks to create a save file, as the game does on an empty memory card |
|
||||
| HD textures | A Dolphin texture pack the player supplies replaces the game's textures (off by default; see HD textures below). Seen working on the console with Hypatia's pack at Outset |
|
||||
| HD textures | A Dolphin texture pack the player supplies replaces the game's textures (on when a pack is in `user/hd_textures`, off otherwise; see [HD textures](#hd-textures-optional)). Seen working on the console with Hypatia's pack on Outset and in its forest. The pack has an optional PlayStation button set for the HUD |
|
||||
| Sound | Not checked by ear in this log |
|
||||
| DevLoop | `devloop-shot` and `devloop-quit` in the title's folder (registered as `windwaker-gc`); the shot answers once the game's loop runs |
|
||||
| Art | The icon and background are the port's own, cel-shaded, made with an image generator (`ps5/art/`) |
|
||||
@@ -48,7 +61,7 @@ The symbol of an empty item slot sits in the middle of its bubble.
|
||||
Download `WindWaker-PS5-vX.Y.Z.zip` from the
|
||||
[latest release](https://github.com/griches/PS5_SwitchWaker/releases/latest) and
|
||||
unpack it: it holds one folder, `PPSA99309`. (Or build that folder yourself, see
|
||||
Build.) It contains no game data.
|
||||
[Build](#build).) It contains no game data.
|
||||
|
||||
**What the console needs**
|
||||
|
||||
@@ -109,8 +122,10 @@ own work, and you download it yourself.
|
||||
The pack tried here is **Hypatia's "WWHD Mod" v2.0001a**, about 5,700 textures
|
||||
covering most of the game. Its downloads are in the first post of
|
||||
[its thread on the Dolphin forum](https://forums.dolphin-emu.org/thread-55004.html).
|
||||
Take **"PC DDS Half Size"** (a `.7z` of 850 MB, 2.5 GB unpacked): the port reads
|
||||
it as it is. The full-size one is four times larger for a picture that is hard
|
||||
Take **"PC DDS Half Size"** (a `.7z` of 850 MB, 2.5 GB unpacked), which is the
|
||||
one used here:
|
||||
[direct download from the author's Google Drive mirror](https://drive.google.com/file/d/1qBK8YOeSAwLpNP3NUkwcXgvUnQPKlgj7/view?usp=sharing)
|
||||
(`Hypatia WWHD Mod v2.0001a (DDS-Half).7z`). The port reads it as it is. The full-size one is four times larger for a picture that is hard
|
||||
to tell apart, and the PNG one is not suitable.
|
||||
|
||||
1. Unpack the `.7z` on a computer. Inside is a folder `GZL`.
|
||||
@@ -125,14 +140,17 @@ to tell apart, and the PNG one is not suitable.
|
||||
With no pack in that folder the option does nothing.
|
||||
|
||||
Seen on the console on 2026-10-12 with that pack: Outset at 3840x2160 and 60
|
||||
images a second with the textures replaced. Not played further than that.
|
||||
images a second with the textures replaced, and its forest. Not played further
|
||||
than that.
|
||||
|
||||
**Button symbols with a pack.** The pack replaces the HUD's buttons as well, with
|
||||
its own GameCube ones, so the DualSense's symbols this port draws are lost while
|
||||
it is on. To have PlayStation buttons with the pack, also copy the `HUD` folder
|
||||
from its `-[Optional Textures]-/[HUD] Controller Buttons - Playstation` over
|
||||
the `HUD` folder in `hd_textures`, replacing files: those label the buttons by
|
||||
position, as this port maps them.
|
||||
position, as this port maps them (Cross where A is, Square for B, Circle for X,
|
||||
Triangle for Y). The button pictures inside message text stay the GameCube's
|
||||
either way.
|
||||
|
||||
## Development
|
||||
|
||||
@@ -149,6 +167,48 @@ COS_BUILD_DIR=$PWD/build/native-mac COS_DISC=/path/to/GZLE01.iso COS_FPS60=1 \
|
||||
--preset sailing --aspect 16:9 --frames 700 --shot 690
|
||||
```
|
||||
|
||||
**Finding a fault on the console.** The game's own `[cos]` lines go to the
|
||||
console's log (klog, port 3232) with the driver's. A `[dev]` section at the end
|
||||
of `user/settings.ini` sets developer variables at the next start, for example
|
||||
`COS_TRACE=scene,res` for every scene created and every file opened. A scene
|
||||
change that stays in one step logs `scene-req-watch` lines every 300 calls: the
|
||||
step, and the processes still waiting to be deleted.
|
||||
|
||||
The stop that 1.0.1 fixes was found that way. Some actors declare their methods
|
||||
as returning `bool` and the game's process framework calls them as returning
|
||||
`int` and compares with `TRUE`. On x86-64 the rest of the register is undefined,
|
||||
so a delete could be refused for ever and the next scene never came; on ARM (the
|
||||
Mac, the Switch) it does not show. The framework now reads only the low byte
|
||||
([`f_pc_method.cpp` in the patch](ps5/patches/switchwaker.patch)). On the Mac, `COS_EVENT_SWEEP_FOLLOW=1` lets
|
||||
`run.sh event-sweep` go on through a stage change an event asks for.
|
||||
|
||||
The crash that 1.0.3 fixes is a third one. A hanging platform of Dragon Roost
|
||||
Cavern whose ropes were cut on an earlier visit is created with them cut, and
|
||||
the rope points are simulated from values nothing has set. A NaN among them
|
||||
reaches the arctangent table as an index: the conversion to an integer gives
|
||||
0x80000000, which the GameCube's 32 bit address wraps back to the first entry
|
||||
and x86-64 turns into a read 4 GB below the table. The lookup now gives the
|
||||
first entry for a NaN or a ratio outside the table ([`c_math.cpp` in the patch](ps5/patches/switchwaker.patch)).
|
||||
It was found from a player's photograph of the console's error message: the
|
||||
two addresses in it, less the load address 0x400000, name `cM_atan2s` and
|
||||
`himo_move` in the release's ELF.
|
||||
|
||||
The crash that 1.0.2 fixes is the same kind of difference. The boss of Dragon
|
||||
Roost Cavern turns towards the player with a step count that is still zero the
|
||||
first time, an integer division by zero: the GameCube and ARM give a quotient
|
||||
and go on, x86-64 raises SIGFPE. The three helpers involved now give 0
|
||||
([`c_lib.cpp` in the patch](ps5/patches/switchwaker.patch)), and so that the next one elsewhere is not a crash,
|
||||
the title handles SIGFPE itself: it finishes the division as ARM does (quotient
|
||||
0, remainder the dividend), logs `integer division fault` with the address, and
|
||||
goes on. The console's code cannot be read at run time, so the build lists
|
||||
every division instruction of the linked title in `res/div-table.bin`
|
||||
([`make-div-table.py`](ps5/tools/make-div-table.py)) and the handler looks the address up; a fault
|
||||
that is not in the list is still a crash with the console's record.
|
||||
`COS_DIV_TEST=1` in `[dev]` divides by zero once at the start to check it. On
|
||||
the Mac, `COS_SAVE_LOAD_WALK=<frames>` makes `run.sh save-load` walk forward
|
||||
from a loaded save (`COS_CARD_DIR` names a card folder holding a player's
|
||||
`.gci`).
|
||||
|
||||
`COS_TEXTURE_DUMPS=1` writes every texture drawn into the cache folder's
|
||||
`texture_dumps`, named as a replacement for it must be named.
|
||||
|
||||
|
||||
@@ -1,3 +1,76 @@
|
||||
diff --git a/game/src/SSystem/SComponent/c_lib.cpp b/game/src/SSystem/SComponent/c_lib.cpp
|
||||
index 0f1f94f..02eaf89 100644
|
||||
--- a/game/src/SSystem/SComponent/c_lib.cpp
|
||||
+++ b/game/src/SSystem/SComponent/c_lib.cpp
|
||||
@@ -8,6 +8,18 @@
|
||||
#include "SSystem/SComponent/c_math.h"
|
||||
#include "dolphin/mtx/mtxvec.h"
|
||||
|
||||
+#if TARGET_PC
|
||||
+// A scale of 0 reaches these (the first cLib_addCalcAngleS2 of d_a_btd.cpp: m5E92 is still 0). The
|
||||
+// GameCube's divw and ARM's sdiv give a quotient without a fault, x86-64's idiv raises SIGFPE: the
|
||||
+// quotient is 0 here, as on ARM.
|
||||
+static inline s32 cLib_div(s32 a, s32 b) {
|
||||
+ return b != 0 ? a / b : 0;
|
||||
+}
|
||||
+#define CLIB_DIV(a, b) cLib_div((a), (b))
|
||||
+#else
|
||||
+#define CLIB_DIV(a, b) ((a) / (b))
|
||||
+#endif
|
||||
+
|
||||
/* 802528A4-802528C4 .text cLib_memCpy__FPvPCvUl */
|
||||
void cLib_memCpy(void* dst, const void* src, unsigned long size) {
|
||||
memcpy(dst, src, size);
|
||||
@@ -160,7 +172,7 @@ void cLib_addCalcPosXZ2(cXyz* pValue, cXyz const& target, f32 scale, f32 maxStep
|
||||
s16 cLib_addCalcAngleS(s16* pValue, s16 target, s16 scale, s16 maxStep, s16 minStep) {
|
||||
s16 diff = target - *pValue;
|
||||
if (*pValue != target) {
|
||||
- s16 step = (diff) / scale;
|
||||
+ s16 step = CLIB_DIV(diff, scale);
|
||||
if (step > minStep || step < -minStep) {
|
||||
if (step > maxStep) {
|
||||
step = maxStep;
|
||||
@@ -191,7 +203,7 @@ s16 cLib_addCalcAngleS(s16* pValue, s16 target, s16 scale, s16 maxStep, s16 minS
|
||||
/* 80253270-802532C4 .text cLib_addCalcAngleS2__FPssss */
|
||||
void cLib_addCalcAngleS2(s16* pValue, s16 target, s16 scale, s16 maxStep) {
|
||||
s16 diff = target - *pValue;
|
||||
- s16 step = diff / scale;
|
||||
+ s16 step = CLIB_DIV(diff, scale);
|
||||
if (step > maxStep) {
|
||||
*pValue += maxStep;
|
||||
} else if (step < -maxStep) {
|
||||
@@ -205,7 +217,7 @@ void cLib_addCalcAngleS2(s16* pValue, s16 target, s16 scale, s16 maxStep) {
|
||||
s32 cLib_addCalcAngleL(s32* pValue, s32 target, s32 scale, s32 maxStep, s32 minStep) {
|
||||
s32 diff = target - *pValue;
|
||||
if (*pValue != target) {
|
||||
- scale = (diff) / scale;
|
||||
+ scale = CLIB_DIV(diff, scale);
|
||||
if (scale > minStep || scale < -minStep) {
|
||||
if (scale > maxStep) {
|
||||
scale = maxStep;
|
||||
diff --git a/game/src/SSystem/SComponent/c_math.cpp b/game/src/SSystem/SComponent/c_math.cpp
|
||||
index 5129f61..8c7e218 100644
|
||||
--- a/game/src/SSystem/SComponent/c_math.cpp
|
||||
+++ b/game/src/SSystem/SComponent/c_math.cpp
|
||||
@@ -111,7 +111,18 @@ static u16 atntable[] = {
|
||||
|
||||
/* 8024609C-802460D0 .text U_GetAtanTable__Fff */
|
||||
u16 U_GetAtanTable(float f0, float f1) {
|
||||
+#if TARGET_PC
|
||||
+ // A NaN reaches this (the cut ropes of d_a_mflft.cpp). The conversion gives 0x80000000 on the
|
||||
+ // GameCube and on x86-64: the GameCube's 32 bit address wraps back to entry 0, x86-64 reads 4 GB
|
||||
+ // below the table and faults. Entry 0 here, as on the GameCube.
|
||||
+ f32 ratio = f0 / f1 * 1024;
|
||||
+ if (!(ratio >= 0.0f && ratio <= 1024.0f)) {
|
||||
+ return atntable[0];
|
||||
+ }
|
||||
+ return atntable[(int)ratio];
|
||||
+#else
|
||||
return atntable[(int)(f0 / f1 * 1024)];
|
||||
+#endif
|
||||
}
|
||||
|
||||
/* 802460D0-80246270 .text cM_atan2s__Fff */
|
||||
diff --git a/game/src/d/d_meter.cpp b/game/src/d/d_meter.cpp
|
||||
index 05ecca5..292ce89 100644
|
||||
--- a/game/src/d/d_meter.cpp
|
||||
@@ -55,6 +128,99 @@ index 05ecca5..292ce89 100644
|
||||
fopMsgM_setPaneData(&i_Meter->field_0x1da8[0], sMainParts1, 'baa');
|
||||
fopMsgM_setPaneData(&i_Meter->field_0x1e18[0], sMainParts1, 'bawe');
|
||||
fopMsgM_setPaneData(&i_Meter->field_0x26a0, sMainParts1, 'bar1');
|
||||
diff --git a/game/src/f_op/f_op_scene_req.cpp b/game/src/f_op/f_op_scene_req.cpp
|
||||
index 08b0cee..0febd75 100644
|
||||
--- a/game/src/f_op/f_op_scene_req.cpp
|
||||
+++ b/game/src/f_op/f_op_scene_req.cpp
|
||||
@@ -35,7 +35,46 @@ static cPhs_State fopScnRq_phase_Done(scene_request_class* i_sceneReq) {
|
||||
return cPhs_NEXT_e;
|
||||
}
|
||||
|
||||
+#if TARGET_PC
|
||||
+#include "f_pc/f_pc_base.h"
|
||||
+#include "f_pc/f_pc_delete_tag.h"
|
||||
+#include <cstdio>
|
||||
+// A scene request that stays in one step: every 300 calls, which step and what the delete queue holds.
|
||||
+static void pc_fopScnRq_watch(scene_request_class* i_sceneReq) {
|
||||
+ static scene_request_class* sReq;
|
||||
+ static int sScene = -2, sNode = -2;
|
||||
+ static unsigned sCalls;
|
||||
+ const int scene = i_sceneReq->mReqPhsProcCls.id, node = i_sceneReq->mCrtReq.mReqPhsProc.id;
|
||||
+ if (i_sceneReq != sReq || scene != sScene || node != sNode) {
|
||||
+ sReq = i_sceneReq;
|
||||
+ sScene = scene;
|
||||
+ sNode = node;
|
||||
+ sCalls = 0;
|
||||
+ return;
|
||||
+ }
|
||||
+ if (++sCalls % 300 != 0) {
|
||||
+ return;
|
||||
+ }
|
||||
+ fprintf(stderr, "[cos] scene-req-watch: %u calls in one step: fade %d, scene step %d, node step %d, proc %d, "
|
||||
+ "delete queue %d, overlap doing %d done %d\n", sCalls, i_sceneReq->mFadeRequest != NULL, scene,
|
||||
+ node, (int)i_sceneReq->mCrtReq.mProcName, (int)g_fpcDtTg_Queue.mSize, (int)fopOvlpM_IsDoingReq(),
|
||||
+ (int)fopOvlpM_IsDone());
|
||||
+ int shown = 0;
|
||||
+ for (node_class* n = g_fpcDtTg_Queue.mpHead; n != NULL && shown < 12; n = n->mpNextNode, shown++) {
|
||||
+ delete_tag_class* tag = (delete_tag_class*)n;
|
||||
+ base_process_class* proc = (base_process_class*)tag->base.mpTagData;
|
||||
+ fprintf(stderr, "[cos] scene-req-watch: deleting proc name %d id %u timer %d\n",
|
||||
+ proc != NULL ? (int)proc->mProfName : -1, proc != NULL ? (unsigned)proc->mBsPcId : 0u,
|
||||
+ (int)tag->mTimer);
|
||||
+ }
|
||||
+ fflush(stderr);
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
static cPhs_State fopScnRq_Execute(scene_request_class* i_sceneReq) {
|
||||
+#if TARGET_PC
|
||||
+ pc_fopScnRq_watch(i_sceneReq);
|
||||
+#endif
|
||||
cPhs_State phase_state = cPhs_Do(&i_sceneReq->mReqPhsProcCls, i_sceneReq);
|
||||
switch (phase_state) {
|
||||
case cPhs_NEXT_e:
|
||||
diff --git a/game/src/f_pc/f_pc_method.cpp b/game/src/f_pc/f_pc_method.cpp
|
||||
index 355f0e7..57070e1 100644
|
||||
--- a/game/src/f_pc/f_pc_method.cpp
|
||||
+++ b/game/src/f_pc/f_pc_method.cpp
|
||||
@@ -5,6 +5,16 @@
|
||||
|
||||
#include "f_pc/f_pc_method.h"
|
||||
|
||||
+#if TARGET_PC
|
||||
+// Some actors declare these methods as returning bool (one byte) and are called here as
|
||||
+// returning int: on x86-64 the rest of the register is whatever was there, so TRUE can read as
|
||||
+// something other than 1 and e.g. a delete is retried for ever (d_a_tag_attention's Mthd_Delete
|
||||
+// at the end of A_mori's MEET_TETORA: the next scene never came). Only the low byte counts.
|
||||
+#define PC_MTD_BOOL(x) ((x) & 0xFF)
|
||||
+#else
|
||||
+#define PC_MTD_BOOL(x) (x)
|
||||
+#endif
|
||||
+
|
||||
/* 8003EF00-8003EF38 .text fpcMtd_Method__FPFPv_iPv */
|
||||
int fpcMtd_Method(process_method_func i_methodFunc, void* i_data) {
|
||||
if (i_methodFunc != NULL)
|
||||
@@ -15,17 +25,17 @@ int fpcMtd_Method(process_method_func i_methodFunc, void* i_data) {
|
||||
|
||||
/* 8003EF38-8003EF5C .text fpcMtd_Execute__FP20process_method_classPv */
|
||||
BOOL fpcMtd_Execute(process_method_class* i_methods, void* i_data) {
|
||||
- return fpcMtd_Method(i_methods->mpExecuteFunc, i_data);
|
||||
+ return PC_MTD_BOOL(fpcMtd_Method(i_methods->mpExecuteFunc, i_data));
|
||||
}
|
||||
|
||||
/* 8003EF5C-8003EF80 .text fpcMtd_IsDelete__FP20process_method_classPv */
|
||||
BOOL fpcMtd_IsDelete(process_method_class* i_methods, void* i_data) {
|
||||
- return fpcMtd_Method(i_methods->mpIsDeleteFunc, i_data);
|
||||
+ return PC_MTD_BOOL(fpcMtd_Method(i_methods->mpIsDeleteFunc, i_data));
|
||||
}
|
||||
|
||||
/* 8003EF80-8003EFA4 .text fpcMtd_Delete__FP20process_method_classPv */
|
||||
BOOL fpcMtd_Delete(process_method_class* i_methods, void* i_data) {
|
||||
- return fpcMtd_Method(i_methods->mpDeleteFunc, i_data);
|
||||
+ return PC_MTD_BOOL(fpcMtd_Method(i_methods->mpDeleteFunc, i_data));
|
||||
}
|
||||
|
||||
/* 8003EFA4-8003EFC8 .text fpcMtd_Create__FP20process_method_classPv */
|
||||
diff --git a/native/src/pc/features/pc_menu.cpp b/native/src/pc/features/pc_menu.cpp
|
||||
index 3a26b60..fb9c5f0 100644
|
||||
--- a/native/src/pc/features/pc_menu.cpp
|
||||
@@ -212,11 +378,128 @@ index aef4e19..c2c3de0 100644
|
||||
debugShotFrameEnd(frame);
|
||||
if (!sShotOn || !wanted(frame)) {
|
||||
return;
|
||||
diff --git a/native/src/pc/harness/sweeps/pc_event_sweep.cpp b/native/src/pc/harness/sweeps/pc_event_sweep.cpp
|
||||
index aeba943..b972772 100644
|
||||
--- a/native/src/pc/harness/sweeps/pc_event_sweep.cpp
|
||||
+++ b/native/src/pc/harness/sweeps/pc_event_sweep.cpp
|
||||
@@ -504,10 +504,23 @@ int demoBank(const char* file) {
|
||||
}
|
||||
|
||||
// A stage change asked for by the running event (or anything else): the run ends here.
|
||||
+bool sFollowing = false;
|
||||
+
|
||||
bool checkStageChange() {
|
||||
if (!dComIfGp_isEnableNextStage()) {
|
||||
return false;
|
||||
}
|
||||
+ // COS_EVENT_SWEEP_FOLLOW=1: go on through the change instead (the run ends with --frames).
|
||||
+ if (getenv("COS_EVENT_SWEEP_FOLLOW") != nullptr) {
|
||||
+ if (!sFollowing) {
|
||||
+ sFollowing = true;
|
||||
+ setDrivenPad(true, 0, 0, 0);
|
||||
+ writef(STDERR_FILENO, "[cos] event-sweep: following the stage change to %s room %d point %d layer %d\n",
|
||||
+ dComIfGp_getNextStageName(), (int)dComIfGp_getNextStageRoomNo(),
|
||||
+ (int)dComIfGp_getNextStagePoint(), (int)dComIfGp_getNextStageLayer());
|
||||
+ }
|
||||
+ return true;
|
||||
+ }
|
||||
detail(" next=%s", dComIfGp_getNextStageName());
|
||||
result("stage-change");
|
||||
stopRun("a stage change");
|
||||
@@ -516,6 +529,9 @@ bool checkStageChange() {
|
||||
} // namespace
|
||||
|
||||
void eventSweepFrame(unsigned int frames) {
|
||||
+ if (sFollowing) {
|
||||
+ return;
|
||||
+ }
|
||||
if (!sChecked) {
|
||||
sChecked = true;
|
||||
if (gConfig.smoke == nullptr || strcmp(gConfig.smoke, "event-sweep") != 0) {
|
||||
diff --git a/native/src/pc/harness/sweeps/pc_save_sweep.cpp b/native/src/pc/harness/sweeps/pc_save_sweep.cpp
|
||||
index 9a9f11f..6720a15 100644
|
||||
--- a/native/src/pc/harness/sweeps/pc_save_sweep.cpp
|
||||
+++ b/native/src/pc/harness/sweeps/pc_save_sweep.cpp
|
||||
@@ -110,6 +110,7 @@ enum State {
|
||||
kTitle, // save-load: START until the name scene
|
||||
kName, // A until the PLAY scene
|
||||
kPlay, // the PLAY scene: wait for the player in the return room
|
||||
+ kWalk, // COS_SAVE_LOAD_WALK: forward from the loaded save
|
||||
kDone,
|
||||
};
|
||||
|
||||
@@ -403,6 +404,8 @@ void compareLoaded() {
|
||||
slot + 1);
|
||||
}
|
||||
|
||||
+unsigned int sWalkFrames = 0;
|
||||
+
|
||||
void loadFrame(unsigned int frames) {
|
||||
++sSince;
|
||||
if (sState == kTitle) {
|
||||
@@ -463,8 +466,34 @@ void loadFrame(unsigned int frames) {
|
||||
}
|
||||
writef(STDERR_FILENO, "[cos] save-load: PASS loaded %s room %d point %d\n", sReturnName, sReturnRoom,
|
||||
sReturnPoint);
|
||||
+ // COS_SAVE_LOAD_WALK=<frames>: go on from the loaded save, the stick held forward and A
|
||||
+ // tapped (through the door ahead), to reproduce a fault a player reports after a save.
|
||||
+ const char* walk = getenv("COS_SAVE_LOAD_WALK");
|
||||
+ if (walk != nullptr && atoi(walk) > 0) {
|
||||
+ sWalkFrames = (unsigned int)atoi(walk);
|
||||
+ sState = kWalk;
|
||||
+ sSince = 0;
|
||||
+ return;
|
||||
+ }
|
||||
pc_exit(PC_EXIT_REACHED);
|
||||
}
|
||||
+ if (sState == kWalk) {
|
||||
+ fopAc_ac_c* player = dComIfGp_getPlayer(0);
|
||||
+ setDrivenPad(true, sSince % kAEvery < 2 ? PAD_BUTTON_A : 0, 0, 127);
|
||||
+ if (sSince % 60 == 0) {
|
||||
+ if (player != nullptr) {
|
||||
+ writef(STDERR_FILENO, "[cos] save-load: walk %u: %s room %d at (%.0f, %.0f, %.0f)\n", sSince,
|
||||
+ dComIfGp_getStartStageName(), (int)fopAcM_GetRoomNo(player), player->current.pos.x,
|
||||
+ player->current.pos.y, player->current.pos.z);
|
||||
+ } else {
|
||||
+ writef(STDERR_FILENO, "[cos] save-load: walk %u: no player\n", sSince);
|
||||
+ }
|
||||
+ }
|
||||
+ if (sSince >= sWalkFrames) {
|
||||
+ writef(STDERR_FILENO, "[cos] save-load: walk done\n");
|
||||
+ pc_exit(PC_EXIT_REACHED);
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
|
||||
} // namespace
|
||||
diff --git a/native/src/pc/runtime/pc_crash.cpp b/native/src/pc/runtime/pc_crash.cpp
|
||||
index 094f649..48d32d3 100644
|
||||
index 094f649..08447d8 100644
|
||||
--- a/native/src/pc/runtime/pc_crash.cpp
|
||||
+++ b/native/src/pc/runtime/pc_crash.cpp
|
||||
@@ -221,6 +221,9 @@ void writeFrames(int fd, void** frames, int n, int exact) {
|
||||
@@ -36,6 +36,10 @@
|
||||
#include <signal.h>
|
||||
#endif
|
||||
|
||||
+#if defined(__PROSPERO__)
|
||||
+#include <cstdlib>
|
||||
+#include <ucontext.h>
|
||||
+#endif
|
||||
#if defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#include <mach/mach.h>
|
||||
@@ -45,6 +49,11 @@
|
||||
#include <sys/uio.h>
|
||||
#endif
|
||||
|
||||
+#if defined(__PROSPERO__)
|
||||
+// The function whose address gives the title's load offset (ps5/tools/make-div-table.py).
|
||||
+extern "C" __attribute__((noinline, used)) void pc_div_table_anchor() {}
|
||||
+#endif
|
||||
+
|
||||
namespace pc {
|
||||
|
||||
namespace {
|
||||
@@ -221,6 +230,9 @@ void writeFrames(int fd, void** frames, int n, int exact) {
|
||||
void writeThreadName(int fd) {
|
||||
#if defined(__SWITCH__)
|
||||
writef(fd, "[cos] thread %llu\n", (unsigned long long)cos_switch_thread_id());
|
||||
@@ -226,14 +509,145 @@ index 094f649..48d32d3 100644
|
||||
#else
|
||||
char name[64] = "";
|
||||
pthread_getname_np(pthread_self(), name, sizeof(name));
|
||||
@@ -399,6 +402,10 @@ void installCrashHandler() {
|
||||
@@ -338,6 +350,14 @@ void writef(int fd, const char* fmt, ...) {
|
||||
if (len >= (int)sizeof(buf)) {
|
||||
len = sizeof(buf) - 1;
|
||||
}
|
||||
+#ifdef __PROSPERO__
|
||||
+ // The console's log takes standard error through stdio only (ps5/src/platform.c).
|
||||
+ if (fd == STDERR_FILENO) {
|
||||
+ fwrite(buf, 1, (size_t)len, stderr);
|
||||
+ fflush(stderr);
|
||||
+ return;
|
||||
+ }
|
||||
+#endif
|
||||
const char* p = buf;
|
||||
while (len > 0) {
|
||||
ssize_t w = write(fd, p, (size_t)len);
|
||||
@@ -394,11 +414,126 @@ void traceLastResource(char* out, size_t size) {
|
||||
out[n - 1] = '\0';
|
||||
}
|
||||
|
||||
+#if defined(__PROSPERO__)
|
||||
+// The game divides integers by zero in places (the first cLib_addCalcAngleS2 of d_a_btd.cpp, before
|
||||
+// c_lib.cpp guarded it): the GameCube's divw and ARM's sdiv give a quotient without a fault, x86-64's
|
||||
+// div/idiv raises SIGFPE. This finishes the instruction as ARM does (quotient 0, remainder the
|
||||
+// dividend) and goes on, with a log line for the first faults. Anything it cannot read as a
|
||||
+// div/idiv gets the default action back, so the console still writes its crash record.
|
||||
+
|
||||
+// The console's code is execute-only, so the handler cannot read the instruction to learn its
|
||||
+// length: the title ships a table of its div/idiv instructions made after the link
|
||||
+// (ps5/tools/make-div-table.py, res/div-table.bin: ELF address, length, operand size).
|
||||
+#pragma pack(push, 1)
|
||||
+struct DivEntry {
|
||||
+ uint32_t address;
|
||||
+ uint8_t length;
|
||||
+ uint8_t size;
|
||||
+};
|
||||
+#pragma pack(pop)
|
||||
+DivEntry* sDivTable = nullptr;
|
||||
+uint32_t sDivCount = 0;
|
||||
+uintptr_t sDivBase = 0; // the load address of ELF address 0
|
||||
+
|
||||
+void loadDivTable() {
|
||||
+ const int fd = open("/app0/res/div-table.bin", O_RDONLY);
|
||||
+ if (fd < 0) {
|
||||
+ writef(STDERR_FILENO, "[cos] no res/div-table.bin: an integer division by zero is a crash\n");
|
||||
+ return;
|
||||
+ }
|
||||
+ struct {
|
||||
+ char magic[8];
|
||||
+ uint64_t anchor;
|
||||
+ uint32_t count;
|
||||
+ } __attribute__((packed)) head;
|
||||
+ if (read(fd, &head, sizeof(head)) == (ssize_t)sizeof(head) && memcmp(head.magic, "COSDIV1", 8) == 0 &&
|
||||
+ head.count > 0 && head.count < 4000000) {
|
||||
+ const size_t bytes = (size_t)head.count * sizeof(DivEntry);
|
||||
+ DivEntry* table = static_cast<DivEntry*>(malloc(bytes));
|
||||
+ size_t got = 0;
|
||||
+ while (table != nullptr && got < bytes) {
|
||||
+ const ssize_t n = read(fd, reinterpret_cast<char*>(table) + got, bytes - got);
|
||||
+ if (n <= 0) {
|
||||
+ break;
|
||||
+ }
|
||||
+ got += (size_t)n;
|
||||
+ }
|
||||
+ if (table != nullptr && got == bytes) {
|
||||
+ sDivBase = reinterpret_cast<uintptr_t>(&pc_div_table_anchor) - (uintptr_t)head.anchor;
|
||||
+ sDivCount = head.count;
|
||||
+ sDivTable = table;
|
||||
+ }
|
||||
+ }
|
||||
+ close(fd);
|
||||
+ writef(STDERR_FILENO, "[cos] division table: %u instruction(s), load offset 0x%llx\n", sDivCount,
|
||||
+ (unsigned long long)sDivBase);
|
||||
+}
|
||||
+
|
||||
+void divFaultHandler(int sig, siginfo_t* info, void* context) {
|
||||
+ (void)info;
|
||||
+ ucontext_t* uc = static_cast<ucontext_t*>(context);
|
||||
+ const uintptr_t rip = (uintptr_t)uc->uc_mcontext.mc_rip;
|
||||
+ const uint64_t address = rip - sDivBase;
|
||||
+ int length = 0, size = 0;
|
||||
+ for (uint32_t lo = 0, hi = sDivCount; lo < hi;) {
|
||||
+ const uint32_t mid = lo + (hi - lo) / 2;
|
||||
+ if (sDivTable[mid].address == address) {
|
||||
+ length = sDivTable[mid].length;
|
||||
+ size = sDivTable[mid].size;
|
||||
+ break;
|
||||
+ }
|
||||
+ if (sDivTable[mid].address < address) {
|
||||
+ lo = mid + 1;
|
||||
+ } else {
|
||||
+ hi = mid;
|
||||
+ }
|
||||
+ }
|
||||
+ if (length == 0) {
|
||||
+ signal(sig, SIG_DFL); // not a listed division: it faults again, into the console's record
|
||||
+ return;
|
||||
+ }
|
||||
+ static unsigned int sCount = 0;
|
||||
+ if (sCount++ < 16) {
|
||||
+ writef(STDERR_FILENO, "[cos] integer division fault at ELF address 0x%llx (%d-byte operand): quotient 0, going on\n",
|
||||
+ (unsigned long long)address, size);
|
||||
+ }
|
||||
+ uint64_t& rax = reinterpret_cast<uint64_t&>(uc->uc_mcontext.mc_rax);
|
||||
+ uint64_t& rdx = reinterpret_cast<uint64_t&>(uc->uc_mcontext.mc_rdx);
|
||||
+ switch (size) {
|
||||
+ case 1: rax = (rax & ~0xFFFFull) | ((rax & 0xFF) << 8); break; // AL = 0, AH = the dividend's low byte
|
||||
+ case 2: rdx = (rdx & ~0xFFFFull) | (rax & 0xFFFF); rax &= ~0xFFFFull; break;
|
||||
+ case 4: rdx = rax & 0xFFFFFFFFull; rax = 0; break;
|
||||
+ default: rdx = rax; rax = 0; break;
|
||||
+ }
|
||||
+ uc->uc_mcontext.mc_rip += length;
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
void installCrashHandler() {
|
||||
#if defined(__SWITCH__)
|
||||
// libnx's exception handler (switch/native/source/cos_switch.cpp) reports crashes; it adds
|
||||
// this state line.
|
||||
cos_switch_set_crash_state_writer(writeState);
|
||||
+#elif defined(__PROSPERO__)
|
||||
+ // PlayStation 5: no handler. The console writes a crash record of its own to klog (registers
|
||||
+ // and a frame-pointer walk), which the port's ps5/tools/run.sh symbolises; a handler here
|
||||
+ // would take the signal first and exit without one.
|
||||
+ // PlayStation 5: no crash handler. The console writes a crash record of its own to klog
|
||||
+ // (registers and a frame-pointer walk), which the port's ps5/tools/run.sh symbolises; a handler
|
||||
+ // here would take the signal first and exit without one. SIGFPE alone is handled, to finish an
|
||||
+ // integer division by zero as the other machines do (divFaultHandler).
|
||||
+ {
|
||||
+ loadDivTable();
|
||||
+ struct sigaction sa = {};
|
||||
+ sa.sa_sigaction = divFaultHandler;
|
||||
+ sa.sa_flags = SA_SIGINFO;
|
||||
+ sigemptyset(&sa.sa_mask);
|
||||
+ sigaction(SIGFPE, &sa, nullptr);
|
||||
+ const char* test = getenv("COS_DIV_TEST");
|
||||
+ if (test != nullptr && atoi(test) > 0) {
|
||||
+ volatile int a = 1234, b = 0;
|
||||
+ volatile int q = a / b;
|
||||
+ volatile int r = a % b;
|
||||
+ writef(STDERR_FILENO, "[cos] div-test: 1234 / 0 = %d, 1234 %% 0 = %d\n", (int)q, (int)r);
|
||||
+ }
|
||||
+ }
|
||||
#else
|
||||
// Alternate signal stack for this (the main) thread, so a stack overflow still reports.
|
||||
static char sAltStack[256 * 1024];
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"conceptId": "99309",
|
||||
"contentBadgeType": 1,
|
||||
"contentId": "UP9000-PPSA99309_00-WINDWAKER0000000",
|
||||
"contentVersion": "01.000.000",
|
||||
"contentVersion": "01.000.003",
|
||||
"downloadDataSize": 256,
|
||||
"gameIntent": {
|
||||
"permittedIntents": [
|
||||
|
||||
@@ -212,3 +212,7 @@ cp "$upstream/native/dsp_hle/LICENSE" "$app/licenses/Dolphin-DSPHLE-LICENSE.txt"
|
||||
cp "$upstream/THIRD_PARTY.md" "$app/licenses/SwitchWaker-THIRD_PARTY.md"
|
||||
cp "$root/.deps/aurora/LICENSE" "$app/licenses/Aurora-LICENSE.txt"
|
||||
cp "$vulkan/LICENSE" "$app/licenses/PS5_Vulkan-LICENSE.txt"
|
||||
# The title's integer division instructions, for the SIGFPE handler that finishes a division by
|
||||
# zero as the GameCube and ARM do (the console's code cannot be read at run time)
|
||||
mkdir -p "$app/res"
|
||||
python3 "$ps5/tools/make-div-table.py" "$work/link/llvm-pie.elf" "$app/res/div-table.bin"
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
# Wind Waker for the PlayStation 5 - the table of the title's integer division instructions.
|
||||
#
|
||||
# ps5/tools/make-div-table.py <linked.elf> <out.bin> [--objdump llvm-objdump] [--nm llvm-nm]
|
||||
#
|
||||
# The game divides integers by zero in places; x86-64 raises SIGFPE where the GameCube and ARM
|
||||
# give a quotient. The port's SIGFPE handler (pc_crash.cpp, divFaultHandler) finishes such an
|
||||
# instruction itself, but the console's code is execute-only, so it cannot read the instruction
|
||||
# to learn its length: this lists every div/idiv of the linked title (address, length, operand
|
||||
# size), and the handler looks the faulting address up. Addresses are the ELF's; the handler
|
||||
# takes the load offset from the anchor function, whose ELF address heads the file.
|
||||
#
|
||||
# File: "COSDIV1\0", u64 anchor address, u32 count, then count sorted entries of
|
||||
# u32 address, u8 length, u8 operand size in bytes (little-endian).
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
import argparse
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ANCHOR = "pc_div_table_anchor"
|
||||
LINE = re.compile(r"^\s*([0-9a-f]+):\s+((?:[0-9a-f]{2} )+)\s*(?:(?:lock|rep\w*|data16|addr32|cs|ds|es|ss|fs|gs)\s+)*(i?div)([bwlq])\s")
|
||||
SIZES = {"b": 1, "w": 2, "l": 4, "q": 8}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("elf")
|
||||
ap.add_argument("out")
|
||||
ap.add_argument("--objdump", default="llvm-objdump")
|
||||
ap.add_argument("--nm", default="llvm-nm")
|
||||
args = ap.parse_args()
|
||||
|
||||
anchor = None
|
||||
for line in subprocess.run([args.nm, args.elf], capture_output=True, text=True, check=True).stdout.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) == 3 and parts[2] == ANCHOR:
|
||||
anchor = int(parts[0], 16)
|
||||
if anchor is None:
|
||||
sys.exit(f"make-div-table: no symbol {ANCHOR} in {args.elf}")
|
||||
|
||||
entries = []
|
||||
dump = subprocess.Popen([args.objdump, "-d", "--no-print-imm-hex", args.elf], stdout=subprocess.PIPE, text=True)
|
||||
for line in dump.stdout:
|
||||
if "div" not in line:
|
||||
continue
|
||||
m = LINE.match(line)
|
||||
if m:
|
||||
entries.append((int(m.group(1), 16), len(m.group(2).split()), SIZES[m.group(4)]))
|
||||
if dump.wait() != 0:
|
||||
sys.exit("make-div-table: objdump failed")
|
||||
if not entries:
|
||||
sys.exit("make-div-table: no division found (objdump's output format changed?)")
|
||||
entries.sort()
|
||||
with open(args.out, "wb") as f:
|
||||
f.write(b"COSDIV1\0" + struct.pack("<QI", anchor, len(entries)))
|
||||
for address, length, size in entries:
|
||||
f.write(struct.pack("<IBB", address, length, size))
|
||||
print(f"make-div-table: {len(entries)} division(s), anchor 0x{anchor:x}: {args.out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user