11 Commits
Author SHA1 Message Date
Gary Riches 83b5a62161 Version 1.0.2: integer divisions by zero no longer end the game
The boss of Dragon Roost Cavern crashed the console as it appeared: its first
cLib_addCalcAngleS2 divides by a step count that is still zero, which the
GameCube and ARM survive and x86-64 answers with SIGFPE.

- c_lib.cpp: the three angle helpers give 0 for a zero scale.
- pc_crash.cpp: on the console SIGFPE is handled; a listed division is
  finished as ARM does (quotient 0, remainder the dividend), logged, and the
  game goes on. The code is execute-only, so the build writes the title's
  division instructions to res/div-table.bin (ps5/tools/make-div-table.py).
  COS_DIV_TEST=1 divides by zero once at the start to check it.
- Mac harness: COS_SAVE_LOAD_WALK walks forward from a loaded save.

Claude-Session: https://claude.ai/code/session_018jvMS8Y8okH8SDZUfd8MP1
2026-10-13 13:30:23 +01:00
Gary Riches fc1563da67 README: real links for the release, sections and sources
Claude-Session: https://claude.ai/code/session_018jvMS8Y8okH8SDZUfd8MP1
2026-10-13 06:32:31 +01:00
Gary Riches 2f91becaa4 README: version 1.0.1, the pack's PlayStation buttons, finding a fault on the console
Claude-Session: https://claude.ai/code/session_018jvMS8Y8okH8SDZUfd8MP1
2026-10-13 06:32:13 +01:00
Gary Riches 5d47aa54c2 Version 1.0.1
Claude-Session: https://claude.ai/code/session_018jvMS8Y8okH8SDZUfd8MP1
2026-10-12 22:56:20 +01:00
Gary Riches 3c816ae645 Version 1.01
Claude-Session: https://claude.ai/code/session_018jvMS8Y8okH8SDZUfd8MP1
2026-10-12 22:54:33 +01:00
Gary Riches aa745f8c4d Fix the hang after the forest cutscene on the console
At the end of A_mori's MEET_TETORA the next scene never came: the old scene's
attention tag could not be deleted. Its delete method returns bool and the
process framework calls it as returning int and compares with TRUE; on x86-64
the rest of the register is undefined, so the delete was retried for ever.
The framework now reads only the low byte of execute, is-delete and delete.

Also: the game's own [cos] lines reach the console's log, a scene request
stuck in one step reports the step and the delete queue, and the event sweep
can follow a stage change (COS_EVENT_SWEEP_FOLLOW=1).

Claude-Session: https://claude.ai/code/session_018jvMS8Y8okH8SDZUfd8MP1
2026-10-12 22:50:21 +01:00
Gary Riches 6bc6d7641d README: a direct link to the texture pack used
Claude-Session: https://claude.ai/code/session_018jvMS8Y8okH8SDZUfd8MP1
2026-10-12 20:28:32 +01:00
Gary Riches 2953584da5 README: a .ciso runs on the console
Claude-Session: https://claude.ai/code/session_018jvMS8Y8okH8SDZUfd8MP1
2026-10-12 17:22:37 +01:00
Gary Riches 14603c312e README: the badges the other ports have
Claude-Session: https://claude.ai/code/session_018jvMS8Y8okH8SDZUfd8MP1
2026-10-12 17:04:42 +01:00
Gary Riches 242f0a1ef1 README: drop the note about the pack's camera-stick picture
Claude-Session: https://claude.ai/code/session_018jvMS8Y8okH8SDZUfd8MP1
2026-10-12 16:36:43 +01:00
Gary Riches e8730bb86c README: the pack's camera-stick picture and how to get the port's own
Claude-Session: https://claude.ai/code/session_018jvMS8Y8okH8SDZUfd8MP1
2026-10-12 16:33:06 +01:00
5 changed files with 524 additions and 17 deletions

No files matched your search

+57 -10
View File
@@ -1,5 +1,9 @@
# Wind Waker (GameCube) for PlayStation 5
[![stars](https://img.shields.io/github/stars/griches/PS5_SwitchWaker?style=flat)](https://github.com/griches/PS5_SwitchWaker/stargazers)
[![release](https://img.shields.io/github/v/release/griches/PS5_SwitchWaker)](https://github.com/griches/PS5_SwitchWaker/releases/latest)
[![licence](https://img.shields.io/badge/licence-GPL--3.0%20as%20built-blue)](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,15 @@ 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.2](https://github.com/griches/PS5_SwitchWaker/releases/tag/v1.0.2)** fixes a crash when the boss of Dragon Roost Cavern appears, and
stops 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
both 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 +36,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 +59,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 +120,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 +138,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 +165,37 @@ 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.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.
+397 -6
View File
@@ -1,3 +1,53 @@
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/d/d_meter.cpp b/game/src/d/d_meter.cpp
index 05ecca5..292ce89 100644
--- a/game/src/d/d_meter.cpp
@@ -55,6 +105,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 +355,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 +486,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];
+1 -1
View File
@@ -10,7 +10,7 @@
"conceptId": "99309",
"contentBadgeType": 1,
"contentId": "UP9000-PPSA99309_00-WINDWAKER0000000",
"contentVersion": "01.000.000",
"contentVersion": "01.000.002",
"downloadDataSize": 256,
"gameIntent": {
"permittedIntents": [
+4
View File
@@ -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"
+65
View File
@@ -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()