Files
Xenia-Canary/docs/CROSS_BUILD_SETUP.md
Sylpheed RE agent 99cc726626 docs: adopt the full cross-build guide (the tracked one was a stub)
The project root carried a 59 KB version of this guide, untracked, on one
disk. This repository carried 6.8 KB of it. Consolidation Phase 6 in the
Sylpheed repo.

Its audience line predates the decision to retire xenia-rs, so a note at the
top says to read 'audit oracle for the Rust port' as 'instrumented build for
RE'. The instructions themselves are unaffected -- clang-cl, xwin, lld-link
and Wine do not care why you want the binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 21:18:30 +02:00

1290 lines
58 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- Adopted 2026-09-13 from an untracked copy in the project root, which was
nine times longer than the stub this repository carried. See
docs/agents/CONSOLIDATION.md in the Sylpheed repo. -->
> ⚠️ **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. `<ObjBase.h>` 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 <https://learn.microsoft.com/windows/apps/windows-sdk/downloads>. 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 <sys/uio.h>
#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/<you>/…` 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 (`<ObjBase.h>`, `<Psapi.h>`, …) 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 <xenia-canary-root> <xwin-splat-dir>")
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: <XENIA_CANARY_SRC>/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 **820 minutes** depending on cores. The heaviest phases are FFmpeg, DirectXShaderCompiler (vendored), glslang. Incremental builds after small source edits typically complete in 1060 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 `<exe_dir>/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-<TAG>` 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 `<filesystem>`, 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.