Merge branch 'pr9-kula' into merge-pr9

# Conflicts:
#	runtime/src/beetle_libretro.cpp
#	runtime/src/dirty_ram_interp.c
#	runtime/src/memory.c
This commit is contained in:
Matthew Stanley
2026-07-06 10:59:27 -07:00
26 changed files with 1291 additions and 20 deletions
+13
View File
@@ -79,3 +79,16 @@ __pycache__/
# DuckStation oracle (local-only; private fork at git@github.com:mstan/duckstation.git)
/duckstation/
_wip/
# Game discs/EXEs and recompiler output (copyrighted, not distributable).
# Only the per-game config (game.toml) is tracked.
games/*/*
!games/*/game.toml
!games/*/seeds.txt
# Per-game memcards created at runtime
*.mcr
!dummy.*.mcr
# macOS .app bundle (contains recompiled BIOS-derived binary — not distributable)
/dist/
+52
View File
@@ -27,6 +27,58 @@ targets are:
- [TombaRecomp](https://github.com/mstan/TombaRecomp) — *Tomba!*
- [MegaManX6Recomp](https://github.com/mstan/MegaManX6Recomp) — *Mega Man X6*
### This fork — *Kula World* (SCES-01000) on Linux
This fork brings up *Kula World* (a.k.a. *Roll Away*) as a new end-to-end
target and ports the framework (recompiler + runtime + Beetle-PSX oracle) to
Linux. It boots to the title screen and menu, recompiled + interpreted, no
HLE:
<p align="center">
<img src="docs/assets/kula-world-titlescreen.png" alt="Kula World title screen running on PSXRecomp" width="480">
</p>
The bring-up surfaced and fixed five runtime bugs — most of them
game-agnostic framework improvements (see `docs/kula-world-findings.md`):
1. **Dirty-RAM interpreter interrupt latency** — a guest spin-wait living in
dirty RAM (libcd `CdSync`) never polled interrupts, so a pending, enabled
CD IRQ was never taken and the game wedged. Now the interpreter polls at
its per-invocation entry on a global counter — general to any dirty-RAM
spin-loop.
2. **Main-RAM 4× mirror** not modeled (crt0 parks `$sp` at `0x807FFFF8`).
3. **GetID region** hard-coded to `SCEI`; now derived from the disc serial
(Tomba/MMX6 get correct `SCEA` too).
4. **CD status bits** ADPBUSY/BUSYSTS corrected against the oracle.
5. **CD controller version** returned the PSone `0x97`; the SCPH-1001 sub-CPU
reports `0x94 09 19 C0`, which the shell's CD-init requires.
### Now also on macOS (Apple Silicon)
Those **five runtime fixes** are what got *Kula World* actually *playing* — and
they are game-agnostic, so they benefit every target, not just this one. On top
of the earlier Windows→Linux POSIX port, bringing the same build up on **macOS
(Apple Silicon / arm64, AppleClang)** needed only **one** extra build fix plus a
small POSIX cleanup — the recompiler and runtime are otherwise portable C:
1. `runtime/src/autocompile.c` — added `#include <stdlib.h>` for `getenv`
(glibc and `windows.h` leak that declaration; the macOS SDK does not).
2. `recompiler/src/main_bios.cpp` — the compiler-probe's `>NUL` (Windows null
device) is now `#ifdef`'d to `/dev/null` on POSIX (it was misdirecting the
redirect and dropping a stray `NUL` file).
The ucontext fiber backend already had the `_XOPEN_SOURCE`/`_DARWIN_C_SOURCE`
guards it needs on macOS, and SDL2 (Homebrew) picks the **Metal** backend
automatically. Result: BIOS → title → menu → a 3D level that rolls under
control, running natively on Apple Silicon. Full macOS steps in
[`docs/MACOS_BUILD_KULA.md`](docs/MACOS_BUILD_KULA.md).
<p align="center">
<img src="docs/assets/kula-world-macos.png" alt="Kula World running natively on macOS (Apple Silicon)" width="480">
<br>
<em>Kula World, statically recompiled, running natively on macOS (Apple Silicon).</em>
</p>
## Philosophy — toward 100% static recompilation
The goal is simple and absolute: **a PS1 game should run as native code, not be
+167
View File
@@ -0,0 +1,167 @@
# Building & running Kula World (PSXRecomp) on macOS — AI/dev handoff
**Audience:** a developer or AI coding agent on macOS (Apple Silicon or Intel).
Goal: a native macOS build of *Kula World* that boots to its menu and plays in
3D, exactly as it already does on Linux and Windows.
This is the macOS counterpart to [`WINDOWS_BUILD_KULA.md`](WINDOWS_BUILD_KULA.md).
The pipeline (clone → user-supplied BIOS+disc → extract the boot EXE → build the
recompiler → generate BIOS C + game C → build `kula-runtime` → run) is identical;
only the toolchain and a couple of platform specifics differ. Read `CLAUDE.md`
first — the no-interpreter / no-HLE / no-stubs rules are load-bearing.
Verified on: **macOS 15.5, Apple Silicon (arm64), AppleClang 17**. The static
path is portable C (an emulator, not a JIT), so Intel Macs should work too.
---
## 1. Files the USER must supply (copyrighted — NOT in the repo)
Same as the Windows doc. Place them relative to the repo root:
| File | Where it goes | What it is |
|---|---|---|
| `SCPH1001.BIN` | `bios/SCPH1001.BIN` | PS1 BIOS dump, 512 KiB, **SCPH1001** (`md5 924e392ed05558ffdb115408c263dccf`). |
| `Kula World (Europe).bin` | `games/kula/Kula World (Europe).bin` | Your own disc dump (raw 2352-byte/sector, ~255 MB). |
| `Kula World (Europe).cue` | `games/kula/Kula World (Europe).cue` | The cue sheet pointing at the `.bin`. |
Do **not** download these — the user provides their own legal dump.
---
## 2. Toolchain (Homebrew instead of MSYS2)
```bash
brew install sdl2 cmake ninja pkg-config
```
Verify SDL2 is visible to pkg-config (the runtime's CMake uses
`pkg_check_modules(SDL2)` on non-MSVC, which Homebrew's `sdl2.pc` satisfies):
```bash
pkg-config --cflags --libs sdl2
# -I/opt/homebrew/include -I/opt/homebrew/include/SDL2 ... -lSDL2
```
If that prints nothing, add Homebrew's pkgconfig dir to `PKG_CONFIG_PATH`
(`export PKG_CONFIG_PATH="$(brew --prefix)/lib/pkgconfig"`). AppleClang builds
the very large generated C fine; `brew install gcc` is optional and not needed.
---
## 3. Clone, check out the branch, drop in the files
```bash
git clone https://github.com/chrisking1981/psxrecomp_kula_world.git
cd psxrecomp_kula_world
git checkout claude/repo-commit-history-1oayou
```
Submodules are not needed. Now place the three user-supplied files from §1.
---
## 4. Extract the game's boot EXE from the disc
`game.toml` expects `games/kula/SCES_010.00` (604160 bytes, ASCII magic
`PS-X EXE`). It is the `BOOT = cdrom:\SCES_010.00;1` binary at the disc root.
Use `dumpsxiso` (from mkpsxiso), or any tool that reads an ISO9660 filesystem
from a raw Mode-2/2352 PS1 image, or a tiny extractor that walks the ISO9660
root directory and copies the extent (each 2352-byte sector carries 2048 bytes
of payload at a 24-byte offset — Mode 2 Form 1). LBA 24 on this disc.
```bash
# sanity check
head -c 8 games/kula/SCES_010.00 # -> PS-X EXE
```
---
## 5. Build the recompiler
```bash
cmake -S recompiler -B recompiler/build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build recompiler/build -j8
```
Produces `recompiler/build/psxrecomp-bios` and `recompiler/build/psxrecomp-game`.
---
## 6. Generate the C (BIOS once, game once)
```bash
# 6a. Full BIOS -> generated/SCPH1001_full.c + SCPH1001_dispatch.c
# NOTE: use --emit-full with the phase-2 seeds. The bare positional form
# (psxrecomp-bios <bios> <out>) runs the Phase-1a boot-slice validator, which
# does NOT emit the full BIOS the runtime links against.
./recompiler/build/psxrecomp-bios bios/SCPH1001.BIN generated \
--emit-full recompiler/seeds/phase2_ghidra_seeds.json
# 6b. Game -> games/kula/generated/SCES_010.00_full.c + _dispatch.c
./recompiler/build/psxrecomp-game --config games/kula/game.toml
```
Sanity check — both must exist and be multi-MB:
```bash
ls -la generated/SCPH1001_full.c games/kula/generated/SCES_010.00_full.c
```
(One skipped BIOS function, a COP1/FPU opcode `0x11`, is expected — the PS1 has
no FPU. `games/kula/generated/SCES_010.00_full.c` is what makes the
`kula-runtime` target appear in the runtime build.)
---
## 7. Build the runtime (the playable binary)
```bash
cmake -S runtime -B runtime/build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DPSX_LAUNCHER=OFF -DPSX_ENABLE_VULKAN=OFF -DPSX_DEBUG_TOOLS=OFF
cmake --build runtime/build --target kula-runtime -j8
```
Output: `runtime/build/kula-runtime` (native Mach-O, links SDL2; SDL picks the
Metal backend on Apple Silicon). Set `-DPSX_DEBUG_TOOLS=ON` for the TCP debug
server on port 4370 (`set_input`, `screenshot`, `pad_status`, …).
---
## 8. Run and play
From the repo root (so the relative disc path resolves):
```bash
./runtime/build/kula-runtime
```
Console shows `psxrecomp: disc region PAL (serial SCES-01000)`, then a window:
Sony boot → **KULA WORLD** title + menu (1 Player / 2 Player / Scores /
Options). At the title, **Start** exits the attract demo; select **1 Player**
with **X** to reach a level and roll the ball with the arrow keys.
Keyboard: arrows = D-pad, **X** = Cross/confirm, **S** = Circle, **Enter** =
Start, **Right Shift** = Select. An SDL game controller works too.
---
## 9. macOS platform notes
- **The runtime is POSIX** thanks to the Linux port (BSD sockets, `dlopen`,
`__atomic`, ucontext fiber backend). Only two small platform gaps needed
fixing for macOS, both guarded so Windows/Linux stay intact:
1. `runtime/src/autocompile.c` — added `#include <stdlib.h>` for `getenv`.
glibc and `windows.h` leak that declaration transitively; the macOS SDK
does not, so AppleClang errored on the implicit declaration.
2. `recompiler/src/main_bios.cpp` — the compiler-probe used `>NUL`, which is
the Windows null device. On POSIX that both misdirects the redirect and
drops a stray `NUL` file in the cwd; now `#ifdef`'d to `/dev/null`.
- The **ucontext fiber backend** (`runtime/src/psx_fiber.c`) already carried the
`_XOPEN_SOURCE`/`_DARWIN_C_SOURCE` guards needed for `makecontext`/
`swapcontext` on macOS — it compiled and ran with no change.
- **No Xvfb needed** — SDL opens a real window.
- **`~/Downloads` is TCC-protected.** If you keep the repo under `~/Downloads`
and launch via a `.app` bundle or Finder, macOS prompts once for Downloads
access (needed to read the BIOS/disc). Launching from a terminal that already
has file access does not prompt.
+238
View File
@@ -0,0 +1,238 @@
# Building & running Kula World (PSXRecomp) on Windows 11 — AI handoff
**Audience:** an AI coding agent (e.g. Claude Code) running on the user's
Windows 11 machine. Follow this top to bottom. The goal: produce a native
Windows build of Kula World that boots to its menu and plays in 3D, exactly
as it already does on Linux.
---
## 0. Context — what already works and what you are doing
This repo is a fork of Matthew Stanley's **PSXRecomp v4**: a *static
recompiler* that translates the PS1 BIOS (`SCPH1001.BIN`) and a game's MIPS
executable into native **C**, which links into an SDL2 runtime that simulates
the PS1 hardware (CPU state, GPU rasterizer, GTE 3D coprocessor, SPU, CD-ROM).
There is **no interpreter for static code, no HLE, no stubs** — read
`CLAUDE.md` before touching anything; those rules are strict and load-bearing.
**What was done in a prior Linux session (already on this branch):**
1. Ported the runtime's platform layer from Windows-only to cross-platform
(sockets, `dlopen`, atomics, misc WIN32) — all behind `#ifdef` guards, so
**Windows still builds unchanged**.
2. Taught the recompiler to accept Kula World's KUSEG-addressed EXE header.
3. Fixed five real bugs that were wedging Kula at boot/first-level-load:
RAM-mirror folding, per-disc GetID region, CD status bits, the SCPH-1001
CD-controller version, and an interrupt-latency bug in the dirty-RAM
interpreter.
Result on Linux: BIOS → title → menu → **select Arcade → 3D level plays, ball
rolls under control.** Your job is to reproduce that on Windows. None of the
above needs redoing — it is all committed. You are just **building** here.
**Branch to use:** `claude/repo-commit-history-1oayou`
(it contains the Linux port + the five fixes + the Kula config + the
`kula-runtime` build target).
---
## 1. Files the USER must supply (copyrighted — NOT in the repo)
These are `.gitignore`d and will not be in a fresh clone. Ask the user to
place them exactly here (paths are relative to the repo root):
| File | Where it goes | What it is |
|---|---|---|
| `SCPH1001.BIN` | `bios/SCPH1001.BIN` | The PS1 BIOS dump (512 KiB). Must be **SCPH1001** (NTSC-U). |
| `Kula World (Europe).bin` | `games/kula/Kula World (Europe).bin` | The user's own disc dump (raw 2352-byte/sector image, ~255 MB). |
| `Kula World (Europe).cue` | `games/kula/Kula World (Europe).cue` | The cue sheet pointing at the `.bin`. |
Do **not** download these. The user has their own legal dump — they provide
them. If they are missing, stop and ask the user for them; do not proceed.
You will additionally **extract one file from the disc** in step 4.
---
## 2. Prerequisites — MSYS2 / mingw-w64 toolchain
PSXRecomp builds with the **MSYS2 mingw64** GCC toolchain (not MSVC — the
generated C and the runtime assume GCC). If MSYS2 is not installed, install it
from https://www.msys2.org/, then open the **"MSYS2 MinGW 64-bit"** shell and:
```bash
pacman -Syu # update; may ask you to reopen the shell, then run again
pacman -S --needed \
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-cmake \
mingw-w64-x86_64-ninja \
mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-pkgconf \
git
```
Always work from the **MinGW 64-bit** shell so the toolchain is on PATH. If in
doubt, prefix commands with `PATH=/c/msys64/mingw64/bin:$PATH`.
---
## 3. Clone the fork and check out the branch
```bash
git clone https://github.com/chrisking1981/psxrecomp_kula_world.git
cd psxrecomp_kula_world
git checkout claude/repo-commit-history-1oayou
```
**Submodules are NOT needed.** `lib/RmlUi` and `lib/freetype` are only used by
the optional launcher GUI, which we disable. The recompiler's own deps
(`fmt`, `rabbitizer`, `toml11`, `ELFIO`) and the runtime's `sljit` are vendored
in-tree and already present after clone. Do not run `git submodule update`.
Now drop in the three user-supplied files from step 1.
---
## 4. Extract the game's boot executable from the disc
The recompiler translates the game's **EXE**, read from a file. `game.toml`
expects it at `games/kula/SCES_010.00`. It is the boot binary named in the
disc's `SYSTEM.CNF` (`BOOT = cdrom:\SCES_010.00;1`), living at the disc root.
Extract it to `games/kula/SCES_010.00` (should be **604160 bytes**). Options:
- **Recommended:** `dumpsxiso` from **mkpsxiso**
(https://github.com/Lameguy64/mkpsxiso). Run it on the `.cue`/`.bin`, then
copy the extracted `SCES_010.00` into `games/kula/`.
- **Or** any tool that reads an ISO9660 filesystem from a raw Mode-2/2352
PS1 image (the `.bin` is 2352 bytes/sector, not a plain 2048 ISO).
- **Or** write a small extractor: parse the ISO9660 primary volume descriptor
(sector 16), walk the root directory to find `SCES_010.00`, and copy its
extent — remembering each 2352-byte sector carries a 2048-byte user payload
at a 24-byte offset (Mode 2 Form 1).
Verify: `SCES_010.00` starts with the ASCII magic `PS-X EXE`.
---
## 5. Build the recompiler
The recompiler is a standalone CMake project under `recompiler/`:
```bash
cmake -S recompiler -B recompiler/build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build recompiler/build -j8
```
This produces `recompiler/build/psxrecomp-bios.exe` and
`recompiler/build/psxrecomp-game.exe`.
---
## 6. Generate the C (BIOS once, game once)
Both outputs are `.gitignore`d, so you must generate them on this machine.
```bash
# 6a. BIOS -> generated/SCPH1001_full.c + SCPH1001_dispatch.c
./recompiler/build/psxrecomp-bios.exe bios/SCPH1001.BIN generated
# 6b. Game -> games/kula/generated/SCES_010.00_full.c + _dispatch.c
# (reads all paths from games/kula/game.toml, incl. out_dir)
./recompiler/build/psxrecomp-game.exe --config games/kula/game.toml
```
Sanity check afterwards:
```bash
ls -la generated/SCPH1001_full.c games/kula/generated/SCES_010.00_full.c
```
Both must exist and be multi-MB. `games/kula/generated/SCES_010.00_full.c` is
what makes the `kula-runtime` target appear in the runtime build.
---
## 7. Build the runtime (produces the playable binary)
```bash
cmake -S runtime -B runtime/build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DPSX_LAUNCHER=OFF -DPSX_ENABLE_VULKAN=OFF -DPSX_DEBUG_TOOLS=OFF
cmake --build runtime/build --target kula-runtime -j8
```
Output: `runtime/build/kula-runtime.exe`. The recompiled BIOS **code**, the
recompiled game **code**, and the whole runtime are compiled *into* this one
binary. (`PSX_DEBUG_TOOLS=OFF` gives a lean play build; turn it `ON` only if
you later want the TCP debug server on port 4370.)
---
## 8. Run and play
Run **from the repo root** (so the relative BIOS/disc paths resolve):
```bash
./runtime/build/kula-runtime.exe
```
You should see `psxrecomp: disc region PAL (serial SCES-01000)` in the console,
then a window: Sony logo boot → **KULA WORLD** title → menu. If SDL2.dll is not
found, copy `/c/msys64/mingw64/bin/SDL2.dll` next to `kula-runtime.exe` (or keep
the MinGW `bin` on PATH).
**Keyboard controls** (from `pad_from_keyboard` in `runtime/src/main.cpp`):
| Key | PS1 button |
|---|---|
| Arrow keys | D-pad (roll the ball / move cursor) |
| **X** | Cross — **confirm / select** |
| **S** | Circle — back/cancel |
| Z | Square |
| A | Triangle |
| Enter | Start |
| Right Shift | Select |
| Q / W | L1 / R1 |
| E / R | L2 / R2 |
An SDL game controller also works — just plug it in.
To reach gameplay: at the menu press **Down/Up** to highlight **Arcade**, then
**X** to start a level. Roll the ball with the arrow keys.
---
## 9. Notes, gotchas, and honest status
- **This is not one self-contained file.** The `.exe` has all the *code* baked
in, but at runtime it still needs, next to it / under the repo root:
`bios/SCPH1001.BIN`, the disc image (`games/kula/Kula World (Europe).cue` +
`.bin`), `games/kula/game.toml`, and `SDL2.dll`. It also creates
`card1.mcd` / `card2.mcd` for saves. Game assets (levels, textures, audio,
video) live on the disc image and are streamed through the simulated CD-ROM
— the recomp translates the *program*, not the *data*.
- **BIOS must be SCPH1001.** The CD-controller version fix (`94 09 19 C0`) is
specific to that BIOS revision. A different BIOS dump may misbehave.
- **Verified working:** boot → menu → navigation → select Arcade → 3D level →
ball control. **Not yet verified:** completing a level and all mechanics,
audio output, FMV/MDEC video, in-practice memory-card save/load. Treat those
as untested, not as known-good. If something breaks there, that is new work —
diagnose it per the `CLAUDE.md` rules (oracle-verify against Beetle PSX; no
printf, no stubs), don't paper over it.
- **If the recompiler or a build step fails**, fix the tool/root cause — do not
route around it (CLAUDE.md §15).
- Building `psx-beetle` (the Beetle PSX oracle) is **optional** and only needed
for cross-checking against real-hardware behavior, not for playing. Skip it
unless you are debugging a divergence.
---
### One-shot summary for the agent
> Clone the fork, check out `claude/repo-commit-history-1oayou`, install the
> MSYS2 mingw64 toolchain + SDL2, have the user drop in `SCPH1001.BIN` and their
> Kula `.bin`/`.cue`, extract `SCES_010.00` from the disc, build the recompiler,
> generate the BIOS C and game C, build the `kula-runtime` target with the
> launcher/vulkan/debug options off, then run `kula-runtime.exe` from the repo
> root and confirm the menu appears and Arcade launches a 3D level.
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

+46
View File
@@ -0,0 +1,46 @@
# Building psx-beetle on Linux
The psx-beetle oracle builds and runs on Linux with the same wire protocol
as on Windows. Verified 2026-07-02 (Kula World boots to demo gameplay;
ping/screenshot served over the TCP debug protocol).
## beetle-psx checkout
`runtime/src/beetle_libretro.cpp` targets the C++ beetle-psx tree. Upstream
master has since been converted to plain C (`libretro.cpp` -> `libretro.c`,
`PS_CPU` class dropped), which no longer compiles against our integration.
Pin the checkout to the last compatible commit — the same base
`docs/beetle_wtrace_hook.patch` was generated from:
```bash
git clone https://github.com/libretro/beetle-psx-libretro.git beetle-psx
cd beetle-psx
git checkout 5759277b # "audit pass" — last C++-tree base we target
patch -p1 < ../docs/beetle_wtrace_hook.patch
patch -p1 < ../docs/beetle_sio_trace_hook.patch
patch -p1 < ../docs/beetle_cdcmd_trace_hook.patch
```
`beetle_cdcmd_trace_hook.patch` adds a CD-command trace callback (fires per
command dispatch in cdc.cpp) exposed as the `cdrom_cmd_dump` / `cdrom_cmd_reset`
debug commands — used to oracle-diff the Kula World CD read sequence.
`beetle_sio_trace_hook.patch` adds `FrontIO::SetSIOTraceCallback` (fires per
completed SIO byte exchange) — an integration hook beetle_libretro.cpp needs
that predated the committed wtrace patch.
## Build
```bash
# Static lib. On unix the artifact is named mednafen_psx_libretro.so but is
# an ar archive (STATIC_LINKING=1); stage it under the name cmake expects.
make platform=unix STATIC_LINKING=1 HAVE_LIGHTREC=0 -j"$(nproc)"
cp mednafen_psx_libretro.so libmednafen_psx.a
cd ../runtime
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPSX_LAUNCHER=OFF -DPSX_DEBUG_TOOLS=ON
ninja -C build psx-beetle
# Headless run (picks the next free port from 4380 if taken; watch stderr)
xvfb-run -a ./runtime/build/psx-beetle ../bios/SCPH1001.BIN --disc <game.cue>
```
+87
View File
@@ -0,0 +1,87 @@
diff --git a/libretro.cpp b/libretro.cpp
index d2e82bca..e9c867c8 100644
--- a/libretro.cpp
+++ b/libretro.cpp
@@ -508,6 +508,11 @@ PS_SPU *PSX_SPU = NULL;
PS_CDC *PSX_CDC = NULL;
FrontIO *PSX_FIO = NULL;
+/* PSXRecomp oracle hooks: NULL when no listener attached. */
+psxrecomp_wtrace_cb_t g_psxrecomp_wtrace_cb = NULL;
+psxrecomp_fntrace_cb_t g_psxrecomp_fntrace_cb = NULL;
+psxrecomp_cdcmd_cb_t g_psxrecomp_cdcmd_cb = NULL;
+
MultiAccessSizeMem<512 * 1024, uint32, false> *BIOSROM = NULL;
MultiAccessSizeMem<65536, uint32, false> *PIOMem = NULL;
MultiAccessSizeMem<2048 * 1024, uint32, false> *MainRAM = NULL;
diff --git a/mednafen/psx/cdc.cpp b/mednafen/psx/cdc.cpp
index 1daaa5b7..a6b31b66 100644
--- a/mednafen/psx/cdc.cpp
+++ b/mednafen/psx/cdc.cpp
@@ -1318,6 +1318,16 @@ int32_t PS_CDC::Update(const int32_t timestamp)
else
{
const CDC_CTEntry *command = &Commands[PendingCommand];
+ /* PSXRecomp oracle hook: CD command about to execute.
+ * PC of the guest at dispatch time — the command was
+ * WRITTEN earlier (latency), so this is the poll/idle
+ * PC, but it still localizes kernel vs shell vs game. */
+ if(MDFN_UNLIKELY(g_psxrecomp_cdcmd_cb))
+ g_psxrecomp_cdcmd_cb(PendingCommand, ArgsReceiveIn,
+ ArgsReceiveIn > 0 ? ArgsReceiveBuf[0] : 0,
+ ArgsReceiveIn > 1 ? ArgsReceiveBuf[1] : 0,
+ ArgsReceiveIn > 2 ? ArgsReceiveBuf[2] : 0,
+ PSX_CPU ? PSX_CPU->GetRegister(PS_CPU::GSREG_PC, NULL, 0) : 0);
next_time = (this->*(command->func))(ArgsReceiveIn, ArgsReceiveBuf);
PendingCommandPhase = 2;
}
diff --git a/mednafen/psx/psx.h b/mednafen/psx/psx.h
index 704c0a19..fc53362d 100644
--- a/mednafen/psx/psx.h
+++ b/mednafen/psx/psx.h
@@ -137,4 +137,45 @@ extern unsigned psx_gpu_overclock_shift;
extern uint8_t analog_combo[2];
extern uint8_t analog_combo_hold;
+/* PSXRecomp oracle: SIO byte trace callback.
+ * Fired by FrontIO when a complete byte exchange finishes. */
+class FrontIO;
+extern FrontIO *PSX_FIO;
+typedef void (*psxrecomp_sio_trace_cb_t)(uint8_t tx, uint8_t rx, uint16_t ctrl);
+
+/* PSXRecomp oracle: RAM write trace callback.
+ * Fired by PS_CPU::SB/SH/SW after the write completes.
+ * addr = virtual address (caller masks for physical filter)
+ * value = stored value (sized by `size`)
+ * pc = PC of the executing store instruction
+ * ra = $ra (GPR[31]) at time of store, for one frame of caller context
+ * size = 1, 2, or 4 (byte / halfword / word) */
+typedef void (*psxrecomp_wtrace_cb_t)(uint32_t addr, uint32_t value,
+ uint32_t pc, uint32_t ra, uint8_t size);
+extern psxrecomp_wtrace_cb_t g_psxrecomp_wtrace_cb;
+
+/* PSXRecomp oracle: function-call (JAL/JALR/J/JR) trace callback.
+ * Fired by PS_CPU on every J/JAL/JR/JALR retire, BEFORE the link write.
+ * Caller filters by target_pc to keep volume bounded.
+ * caller_pc = PC of the J/JAL/JR/JALR instruction
+ * target_pc = computed branch target
+ * parent_ra = GPR[31] at time of call (parent's return address)
+ * a0, a1 = argument registers at time of call
+ * kind = 1=J, 2=JAL, 3=JR, 4=JALR */
+typedef void (*psxrecomp_fntrace_cb_t)(uint32_t caller_pc, uint32_t target_pc,
+ uint32_t parent_ra, uint32_t a0,
+ uint32_t a1, uint8_t kind);
+extern psxrecomp_fntrace_cb_t g_psxrecomp_fntrace_cb;
+
+/* PSXRecomp oracle: CD command trace callback.
+ * Fired by PS_CDC just before a command handler executes.
+ * cmd = command byte (0x01 GetStat, 0x02 Setloc, 0x06 ReadN, 0x09 Pause,
+ * 0x0A Init, 0x0E Setmode, 0x15 SeekL, 0x1A GetID, 0x1B ReadS, ...)
+ * nargs = number of argument bytes
+ * arg0..arg2 = first three argument bytes (0 if absent) */
+typedef void (*psxrecomp_cdcmd_cb_t)(uint8_t cmd, uint8_t nargs,
+ uint8_t arg0, uint8_t arg1, uint8_t arg2,
+ uint32_t pc);
+extern psxrecomp_cdcmd_cb_t g_psxrecomp_cdcmd_cb;
+
#endif
+35
View File
@@ -0,0 +1,35 @@
diff --git a/mednafen/psx/frontio.cpp b/mednafen/psx/frontio.cpp
index a35b022f..02e7ce0b 100644
--- a/mednafen/psx/frontio.cpp
+++ b/mednafen/psx/frontio.cpp
@@ -783,6 +783,10 @@ int32_t FrontIO::Update(int32_t timestamp)
ReceiveInProgress = false;
ReceiveBufferAvail = true;
+ /* PSXRecomp oracle hook: byte exchange complete. */
+ if(MDFN_UNLIKELY(sio_trace_cb))
+ sio_trace_cb(TransmitBuffer, ReceiveBuffer, (uint16_t)Control);
+
if(Control & 0x800)
{
istatus = true;
diff --git a/mednafen/psx/frontio.h b/mednafen/psx/frontio.h
index 8a67f5af..d9a260ca 100644
--- a/mednafen/psx/frontio.h
+++ b/mednafen/psx/frontio.h
@@ -95,8 +95,15 @@ class FrontIO
int StateAction(StateMem* sm, int load, int data_only);
+ /* PSXRecomp oracle hook: fires once per completed SIO byte exchange
+ * (tx byte sent, rx byte received, current Control register). */
+ typedef void (*sio_trace_cb_t)(uint8_t tx, uint8_t rx, uint16_t ctrl);
+ void SetSIOTraceCallback(sio_trace_cb_t cb) { sio_trace_cb = cb; }
+
private:
+ sio_trace_cb_t sio_trace_cb = nullptr;
+
void DoDSRIRQ(void);
void CheckStartStopPending(int32_t timestamp, bool skip_event_set = false);
+331
View File
@@ -0,0 +1,331 @@
# Kula World (SCES-01000) — bring-up findings
Status: SOLVED — Kula World boots to its title screen + menu (Arcade / Time
Trial / Load Game). Five bugs found and fixed this session; the final one was
the wedge.
## Setup
- `games/kula/game.toml` — SCES-01000, load 0x80011000, entry 0x80069F60
(KUSEG header addresses normalized by the recompiler; see the PS-X EXE
parser change).
- Recompile: 5297 functions, generated C compiles clean.
- Runtime target `kula-runtime` (runtime/CMakeLists.txt), Linux + Xvfb.
- Oracle: `psx-beetle` (docs/beetle-linux.md) boots the same dump + BIOS
to demo gameplay — ground truth for every comparison below.
## Bug 1 — RAM mirror not modeled (FIXED)
crt0 parks $sp in the 4th RAM mirror (0x807FFFF8). Guest accesses to the
0x00200000..0x007FFFFF mirrors fell into the open-bus no-op: stack writes
vanished, $ra read back 0, game jumped to address 0 at frame ~891.
Fix: `psx_phys_addr()` folds the 4x mirror before the RAM bounds check
(runtime/src/memory.c). Diagnosed via the null-dispatch capture-freeze on
the fntrace ring (runtime/src/fntrace.c).
## Bug 2 — GetID region hardcoded (FIXED)
GetID's last four response bytes were hardcoded 'SCEI' (NTSC-J). The
kernel CD driver revalidates disc region (ReadTOC + GetID) after the
game's first file load; a PAL disc reporting 'SCEI' threw it into an
endless GetStat/Init loop — CD froze at sector 307 (LBA 318, just before
the HIRO level directory at LBA 319), black screen.
Fix: `cdrom_set_disc_scex()` fed at launch from the disc's SYSTEM.CNF
serial via the existing disc_identity module (PAL->SCEE etc). Verified:
"disc region PAL (serial SCES-01000)"; the game advanced from a hard pin
at frame 890 to 11000+, CD position reached LBA 319.
## Bug 3 — game's CD-init retry loop never completes (OPEN)
After the region fix the game runs its main loop (VBLANK ticks,
I_MASK=0x0D, funcs cycling) but never renders: GPU draws frozen at 10054,
no new GPU DMA, no ReadN ever issued, sectors frozen at 307.
The game's OWN CD driver (game code, all commands issued from pc 0x63A14,
not the BIOS) runs a slow retry state machine, ~150 frames (2.5 s) between
every command:
frame 890 : BIOS ReadTOC + GetID (region revalidation, now passes)
frame 1040 : GetStat -> stat 0x02
frame 1190 : Init -> stat 0x02
frame 1438 : GetStat -> stat 0x02
frame 1586 : Init -> stat 0x02
... GetStat/Init alternating, then transitions to ...
frame 2971+: Setloc(LBA 16 = ISO PVD) -> Pause, repeating 28x
KEY FINDING: every CD response is CORRECT. GetStat/Init/Pause all return
stat=0x02 (motor on, no error, idle) — exactly what a healthy idle drive
reports — and the INT3/INT2 ACK/COMPLETE pair fires for each. The game
reads the right bytes and STILL re-loops. So this is NOT a wrong-response
-value bug (unlike bug 2).
Because the responses are correct, the game must be waiting on a TIMING or
EVENT condition, not a status value:
- a per-poll counter / elapsed-time threshold it never reaches, or
- an interrupt/event delivery (the Init INT2 COMPLETE, or a CD-IRQ-set
"command done" flag its ISR latches) that is subtly mis-delivered, so
the main loop's "init done" check never trips and it retries forever.
The 2.5 s cadence is the tell: that is a retry-with-timeout, i.e. the game
issues a command, waits ~2.5 s for a completion signal, times out, retries.
The signal it waits for is what our controller/IRQ path delivers wrong.
### Oracle-diff result (DONE — divergence localized)
Built a CD-command trace hook into psx-beetle (beetle_cdcmd_trace_hook.patch:
a callback in cdc.cpp's command dispatch + `cdrom_cmd_dump` / `cdrom_cmd_reset`
debug commands on the oracle) and captured its boot->demo CD command stream.
The oracle's game reads level data with a clean per-sector pattern:
Setloc(LBA) -> SeekL -> Setmode(0x80) -> ReadN -> Pause (x22 ReadN)
plus GetlocL (0x10) position polling (x89). It issues SeekL 18x and ReadN 22x.
Our runtime's game, in the wedge window, issues **NEITHER SeekL NOR ReadN** —
only GetStat/Init/Setloc/Pause. So our game took an error/retry branch the
oracle never enters.
CRUCIAL timeline fact: our runtime DID deliver 307 sectors successfully during
early boot (so ReadN + sector-data delivery WORK). Reading only stops at
frame ~890 — the moment the BIOS does its mid-game region revalidation
(ReadTOC + GetID). After that revalidation the game can never resume reading;
it drops into the GetStat/Init/Setloc/Pause retry loop and stays there.
So the root cause is narrowed to: the BIOS ReadTOC+GetID revalidation (or the
CD-controller state it leaves behind — drive repositioned to lead-in / TOC,
stat/mode reset) prevents the game's CD driver from resuming reads. The region
fix got the revalidation to PASS, but the post-revalidation CD state still
breaks the resume. The oracle does not perform this mid-game revalidation in
the same way, so it reads continuously.
### Sharper localization (DONE) — spurious BIOS disc re-identify
Full oracle command trace confirms: the oracle NEVER issues ReadTOC (0x1E).
Our runtime issues it at frame 890, from the BIOS (Pause@BFC05FE8 ->
ReadTOC@BFC0D760 -> GetID@BFC0D7F0). The sector reads right before are clean
(stat=0x22 motor+read, no error/shell/seek bits), so this is NOT triggered by
a CD error we injected.
Since BOTH runtimes execute the SAME BIOS bytes, our runtime reaching the
BFC0Dxxx disc-identify path while the oracle never does means the BIOS/game
takes a DIFFERENT BRANCH — driven by state, not by the CD hardware sim:
- most likely a kernel "disc already identified" flag that our earlier boot
left unset/cleared (so the BIOS re-identifies), where the oracle's is set;
- or a recompiler-level game-logic divergence that routes our game into a
BIOS disc service the oracle's run never calls.
After the (now-passing) re-identify, the game's own CD driver cannot resume:
it drops into GetStat/Init then Setloc(16)/Pause and never issues SeekL/ReadN
again — the visible wedge.
Next tool: instruction/branch-level first-divergence hunt between the two
processes (CLAUDE.md's find_first_divergence) to find where the game/BIOS
first branches differently around frame 890. Then either set/preserve the
kernel disc-identify flag correctly, or fix the recompiled branch. The CD
command hook (cdrom_cmd_dump) and the fntrace/wtrace rings on both sides are
the instruments; this is a dedicated next-session task.
### Round 3 narrowing (self-stop trap) — the boot flows diverge from power-on
New tool: PSX_CD_TRAP_CMD=<byte> [PSX_CD_TRAP_NTH=<n>] makes the runtime
SIGSTOP itself the instant the Nth matching CD command dispatches (gdb
conditional breakpoints slow the emu ~100x; the self-stop costs nothing).
Attach gdb to the frozen process at leisure.
Findings from trapping ReadTOC #1 and #2 plus a full command-history dump
(command_history ring, 8192 entries, survives the data-event flood):
1. ReadTOC #1 (frame ~545) is the SHELL's disc check (pcs 0xBFC3Fxxx /
0xBFC40xxx; RAM at 0x56FF0 still holds shell bytes — EXE not loaded yet).
Legitimate.
2. ReadTOC #2 (frame ~890, the pre-wedge one) is issued from the BIOS BOOT
FLOW itself — guest stack at the trap holds ONLY BIOS-ROM return
addresses (Main/StartKernel chain -> 0xBFC0D5A4 -> 0xBFC0D72C), zero game
frames. It is the boot's post-EXE-load disc re-check, not a game call.
3. OUR full boot command stream (from power-on):
Test[20], GetStat, GetID, ReadTOC, GetStat, GetID, <- shell check
license sectors LBA 4-5, GetID,
Init, PVD/dir reads, per-sector EXE load ... Pause(f889),
ReadTOC+GetID (f890, irq-masked, polled) <- boot re-check
ORACLE's full stream (same SCPH1001 bytes!):
Init, Init, GetTN, GetID, Setmode(0), PVD/dir reads,
Init x3, per-sector EXE load ... -> game reads, NO Test, NO ReadTOC ever.
4. So the divergence does NOT start at frame 890 — the flows differ from the
FIRST command: the oracle's kernel does an early CdInit (Init,Init,GetTN)
that our recompiled BIOS never issues, and our shell uses the
Test+ReadTOC-based check the oracle never runs. Same ROM bytes, different
path => either a recompiled-BIOS branch divergence in early boot, or a
hardware-sim input (timer/SPU/CD status read) steering an early branch
differently.
Next session: first-divergence hunt anchored at the FIRST CD command
(oracle: Init; ours: Test at f539). Instrument the early boot branch that
selects between these kernel CD-init paths. The 0xBFC0Dxxx CD-driver region
is not covered by docs/psx_bios_disasm.txt — needs Ghidra (source of truth
#2) or manual decode of those ROM bytes. The game-side wedge (GetStat/Init
retry loop) is downstream of this boot-flow divergence.
### Round 4 — status-register fix + boot-diff instrumentation status
Fixed vs the oracle source (beetle cdc.cpp Read A==0): our 0x1F801800
status register raised bit 2 (ADPBUSY, "XA-ADPCM playing") permanently —
it must idle at 0 — and never raised bit 7 (BUSYSTS, command written but
response not ready; now mapped to our queued-command window). Verified the
fix does NOT change the boot command stream, so ADPBUSY was not the branch
input steering the early boot; the fix stands on correctness regardless.
Environment note: the "everything got 40x slower" mystery was real and
environmental — leftover wedged instances before a container restart. A
clean baseline runs ~50 fps and reaches the frame-890 re-check in ~13 s.
Rule-15 lesson: measure the baseline before blaming the tool.
Instrumentation gap found while diffing boots: the oracle's cdcmd hook has
no guest PC, and reconstructing command writes from wtrace (index-register
tracking) is unreliable because IRQ handlers interleave index changes.
Also learned: pc 0x63A14 is simply the game driver's generic
write-byte-to-CD-register routine (same pc for all registers, on the
oracle too) — not specific to the retry loop.
NEXT (small, sharp): extend the beetle cdcmd hook with the guest PC
(PS_CPU exposes it), recapture both boot command streams with PCs, and
diff the first divergent command's call site. Then disassemble that
site's branch (capstone is installed) and inspect its inputs on both
sides. All tooling for this exists after this session: self-stop trap,
command-history ring parse, oracle cdcmd dump, oracle wtrace/fntrace.
### Round 5 — ROOT MECHANISM FOUND: kernel flag [0xDFFC] drives the frame-890 ReadTOC
New tool: PSX_FNTRACE_ALL=1 records every dispatch from power-on (fntrace.c),
so the ring holds the earliest boot execution for offline diffs.
Full mechanistic chain of the wedge, traced with the self-stop trap +
fntrace-all + a gdb watchpoint + capstone disassembly of the ROM:
1. The frame-890 ReadTOC is issued by the kernel CD-open function at
0xBFC0D570 (called from the boot EXE-load path at 0xBFC06C78 / 0xBFC06E98,
a0=name ptr, a1=1). Its body:
lw $t7, -0x2004($t6) ; t7 = [0xA000DFFC]
beqz $t7, skip_readtoc ; flag==0 -> SKIP ReadTOC
jal 0xBFC0D72C ; flag!=0 -> ReadTOC (0x1E) then GetID
2. Kernel flag [0xDFFC] (physical 0xDFFC, just below the kernel heap at
0xE000) is the discriminator:
ours : [0xDFFC] = 1 at frame 890 -> ReadTOC -> wedge
oracle : [0xDFFC] = 0 at the decision -> skip -> reads continue
Confirmed: at frame 539 (first CD cmd) [0xDFFC]==0 on ours; it flips to 1
at frame 541.
3. The setter is func_1FC1DF50 (ROM 0xBFC1DF50), called from SHELL code
(ra=0x80036024). Its store `sw $t6,-0x2004($t7)` with $t6=1 writes
[0xDFFC]=1 on the a2==7 path; a sibling path writes 0 (0xBFC1DF5C).
4. The shells diverge: our shell runs functions at RAM 0x35000-0x39000
(reaching the flag-setter at 0x36024, issuing Test[0x20]/ReadTOC), while
the oracle's shell runs 0x30D00-0x33200 (issuing Init/GetTN) and never
calls the flag-setter. Same verbatim shell bytes (RAM 0x30D00 == ROM
0x18D00 confirmed) -> a BRANCH divergence, driven by an input, not code.
So bug 3's root is: our shell takes a different early branch, calls the CD
control func that sets [0xDFFC]=1, and the boot's CD-open then does an extra
ReadTOC that breaks the game's CD reads. The oracle's shell never sets the
flag. REMAINING: the single shell branch (in 0x30000-0x35DC8) that sends our
shell down the 0x36000 path vs the oracle's 0x30000 path, and the hardware/
memory input it reads. That input is the true first divergence.
### Round 6 — version fix removes the spurious ReadTOC (boot now matches oracle)
func_1FC1DF50 (the shell CD-init) sets [0xDFFC]=1 when the CD controller
version byte (Test 0x20, response[0]) is >= 0x95. Our cdrom.c returned the
PSone-era 0x97 0x01 0x10 0xC2 (copied from beetle, which hardcodes it for all
BIOSes) — wrong for the SCPH-1001 BIOS, whose real 1994 sub-CPU reports
0x94 0x09 0x19 0xC0 (high byte 0x94 < 0x95). Fixed cdrom.c Test 0x20 to the
SCPH-1001 value.
RESULT: [0xDFFC] no longer becomes 1 (reads 0x6458, a pointer, like the
oracle's 0x3660); the frame-890 ReadTOC is GONE; the boot command stream now
matches the oracle's (Setloc->SeekL->Setmode->ReadN->Pause x8, no ReadTOC).
This is a genuine correctness fix and closes the boot-flow divergence.
BUT the game still wedges: from ~frame 1039 the GAME's own CD driver (pc
0x63A14, game code) loops GetStat/Init every ~150 frames and never proceeds
to SeekL/ReadN. So the frame-890 ReadTOC was NOT the wedge cause (round 5's
conclusion was wrong) — it was a real but separate inaccuracy. The true wedge
is downstream, in the GAME's CD-init retry, and persists with the boot now
oracle-matched. funcs still cycle (0xf40/0x2dbc) and frames advance, so it is
a spin-wait: the game polls a condition its CD-init never satisfies. NEXT:
diff the game-phase CD command/response sequence (pc 0x63A14) against the
oracle's game, which does the same GetStat/Init but then proceeds to read.
### Round 7 — remaining wedge localized: CD IRQ pending+enabled but never serviced
With the boot fixed, the visible wedge is the game's libcd CD-init retry.
Live state during the loop (gdb):
i_stat = 0x05 (bit0 VBLANK + bit2 CDROM) <- CD IRQ IS raised to the CPU
i_mask = 0x0d (bits 0,2,3 enabled) <- CD IRQ IS enabled
cdrom: irq_flag=0x03 (INT3 unacked), response_count=1, response_read=0
funcs cycle 0xf40 (ReturnFromException) + 0x2dbc -> CPU DOES take exceptions
So a CD command completed (INT3 + response waiting), the CDROM interrupt is
pending AND unmasked, and the CPU is taking exceptions (VBLANK bit0 gets
cleared each frame) -- yet the CDROM bit (bit2) is NEVER cleared and the
response FIFO is never drained. The game's/libcd's CD interrupt handler in the
BIOS IRQ chain (IntRP) is not servicing the CD IRQ. libcd's CdSync therefore
never completes -> the GetStat/Init retry loop -> black screen.
This is a distinct, deeper issue from everything fixed today (it is a BIOS-
IRQ-chain / libcd-handler / possibly dynamic-handler-install matter, CLAUDE.md
rule 18 territory), not a CD-response-value bug. NEXT: inspect the BIOS IntRP
handler chain and whether libcd's CD handler is installed + run; check our CD
IRQ presentation vs the BIOS root handler's expectations (present_cdrom_irq
generation latch, and whether the handler that should drain the response FIFO
and ack is being dispatched at all).
## Session tally (fixes shipped)
- Bug 1 RAM mirror (memory.c) — FIXED
- Bug 2 GetID region (cdrom.c + main.cpp) — FIXED
- Bug 3a CD status bits ADPBUSY/BUSYSTS (cdrom.c) — FIXED (correctness)
- Bug 3b CD controller version 0x94.. for SCPH-1001 (cdrom.c) — FIXED; removes
the spurious boot ReadTOC, boot command stream now matches the oracle
- Bug 3c (OPEN) libcd CD-IRQ not serviced -> game CD-init never completes
Tooling built: null-dispatch fntrace freeze, PSX_CD_TRAP_CMD self-stop trap
(runtime + oracle), PSX_FNTRACE_ALL boot trace, oracle cdrom_cmd_dump hook,
Linux port of runtime+oracle, beetle-linux build docs + patches.
### Round 8 — ROOT CAUSE FOUND AND FIXED: dirty-RAM interpreter interrupt latency
The wedge: at ~frame 1039 the game froze in bursts of ~14s, then advanced.
During each freeze:
SR = 0x40000401 (IEc=1, IM2=1) -> interrupts enabled + hw line unmasked
Cause = 0x400 (IP2=1) -> hardware interrupt pending
i_stat & i_mask = 0x5 -> VBLANK + CDROM pending and enabled
in_exception=0, cooldown irrelevant
total_checks FROZEN -> psx_check_interrupts is NOT being called
g_dirty_ram_insns_run advancing ~15M/s, blocks_run ~2M/s
So the MIPS interrupt-take condition (IEc && IM2 && IP2) was fully satisfied,
yet no interrupt was taken because psx_check_interrupts was never called. The
CPU was in the DIRTY-RAM INTERPRETER running libcd's CdSync spin-wait (a guest
loop that lives entirely in dirty RAM), re-entering the interpreter once per
short (~7-insn) block. The interpreter's only interrupt poll was on the
local-dirty-flow path gated by the PER-INVOCATION `(insns_executed & 0xFFF)`
counter, which for short blocks never reaches 0x1000 and, when the loop exits
to dispatch each iteration, is never reached at all. So the spin ran millions
of instructions with no interrupt poll; the CD IRQ that sets libcd's wait-flag
was never serviced; the loop only broke when a rare timeout re-dispatched.
FIX (runtime/src/dirty_ram_interp.c): poll interrupts at the interpreter's
per-invocation entry, gated by a GLOBAL invocation counter (every 64 entries),
with cpu->pc set so the resume is coherent. This is the interpreter analogue
of a static block-leader poll. psx_check_interrupts runs the handler and
returns with registers restored, so the loop safely continues and immediately
sees its wait-flag set.
RESULT: Kula World boots to the title screen and menu (Arcade / Time Trial /
Load Game / Back). Pixels on screen — bug 3 SOLVED.
Why this only bit Kula and not Tomba/MMX6: Kula uses the BIOS/libcd CD path
whose CdSync spin-waits in a dirty-RAM loop; the working titles use their own
CD drivers / loop shapes that hit the interrupt poll differently. This fix is
general — any dirty-RAM guest spin-loop now yields to interrupts.
+26
View File
@@ -0,0 +1,26 @@
# Kula World (Europe) — SCES-01000
# Values read from the PS-X EXE header of SCES_010.00 (extracted from the
# disc root, LBA 24). Note: the header stores KUSEG addresses (0x00011000);
# the parser normalizes them to KSEG0.
[game]
name = "Kula World"
id = "SCES-01000"
exe = "games/kula/SCES_010.00"
load_address = "0x80011000"
entry_pc = "0x80069F60"
text_size = "0x93000"
stack_base = "0x801FFFF0"
disc = "games/kula/Kula World (Europe).cue"
[recompiler]
# No Ghidra seeds yet — discovery walks the call graph from entry_pc alone.
seeds = "games/kula/seeds.txt"
out_dir = "games/kula/generated"
strict = true
[runtime]
debug_port = 4370
window_title = "Kula World (PSXRecomp)"
controller = "digital"
memcard_dir = "."
View File
+9 -1
View File
@@ -304,7 +304,15 @@ std::string find_c_compiler(const std::optional<std::string>& cli_override) {
static const std::array<const char*, 3> candidates = { "gcc", "clang", "cl" };
for (const char* c : candidates) {
// Probe with --version. We don't parse output; non-zero exit means not present.
const std::string probe = std::string(c) + " --version >NUL 2>&1";
// The null device differs by platform: NUL on Windows, /dev/null on
// POSIX. Using ">NUL" on macOS/Linux both misdirects the redirect and
// leaves a stray file named "NUL" in the cwd.
#ifdef _WIN32
const std::string null_dev = "NUL";
#else
const std::string null_dev = "/dev/null";
#endif
const std::string probe = std::string(c) + " --version >" + null_dev + " 2>&1";
if (std::system(probe.c_str()) == 0) {
return c;
}
+16
View File
@@ -160,6 +160,22 @@ std::optional<PS1Executable> PS1ExeParser::parse_buffer(
// Copy header (first 2048 bytes)
std::memcpy(&exe.header, buffer.data(), sizeof(PS1ExeHeader));
// Some EXEs (e.g. Kula World SCES-01000) store KUSEG addresses
// (0x00011000) instead of KSEG0 (0x80011000). KUSEG 0x0-0x1FFFFFFF
// mirrors the same physical RAM, so normalize to KSEG0 before
// validation. Only remap addresses that fall inside the 2 MB RAM
// mirror; 0 stays 0 (unused fields like initial_gp).
auto to_kseg0 = [](uint32_t& addr) {
if (addr != 0 && addr < 0x00200000) addr |= 0x80000000;
};
to_kseg0(exe.header.initial_pc);
to_kseg0(exe.header.initial_gp);
to_kseg0(exe.header.load_address);
to_kseg0(exe.header.memfill_start);
to_kseg0(exe.header.initial_sp);
to_kseg0(exe.header.initial_fp);
to_kseg0(exe.header.stack_base);
// Validate header
if (!validate_header(exe.header, error_msg)) {
return std::nullopt;
+20
View File
@@ -113,3 +113,23 @@ if(NOT MSVC AND EXISTS "${BEETLE_LIB}")
elseif(NOT MSVC)
message(STATUS "psx-beetle: libmednafen_psx.a not found - skipping psx-beetle")
endif()
# ---- Kula World target (SCES-01000) ----
#
# Only defined when the locally-recompiled game output exists (the disc dump,
# EXE, and generated C are untracked — see games/*/* in .gitignore), so a
# fresh clone without the game still configures cleanly.
set(KULA_GENERATED "${PSXRECOMP_ROOT}/games/kula/generated")
if(EXISTS "${KULA_GENERATED}/SCES_010.00_full.c")
psxrecomp_add_runtime_target(kula-runtime
GAME_GENERATED_FULL_C "${KULA_GENERATED}/SCES_010.00_full.c"
GAME_GENERATED_DISPATCH_C "${KULA_GENERATED}/SCES_010.00_dispatch.c"
DEBUG_PORT 4370
WINDOW_TITLE "Kula World (PSXRecomp)"
DEFAULT_BIOS_PATH "${PSXRECOMP_ROOT}/bios/SCPH1001.BIN"
DEFAULT_GAME_CONFIG_PATH "${PSXRECOMP_ROOT}/games/kula/game.toml"
)
else()
message(STATUS "kula-runtime: games/kula/generated/SCES_010.00_full.c not found - skipping")
endif()
+3
View File
@@ -8,6 +8,9 @@ extern "C" {
#endif
void cdrom_init(const char* cue_path);
/* Disc license region for GetID ("SCEE"/"SCEA"/"SCEI"); derived from the
* mounted disc's boot serial at launch. Defaults to "SCEA" (SCPH1001). */
void cdrom_set_disc_scex(const char scex[4]);
/* Set disc speed multiplier immediately. divisor=0 → instant, 1 → 1x, 2 → 2x. */
void cdrom_set_speed(int divisor);
+1
View File
@@ -5,6 +5,7 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h> /* getenv — declared here; glibc/windows.h leak it, macOS SDK does not */
#include <string.h>
#ifdef _WIN32
+46
View File
@@ -67,6 +67,15 @@ extern uint32_t beetle_get_sio_trace(uint32_t *out_seq, uint8_t *out_tx,
extern uint32_t beetle_get_sio_trace_total(void);
extern void beetle_reset_sio_trace(void);
/* CD command trace */
extern uint32_t beetle_get_cdcmd_trace(uint32_t *out_seq, uint8_t *out_cmd,
uint8_t *out_nargs, uint8_t *out_a0,
uint8_t *out_a1, uint8_t *out_a2,
uint32_t *out_pc,
int max_count);
extern uint32_t beetle_get_cdcmd_trace_total(void);
extern void beetle_reset_cdcmd_trace(void);
/* wtrace */
extern int beetle_wtrace_arm(uint32_t lo, uint32_t hi);
extern int beetle_wtrace_disarm(int slot);
@@ -471,6 +480,41 @@ static void h_sio_trace(int id, const char *json) {
free(seqs); free(txs); free(rxs); free(ctrl);
}
static void h_cdrom_cmd_dump(int id, const char *json) {
int count = json_get_int(json, "count", 256);
if (count < 1) count = 1;
if (count > 8192) count = 8192;
uint32_t *seqs = (uint32_t*)malloc(count * sizeof(uint32_t));
uint32_t *pcs = (uint32_t*)malloc(count * sizeof(uint32_t));
uint8_t *cmds = (uint8_t*)malloc(count), *nargs = (uint8_t*)malloc(count);
uint8_t *a0 = (uint8_t*)malloc(count), *a1 = (uint8_t*)malloc(count), *a2 = (uint8_t*)malloc(count);
if (!seqs || !pcs || !cmds || !nargs || !a0 || !a1 || !a2) {
free(seqs); free(pcs); free(cmds); free(nargs); free(a0); free(a1); free(a2);
send_err(id, "alloc"); return;
}
uint32_t got = beetle_get_cdcmd_trace(seqs, cmds, nargs, a0, a1, a2, pcs, count);
uint32_t total = beetle_get_cdcmd_trace_total();
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%u,\"count\":%u,\"entries\":[",
id, total, got);
for (uint32_t i = 0; i < got; i++) {
if (i > 0) send_fmt(",");
send_fmt("{\"seq\":%u,\"cmd\":\"0x%02X\",\"nargs\":%u,"
"\"a0\":\"0x%02X\",\"a1\":\"0x%02X\",\"a2\":\"0x%02X\","
"\"pc\":\"0x%08X\"}",
seqs[i], cmds[i], nargs[i], a0[i], a1[i], a2[i], pcs[i]);
}
send_fmt("]}\n");
free(seqs); free(pcs); free(cmds); free(nargs); free(a0); free(a1); free(a2);
}
static void h_cdrom_cmd_reset(int id, const char *json) {
(void)json;
beetle_reset_cdcmd_trace();
send_ok(id);
}
static void h_sio_write_window(int id, const char *json) {
int which = json_get_int(json, "which", 0);
int before = json_get_int(json, "before", 4);
@@ -1490,6 +1534,8 @@ static const CmdEntry CMDS[] = {
{ "vram_peek", h_vram_peek },
{ "sio_trace_reset", h_sio_trace_reset },
{ "sio_trace", h_sio_trace },
{ "cdrom_cmd_dump", h_cdrom_cmd_dump },
{ "cdrom_cmd_reset", h_cdrom_cmd_reset },
{ "sio_write_window", h_sio_write_window },
/* wtrace — normalized verb set (parity contract with psx-runtime). */
{ "wtrace_arm", h_wtrace_arm },
+63
View File
@@ -15,6 +15,7 @@
#include <cstdlib>
#include <cstring>
#include <cstdarg>
#include <csignal>
#include "libretro.h"
#include "mednafen/psx/psx.h"
@@ -59,6 +60,36 @@ static void sio_trace_callback(uint8_t tx, uint8_t rx, uint16_t ctrl) {
s_sio_trace_idx = (s_sio_trace_idx + 1) % BEETLE_SIO_TRACE_CAP;
}
/* ---- CD command trace (oracle-diff for the Kula World CD-init wedge) ---- */
#define BEETLE_CDCMD_TRACE_CAP 8192
struct BeetleCdcmdEntry {
uint32_t seq; uint32_t pc; uint8_t cmd, nargs, a0, a1, a2;
};
static BeetleCdcmdEntry s_cdcmd_trace[BEETLE_CDCMD_TRACE_CAP];
static int s_cdcmd_idx = 0;
static uint32_t s_cdcmd_seq = 0;
static void cdcmd_trace_callback(uint8_t cmd, uint8_t nargs,
uint8_t a0, uint8_t a1, uint8_t a2,
uint32_t pc) {
/* Symmetric self-stop trap (mirror of the runtime's PSX_CD_TRAP_CMD):
* SIGSTOP on the Nth matching CD command so a debugger can freeze the
* oracle at the exact boot point and diff its state against ours. */
{
static long trap_cmd = -2, trap_nth = 1, hits = 0;
if (trap_cmd == -2) {
const char *e2 = getenv("PSX_CD_TRAP_CMD");
trap_cmd = (e2 && *e2) ? strtol(e2, NULL, 0) : -1;
const char *e3 = getenv("PSX_CD_TRAP_NTH");
if (e3 && *e3) trap_nth = strtol(e3, NULL, 0);
}
if ((long)cmd == trap_cmd && ++hits == trap_nth) raise(SIGSTOP);
}
BeetleCdcmdEntry *e = &s_cdcmd_trace[s_cdcmd_idx];
e->seq = s_cdcmd_seq++; e->pc = pc; e->cmd = cmd; e->nargs = nargs;
e->a0 = a0; e->a1 = a1; e->a2 = a2;
s_cdcmd_idx = (s_cdcmd_idx + 1) % BEETLE_CDCMD_TRACE_CAP;
}
/* ---- wtrace_all ring (ALWAYS-ON; no filter; captures every write) ----
* Per global rule "ring buffers always-on": every write hits this ring
* unconditionally so probes connected late can still query the last ~1s
@@ -527,6 +558,8 @@ extern "C" int beetle_init_with_disc(const char *bios_path, const char *disc_pat
g_psxrecomp_rtrace_cb = rtrace_callback;
g_psxrecomp_irq_cb = irq_event_callback;
std::fprintf(stderr, "[psx-beetle] wtrace + fntrace + rtrace + irq callbacks registered\n");
g_psxrecomp_cdcmd_cb = cdcmd_trace_callback;
std::fprintf(stderr, "[psx-beetle] wtrace + fntrace + cdcmd callbacks registered\n");
std::fflush(stderr);
return 0;
@@ -693,6 +726,36 @@ extern "C" void beetle_reset_sio_trace(void) {
memset(s_sio_trace, 0, sizeof(s_sio_trace));
}
/* ---- CD command trace accessors ---- */
extern "C" uint32_t beetle_get_cdcmd_trace(uint32_t *out_seq, uint8_t *out_cmd,
uint8_t *out_nargs, uint8_t *out_a0,
uint8_t *out_a1, uint8_t *out_a2,
uint32_t *out_pc,
int max_count)
{
int avail = (int)(s_cdcmd_seq < (uint32_t)BEETLE_CDCMD_TRACE_CAP
? s_cdcmd_seq : BEETLE_CDCMD_TRACE_CAP);
int count = max_count < avail ? max_count : avail;
int start = (s_cdcmd_idx - count + BEETLE_CDCMD_TRACE_CAP) % BEETLE_CDCMD_TRACE_CAP;
for (int i = 0; i < count; i++) {
const BeetleCdcmdEntry *e = &s_cdcmd_trace[(start + i) % BEETLE_CDCMD_TRACE_CAP];
out_seq[i] = e->seq;
out_cmd[i] = e->cmd;
out_nargs[i] = e->nargs;
out_a0[i] = e->a0;
out_a1[i] = e->a1;
out_a2[i] = e->a2;
out_pc[i] = e->pc;
}
return (uint32_t)count;
}
extern "C" uint32_t beetle_get_cdcmd_trace_total(void) { return s_cdcmd_seq; }
extern "C" void beetle_reset_cdcmd_trace(void) {
s_cdcmd_idx = 0;
s_cdcmd_seq = 0;
memset(s_cdcmd_trace, 0, sizeof(s_cdcmd_trace));
}
/* ---- wtrace accessors ---- */
extern "C" int beetle_wtrace_arm(uint32_t lo, uint32_t hi) {
if (s_wtrace_range_count >= BEETLE_WTRACE_MAX_RANGES) return -1;
+62 -9
View File
@@ -16,6 +16,9 @@
#include "event_ring.h"
#include <string.h>
#include <stdlib.h>
#ifndef _WIN32
# include <signal.h>
#endif
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
@@ -49,6 +52,21 @@ static uint8_t request_reg;
static uint8_t irq_enable;
static uint8_t irq_flag;
/* Disc license region string returned in GetID's last four response bytes
* ("SCEE" PAL / "SCEA" NTSC-U / "SCEI" NTSC-J). Real hardware reports the
* region of the INSERTED DISC (mechacon reads it from the license area);
* the BIOS CD driver revalidates it when the kernel CD subsystem
* reinitializes mid-game, and a mismatch throws it into an endless
* GetStat/Init retry loop (Kula World wedged at its first level load this
* way). Set from the mounted disc's SYSTEM.CNF serial at launch
* (main.cpp); the default matches the console region of the one supported
* BIOS (SCPH1001, NTSC-U). */
static uint8_t disc_scex[4] = { 'S', 'C', 'E', 'A' };
void cdrom_set_disc_scex(const char scex[4]) {
memcpy(disc_scex, scex, 4);
}
/* CPS-native CD interrupt single-outstanding latch.
*
* The CD controller serializes responses: it presents one INT to the CPU
@@ -972,6 +990,24 @@ static void queue_or_exec_command(uint8_t cmd) {
}
static void exec_command(uint8_t cmd) {
#if !defined(PSX_NO_DEBUG_TOOLS) && !defined(_WIN32)
/* Self-stop trap: PSX_CD_TRAP_CMD=<byte> makes the process SIGSTOP
* itself the moment that CD command dispatches, so a debugger can
* attach and read the full native+guest call chain at the exact
* instant with ZERO run-speed cost. (A gdb conditional breakpoint on
* this function slows the emu two orders of magnitude via ptrace —
* unusable for wedges that need minutes of healthy boot first.) */
{
static long trap_cmd = -2, trap_nth = 1, trap_hits = 0;
if (trap_cmd == -2) {
const char *e = getenv("PSX_CD_TRAP_CMD");
trap_cmd = (e && *e) ? strtol(e, NULL, 0) : -1;
e = getenv("PSX_CD_TRAP_NTH"); /* stop on the Nth match (default 1st) */
if (e && *e) trap_nth = strtol(e, NULL, 0);
}
if ((long)cmd == trap_cmd && ++trap_hits == trap_nth) raise(SIGSTOP);
}
#endif
trace_cdrom('C', 0, cmd, 0);
/* ENQUEUE: a CD command was issued (aux = command byte). */
event_ring_record_aux(EV_ENQ, (uint8_t)SRC_CD_CMD, (uint32_t)cmd);
@@ -1249,10 +1285,19 @@ static void exec_command(uint8_t cmd) {
case 0x19: /* Test */
if (param_count >= 1 && param_fifo[0] == 0x20) {
response_push(0x97);
response_push(0x01);
response_push(0x10);
response_push(0xC2);
/* CD controller firmware version (BCD date + region). This BIOS is
* SCPH-1001, whose sub-CPU reports the 1994 controller: 94/09/19 C0.
* The value must be < 0x95 in the high byte — the shell's CD-init
* (func at ROM 0x1DF50) sets kernel flag [0xA000DFFC]=1 when the
* version byte >= 0x95, which later makes the boot CD-open
* (0xBFC0D570) issue a spurious ReadTOC that wedges the game's
* streaming reads. Beetle hardcodes the PSone-era 0x97 regardless of
* BIOS, which is wrong for SCPH-1001; matching the real 1994
* controller keeps the flag clear (Kula World demo-load wedge). */
response_push(0x94);
response_push(0x09);
response_push(0x19);
response_push(0xC0);
set_irq(CDIRQ_ACK);
} else {
response_push(stat_reg);
@@ -1335,10 +1380,10 @@ static void process_pending(uint32_t cycles) {
response_push(0x00);
response_push(0x20);
response_push(0x00);
response_push('S');
response_push('C');
response_push('E');
response_push('I');
response_push(disc_scex[0]);
response_push(disc_scex[1]);
response_push(disc_scex[2]);
response_push(disc_scex[3]);
set_irq(CDIRQ_COMPLETE);
}
fire_cdrom_irq();
@@ -1463,11 +1508,19 @@ uint32_t cdrom_read(uint32_t addr) {
switch (addr) {
case 0x1F801800: {
uint8_t s = index_reg & 0x03;
s |= (1 << 2); /* ADPCM empty */
/* Bit 2 is ADPBUSY (XA-ADPCM playback in progress), NOT "ADPCM
* empty" — it must idle at 0. The oracle (beetle cdc.cpp, Read
* A==0) never raises it. Raising it permanently made every BIOS
* status poll see "XA busy" and steered the kernel CD driver
* init down a different branch from real hardware. */
if (param_count == 0) s |= (1 << 3);
if (param_count < PARAM_FIFO_SIZE) s |= (1 << 4);
if (response_read < response_count) s |= (1 << 5);
if (data_fifo_ready()) s |= (1 << 6);
/* Bit 7 BUSYSTS: command written but not yet executed (our queued
* path; the synchronous path leaves no guest-observable window).
* Mirrors beetle's PendingCommandCounter/phase<=1 busy window. */
if (queued_cmd.pending) s |= (1 << 7);
ret = s;
break;
}
+2
View File
@@ -7,6 +7,8 @@
* overlay_backend_resolve (env PSX_OVERLAY_BACKEND > [runtime] overlay_backend >
* auto; auto => gcc if a compile cmd is wired, else sljit). gcc is the default. */
#include <stddef.h>
#include "code_provider.h"
#include "autocompile.h"
#include "overlay_sljit.h"
+17
View File
@@ -2178,6 +2178,23 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
#ifndef PSX_NO_DEBUG_TOOLS
extern void debug_server_cyc_observe(uint32_t block_leader_phys);
#endif
/* Async-interrupt latency fix. A guest wait loop that lives ENTIRELY in
* dirty RAM (e.g. libcd's CdSync spin, as in Kula World) re-enters this
* interpreter once per short (~7-insn) block and exits before the
* per-invocation `(insns_executed & 0xFFF)` gate below can fire — so a
* pending, IEc+IM2-enabled interrupt is never taken and the loop spins for
* seconds while the CD IRQ that would set its wait-flag is never serviced.
* Poll on a GLOBAL invocation counter so short-block loops still yield to
* interrupts (this is the interpreter analogue of a block-leader poll in
* static code). psx_check_interrupts runs the handler and returns with
* registers restored, so continuing the loop afterward is safe. */
{
static uint32_t s_interp_entry_poll = 0;
if ((++s_interp_entry_poll & 0x3Fu) == 0) {
cpu->pc = pc;
psx_check_interrupts(cpu);
}
}
for (int i = 0; i < MAX_INSNS_PER_DISPATCH; i++) {
uint32_t next_pc = 0;
#ifndef PSX_NO_DEBUG_TOOLS
+16 -2
View File
@@ -43,6 +43,13 @@ void fntrace_set_game_range(uint32_t lo, uint32_t hi) {
int fntrace_is_game_started(void) { return s_game_started; }
static inline int armed_match(uint32_t target) {
/* PSX_FNTRACE_ALL=1 records every dispatch from power-on — the ring
* then holds the earliest boot execution (before any TCP client can
* arm it) for offline first-divergence diffs. Checked once. */
static int boot_all = -1;
if (boot_all == -1) { const char *e = getenv("PSX_FNTRACE_ALL");
boot_all = (e && *e && *e != '0') ? 1 : 0; }
if (boot_all) return 1;
if (s_arm_record_all) return 1;
/* Hot path: when nothing is armed, record nothing. Recording every
* dispatch by default makes the ring fill in seconds and burns ~10%
@@ -101,11 +108,18 @@ void fntrace_record(CPUState* cpu, uint32_t target) {
boot_state_trigger_capture(cpu);
}
}
if (!armed_match(target)) return;
/* Honor the one-shot capture freeze (insn_freeze): once latched, the ring
* preserves the pre-divergence window instead of evicting it. */
extern int g_insn_log_frozen;
if (g_insn_log_frozen) return;
int was_frozen = g_insn_log_frozen;
/* Null-page dispatch guard: a dispatch whose phys target sits below the
* exception vectors (0x80/0xA0) is a jump through a null/garbage pointer —
* the wedge itself, never legitimate code. Latch the capture freeze so the
* ring preserves the window that led here (this culprit entry, with its
* ra, still records; the wedge storm after it does not). */
if ((target & 0x1FFFFFFFu) < 0x40u) g_insn_log_frozen = 1;
if (!armed_match(target)) return;
if (was_frozen) return;
uint64_t idx = g_fntrace_seq++ & (FNTRACE_RING_CAP - 1);
FntraceEntry* e = &g_fntrace_ring[idx];
e->frame = (uint32_t)s_frame_count;
+14
View File
@@ -2580,6 +2580,20 @@ int main(int argc, char** argv) {
std::fprintf(stdout, "psxrecomp: SPU float-shadow enabled (verified-enhancement)\n");
spu_init();
cdrom_init(disc_path_str.empty() ? NULL : disc_path_str.c_str());
if (!disc_path_str.empty()) {
/* GetID must report the inserted disc's license region (the BIOS CD
* driver revalidates it mid-game). Derive it from the disc's boot
* serial via the same disc_identity module the launch check uses. */
const auto ident = PSXRecompV4::identify_disc(
disc_path_str, /*expected_serial*/"", /*expected_crc*/0,
/*has_expected_crc*/false, /*compute_crc*/false);
if (ident.region == "PAL") cdrom_set_disc_scex("SCEE");
else if (ident.region == "NTSC-J") cdrom_set_disc_scex("SCEI");
else if (ident.region == "NTSC-U") cdrom_set_disc_scex("SCEA");
if (!ident.region.empty())
std::fprintf(stdout, "psxrecomp: disc region %s (serial %s)\n",
ident.region.c_str(), ident.detected_serial.c_str());
}
{
int divisor = 1; /* default: authentic 1x timing */
if (disc_speed == "instant") divisor = 0;
+19 -6
View File
@@ -32,6 +32,19 @@ static uint8_t ram[RAM_SIZE];
static uint8_t scratchpad[SCRATCHPAD_SIZE];
static uint8_t bios_rom[BIOS_ROM_SIZE];
/* Physical address translation for guest accesses. The 2 MB main RAM is
* mirrored 4x across the first 8 MB of each segment (mem-ctrl RAM_SIZE
* register, default 0x0B88) and games rely on it — Kula World's crt0
* parks $sp in the 4th mirror (0x807FFFF8). Fold the mirrors here so the
* RAM bounds checks below see canonical offsets; without this, mirror
* writes fell into the open-bus no-op and mirror reads returned 0 (the
* guest's stack silently vanished and $ra came back as 0). */
static inline uint32_t psx_phys_addr(uint32_t addr) {
uint32_t phys = addr & 0x1FFFFFFFu;
if (phys < 0x00800000u) phys &= (uint32_t)(RAM_SIZE - 1);
return phys;
}
/* Expose RAM pointer for oracle comparison (find_first_divergence). */
uint8_t *memory_get_ram_ptr(void) { return ram; }
uint8_t *memory_get_scratchpad_ptr(void) { return scratchpad; }
@@ -924,7 +937,7 @@ static uint32_t psx_read_word_raw(uint32_t addr) {
* fatal (frame 9705). Ignore writes, read as 0, count for telemetry. */
if (addr >= 0xC0000000u) { g_kseg2_ignored_reads++; return 0; }
uint32_t phys = addr & 0x1FFFFFFFu;
uint32_t phys = psx_phys_addr(addr);
if (phys < RAM_SIZE) {
uint32_t v = (uint32_t)ram[phys]
@@ -1014,7 +1027,7 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) {
* We have no cache model, so silently discard RAM/scratchpad writes. */
if (sr_ptr && (*sr_ptr & 0x10000u)) return;
uint32_t phys = addr & 0x1FFFFFFFu;
uint32_t phys = psx_phys_addr(addr);
if (phys < RAM_SIZE) {
if (phys == D44_PHYS) d44_note(phys, read_ram_word(phys), val);
@@ -1073,7 +1086,7 @@ uint16_t psx_read_half(uint32_t addr) {
static uint16_t psx_read_half_raw(uint32_t addr) {
/* KSEG2 guard — see psx_read_word_raw. */
if (addr >= 0xC0000000u) { g_kseg2_ignored_reads++; return 0; }
uint32_t phys = addr & 0x1FFFFFFFu;
uint32_t phys = psx_phys_addr(addr);
if (phys < RAM_SIZE) {
return (uint16_t)ram[phys] | ((uint16_t)ram[phys + 1] << 8);
@@ -1108,7 +1121,7 @@ static void psx_write_half_raw(uint32_t addr, uint16_t val) {
/* KSEG2 guard — see psx_read_word_raw. */
if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; }
uint32_t phys = addr & 0x1FFFFFFFu;
uint32_t phys = psx_phys_addr(addr);
if (phys < RAM_SIZE) {
debug_server_trace_write_check(phys, (uint32_t)read_ram_half(phys), (uint32_t)val, 2);
@@ -1157,7 +1170,7 @@ uint8_t psx_read_byte(uint32_t addr) {
static uint8_t psx_read_byte_raw(uint32_t addr) {
/* KSEG2 guard — see psx_read_word_raw. */
if (addr >= 0xC0000000u) { g_kseg2_ignored_reads++; return 0; }
uint32_t phys = addr & 0x1FFFFFFFu;
uint32_t phys = psx_phys_addr(addr);
if (phys < RAM_SIZE) {
return ram[phys];
@@ -1336,7 +1349,7 @@ static void psx_write_byte_raw(uint32_t addr, uint8_t val) {
/* KSEG2 guard — see psx_read_word_raw. */
if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; }
uint32_t phys = addr & 0x1FFFFFFFu;
uint32_t phys = psx_phys_addr(addr);
if (phys < RAM_SIZE) {
debug_server_trace_write_check(phys, (uint32_t)ram[phys], (uint32_t)val, 1);
+8 -2
View File
@@ -498,7 +498,13 @@ static void persist_sljit_shard(uint32_t entry_phys, uint32_t lo, uint32_t len,
* sets s_async_cache_dirty; the DISPATCH thread rescans-and-registers on its
* next miss (candidate table stays single-threaded). PSX_SLJIT_SYNC=1 forces
* the legacy synchronous JIT-on-miss path. */
#ifdef _WIN32
static volatile LONG s_async_cache_dirty = 0;
# define async_cache_dirty_exchange(v) InterlockedExchange(&s_async_cache_dirty, (v))
#else
static volatile int s_async_cache_dirty = 0;
# define async_cache_dirty_exchange(v) __atomic_exchange_n(&s_async_cache_dirty, (v), __ATOMIC_SEQ_CST)
#endif
static int g_sljit_async = 1;
/* MASTER SAFETY GATE (2026-06-25): the sljit EMITTER mistranslates some overlay
@@ -935,7 +941,7 @@ void overlay_loader_async_publish(uint32_t entry_phys, uint32_t lo, uint32_t len
if (!wok) { remove(tmp); return; }
if (!MoveFileExA(tmp, path, MOVEFILE_REPLACE_EXISTING)) { remove(tmp); return; }
s_sljit_persist_writes++; /* telemetry-only; benign cross-thread incr */
InterlockedExchange(&s_async_cache_dirty, 1);
async_cache_dirty_exchange(1);
#else
(void)entry_phys; (void)lo; (void)len; (void)crc; (void)blob; (void)blob_size;
#endif
@@ -1608,7 +1614,7 @@ int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) {
if (head < 0 && s_active && overlay_cache_window_contains(phys)) {
/* Pick up shards the background compile worker just published (idempotent
* rescan; runs only when a publish has flipped the dirty flag). */
if (g_sljit_tier_enabled && g_sljit_async && InterlockedExchange(&s_async_cache_dirty, 0)) {
if (g_sljit_tier_enabled && g_sljit_async && async_cache_dirty_exchange(0)) {
scan_sljit_cache_dir();
head = idx_head(phys);
}