> ⚠️ **The audience line below predates a decision.** It assumes the `xenia-rs` > Rust emulator, which is **retired** — Canary *is* the emulator and the oracle > now, and this guide's value is unchanged: it is how you get a Windows-MSVC > debug build running under Wine. Read "audit oracle for the Rust port" as > "instrumented build for RE". # Building xenia-canary as a Windows Debug Binary on Linux — Complete Setup Guide > **Audience**: A fresh Linux machine with the `xenia-rs` Rust project cloned but no `xenia-canary` checkout yet, and no cross-compile toolchain. By the end of this guide you will have an instrumentation-ready Windows-MSVC debug build of xenia-canary running under Wine, suitable as the audit oracle for the Rust port. --- ## Preface — context for a fresh receiver ### Why this build exists The receiver's Rust project (`xenia-rs`) is a partial reimplementation of [xenia-canary](https://github.com/xenia-canary/xenia-canary), the community-maintained fork of the Xenia Xbox 360 emulator. Audits compare `xenia-rs` execution traces against canary execution traces on the same guest binary (the Project Sylpheed XEX, embedded in the project ISO) to identify divergences. For those comparisons to be meaningful: 1. **Canary must be debug-mode** so XELOG channels (`i> Setup: ...`, `K> XThread::Execute ...`, etc.) emit verbosely. 2. **Canary must run on the same host as `xenia-rs`** to remove machine-state confounds. 3. **Canary must accept custom audit instrumentation** (new `cvars::audit_XX_*` flags and `XELOGI("AUDIT-XX-…")` trace lines) that can be toggled per run. A **Linux-native** canary build exists (`xb build` in the upstream tree) but in practice doesn't render past the splash screen on this stack — the host XCB / Vulkan pipeline gets stuck before the front-end UI advances. The **Windows-targeted** canary build runs the game cleanly when launched under Wine with the right runtime DLLs (`vkd3d-proton` + `DXVK`). So the workable oracle is: cross-compile a Windows debug build on Linux, run it under Wine. ### What you'll produce | Artifact | Path | Approx size | |---|---|---| | Windows PE32+ executable | `xenia-canary/build-cross/bin/Windows/Debug/xenia_canary.exe` | 27 MB | | Matching CodeView PDB | `xenia-canary/build-cross/bin/Windows/Debug/xenia_canary.pdb` | 114 MB | ### Time and disk budget | Resource | First-time | Incremental | |---|---|---| | Wall time (decent box: 16+ cores, 30 Mbps net) | ~30 minutes total | < 60 seconds per code change | | Network download | ~2.3 GB (xwin ~600 MB + Win10 SDK ISO ~1.1 GB + Wine bits ~150 MB + xenia source ~400 MB) | 0 | | Free disk required | ~10 GB | ~6 GB (build dir keeps growing as you instrument) | ### Workflow at a glance The setup is a 12-phase pipeline. Each phase has a **[CHECKPOINT]** verification command — run it before moving on. Failure-mode lookups are in Appendix A. ``` Phase 0 → Workspace baseline + path conventions Phase 1 → System packages (apt + cargo) Phase 2 → clang-cl symlink Phase 3 → xwin: download + splat the Win10 SDK & MSVC CRT Phase 4 → Win10 SDK fxc.exe: ISO download + CAB extraction Phase 5 → Wine runtime DLL trio (vkd3d-proton + DXVK + system Wine) Phase 6 → Clone xenia-canary Phase 7 → Apply 8 build-fix source patches Phase 8 → Add 3 cross-compile config files Phase 9 → Configure (cmake --preset) Phase 10 → Build (cmake --build) Phase 11 → Smoke test under Wine (no error dialogs!) Phase 12 → Wire into the xenia-rs audit workflow ``` Phases 3 and 4 can run in parallel (both are bandwidth-bound and independent). Everything else is sequential. --- ## Phase 0 — Workspace baseline & path conventions This guide uses the following placeholders. Bind them in your shell once and re-use them throughout — every command below assumes they are set. ```bash export XENIA_RS_ROOT="/home/$(whoami)/RE - Project Sylpheed" # adjust if you keep things elsewhere export XENIA_CANARY_SRC="$XENIA_RS_ROOT/xenia-canary" export XWIN_DIR="$HOME/.xwin/splat" export FXC_CROSS_DIR="$HOME/.local/share/xenia-cross/fxc" export ISO_PATH="$XENIA_RS_ROOT/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso" ``` > **[CHECKPOINT P0]** Confirm `xenia-rs/` exists at the expected location and the ISO is readable: > ```bash > ls -d "$XENIA_RS_ROOT/xenia-rs" && test -r "$ISO_PATH" && echo OK > ``` > Expected output: a directory listing and `OK`. If the path is different on your system, re-export the variables accordingly. The workspace layout you're targeting: ``` $XENIA_RS_ROOT/ ├── xenia-rs/ ← already present (your Rust port + audit-runs) ├── sylpheed-reborn/ ← already present (asset-side Rust tools, optional) ├── xenia-canary/ ← will be cloned in Phase 6 (THIS GUIDE'S PRODUCT) │ └── build-cross/bin/Windows/Debug/ ← will be built in Phase 10 │ ├── xenia_canary.exe ← the binary you'll run │ └── xenia_canary.pdb ← debug symbols for crash backtraces └── Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso ← already present ``` --- ## Phase 1 — System packages The package list is one apt command. Cargo is needed for `xwin` and isn't shipped with most distros. ### 1.1 apt packages ```bash sudo apt update sudo apt install -y \ clang-18 lld-18 llvm-18 \ cmake ninja-build \ python3 \ wine64 winetricks \ p7zip-full \ curl wget \ git \ pkg-config \ build-essential ``` **Tested distros**: Ubuntu 24.04 LTS, Linux Mint 22.1 (Xia, Ubuntu-24.04-base). Other Debian/Ubuntu derivatives ≥ 24.04 should work identically. On older LTS (22.04) the clang ≥ 16 floor for xwin's MSVC STL may not be met from the default repos — add LLVM's official apt source if so. ### 1.2 Rust toolchain If you don't already have `cargo`: ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source "$HOME/.cargo/env" ``` The receiver may already have a Rust toolchain because `xenia-rs` is Rust. Confirm: ```bash cargo --version # ≥ 1.70 rustc --version # ≥ 1.70 ``` > **[CHECKPOINT P1]** All required tools resolve and report a sane version: > ```bash > for t in clang-18 lld-18 ninja cmake python3 wine winetricks 7z cargo; do > command -v $t > /dev/null && echo "OK $t" || echo "MISSING $t" > done > ``` --- ## Phase 2 — `clang-cl` symlink **Why this is its own phase**: Ubuntu's `clang-18` package ships `clang-18`, `clang++-18`, and `clang-cpp-18` but **not** `clang-cl`. clang only activates its MSVC-driver mode when `argv[0] == "clang-cl"` or `--driver-mode=cl` is passed. The cleanest fix is one symlink. ```bash sudo ln -s /usr/bin/clang-18 /usr/bin/clang-cl ``` Verify both that the symlink exists and that clang reports the MSVC target when invoked through it: ```bash ls -la /usr/bin/clang-cl clang-cl --version # Expected: third line says Target: x86_64-pc-windows-msvc ``` `lld-link`, `llvm-rc`, `llvm-lib`, `llvm-mt` are already on PATH from the apt packages. Confirm: ```bash for t in lld-link llvm-rc llvm-lib llvm-mt; do command -v $t > /dev/null && echo "OK $t" done ``` > **[CHECKPOINT P2]** `clang-cl --version` reports `Target: x86_64-pc-windows-msvc`. All four LLVM utilities are on PATH. --- ## Phase 3 — `xwin`: download & splat the Windows SDK + MSVC CRT `xwin` (by Jake Shadle) downloads Microsoft's redistributable CRT + Windows SDK directly from Microsoft's update servers and arranges them into a usable cross-compile sysroot. ~600 MB download, ~807 MB on disk. ```bash cargo install --locked xwin xwin --accept-license --arch x86_64 splat \ --include-debug-libs \ --output "$XWIN_DIR" ``` Notes & caveats: - `--include-debug-libs` is **mandatory** — even though we'll force the release CRT for runtime (Phase 8.3), headers reference `_ITERATOR_DEBUG_LEVEL` library symbols at link time. - The default splat layout is **flat** (`crt/include/`, `sdk/include/{ucrt,um,shared,winrt}/`). **Do not** pass `--use-winsysroot-style` — our toolchain uses explicit `-imsvc` flags against the flat layout. If you change this, rewrite the toolchain accordingly. - xwin auto-creates **lowercase symlinks** for headers that ship with uppercase names (e.g. `windows.h → Windows.h`). It does **not** create the reverse. Phase 9 includes a helper script that patches the reverse-case aliases that xenia needs (e.g. `` against a SDK that ships `objbase.h` only). > **[CHECKPOINT P3]** Splat directory is populated: > ```bash > test -d "$XWIN_DIR/crt/include" \ > && test -d "$XWIN_DIR/sdk/include/um" \ > && du -sh "$XWIN_DIR" # ≈ 807 MB > ``` --- ## Phase 4 — Real Win10 SDK fxc.exe **Why this can't be skipped**: xenia's D3D12 backend uses Shader Model 5.1 (`cs_5_1`, `ps_5_1`, …). The legacy DirectX SDK June 2010 fxc (commonly distributed via `winetricks dxsdk_jun2010`) caps at SM 5.0 — it cannot compile current xenia shaders. **No standalone NuGet package contains just `fxc.exe`** (the `Microsoft.Windows.SDK.BuildTools` NuGet does **not** contain fxc despite the name). The reliable path is to fetch the full SDK ISO and extract two files surgically. This phase can run in parallel with Phase 3 — both are bandwidth-bound and independent. ### 4.1 Download the SDK ISO (~1.1 GB) ```bash mkdir -p "$FXC_CROSS_DIR" curl -L -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" \ -o ~/winsdk.iso \ "https://go.microsoft.com/fwlink/?linkid=2361406" # Verify file ~/winsdk.iso # Expected: "UDF filesystem data (version 1.5) 'KSDKWIN_…'" # NOT a PDF — see caveat below ``` **Caveat ⚠️ Microsoft's `go.microsoft.com/fwlink/?linkid=…` URLs rotate over time**, and they occasionally serve an unrelated PDF if the request lacks a browser User-Agent. The `-A` flag avoids the PDF redirect. If the link returns a PDF (`file` reports `PDF document`) or 404s, get the current "Installer"/"ISO" link from . Any 10.0.22000+ release contains an SM 5.1-capable fxc; pick the latest stable. ### 4.2 Extract the right MSI's CABs The fxc-bearing MSI is `Windows SDK for Windows Store Apps Tools-x86_en-us.msi`. It references 10 CABs by hash name. ```bash # Pull only Tools MSIs out of the ISO mkdir -p /tmp/winsdk-extract /tmp/sdk-cabs/extracted 7z x ~/winsdk.iso 'Installers/*Tools*.msi' -o/tmp/winsdk-extract -aoa >/dev/null # Confirm the right MSI MSI="/tmp/winsdk-extract/Installers/Windows SDK for Windows Store Apps Tools-x86_en-us.msi" strings "$MSI" 2>/dev/null | grep -q 'fxc\.exe' || \ { echo "FAIL: fxc not referenced in expected MSI"; exit 1; } # Pull the 10 CABs the MSI references CABS=$(strings "$MSI" | grep -oE '[0-9a-f]{32}\.cab' | sort -u) mkdir -p /tmp/sdk-cabs for c in $CABS; do 7z x ~/winsdk.iso "Installers/$c" -o/tmp/sdk-cabs -aoa >/dev/null done ls /tmp/sdk-cabs/Installers/*.cab | wc -l # → 10 # Extract every CAB into a flat dir of content-hashed payload files for c in /tmp/sdk-cabs/Installers/*.cab; do 7z x "$c" -o/tmp/sdk-cabs/extracted -aoa >/dev/null done ``` ### 4.3 Identify and copy the x64 fxc.exe + d3dcompiler_47.dll CAB payload files use opaque hashed names. We identify the right ones by PE architecture (`file`) + content strings. ```bash # fxc.exe: PE32+ x86-64 console executable containing "Direct3D Shader Compiler" in resources (~183 KB) for f in /tmp/sdk-cabs/extracted/*; do if file "$f" | grep -q 'PE32+ executable (console) x86-64' \ && strings -n 8 "$f" 2>/dev/null | grep -q 'Direct3D Shader Compiler'; then cp "$f" "$FXC_CROSS_DIR/fxc.exe" echo "fxc.exe installed from: $f" break fi done test -s "$FXC_CROSS_DIR/fxc.exe" || \ { echo "ERROR: fxc.exe not found in extracted CABs" >&2; exit 1; } # d3dcompiler_47.dll: PE32+ x86-64 DLL, ~4.75 MB, containing "D3DCompile_47" export string for f in /tmp/sdk-cabs/extracted/*; do sz=$(stat -c %s "$f") if [ "$sz" -gt 4500000 ] && [ "$sz" -lt 5000000 ] \ && file "$f" | grep -q 'PE32+ executable (DLL).*x86-64' \ && strings -n 8 "$f" 2>/dev/null | grep -qE '^D3DCompile_47$'; then cp "$f" "$FXC_CROSS_DIR/d3dcompiler_47.dll" echo "d3dcompiler_47.dll installed from: $f" break fi done test -s "$FXC_CROSS_DIR/d3dcompiler_47.dll" || \ { echo "ERROR: d3dcompiler_47.dll not found in extracted CABs" >&2; exit 1; } ``` ### 4.4 Verify under Wine ```bash wine "$FXC_CROSS_DIR/fxc.exe" /? 2>&1 | head -3 # Expected first line: "Microsoft (R) Direct3D Shader Compiler 10.0.…" wine "$FXC_CROSS_DIR/fxc.exe" /? 2>&1 | grep -E 'cs_5_1|ds_5_1' | head -1 # Expected: a line listing cs_5_1 among supported profiles ``` > **[CHECKPOINT P4]** Both files installed; fxc reports a 10.0.x version and lists `cs_5_1`. Cleanup optional but recommended: > ```bash > rm -rf /tmp/winsdk-extract /tmp/sdk-cabs ~/winsdk.iso > ``` --- ## Phase 5 — Wine runtime DLL trio xenia under Wine needs **three matched DLL overrides** in the active prefix. **All three pieces are required** — a partial install is worse than nothing because it segfaults on swapchain creation. | File(s) | Source | Provides | |---|---|---| | `d3d12.dll`, `d3d12core.dll` | **vkd3d-proton** ≥ 3.0 | D3D12 → Vulkan translation | | `dxgi.dll` | **DXVK** ≥ 2.4 | DXGI factory/swapchain (shared D3D9/10/11/12) | | host Wine | **system Wine 9.0** | NT API surface, JIT-friendly DLL address layout | The combo MUST be **vkd3d-proton + DXVK**, not just vkd3d-proton alone — Wine's *builtin* `dxgi` and vkd3d-proton's `d3d12` pass each other malformed swapchain handles and crash in `vkd3d_instance_get_vk_instance` (see Appendix A). ### 5.1 Install both into the default prefix ```bash WINEDEBUG=-all WINETRICKS_DOWNLOADER=wget winetricks -q vkd3d WINEDEBUG=-all WINETRICKS_DOWNLOADER=wget winetricks -q dxvk ``` Each verb downloads its tarball to `~/.cache/winetricks/`, extracts it into a temp prefix, and `ln -s`'s the DLLs into `~/.wine/drive_c/windows/system32/`. Takes ~3 minutes each. ### 5.2 Verify the trio ```bash # Size fingerprints stat -c '%n = %s bytes' \ "$HOME/.wine/drive_c/windows/system32/d3d12core.dll" \ "$HOME/.wine/drive_c/windows/system32/dxgi.dll" # Expected: # d3d12core.dll ≈ 6 MB (vkd3d-proton) — Wine builtin would be ~66 KB # dxgi.dll ≈ 3 MB (DXVK) — Wine builtin would be ~1.5 MB # String fingerprints strings "$HOME/.wine/drive_c/windows/system32/d3d12core.dll" | grep -i 'vkd3d-proton' | head -1 # Must hit a vkd3d-proton path string strings "$HOME/.wine/drive_c/windows/system32/dxgi.dll" | grep -i 'DXVK' | head -1 # Must hit DxvkAdap, DXVK:, or similar ``` > **[CHECKPOINT P5]** Both fingerprint checks pass. ### 5.3 What NOT to use - **Wine GE / Glorious Eggroll**: has esync/gamemode shims loaded in `0xA0000000-0xAFFFFFFF` — collides with xenia's hardcoded JIT generated-code region (see Appendix C). - **`winetricks d3dcompiler_47`** alone: that's the *runtime* DLL, irrelevant here. - **`winetricks dxsdk_jun2010`** alone: that's the SM 5.0 legacy fxc, fails on current shaders (see Phase 4). - **Suppressing `WINEDEBUG=-all`** when debugging a crash: masks the SEH stderr you need. --- ## Phase 6 — Clone xenia-canary ### 6.1 Decision: Plan A (vanilla upstream) or Plan B (continue prior audit work) | | Plan A | Plan B | |---|---|---| | Source | Fresh clone from upstream | Copy / rsync from a peer machine that has prior audit work | | Pros | 100% reproducible, minimal | Picks up `audit_NN_*` / `phase_b_*` accumulated instrumentation | | Cons | Loses any in-tree audit instrumentation | Less reproducible across machines | | Recommendation | Use this for first-time setup | Switch to this only if you're picking up a specific audit family | This guide assumes **Plan A**. If you go Plan B, skip ahead to Phase 9 — the eight build-fix patches in Phase 7 are already applied on the peer machine. ### 6.2 Clone ```bash cd "$XENIA_RS_ROOT" git clone --recurse-submodules \ https://github.com/xenia-canary/xenia-canary.git \ "$XENIA_CANARY_SRC" cd "$XENIA_CANARY_SRC" git submodule update --init --recursive git rev-parse HEAD ``` **Pinned commit for this recipe**: `6de80dffe261b368ecefee36c9b2b337335228c0` (early 2026 mainline). If upstream has moved beyond, the patches in Phase 7 may need fuzzy-applying: ```bash git checkout 6de80dffe261b368ecefee36c9b2b337335228c0 git submodule update --init --recursive ``` > **[CHECKPOINT P6]** `xenia-canary/` exists with submodules populated: > ```bash > test -f "$XENIA_CANARY_SRC/CMakeLists.txt" \ > && test -d "$XENIA_CANARY_SRC/third_party/SDL2/src" \ > && test -d "$XENIA_CANARY_SRC/third_party/DirectXShaderCompiler/include" \ > && echo "OK xenia-canary source ready" > ``` --- ## Phase 7 — Build-fix source patches (eight required) These patches **only fix build-time compilation issues** under clang-cl + xwin. None of them changes runtime behavior. They are required for both Plans A and B — apply each, then proceed. For each patch below: the `OLD` snippet is unique enough to anchor with an Edit-tool-style replacement. Apply them in any order. ### 7.1 `src/xenia/base/mapped_memory_win.cc` — constexpr → const clang-cl rejects `reinterpret_cast` in constant expressions (per the C++ standard); MSVC accepts as an extension. `INVALID_HANDLE_VALUE` expands to `((HANDLE)(LONG_PTR)-1)` — a chained int-to-pointer cast. ```diff - // chrispy: made inline const to get around clang error - static inline constexpr HANDLE kFileHandleInvalid = INVALID_HANDLE_VALUE; + // INVALID_HANDLE_VALUE expands to a reinterpret_cast which MSVC accepts + // in constexpr as an extension; clang-cl rejects it per C++ standard. + static inline const HANDLE kFileHandleInvalid = INVALID_HANDLE_VALUE; ``` ### 7.2 `src/xenia/app/main_resources.rc` — backslash → forward slash llvm-rc on Linux treats `\` as a literal filename character, so `..\\..\\..\\assets\\icon\\icon.ico` becomes a single literal name and the file isn't found. Forward slashes work on both rc.exe and llvm-rc. ```diff - MAINICON ICON "..\\..\\..\\assets\\icon\\icon.ico" + MAINICON ICON "../../../assets/icon/icon.ico" ``` ### 7.3 `third_party/snappy/snappy-stubs-public.h` — POSIX gate on `!_WIN32` This header is checked-in pre-generated; whoever originally ran cmake on snappy did so on a Linux host, so `HAVE_SYS_UIO_H` was baked in as `1`. Make the gate target-platform-aware. Apply two edits in the same file: ```diff - #if 1 // HAVE_SYS_UIO_H + // Pre-generated as 1 on the original Linux host; gate on platform so cross + // compiles to Windows fall back to the iovec definition below. + #if !defined(_WIN32) // HAVE_SYS_UIO_H #include #endif // HAVE_SYS_UIO_H ``` ```diff - #if !1 // !HAVE_SYS_UIO_H + #if defined(_WIN32) // !HAVE_SYS_UIO_H ``` ### 7.4 `third_party/zlib-ng/zconf-ng.h` — gate `Z_HAVE_UNISTD_H` on `!_WIN32` Same root cause as 7.3. ```diff - #if 1 /* was set to #if 1 by configure/cmake/etc */ + /* Pre-generated as 1 on the Linux host; gate on platform. */ + #if !defined(_WIN32) /* was set to #if 1 by configure/cmake/etc */ # define Z_HAVE_UNISTD_H #endif ``` ### 7.5 `third_party/CMakeLists.txt` — apply zlib-ng AVX flags under clang-cl too Real `cl.exe` exposes AVX-512 intrinsics unconditionally; clang-cl follows standard clang and requires `-mavx512*` per file. Extend the existing gate. ```diff - if(NOT MSVC) + if(NOT MSVC OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") target_compile_definitions(zlib-ng PRIVATE HAVE_BUILTIN_CTZ HAVE_BUILTIN_CTZLL) set_source_files_properties( zlib-ng/arch/x86/adler32_avx2.c zlib-ng/arch/x86/chunkset_avx2.c zlib-ng/arch/x86/compare256_avx2.c zlib-ng/arch/x86/slide_hash_avx2.c PROPERTIES COMPILE_OPTIONS "-mavx2;-mbmi2" ) set_source_files_properties( zlib-ng/arch/x86/adler32_avx512.c zlib-ng/arch/x86/chunkset_avx512.c PROPERTIES COMPILE_OPTIONS "-mavx512f;-mavx512dq;-mavx512vl;-mavx512bw;-mbmi2" ) set_source_files_properties( zlib-ng/arch/x86/adler32_avx512_vnni.c PROPERTIES COMPILE_OPTIONS "-mavx512f;-mavx512dq;-mavx512vl;-mavx512bw;-mavx512vnni;-mbmi2" ) endif() ``` ### 7.6 `CMakeLists.txt` (top-level) — gate `/RTCsu` + auto-generate `version.h` Two edits in the same file. **7.6.a** — `/RTCsu` is MSVC-only; clang-cl warns per-TU. Gate it. Around line 145: ```diff - string(APPEND CMAKE_C_FLAGS_CHECKED " /RTCsu /fsanitize=address") - string(APPEND CMAKE_CXX_FLAGS_CHECKED " /RTCsu /fsanitize=address") + if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC" + AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + string(APPEND CMAKE_C_FLAGS_CHECKED " /RTCsu /fsanitize=address") + string(APPEND CMAKE_CXX_FLAGS_CHECKED " /RTCsu /fsanitize=address") + else() + string(APPEND CMAKE_C_FLAGS_CHECKED " /fsanitize=address") + string(APPEND CMAKE_CXX_FLAGS_CHECKED " /fsanitize=address") + endif() ``` **7.6.b** — `xenia-build.py:generate_version_h()` normally runs as part of `xb setup`; CMake-direct flows need it called explicitly. After the existing `find_package(Python3 REQUIRED COMPONENTS Interpreter)` line, add: ```cmake # Generate build-tree version.h via xenia-build.py's generate_version_h() # (normally invoked by `xb premake`/`xb build`; CMake-direct flows need it too). execute_process( COMMAND ${Python3_EXECUTABLE} -c "import importlib.util,sys; \ spec=importlib.util.spec_from_file_location('xb',r'${PROJECT_SOURCE_DIR}/xenia-build.py'); \ m=importlib.util.module_from_spec(spec); spec.loader.exec_module(m); \ m.generate_version_h(r'${CMAKE_BINARY_DIR}')" WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" RESULT_VARIABLE _xenia_version_rc ) if(NOT _xenia_version_rc EQUAL 0) message(WARNING "version.h generation failed (rc=${_xenia_version_rc}); writing stub.") file(WRITE "${CMAKE_BINARY_DIR}/version.h" "#ifndef GENERATED_VERSION_H_\n#define GENERATED_VERSION_H_\n#define XE_BUILD_BRANCH \"unknown\"\n#define XE_BUILD_COMMIT \"unknown\"\n#define XE_BUILD_COMMIT_SHORT \"unknown\"\n#define XE_BUILD_DATE __DATE__\n#endif\n") endif() ``` ### 7.7 `cmake/XeniaHelpers.cmake` — propagate `FXC_PATH` to ninja-spawned subprocesses CMake's `set(ENV{X} …)` persists only for configure-time `execute_process`, not for ninja's runtime. Wrap each shader-compile command in `cmake -E env`. In the function `xe_shader_rules_dxbc`, two edits: **Add this block right after `set(_bytecode_dir …)`:** ```cmake # Propagate FXC_PATH from the configure-time env so ninja-spawned python # subprocesses can find fxc.exe (CMake `set(ENV{...})` doesn't reach build). set(_env_prefix "") if(DEFINED ENV{FXC_PATH}) set(_env_prefix ${CMAKE_COMMAND} -E env "FXC_PATH=$ENV{FXC_PATH}") endif() ``` **Modify the per-shader command emission inside the `foreach`:** ```diff - list(APPEND _commands COMMAND ${Python3_EXECUTABLE} "${_script}" "${src}" "${_bytecode_dir}/${_id}.h") + list(APPEND _commands COMMAND ${_env_prefix} ${Python3_EXECUTABLE} "${_script}" "${src}" "${_bytecode_dir}/${_id}.h") ``` ### 7.8 `tools/build/compile_shader_dxbc.py` — winepath-translate paths When fxc.exe runs under Wine, unix paths like `/home//…` are parsed by fxc as switches (it sees `/h` as the help switch and bails). Convert input/output/include paths to Windows form first via `winepath -w`. After the `is_dxc = "dxc" in os.path.basename(fxc).lower()` line and **before** building `compiler_args`: ```diff - # Start with base command — use wine on non-Windows platforms. if sys.platform != "win32": + def _wineify(p): + try: + out = subprocess.check_output(["winepath", "-w", p], + stderr=subprocess.DEVNULL) + return out.decode("utf-8", "replace").strip() + except (OSError, subprocess.CalledProcessError): + return p + input_path = _wineify(input_path) + output_path = _wineify(output_path) + src_dir = _wineify(src_dir) compiler_args = ["wine", fxc] else: compiler_args = [fxc] ``` > **[CHECKPOINT P7]** All eight patches applied. Sanity-check at the top of each file: > ```bash > cd "$XENIA_CANARY_SRC" > grep -q 'inline const HANDLE kFileHandleInvalid' src/xenia/base/mapped_memory_win.cc && echo "OK 7.1" > grep -q '"../../../assets/icon/icon.ico"' src/xenia/app/main_resources.rc && echo "OK 7.2" > grep -q '!defined(_WIN32) // HAVE_SYS_UIO_H' third_party/snappy/snappy-stubs-public.h && echo "OK 7.3" > grep -q '!defined(_WIN32) /\* was set to #if 1' third_party/zlib-ng/zconf-ng.h && echo "OK 7.4" > grep -q 'NOT MSVC OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang"' third_party/CMakeLists.txt && echo "OK 7.5" > grep -q 'CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC"' CMakeLists.txt && echo "OK 7.6a" > grep -q 'generate_version_h(r' CMakeLists.txt && echo "OK 7.6b" > grep -q '_env_prefix' cmake/XeniaHelpers.cmake && echo "OK 7.7" > grep -q '_wineify' tools/build/compile_shader_dxbc.py && echo "OK 7.8" > ``` > Expected: 9 × `OK`. --- ## Phase 8 — Add the three cross-compile config files These add a new `cross-win-clangcl` preset alongside existing `default`/`vs`/`vs-arm64` presets without disturbing them. ### 8.1 New file: `cmake/toolchains/linux-to-win-msvc.cmake` Create the directory and file. **Copy this verbatim** — reformatting line-continuations inside CMake `set(... "string")` would break `/I` flag parsing. The toolchain is gnarly because it has to bridge multiple incompatibilities at once; comments inline explain. ```bash mkdir -p "$XENIA_CANARY_SRC/cmake/toolchains" ``` ```cmake # cmake/toolchains/linux-to-win-msvc.cmake # Linux host -> Windows MSVC-ABI cross toolchain using clang-cl + lld-link # + xwin-supplied Win10 SDK/CRT. Driven by Ninja Multi-Config. set(CMAKE_SYSTEM_NAME Windows) set(CMAKE_SYSTEM_PROCESSOR x86_64) if(NOT DEFINED XWIN_DIR) if(DEFINED ENV{XWIN_DIR}) set(XWIN_DIR "$ENV{XWIN_DIR}") else() set(XWIN_DIR "$ENV{HOME}/.xwin/splat") endif() endif() set(XWIN_DIR "${XWIN_DIR}" CACHE PATH "xwin splat root (contains crt/ and sdk/)") if(NOT EXISTS "${XWIN_DIR}/crt/include") message(FATAL_ERROR "XWIN_DIR=${XWIN_DIR} missing crt/include - run xwin splat.") endif() set(CMAKE_C_COMPILER clang-cl) set(CMAKE_CXX_COMPILER clang-cl) set(CMAKE_LINKER lld-link) set(CMAKE_RC_COMPILER llvm-rc) set(CMAKE_AR llvm-lib) set(CMAKE_MT llvm-mt) set(CMAKE_C_COMPILER_TARGET x86_64-pc-windows-msvc) set(CMAKE_CXX_COMPILER_TARGET x86_64-pc-windows-msvc) set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) # Force /MD (release CRT) for every config. xenia's CMakeLists.txt does a # `string(REPLACE "/MDd" "/MD" ...)` on CMAKE_CXX_FLAGS_DEBUG, but with # clang-cl, the runtime selection comes from CMAKE_MSVC_RUNTIME_LIBRARY # (which expands to -MDd in dash form), so the substitution misses. # Pinning the policy here avoids the need for non-redistributable debug # CRT DLLs (MSVCP140D.dll etc) at runtime, which xwin doesn't ship. cmake_policy(SET CMP0091 NEW) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL") # xwin's default splat is a flat layout (crt/include, sdk/include/{ucrt,um,shared,...}), # which clang-cl's /winsysroot does NOT understand. Use explicit -imsvc + -libpath: # instead. Re-running `xwin splat --use-winsysroot-style` would also work but # requires re-downloading ~600 MB. # Use SHELL: prefix so CMake doesn't deduplicate repeated -imsvc tokens. add_compile_options( "SHELL:-imsvc \"${XWIN_DIR}/crt/include\"" "SHELL:-imsvc \"${XWIN_DIR}/sdk/include/ucrt\"" "SHELL:-imsvc \"${XWIN_DIR}/sdk/include/um\"" "SHELL:-imsvc \"${XWIN_DIR}/sdk/include/shared\"" "SHELL:-imsvc \"${XWIN_DIR}/sdk/include/winrt\"" ) add_link_options( "/libpath:${XWIN_DIR}/crt/lib/x86_64" "/libpath:${XWIN_DIR}/sdk/lib/ucrt/x86_64" "/libpath:${XWIN_DIR}/sdk/lib/um/x86_64" ) # xwin pulls the latest MSVC STL which now hard-asserts Clang >= 19. Our host # has Clang 18, which works fine in practice — opt out of the version check. # (See yvals_core.h STL1000 in $XWIN_DIR/crt/include.) add_compile_definitions(_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH) # llvm-rc needs SDK headers explicitly + the resource file's own dir so the # RC's relative ICON path (../../../assets/icon/icon.ico) resolves. get_filename_component(_toolchain_dir "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) get_filename_component(_xenia_root "${_toolchain_dir}/../.." ABSOLUTE) set(CMAKE_RC_FLAGS_INIT "/I \"${XWIN_DIR}/sdk/include/um\" /I \"${XWIN_DIR}/sdk/include/shared\" /I \"${_xenia_root}/src/xenia/app\"") # Quiet MSVC-STL false positives and xenia-specific warnings that clang-cl # emits but cl.exe doesn't (treated as errors under /WX). add_compile_options( -Wno-microsoft-include -Wno-unused-command-line-argument -Wno-ignored-pragma-intrinsic -Wno-nonportable-include-path -Wno-pragma-pack -Wno-tautological-pointer-compare -Wno-microsoft-cast -Wno-deprecated-declarations # These are silenced for native Linux Clang in CMakeLists.txt's else() branch, # but the if(MSVC) branch fires under clang-cl and skips them — so re-add. -Wno-switch -Wno-attributes -Wno-deprecated-register -Wno-deprecated-volatile -Wno-deprecated-enum-enum-conversion # cl.exe accepts __pragma(optimize("s",on)); clang-cl only knows the empty # argument form. xenia gates XE_MSVC_OPTIMIZE_SMALL on _MSC_VER so the # rejected pragma still emits from the clang-cl path. Treat as no-op. -Wno-ignored-pragmas # xenia decorates several `virtual` methods with XE_FORCEINLINE; clang-cl # then complains that the inline body isn't visible in includer TUs # (definitions live in command_processor.cc). cl.exe accepts this silently. -Wno-undefined-inline -Wno-sizeof-pointer-memaccess # `'ZM'` (PE magic, stored little-endian as "MZ" in the binary) — cl.exe # accepts the multi-char constant silently; clang-cl errors under /WX. -Wno-multichar ) # _mm_cvtsi64x_si128 is an MSVC-only alias for the standard _mm_cvtsi64_si128. # Used (gated on XE_PLATFORM_WIN32) in xenia/gpu/draw_util.cc. add_compile_definitions(_mm_cvtsi64x_si128=_mm_cvtsi64_si128) # Plumb FXC for tools/build/compile_shader_dxbc.py (wine fxc auto-prepend). # Use real Win10 SDK fxc.exe 10.x (supports SM 5_1, produces vkd3d-proton- # acceptable DXBC). if(DEFINED ENV{FXC_PATH}) set(ENV_FXC "$ENV{FXC_PATH}") else() set(ENV_FXC "$ENV{HOME}/.local/share/xenia-cross/fxc/fxc.exe") endif() set(ENV{FXC_PATH} "${ENV_FXC}") set(CMAKE_FIND_ROOT_PATH "${XWIN_DIR}") set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) ``` ### 8.2 New file: `cmake/toolchains/xwin-case-symlinks.py` xwin auto-creates lowercase aliases for SDK headers shipped with uppercase names (`windows.h → Windows.h`) but doesn't create the reverse aliases. xenia includes some headers using mixed-case spellings (``, ``, …) that match SDK files only on a case-insensitive filesystem. This helper scans xenia's `#include <...>` directives and creates the missing case-symlinks. Idempotent. ```python #!/usr/bin/env python3 """Scan xenia source for #include <...> directives and ensure case-aliases exist in xwin's flat SDK/CRT include dirs. Idempotent — safe to re-run.""" from __future__ import annotations import os, re, sys from pathlib import Path INCLUDE_RE = re.compile(r'^\s*#\s*include\s*<([A-Za-z][A-Za-z0-9_/.\-]*\.h)>', re.MULTILINE) SOURCE_ROOTS = ["src", "third_party/SDL2/src", "third_party/SDL2/include", "third_party/discord-rpc/src", "third_party/fmt", "third_party/imgui"] SOURCE_EXTS = {".h", ".hpp", ".inc", ".c", ".cc", ".cpp", ".cxx"} def collect_includes(repo_root: Path) -> set[str]: seen: set[str] = set() for root in SOURCE_ROOTS: base = repo_root / root if not base.exists(): continue for path in base.rglob("*"): if path.suffix.lower() not in SOURCE_EXTS: continue try: txt = path.read_text(encoding="utf-8", errors="ignore") except OSError: continue for m in INCLUDE_RE.finditer(txt): seen.add(m.group(1)) return seen def fix_dir(target_dir: Path, want: set[str]) -> int: if not target_dir.is_dir(): return 0 idx = {e.name.lower(): e.name for e in target_dir.iterdir()} created = 0 for name in want: if "/" in name: continue actual = idx.get(name.lower()) if actual is None or actual == name: continue link = target_dir / name if link.exists() or link.is_symlink(): continue try: link.symlink_to(actual) created += 1 except OSError: pass return created if __name__ == "__main__": if len(sys.argv) != 3: sys.exit("usage: xwin-case-symlinks.py ") repo = Path(sys.argv[1]).resolve() xwin = Path(sys.argv[2]).resolve() want = collect_includes(repo) total = 0 for d in (xwin/"crt/include", xwin/"sdk/include/ucrt", xwin/"sdk/include/um", xwin/"sdk/include/shared", xwin/"sdk/include/winrt"): total += fix_dir(d, want) print(f"Created {total} case-symlinks across {len(want)} unique includes.") ``` ### 8.3 Edit: `CMakePresets.json` Add **one configure preset** and **three build presets** alongside the existing entries. The full set of `existing` arrays is unchanged; only the additions matter: In the `configurePresets` array, after the last existing entry, add: ```json { "name": "cross-win-clangcl", "displayName": "Cross (Linux→Win MSVC) clang-cl + xwin", "generator": "Ninja Multi-Config", "binaryDir": "${sourceDir}/build-cross", "toolchainFile": "${sourceDir}/cmake/toolchains/linux-to-win-msvc.cmake", "condition": { "type": "notEquals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "cacheVariables": { "XWIN_DIR": "$env{HOME}/.xwin/splat", "FXC_PATH": "$env{HOME}/.local/share/xenia-cross/fxc/fxc.exe" } } ``` In the `buildPresets` array, append: ```json { "name": "cross-debug", "configurePreset": "cross-win-clangcl", "configuration": "Debug" }, { "name": "cross-release", "configurePreset": "cross-win-clangcl", "configuration": "Release" }, { "name": "cross-checked", "configurePreset": "cross-win-clangcl", "configuration": "Checked" } ``` > **[CHECKPOINT P8]** All three config files exist: > ```bash > test -f "$XENIA_CANARY_SRC/cmake/toolchains/linux-to-win-msvc.cmake" && echo "OK toolchain" > test -f "$XENIA_CANARY_SRC/cmake/toolchains/xwin-case-symlinks.py" && echo "OK case-symlinks" > grep -q 'cross-win-clangcl' "$XENIA_CANARY_SRC/CMakePresets.json" && echo "OK presets" > ``` --- ## Phase 9 — Configure cmake ### 9.1 Run the case-symlink helper (one-time) ```bash cd "$XENIA_CANARY_SRC" python3 cmake/toolchains/xwin-case-symlinks.py "$XENIA_CANARY_SRC" "$XWIN_DIR" # Expected: "Created N case-symlinks across M unique includes." # Typical first-run N ≈ 5 (e.g. ObjBase.h, Psapi.h, …). ``` ### 9.2 Configure with the new preset ```bash cmake --preset cross-win-clangcl ``` **Expected tail of output**: ``` -- The C compiler identification is Clang 18.1.3 with MSVC-like command-line -- The CXX compiler identification is Clang 18.1.3 with MSVC-like command-line -- Target architecture: XE_TARGET_AARCH64=FALSE XE_TARGET_X86_64=TRUE (CMAKE_SYSTEM_PROCESSOR=x86_64) -- Found Python3: /usr/bin/python3 (found version "3.x.x") found components: Interpreter -- Configuring done (0.5s) -- Generating done (0.1s) -- Build files have been written to: /build-cross ``` A trailing `CMake Warning: Manually-specified variables were not used by the project: FXC_PATH` is **harmless** — the toolchain reads `FXC_PATH` via `$ENV{...}`, not via CMake cache. > **[CHECKPOINT P9]** Configure step generates `build-cross/`: > ```bash > test -f "$XENIA_CANARY_SRC/build-cross/build-Debug.ninja" && echo "OK configure" > test -f "$XENIA_CANARY_SRC/build-cross/version.h" && echo "OK version.h" > ``` If configure fails: see Appendix A, especially rows tagged **Configure**. --- ## Phase 10 — Build `xenia-app` ```bash cmake --build "$XENIA_CANARY_SRC/build-cross" \ --preset cross-debug \ --target xenia-app \ -j$(nproc) ``` The first build is ~927 ninja targets and takes **8–20 minutes** depending on cores. The heaviest phases are FFmpeg, DirectXShaderCompiler (vendored), glslang. Incremental builds after small source edits typically complete in 10–60 seconds. **Expected final three lines on success**: ``` [925/927] Linking CXX static library obj/Windows/Debug/xenia-ui-d3d12.lib [926/927] Linking CXX static library obj/Windows/Debug/xenia-gpu-d3d12.lib [927/927] Linking CXX executable bin/Windows/Debug/xenia_canary.exe ``` Verify: ```bash ls -lh "$XENIA_CANARY_SRC/build-cross/bin/Windows/Debug/"xenia_canary.{exe,pdb} file "$XENIA_CANARY_SRC/build-cross/bin/Windows/Debug/xenia_canary.exe" # Expected: PE32+ executable (GUI) x86-64, for MS Windows, 9 sections ``` > **[CHECKPOINT P10]** Both `.exe` and `.pdb` are present, exe is PE32+ x86-64. If build fails: see Appendix A, rows tagged **Build**. --- ## Phase 11 — Smoke test under Wine This is the make-or-break verification. The previous phases all could succeed and you still get a runtime crash — **especially if Phase 5 (DLL trio) is not solid**. ### 11.1 The test ```bash cd "$XENIA_CANARY_SRC/build-cross/bin/Windows/Debug" rm -f xenia.log WINEDEBUG=-all wine ./xenia_canary.exe --mute=true "$ISO_PATH" & WINE_PID=$! sleep 30 wineserver -k # kill xenia; this is expected — we just want startup behavior echo "==== Smoke test results ====" echo "errors: $(grep -c '^!>' xenia.log)" echo "fatals: $(grep -c '^x>' xenia.log) # MUST be 0" echo "lines: $(wc -l < xenia.log) # ≥ 1000 = healthy" echo "adapter: $(grep DXGI xenia.log | head -1)" echo "deepest: $(grep XThread::Execute xenia.log | tail -1)" ``` ### 11.2 Pass / fail criteria | Check | Pass | |---|---| | **No `Unhandled Exception in Xenia` GUI dialog popped during the 30 s window** | required | | `^x>` (fatal) line count is **0** | required | | `xenia.log` ≥ 1000 lines | required | | Log shows `i> Setup: Initializing Memory/Exports/Processor/Audio/Graphics/HID/VFS/Kernel` | required | | Log shows `K> XThread::Execute thid N (handle=…, '…', native=…)` for ≥ 5 threads | required | | `DXGI adapter:` line shows your real GPU (not WARP) | required for renderer audits, optional otherwise | | Log shows `i> Skipping draw - pipeline not ready: …` repeating | normal (async pipeline compile) | | Log shows `!> Failed to create graphics pipeline …` rarely | non-fatal (occasional shader miss) | If a GUI dialog appeared or `^x>` count > 0: go to Appendix A, **Runtime** rows. ### 11.3 Inspect the binary in a debugger (optional) Wine ships `winedbg`. For most audit work you don't need it, but if a crash needs traceback: ```bash WINEDEBUG=err+seh,err+exception wine ./xenia_canary.exe "$ISO_PATH" 2>&1 \ | tee /tmp/wine-trace.log grep -E 'Unhandled exception|=>0|=>1|=>2' /tmp/wine-trace.log ``` > **[CHECKPOINT P11]** Smoke test passes: 0 fatals, ≥ 1000 log lines, no error dialog. --- ## Phase 12 — Wire into the xenia-rs audit workflow The Windows debug build's role in the audit workflow: 1. **It is the sole canary oracle going forward.** The Linux-native canary build at `xenia-canary/build/bin/Linux/Debug/xenia_canary` is **deprecated** for audits — it doesn't reach the same boot trajectory. Use the cross-build at `xenia-canary/build-cross/bin/Windows/Debug/xenia_canary.exe`. 2. **All canary launches MUST include `--mute=true`** (project policy established 2026-05-12). `--mute=true` is semantically transparent — XAudio2 still initializes, the same code paths fire, the output is silent. Do **NOT** use `--apu=nop` as a substitute; it switches the audio backend entirely and can shift the boot trajectory in non-obvious ways, invalidating cross-engine comparisons. 3. **Logs default to `/xenia.log`**. For audit runs, point `--log_file=` somewhere predictable, e.g. `$XENIA_RS_ROOT/xenia-rs/audit-runs/audit_XX/canary.log`. ### 12.1 A reusable launcher Place this somewhere on PATH or in `$XENIA_RS_ROOT/`: ```bash #!/bin/bash # launch-canary.sh — invoke the Windows debug build under Wine, audit-style. set -euo pipefail XENIA_CROSS_EXE="${XENIA_CANARY_SRC:-$HOME/RE - Project Sylpheed/xenia-canary}/build-cross/bin/Windows/Debug/xenia_canary.exe" LOG="${LOG:-$(pwd)/canary.log}" WINEDEBUG=-all wine "$XENIA_CROSS_EXE" \ --mute=true \ --log_file="$LOG" \ "$@" ``` Usage: ```bash LOG="$XENIA_RS_ROOT/xenia-rs/audit-runs/audit_71/canary.log" \ ./launch-canary.sh "$ISO_PATH" --audit_71_my_probe=true ``` ### 12.2 Coordinating with xenia-rs In the typical audit workflow: 1. Both engines (canary and xenia-rs) are run against the same ISO. 2. Both engines emit a log file. 3. The audit script diffs the two for divergences (timing, register state, kernel calls, memory ops, …). Where to put canary's log relative to xenia-rs's expectations is project-specific; check the audit-runs/ directory in `xenia-rs` for the established convention. --- ## Adding instrumentation — the audit recipe inline You'll add a new `XELOGI("AUDIT-XX-…")` trace site for almost every audit. The pattern is three steps. **Add instrumentation, do not change runtime behavior** — see "Golden rule" below. ### Step 1: Declare the cvar in `src/xenia/cpu/cpu_flags.h` (or a topically-better-suited `*_flags.h`: `apu_flags.h`, `gpu_flags.h`, `hid_flags.h`, `kernel_flags.h`) ```cpp DECLARE_bool(audit_XX_my_probe); // or: DECLARE_uint64(audit_XX_focus_pc); DECLARE_string(audit_XX_filter); ``` Macros available: `DECLARE_bool / int32 / uint32 / int64 / uint64 / double / string / path`. See `src/xenia/base/cvar.h:349-419` for the full list. ### Step 2: Define the cvar in the matching `*_flags.cc` ```cpp DEFINE_bool(audit_XX_my_probe, false, "Audit-XX: short human-readable description.", "Audit"); // category — keep "Audit" ``` Default **false** (off until explicitly enabled per run). Default `true` only for permanent always-on markers like `AUDIT-DEMO`. ### Step 3: Add the trace site ```cpp #include "xenia/cpu/cpu_flags.h" // if not already included // at the call site: if (cvars::audit_XX_my_probe) { XELOGI("AUDIT-XX-EVENT field1={:#x} field2={} ...", field1, field2); } ``` Use a `AUDIT-XX-` prefix in the log string — makes grepping trivial. ### Step 4: Rebuild and run ```bash cmake --build build-cross --preset cross-debug --target xenia-app -j$(nproc) WINEDEBUG=-all wine ./build-cross/bin/Windows/Debug/xenia_canary.exe \ --mute=true --audit_XX_my_probe=true "$ISO_PATH" & sleep 30 ; wineserver -k grep AUDIT-XX ./build-cross/bin/Windows/Debug/xenia.log ``` ### Log channels reference From `src/xenia/base/logging.h`: | Macro | Prefix | Use for | |---|---|---| | `XELOGI` | `i>` | Info — most audit traces | | `XELOGW` | `w>` | Warnings — non-fatal anomalies | | `XELOGE` | `!>` | Errors — failed paths | | `XELOGD` | `d>` | Debug-level (verbose) | | `XELOGCPU` | `C>` | PPC JIT, register state | | `XELOGAPU` | `A>` | Audio system | | `XELOGGPU` | `G>` | Graphics / D3D12 / Vulkan | | `XELOGKERNEL` | `K>` | Kernel calls, handle/object table | | `XELOGFS` | `F>` | VFS, file I/O | `x>` (fatal) only comes from `xe::FatalError()` — **don't** use it for instrumentation; it `exit()`s the process. ### Golden rule **Permitted edits**: new `XELOG*()` calls, new cvars, new `#if`-gated diagnostic blocks, new `audit_NN_*.cc` files (separate translation units that only observe). **Forbidden edits**: changing control flow, return values, struct layouts, timing, or anything else that influences runtime semantics. The whole point of this build is to *observe* the same code path the official binary runs; silently changing logic invalidates every audit downstream. If you think a behavior change is needed for instrumentation, ask first. --- ## Quick verification: am I in steady state? When picking the project up again later, run this one block to verify all phases still pass: ```bash echo "=== P1 system packages ===" for t in clang-18 lld-18 ninja cmake python3 wine winetricks 7z cargo; do command -v $t > /dev/null && echo "OK $t" || echo "MISSING $t" done echo "=== P2 clang-cl ===" clang-cl --version 2>&1 | grep -q 'Target: x86_64-pc-windows-msvc' && echo "OK clang-cl" echo "=== P3 xwin splat ===" test -d "$HOME/.xwin/splat/crt/include" && echo "OK splat present" echo "=== P4 fxc.exe ===" test -s "$HOME/.local/share/xenia-cross/fxc/fxc.exe" \ && wine "$HOME/.local/share/xenia-cross/fxc/fxc.exe" /? 2>&1 | grep -q 'cs_5_1' \ && echo "OK fxc with SM 5_1" echo "=== P5 Wine DLL trio ===" sz12=$(stat -c %s "$HOME/.wine/drive_c/windows/system32/d3d12core.dll" 2>/dev/null || echo 0) szDX=$(stat -c %s "$HOME/.wine/drive_c/windows/system32/dxgi.dll" 2>/dev/null || echo 0) [ "$sz12" -gt 1000000 ] && echo "OK d3d12core ($sz12 bytes, vkd3d-proton)" \ || echo "FAIL d3d12core ($sz12 bytes — re-run winetricks vkd3d)" [ "$szDX" -gt 2000000 ] && echo "OK dxgi ($szDX bytes, DXVK)" \ || echo "FAIL dxgi ($szDX bytes — re-run winetricks dxvk)" echo "=== P6-8 patched source ===" cd "$XENIA_CANARY_SRC" 2>/dev/null && { grep -q 'inline const HANDLE kFileHandleInvalid' src/xenia/base/mapped_memory_win.cc && echo "OK 7.1" grep -q '"../../../assets/icon/icon.ico"' src/xenia/app/main_resources.rc && echo "OK 7.2" grep -q '!defined(_WIN32)' third_party/snappy/snappy-stubs-public.h && echo "OK 7.3" grep -q '!defined(_WIN32)' third_party/zlib-ng/zconf-ng.h && echo "OK 7.4" grep -q 'NOT MSVC OR CMAKE_CXX_COMPILER_ID' third_party/CMakeLists.txt && echo "OK 7.5" grep -q 'CMAKE_CXX_COMPILER_FRONTEND_VARIANT' CMakeLists.txt && echo "OK 7.6a" grep -q 'generate_version_h' CMakeLists.txt && echo "OK 7.6b" grep -q '_env_prefix' cmake/XeniaHelpers.cmake && echo "OK 7.7" grep -q '_wineify' tools/build/compile_shader_dxbc.py && echo "OK 7.8" test -f cmake/toolchains/linux-to-win-msvc.cmake && echo "OK 8.1" test -f cmake/toolchains/xwin-case-symlinks.py && echo "OK 8.2" grep -q 'cross-win-clangcl' CMakePresets.json && echo "OK 8.3" } echo "=== P10 build artifact ===" test -f "$XENIA_CANARY_SRC/build-cross/bin/Windows/Debug/xenia_canary.exe" \ && echo "OK build artifact" ``` A clean steady state prints ~20 `OK` lines. --- ## Appendix A — Failure-mode matrix (extended) | Phase | Symptom | Probable cause | Fix | |---|---|---|---| | Configure | `fatal error: 'stdio.h' file not found` (or `cstdint`, `algorithm`, etc.) | xwin splat layout incompatible with `/winsysroot` | Use the toolchain in §8.1 verbatim — explicit `-imsvc` with `SHELL:` prefix | | Configure | `Manually-specified variables were not used by the project: FXC_PATH` warning | Harmless — toolchain reads from `$ENV{FXC_PATH}`, not cache | Ignore | | Configure | `Could NOT find Python3` | python3 not on PATH | `apt install python3` | | Configure | `Manually-specified variables were not used by the project: CMAKE_TOOLCHAIN_FILE` | Toolchain file path wrong | Verify §9.2's `cmake --preset` command and that the toolchain exists at the expected path | | Build | `static assertion failed: STL1000: Unexpected compiler version, expected Clang 19.0.0 or newer` | xwin's MSVC STL version-pinned | Verify `add_compile_definitions(_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH)` is in the toolchain (§8.1) | | Build | `fatal error: 'ObjBase.h' file not found` (or other mixed-case Windows header) | xwin missed a case-alias | Re-run `cmake/toolchains/xwin-case-symlinks.py` (§9.1); add the new dir to SOURCE_ROOTS if it's under a non-standard third_party path | | Build | `'fxc.exe' is not recognized` or `Unknown or invalid option '/foo/bar'` | `FXC_PATH` env not propagated, or unix path passed to fxc | Confirm §7.7 (XeniaHelpers.cmake `_env_prefix`) and §7.8 (winepath) patches are applied | | Build | `error X3506: unrecognized compiler target 'cs_5_1'` | Using legacy DX SDK Jun 2010 fxc 9.29 (SM 5.0 max) | Install Win10 SDK fxc per §4 | | Build | `Unknown or invalid option '/Qstrip_priv'` | Same as above — legacy fxc | Same fix | | Build | `'INVALID_HANDLE_VALUE' must be initialized by a constant expression` | clang-cl strict constexpr check | Apply §7.1 patch | | Build | `'cstdint' file not found` inside ``, or `'sys/uio.h' file not found` from snappy | clang-cl pulling Linux-pre-generated POSIX headers | Apply §7.3 + §7.4 patches | | Build | `call to undeclared function '_mm256_broadcastsi128_si256'` etc. | clang-cl needs per-file `-mavx512*` flags | Apply §7.5 patch | | Build | `Error in ICON statement (ID MAINICON): file not found ..\\..\\..\\assets\\icon\\icon.ico` | llvm-rc backslash literalism | Apply §7.2 patch | | Build | missing `version.h` | xenia-build.py wasn't invoked | Apply §7.6b (the `execute_process` block) | | Build | `-Wignored-pragmas: argument 's' to '#pragma optimize'; expected ""` treated as error | clang-cl strictness | Add `-Wno-ignored-pragmas` to toolchain (already in §8.1) | | Build | clang-cl complaints `multi-character character constant` for `'ZM'` etc. | clang-cl errors under `/WX` | `-Wno-multichar` in toolchain (already in §8.1) | | Build | `inline function 'X::Y' is not defined` | `XE_FORCEINLINE virtual` methods | `-Wno-undefined-inline` in toolchain (already in §8.1) | | Build | LNK errors about missing debug-CRT symbols | Inconsistent CRT runtime selection | Verify `CMAKE_MSVC_RUNTIME_LIBRARY = MultiThreadedDLL` (§8.1) | | Runtime | Dialog: `Unhandled Exception in Xenia / 0x578 Invalid window handle / NTSTATUS 0xC0010578` | vkd3d-proton + Wine builtin dxgi mismatch — crash in `vkd3d_instance_get_vk_instance` at swapchain init | Install BOTH `winetricks vkd3d` AND `winetricks dxvk` per §5 | | Runtime | `D3D12RenderTargetCache: Failed to create Resolve Copy Fast 32bpp 1x/2xMSAA …` then `x> Unable to setup command processor internal state` | vkd3d-proton ≤ 2.14 (too old for current xenia) | Re-run `winetricks -q vkd3d` to fetch current release; confirm `d3d12core.dll` is ~6 MB | | Runtime | `!> Unable to allocate code cache generated code storage` (range 0xA0000000-0xAFFFFFFF) | Wine GE / Proton variant has shims loaded in xenia's hardcoded JIT region | Use `/usr/bin/wine` (system Wine 9.0); see Appendix C | | Runtime | `i> DXGI adapter: Radeon RX 6800/6800 XT / 6900 XT (vendor 0x1002 …)` with no `(RADV NAVI21)` or vkd3d-proton suffix | Wine builtin vkd3d is loaded, not vkd3d-proton | Re-check §5 trio install; the DXVK-flavored dxgi must also be in place | | Runtime | Log file empty / no `xenia.log` created | Working dir wrong, or wine prefix not properly set | `cd` into the exe's directory before invoking wine; ensure `WINEPREFIX` is unset or points to a primed prefix | | Runtime | `i> Skipping draw - pipeline not ready: VS …` repeats forever | Normal — pipelines compile asynchronously | Not an error; wait or trigger gameplay input | | Runtime | Random GUI dialog at startup with "Visual C++ runtime not installed" | `CMAKE_MSVC_RUNTIME_LIBRARY` wasn't set to `MultiThreadedDLL`, debug CRT DLLs missing | Verify §8.1 — toolchain must force `/MD`, not `/MDd` | --- ## Appendix B — Why each patch exists (one-liner per) | § | Patch | Root cause | |---|---|---| | 7.1 | `mapped_memory_win.cc` constexpr→const | clang-cl rejects reinterpret_cast in constant expressions; MSVC accepts as extension | | 7.2 | `main_resources.rc` backslash→forward slash | llvm-rc on Linux treats `\` as literal filename char; can't resolve relative path | | 7.3 | `snappy-stubs-public.h` `!_WIN32` gate | Header was pre-generated on a Linux host, hardcoded `HAVE_SYS_UIO_H=1` | | 7.4 | `zconf-ng.h` `!_WIN32` gate | Same — `Z_HAVE_UNISTD_H` baked in on Linux | | 7.5 | `third_party/CMakeLists.txt` add `Clang` to AVX flag gate | cl.exe exposes intrinsics unconditionally; clang-cl needs `-mavx512*` per file | | 7.6a | `CMakeLists.txt` `/RTCsu` gate | `/RTCsu` is MSVC-only (warns per TU under clang-cl) | | 7.6b | `CMakeLists.txt` `version.h` execute_process | `version.h` normally generated by `xb setup`; CMake-direct flow needs explicit call | | 7.7 | `XeniaHelpers.cmake` env propagation | CMake `set(ENV{...})` doesn't survive ninja's subprocess spawn | | 7.8 | `compile_shader_dxbc.py` winepath | fxc.exe parses `/path/to/foo` as a switch (`/p`) instead of a path | All eight are **build-fix patches**. None changes runtime semantics. Clang-cl-specific runtime warnings silenced in toolchain (§8.1's `-Wno-*` list): | Flag | Why | |---|---| | `-Wno-microsoft-include` | xenia uses MSVC-style relative paths in some `#include`s | | `-Wno-unused-command-line-argument` | `/MP` etc. silently accepted by clang-cl | | `-Wno-ignored-pragma-intrinsic` | `#pragma intrinsic(...)` from MSVC headers | | `-Wno-nonportable-include-path` | Case mismatches caught by clang on Linux but accepted by MSVC | | `-Wno-pragma-pack` | `#pragma pack` in third_party deps | | `-Wno-tautological-pointer-compare` | `&name == nullptr` in xenia's threading code | | `-Wno-microsoft-cast` | `static_cast`s clang considers questionable | | `-Wno-deprecated-declarations` | Win32 API surface | | `-Wno-switch` | xenia uses non-exhaustive switches | | `-Wno-attributes` | various MSVC attribute syntax | | `-Wno-deprecated-{register,volatile,enum-enum-conversion}` | C++ standard tightenings | | `-Wno-ignored-pragmas` | `__pragma(optimize("s",on))` — clang-cl only knows the `""` arg | | `-Wno-undefined-inline` | `XE_FORCEINLINE virtual` methods defined in a separate TU | | `-Wno-sizeof-pointer-memaccess` | one real-bug case in `winkey_input_driver.cc` | | `-Wno-multichar` | `'ZM'` PE magic literal in `windowed_app_main_win.cc` | Plus `_mm_cvtsi64x_si128=_mm_cvtsi64_si128` macro (MSVC-only intrinsic alias). --- ## Appendix C — Alternative Wine runtimes | Setup | Works? | Notes | |---|---|---| | **System Wine 9.0 + winetricks vkd3d + winetricks dxvk** | ✅ **recommended** | This recipe | | Lutris with system Wine + Lutris's runtime DLLs | ✅ | Works if `~/.local/share/lutris/runtime/vkd3d/v<3.0+>/` and `runtime/dxvk/v<2.4+>/` are present. Lutris's bundled vkd3d-proton 2.14 is too old for current xenia — manually drop in 3.0+ via the same approach | | Steam Proton | ✅ | Proton ships its own vkd3d-proton + DXVK. Run from inside a Proton-managed prefix (the receiver may have done this previously — Steam-origin DLLs in `/some/path/drive_c/windows/system32/` won't hurt) | | **Wine GE (Glorious Eggroll)** | ❌ **DO NOT USE** | Wine GE's esync/gamemode shims load in `0xA0000000-0xAFFFFFFF`, colliding with xenia's hardcoded JIT generated-code region. Symptom: `!> Unable to allocate code cache generated code storage`. The older May 2025 xenia release tolerated this; current does not. | | Wine Staging | Untested | Should work — has more permissive vkd3d-proton hooks | | Vanilla Wine without vkd3d/dxvk | ❌ | Wine builtin vkd3d ≠ vkd3d-proton; pipeline init fails | --- ## Appendix D — Useful debugging commands ```bash # Inspect which d3d12.dll Wine is using (right now) stat -c '%n is %s bytes' "$WINEPREFIX/drive_c/windows/system32/d3d12core.dll" # 6 MB = vkd3d-proton, 66 KB = Wine builtin # Confirm dxgi.dll is DXVK strings "$WINEPREFIX/drive_c/windows/system32/dxgi.dll" | grep -i 'DXVK' | head -1 # DxvkAdap / DXVK: … = DXVK; nothing = Wine builtin # Capture a Wine SEH backtrace from a crash WINEDEBUG=err+seh,err+exception wine ./xenia_canary.exe "$ISO_PATH" 2>&1 \ | tee /tmp/wine-trace.log grep -E 'Unhandled exception|=>0|=>1|=>2' /tmp/wine-trace.log # Force a specific DLL override for one run (without modifying registry) WINEDLLOVERRIDES="d3d12,d3d12core,dxgi=n,b" wine ./xenia_canary.exe … # n,b = native, then builtin fallback # Inspect xenia.log live as the binary runs tail -f "$XENIA_CANARY_SRC/build-cross/bin/Windows/Debug/xenia.log" & TAIL_PID=$! WINEDEBUG=-all wine ./xenia_canary.exe --mute=true "$ISO_PATH" kill $TAIL_PID 2>/dev/null # Find a specific symbol in the produced binary via the PDB # (requires Wine-runnable WinDbg or x64dbg; alternatively use llvm-pdbutil) llvm-pdbutil dump --symbols build-cross/bin/Windows/Debug/xenia_canary.pdb \ | grep -i AUDIT_DEMO_SETUP_TRACE ``` --- ## Appendix E — Locked version pin table For long-term reproducibility. All versions tested 2026-05-12; substitute current stables if needed. ``` Distro: Linux Mint 22.1 (Ubuntu 24.04 LTS base) clang/lld/llvm-rc: 18.1.3-1ubuntu1 ninja: 1.11.1 cmake: 3.28.3 python3: 3.12.3 wine: 9.0~repack-4build3 winetricks: 20240105 p7zip-full: 16.02 xwin: 0.9.0 vkd3d-proton: 3.0.1 (current upstream stable, via winetricks) DXVK: 2.x (current upstream stable, via winetricks) Win10 SDK fxc: 10.0.28000.1839 (April 2026; any 10.0.22000+ works) xenia-canary commit: 6de80dffe261b368ecefee36c9b2b337335228c0 ``` --- ## Appendix F — Recovery: sanity-rebuild from scratch If something gets weird in `build-cross/` and you want a true clean rebuild without re-running Phases 1-9: ```bash cd "$XENIA_CANARY_SRC" rm -rf build-cross cmake --preset cross-win-clangcl cmake --build build-cross --preset cross-debug --target xenia-app -j$(nproc) ``` This re-runs only Phases 9-10. Phases 1-8 (toolchain, xwin, fxc, Wine, source patches, config files) are persistent. If the source tree itself gets corrupted but Phases 1-5 are still intact: ```bash cd "$XENIA_CANARY_SRC" git stash # save any uncommitted instrumentation git reset --hard 6de80dffe261b368ecefee36c9b2b337335228c0 git submodule update --recursive --force # Re-apply Phase 7 patches and Phase 8 config files # (or `git stash pop` if you had committed them) ``` If you suspect the Wine prefix is what's broken: ```bash mv "$HOME/.wine" "$HOME/.wine.broken.$(date +%s)" wineboot # creates a fresh ~/.wine winetricks -q vkd3d winetricks -q dxvk ``` A fresh prefix loses any winetricks state (corefonts, DLL overrides for other Wine apps you had configured) — only do this if you specifically suspect prefix corruption. --- ## Closing notes If you reached this line and the Phase 11 smoke test passes, you have a working cross-build. Hand it to your audit work; the instrumentation recipe in this doc (above) is identical to the one in [memory/project_xenia_canary_cross_build.md](../.claude/projects/-home-fabi-RE---Project-Sylpheed/memory/project_xenia_canary_cross_build.md) (which is the live quick-reference for routine work). For non-routine investigations (a crash that doesn't match Appendix A, a new patch needed because upstream xenia-canary moved, etc.) — capture both the failing command and the relevant log/trace excerpt, then update Appendix A with the new symptom → fix pair so the next agent doesn't have to rediscover it.