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>
58 KiB
⚠️ The audience line below predates a decision. It assumes the
xenia-rsRust 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-rsRust project cloned but noxenia-canarycheckout 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, 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:
- Canary must be debug-mode so XELOG channels (
i> Setup: ...,K> XThread::Execute ..., etc.) emit verbosely. - Canary must run on the same host as
xenia-rsto remove machine-state confounds. - Canary must accept custom audit instrumentation (new
cvars::audit_XX_*flags andXELOGI("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.
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:ls -d "$XENIA_RS_ROOT/xenia-rs" && test -r "$ISO_PATH" && echo OKExpected 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
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:
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:
cargo --version # ≥ 1.70
rustc --version # ≥ 1.70
[CHECKPOINT P1] All required tools resolve and report a sane version:
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.
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:
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:
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 --versionreportsTarget: 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.
cargo install --locked xwin
xwin --accept-license --arch x86_64 splat \
--include-debug-libs \
--output "$XWIN_DIR"
Notes & caveats:
--include-debug-libsis mandatory — even though we'll force the release CRT for runtime (Phase 8.3), headers reference_ITERATOR_DEBUG_LEVELlibrary 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-imsvcflags 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 shipsobjbase.honly).
[CHECKPOINT P3] Splat directory is populated:
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)
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.
# 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.
# 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
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: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
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
# 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_47alone: that's the runtime DLL, irrelevant here.winetricks dxsdk_jun2010alone: that's the SM 5.0 legacy fxc, fails on current shaders (see Phase 4).- Suppressing
WINEDEBUG=-allwhen 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
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:
git checkout 6de80dffe261b368ecefee36c9b2b337335228c0
git submodule update --init --recursive
[CHECKPOINT P6]
xenia-canary/exists with submodules populated: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.
- // 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.
- 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:
- #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
- #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.
- #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.
- 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:
- 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:
# 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 …):
# 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:
- 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:
- # 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:
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.
mkdir -p "$XENIA_CANARY_SRC/cmake/toolchains"
# 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.
#!/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:
{
"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:
{ "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:
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)
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
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/: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
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:
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
.exeand.pdbare 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
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:
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:
-
It is the sole canary oracle going forward. The Linux-native canary build at
xenia-canary/build/bin/Linux/Debug/xenia_canaryis deprecated for audits — it doesn't reach the same boot trajectory. Use the cross-build atxenia-canary/build-cross/bin/Windows/Debug/xenia_canary.exe. -
All canary launches MUST include
--mute=true(project policy established 2026-05-12).--mute=trueis semantically transparent — XAudio2 still initializes, the same code paths fire, the output is silent. Do NOT use--apu=nopas a substitute; it switches the audio backend entirely and can shift the boot trajectory in non-obvious ways, invalidating cross-engine comparisons. -
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/:
#!/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:
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:
- Both engines (canary and xenia-rs) are run against the same ISO.
- Both engines emit a log file.
- 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)
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
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
#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
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:
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 #includes |
-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_casts 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
# 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:
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:
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:
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 (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.