Fable-2-Recomp README

The project's README from github.com/himdo/Fable-2-Recomp. As one markdown file: README.md.

Fable 2 — ReXGlue Recompiled Project

First and for most this project was made to test the capabilities of local and open source AI. This project was made using Qwen3.8 27b. After about an hour it got the game running. Then after a large amount of human trial and error I was able to get enough of the functions mapped to be able to complete the game. I currently do not consider this to be fully complete as I have yet to 100% the game. The sha256 of the iso that started this project is: "685a0d3bea9718812f17bcd155907a5359a548b6d3d8342dd2a6c944f45e35ff" and its the Fable 2 GOTY for USA and Europe.

Recompilation of Fable 2 (Xbox 360, title ID 4D5307F1) using the ReXGlue SDK v0.10.0. Guest PPC code is statically recompiled to C++ at build time by rexglue codegen, driven by fable_2_manifest.toml.

File structure required to run the exe

The executable looks for its content in its own directory only (no ancestor search). That directory is mounted as the guest's game:/ (and d:) drive, and everything the game writes also goes next to the exe:

The build produces only the exe + runtime plugins — it does not copy the ~6.5 GB game content into the build directory. To run in place, either stage the content next to the exe once (tools\stage_content.cmd, incremental — later runs sync changed files only) or point the exe at an existing content tree with --game_data_root=<path>. With the content staged, the whole build folder is portable: copy it anywhere and it runs, with saves staying inside. If the content is missing, the game shows a clear error dialog naming the missing piece instead of failing deep in guest code.

Layout of a runnable build directory (out/build/win-amd64-debug/):

out/build/win-amd64-debug/
├── fable_2.exe                   <- the recompiled game
├── default.xex                   <- game executable (the content marker; staged by you)
├── data/                         <- game content (art, audio, scripts, ~6.5 GB; staged by you)
│   ├── art/
│   ├── audio/
│   ├── Globals/
│   ├── dir.manifest
│   ├── gamescripts.bnk
│   └── ...
├── nxeart/                       <- shader/HEC data (staged by you)
├── $SystemUpdate/                <- system update payload (staged by you; PIRS container, not applied)
├── saves/                        <- save files / settings / profiles (created at runtime)
├── cache/                        <- runtime caches (created at runtime)
├── logs/ + fable_2.toml          <- logs + cvar config (created at runtime)
├── fable2_config.toml            <- recomp user config (staged at build; created at runtime if missing)
├── fable2_patches.toml           <- guest-image (data) patch table (staged at build; created at runtime if missing)
├── README.md                     <- how to run this (staged by the build)
└── rexruntime.dll, rexgpu-xenos*.dll, ... <- staged runtime + GPU plugins (by the build)

Project root (build sources + the content that gets staged from here):

