mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
Merge pull request #431 from RetroPortingToolKit/docs/beetle-macos
docs: build the psx-beetle oracle on macOS (plus the missing Beetle hook patches)
This commit is contained in:
+1
-1
@@ -41,7 +41,7 @@ release-stage/
|
||||
*.zip
|
||||
|
||||
# Vendored upstream sources (cloned locally, not checked in)
|
||||
beetle-psx/
|
||||
beetle-psx
|
||||
|
||||
# Explicit-fallback SDL2 / dep zips (large, regenerable)
|
||||
sdl2-dev.zip
|
||||
|
||||
+26
-11
@@ -16,16 +16,21 @@ Pin the checkout to the last compatible commit — the same base
|
||||
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
|
||||
# cdcmd re-inserts the wtrace globals + typedefs — drop the duplicate block
|
||||
# in libretro.cpp / mednafen/psx/psx.h if the build errors on redefinition.
|
||||
# Also needed (not yet in docs/*.patch): rtrace/irq callbacks, guest-cycle
|
||||
# accumulator, PS_CDC::PSXRecomp_GetDecodeVolume, cdc/dma irq peeks. Apply
|
||||
# from a prior local beetle-psx tree or re-land those hooks by hand.
|
||||
for p in wtrace_hook sio_trace_hook cdcmd_trace_hook rtrace_irq_hook \
|
||||
guest_cycles_hook cdc_dma_peek_hook spu_event_hook; do
|
||||
git apply ../docs/beetle_$p.patch # in this order; each builds on the last
|
||||
done
|
||||
```
|
||||
|
||||
Every hook `beetle_libretro.cpp` and `beetle_debug_server.c` link against is
|
||||
now in `docs/beetle_*.patch` (added 2026-09-29): the rtrace/IRQ callbacks, the
|
||||
guest-cycle clock and the per-instruction PC hook behind `cyc_watch`, the
|
||||
CDC/DMA IRQ peeks, `PS_CDC::PSXRecomp_GetDecodeVolume` and the SPU event
|
||||
calls. `beetle_cdcmd_trace_hook.patch` is now a diff on top of the wtrace and
|
||||
SIO patches, so there is no duplicate block to remove. `beetle-macos.md` has a
|
||||
table of what each patch adds. The set was verified from a fresh clone on
|
||||
macOS; this Linux recipe has not been re-run with it.
|
||||
|
||||
`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.
|
||||
@@ -43,16 +48,26 @@ 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_RECOMP_UI=OFF -DPSX_DEBUG_TOOLS=ON
|
||||
# Current master's runtime configure also needs the lib/recomp-net and
|
||||
# lib/retcomm-rbengine submodules (git submodule update --init ..., or
|
||||
# -DPSX_REWIND=OFF) and recompiled BIOS C. psx-beetle links no recompiled
|
||||
# code, so -DPSXRECOMP_ALLOW_NO_BIOS=ON skips the BIOS check
|
||||
# (beetle-macos.md, step 3).
|
||||
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPSX_RECOMP_UI=OFF -DPSX_DEBUG_TOOLS=ON \
|
||||
-DPSXRECOMP_ALLOW_NO_BIOS=ON
|
||||
ninja -C build psx-beetle
|
||||
|
||||
# Run (SDL window; use software renderer if GL context is flaky).
|
||||
# Beetle looks for lowercase scph5501.bin in the BIOS directory (SHA1
|
||||
# 0555C6FA… — SCPH-5501). Symlink or copy next to SCPH1001.BIN.
|
||||
# Default debug port is 4380 (override with --port).
|
||||
# psxrecomp's bios/openbios.bin works under that name too; see
|
||||
# beetle-macos.md ("BIOS") for the silent fallback to Beetle's own OpenBIOS.
|
||||
# Default debug port is 4382 (override with --port).
|
||||
SDL_RENDER_DRIVER=software ./runtime/build/psx-beetle ../bios/SCPH1001.BIN --disc <game.cue> --port 4380
|
||||
|
||||
# Headless (if xvfb is installed):
|
||||
# Headless: SDL's dummy video driver needs no X server.
|
||||
# SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software ./runtime/build/psx-beetle ...
|
||||
# Or, if xvfb is installed:
|
||||
# xvfb-run -a ./runtime/build/psx-beetle ../bios/SCPH1001.BIN --disc <game.cue>
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
# Building psx-beetle on macOS
|
||||
|
||||
`psx-beetle` is the Beetle PSX (Mednafen) oracle. It speaks the same TCP debug
|
||||
protocol as `psx-runtime`, so the comparison tools (`tools/cycle_compare.py`,
|
||||
`tools/cycle_testrom/measure.py`, the parity and device-trace diffs) can point
|
||||
at either backend.
|
||||
|
||||
Verified 2026-09-29 on Apple Silicon (macOS 27, Apple clang 21, Homebrew SDL3),
|
||||
starting from a fresh clone of the official Beetle repository:
|
||||
|
||||
- psx-beetle boots psxrecomp's bundled OpenBIOS to its shell, headless;
|
||||
- `ping` answers `backend=beetle`, and `guest_cycles` advances about 565,050
|
||||
cycles per frame (33.8688 MHz at 59.94 Hz is 565,045);
|
||||
- ruler #2 (`tools/cycle_testrom`) reproduces the oracle values recorded in
|
||||
`internal/FAITHFUL_TIMING_PLAN.md` on all 13 loops of the 13-loop ROM
|
||||
(`icache_miss +14`) and on all 15 loops of the current ROM, where
|
||||
`icache_miss` reads +16 (see step 6).
|
||||
|
||||
The Linux recipe is in `beetle-linux.md`. The Beetle side of both is the same
|
||||
patch set.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Xcode Command Line Tools (clang, make, ar).
|
||||
- Homebrew: `brew install cmake ninja sdl3`.
|
||||
- Python 3 for the measurement tools.
|
||||
|
||||
## 1. Beetle checkout and hooks
|
||||
|
||||
Keep the checkout outside the psxrecomp tree and symlink it in. The symlink
|
||||
`beetle-psx` at the psxrecomp root is gitignored.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/libretro/beetle-psx-libretro.git ~/dev/beetle-psx
|
||||
cd ~/dev/beetle-psx
|
||||
git checkout 5759277b # last C++-tree base; newer upstream is plain C
|
||||
PSXRECOMP=<path to psxrecomp>
|
||||
for p in wtrace_hook sio_trace_hook cdcmd_trace_hook rtrace_irq_hook \
|
||||
guest_cycles_hook cdc_dma_peek_hook spu_event_hook; do
|
||||
git apply "$PSXRECOMP/docs/beetle_$p.patch"
|
||||
done
|
||||
```
|
||||
|
||||
Apply the patches in this order: each one is a diff on top of the one before.
|
||||
|
||||
| patch | adds | psxrecomp user |
|
||||
|---|---|---|
|
||||
| `beetle_wtrace_hook` | store callback, J/JAL/JR/JALR callback | `wtrace_*`, `fntrace_*`, parity ring |
|
||||
| `beetle_sio_trace_hook` | `FrontIO::SetSIOTraceCallback` | `sio_trace` |
|
||||
| `beetle_cdcmd_trace_hook` | CD command callback | `cdrom_cmd_dump` |
|
||||
| `beetle_rtrace_irq_hook` | CPU load callback; IRQ rising-edge callback | `rtrace_*`, parity reads, device trace |
|
||||
| `beetle_guest_cycles_hook` | absolute guest clock; per-instruction PC hook | `ping.guest_cycles`, `cyc_watch*` |
|
||||
| `beetle_cdc_dma_peek_hook` | CD INT type, DMA completion flags, CD decode volume | device trace detail, `beetle_cdc_decode_volume` |
|
||||
| `beetle_spu_event_hook` | KEYON/KEYOFF/END_STOP/END_LOOP events | `spu_events` |
|
||||
|
||||
`runtime/src/beetle_libretro.cpp` and `beetle_debug_server.c` link against
|
||||
every hook, so a tree missing any of them fails to link. Two limits:
|
||||
|
||||
- `beetle_core_get_guest_cycles()` returns the clock at the last frame
|
||||
boundary. That is exact where the debug server samples it (between frames),
|
||||
but device-trace and parity stamps taken inside a frame carry the
|
||||
frame-start value. `cyc_watch` uses the exact per-instruction clock.
|
||||
- `exc_ring` answers an empty ring. Its core hook was never published and no
|
||||
tool reads it.
|
||||
|
||||
## 2. Static library
|
||||
|
||||
```bash
|
||||
cd ~/dev/beetle-psx
|
||||
make platform=osx STATIC_LINKING=1 HAVE_LIGHTREC=0 -j"$(sysctl -n hw.ncpu)"
|
||||
# With STATIC_LINKING=1 the output is an ar archive, whatever the name says.
|
||||
cp mednafen_psx_libretro.dylib libmednafen_psx.a
|
||||
ln -s ~/dev/beetle-psx "$PSXRECOMP/beetle-psx"
|
||||
```
|
||||
|
||||
The build takes about 20 seconds on an M-series Mac.
|
||||
|
||||
## 3. psx-beetle
|
||||
|
||||
```bash
|
||||
cd "$PSXRECOMP"
|
||||
# The runtime configure needs these submodules even for psx-beetle
|
||||
# (or configure with -DPSX_REWIND=OFF).
|
||||
git submodule update --init lib/recomp-net lib/retcomm-rbengine
|
||||
cmake -S runtime -B runtime/build-beetle -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DPSX_RECOMP_UI=OFF -DPSX_DEBUG_TOOLS=ON -DPSXRECOMP_ALLOW_NO_BIOS=ON
|
||||
ninja -C runtime/build-beetle psx-beetle
|
||||
```
|
||||
|
||||
Configure must print `psx-beetle: found .../beetle-psx/libmednafen_psx.a`;
|
||||
otherwise the target is skipped silently. `PSXRECOMP_ALLOW_NO_BIOS=ON` lets the
|
||||
runtime configure without recompiled BIOS C. psx-beetle links no recompiled
|
||||
code; drop the flag once `tools/regen_bios.sh` has run.
|
||||
|
||||
## 4. BIOS: OpenBIOS under a retail name
|
||||
|
||||
psx-beetle uses the directory of the BIOS path it is given as Beetle's system
|
||||
directory. Beetle picks the BIOS from there by disc region: `scph5500.bin`
|
||||
(JP), `scph5501.bin` (NA) or `scph5502.bin` (EU). A file under the right name
|
||||
with an unexpected SHA-1 gets a warning and is used anyway. If none is found,
|
||||
Beetle quietly boots **its own embedded OpenBIOS**, a different build (SHA-1
|
||||
`bfd08f9d5c3def7b5b3662f9ec44af316f95aada` at 5759277b) from the one the native
|
||||
runtime runs. So put psxrecomp's image under all three names:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/psx-oracle/bios && cd ~/psx-oracle/bios
|
||||
for n in scph5500 scph5501 scph5502; do cp "$PSXRECOMP/bios/openbios.bin" $n.bin; done
|
||||
# No-disc (shell) boots load <system dir>/dummy.cue and then eject it:
|
||||
head -c $((2352 * 300)) /dev/zero > dummy.bin
|
||||
printf 'FILE "dummy.bin" BINARY\n TRACK 01 MODE2/2352\n INDEX 01 00:00:00\n' > dummy.cue
|
||||
```
|
||||
|
||||
The 2026-09-29 run used `bios/openbios.bin` with SHA-1
|
||||
`95419841b5104d552b14810b1ecbe6c1358bcdf1`. Confirm Beetle loaded it: its log
|
||||
prints `Obtained SHA1: 95419841...`, and a `read_ram` of `0x1FC00000`, length
|
||||
`0x80000`, must hash to the same value. No retail BIOS is needed for anything
|
||||
on this page.
|
||||
|
||||
## 5. Run
|
||||
|
||||
```bash
|
||||
cd ~/psx-oracle # memory cards and relative screenshot paths land here
|
||||
SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software \
|
||||
"$PSXRECOMP/runtime/build-beetle/psx-beetle" ~/psx-oracle/bios/scph5501.bin \
|
||||
--port 4382 [--disc <game>.cue]
|
||||
```
|
||||
|
||||
The two variables make it headless: SDL gives environment hints precedence
|
||||
over psx-beetle's `opengl` render hint, and the dummy video driver opens no
|
||||
window. Without them psx-beetle opens a 640x480 window. Both ways it paces to
|
||||
59.94 Hz (in the window, hold Tab for turbo). The default port is 4382.
|
||||
|
||||
Smoke test:
|
||||
|
||||
```bash
|
||||
python3 tools/debug_client.py --port 4382 ping # "backend": "beetle"
|
||||
python3 tools/debug_client.py --port 4382 screenshot /abs/path/shell.png
|
||||
```
|
||||
|
||||
With no disc the screenshot is the OpenBIOS shell: a rotating shaded cube
|
||||
whose colours cycle.
|
||||
|
||||
## 6. Self-check: ruler #2
|
||||
|
||||
Before trusting a new oracle build, run the cycle micro-benchmark ROM on it.
|
||||
The steps (ROM, boot disc without mkpsxiso, native build) are in
|
||||
`tools/cycle_testrom/README.md`. Beetle gives, per loop iteration:
|
||||
|
||||
| baseline | alu | load | load2 | load_use | div | div_spaced | mult |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 3 | 4 | 8 | 14 | 8 | 41 | 41 | 18 |
|
||||
|
||||
| gte_rtps | gte_nclip | gte_read_use | ld_div | mmio_timer | mmio_spu | icache_miss |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 14 | 7 | 14 | 49 | 6 | 41 | 19 |
|
||||
|
||||
These are the recorded oracle values. `icache_miss` is 19 (+16) on the current
|
||||
ROM and 17 (+14) on the 13-loop ROM of 2026-06-27, because the loop moved
|
||||
within its cache line (see the README). If a build disagrees, fix the oracle
|
||||
before comparing anything against it.
|
||||
|
||||
## 7. Boot anchors: arm cyc_watch from power-on
|
||||
|
||||
Reset, the kernel copy, the first A0/B0/C0 calls and the shell pass within the
|
||||
first frames, before a TCP `cyc_watch` arm can land. Both backends therefore
|
||||
read the watch from their environment at start-up:
|
||||
|
||||
| variable | meaning |
|
||||
|---|---|
|
||||
| `PSX_CYC_WATCH` | anchor PC, or `<pc>-<end>` for region mode (each hit is one pass's cycle count) |
|
||||
| `PSX_CYC_WATCH_N` | hits to record (default 16) |
|
||||
| `PSX_BEETLE_UNPACED=1` | psx-beetle only: skip the 59.94 Hz wall-clock pacing. Guest timing is unchanged; headless boots just finish sooner |
|
||||
|
||||
Read the rings with `tools/cycle_compare.py <pc> --no-arm` (or `cyc_watch_dump`
|
||||
on each port). For an LLE boot on the native side, set `PSX_BIOS_HLE=0`.
|
||||
|
||||
Anchor keys can differ between the two sides. A BIOS copy window declared with
|
||||
`dispatch_key = "rom"` keeps its ROM address as the native key; Beetle sees the
|
||||
PC the code runs at. Both `bios/OpenBIOS.toml` and `bios/SCPH1001.toml` declare
|
||||
the shell that way:
|
||||
|
||||
| anchor | Beetle | native |
|
||||
|---|---|---|
|
||||
| OpenBIOS shell entry | `0x80030000` | `0xBFC0A500` |
|
||||
| SCPH-1001 shell entry | `0x80030000` | `0xBFC18000` |
|
||||
|
||||
A native watch on `0x80030000` records nothing. Kernel windows
|
||||
(`dispatch_key = "ram"`) and ROM code use the same PC on both sides.
|
||||
|
||||
Example, OpenBIOS shell entry (the cycle-test disc from step 6 inserted):
|
||||
|
||||
```bash
|
||||
PSX_CYC_WATCH=0x80030000 PSX_CYC_WATCH_N=1 PSX_BEETLE_UNPACED=1 \
|
||||
SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software \
|
||||
"$PSXRECOMP/runtime/build-beetle/psx-beetle" ~/psx-oracle/bios/scph5501.bin \
|
||||
--port 4382 --disc disc/cyctest.cue &
|
||||
PSX_CYC_WATCH=0xBFC0A500 PSX_CYC_WATCH_N=1 PSX_BIOS_HLE=0 \
|
||||
build/Cycle_Test_ROM --no-launcher --headless --game game.toml \
|
||||
--bios ../../bios/openbios.bin --disc disc/cyctest.cue --debug-port 4600 &
|
||||
python3 ../cycle_compare.py 0x80030000 --no-arm --hits 1 --native-port 4600
|
||||
```
|
||||
|
||||
(run from `tools/cycle_testrom`). Beetle records its first hit at guest cycle
|
||||
5,015,670 with `bios/openbios.bin` (SHA-1 `95419841…`). `cycle_compare.py`
|
||||
warns that `anchor_phys` differs between the backends; for a ROM-keyed window
|
||||
that is expected.
|
||||
@@ -0,0 +1,58 @@
|
||||
diff --git a/mednafen/psx/cdc.cpp b/mednafen/psx/cdc.cpp
|
||||
index a6b31b66..f2c460b9 100644
|
||||
--- a/mednafen/psx/cdc.cpp
|
||||
+++ b/mednafen/psx/cdc.cpp
|
||||
@@ -2438,3 +2438,23 @@ PS_CDC::CDC_CTEntry PS_CDC::Commands[0x20] =
|
||||
{ /* 0x1E, */ 0, 0, "ReadTOC", &PS_CDC::Command_ReadTOC, &PS_CDC::Command_ReadTOC_Part2 },
|
||||
{ /* 0x1F, */ 0, 0, NULL, NULL, NULL },
|
||||
};
|
||||
+
|
||||
+/* PSXRecomp oracle peeks. */
|
||||
+void PS_CDC::PSXRecomp_GetDecodeVolume(uint8 out[2][2]) const
|
||||
+{
|
||||
+ for(unsigned i = 0; i < 2; i++)
|
||||
+ for(unsigned j = 0; j < 2; j++)
|
||||
+ out[i][j] = DecodeVolume[i][j];
|
||||
+}
|
||||
+
|
||||
+uint8 PS_CDC::PSXRecomp_GetIRQBuffer(void) const
|
||||
+{
|
||||
+ return IRQBuffer;
|
||||
+}
|
||||
+
|
||||
+/* INT type of the pending CD interrupt (low nibble of IRQBuffer:
|
||||
+ * 1=data ready, 2=complete, 3=acknowledge, 4=data end, 5=error). */
|
||||
+extern "C" unsigned psxrecomp_cdc_irq_type(void)
|
||||
+{
|
||||
+ return PSX_CDC ? (unsigned)(PSX_CDC->PSXRecomp_GetIRQBuffer() & 0xF) : 0u;
|
||||
+}
|
||||
diff --git a/mednafen/psx/cdc.h b/mednafen/psx/cdc.h
|
||||
index fabf2053..c582b9f5 100644
|
||||
--- a/mednafen/psx/cdc.h
|
||||
+++ b/mednafen/psx/cdc.h
|
||||
@@ -44,6 +44,10 @@ class PS_CDC
|
||||
int DriveStatus;
|
||||
#endif
|
||||
|
||||
+ /* PSXRecomp oracle peeks (read-only, no side effects). */
|
||||
+ void PSXRecomp_GetDecodeVolume(uint8 out[2][2]) const; // [data_source][output_port], 0x80 = unity
|
||||
+ uint8 PSXRecomp_GetIRQBuffer(void) const;
|
||||
+
|
||||
private:
|
||||
CDIF *Cur_CDIF;
|
||||
bool DiscChanged;
|
||||
diff --git a/mednafen/psx/dma.cpp b/mednafen/psx/dma.cpp
|
||||
index 27da2cd2..2f58b8ca 100644
|
||||
--- a/mednafen/psx/dma.cpp
|
||||
+++ b/mednafen/psx/dma.cpp
|
||||
@@ -727,3 +727,10 @@ int DMA_StateAction(StateMem *sm, int load, int data_only)
|
||||
|
||||
return(ret);
|
||||
}
|
||||
+
|
||||
+/* PSXRecomp oracle peek: per-channel DMA completion flags (DICR bits 24-30,
|
||||
+ * bit n = channel n completed). */
|
||||
+extern "C" unsigned psxrecomp_dma_int_status(void)
|
||||
+{
|
||||
+ return (unsigned)DMAIntStatus;
|
||||
+}
|
||||
@@ -1,19 +1,15 @@
|
||||
diff --git a/libretro.cpp b/libretro.cpp
|
||||
index d2e82bca..e9c867c8 100644
|
||||
index 99b87c80..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;
|
||||
@@ -511,6 +511,7 @@ 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
|
||||
@@ -36,43 +32,13 @@ index 1daaa5b7..a6b31b66 100644
|
||||
PendingCommandPhase = 2;
|
||||
}
|
||||
diff --git a/mednafen/psx/psx.h b/mednafen/psx/psx.h
|
||||
index 704c0a19..fc53362d 100644
|
||||
index e16a230f..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;
|
||||
@@ -167,4 +167,15 @@ typedef void (*psxrecomp_fntrace_cb_t)(uint32_t caller_pc, uint32_t target_pc,
|
||||
uint32_t a1, uint8_t kind);
|
||||
extern psxrecomp_fntrace_cb_t g_psxrecomp_fntrace_cb;
|
||||
|
||||
+/* 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,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
diff --git a/libretro.cpp b/libretro.cpp
|
||||
index 0924113d..5258d66e 100644
|
||||
--- a/libretro.cpp
|
||||
+++ b/libretro.cpp
|
||||
@@ -514,6 +514,13 @@ psxrecomp_fntrace_cb_t g_psxrecomp_fntrace_cb = NULL;
|
||||
psxrecomp_cdcmd_cb_t g_psxrecomp_cdcmd_cb = NULL;
|
||||
psxrecomp_rtrace_cb_t g_psxrecomp_rtrace_cb = NULL;
|
||||
psxrecomp_irq_cb_t g_psxrecomp_irq_cb = NULL;
|
||||
+psxrecomp_pc_cb_t g_psxrecomp_pc_cb = NULL;
|
||||
+uint64_t psxrecomp_guest_cycle_base = 0;
|
||||
+
|
||||
+extern "C" unsigned long long beetle_core_get_guest_cycles(void)
|
||||
+{
|
||||
+ return (unsigned long long)psxrecomp_guest_cycle_base;
|
||||
+}
|
||||
|
||||
MultiAccessSizeMem<512 * 1024, uint32, false> *BIOSROM = NULL;
|
||||
MultiAccessSizeMem<65536, uint32, false> *PIOMem = NULL;
|
||||
@@ -1277,6 +1284,9 @@ static void PSX_Power(void)
|
||||
PSX_PRNG.c = 6543217;
|
||||
PSX_PRNG.lcgo = 0xDEADBEEFCAFEBABEULL;
|
||||
|
||||
+ /* PSXRecomp oracle: the guest clock restarts at power-on. */
|
||||
+ psxrecomp_guest_cycle_base = 0;
|
||||
+
|
||||
cd_warned_slow = false;
|
||||
|
||||
memset(MainRAM->data32, 0, 2048 * 1024);
|
||||
@@ -4825,6 +4835,10 @@ void retro_run(void)
|
||||
|
||||
assert(timestamp);
|
||||
|
||||
+ /* PSXRecomp oracle: fold this frame's CPU timestamp into the absolute
|
||||
+ * guest clock before the per-frame timestamp rebase below. */
|
||||
+ psxrecomp_guest_cycle_base += (uint32_t)timestamp;
|
||||
+
|
||||
ForceEventUpdates(timestamp);
|
||||
|
||||
espec->SoundBufSize = IntermediateBufferPos;
|
||||
diff --git a/mednafen/psx/cpu.cpp b/mednafen/psx/cpu.cpp
|
||||
index cbfd20dc..d1789712 100644
|
||||
--- a/mednafen/psx/cpu.cpp
|
||||
+++ b/mednafen/psx/cpu.cpp
|
||||
@@ -709,6 +709,11 @@ pscpu_timestamp_t PS_CPU::RunReal(pscpu_timestamp_t timestamp_in)
|
||||
}
|
||||
#endif
|
||||
|
||||
+ /* PSXRecomp oracle hook: per-instruction PC + absolute guest cycle,
|
||||
+ * sampled before this instruction's fetch is charged. */
|
||||
+ if(MDFN_UNLIKELY(g_psxrecomp_pc_cb))
|
||||
+ g_psxrecomp_pc_cb(PC, psxrecomp_guest_cycle_base + (uint32_t)timestamp);
|
||||
+
|
||||
//
|
||||
// Instruction fetch
|
||||
//
|
||||
diff --git a/mednafen/psx/psx.h b/mednafen/psx/psx.h
|
||||
index 05122c8c..0e61795d 100644
|
||||
--- a/mednafen/psx/psx.h
|
||||
+++ b/mednafen/psx/psx.h
|
||||
@@ -198,4 +198,24 @@ extern psxrecomp_rtrace_cb_t g_psxrecomp_rtrace_cb;
|
||||
typedef void (*psxrecomp_irq_cb_t)(int which);
|
||||
extern psxrecomp_irq_cb_t g_psxrecomp_irq_cb;
|
||||
|
||||
+/* PSXRecomp oracle: absolute guest CPU clock. psxrecomp_guest_cycle_base is
|
||||
+ * the sum of the CPU timestamps of every completed retro_run() frame since
|
||||
+ * the last power-on (timestamps restart at 0 each frame), so inside a frame
|
||||
+ * the absolute guest cycle of an instruction is base + the CPU's running
|
||||
+ * timestamp. beetle_core_get_guest_cycles() returns the base, i.e. the exact
|
||||
+ * clock between frames, where the debug server samples it. */
|
||||
+extern uint64_t psxrecomp_guest_cycle_base;
|
||||
+extern "C" unsigned long long beetle_core_get_guest_cycles(void);
|
||||
+
|
||||
+/* PSXRecomp oracle: per-instruction PC hook (drives the cyc_watch
|
||||
+ * comparator). Fired by PS_CPU::RunReal before EVERY instruction fetch,
|
||||
+ * i.e. before any cycle of that instruction (fetch, execute, stalls) is
|
||||
+ * charged.
|
||||
+ * pc = guest PC about to be fetched
|
||||
+ * guest_cycles = absolute guest cycles charged for all prior instructions
|
||||
+ * NULL (the default) costs one predicted-not-taken branch per instruction;
|
||||
+ * the listener should install it only while a watch is armed. */
|
||||
+typedef void (*psxrecomp_pc_cb_t)(uint32_t pc, uint64_t guest_cycles);
|
||||
+extern psxrecomp_pc_cb_t g_psxrecomp_pc_cb;
|
||||
+
|
||||
#endif
|
||||
@@ -0,0 +1,144 @@
|
||||
diff --git a/libretro.cpp b/libretro.cpp
|
||||
index e9c867c8..0924113d 100644
|
||||
--- a/libretro.cpp
|
||||
+++ b/libretro.cpp
|
||||
@@ -512,6 +512,8 @@ FrontIO *PSX_FIO = NULL;
|
||||
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;
|
||||
+psxrecomp_rtrace_cb_t g_psxrecomp_rtrace_cb = NULL;
|
||||
+psxrecomp_irq_cb_t g_psxrecomp_irq_cb = NULL;
|
||||
|
||||
MultiAccessSizeMem<512 * 1024, uint32, false> *BIOSROM = NULL;
|
||||
MultiAccessSizeMem<65536, uint32, false> *PIOMem = NULL;
|
||||
diff --git a/mednafen/psx/cpu.cpp b/mednafen/psx/cpu.cpp
|
||||
index ea01b8bc..cbfd20dc 100644
|
||||
--- a/mednafen/psx/cpu.cpp
|
||||
+++ b/mednafen/psx/cpu.cpp
|
||||
@@ -1426,6 +1426,10 @@ pscpu_timestamp_t PS_CPU::RunReal(pscpu_timestamp_t timestamp_in)
|
||||
uint32_t value = ReadMemory<uint32>(timestamp, address, false, true);
|
||||
GTE_WriteDR(rt, value);
|
||||
|
||||
+ /* PSXRecomp oracle hook: load trace. */
|
||||
+ if(MDFN_UNLIKELY(g_psxrecomp_rtrace_cb))
|
||||
+ g_psxrecomp_rtrace_cb(address, value, PC, GPR[31], 4);
|
||||
+
|
||||
if (PGXP_GetModes() & PGXP_MODE_GTE)
|
||||
PGXP_GTE_LWC2(instr, value, address);
|
||||
|
||||
@@ -2244,6 +2248,9 @@ pscpu_timestamp_t PS_CPU::RunReal(pscpu_timestamp_t timestamp_in)
|
||||
|
||||
LDWhich = rt;
|
||||
LDValue = (int32)ReadMemory<int8>(timestamp, address);
|
||||
+ /* PSXRecomp oracle hook: load trace. */
|
||||
+ if(MDFN_UNLIKELY(g_psxrecomp_rtrace_cb))
|
||||
+ g_psxrecomp_rtrace_cb(address, LDValue & 0xFFu, PC, GPR[31], 1);
|
||||
|
||||
if (PGXP_GetModes() & PGXP_MODE_MEMORY)
|
||||
PGXP_CPU_LB(instr, LDValue, address);
|
||||
@@ -2268,6 +2275,9 @@ pscpu_timestamp_t PS_CPU::RunReal(pscpu_timestamp_t timestamp_in)
|
||||
|
||||
LDWhich = rt;
|
||||
LDValue = ReadMemory<uint8>(timestamp, address);
|
||||
+ /* PSXRecomp oracle hook: load trace. */
|
||||
+ if(MDFN_UNLIKELY(g_psxrecomp_rtrace_cb))
|
||||
+ g_psxrecomp_rtrace_cb(address, LDValue, PC, GPR[31], 1);
|
||||
|
||||
if (PGXP_GetModes() & PGXP_MODE_MEMORY)
|
||||
PGXP_CPU_LBU(instr, LDValue, address);
|
||||
@@ -2301,6 +2311,9 @@ pscpu_timestamp_t PS_CPU::RunReal(pscpu_timestamp_t timestamp_in)
|
||||
|
||||
LDWhich = rt;
|
||||
LDValue = (int32)ReadMemory<int16>(timestamp, address);
|
||||
+ /* PSXRecomp oracle hook: load trace. */
|
||||
+ if(MDFN_UNLIKELY(g_psxrecomp_rtrace_cb))
|
||||
+ g_psxrecomp_rtrace_cb(address, LDValue & 0xFFFFu, PC, GPR[31], 2);
|
||||
}
|
||||
if (PGXP_GetModes() & PGXP_MODE_MEMORY)
|
||||
PGXP_CPU_LH(instr, LDValue, address);
|
||||
@@ -2334,6 +2347,9 @@ pscpu_timestamp_t PS_CPU::RunReal(pscpu_timestamp_t timestamp_in)
|
||||
|
||||
LDWhich = rt;
|
||||
LDValue = ReadMemory<uint16>(timestamp, address);
|
||||
+ /* PSXRecomp oracle hook: load trace. */
|
||||
+ if(MDFN_UNLIKELY(g_psxrecomp_rtrace_cb))
|
||||
+ g_psxrecomp_rtrace_cb(address, LDValue, PC, GPR[31], 2);
|
||||
}
|
||||
|
||||
if (PGXP_GetModes() & PGXP_MODE_MEMORY)
|
||||
@@ -2369,6 +2385,9 @@ pscpu_timestamp_t PS_CPU::RunReal(pscpu_timestamp_t timestamp_in)
|
||||
|
||||
LDWhich = rt;
|
||||
LDValue = ReadMemory<uint32>(timestamp, address);
|
||||
+ /* PSXRecomp oracle hook: load trace. */
|
||||
+ if(MDFN_UNLIKELY(g_psxrecomp_rtrace_cb))
|
||||
+ g_psxrecomp_rtrace_cb(address, LDValue, PC, GPR[31], 4);
|
||||
}
|
||||
|
||||
if (PGXP_GetModes() & PGXP_MODE_MEMORY)
|
||||
@@ -2504,6 +2523,10 @@ pscpu_timestamp_t PS_CPU::RunReal(pscpu_timestamp_t timestamp_in)
|
||||
break;
|
||||
}
|
||||
|
||||
+ /* PSXRecomp oracle hook: load trace (merged register value). */
|
||||
+ if(MDFN_UNLIKELY(g_psxrecomp_rtrace_cb))
|
||||
+ g_psxrecomp_rtrace_cb(address, LDValue, PC, GPR[31], 4);
|
||||
+
|
||||
if (PGXP_GetModes() & PGXP_MODE_MEMORY)
|
||||
PGXP_CPU_LWL(instr, LDValue, address);
|
||||
|
||||
@@ -2583,6 +2606,10 @@ pscpu_timestamp_t PS_CPU::RunReal(pscpu_timestamp_t timestamp_in)
|
||||
break;
|
||||
}
|
||||
|
||||
+ /* PSXRecomp oracle hook: load trace (merged register value). */
|
||||
+ if(MDFN_UNLIKELY(g_psxrecomp_rtrace_cb))
|
||||
+ g_psxrecomp_rtrace_cb(address, LDValue, PC, GPR[31], 4);
|
||||
+
|
||||
if (PGXP_GetModes() & PGXP_MODE_MEMORY)
|
||||
PGXP_CPU_LWR(instr, LDValue, address);
|
||||
|
||||
diff --git a/mednafen/psx/irq.cpp b/mednafen/psx/irq.cpp
|
||||
index 029b9f84..d99dbbb3 100644
|
||||
--- a/mednafen/psx/irq.cpp
|
||||
+++ b/mednafen/psx/irq.cpp
|
||||
@@ -73,6 +73,10 @@ void IRQ_Assert(int which, bool status)
|
||||
Asserted |= 1 << which;
|
||||
//Status |= 1 << which;
|
||||
Status |= (old_Asserted ^ Asserted) & Asserted;
|
||||
+
|
||||
+ /* PSXRecomp oracle hook: rising edge of this IRQ line. */
|
||||
+ if(MDFN_UNLIKELY(g_psxrecomp_irq_cb) && ((old_Asserted ^ Asserted) & Asserted))
|
||||
+ g_psxrecomp_irq_cb(which);
|
||||
}
|
||||
|
||||
Recalc();
|
||||
diff --git a/mednafen/psx/psx.h b/mednafen/psx/psx.h
|
||||
index fc53362d..05122c8c 100644
|
||||
--- a/mednafen/psx/psx.h
|
||||
+++ b/mednafen/psx/psx.h
|
||||
@@ -178,4 +178,24 @@ typedef void (*psxrecomp_cdcmd_cb_t)(uint8_t cmd, uint8_t nargs,
|
||||
uint32_t pc);
|
||||
extern psxrecomp_cdcmd_cb_t g_psxrecomp_cdcmd_cb;
|
||||
|
||||
+/* PSXRecomp oracle: CPU load (read) trace callback — the read-side twin of
|
||||
+ * the wtrace hook. Fired by PS_CPU after LB/LBU/LH/LHU/LW/LWL/LWR/LWC2 read
|
||||
+ * memory (not on an address-error exception).
|
||||
+ * addr = virtual address of the access (LWL/LWR: the unaligned address)
|
||||
+ * value = loaded bus value, zero-extended to `size` bytes
|
||||
+ * (LWL/LWR: the merged register value, size 4)
|
||||
+ * pc = PC of the executing load instruction
|
||||
+ * ra = $ra (GPR[31]) at time of load
|
||||
+ * size = 1, 2, or 4 */
|
||||
+typedef void (*psxrecomp_rtrace_cb_t)(uint32_t addr, uint32_t value,
|
||||
+ uint32_t pc, uint32_t ra, uint8_t size);
|
||||
+extern psxrecomp_rtrace_cb_t g_psxrecomp_rtrace_cb;
|
||||
+
|
||||
+/* PSXRecomp oracle: interrupt-line rising-edge callback. Fired by
|
||||
+ * IRQ_Assert() when source `which` (the I_STAT bit number: 0=VBLANK,
|
||||
+ * 2=CD, 3=DMA, ...) goes from deasserted to asserted, i.e. exactly when
|
||||
+ * the I_STAT bit latches. */
|
||||
+typedef void (*psxrecomp_irq_cb_t)(int which);
|
||||
+extern psxrecomp_irq_cb_t g_psxrecomp_irq_cb;
|
||||
+
|
||||
#endif
|
||||
@@ -0,0 +1,51 @@
|
||||
diff --git a/mednafen/psx/spu.cpp b/mednafen/psx/spu.cpp
|
||||
index 513628ae..b89efa08 100644
|
||||
--- a/mednafen/psx/spu.cpp
|
||||
+++ b/mednafen/psx/spu.cpp
|
||||
@@ -73,6 +73,12 @@
|
||||
|
||||
#include "../state_helpers.h"
|
||||
|
||||
+/* PSXRecomp oracle: SPU voice event sink, defined by the psx-beetle host
|
||||
+ * (runtime/src/beetle_libretro.cpp). kind: 1=KEYON 2=KEYOFF 3=END_STOP
|
||||
+ * 4=END_LOOP; addr = SPU RAM address in 16-bit units; env = ADSR level. */
|
||||
+extern "C" void psxrecomp_beetle_spu_event(unsigned kind, unsigned voice,
|
||||
+ unsigned addr, unsigned env);
|
||||
+
|
||||
uint32_t IntermediateBufferPos;
|
||||
int16_t IntermediateBuffer[4096][2];
|
||||
|
||||
@@ -328,6 +334,11 @@ void PS_SPU::RunDecoder(SPU_Voice *voice)
|
||||
|
||||
BlockEnd |= 1 << (voice - Voices);
|
||||
|
||||
+ /* PSXRecomp oracle hook: loop-end flag reached (repeat / stop). */
|
||||
+ psxrecomp_beetle_spu_event((voice->DecodeFlags & 0x2) ? 4 : 3,
|
||||
+ (unsigned)(voice - Voices), voice->CurAddr,
|
||||
+ (unsigned)(uint16)voice->ADSR.EnvLevel);
|
||||
+
|
||||
if(!(voice->DecodeFlags & 0x2)) // Force enveloping to 0 if not "looping". TODO: Should we reset the ADSR divider counter too?
|
||||
{
|
||||
if(!(Noise_Mode & (1 << (voice - Voices))))
|
||||
@@ -925,6 +936,10 @@ int32 PS_SPU::UpdateFromCDC(int32 clocks)
|
||||
if(voice->DecodePlayDelay < 3)
|
||||
{
|
||||
ReleaseEnvelope(voice);
|
||||
+
|
||||
+ /* PSXRecomp oracle hook: KEYOFF took effect. */
|
||||
+ psxrecomp_beetle_spu_event(2, voice_num, voice->CurAddr,
|
||||
+ (unsigned)(uint16)voice->ADSR.EnvLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -950,6 +965,10 @@ int32 PS_SPU::UpdateFromCDC(int32 clocks)
|
||||
voice->CurPhase = 0;
|
||||
voice->CurAddr = voice->StartAddr & ~0x7;
|
||||
voice->IgnoreSampLA = false;
|
||||
+
|
||||
+ /* PSXRecomp oracle hook: KEYON took effect. */
|
||||
+ psxrecomp_beetle_spu_event(1, voice_num, voice->CurAddr,
|
||||
+ (unsigned)(uint16)voice->ADSR.EnvLevel);
|
||||
}
|
||||
|
||||
if(!(SPUControl & 0x8000))
|
||||
@@ -909,6 +909,10 @@ on a fixed region -> next.
|
||||
rebuild runtime/cyctest. New ruler #2 loops `mmio_timer` (Timer0 read → +3 = 1 dev + 2 compl)
|
||||
and `mmio_spu` (32-bit SPU read → +38 = 36 + 2). VALIDATED: cyctest COMPILED (4600) == Beetle
|
||||
(4382) EXACT on ALL 15 loops incl. mmio_timer +3 / mmio_spu +38; the 13 prior loops unchanged.
|
||||
(Correction, 2026-09-29: except `icache_miss`, which reads +16 on this 15-loop ROM on both
|
||||
backends, not +14. The two new loops sit before it and move its loop top from 0x80010144 to
|
||||
0x80010170, a different offset in its cache line, so each of its two misses costs one cycle
|
||||
more. Re-measured against live psx-beetle; see tools/cycle_testrom/README.md.)
|
||||
Tomba 2 boots past the BIOS to its "SCEA Presents" intro splash, total_checks advancing, no
|
||||
freeze (the faster-MMIO change did not trigger a device-timing cascade like load=4 did).
|
||||
RESIDUAL (documented, unmodeled dynamic axis): DMACycleSteal — Beetle adds the live DMA
|
||||
|
||||
@@ -957,14 +957,18 @@ if(NOT MSVC)
|
||||
)
|
||||
endif()
|
||||
|
||||
# ---- psx-beetle target (Beetle PSX libretro standalone, MinGW) ----
|
||||
# ---- psx-beetle target (Beetle PSX libretro standalone; MinGW, Linux, macOS) ----
|
||||
#
|
||||
# Standalone binary that runs Beetle PSX (mednafen-psx libretro core) with
|
||||
# a debug server exposing the SAME wire-protocol commands as psx-runtime,
|
||||
# on a different port (4380). No recomp code linked.
|
||||
# on its own port (default 4382, set in beetle_main.cpp; --port N). No recomp
|
||||
# code linked.
|
||||
#
|
||||
# Requires beetle-psx built via:
|
||||
# Requires beetle-psx with the docs/beetle_*.patch hooks, built via:
|
||||
# cd beetle-psx && make platform=mingw_x86_64 STATIC_LINKING=1 HAVE_LIGHTREC=0
|
||||
# (platform=unix on Linux, platform=osx on macOS; there the archive is named
|
||||
# mednafen_psx_libretro.so/.dylib; copy it to libmednafen_psx.a). Recipes:
|
||||
# docs/beetle-linux.md, docs/beetle-macos.md.
|
||||
|
||||
set(BEETLE_LIB "${CMAKE_SOURCE_DIR}/../beetle-psx/libmednafen_psx.a")
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* set_input, clear_input, pad_status, screenshot_file, sio_trace_*,
|
||||
* wtrace_*, fntrace_*.
|
||||
*
|
||||
* Default port: 4380 (compile-time DEFAULT_DEBUG_PORT).
|
||||
* Port: psx-beetle passes 4382 unless --port is given (beetle_main.cpp);
|
||||
* 4380 is only the fallback for a port <= 0.
|
||||
* JSON-over-newline protocol, same as recomp side.
|
||||
*/
|
||||
|
||||
@@ -15,6 +16,8 @@
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "host_time.h"
|
||||
|
||||
#ifndef DEFAULT_DEBUG_PORT
|
||||
#error DEFAULT_DEBUG_PORT must be defined by the beetle runtime target.
|
||||
#endif
|
||||
@@ -28,6 +31,7 @@
|
||||
# define sock_close closesocket
|
||||
#else
|
||||
# include <sys/socket.h>
|
||||
# include <sys/select.h>
|
||||
# include <netinet/in.h>
|
||||
# include <unistd.h>
|
||||
# include <fcntl.h>
|
||||
@@ -189,17 +193,50 @@ static int s_input_override = -1;
|
||||
static int s_input_frames = 0;
|
||||
|
||||
/* ---- Send helpers ---- */
|
||||
|
||||
/* The client socket is non-blocking. A reply larger than the free space in
|
||||
* the kernel send buffer (a 2 MB read_ram is ~4 MB of hex; macOS starts a
|
||||
* TCP send buffer at 128 KB) makes send() fail with EWOULDBLOCK
|
||||
* part-way through. Wait for the socket to drain and carry on, like
|
||||
* debug_server.c's send_all_blocking, instead of dropping the connection
|
||||
* and leaving the client a truncated JSON line. */
|
||||
#define SEND_TOTAL_BUDGET_MS 15000
|
||||
|
||||
static int send_would_block(void) {
|
||||
#ifdef _WIN32
|
||||
return WSAGetLastError() == WSAEWOULDBLOCK;
|
||||
#else
|
||||
return errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Wait up to 100 ms for the client socket to become writable. */
|
||||
static int send_wait_writable(void) {
|
||||
fd_set wr;
|
||||
FD_ZERO(&wr);
|
||||
FD_SET(s_client, &wr);
|
||||
struct timeval tv = { 0, 100 * 1000 };
|
||||
return select((int)s_client + 1, NULL, &wr, NULL, &tv) >= 0;
|
||||
}
|
||||
|
||||
static void send_raw(const char *data, int n) {
|
||||
if (s_client == SOCK_INVALID) return;
|
||||
int off = 0;
|
||||
const uint64_t t_start = psx_host_mono_ms();
|
||||
while (off < n) {
|
||||
int k = send(s_client, data + off, n - off, 0);
|
||||
if (k <= 0) {
|
||||
sock_close(s_client);
|
||||
s_client = SOCK_INVALID;
|
||||
return;
|
||||
if (k > 0) {
|
||||
off += k;
|
||||
continue;
|
||||
}
|
||||
off += k;
|
||||
if (k < 0 && send_would_block() &&
|
||||
psx_host_mono_ms() - t_start < SEND_TOTAL_BUDGET_MS &&
|
||||
send_wait_writable()) {
|
||||
continue;
|
||||
}
|
||||
sock_close(s_client);
|
||||
s_client = SOCK_INVALID;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1018,7 +1018,9 @@ extern "C" uint64_t beetle_spu_event_total(void) { return s_spu_event_seq; }
|
||||
* Returns 0 if the CDC isn't up. */
|
||||
extern "C" int beetle_cdc_decode_volume(unsigned out[4])
|
||||
{
|
||||
extern PS_CDC *PSX_CDC;
|
||||
/* PSX_CDC is declared by mednafen/psx/psx.h. A block-scope redeclaration
|
||||
* here would get C language linkage from the enclosing extern "C"
|
||||
* function, which clang rejects against psx.h's C++ declaration. */
|
||||
if (!PSX_CDC) return 0;
|
||||
uint8 m[2][2];
|
||||
PSX_CDC->PSXRecomp_GetDecodeVolume(m);
|
||||
@@ -1289,26 +1291,90 @@ extern "C" int beetle_history_get_snapshot(int slot, uint32_t *out_addr, int *ou
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- cyc_watch / exc_ring stubs ----
|
||||
* Full per-instruction sampling lives in a beetle-psx DebugMode CPUHook that
|
||||
* was never checked into docs/*.patch. GPU/OT oracle work only needs these
|
||||
* symbols to link; wire returns empty/unarmed until the hook patch lands. */
|
||||
/* ---- cyc_watch: Beetle half of the per-anchor cycle comparator ----
|
||||
* Same spec and wire format as the native cyc_watch (runtime/src/debug_server.c,
|
||||
* tools/cycle_compare.py, tools/cycle_testrom/measure.py): an entry is taken
|
||||
* BEFORE the anchor instruction is fetched, and its `cycles` is the absolute
|
||||
* guest cycle count charged for all prior instructions. With an `end` anchor
|
||||
* (region mode) each entry is instead the cycles of one A->B pass.
|
||||
*
|
||||
* Driven by the beetle-psx per-instruction PC hook g_psxrecomp_pc_cb
|
||||
* (docs/beetle_guest_cycles_hook.patch). The hook is installed only while a
|
||||
* watch is armed, so an idle oracle pays one untaken branch per instruction.
|
||||
* Beetle samples before every instruction; native samples at block leaders,
|
||||
* so anchor a block leader (branch target / function entry) on both. */
|
||||
#define BEETLE_CYC_WATCH_CAP 1024 /* == native CYC_WATCH_RING_CAP */
|
||||
struct BeetleCycWatchEntry {
|
||||
uint32_t hit_index;
|
||||
uint32_t pc; /* matched physical PC */
|
||||
uint64_t cycles; /* absolute guest cycles (region mode: A->B delta) */
|
||||
};
|
||||
static BeetleCycWatchEntry s_cw_ring[BEETLE_CYC_WATCH_CAP];
|
||||
static uint32_t s_cw_anchor_raw = 0, s_cw_anchor_phys = 0;
|
||||
static uint32_t s_cw_end_raw = 0, s_cw_end_phys = 0;
|
||||
static uint32_t s_cw_max_hits = 0, s_cw_hits = 0;
|
||||
static int s_cw_armed = 0;
|
||||
static int s_cw_armed = 0;
|
||||
static int s_cw_in_region = 0;
|
||||
static uint64_t s_cw_region_start = 0;
|
||||
|
||||
static void cyc_watch_disarm(void) {
|
||||
s_cw_armed = 0;
|
||||
g_psxrecomp_pc_cb = NULL;
|
||||
}
|
||||
|
||||
/* Runs on the emulation thread before every instruction while armed. */
|
||||
static void cyc_watch_pc_cb(uint32_t pc, uint64_t guest_cycles) {
|
||||
const uint32_t phys = pc & 0x1FFFFFFFu;
|
||||
if (s_cw_end_phys != 0u) { /* REGION mode (A..B) */
|
||||
if (!s_cw_in_region) {
|
||||
if (phys == s_cw_anchor_phys) {
|
||||
s_cw_region_start = guest_cycles;
|
||||
s_cw_in_region = 1;
|
||||
}
|
||||
} else if (phys == s_cw_end_phys) {
|
||||
BeetleCycWatchEntry *e = &s_cw_ring[s_cw_hits];
|
||||
e->hit_index = s_cw_hits;
|
||||
e->pc = phys;
|
||||
e->cycles = guest_cycles - s_cw_region_start;
|
||||
s_cw_hits++;
|
||||
s_cw_in_region = 0;
|
||||
if (s_cw_hits >= s_cw_max_hits) cyc_watch_disarm();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (phys != s_cw_anchor_phys) return; /* single-anchor mode */
|
||||
BeetleCycWatchEntry *e = &s_cw_ring[s_cw_hits];
|
||||
e->hit_index = s_cw_hits;
|
||||
e->pc = phys;
|
||||
e->cycles = guest_cycles;
|
||||
s_cw_hits++;
|
||||
if (s_cw_hits >= s_cw_max_hits) cyc_watch_disarm();
|
||||
}
|
||||
|
||||
extern "C" void beetle_cyc_watch_arm(uint32_t anchor_raw, uint32_t end_raw, int n) {
|
||||
s_cw_anchor_raw = anchor_raw;
|
||||
s_cw_anchor_phys = anchor_raw & 0x1FFFFFFFu;
|
||||
s_cw_end_raw = end_raw;
|
||||
s_cw_end_phys = end_raw & 0x1FFFFFFFu;
|
||||
s_cw_max_hits = (n > 0) ? (uint32_t)n : 16u;
|
||||
s_cw_hits = 0;
|
||||
if (n < 1) n = 1;
|
||||
if (n > BEETLE_CYC_WATCH_CAP) n = BEETLE_CYC_WATCH_CAP;
|
||||
cyc_watch_disarm(); /* no sampling mid-reset */
|
||||
s_cw_anchor_raw = anchor_raw;
|
||||
s_cw_anchor_phys = anchor_raw & 0x1FFFFFFFu;
|
||||
s_cw_end_raw = end_raw;
|
||||
s_cw_end_phys = end_raw & 0x1FFFFFFFu;
|
||||
s_cw_max_hits = (uint32_t)n;
|
||||
s_cw_hits = 0;
|
||||
s_cw_in_region = 0;
|
||||
s_cw_region_start = 0;
|
||||
std::memset(s_cw_ring, 0, sizeof(s_cw_ring));
|
||||
s_cw_armed = 1;
|
||||
g_psxrecomp_pc_cb = cyc_watch_pc_cb;
|
||||
}
|
||||
extern "C" void beetle_cyc_watch_clear(void) {
|
||||
s_cw_armed = 0; s_cw_hits = 0;
|
||||
cyc_watch_disarm();
|
||||
s_cw_hits = 0;
|
||||
s_cw_anchor_raw = s_cw_anchor_phys = 0;
|
||||
s_cw_end_raw = s_cw_end_phys = 0;
|
||||
s_cw_in_region = 0;
|
||||
s_cw_region_start = 0;
|
||||
std::memset(s_cw_ring, 0, sizeof(s_cw_ring));
|
||||
}
|
||||
extern "C" void beetle_cyc_watch_get_state(uint32_t *anchor_raw, uint32_t *anchor_phys,
|
||||
uint32_t *end_raw, uint32_t *end_phys,
|
||||
@@ -1323,10 +1389,16 @@ extern "C" void beetle_cyc_watch_get_state(uint32_t *anchor_raw, uint32_t *ancho
|
||||
}
|
||||
extern "C" int beetle_cyc_watch_get(uint32_t i, uint32_t *hit_index, uint32_t *pc,
|
||||
unsigned long long *cyc) {
|
||||
(void)i; (void)hit_index; (void)pc; (void)cyc;
|
||||
return 0;
|
||||
if (i >= s_cw_hits) return 0;
|
||||
const BeetleCycWatchEntry *e = &s_cw_ring[i];
|
||||
if (hit_index) *hit_index = e->hit_index;
|
||||
if (pc) *pc = e->pc;
|
||||
if (cyc) *cyc = (unsigned long long)e->cycles;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* exc_ring stays a stub: no psxrecomp tool reads it, and the exception-ring
|
||||
* hook it wrapped was never published. The wire answers an empty ring. */
|
||||
extern "C" int retro_psxref_exc_ring_dump(char *out, int cap) {
|
||||
if (!out || cap < 32) return -1;
|
||||
return std::snprintf(out, (size_t)cap, "{\"ok\":true,\"entries\":[]}");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* beetle_main.cpp — psx-beetle entry point.
|
||||
* Standalone harness around Beetle PSX libretro core. SDL window for
|
||||
* Beetle's framebuffer, keyboard input, TCP debug server on port 4380. */
|
||||
* Beetle's framebuffer, keyboard input, TCP debug server on port 4382
|
||||
* (--port N). */
|
||||
|
||||
#include "psx_sdl.h"
|
||||
#include "frame_pacing.h"
|
||||
@@ -15,6 +16,7 @@ int beetle_init(const char* bios_path);
|
||||
int beetle_init_with_disc(const char* bios_path, const char* disc_path);
|
||||
int beetle_wtrace_arm(uint32_t lo, uint32_t hi);
|
||||
int beetle_rtrace_arm(uint32_t lo, uint32_t hi);
|
||||
void beetle_cyc_watch_arm(uint32_t anchor_raw, uint32_t end_raw, int n);
|
||||
void beetle_shutdown(void);
|
||||
void beetle_run_frame(uint16_t pad1_buttons);
|
||||
int beetle_get_framebuffer(uint32_t **out_pixels, unsigned *out_w, unsigned *out_h);
|
||||
@@ -151,6 +153,20 @@ int main(int argc, char** argv) {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
beetle_debug_server_init(dbg_port);
|
||||
std::fprintf(stdout, "psx-beetle: debug server on port %d\n", dbg_port);
|
||||
|
||||
/* PSX_CYC_WATCH="<pc>" or "<pc>-<end>" + PSX_CYC_WATCH_N (default 16):
|
||||
* arm cyc_watch before the first frame, as psx-runtime does, so boot
|
||||
* anchors are recorded from power-on on both backends. */
|
||||
if (const char* cw = std::getenv("PSX_CYC_WATCH"); cw && cw[0]) {
|
||||
char* e = nullptr;
|
||||
uint32_t pc = (uint32_t)std::strtoul(cw, &e, 0);
|
||||
uint32_t end = (e && *e == '-') ? (uint32_t)std::strtoul(e + 1, nullptr, 0) : 0u;
|
||||
const char* ns = std::getenv("PSX_CYC_WATCH_N");
|
||||
int n = (ns && ns[0]) ? (int)std::strtol(ns, nullptr, 0) : 16;
|
||||
beetle_cyc_watch_arm(pc, end, n);
|
||||
std::fprintf(stdout, "psx-beetle: cyc_watch armed from boot pc=0x%08X end=0x%08X n=%d\n",
|
||||
pc, end, n);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* General control-flow parity trace (oracle producer). Armed from boot via
|
||||
@@ -250,6 +266,13 @@ int main(int argc, char** argv) {
|
||||
* sustain unlocked rate (turbo). */
|
||||
const Uint8* keys = SDL_GetKeyboardState(NULL);
|
||||
if (keys && keys[SDL_SCANCODE_TAB]) continue;
|
||||
/* PSX_BEETLE_UNPACED=1: no wall-clock pacing, for headless oracle
|
||||
* runs (guest timing is unaffected; only the host waits less). */
|
||||
static const int unpaced = [] {
|
||||
const char* u = std::getenv("PSX_BEETLE_UNPACED");
|
||||
return (u && u[0] && u[0] != '0') ? 1 : 0;
|
||||
}();
|
||||
if (unpaced) continue;
|
||||
constexpr double FRAME_MS = 1000.0 / 59.94;
|
||||
static FramePacer pacer = { 0 };
|
||||
frame_pacer_wait(&pacer, FRAME_MS);
|
||||
|
||||
+37
-15
@@ -9991,24 +9991,12 @@ static void handle_wtrace_ranges(int id, const char *json)
|
||||
|
||||
/* ---- cyc_watch command handlers (see cyc_watch_observe above) ---- */
|
||||
|
||||
/* cyc_watch — arm an anchor PC. {"pc":"0x...","n":16}. Clears the ring,
|
||||
* masks the anchor to physical, and starts recording. The Beetle side
|
||||
* (psx-beetle, added parent-side) implements the SAME command/spec. */
|
||||
static void handle_cyc_watch(int id, const char *json)
|
||||
/* Arm cyc_watch: clear the ring, mask the anchor(s) to physical, start
|
||||
* recording. end_raw != 0 selects REGION mode (each entry = Δcycles A->B). */
|
||||
static void cyc_watch_arm(uint32_t raw, uint32_t end_raw, int n)
|
||||
{
|
||||
char pcbuf[64];
|
||||
if (!json_get_str(json, "pc", pcbuf, sizeof(pcbuf))) {
|
||||
send_err(id, "cyc_watch requires pc");
|
||||
return;
|
||||
}
|
||||
uint32_t raw = hex_to_u32(pcbuf);
|
||||
int n = json_get_int(json, "n", 16);
|
||||
if (n < 1) n = 1;
|
||||
if (n > CYC_WATCH_RING_CAP) n = CYC_WATCH_RING_CAP;
|
||||
/* Optional second anchor -> REGION mode: each entry = Δcycles of one A->B pass. */
|
||||
char endbuf[64];
|
||||
uint32_t end_raw = json_get_str(json, "end", endbuf, sizeof(endbuf)) ? hex_to_u32(endbuf) : 0u;
|
||||
|
||||
/* Disarm first so the hot path can't sample mid-reset. */
|
||||
s_cyc_watch_armed = 0;
|
||||
s_cyc_watch_anchor_raw = raw;
|
||||
@@ -10023,6 +10011,22 @@ static void handle_cyc_watch(int id, const char *json)
|
||||
s_cyc_watch_last_cycle = 0xFFFFFFFFFFFFFFFFull;
|
||||
memset(s_cyc_watch_ring, 0, sizeof(s_cyc_watch_ring));
|
||||
s_cyc_watch_armed = 1;
|
||||
}
|
||||
|
||||
/* cyc_watch — arm an anchor PC. {"pc":"0x...","n":16}. Clears the ring,
|
||||
* masks the anchor to physical, and starts recording. The Beetle side
|
||||
* (psx-beetle, added parent-side) implements the SAME command/spec. */
|
||||
static void handle_cyc_watch(int id, const char *json)
|
||||
{
|
||||
char pcbuf[64];
|
||||
if (!json_get_str(json, "pc", pcbuf, sizeof(pcbuf))) {
|
||||
send_err(id, "cyc_watch requires pc");
|
||||
return;
|
||||
}
|
||||
/* Optional second anchor -> REGION mode: each entry = Δcycles of one A->B pass. */
|
||||
char endbuf[64];
|
||||
uint32_t end_raw = json_get_str(json, "end", endbuf, sizeof(endbuf)) ? hex_to_u32(endbuf) : 0u;
|
||||
cyc_watch_arm(hex_to_u32(pcbuf), end_raw, json_get_int(json, "n", 16));
|
||||
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"anchor\":\"0x%08X\","
|
||||
"\"anchor_phys\":\"0x%08X\",\"end\":\"0x%08X\",\"end_phys\":\"0x%08X\","
|
||||
@@ -14466,6 +14470,24 @@ void debug_server_init(int port)
|
||||
}
|
||||
}
|
||||
}
|
||||
/* PSX_CYC_WATCH="<pc>" or "<pc>-<end>" (region mode) arms cyc_watch
|
||||
* before the first guest instruction, with PSX_CYC_WATCH_N hits
|
||||
* (default 16). Boot anchors (reset, the kernel copy, the first
|
||||
* syscalls, the shell) pass before any TCP arm can land. psx-beetle
|
||||
* reads the same variables, so one boot of each backend gives the
|
||||
* same anchor from cycle 0 (tools/cycle_compare.py --no-arm). */
|
||||
{
|
||||
const char *cw = getenv("PSX_CYC_WATCH");
|
||||
if (cw && *cw) {
|
||||
char *e = NULL;
|
||||
uint32_t pc = (uint32_t)strtoul(cw, &e, 0);
|
||||
uint32_t end = (e && *e == '-') ? (uint32_t)strtoul(e + 1, NULL, 0) : 0u;
|
||||
const char *ns = getenv("PSX_CYC_WATCH_N");
|
||||
cyc_watch_arm(pc, end, (ns && *ns) ? (int)strtol(ns, NULL, 0) : 16);
|
||||
fprintf(stdout, "psxrecomp: cyc_watch armed from boot pc=0x%08X end=0x%08X n=%u\n",
|
||||
pc, end, s_cyc_watch_max_hits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
@@ -19,6 +19,16 @@ The native side (psx-runtime) implements `cyc_watch` / `cyc_watch_dump` /
|
||||
`cyc_watch_clear` in runtime/src/debug_server.c. The Beetle side
|
||||
(psx-beetle) implements the SAME commands to the SAME spec (added parent-side).
|
||||
|
||||
BOOT ANCHORS: reset, the kernel copy, the first syscalls and the shell pass in
|
||||
the first frames, before a TCP arm can land. Arm them from power-on instead:
|
||||
start each backend with PSX_CYC_WATCH=<pc> (or <pc>-<end> for REGION mode) and
|
||||
PSX_CYC_WATCH_N=<hits> (default 16) in its environment, then run this tool with
|
||||
--no-arm to dump and diff. The anchor key can differ per side: a BIOS copy
|
||||
window with dispatch_key = "rom" (the shell, in both bios/OpenBIOS.toml and
|
||||
bios/SCPH1001.toml) is keyed natively at its ROM address, so the OpenBIOS shell
|
||||
entry is 0x80030000 in Beetle but 0xBFC0A500 natively (SCPH-1001: 0xBFC18000).
|
||||
Windows with dispatch_key = "ram" (the kernels) use the RAM PC on both sides.
|
||||
|
||||
Wire format (request):
|
||||
{"cmd":"cyc_watch","pc":"0x80012345","n":16}
|
||||
{"cmd":"cyc_watch_dump"}
|
||||
|
||||
@@ -15,8 +15,13 @@ set(PSXRECOMP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH
|
||||
|
||||
include("${PSXRECOMP_ROOT}/runtime/runtime.cmake")
|
||||
|
||||
set(GAME_GENERATED_FULL
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/generated/cycle_testrom.exe_full.c")
|
||||
# psxrecomp-game writes the recompiled text as shards (<exe>_full_NN.c).
|
||||
file(GLOB GAME_GENERATED_FULL
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/generated/cycle_testrom.exe_full_*.c")
|
||||
if(NOT GAME_GENERATED_FULL)
|
||||
message(FATAL_ERROR "cycle_testrom: no generated/cycle_testrom.exe_full_*.c; "
|
||||
"run psxrecomp-game --config game.toml --project-root ../.. first (README)")
|
||||
endif()
|
||||
set(GAME_GENERATED_DISPATCH
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/generated/cycle_testrom.exe_dispatch.c")
|
||||
|
||||
|
||||
@@ -70,6 +70,40 @@ cd disc && ../../mkpsxiso/mkpsxiso-2.20-win64/mkpsxiso.exe -y cyctest.xml
|
||||
copyrighted Sony data, local only. Everything else (gen, xml, SYSTEM.CNF) is
|
||||
tracked, so each developer reproduces the disc from a disc they own.
|
||||
|
||||
### Without mkpsxiso (macOS, Linux)
|
||||
|
||||
`mkdisc.py` builds the same kind of disc in pure Python: an ISO9660 volume
|
||||
with `SYSTEM.CNF` and the EXE as `CYCT_001.01`, in Mode 2 Form 1 sectors with
|
||||
a correct EDC/ECC (Beetle checks them). It copies the license area (sectors
|
||||
0-15) verbatim from the raw `.bin` of a disc you own. The license is needed
|
||||
even with OpenBIOS, which does not check it: Beetle's CD controller reports a
|
||||
disc without the license string as unlicensed, and OpenBIOS then retries
|
||||
GetID forever.
|
||||
|
||||
```bash
|
||||
python3 gen_testrom.py cycle_testrom.exe
|
||||
python3 mkdisc.py --cnf disc/SYSTEM.CNF --exe cycle_testrom.exe \
|
||||
--exe-name CYCT_001.01 --volume CYCT00101 --out disc/cyctest.bin \
|
||||
--license-from "<a PS1 disc you own>.bin"
|
||||
```
|
||||
|
||||
The native side, from the framework root (the recompiled OpenBIOS comes
|
||||
from `tools/regen_bios.sh --config bios/OpenBIOS.toml`):
|
||||
|
||||
```bash
|
||||
cd tools/cycle_testrom
|
||||
../../recompiler/build/psxrecomp-game --config game.toml --project-root ../..
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DPSX_RECOMP_UI=OFF -DPSX_DEBUG_TOOLS=ON
|
||||
ninja -C build psx-cyctest # binary: build/Cycle_Test_ROM
|
||||
build/Cycle_Test_ROM --no-launcher --headless --game game.toml \
|
||||
--bios ../../bios/openbios.bin --disc disc/cyctest.cue --debug-port 4600
|
||||
```
|
||||
|
||||
`--headless` runs unpaced with no window; a plain `&` launch works on macOS.
|
||||
Beetle runs headless too: `SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software
|
||||
psx-beetle ...` (see `docs/beetle-macos.md`).
|
||||
|
||||
## Measure
|
||||
|
||||
```bash
|
||||
@@ -134,6 +168,29 @@ STAGE 2 (pending): charge the same model in the compiled emitters (per cache-lin
|
||||
leader) and flip PSX_ICACHE on by default; validate ruler #1's cold first-hit spike
|
||||
(Beetle 84/77 vs steady 56) on the compiled path.
|
||||
|
||||
**The icache_miss cost depends on where the loop sits in its cache line.** A
|
||||
miss charges +3 plus one cycle per word from the missed word to the end of the
|
||||
16-byte line, so each of the loop's two misses costs 4 to 7 cycles. The +14
|
||||
above was measured with the loop top at 0x80010144 (word 1 of its line: 6 per
|
||||
miss). Adding the `mmio_timer`/`mmio_spu` loops moved it to 0x80010170 (word 0:
|
||||
7 per miss), so the current 15-loop ROM reads **+16 (per-iter 19) on both
|
||||
backends**. Re-measured 2026-09-29 with psx-beetle built on macOS
|
||||
(`docs/beetle-macos.md`), OpenBIOS on both backends: the 13-loop ROM from
|
||||
0edb9355 gives +14 and the current one +16. Beetle's per-iteration cycles for
|
||||
the other loops (the mmio loops exist only on the current ROM; the rest read
|
||||
the same on both):
|
||||
|
||||
| baseline | alu | load | load2 | load_use | div | div_spaced | mult |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 3 | 4 | 8 | 14 | 8 | 41 | 41 | 18 |
|
||||
|
||||
| gte_rtps | gte_nclip | gte_read_use | ld_div | mmio_timer | mmio_spu |
|
||||
|---|---|---|---|---|---|
|
||||
| 14 | 7 | 14 | 49 | 6 | 41 |
|
||||
|
||||
(The gte rows in the 2026-06-26 table below are the warm-up values; the
|
||||
steady-state ones are these, see FAITHFUL_TIMING_PLAN.md 2026-06-27.)
|
||||
|
||||
## Beetle ORACLE results (2026-06-26 — the HW cost targets)
|
||||
|
||||
Per-iteration cycle delta, and component cost (minus baseline=3):
|
||||
|
||||
@@ -9,7 +9,7 @@ exe = "cycle_testrom.exe"
|
||||
disc = "disc/cyctest.cue" # synthetic licensed disc (gen via tools/mkpsxiso)
|
||||
load_address = "0x80010000"
|
||||
entry_pc = "0x80010000"
|
||||
text_size = "0x00000800"
|
||||
text_size = "0x00002000" # whole 0x1800-byte text, page-rounded (the parser rejects an unaligned truncation)
|
||||
|
||||
[recompiler]
|
||||
seeds = "seeds.txt"
|
||||
|
||||
Executable
+211
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python3
|
||||
"""mkdisc.py - build a minimal PS1 boot disc (MODE2/2352 BIN + CUE) in pure Python.
|
||||
|
||||
A stand-in for mkpsxiso where it is not available (macOS/Linux without the
|
||||
Windows binary). Writes an ISO9660 volume with SYSTEM.CNF and one boot EXE,
|
||||
as Mode 2 Form 1 sectors with a correct EDC/ECC (Beetle verifies them).
|
||||
|
||||
Sectors 0-15 (the license area) are zero-filled unless --license-from names
|
||||
a raw MODE2/2352 .bin of a PS1 disc you own; its first 16 sectors are copied
|
||||
verbatim. That area is copyrighted Sony data: keep the built image local
|
||||
(disc/*.bin and disc/*.cue are gitignored), never commit or redistribute it.
|
||||
It is needed even with OpenBIOS: Beetle's CD controller reports a disc
|
||||
without the license string in sector 4 as unlicensed, and OpenBIOS then
|
||||
retries GetID forever instead of booting.
|
||||
|
||||
Usage (from tools/cycle_testrom, after gen_testrom.py):
|
||||
python3 mkdisc.py --cnf disc/SYSTEM.CNF --exe cycle_testrom.exe \
|
||||
--exe-name CYCT_001.01 --volume CYCT00101 --out disc/cyctest.bin \
|
||||
--license-from "<a PS1 disc you own>.bin"
|
||||
"""
|
||||
import argparse, os, struct
|
||||
|
||||
SECTOR = 2352
|
||||
USER = 2048
|
||||
|
||||
# ---- EDC / ECC (ECMA-130 Annex A/B; same tables as ecm.c / Mednafen lec) ----
|
||||
EDC_TABLE = []
|
||||
for i in range(256):
|
||||
e = i
|
||||
for _ in range(8):
|
||||
e = (e >> 1) ^ (0xD8018001 if e & 1 else 0)
|
||||
EDC_TABLE.append(e)
|
||||
|
||||
def edc(data):
|
||||
e = 0
|
||||
for b in data:
|
||||
e = (e >> 8) ^ EDC_TABLE[(e ^ b) & 0xFF]
|
||||
return e
|
||||
|
||||
ECC_F = [0] * 256
|
||||
ECC_B = [0] * 256
|
||||
for i in range(256):
|
||||
j = ((i << 1) ^ (0x11D if i & 0x80 else 0)) & 0xFF
|
||||
ECC_F[i] = j
|
||||
ECC_B[i ^ j] = i
|
||||
|
||||
def ecc_block(sec, major_count, minor_count, major_mult, minor_inc, dest):
|
||||
size = major_count * minor_count
|
||||
for major in range(major_count):
|
||||
index = (major >> 1) * major_mult + (major & 1)
|
||||
a = b = 0
|
||||
for _ in range(minor_count):
|
||||
t = sec[0xC + index]
|
||||
index += minor_inc
|
||||
if index >= size:
|
||||
index -= size
|
||||
a ^= t
|
||||
b ^= t
|
||||
a = ECC_F[a]
|
||||
a = ECC_B[ECC_F[a] ^ b]
|
||||
sec[dest + major] = a
|
||||
sec[dest + major + major_count] = a ^ b
|
||||
|
||||
def bcd(n):
|
||||
return ((n // 10) << 4) | (n % 10)
|
||||
|
||||
def mode2_form1(lba, user, submode=0x08):
|
||||
assert len(user) == USER
|
||||
s = bytearray(SECTOR)
|
||||
s[0] = 0x00
|
||||
s[1:11] = b"\xFF" * 10
|
||||
s[11] = 0x00
|
||||
a = lba + 150
|
||||
s[12:16] = bytes([bcd(a // 4500), bcd((a // 75) % 60), bcd(a % 75), 2])
|
||||
sub = bytes([0, 0, submode, 0])
|
||||
s[16:20] = sub
|
||||
s[20:24] = sub
|
||||
s[24:24 + USER] = user
|
||||
s[2072:2076] = struct.pack("<I", edc(s[16:2072]))
|
||||
hdr = bytes(s[12:16])
|
||||
s[12:16] = b"\0\0\0\0" # mode 2: header excluded from ECC
|
||||
ecc_block(s, 86, 24, 2, 86, 0x81C) # P
|
||||
ecc_block(s, 52, 43, 86, 88, 0x8C8) # Q
|
||||
s[12:16] = hdr
|
||||
return bytes(s)
|
||||
|
||||
# ---- ISO9660 ----
|
||||
def both16(v): return struct.pack("<H", v) + struct.pack(">H", v)
|
||||
def both32(v): return struct.pack("<I", v) + struct.pack(">I", v)
|
||||
def padstr(s, n): return s.encode("ascii").ljust(n, b" ")[:n]
|
||||
DATE7 = bytes([126, 9, 29, 0, 0, 0, 0]) # 2026-09-29 00:00:00 GMT
|
||||
DATE17 = b"2026092900000000" + b"\0"
|
||||
|
||||
def dirrec(ident, lba, size, is_dir):
|
||||
ln = 33 + len(ident)
|
||||
if ln & 1:
|
||||
ln += 1
|
||||
r = bytearray(ln)
|
||||
r[0] = ln
|
||||
r[2:10] = both32(lba)
|
||||
r[10:18] = both32(size)
|
||||
r[18:25] = DATE7
|
||||
r[25] = 2 if is_dir else 0
|
||||
r[28:32] = both16(1)
|
||||
r[32] = len(ident)
|
||||
r[33:33 + len(ident)] = ident
|
||||
return bytes(r)
|
||||
|
||||
def sectors_for(n):
|
||||
return max(1, (n + USER - 1) // USER)
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--cnf", required=True)
|
||||
p.add_argument("--exe", required=True)
|
||||
p.add_argument("--exe-name", required=True, help="ISO name, e.g. CYCT_001.01")
|
||||
p.add_argument("--volume", default="PSXDISC")
|
||||
p.add_argument("--out", required=True, help="output .bin (a .cue is written beside it)")
|
||||
p.add_argument("--license-from", help="raw MODE2/2352 .bin of a PS1 disc you own: "
|
||||
"copy its sectors 0-15 (the license area) verbatim")
|
||||
p.add_argument("--pad-sectors", type=int, default=300)
|
||||
a = p.parse_args()
|
||||
|
||||
cnf = open(a.cnf, "rb").read()
|
||||
exe = open(a.exe, "rb").read()
|
||||
files = sorted([(a.exe_name.upper() + ";1", exe), ("SYSTEM.CNF;1", cnf)])
|
||||
|
||||
LBA_PVD, LBA_TERM, LBA_LPT, LBA_MPT, LBA_ROOT = 16, 17, 18, 19, 20
|
||||
lba = 21
|
||||
layout = []
|
||||
for name, data in files:
|
||||
layout.append((name, lba, data))
|
||||
lba += sectors_for(len(data))
|
||||
total = max(lba, a.pad_sectors)
|
||||
|
||||
root = bytearray()
|
||||
root += dirrec(b"\x00", LBA_ROOT, USER, True)
|
||||
root += dirrec(b"\x01", LBA_ROOT, USER, True)
|
||||
for name, flba, data in layout:
|
||||
root += dirrec(name.encode("ascii"), flba, len(data), False)
|
||||
assert len(root) <= USER
|
||||
root = bytes(root).ljust(USER, b"\0")
|
||||
|
||||
pt_l = struct.pack("<BBIH", 1, 0, LBA_ROOT, 1) + b"\0\0"
|
||||
pt_m = struct.pack(">BBIH", 1, 0, LBA_ROOT, 1) + b"\0\0"
|
||||
|
||||
pvd = bytearray(USER)
|
||||
pvd[0] = 1
|
||||
pvd[1:6] = b"CD001"
|
||||
pvd[6] = 1
|
||||
pvd[8:40] = padstr("PLAYSTATION", 32)
|
||||
pvd[40:72] = padstr(a.volume, 32)
|
||||
pvd[80:88] = both32(total)
|
||||
pvd[120:124] = both16(1)
|
||||
pvd[124:128] = both16(1)
|
||||
pvd[128:132] = both16(USER)
|
||||
pvd[132:140] = both32(len(pt_l))
|
||||
pvd[140:144] = struct.pack("<I", LBA_LPT)
|
||||
pvd[148:152] = struct.pack(">I", LBA_MPT)
|
||||
pvd[156:190] = dirrec(b"\x00", LBA_ROOT, USER, True)
|
||||
pvd[190:318] = padstr(a.volume, 128)
|
||||
pvd[318:446] = padstr("PSXRECOMP", 128)
|
||||
pvd[446:574] = padstr("MKDISC.PY", 128)
|
||||
pvd[574:702] = padstr("PLAYSTATION", 128)
|
||||
pvd[702:739] = padstr("", 37)
|
||||
pvd[739:776] = padstr("", 37)
|
||||
pvd[776:813] = padstr("", 37)
|
||||
for off in (813, 830, 847, 864):
|
||||
pvd[off:off + 17] = DATE17
|
||||
pvd[881] = 1
|
||||
|
||||
term = bytearray(USER)
|
||||
term[0] = 0xFF
|
||||
term[1:6] = b"CD001"
|
||||
term[6] = 1
|
||||
|
||||
out = bytearray()
|
||||
if a.license_from:
|
||||
with open(a.license_from, "rb") as f:
|
||||
lic = f.read(16 * SECTOR)
|
||||
assert len(lic) == 16 * SECTOR
|
||||
out += lic
|
||||
else:
|
||||
for i in range(16):
|
||||
out += mode2_form1(i, bytes(USER))
|
||||
out += mode2_form1(LBA_PVD, bytes(pvd), 0x09)
|
||||
out += mode2_form1(LBA_TERM, bytes(term), 0x89)
|
||||
out += mode2_form1(LBA_LPT, pt_l.ljust(USER, b"\0"), 0x89)
|
||||
out += mode2_form1(LBA_MPT, pt_m.ljust(USER, b"\0"), 0x89)
|
||||
out += mode2_form1(LBA_ROOT, root, 0x89)
|
||||
cur = 21
|
||||
for name, flba, data in layout:
|
||||
n = sectors_for(len(data))
|
||||
for k in range(n):
|
||||
chunk = data[k * USER:(k + 1) * USER].ljust(USER, b"\0")
|
||||
out += mode2_form1(cur, chunk, 0x89 if k == n - 1 else 0x08)
|
||||
cur += 1
|
||||
while cur < total:
|
||||
out += mode2_form1(cur, bytes(USER))
|
||||
cur += 1
|
||||
|
||||
with open(a.out, "wb") as f:
|
||||
f.write(out)
|
||||
cue = os.path.splitext(a.out)[0] + ".cue"
|
||||
with open(cue, "w") as f:
|
||||
f.write('FILE "%s" BINARY\n TRACK 01 MODE2/2352\n INDEX 01 00:00:00\n'
|
||||
% os.path.basename(a.out))
|
||||
print(f"wrote {a.out} ({total} sectors) and {cue}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user