llvm-mingw for windows on linux cross-compile and MSYS2 compatibility

New files:
- build-clangtron-windows.sh - a build script for llvm-mingw
- build-clangtron-windows.bat - a helper for finding and opening a clang msys2 envitronment prompt on windows
- /docs/BUILDING-CLANGTRON-WINDOWS.md - user guide for the new build script

Changes:
- vfs_real.cpp: extend _stat64 guard to __MINGW32__ so _wstat64 gets the  right struct type under llvm-mingw and MSYS2
- src/citron/CMakeLists.txt: restrict -fuse-ld=bfd to GNU compiler only;
  Clang does not accept this flag
- CMakeModules/PGO.cmake: add CITRON_PGO_FLAGS_MANAGED_BY_SCRIPT guard so
  external build scripts can supply PGO flags without conflicting with
  CMake's own injection
- CMakeModules/CopyMinGWDeps.cmake: fall back to Qt6_DIR/../../../plugins
  for plugin deployment when the MSYS2 share path doesn't exist (fixes Qt
  plugin and TLS deployment on Linux cross-compile hosts)
- src/common/logging.cpp, src/hid_core/resources/npad/npad.cpp: add   [[clang::no_profile_instrument_function]] on FmtLogMessageImpl and
  NPad::OnUpdate to suppress ~87M discarded CS-PGO counter hits