Fable 2 Rexglue/ ├── default.xex <- source content: game executable (the content marker) ├── data/ <- source content (art, audio, scripts, ~6.5 GB) ├── nxeart/ <- shader/HEC data ├── $SystemUpdate/ <- system update payload (PIRS container, not applied) ├── fable_2_manifest.toml <- codegen manifest (functions, switch tables, jmpbuf addrs) │ <- every function carries a name (see FUNCTION_NAMES.md) ├── fable2_switch_tables.toml <- 846 [[switch_tables]] entries, pulled in via [entrypoint] includes ├── FUNCTION_NAMES.md <- what each manifest function is + how to trigger it ├── src/ <- host application code │ ├── main.cpp <- entry point (app + probes + keyboard gamepad wiring) │ ├── vulkan_smoke.cpp <- standalone Vulkan pipeline smoke test (fable_2_vulkan_smoke) │ ├── core/ <- app body + config + patches + tracing │ │ ├── fable_2_app.h <- OnConfigurePaths() content-root search; OnPreSetup wires input │ │ ├── fable2_config.{h,cpp} <- fable2_config.toml loader (config cvars) │ │ ├── fable2_patches.{h,cpp}<- guest-image patch table (fable2_patches.toml) │ │ ├── fable2_hooks.cpp <- mid-asm hook bodies referenced by the manifest │ │ ├── fable2_f5_lua.h <- F5 external-Lua runner (src/lua/F5.lua) │ │ ├── fable2_func_trace.h <- guest function-call tracing (PCH force-include) │ │ └── xex_verify.h <- default.xex integrity verification │ ├── diagnostics/ <- probe/diagnostic headers (FPS, text, heap, deadbeef, ...) │ ├── input/ <- keyboard_gamepad.h + remote-control pad server/driver │ └── lua/ <- external game scripts (staged to data/scripts/recomp/) ├── docs/ <- investigation notes + RE artifacts │ ├── main_menu_crash_fix.md │ ├── FPS_CAP_INVESTIGATION.md │ └── analysis/ <- function-scan outputs + seed candidates ├── generated/ <- codegen output (291 recomp files, ~305 MB, regenerated by build) │ ├── default/ │ └── rexglue.cmake ├── out/build/win-amd64-debug/ <- build directory (exe + plugins; stage content to run in place) ├── XenonRecomp/ <- ORIGINAL XenonRecomp/XenonAnalyse configs (reference only) │ ├── fable2.toml │ └── fable2_switch_tables.toml ├── thirdparty/ <- tracked: SDK patches + submodule pins + vendored Vulkan headers │ ├── sdk_mainmenu_crash_fix.patch <- local SDK source modification (main-menu crash fix) │ ├── sdk_allocfixed_patch.patch <- local SDK source modification (AllocFixed) │ ├── xmemory.cpp.allocfixed-patched.bak <- pre-patch backup of the SDK source file │ ├── rexglue-sdk-local.patch <- the local SDK source modifications (Vulkan + FPS work) │ ├── rexglue-sdk-submodule-pins.txt <- SDK submodule SHAs the current DLLs were built against │ ├── vulkan/ + vk_video/ <- vendored Khronos headers (Vulkan 1.3.282) │ ├── rexglue-sdk/ <- (untracked) prebuilt SDK, fetched by tools/setup_sdk.cmd │ └── rexglue-sdk-src/ <- (untracked) SDK source, fetched by tools/setup_sdk_src.cmd (Vulkan only) ├── tools/ <- build/launch scripts + RE helpers │ ├── fable2.cmd <- launcher: stages runtime/plugin pair, picks backend │ ├── fable2-uncapped.cmd <- launcher wrapper: sets REX_VSYNC=0 (uncaps the 30 fps lock) │ ├── setup_sdk.cmd <- fetch prebuilt SDK v0.10.0 -> thirdparty/rexglue-sdk (auto-run by build.cmd) │ ├── setup_sdk_src.cmd <- fetch + pin + patch SDK source -> thirdparty/rexglue-sdk-src (Vulkan only) │ ├── stage_content.cmd <- copy the game content next to a built exe (manual, incremental) │ ├── build_sdk_vulkan.cmd <- build the Vulkan GPU plugin from the SDK source │ ├── crash_capture.cmd <- run fable_2.exe under lldb for crash backtraces │ ├── dump_funcs.py <- dump per-function disassembly from generated/default │ ├── classify.py / merge_names.py / apply_manifest.py / gen_md.py │ │ <- function-naming pipeline (manifest -> FUNCTION_NAMES.md) │ ├── scan_branches.py / scan_funcs.py <- code-range scanners (write docs/analysis/) │ └── capture/crash helpers <- capgame*.ps1, listwins*.ps1, enumwin.ps1, cropgame.ps1, │ crash_bt.py, run_capture.sh ├── scratch/ <- (untracked) old build logs, screenshots, session exports ├── build.cmd ├── CMakeLists.txt └── CMakePresets.json


Notes:

