diff --git a/CMakePresets.json b/CMakePresets.json index bc05703..d5c5d01 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -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", diff --git a/README.md b/README.md index 3318ddc..98e59bf 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/src/ge_hooks.cpp b/src/ge_hooks.cpp index f621399..3564eba 100644 --- a/src/ge_hooks.cpp +++ b/src/ge_hooks.cpp @@ -14,6 +14,7 @@ #include #include "ge_init.h" // PPCRegister/PPCContext + generated function decls +#include // REXCVAR_DEFINE_BOOL / REXCVAR_GET (ge_freeze_diag) #include // ThreadState, kernel_state, memory #include #include @@ -35,6 +36,25 @@ #include // 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 @@ -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 started{false}; bool expected = false; if (started.compare_exchange_strong(expected, true)) { @@ -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(cpp)->rpi() : 0; @@ -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; } @@ -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); } diff --git a/src/ge_postfx.cpp b/src/ge_postfx.cpp index e7f60a6..72e6f48 100644 --- a/src/ge_postfx.cpp +++ b/src/ge_postfx.cpp @@ -12,8 +12,11 @@ #include #include +#include // ImDrawListSharedData (complete type) for GetDrawListSharedData() +#include #include +#include // --- Tunable cvars (category "PostFX"; persisted via the menu's Save) --- REXCVAR_DEFINE_BOOL(postfx_enabled, false, "PostFX", "Enable the post-processing filter"); @@ -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(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 s_verts; + static std::vector 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(H / 3.f) + 1; + s_verts.reserve(static_cast(n) * 4); + s_idxs.reserve(static_cast(n) * 6); + ImDrawIdx vi = 0; + for (float y = 0.f; y < H; y += 3.f, vi = static_cast(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(vi + 1)); + s_idxs.push_back(static_cast(vi + 2)); + s_idxs.push_back(vi); + s_idxs.push_back(static_cast(vi + 2)); + s_idxs.push_back(static_cast(vi + 3)); + } + s_W = W; s_H = H; s_col = col; s_uv = uv; + } + + const int nv = static_cast(s_verts.size()); + const int ni = static_cast(s_idxs.size()); + if (nv > 0) { + const auto base = static_cast(dl->_VtxCurrentIdx); + dl->PrimReserve(ni, nv); + memcpy(dl->_VtxWritePtr, s_verts.data(), static_cast(nv) * sizeof(ImDrawVert)); + for (int i = 0; i < ni; ++i) + dl->_IdxWritePtr[i] = static_cast(s_idxs[i] + base); + dl->_VtxWritePtr += nv; + dl->_IdxWritePtr += ni; + dl->_VtxCurrentIdx += static_cast(nv); } }