From cbaa0dbad9384efa218a5856201d4202992ebc1a Mon Sep 17 00:00:00 2001 From: Vincent Hengel Date: Tue, 29 Sep 2026 20:02:02 +0200 Subject: [PATCH] Windows DLL build: base, ultragui and SDL3 as DLLs, mingw dev shell --- CMakeLists.txt | 27 +++++++++++++++-- cmake/toolchain-mingw-w64.cmake | 53 +++++++++++++++++++++------------ docs/STRUCTURE.md | 14 +++++++-- flake.nix | 25 ++++++++++++++++ rxe/net/http/CMakeLists.txt | 3 +- rxe/render/path_motion_test.cc | 8 ++--- rxe/ui/ugui/CMakeLists.txt | 5 ++++ third_party/equilibrium | 2 +- 8 files changed, 106 insertions(+), 31 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 493339a..c06ed19 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,14 @@ if(RX_SHARED) "-DRX_INSTALL=OFF (it defaults off for add_subdirectory consumers).") endif() set(RX_LIB_TYPE SHARED) + if(WIN32 AND RX_MIMALLOC) + # new_override.cc replaces operator new in the executable only. A DLL cannot + # see an executable's replacement on Windows, so a block allocated in a + # module would be freed through mimalloc. Component builds are for + # iteration, not benchmarks. + message(STATUS "RX_SHARED on Windows: mimalloc off") + set(RX_MIMALLOC OFF CACHE BOOL "" FORCE) + endif() # Third-party static archives (equilibrium::base, volk, VMA, Jolt, kinema, NRD, # FSR3) get linked into rx shared objects, so their objects must be PIC. set(CMAKE_POSITION_INDEPENDENT_CODE ON) @@ -221,9 +229,16 @@ if(NOT SDL3_FOUND AND TARGET SDL3::SDL3) endif() if(NOT SDL3_FOUND AND RX_FETCH_SDL3) set(SDL_TEST_LIBRARY OFF) - # Static keeps the artifacts self contained, no SDL3.dll shipping. - set(SDL_SHARED OFF) - set(SDL_STATIC ON) + if(RX_SHARED AND WIN32) + # Every DLL that links a static SDL gets its own copy, with its own event + # queue and window list; one SDL3.dll keeps them one. + set(SDL_SHARED ON) + set(SDL_STATIC OFF) + else() + # Static keeps the artifacts self contained, no SDL3.dll shipping. + set(SDL_SHARED OFF) + set(SDL_STATIC ON) + endif() FetchContent_Declare(SDL3 GIT_REPOSITORY https://github.com/libsdl-org/SDL.git GIT_TAG release-3.2.16 @@ -259,6 +274,12 @@ if(NOT TARGET equilibrium::base) # std::memory_order arguments) does not keep it from its cache. set(EQ_USE_STD_ATOMIC OFF CACHE BOOL "" FORCE) set(EQ_BUILD_TESTS OFF CACHE BOOL "" FORCE) + # PE gives each DLL its own copy of a static library's globals (the ELF build + # shares them through default visibility), so the Windows component build + # needs base as one DLL for one Option/Feature registry, log and allocator. + if(RX_SHARED AND WIN32) + set(EQ_BASE_SHARED ON CACHE BOOL "" FORCE) + endif() add_subdirectory(third_party/equilibrium equilibrium EXCLUDE_FROM_ALL) # base's std::formatter specializations exist for consumers that format base # strings with ; nothing here does, and they drag into every diff --git a/cmake/toolchain-mingw-w64.cmake b/cmake/toolchain-mingw-w64.cmake index ba028f1..3995703 100644 --- a/cmake/toolchain-mingw-w64.cmake +++ b/cmake/toolchain-mingw-w64.cmake @@ -1,24 +1,26 @@ -# Cross toolchain: x86_64-w64-mingw32 via the nixpkgs cross gcc wrapper, used -# to build the d3d12 RHI acceptance tests as Windows PEs and run them under -# Wine (box64-emulated on aarch64 hosts). Configure inside `nix develop` so -# dxc/glslang stay the host tools: -# cmake -B build/mingw -G Ninja \ +# Cross toolchain: x86_64-w64-mingw32 (ucrt) via the nixpkgs cross gcc +# wrapper. Builds rx as Windows PEs (static, or DLLs with -DRX_SHARED=ON) that +# run under Wine. Configure inside `nix develop .#mingw`, which provides the +# compiler, the Windows libraries and the host shader tools: +# cmake -B build/mingw -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ # -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-mingw-w64.cmake \ -# -DRX_MODULES=ui/events\;ui/window\;asset\;render -DRX_RHI_VULKAN=OFF -DRX_RHI_D3D12=ON \ -# -DRX_BUILD_TESTS=ON -DRX_BUILD_RUNTIME=OFF -# The MINGW_CC/MINGW_MCF paths can be overridden with -DRX_MINGW_CC=... / -# -DRX_MINGW_MCF=... when the store paths differ. +# -DRX_NRD=OFF -DRX_DLSS=OFF -DRX_FSR3=OFF -DRX_MIMALLOC=OFF -DRX_FETCH_SDL3=ON \ +# -DRX_RXPACK=$PWD/build/linux/apps/rxpack/rxpack +# RX_RXPACK is a host rxpack that packs the engine archives (Data/*.rxp), which +# the cross-built one cannot run to do. Add -DRX_SHARED=ON for DLLs. +# RX_MINGW_CC, RX_MINGW_MCF, RX_MINGW_MCF_DEV and RX_MINGW_ROOTS come from that +# shell's environment; -D overrides them. set(CMAKE_SYSTEM_NAME Windows) set(CMAKE_SYSTEM_PROCESSOR x86_64) -if(NOT DEFINED RX_MINGW_CC) - set(RX_MINGW_CC "/nix/store/ha64priyfaxkdg7ylvwz3pc0p5vw9nfz-x86_64-w64-mingw32-gcc-wrapper-15.2.0") -endif() -if(NOT DEFINED RX_MINGW_MCF) - set(RX_MINGW_MCF "/nix/store/y8iv45r55ryaxgwdxjkbbprmcd8jyl91-mcfgthread-x86_64-w64-mingw32-2.4.1") -endif() -if(NOT DEFINED RX_MINGW_MCF_DEV) - set(RX_MINGW_MCF_DEV "/nix/store/75dflqgi4q9saldsdzpwx56wf6cnpfhv-mcfgthread-x86_64-w64-mingw32-2.4.1-dev") +foreach(_var RX_MINGW_CC RX_MINGW_MCF RX_MINGW_MCF_DEV RX_MINGW_ROOTS) + if(NOT DEFINED ${_var} AND DEFINED ENV{${_var}}) + set(${_var} "$ENV{${_var}}") + endif() +endforeach() +if(NOT RX_MINGW_CC OR NOT RX_MINGW_MCF OR NOT RX_MINGW_MCF_DEV) + message(FATAL_ERROR "toolchain-mingw-w64: RX_MINGW_CC/MCF/MCF_DEV unset; " + "configure inside `nix develop .#mingw`") endif() # libstdc++'s gthr model on this toolchain is mcfgthread; its headers live in # the dev output. SHELL: keeps each "-isystem " pair intact through @@ -29,16 +31,29 @@ add_compile_options("SHELL:-isystem ${RX_MINGW_MCF_DEV}/include") add_compile_options("SHELL:-isystem ${CMAKE_CURRENT_LIST_DIR}/mingw-shims") # gcc has no MSVC __int64 builtin (equilibrium's minwin.h relies on it). add_compile_options("-D__int64=long long") +# COFF caps an object at 32k sections; unity sources like harfbuzz.cc exceed it. +add_compile_options(-Wa,-mbig-obj) set(CMAKE_C_COMPILER "${RX_MINGW_CC}/bin/x86_64-w64-mingw32-gcc") set(CMAKE_CXX_COMPILER "${RX_MINGW_CC}/bin/x86_64-w64-mingw32-g++") set(CMAKE_RC_COMPILER "${RX_MINGW_CC}/bin/x86_64-w64-mingw32-windres" CACHE FILEPATH "" ) -# mcfgthread ships separately from the gcc wrapper in nixpkgs cross. -add_link_options("-L${RX_MINGW_MCF}/lib") +# CMake's compiler checks re-read this file in a scratch project; forward the +# overrides so they see the same store paths. +list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES RX_MINGW_CC RX_MINGW_MCF RX_MINGW_MCF_DEV RX_MINGW_ROOTS) +# mcfgthread ships separately from the gcc wrapper in nixpkgs cross. *_INIT +# rather than add_link_options so the compiler checks link too. +set(CMAKE_EXE_LINKER_FLAGS_INIT "-L${RX_MINGW_MCF}/lib") +set(CMAKE_SHARED_LINKER_FLAGS_INIT "-L${RX_MINGW_MCF}/lib") +set(CMAKE_MODULE_LINKER_FLAGS_INIT "-L${RX_MINGW_MCF}/lib") # Self-contained test binaries: no libstdc++/libgcc DLL hunting under Wine. add_link_options(-static-libstdc++ -static-libgcc) +# Target packages come from the mingw sysroot or are fetched, never from the +# host dev shell (whose SDL3, OpenSSL, ... are Linux builds). The *_MODE_* lines +# below only take effect with a root path set. +set(CMAKE_FIND_ROOT_PATH "${RX_MINGW_CC}" "${RX_MINGW_MCF}" ${RX_MINGW_ROOTS}) 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) diff --git a/docs/STRUCTURE.md b/docs/STRUCTURE.md index d449fb1..d2f6e9f 100644 --- a/docs/STRUCTURE.md +++ b/docs/STRUCTURE.md @@ -326,6 +326,7 @@ out of scope. |---|---|---| | `linux-dev` (`build/linux-dev`) | ON | day-to-day work: one DSO per module | | `linux`, `windows`, `android`, ... | OFF | shipping, benchmarks, install/export | +| mingw cross (`nix develop .#mingw`, toolchain file) | either | Windows builds on Linux, run under Wine | Benchmarks and captures keep running on the static presets, so the shared build never skews a performance number. @@ -344,9 +345,16 @@ What has to exist once per process, and how the shared build keeps it so: - log sinks, the feature table, app identity and the memory tracker live in `rx_foundation` and are reached through exported functions; the allocator override is compiled into the executable only (`rx_enable_mimalloc`). -- Windows: PE has no unique symbols, so a `windows-dev` preset first needs - equilibrium's base linked as a DLL and the shared CRT (`/MD`) everywhere. Not - done yet. +- Windows: PE has no unique symbols, and every DLL that links a static library + gets its own copy of its globals. So what must be one per process is a DLL + there: equilibrium's base (`EQ_BASE_SHARED`, which also moves the + Option/Feature chain roots out of line), libultragui (`ULTRAGUI_SHARED`) and + SDL3. zetanet, kinema, stb and the imgui core stay static in each DLL that + links them: no state of theirs crosses modules (zetanet's packet pools fall + back to the one CRT heap, and only the executable calls imgui). mimalloc is + off, because an executable's `operator new` replacement does not reach a DLL. + Verified with the mingw cross build (`nix develop .#mingw`, + `cmake/toolchain-mingw-w64.cmake`) under Wine; not yet with MSVC. ## Violations to fix before moving files diff --git a/flake.nix b/flake.nix index 041cfb8..38443bc 100644 --- a/flake.nix +++ b/flake.nix @@ -227,6 +227,31 @@ fi ''; }; + } // pkgs.lib.optionalAttrs (pkgs.stdenv.hostPlatform.system == "x86_64-linux") { + # Windows cross build (cmake/toolchain-mingw-w64.cmake): the default + # shell's host tools plus a ucrt64 mingw gcc and Windows builds of the + # libraries rx links. UCRT, not msvcrt, because that + # is the CRT a real Windows build uses (msvcrt lacks _strtof_l). + mingw = let + w = pkgs.pkgsCross.ucrt64; + # freetype and harfbuzz are not here: libultragui builds them from + # source on Windows (their nixpkgs cross builds pull a broken python). + roots = [ w.windows.mcfgthreads w.openssl.dev w.openssl.out ]; + in pkgs.mkShell { + inputsFrom = [ self.devShells.${pkgs.stdenv.hostPlatform.system}.default ]; + # The cross gcc on PATH too: windres runs it as its preprocessor. + # No wine here: this nixpkgs' wine segfaults inside the dev shell's + # environment; run the PEs with the host's wine. + packages = [ w.buildPackages.gcc ]; + RX_MINGW_CC = "${w.buildPackages.gcc}"; + RX_MINGW_MCF = "${w.windows.mcfgthreads}"; + RX_MINGW_MCF_DEV = "${w.windows.mcfgthreads.dev}"; + RX_MINGW_ROOTS = pkgs.lib.concatStringsSep ";" (map toString roots); + # zetanet's find_package(OpenSSL): headers and import libs live in + # different outputs. + OPENSSL_ROOT_DIR = "${w.openssl.dev}:${w.openssl.out}"; + shellHook = self.devShells.${pkgs.stdenv.hostPlatform.system}.default.shellHook; + }; }); }; } diff --git a/rxe/net/http/CMakeLists.txt b/rxe/net/http/CMakeLists.txt index 588bee4..f95dc9d 100644 --- a/rxe/net/http/CMakeLists.txt +++ b/rxe/net/http/CMakeLists.txt @@ -32,7 +32,8 @@ if(WIN32) target_link_libraries(rx_http PRIVATE ws2_32) endif() -if(RX_BUILD_TESTS) +# Not on Windows: the test's local server speaks POSIX sockets. +if(RX_BUILD_TESTS AND NOT WIN32) # rx::http unit tests: url parsing, response-head parsing, chunked decoding and # the refusals that happen before a socket is opened. Nothing here touches the # network; the live fetch inside it only runs with RX_HTTP_LIVE set. diff --git a/rxe/render/path_motion_test.cc b/rxe/render/path_motion_test.cc index f690c04..2e70886 100644 --- a/rxe/render/path_motion_test.cc +++ b/rxe/render/path_motion_test.cc @@ -3,16 +3,16 @@ #include #include +#include "base/algorithm.h" +#include "base/memory/mem_ops.h" +#include "foundation/math/scalar.h" #include "rxe/asset/mesh.h" +#include "rxe/gpu/rhi/device.h" #include "rxe/render/core/bindless.h" #include "rxe/render/gi/path_scene_history.h" #include "rxe/render/gi/skinned_rt.h" -#include "rxe/gpu/rhi/device.h" #include "shaders/path_motion_recon_cs_hlsl.h" #ifdef RX_HAS_NRD -#include "base/algorithm.h" -#include "base/memory/mem_ops.h" -#include "foundation/math/scalar.h" #include "shaders/path_motion_nrd_cs_hlsl.h" #endif diff --git a/rxe/ui/ugui/CMakeLists.txt b/rxe/ui/ugui/CMakeLists.txt index 0178141..dffc26b 100644 --- a/rxe/ui/ugui/CMakeLists.txt +++ b/rxe/ui/ugui/CMakeLists.txt @@ -28,6 +28,11 @@ if(NOT TARGET ultragui) # No STL, no exceptions: ugui's containers are base's (ugui::String is # base::String), built against rx's own equilibrium::base. set(ULTRAGUI_USE_BASE ON CACHE BOOL "" FORCE) + # One ultragui for all of rx's DLLs on Windows (see ULTRAGUI_SHARED); ELF + # shares a static copy's globals already. + if(RX_SHARED AND WIN32) + set(ULTRAGUI_SHARED ON CACHE BOOL "" FORCE) + endif() set(ULTRAGUI_PLATFORM_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/ugui_platform.cc" CACHE FILEPATH "" FORCE) set(ULTRAGUI_RHI_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/ugui_rhi.cc" diff --git a/third_party/equilibrium b/third_party/equilibrium index be9368c..3d1355b 160000 --- a/third_party/equilibrium +++ b/third_party/equilibrium @@ -1 +1 @@ -Subproject commit be9368ceb0958f6dbecf10cfc7000d26acaddec6 +Subproject commit 3d1355b901f5c25dd08cfc26971aaf7290943030