- **Only `default.xex` is the search marker, and the search is the exe's own directory only.** The other content folders (`data/`, `nxeart/`, `$SystemUpdate/`) must sit next to it; the guest references them via `game:/` paths at runtime. If `default.xex` (or the `data/` folder) is missing, the game shows an error dialog naming the missing piece instead of guessing elsewhere.
- **You must supply the game content yourself** (it is not in the repo): rip the Fable 2 GOTY (USA/EU) disc (the one with SHA-256 above) and put `default.xex`, `data/`, `nxeart/`, and `$SystemUpdate/` in the project root. The build does not copy this into the build directories — run `tools\stage_content.cmd <project root> out\build\<preset>` to stage it next to an exe (re-running re-syncs changed files), or use `--game_data_root`. `default.xex` is the unpatched original (the only available patch, `$SystemUpdate/su20076000_00000000`, is a PIRS container that XexPatcher cannot apply).
  - **How to extract:** rip the disc to an ISO, then open it with **[XboxImageExtractor](https://github.com/dromex1/XboxImageExtractor)** — a GUI tool for Xbox 360 game images. It lists the image's filesystem; select `default.xex`, `data`, and `$SystemUpdate` and extract them into the project root (`data` and `$SystemUpdate` extract as folders). You can also grab `nxeart` and anything else the tool lists.
- **Saves live in `<build dir>\saves\`** — back that folder up to keep your progress, and copy it between build trees (Debug/Release) or machines to carry a save over.
- The `--game_data_root <path>` override still points the content root at a different tree (e.g. to run from a shared content copy without staging); saves/cache still land next to the exe.
- `generated/` and `out/` are build artifacts — safe to delete, the build regenerates them.
- `XenonRecomp/` is kept only as a historical reference for the pre-ReXGlue port; nothing in the build reads it.

## Running

out\build\win-amd64-debug\fable_2.exe


(saves and caches are created next to the exe on first launch; the exe needs
its content next to it — see above — otherwise it shows an error dialog)

Optional command-line overrides (all normal `--cvar value` args):

| Argument | Effect |
|---|---|
| `--game_data_root <path>` | Use a different content directory (beats the exe-dir-only search) |
| `--update_data_root <path>` | Optional update content root |
| `--window_width` / `--window_height` / `--fullscreen` | Presentation options |
| `--keyboard_gamepad_map <map>` | Host keyboard -> guest gamepad button map (see below) |

## User config (fable2_config.toml)

The recomp has its own human-readable user config, `fable2_config.toml`, next
to the exe — separate from `fable_2.toml`, which is the ReXGlue SDK's cvar
config. It is staged by the build (once; user edits survive rebuilds) and the
game recreates it with defaults on launch if it is ever missing. Loaded in
`Fable2App::OnPostInitLogging()`; code in `src/core/fable2_config.{h,cpp}`.

- Missing keys -> built-in defaults; wrong types / unknown sections -> logged
  warnings, defaults used; a syntax error -> dialog + defaults (never blocks
  launch).
- To add a setting: add a member to `fable2::config::Values` (with default),
  read it in `Load()`, and document the key in `config/fable2_config.toml`
  **and** the embedded template in `src/core/fable2_config.cpp` (keep both in
  sync).
- Settings that back an SDK cvar (currently the `[input]` section:
  `keyboard_gamepad_map`, `mouse_look`, `mouse_look_scale`) are seeded into
  the cvar at startup — but only when no higher-priority
  source set it: `fable_2.toml` (cvar config), `REX_*` env vars, and the
  command line (e.g. `--keyboard_gamepad_map`) still win, and the F3 console
  can still change the value live.
- The `[patches]` section holds runtime toggles for the recomp-level
  (mid-asm hook) patches, consulted by the hook bodies on every call
  (`src/core/fable2_hooks.cpp`) — flip one and relaunch to A/B a patch with no
  rebuild. Currently: `fps_60` (60 FPS hook; `true` = main loop ~60/s,
  `false` = original 30/s). Guest-image data patches are a different file:
  `fable2_patches.toml` (see above).

## Guest-image patches (fable2_patches.toml)

Data patches for the loaded `default.xex` guest image (Xenia game-patches
format), applied before the guest module launches: `Fable2App::OnPostLoadXexImage()`
→ `fable2::patches::Load()` + `ApplyAll()` (code in `src/core/fable2_patches.{h,cpp}`).
Same lifecycle as the user config: staged by the build, recreated with the
built-in defaults if missing, and a broken file falls back to the built-ins
(dialog + log) so it never blocks launch. Each `[[patch]]` has an `enabled`
toggle (default true) — flip it in the file and relaunch to A/B a patch with
no rebuild. **Scope: data patches only** — code-region ops are inert in this
recomp (guest `.text` is never executed); code patches are mid-asm hooks
instead. Full details, the current patch list, and how code patches work:
`docs/patches.md`.

## Frame rate — 30 fps cap, and how to lift it

The native Xbox 360 build runs at 30 fps. The lock is a **guest-side frame
gate**: every frame the game waits for the GPU vblank counter (written back by
the ReXGlue command processor once per vblank) to advance **2 units** — two
16.6 ms vblanks = 33 ms. The ReXGlue vblank worker paces itself from the
`vsync` cvar: on = guest refresh rate (60 Hz), off = ~1000 Hz.

**Uncapped launcher (no rebuild, no SDK changes):**

out\build\win-amd64-release\fable2-uncapped.cmd D3D12, uncapped out\build\win-amd64-release\fable2-uncapped.cmd vulkan Vulkan, uncapped


It just sets `REX_VSYNC=0` (the SDK's env-var form of the `vsync` cvar) and
calls `fable2.cmd`. The vblank counter then advances ~1000x/s, the guest's
"wait +2 units" gate completes in ~2 ms, and the game runs uncapped (observed
20-100 fps depending on scene; check the Guest FPS line in the F3 overlay).

Equivalent one-liner: `$env:REX_VSYNC=0; .\fable2.cmd d3d12` (PowerShell), or
toggle `vsync = false` in the F3 cvar overlay live, or put it in
`fable_2.toml` next to the exe. To run at the native 30 fps, use `fable2.cmd`
without the env var.

Full investigation record (what was ruled out, the counter chain, the decisive
experiments): `docs/FPS_CAP_INVESTIGATION.md`.

## Guest function-call tracing (fable2_func_trace.log)

To figure out what each recompiled function does, every guest function entry
can be logged by name. Codegen emits `REX_FUNC_PROLOGUE()` at the top of
every function in `generated/default/fable_2_recomp.*.cpp`; the build hooks
that one macro (via `src/core/fable2_func_trace.h`, appended to the recompiled
PCH after the generated pch — no generated files are modified) so each entry
logs its name to `fable2_func_trace.log` next to the exe. Consecutive calls
of the same function are run-length encoded (per thread) to keep the file
small:

GetNewGameLoadingGlobal sub_82189708 x 4821 LoadingScreen_Virtual43 sub_82CC1BC0 x 3 ...


(`x N` means that function was called N times in a row; a bare name is a run
of one.)

**Session summary:** call counts are accumulated separately and written to
`fable2_func_summary.log` (same folder), one line per function, sorted by
total count:

18422331 x MainRenderLoop_82B9CD68 9711204 x __restgprlr_28 54 x Story_FirstChildCombat


It's refreshed every 5 s while tracing (and once more on clean exit), so you
can watch it in another window to see at a glance what's being called; the
last write is at most ~5 s stale even if the game exits via ExitProcess.
Counts respect the same on/off + filter as the text log.

Off by default (one atomic load per call when off). Enable with:

set FABLE2_FUNC_TRACE=1 rem every guest function entry set FABLE2_FUNC_TRACE_FILTER=LoadingScreen rem optional: only names containing this


Either output can be turned off independently (default: both on):
`FABLE2_FUNC_TRACE_LOG=0` skips `fable2_func_trace.log` (summary only),
`FABLE2_FUNC_TRACE_SUMMARY=0` skips `fable2_func_summary.log` (trace only).

or at runtime from a named-function override (see `src/diagnostics/fps_probe.h` for the
override pattern): `Fable2FuncTraceSetEnabled(true)` /
`Fable2FuncTraceSetFilter("LoadingScreen")` /
`Fable2FuncTraceSetSubsOnly(true)` /
`Fable2FuncTraceSetLogEnabled(false)` /
`Fable2FuncTraceSetSummaryEnabled(false)` / `Fable2FuncTraceFlush()`
(declared `extern "C"` in `src/core/fable2_func_trace.h`).

**Naming mode** (`FABLE2_FUNC_TRACE_SUBS_ONLY=1`): log only the unnamed
guest functions - names matching `sub_` + hex digits - dropping named
functions, the `__savegprlr_*`/`__restgprlr_*`/`__savevmx_*` register
helpers, and `xstart`. Both the text log and the summary honor it, so the
summary becomes a ranked list of the unnamed hot functions to name next.
Composes with the substring filter (`FABLE2_FUNC_TRACE_FILTER=82B9` narrows
to an address range).

**Launcher:** `fable2-functrace.cmd` (staged next to the exe) does the env
var dance for you and renames the previous session's log to
`fable2_func_trace_prev.log` first:

Arguments are keywords in any order (`subs` enables naming mode,
`trace`/`summary` pick which output file(s) are written, the backend word
picks the GPU path, anything else is the substring filter):

fable2-functrace.cmd D3D12, trace every call fable2-functrace.cmd vulkan Vulkan, trace every call fable2-functrace.cmd subs naming mode: only sub_ functions fable2-functrace.cmd subs LoadingScreen naming mode + substring filter fable2-functrace.cmd d3d12 82B9 subs D3D12, address-range naming mode fable2-functrace.cmd summary summary file only, no trace log fable2-functrace.cmd trace trace log only, no summary file


With no `trace`/`summary` keyword both files are written (the default).
`summary` is the cheap option for long sessions - it avoids the multi-GB
sequential log while still accumulating the call counts.

It writes its own lightweight log (same pattern as `fps_probe.log`) rather
than the SDK spdlog logger, which would be far too slow at Fable 2's call
rate. To remove the feature: delete the `target_precompile_headers` block in
CMakeLists.txt + `src/core/fable2_func_trace.h` and rebuild.

## Keyboard controls

The game normally reads a gamepad via the Xbox 360 `XamInputGetState` API. A
synthetic "keyboard gamepad" input driver (`src/input/keyboard_gamepad.h`) is added
on top of the default SDL driver, so host keyboard keys can drive the guest on
top of (OR-merged with) whatever a real gamepad reports. The physical pad
keeps working; the keyboard just adds buttons. It is wired up in
`Fable2App::OnPreSetup` via `config.input_factory`.

The mapping is the `keyboard_gamepad_map` cvar, format `Key:Button,Key:Button,...`. Its default is no longer hardcoded in the binary: it comes from `[input] keyboard_gamepad_map` in `fable2_config.toml` (see User config above), which you can edit to remap permanently. The command line and the F3 console still override it per-launch / live.

- **Key** — a host key name understood by `rex::ui::ParseVirtualKey`
  (`E`, `Space`, `LeftShift`, `F1`, ...).
- **Button** — a guest gamepad input: `A`, `B`, `X`, `Y`, `LB`/`RB` (shoulders),
  `LT`/`RT` (triggers), `Up`/`Down`/`Left`/`Right` (dpad), `Pause` (Start),
  `Select` (Back), `L3`/`R3` (thumb clicks), and `StickUp`/`StickDown`/
  `StickLeft`/`StickRight` (left thumbstick, full deflection while held).

The default layout is:

| Key(s) | Guest input |
|---|---|
| `E` / `2` / `1` / `3` | `A` / `B` / `X` / `Y` |
| `W` `A` `S` `D` | Left stick (up / left / down / right) |
| `Escape` | Pause (Start) |
| `M` | Select (Back) |
| `Q` / `Tab` | Left / Right trigger |
| `F1` `F2` `F3` `F4` | Dpad up / down / left / right |

Remap at launch without recompiling, e.g.

fable_2.exe --keyboard_gamepad_map "E:A,B:B,Space:L3,Enter:Start"


### Mouse look (right stick)

Mouse movement is mapped to the guest **right stick** for camera control. The
movement since the previous poll is converted into stick deflection, so you
**sweep the mouse to look and stop to stop**. Two cvars control it:

| Argument | Effect |
|---|---|
| `--mouse_look <bool>` | Enable/disable mouse look (default `true`) |
| `--mouse_look_scale <n>` | Sensitivity: right-stick units per pixel of mouse movement (default `256`; larger = more sensitive) |

The defaults come from `[input] mouse_look` / `[input] mouse_look_scale` in
`fable2_config.toml` (edit there to change them permanently), the command
line overrides per launch, and both cvars are hot-reloadable from the in-game
console, so you can dial in the sensitivity live. Example: `fable_2.exe
--mouse_look_scale 512` for a more sensitive camera.

All cvars above are hot-reloadable, so they can also be changed from the in-game console.

## F5 — run an external Lua script

Pressing **F5** (host keyboard) runs an external Lua file in the in-game Lua
state, exactly the way the game's own `RunScript(path)` global does — but
triggered from the host. This lets you drop a plain `.lua` file on disk and run
it against the live game (no recompile of the scripts needed).

- **Default file:** `data/scripts/recomp/F5.lua` (the build stages
  `src/lua/*.lua` into `data/scripts/recomp/` next to the exe). The shipped
  `F5.lua` snapshots the hero's position (`QuestManager.HeroEntity:GetPosition()`)
  and shows `X / Y / Z` in a message box.
- **Path:** set by the `f5_lua_path` cvar (default `scripts/recomp/F5.lua`,
  resolved relative to the VFS root `data/`). Override per-launch with
  `fable_2.exe --f5_lua_path "scripts/other/MyScript.lua"`.
- **How it works:** `src/core/fable2_f5_lua.h` captures the
  `CScriptManager::RunScript` callable the first time the game loads a `.lua`
  script (via a probe on the LuaPlus bound-method dispatcher), then replays that
  call with your path when F5 is pressed. The file is loaded fresh on each press,
  so you can edit it live (the VFS re-reads it).
- **The script runs in the game's global Lua environment**, so it has the full
  game API (`QuestManager`, `Debug`, `GUI`, `Creature`, `Player`, ...). Plain
  text is fine — `RunScript`/`loadfile` compile it for you.

Implementation: F5 edge-detection in `src/input/keyboard_gamepad.h`, a per-frame
replay from the `MainRenderLoop` hook in `src/diagnostics/fps_meter.h`, and the
string-build + `RunScript` call in `src/core/fable2_f5_lua.h`.

## Remote control (AI input channel)

`fable_2.exe` runs a localhost TCP **remote control server** so an external
AI/automation harness can drive the guest gamepad over JSON-lines messages —
no human at the keyboard. Design doc: `plans/ai-remote-input-control.md`.

**Debug builds only.** This is a debugging/automation channel — it opens a
localhost TCP port and injects guest input — so it must not ship to players.
It is gated on the `FABLE2_REMOTE_CONTROL` macro, defined only for the Debug
build; Release / RelWithDebInfo builds compile out the pad driver + server
entirely (no port is opened and `fable2_control.py` is not staged).

- **Where:** `127.0.0.1:8791` by default (configurable, see below). One JSON
  object per line; every request gets exactly one response line; keep-alive
  connections are supported.
- **How:** a second synthetic pad driver
  (`src/input/remote_gamepad_driver.h`) is registered next to the keyboard driver,
  fed by the server (`src/input/remote_control_server.h`). It OR-merges with the
  human pads and is **not** gated on window focus, so the AI can drive the
  game while it's in the background.

Quick start (from the build dir, game running):

python tools\fable2_control.py ping python tools\fable2_control.py press A --hold 120 python tools\fable2_control.py get-state python tools\fable2_control.py script --file repro.json


Or speak the protocol directly (any language):

{"cmd":"press","input":"RT","hold_ms":900} < {"ok":true,"input":"RT","release_in_ms":900} {"cmd":"script","steps":[ {"delay_ms":500,"op":"press","input":"LB","hold_ms":2000}, {"delay_ms":1000,"op":"press","input":"A","hold_ms":80}]} < {"ok":true,"duration_ms":2500}


`hold_ms` ≤ 0 (or omitted) means *hold until released/cleared*; a positive value
auto-releases after that many ms. In a `script`, each step's `delay_ms` is a **gap
since the previous step** on a single clock — the example presses LB at t=500 (held to
t=2500) and A at t=1500 (held to t=1580), so `duration_ms` (when the last input stops
being active) is 2500.

Commands: `ping`, `info`, `auth`, `press` (`input`, `hold_ms`, `value`),
`release`, `stick` (`input` = `StkLx`/`StkLy`/`StkRx`/`StkRy`, `value`,
`hold_ms`), `state` (sticky baseline: `buttons[]`, `triggers{LT,RT}`,
`stk{lx,ly,rx,ry}`), `clear`, `script` (atomic timed sequence), `get_state`,
`game_state` (current boot/menu state, see below), `cvar` (get/set any cvar
by name), `enable`/`disable`. Input names match the
keyboard-gamepad vocabulary (`A`/`B`/`X`/`Y`, `LB`/`RB`, `LT`/`RT`,
`Up`/`Down`/`Left`/`Right`, `Start`/`Back`, `L3`/`R3`, stick direction
shorthands). `StkLy` positive = forward (Fable 2 convention). `script` is a
single atomic message, so a repro sequence runs with no network round-trips
between steps.

### Game state

`game_state` reports which boot/menu screen the game is on so the AI can
navigate and verify its actions:

python tools\fable2_control.py game-state

{"cmd":"game_state"} < {"ok":true,"state":{"code":2,"name":"PressAScreen"}}


The classifier samples once per second on a background thread (independent of
the render loop, so it keeps working during the movie, which renders video with
no UI text) and reports one of:

| State | Meaning |
|---|---|
| `?` (Unknown) | Arena not mapped yet / undetermined (first ~1 s). |
| `PreMainMenu` | Splash / intro, before the "Press A" prompt. |
| `PressAScreen` | The "Press A to start" prompt is on screen. |
| `MainMenuMovie` | The idle movie is playing (no prompt / menu). |
| `MainMenu` | The main menu (reached by pressing A on the prompt). |

This follows the game's own state machine:
`PreMainMenu →(time)→ PressAScreen →(time)→ MainMenuMovie →(time)→
PressAScreen`, with `PressAScreen →(A)→ MainMenu` and
`MainMenuMovie →(A)→ PressAScreen`. The prompt vs. the menu are visually
indistinguishable (same element-list draw rate), so the transition into the
menu is driven by the A-press, observed on the final merged pad state (remote +
keyboard + physical — it works no matter which input drives A) and latched with
its exact timestamp. State changes are logged to the in-game logger
(`REXSYS_INFO`, same channel as the remote control) on transition only, and
every sample is written to `fable2_state_probe.log` when
`FABLE2_STATE_PROBE=1` is set.

Config (`[remote]` in `fable2_config.toml`): `enabled` (default `true`),
`host` (`127.0.0.1`; `0.0.0.0` exposes it on all interfaces), `port`
(`8791`; if busy, `+1..+9` are tried, the bound port is logged at startup),
`token` (empty = no auth; when set, each connection's first line must be
`{"cmd":"auth","token":"..."}`). Every received command is logged (audit
trail) to `logs/`.

## Building

Everything the build needs is either in this repo or auto-fetched — **no
hardcoded paths**. Prerequisites (all standard tools):

- **CMake** ≥ 3.25 (on PATH)
- **LLVM** (clang/clang++/lld) — on PATH or the default `C:\Program Files\LLVM` install
- **Ninja** (on PATH, `%USERPROFILE%\bin`, or the WinGet package dir)
- **Internet** on first build — `build.cmd` auto-downloads the prebuilt
  ReXGlue SDK v0.10.0 (~100 MB) from the
  [official release](https://github.com/rexglue/rexglue-sdk/releases/tag/v0.10.0)
  into `thirdparty\rexglue-sdk\`. A pre-existing SDK is used instead if found:
  `thirdparty\rexglue-sdk\win-amd64` first, then a sibling
  `..\rexglue-sdk-0.10.0-win-amd64\win-amd64`.

build.cmd # configure + fable_2_codegen (runs rexglue codegen on the manifest) build.cmd fable_2 # configure + build the full executable build.cmd # any other CMake target build.cmd -release [t] # build as Release (-O3) instead of Debug (-r is short form)


The first `build.cmd` run fetches the SDK if needed, then configures and
builds — that is all a fresh checkout requires (plus the game content above).

Manual/advanced setup (normally not needed):

tools\setup_sdk.cmd # fetch the prebuilt SDK only (what build.cmd auto-runs) tools\setup_sdk_src.cmd # fetch the SDK SOURCE for the Vulkan plugin: clones # rexglue/rexglue-sdk at the pinned commit # (f5337cdc), pins its submodules to the SHAs in # thirdparty/rexglue-sdk-submodule-pins.txt, and # applies thirdparty/rexglue-sdk-local.patch. LONG # (several GB). git # only needed for setup_sdk_src.cmd


The build also accepts explicit overrides if you keep the SDK elsewhere:
`cmake -DREXGLUE_SDK_ROOT=<prebuilt SDK root> -DREXGLUE_SDK_SOURCE=<SDK source>`
(`build.cmd` sets `REXGLUE_SDK_ROOT`/`REXGLUE_SDK_SOURCE` from its own search).

- Toolchain: the `win-amd64-debug` preset (Ninja + clang) by default. The SDK's public headers use clang builtins (`__builtin_bswap32`, `__VA_OPT__`), so plain MSVC `cl` cannot compile the generated code — a `win-msvc-debug` preset exists but only works for targets that don't compile `generated/`.
- **Release builds:** `build.cmd` defaults to the `win-amd64-debug` preset (`-O0`, `out\build\win-amd64-debug`). Pass the `-release` flag (short form: `-r`) to select the `win-amd64-release` preset (`-O3`, `out\build\win-amd64-release`):

build.cmd -release fable_2_codegen build.cmd -release fable_2


The release build stages the release runtime/GPU plugins (`rexruntime.dll`, `rexgpu-xenos.dll`) instead of the debug ones (`rexruntimed.dll`, `rexgpu-xenosd.dll`). **Debug builds are drastically slower at runtime** — the recompiled guest code, the runtime, and the Xenos GPU emulator all run at `-O0` with assertions enabled — so use Release for any performance-sensitive run. Omit the flag to build Debug again; the two build trees are independent and can coexist.
- Codegen runs as part of the build and re-runs automatically when `fable_2_manifest.toml` or `default.xex` change (tracked via the generated DEPFILE).
- A full clean build recompiles ~291 generated translation units (60,462 guest functions).

## Vulkan renderer

The goal is a Vulkan rendering path for the game (the SDK's Windows GPU plugin only ships a
D3D12 Xenos backend; the SDK's own Vulkan backend lives in `rexglue-sdk-src/src/graphics/vulkan/`
but is not compiled into the shipped plugin). Per the project constraint, all Vulkan work lives in
this repo. The SDK source itself is built as stock upstream **plus this
project's tracked local patch** (`thirdparty/rexglue-sdk-local.patch` —
see below); the upstream repo is never forked or pushed to.

**Stage 1 — foundation (DONE, verified).** A self-contained Vulkan pipeline that creates a window,
inits Vulkan, and presents a clear color, proving the whole stack on a real GPU:

build.cmd -release fable_2_vulkan_smoke out\build\win-amd64-release\fable_2_vulkan_smoke.exe # -> stable blue window


Pipeline: dynamic `vulkan-1.dll` loader (no import lib needed) → `VkInstance` → Win32 surface →
device (graphics+present queue) → swapchain (3 images) → render pass → per-image framebuffers +
command buffers (clear-color subpass) → semaphores/fences → present loop. Logs to
`vulkan_smoke.log` next to the exe.

Key implementation notes (hard-won):
- **Window via the SDK's UI framework, not raw Win32/SDL.** The window is created with
  `rex::ui::SDLWindowedAppContext` + `rex::ui::Window` from the *shared* `rexruntime.dll` — the
  same mechanism the game uses. Raw `CreateWindowExW` and the *static* SDL build both fail to
  create/init a window here; only the shared-SDL path (the `rex::ui::*` exports) works. The target
  links the `rexruntime.lib` import lib and stages `rexruntime.dll` next to the exe.
- `#define NOMINMAX` before `<windows.h>` is required (the SDK's `math.h` uses `std::min` /
  `std::numeric_limits::max`, which the Windows `min`/`max` macros would break).
- Image views backing the swapchain **must outlive the framebuffers** (destroying them early makes
  the driver fault in `vkCmdBeginRenderPass`).
- Vendored Khronos headers live in `thirdparty/vulkan` + `thirdparty/vk_video` (Vulkan 1.3.282).

**Stage 2 — render the game via Vulkan (in progress).** The SDK's Vulkan backend is a complete,
~1.4 MB renderer compiled into `rexgpu-xenos` only when `REXGLUE_USE_VULKAN=ON` (OFF by default on
Windows). Rather than re-implementing it, we build the SDK source **plus the
project's local patch** (`thirdparty/rexglue-sdk-local.patch`) into a Vulkan GPU
plugin and load it via the `gpu_plugin` mechanism, so the renderer can be **swapped at launch, no
rebuild of the game**:

tools\build_sdk_vulkan.cmd # builds the SDK source with REXGLUE_USE_VULKAN=ON # (auto-fetches it first via tools\setup_sdk_src.cmd: # clone at the pinned commit + submodule pins + # thirdparty\rexglue-sdk-local.patch, into # thirdparty\rexglue-sdk-src) -> the plugin lands # in \out\win-amd64\Release\rexgpu-xenos.dll


The SDK source is the stock `rexglue/rexglue-sdk` repo **plus this project's
local modifications** (`thirdparty/rexglue-sdk-local.patch`: the main-menu
crash fix in `exception_handler_win.cpp` + `xmemory.cpp`, and the Vulkan/
FPS instrumentation — see `docs/main_menu_crash_fix.md` and the patch
header). `tools/setup_sdk_src.cmd` reproduces that exact tree from the
tracked patch + submodule pins, so the source never has to live outside
this repo.

Two plugins are staged next to the exe, selected by the `gpu_plugin` argument:

out\build\win-amd64-release\fable_2.exe --gpu_plugin=xenos # D3D12 (default; prebuilt plugin) out\build\win-amd64-release\fable_2.exe --gpu_plugin=xenos-vulkan # Vulkan (source-built plugin)


- `rexgpu-xenos.dll` is the prebuilt D3D12 plugin (staged by `GPU_PLUGINS xenos`).
- `rexgpu-xenos-vulkan.dll` is the source-built plugin (staged by CMake from the SDK build tree,
  Release-only). `Fable2App::OnPreSetup` loads it with `LoadGpuPlugin("xenos-vulkan", "vulkan")`,
  forcing the Vulkan backend (the plugin is compiled with both; the default `"any"` would pick D3D12).
- The source plugin must match the game's runtime: the Release source plugin imports `rexruntime.dll`,
  so it is staged for the Release game only (the Debug game links `rexruntimed.dll`).
- The SDK source location is resolved relative to this repo (`thirdparty/rexglue-sdk-src`,
  or a sibling `..\rexglue-sdk-src` for pre-existing setups) and can be overridden with
  `-DREXGLUE_SDK_SOURCE=<path>`.

## Manifest highlights (fable_2_manifest.toml)

Migrated from the XenonRecomp config (`XenonRecomp/fable2.toml`):

- 87 manual function boundaries. ReXGlue forbids the overlapping boundaries XenonRecomp allowed: three nested pairs were trimmed to end where the inner function begins, and `0x82C8D3F0` keeps its original full size with the nested `0x82C8D4E8` entry removed (the gap-fill otherwise extended the outer and emitted an illegal cross-function `goto`). Original sizes are recorded in the file's comments and in `XenonRecomp/fable2.toml`.
- 13 "gap seed" function entries (no size — the scanner sizes them) for branch targets the auto-discovery never registered: a chain of 8-byte thunks at `0x82C000F0`–`0x82C00128` plus stragglers (`0x82BEA25C`, `0x82CD7948`, `0x82C14D58`, `0x82F279D8`, `0x82E7E4F8`, ...).
- `setjmp_address = 0x83000200`, `longjmp_address = 0x82CA9260`, and two `[[invalid_instructions]]` data-table skips, carried over verbatim.
- The XenonRecomp register save/restore helper addresses (`restgprlr_14` etc.) have no ReXGlue equivalent and are preserved only as comments.
- Known runtime risk: codegen logs a handful of non-fatal "Unresolved conditional branch" warnings (e.g. `0x82C8D408`, `0x82C99E74`, `0x82C9A0BC`); those paths emit `REX_FATAL` if hit.

## Function naming rule (important)

A manifest `name` becomes a C symbol: codegen emits the function body as
`__imp__<name>` (extern "C"). A name must therefore NOT equal an XAPI export
of `rexruntimed.dll` (the DLL exports `__imp__<XAPI name>` for every
xboxkrnl/xam hook). If it does, the local definition silently shadows the
DLL import at link time (no linker error), and the generated import-thunk
registration (`fable_2_register.cpp`) routes XAPI calls into the guest
function body instead of the SDK kernel hook. That is what broke audio when
the game-internal KeWait re-implementations were named
`KeWaitForSingleObject` / `KeWaitForMultipleObjects`: the xboxkrnl import
thunks (0x832B28AC / 0x832B2CDC) started landing in the guest wrappers,
whose timeout field is milliseconds while kernel callers pass 100ns units
(0 = infinite). They are named `..._Guest` for this reason — keep it that
way. Check before committing name changes:

python tools/check_manifest_collisions.py


Fable-2-Recomp (himdo/Fable-2-Recomp)

Searching Database…

What is this?A native PC port of Fable 2: the Xbox 360 game's PowerPC code is statically recompiled into a Windows executable with the ReXGlue SDK, so it runs on PC without a console.
Is this an emulator, or how does it differ from one?It differs from a classic emulator: a build step converts the guest PPC code to C++ at compile time via rexglue codegen, producing a native exe plus runtime plugins instead of interpreting code at runtime.
How do I get it running?Build with build.cmd (it auto-fetches the prebuilt ReXGlue SDK), then either stage the game content next to the exe with tools\stagecontent.cmd or point the exe at existing content with --gamedataroot.
What do I need before trying it?The Fable 2 GOTY disc image for USA/Europe (its sha256 is listed in the README), about 6.5 GB of game content, and 64-bit Windows; git is only needed to build the optional Vulkan plugin from SDK source.
Is the port complete?The game is completable — the author finished it — but the project is not considered fully complete yet, as the author has not 100%'d the game, so expect rough edges.
Can I move the installation to another PC?Yes — with content staged next to it, the whole build folder is portable: copy it anywhere and it runs with saves staying inside; missing content shows a clear error naming the missing piece.
Can I choose between graphics backends?Yes: D3D12 is the default and ships as a prebuilt plugin, while a Vulkan plugin can be built from the ReXGlue SDK source (a multi-GB fetch) and selected with --gpuplugin=xenos-vulkan.
Why did the author build this project?It was created to test what local, open-source AI can do: the author says the game was running after about an hour of work by Qwen3.8 27b, after which a large amount of human trial and error mapped enough functions to complete the game.
Can I play with a keyboard instead of a controller?Yes — the exe accepts a --keyboardgamepadmap flag that maps keyboard keys to gamepad inputs (for example E to A, Space to L3, Enter to Start), and a bundled tools\fable2control.py script can press buttons or replay scripted input sequences.
Can I apply gameplay patches like with Xenia?Yes — the recomp reads fable2patches.toml, a guest-image patch table in Xenia game-patches format, and each patch has its own enabled toggle.
Creator
himdo
Size
4.8 MB
Stars
452
Age
1mo
Commits · 90 Days