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

58 KiB
Raw Blame History

⚠️ 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, 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.

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 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

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

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 --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.

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:

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_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

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.bxenia-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

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 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:

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

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:

  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/:

#!/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:

  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)

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.