- vcpkg.json: add opus and openssl as top-level Windows dependencies
This commit is contained in:
cheezwiz7899
2026-04-18 13:50:13 +10:00
parent 69bc98a161
commit f5920920c9
10 changed files with 4796 additions and 3 deletions
+11
View File
@@ -68,6 +68,17 @@ endforeach()
# windeployqt6 is unreliable when WINDEPLOYQT_EXECUTABLE is not found by cmake,
# so we copy the plugin directories directly from the Qt share tree.
set(QT_PLUGIN_BASE \"\${MINGW_BIN}/../share/qt6/plugins\")
# On a Linux cross-compile host the toolchain bin dir has no Qt share tree.
# Fall back to the path derived from Qt6_DIR (resolved at configure time).
# Qt6_DIR points to <qt_root>/lib/cmake/Qt6, so ../../.. is <qt_root>.
if (NOT EXISTS \\"\\${QT_PLUGIN_BASE}\\")
set(QT_PLUGIN_BASE \\"${Qt6_DIR}/../../../plugins\\")
if (EXISTS \\"\\${QT_PLUGIN_BASE}\\")
message(STATUS \\"Qt plugins: aqt install at \\${QT_PLUGIN_BASE}\\")
else()
set(QT_PLUGIN_BASE \\"\\")
endif()
endif()
if (EXISTS \"\${QT_PLUGIN_BASE}\")
# Core platform / style / imageformat plugins
foreach(plugin_dir platforms styles imageformats)
+16 -1
View File
@@ -21,6 +21,17 @@ endif()
# Create the profile directory if it doesn't exist
file(MAKE_DIRECTORY "${CITRON_PGO_PROFILE_DIR}")
# Allow an external build script (e.g. build-for-clang-windows.sh) to supply its
# own PGO compiler/linker flags directly via CMAKE_C/CXX_FLAGS_RELEASE and
# CMAKE_EXE_LINKER_FLAGS_RELEASE. When this cache variable is ON the GCC/Clang
# flag-injection block below is bypassed so that the script flags and CMake's own
# PGO flags do not collide (which would produce invalid combinations such as
# -fprofile-generate with -fprofile-instr-generate simultaneously).
if(NOT DEFINED CITRON_PGO_FLAGS_MANAGED_BY_SCRIPT)
set(CITRON_PGO_FLAGS_MANAGED_BY_SCRIPT OFF CACHE BOOL
"PGO compiler/linker flags are supplied externally by build-for-clang-windows.sh")
endif()
# Apply /GL globally so ALL compilation units (libraries included) emit MSIL for
# the linker to instrument (GENERATE) or optimize (USE). Without this, only the
# executable target's own sources are visible to PGO -- the hot code in video_core,
@@ -33,7 +44,11 @@ if(MSVC)
endif()
# GCC/Clang: apply profile flags globally so library targets are also instrumented
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if(CITRON_PGO_FLAGS_MANAGED_BY_SCRIPT)
if(CITRON_ENABLE_PGO_GENERATE OR CITRON_ENABLE_PGO_USE)
message(STATUS "PGO: compiler/linker flags are managed externally by build-for-clang-windows.sh")
endif()
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if(CITRON_ENABLE_PGO_GENERATE)
add_compile_options(-fprofile-generate -fprofile-dir=${CITRON_PGO_PROFILE_DIR})
add_link_options(-fprofile-generate -fprofile-dir=${CITRON_PGO_PROFILE_DIR})
+85
View File
@@ -0,0 +1,85 @@
@echo off
REM SPDX-FileCopyrightText: 2026 citron Emulator Project
REM SPDX-License-Identifier: GPL-3.0-or-later
REM
REM build-clangtron-windows.bat
REM Opens an MSYS2 CLANG64 shell in the repository directory, prints the
REM build-clangtron-windows.sh help text, and leaves the prompt open for you
REM to run build stages manually.
REM
REM This is NOT a one-click build. See docs/BUILDING-CLANG-MINGW.md for
REM the full pipeline.
setlocal enabledelayedexpansion
REM ---------- locate MSYS2 ----------
set "MSYS2_PATH="
for %%P in (
"C:\msys64"
"C:\msys2"
"%USERPROFILE%\msys64"
"%USERPROFILE%\msys2"
"D:\msys64"
"D:\msys2"
) do (
if exist "%%~P\usr\bin\bash.exe" (
set "MSYS2_PATH=%%~P"
goto :found_msys2
)
)
REM Try to locate via PATH
where bash.exe >nul 2>&1
if %ERRORLEVEL% equ 0 (
for /f "delims=" %%I in ('where bash.exe') do (
set "BASH_LOC=%%~dpI"
if exist "!BASH_LOC!..\..\..\usr\bin\bash.exe" (
for %%J in ("!BASH_LOC!..\..\..") do set "MSYS2_PATH=%%~fJ"
goto :found_msys2
)
)
)
echo.
echo ERROR: Could not find an MSYS2 installation.
echo.
echo Install MSYS2 from https://www.msys2.org/ then run setup:
echo.
echo build-clangtron-windows.bat
echo bash build-clangtron-windows.sh setup
echo.
echo Or set MSYS2_PATH before running:
echo set MSYS2_PATH=C:\msys64
echo build-clangtron-windows.bat
echo.
pause
exit /b 1
:found_msys2
echo Found MSYS2 at: %MSYS2_PATH%
REM ---------- resolve script directory as MSYS2 path ----------
set "SCRIPT_DIR=%~dp0"
if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
REM C:\foo\bar -> /c/foo/bar
set "MSYS_SOURCE=%SCRIPT_DIR:\=/%"
set "DRIVE=%MSYS_SOURCE:~1,1%"
set "MSYS_SOURCE=/%DRIVE%%MSYS_SOURCE:~2%"
REM Lowercase the drive letter (MSYS2 paths are lowercase)
for %%L in (a b c d e f g h i j k l m n o p q r s t u v w x y z) do (
set "MSYS_SOURCE=!MSYS_SOURCE:/%%L=/%%L/!"
)
REM ---------- open CLANG64 shell, print help, leave prompt open ----------
echo.
echo Opening MSYS2 CLANG64 shell.
echo The build script help will be printed above the prompt.
echo Run stages manually — see docs/BUILDING-CLANG-MINGW.md for guidance.
echo.
"%MSYS2_PATH%\usr\bin\env.exe" MSYSTEM=CLANG64 ^
"%MSYS2_PATH%\usr\bin\bash.exe" --login -c ^
"cd '%MSYS_SOURCE%' && bash build-clangtron-windows.sh --help; exec bash"
File diff suppressed because it is too large Load Diff
+273
View File
@@ -0,0 +1,273 @@
# Building Citron Neo for Windows with Clang/LLVM
This document covers building a Windows PE (`citron.exe`) using `build-clangtron-windows.sh` — a multi-stage pipeline that combines Clang cross-compilation with Profile-Guided Optimization (PGO) and Link-Time Optimization (LTO) for a fully optimized release binary.
---
## Quick Start (Recommended: IR PGO + Full LTO)
```bash
# 1. First time only: install toolchain and dependencies
./build-clangtron-windows.sh setup
# 2. Clone citron Neo and its submodules if you haven't already
git clone --recursive https://github.com/citron-neo/emulator.git
cd emulator
# 3. Build the PGO instrumentation binary
./build-clangtron-windows.sh generate --pgo-type ir --lto full
# 4. Copy build/generate/bin/ to a Windows machine and run citron.exe
# Play games for 15-30 minutes, then exit cleanly (File > Exit or Ctrl+Q)
# A file named default-<pid>.profraw will appear next to citron.exe
# Copy that file back to build/pgo-profiles/ on the build machine
# 5. Build the optimized binary
./build-clangtron-windows.sh use --pgo-type ir --lto full
# Output: build/use/bin/citron.exe
```
> **Windows users:** double-click `build-clangtron-windows.bat` to open an MSYS2 CLANG64 shell with help pre-printed, then run stages manually.
---
## Requirements
### Linux (cross-compile, full pipeline)
The `setup` stage installs all of these automatically:
| Tool | Purpose |
|---|---|
| `clang-21` / `clang++-21` | Host compiler for PGO merge and Linux ELF |
| `lld-21` | Linker for LTO |
| `llvm-profdata-21` | Merges `.profraw``.profdata` |
| `llvm-bolt-21` | ELF binary optimization (BOLT stage) |
| `perf` | Linux branch-stack profiling (Propeller stage) |
| `cmake` + `ninja` | Build system |
| `llvm-mingw` | Downloaded automatically: Clang + libc++ + compiler-rt for Windows x86_64 |
| `aqt` (Python) | Downloads Qt for the Windows target |
### Windows (MSYS2 CLANG64, generate/use stages only)
Install [MSYS2](https://www.msys2.org/) and run `setup` from the CLANG64 terminal:
```bash
./build-clangtron-windows.sh setup
```
`pacman` handles the toolchain. The ELF, BOLT, and Propeller stages require a Linux host and will exit with an error on Windows until COFF/PE BBAddrMap support lands in LLVM (see [RFC](https://discourse.llvm.org/t/rfc-extend-bbaddrmap-support-to-coff-windows/90232)).
---
## Build Strategy
### Why Clang and not MSVC or GCC?
Citron Neo's Windows builds use Clang via [llvm-mingw](https://github.com/mstorsjo/llvm-mingw) — a self-contained toolchain that provides Clang, LLD, libc++, and compiler-rt for MinGW-w64 targets. Using Clang uniformly across both the build host and target means:
- **IR PGO and CS-IRPGO are available.** MSVC PGO operates at the linker level and cannot instrument the same code paths.
- **Full LTO works end-to-end.** LLD handles both the cross-link and the LTO backend in a single pass.
- **No GCC runtime dependencies.** The binary ships `libc++.dll` and `libunwind.dll` instead of `libstdc++-6.dll` / `libgcc_s_seh-1.dll`, avoiding the `libwinpthread-1.dll` TLS race that GCC FFmpeg DLLs trigger.
### Dependency handling
**System dependencies** (Boost, zlib, zstd, fmt, etc.) are built from source by vcpkg using the same llvm-mingw toolchain, with a custom triplet (`x64-mingw-llvm-static`) that forces static linkage against libc++ instead of libstdc++.
**Qt** is downloaded via `aqt` directly into the build tree. The Windows target variant (`win64_llvm_mingw`) is a pre-built Qt that matches the llvm-mingw ABI. The build script fetches `qtmultimedia` and `qtimageformats` alongside the base package because the base aqt install omits them.
**FFmpeg** is re-built from source with llvm-mingw after cmake downloads the GCC variant, to eliminate the `libwinpthread-1.dll` dependency that the upstream GCC FFmpeg DLLs carry.
**Precompiled headers** are disabled globally. IR PGO instruments the PCH itself, causing flag-set mismatches between stages that silently invalidate it. Unity builds already batch translation units more aggressively than PCH does, so there is no compile-time penalty.
---
## Stages
```
setup → generate → [Windows profiling session] → use → [optional: csgenerate → use]
→ [experimental: bolt / propeller]
```
### `setup`
Run once per machine. On Linux: installs apt packages, downloads and builds llvm-bolt from source (not in the LLVM apt repository for current versions), downloads llvm-mingw, and runs shared setup steps. On MSYS2: installs packages via pacman.
```bash
./build-clangtron-windows.sh setup
```
### `generate` — Stage 1: PGO instrumentation build
Compiles citron.exe with PGO counter instrumentation embedded. The binary runs at reduced speed but writes a `.profraw` profile file on clean exit. This file captures which code paths are hot at runtime.
```bash
./build-clangtron-windows.sh generate --pgo-type ir --lto full
# Output: build/generate/bin/citron.exe
```
After building, copy the entire `build/generate/bin/` directory to a Windows machine, run `citron.exe`, play for 1530 minutes covering a representative mix of games and menus, then exit cleanly. Copy the resulting `default-<pid>.profraw` file (or directory, for IR PGO) back to `build/pgo-profiles/`.
**Important:** exit citron cleanly (File → Exit or Ctrl+Q). Killing the process prevents the profraw from being written.
### `use` — Stage 2: Optimized build
Merges any `.profraw` files in `build/pgo-profiles/` into `default.profdata`, then rebuilds citron.exe with `-fprofile-use` applied at both compile and link time. Full LTO re-runs the optimizer across all bitcode modules at link time with the profile data available, maximizing inlining and branch prediction on hot paths.
```bash
./build-clangtron-windows.sh use --pgo-type ir --lto full
# Output: build/use/bin/citron.exe
```
The `--pgo-type` and `--lto` flags **must match** between `generate` and `use` when using IR PGO. The IR-level profile is keyed to the specific optimized IR produced at generate time; a flag mismatch restructures the IR and causes the entire profile to hash-mismatch and be discarded.
### `csgenerate` — Stage 1b: Context-Sensitive PGO (optional, IR PGO only)
CS-IRPGO adds a second instrumentation layer on top of a binary that is already optimized with the stage 1 profile. It captures per-call-site counters rather than per-function counters, giving the compiler separate profiles for each inlined copy of a hot function.
```bash
# Requires: default.profdata already exists (produced by running `use` after stage 1)
./build-clangtron-windows.sh csgenerate --pgo-type ir --lto full
# Output: build/cs-generate/bin/citron.exe
```
Run this binary on Windows for another 1530 minutes using the same gameplay as session 1. Copy the resulting `cs-default-<pid>.profraw` files to `build/pgo-profiles/cs/`, then re-run `use`. The `use` stage auto-detects the `cs/` directory and merges both profiles automatically.
**Critical invariant:** `csgenerate` must always use `default.profdata` (stage 1 only) as its `-fprofile-use` input — never `merged.profdata`. Using merged data changes the IR that the CS counters are keyed to, making the resulting profile unloadable in the final `use` build.
### No-PGO baseline build
To produce an unoptimized release binary (useful for comparison or debugging):
```bash
./build-clangtron-windows.sh use --pgo-type none --lto full
# Output: build/use-nopgo/bin/citron.exe
# Fully unoptimized (no PGO, no LTO):
./build-clangtron-windows.sh use --pgo-type none --lto none
```
---
## LTO Modes
| Mode | Flag | Build time | Runtime perf | Notes |
|---|---|---|---|---|
| `full` | `-flto` | Slowest | Best | Default. Whole-program IR merged at link time. |
| `thin` | `-flto=thin` | Faster | Good | Parallel ThinLTO. Slightly weaker inlining. |
| `none` | — | Fastest | Baseline | Not recommended for release. |
`--lite-lto` is an alias for `--lto thin`. `--no-lto` is an alias for `--lto none`.
---
## PGO Modes
| Mode | Flag set | Notes |
|---|---|---|
| `ir` | `-fprofile-generate` / `-fprofile-use` | Default. Counters at optimized-IR level. Most accurate for inlining. CS-IRPGO available. LTO mode must match between stages. |
| `fe` | `-fprofile-instr-generate` / `-fprofile-instr-use` | Frontend PGO. Counters before optimization passes. More robust to flag changes between stages. CS-IRPGO not available. |
| `none` | — | No PGO. Used for baseline or `build-elf` without profile data. |
---
## Additional Options
| Option | Default | Description |
|---|---|---|
| `--source DIR` | current directory | Path to the citron Neo source tree |
| `--build DIR` | `./build` | Build root directory |
| `--jobs N` | `nproc` | Parallel compile jobs |
| `--unity` | off | Enable unity builds (~3090% faster compilation, no runtime effect) |
| `--clang-version N` | `21` | Host Clang version (Linux only) |
| `--llvm-mingw-version VER` | `20260224` | llvm-mingw release tag to download (Linux only) |
---
## Experimental: BOLT and Propeller (Linux only)
> **These stages are experimental, require a Linux host, and currently provide little to no measurable performance gain for typical usage. They are documented here for completeness.**
Both stages use a native Linux ELF binary as a profiling proxy for the Windows PE. Because BOLT and Propeller operate on ELF binaries and LLVM does not yet support COFF/PE BBAddrMap (tracking: [RFC](https://discourse.llvm.org/t/rfc-extend-bbaddrmap-support-to-coff-windows/90232)), the layout information is extracted from the ELF and applied to the PE via the linker's `/order:@` flag. This gives function-level reordering but not basic-block layout, and because Full LTO inlines many hot functions into their callers, agreement rates between the ELF profile and the PE are typically 3864% — meaning a significant portion of the ordering guidance is already lost before it reaches the PE.
### `build-elf` — Stage 2b: Linux ELF for profiling
```bash
./build-clangtron-windows.sh build-elf --pgo-type ir --lto full
# Output: build/use-elf/bin/citron (Linux ELF, not a Windows binary)
```
This stage is invoked automatically by `bolt` and `propeller` if the ELF is not already present.
### `bolt` — Stage 3A: BOLT function-order optimization
Instruments the Linux ELF with BOLT, profiles it natively, extracts the hot function order from the optimized ELF, and re-links the Windows PE with `/order:@` to place hot functions at the start of `.text`.
```bash
./build-clangtron-windows.sh bolt --pgo-type ir --lto full
# Pauses mid-stage: run the instrumented ELF, play for 15-30 min, press Enter
# Output: build/bolt/bin/citron.exe
```
Requires `llvm-bolt`, built from source by `setup` since it is not in the LLVM apt repository for current versions.
### `propeller` — Stage 3B: Propeller BB+function layout
Collects a branch-stack profile of the Linux ELF via `perf record -b`, converts it to a Propeller layout profile using `generate_propeller_profiles`, and rebuilds the Windows PE with the function ordering applied. Basic-block layout is generated but cannot currently be applied to the PE (ELF-only flag), so only function ordering benefits the final binary.
```bash
./build-clangtron-windows.sh propeller --pgo-type ir --lto full
# Pauses mid-stage: run citron under perf, play for 15-30 min, press Enter
# Output: build/propeller/bin/citron.exe
```
Requires hardware branch-stack support (`perf -b`): AMD Zen 4+ with kernel 6.1+, or Intel with LBR. The setup stage installs `generate_propeller_profiles` from [google/llvm-propeller](https://github.com/google/llvm-propeller).
---
## Build Output Structure
```
build/
├── generate/bin/citron.exe Stage 1 instrumented binary (run on Windows for profiling)
├── cs-generate/bin/citron.exe Stage 1b CS-instrumented binary
├── use/bin/citron.exe Stage 2 optimized binary (main output)
├── use-nopgo/bin/citron.exe No-PGO baseline binary
├── bolt/bin/citron.exe BOLT-relinked binary (experimental)
├── propeller/bin/citron.exe Propeller-relinked binary (experimental)
├── pgo-profiles/
│ ├── default-<pid>.profraw Copy profraw files here from Windows
│ ├── default.profdata Merged stage 1 profile (auto-generated)
│ ├── merged.profdata Merged stage1 + CS profile (auto-generated)
│ └── cs/ Copy CS profraw files here
├── llvm-mingw/ Downloaded llvm-mingw toolchain (Linux)
└── generate/externals/qt/ Downloaded Qt for Windows target
```
---
## Troubleshooting
**No `.profraw` file after running the generate binary**
The profile is only written on a clean exit. Exit via File → Exit or Ctrl+Q. Do not kill the process. The generate binary also performs an instrumentation check immediately after build and will warn if profile runtime symbols were stripped.
**`LTO mismatch` error when running `use`**
The `--lto` value must match between `generate` and `use` when using IR PGO. Re-run `generate` with the matching `--lto` flag, or re-run `use` with the flag that `generate` used.
**`default.profdata not found` for csgenerate**
Run `use` first after collecting stage 1 profraw files. The `use` stage merges the profraw files and produces `default.profdata`, which `csgenerate` requires.
**Qt deploy warning on Linux: "Qt plugin base not found"**
This warning comes from `CopyMinGWDeps.cmake` during the cmake build step. The script already handles plugin deployment independently via `deploy_runtime_dlls` after the build completes. Qt plugins including TLS backends are copied from the aqt installation into `bin/` at that point. The warning is cosmetic on a Linux cross-compile host.
**MSYS2: `pacman: command not found`**
Launch the script from the **MSYS2 CLANG64** terminal, not a standard Windows Command Prompt or PowerShell. Use `build-clangtron-windows.bat` to open the correct environment automatically.
**Build fails with `-fuse-ld=bfd` not found**
This flag is GCC-only and should not reach Clang builds. Ensure you are using the upstream `src/citron/CMakeLists.txt` with the fix applied (see commit notes), or update from the repository.
+4 -1
View File
@@ -487,8 +487,11 @@ elseif(WIN32)
endif()
# Robust static linking and linker selection under GCC LTO
# -fuse-ld=bfd is a GCC-only flag — Clang (llvm-mingw / MSYS2 clang64) uses
# lld by default and does not accept -fuse-ld=bfd. The generator expression
# restricts the flag to GNU builds so MinGW/Clang cross-compile builds are unaffected.
if (NOT MSVC AND NOT APPLE)
target_link_options(citron PRIVATE -fuse-ld=bfd)
target_link_options(citron PRIVATE "$<$<CXX_COMPILER_ID:GNU>:-fuse-ld=bfd>")
if (MINGW AND CITRON_ENABLE_LTO)
target_link_options(citron PRIVATE "-Wl,--allow-multiple-definition")
endif()
+3
View File
@@ -435,6 +435,9 @@ void SetColorConsoleBackendEnabled(bool enabled) noexcept {
if (logging_instance)
logging_instance->color_console_backend.enabled = enabled;
}
#ifdef __clang__
[[clang::no_profile_instrument_function]]
#endif
void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename,
unsigned int line_num, const char* function, fmt::string_view format,
const fmt::format_args& args) {
+1 -1
View File
@@ -16,7 +16,7 @@
// For FileTimeStampRaw
#include <sys/stat.h>
#ifdef _MSC_VER
#if defined(_MSC_VER) || defined(__MINGW32__)
#define stat _stat64
#endif
+3
View File
@@ -463,6 +463,9 @@ void NPad::RequestPadStateUpdate(u64 aruid, Core::HID::NpadIdType npad_id) {
}
}
#ifdef __clang__
[[clang::no_profile_instrument_function]]
#endif
void NPad::OnUpdate(const Core::Timing::CoreTiming& core_timing) {
if (ref_counter == 0) {
return;
+8
View File
@@ -29,6 +29,14 @@
"lz4",
"nlohmann-json",
"openal-soft",
{
"name": "openssl",
"platform": "windows"
},
{
"name": "opus",
"platform": "windows"
},
"zlib",
"zstd"
],