Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/${presetName}",
"cacheVariables": {
"CMAKE_C_COMPILER": "clang-20",
"CMAKE_CXX_COMPILER": "clang++-20"
"CMAKE_C_COMPILER": "clang",
"CMAKE_CXX_COMPILER": "clang++"
},
"condition": {
"type": "equals",
Expand Down
81 changes: 62 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,33 +56,76 @@ joiners — only the host's server port has to be reachable.
Most people should just use the [Releases](../../releases). To build it yourself
you need the recompiler toolchain and your own copy of the game.

**Prerequisites**
- The [ReXGlue SDK](https://github.com/jeffory/GoldenEye-Recomp-rexglue) (provides the `rexglue` CLI + runtime).
- CMake 3.25+, a C++23 **Clang** toolchain, Python 3.
- **Windows:** Clang inside a Visual Studio (MSVC) environment.
- **Linux:** Clang 18+ with **libc++** (`-stdlib=libc++`) — the SDK uses
`std::expected` / `std::jthread`.
- **Android:** the NDK (r27) — see [`android/README.md`](android/README.md).
- Your own GoldenEye 007 XBLA game files, placed in `assets/`.

**Steps — desktop (Windows / Linux)**
### Common prerequisites (all platforms)

- The [ReXGlue SDK](https://github.com/jeffory/GoldenEye-Recomp-rexglue) — a local
checkout that provides the `rexglue` CLI + runtime. The build points at it via
`-DREXSDK_DIR=/path/to/GoldenEye-Recomp-rexglue`.
- **CMake 3.25+**, a **C++23 Clang** toolchain, and **Python 3** (used by codegen).
- Your own **GoldenEye 007 XBLA game files**, placed in `assets/`.

Every build starts with the same codegen step, run once from the repo root. It
turns *your* game copy into recompiled C++ under `generated/`:

```sh
# 1. Generate the recompiled game code from your copy (creates generated/).
rexglue codegen --max_jump_table_entries 2048 ge_config.toml
```

# 2. Configure for your platform, pointing at your local ReXGlue SDK checkout.
# Presets: win-amd64 / win-arm64 / linux-amd64 / linux-arm64,
# each in -debug / -release / -relwithdebinfo.
cmake --preset linux-amd64-relwithdebinfo -DREXSDK_DIR=/path/to/GoldenEye-Recomp-rexglue
The desktop builds use [CMake presets](CMakePresets.json). Each platform/arch has
`-debug`, `-release`, and `-relwithdebinfo` variants
(`win-amd64`, `win-arm64`, `linux-amd64`, `linux-arm64`); swap the preset name in
the commands below to pick a different target or build type.

# 3. Build.
### Linux (x86-64)

- **Clang 18+** with **libc++** (`-stdlib=libc++`) — the SDK uses `std::expected` /
`std::jthread`. The presets invoke plain `clang` / `clang++`; if your distro
only ships versioned binaries (e.g. `clang-20`), either symlink them or override
`CMAKE_C_COMPILER` / `CMAKE_CXX_COMPILER`.
- **Ninja** (the presets' generator).

```sh
# After running codegen above:
cmake --preset linux-amd64-relwithdebinfo -DREXSDK_DIR=/path/to/GoldenEye-Recomp-rexglue
cmake --build --preset linux-amd64-relwithdebinfo
# Binary: out/build/linux-amd64-relwithdebinfo/ge → run with ./ge
```

### Windows (x64)

- **Clang** (LLVM, `clang` / `clang++`) inside a **Visual Studio (MSVC)**
environment — open an *x64 Native Tools* developer prompt so the MSVC headers
and libs are on `PATH`.
- **Ninja** and **CMake** (both ship with the VS installer's CMake component).

```bat
:: After running codegen above, from the x64 Native Tools prompt:
cmake --preset win-amd64-relwithdebinfo -DREXSDK_DIR=C:\path\to\GoldenEye-Recomp-rexglue
cmake --build --preset win-amd64-relwithdebinfo
:: Binary: out\build\win-amd64-relwithdebinfo\ge.exe
```

### Android (arm64-v8a, experimental)

Builds an APK with Gradle + the NDK, which drives this same CMake. `arm64-v8a`
only (the guest reserves a 4 GiB address space).

- Android SDK + **NDK 27.1.12297006** (Vulkan-capable).
- The ReXGlue SDK checkout, passed as `-PrexSdkDir` (default
`../../GoldenEye-Recomp-rexglue`).

```sh
# After running codegen above:
cd android
./gradlew assembleRelease -PrexSdkDir=/path/to/GoldenEye-Recomp-rexglue
# APK: android/app/build/outputs/apk/release/
```

**Android (arm64-v8a, experimental)** builds an APK with Gradle + the NDK, which
drives this same CMake — see [`android/README.md`](android/README.md). On-device
Install the (debug-signed) APK on a controller-equipped device. On-device
game-asset delivery is still being wired up, and cold boot uses an auto-retry
watchdog (background in [`docs/boot-startup-race.md`](docs/boot-startup-race.md)).
watchdog. See [`android/README.md`](android/README.md) for how the
NativeActivity / Vulkan-surface shell wires together, and
[`docs/boot-startup-race.md`](docs/boot-startup-race.md) for the boot race.

> [!NOTE]
> **Continuous integration.** Every push is compile-verified on **Linux, Windows,
Expand Down
60 changes: 50 additions & 10 deletions src/ge_hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <cstring>

#include "ge_init.h" // PPCRegister/PPCContext + generated function decls
#include <rex/cvar.h> // REXCVAR_DEFINE_BOOL / REXCVAR_GET (ge_freeze_diag)
#include <rex/hook.h> // ThreadState, kernel_state, memory
#include <rex/runtime.h>
#include <rex/system/xmemory.h>
Expand All @@ -35,6 +36,25 @@
#include <shellapi.h> // ShellExecuteW (WIN32_LEAN_AND_MEAN excludes it)
#endif

// Freeze watchdog + per-stall pipeline diagnostics. These were always-on while
// the frame-pacing freeze was being diagnosed: a 250ms-polling watchdog thread
// that, on a stall, walks every guest thread's stack (probing page readability
// via /dev/null on POSIX) and emits heavy logs, plus a per-present heartbeat.
// They cost CPU every frame and have no place in a shipping build, so they are
// gated behind this debug cvar (default OFF). Flip it on to investigate a stall.
REXCVAR_DEFINE_BOOL(ge_freeze_diag, false, "GPU",
"GoldenEye: enable the freeze watchdog + per-stall pipeline diagnostics "
"(heavy logging + guest-thread stack walks; OFF in shipping builds)")
.lifecycle(rex::cvar::Lifecycle::kHotReload);

// CP-progress handshake, defined in the rexglue base repo's command_processor.cpp.
// rex_ge_cp_progress_seq() snapshots a counter the CP worker bumps on every ring
// drain; rex_ge_cp_wait_progress() blocks until that counter moves past the
// snapshot (real CP progress) or the bounded timeout elapses -- replacing the
// old std::this_thread::yield() busy-spin on the GPU-completion path.
extern "C" uint64_t rex_ge_cp_progress_seq();
extern "C" void rex_ge_cp_wait_progress(uint64_t last_seq, uint32_t timeout_us);

namespace ge {
// Relaunch this same executable as a fresh, detached process. Used by the ONLINE
// pause-menu tab's "Save & Restart": the new instance reads the just-written
Expand Down Expand Up @@ -421,6 +441,7 @@ void ge_watchdog_thread() {
}

inline void ge_start_watchdog_once() {
if (!REXCVAR_GET(ge_freeze_diag)) return; // diagnostics off in shipping builds
static std::atomic<bool> started{false};
bool expected = false;
if (started.compare_exchange_strong(expected, true)) {
Expand Down Expand Up @@ -459,6 +480,10 @@ void ge_dbg_now(PPCRegister& r9, PPCRegister& r30) {
g_ge_idblk.store(idblk, std::memory_order_relaxed);
g_dbgnow_calls.fetch_add(1, std::memory_order_relaxed);
ge_start_watchdog_once();
// Snapshot the CP progress counter BEFORE sampling CP state below, so a drain
// that lands between our sample and our wait is not lost (the wait re-checks).
// Only consumed by the arm64 blocking-wait path below; unused on desktop.
[[maybe_unused]] uint64_t cp_seq = rex_ge_cp_progress_seq();
auto* cpp = ge_cp();
uint32_t cpc = cpp ? cpp->counter() : 0;
uint32_t rpi = cpp ? static_cast<CPProbe*>(cpp)->rpi() : 0;
Expand Down Expand Up @@ -515,14 +540,29 @@ void ge_dbg_now(PPCRegister& r9, PPCRegister& r30) {
drawn = true;
if (dev) base[dev + 10941u] |= 0x02u; // stalled: skip this GPU wait
}
// The six GPU-completion waits poll this routine in a TIGHT busy spin. With
// dozens of guest threads that oversubscribes the cores and starves the
// rexglue CP worker thread -- which is the very thread that must advance the
// ring read pointer / swap counter to satisfy (a)/(b). Result: the fence
// never advances, the spin never exits = freeze (visual stops, audio thread
// keeps running on its own core). Yield here while still waiting so the CP
// worker reliably gets CPU and can finish the frame.
if (!drawn) std::this_thread::yield();
// The six GPU-completion waits poll this routine. How we wait here is
// arch-gated, because the right answer depends on core count:
//
// - arm64 handhelds (few cores): a TIGHT yield busy-spin oversubscribes the
// cores and starves the rexglue CP worker (the very thread that must
// advance the ring read pointer / swap counter to satisfy (a)/(b)), so the
// fence never advances and the spin never exits = freeze. Instead BLOCK on
// real CP progress: the CP worker bumps the seq behind rex_ge_cp_progress_seq
// on every drain, waking us the instant it advances. Bounded timeout just
// lets the ~80ms skip-bit fallback above still run. No core burned.
//
// - desktop x86-64 (many cores): the CP worker always gets a core, so the
// plain yield is correct AND the blocking wait actively regresses boot --
// it froze GoldenEye on the very first intro screen (render skipped every
// frame while the loop ran at 60fps). Confirmed by bisect: main (yield)
// boots to the menu, the blocking-wait commit freezes. Keep the yield here.
if (!drawn) {
#if defined(__aarch64__)
rex_ge_cp_wait_progress(cp_seq, 2000u); // <=2ms, woken on progress
#else
std::this_thread::yield();
#endif
}
} else {
s_waiting = false;
}
Expand Down Expand Up @@ -557,8 +597,8 @@ void ge_diag_vdswap(PPCRegister& r31, PPCRegister& r30) {
g_present_cpcnt.store(cpc, std::memory_order_relaxed);
g_present_tb.store((uint32_t)REX_QUERY_TIMEBASE(), std::memory_order_relaxed);

static uint32_t n = 0; // throttled fps heartbeat
if ((n++ & 0x3F) == 0)
static uint32_t n = 0; // throttled fps heartbeat (diag only)
if ((n++ & 0x3F) == 0 && REXCVAR_GET(ge_freeze_diag))
REXKRNL_INFO("GEGPU present#{} dev={:#x} cpcnt={}", n, a1, cpc);
}

Expand Down
52 changes: 49 additions & 3 deletions src/ge_postfx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
#include <rex/cvar.h>

#include <imgui.h>
#include <imgui_internal.h> // ImDrawListSharedData (complete type) for GetDrawListSharedData()

#include <cstring>
#include <string>
#include <vector>

// --- Tunable cvars (category "PostFX"; persisted via the menu's Save) ---
REXCVAR_DEFINE_BOOL(postfx_enabled, false, "PostFX", "Enable the post-processing filter");
Expand Down Expand Up @@ -149,11 +152,54 @@ void PostFxOverlay::OnDraw(ImGuiIO& io) {
}

// Scanlines (CRT): thin dark horizontal lines every few pixels.
// Geometry is built once per unique (W, H, colour, atlas-UV) combination and
// submitted as a single PrimReserve+memcpy instead of one AddRectFilled per
// line (~360 calls at 1080p, ~720 at 4K).
const float scan = static_cast<float>(REXCVAR_GET(postfx_scanlines));
if (scan > 0.001f) {
const ImU32 line = IM_COL32(0, 0, 0, a8(scan * 0.7f));
for (float y = 0.0f; y < H; y += 3.0f) {
dl->AddRectFilled(ImVec2(0, y), ImVec2(W, y + 1.0f), line);
const ImU32 col = IM_COL32(0, 0, 0, a8(scan * 0.7f));
const ImVec2 uv = ImGui::GetDrawListSharedData()->TexUvWhitePixel;

static float s_W{}, s_H{};
static ImU32 s_col{};
static ImVec2 s_uv{-1.f, -1.f};
static std::vector<ImDrawVert> s_verts;
static std::vector<ImDrawIdx> s_idxs;

if (W != s_W || H != s_H || col != s_col || uv.x != s_uv.x || uv.y != s_uv.y) {
s_verts.clear();
s_idxs.clear();
const int n = static_cast<int>(H / 3.f) + 1;
s_verts.reserve(static_cast<size_t>(n) * 4);
s_idxs.reserve(static_cast<size_t>(n) * 6);
ImDrawIdx vi = 0;
for (float y = 0.f; y < H; y += 3.f, vi = static_cast<ImDrawIdx>(vi + 4)) {
const float y1 = y + 1.f;
s_verts.push_back({{0.f, y }, uv, col});
s_verts.push_back({{W, y }, uv, col});
s_verts.push_back({{W, y1}, uv, col});
s_verts.push_back({{0.f, y1}, uv, col});
s_idxs.push_back(vi);
s_idxs.push_back(static_cast<ImDrawIdx>(vi + 1));
s_idxs.push_back(static_cast<ImDrawIdx>(vi + 2));
s_idxs.push_back(vi);
s_idxs.push_back(static_cast<ImDrawIdx>(vi + 2));
s_idxs.push_back(static_cast<ImDrawIdx>(vi + 3));
}
s_W = W; s_H = H; s_col = col; s_uv = uv;
}

const int nv = static_cast<int>(s_verts.size());
const int ni = static_cast<int>(s_idxs.size());
if (nv > 0) {
const auto base = static_cast<ImDrawIdx>(dl->_VtxCurrentIdx);
dl->PrimReserve(ni, nv);
memcpy(dl->_VtxWritePtr, s_verts.data(), static_cast<size_t>(nv) * sizeof(ImDrawVert));
for (int i = 0; i < ni; ++i)
dl->_IdxWritePtr[i] = static_cast<ImDrawIdx>(s_idxs[i] + base);
dl->_VtxWritePtr += nv;
dl->_IdxWritePtr += ni;
dl->_VtxCurrentIdx += static_cast<unsigned int>(nv);
}
}

Expand Down
Loading