From 5fd1d5f5676e69d0a263b3a1be32f3aee39b5033 Mon Sep 17 00:00:00 2001 From: "Marco Antonio J. Costa" Date: Wed, 22 Jul 2026 15:09:57 -0300 Subject: [PATCH] build with clang-cl from a toolchain file and a preset The tree compiles and links under clang-cl, so make that a configuration anyone can ask for rather than a command line to reconstruct: MSVC_DIRECTORY=/path/to/msvc cmake --preset '1dot13 Release clang-cl' It is a check on the source, not a port -- MSVC still builds what ships. What it buys is a second front end over every translation unit, which is how the inline assembly with no operand size and the resource string with a raw 0xA9 were found, and it runs natively: clang-cl, lld-link, llvm-rc and llvm-lib need no Wine, so a full JA2.exe takes a fraction of the time. The toolchain file names every MSVC and SDK include directory, because clang-cl does not read INCLUDE and LIB the way cl does, and picks the newest version of each tree rather than pinning one. MSVC_DIRECTORY says where they live and is reported plainly when it is missing or wrong. build a bindable Bink import library when not building with MSVC The shipped binkw32.lib is an old ordinal-based long-format import library. MSVC's link.exe binds it, but clang-cl's lld-link does not: it leaves every Bink import unresolved, so at run time the calls jump into arbitrary code and crash the game at startup, six access violations deep in unrelated code before the game's own handler gives up. Rebuild the import library from binkw32.def with llvm-dlltool on any front end other than MSVC. The library imports by ordinal, for the reasons binkw32.def records: binkw32.dll exports decorated stdcall names that keep their leading underscore (_BinkOpen@8), and llvm-dlltool strips that underscore from the imported name whatever --no-leading-underscore says, so a by-name import would look up a name the DLL does not export and Wine would substitute a builtin stub that aborts ("unimplemented function binkw32.dll.BinkSetSoundSystem@8"). Ordinals have no such ambiguity and match the retail DLL. MSVC keeps binding the shipped library, so the retail build is untouched. Co-Authored-By: Claude Opus 4.8 refactor toolchain make the preset template something CMake will read The template carries two // comments. JSON has no comments and neither does the preset format, so CMake refuses the whole file: CMakePresets.json:10: Missing '}' or object member name Which means the file CopyUserPresetTemplate drops into the source directory has never worked as handed over -- "cmake --preset '1dot13 Release'" fails until you delete the comments yourself. The text they carried is worth keeping, so it moves to the description field the format does have, and cmake --preset '1dot13 Release' now configures. --- CMakeLists.txt | 9 +- binkw32.def | 104 ++++++++++++++++++ cmake/clang-toolchain.cmake | 39 ------- cmake/presets/CMakePresets.json | 3 +- cmake/toolchains/clang-cl.cmake | 91 +++++++++++++++ .../mingw.cmake} | 0 6 files changed, 204 insertions(+), 42 deletions(-) create mode 100644 binkw32.def delete mode 100644 cmake/clang-toolchain.cmake create mode 100644 cmake/toolchains/clang-cl.cmake rename cmake/{mingw-toolchain.cmake => toolchains/mingw.cmake} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index b74165cc7..511cb0e47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,8 +76,15 @@ add_subdirectory(lua) add_subdirectory(Multiplayer) add_subdirectory(wine) +# A non-MSVC toolchain rebuilds the Bink import library and points +# binkw32_lib at it (see clang-cl.cmake); MSVC binds the shipped +# library fine, so fall back to that. +if(NOT binkw32_lib) + set(binkw32_lib "${CMAKE_SOURCE_DIR}/binkw32.lib") +endif() + set(Ja2_Libraries -"${CMAKE_SOURCE_DIR}/binkw32.lib" +"${binkw32_lib}" "${CMAKE_SOURCE_DIR}/libexpatMT.lib" "${CMAKE_SOURCE_DIR}/lua51.lib" "${CMAKE_SOURCE_DIR}/lua51.vc9.lib" diff --git a/binkw32.def b/binkw32.def new file mode 100644 index 000000000..1ac234d5f --- /dev/null +++ b/binkw32.def @@ -0,0 +1,104 @@ +; Import definition for binkw32.dll, used to rebuild the Bink import library +; when building with clang-cl.cmake +; +; Two things forced the shape of this file: +; 1. lld-link cannot bind the ordinal-based long-format import library the +; game ships (binkw32.lib). It leaves every Bink call unresolved, and at +; run time the calls jump into arbitrary code and crash at startup. +; 2. binkw32.dll exports decorated stdcall names that keep their leading +; underscore (_BinkOpen@8). llvm-dlltool strips that underscore from the +; imported name whatever the --no-leading-underscore flag says, so a +; by-name import would look up "BinkOpen@8", not find it in the DLL, and +; Wine would substitute a builtin stub that aborts ("unimplemented +; function binkw32.dll.BinkSetSoundSystem@8"). +; +; Importing by ordinal (NONAME) sidesteps both: there is no name to mismatch, +; and the ordinals are those of the retail binkw32.dll the game ships. MSVC's +; link.exe binds the shipped library fine and keeps using it. +LIBRARY binkw32.dll +EXPORTS +_BinkBufferBlit@12 @1 NONAME +_BinkBufferCheckWinPos@12 @2 NONAME +_BinkBufferClear@8 @3 NONAME +_BinkBufferClose@4 @4 NONAME +_BinkBufferGetDescription@4 @5 NONAME +_BinkBufferGetError@0 @6 NONAME +_BinkBufferLock@4 @7 NONAME +_BinkBufferOpen@16 @8 NONAME +_BinkBufferSetDirectDraw@8 @9 NONAME +_BinkBufferSetHWND@8 @10 NONAME +_BinkBufferSetOffset@12 @11 NONAME +_BinkBufferSetResolution@12 @12 NONAME +_BinkBufferSetScale@12 @13 NONAME +_BinkBufferUnlock@4 @14 NONAME +_BinkCheckCursor@20 @15 NONAME +_BinkClose@4 @16 NONAME +_BinkCloseTrack@4 @17 NONAME +_BinkCopyToBuffer@28 @18 NONAME +_BinkCopyToBufferRect@44 @19 NONAME +_BinkDDSurfaceType@4 @20 NONAME +_BinkDX8SurfaceType@4 @21 NONAME +_BinkDoFrame@4 @22 NONAME +_BinkGetError@0 @23 NONAME +_BinkGetKeyFrame@12 @24 NONAME +_BinkGetRealtime@12 @25 NONAME +_BinkGetRects@8 @26 NONAME +_BinkGetSummary@8 @27 NONAME +_BinkGetTrackData@8 @28 NONAME +_BinkGetTrackID@8 @29 NONAME +_BinkGetTrackMaxSize@8 @30 NONAME +_BinkGetTrackType@8 @31 NONAME +_BinkGoto@12 @32 NONAME +_BinkIsSoftwareCursor@8 @33 NONAME +_BinkLogoAddress@0 @34 NONAME +_BinkNextFrame@4 @35 NONAME +_BinkOpen@8 @36 NONAME +_BinkOpenDirectSound@4 @37 NONAME +_BinkOpenMiles@4 @38 NONAME +_BinkOpenTrack@8 @39 NONAME +_BinkOpenWaveOut@4 @40 NONAME +_BinkPause@8 @41 NONAME +_BinkRestoreCursor@4 @42 NONAME +_BinkService@4 @43 NONAME +_BinkSetError@4 @44 NONAME +_BinkSetFrameRate@8 @45 NONAME +_BinkSetIO@4 @46 NONAME +_BinkSetIOSize@4 @47 NONAME +_BinkSetMixBinVolumes@20 @48 NONAME +_BinkSetMixBins@16 @49 NONAME +_BinkSetPan@12 @50 NONAME +_BinkSetSimulate@4 @51 NONAME +_BinkSetSoundOnOff@8 @52 NONAME +_BinkSetSoundSystem@8 @53 NONAME +_BinkSetSoundTrack@8 @54 NONAME +_BinkSetVideoOnOff@8 @55 NONAME +_BinkSetVolume@12 @56 NONAME +_BinkWait@4 @57 NONAME +_RADSetMemory@8 @58 NONAME +_RADTimerRead@0 @59 NONAME +_YUV_blit_16a1bpp@52 @60 NONAME +_YUV_blit_16a1bpp_mask@60 @61 NONAME +_YUV_blit_16a4bpp@52 @62 NONAME +_YUV_blit_16a4bpp_mask@60 @63 NONAME +_YUV_blit_16bpp@48 @64 NONAME +_YUV_blit_16bpp_mask@56 @65 NONAME +_YUV_blit_24bpp@48 @66 NONAME +_YUV_blit_24bpp_mask@56 @67 NONAME +_YUV_blit_24rbpp@48 @68 NONAME +_YUV_blit_24rbpp_mask@56 @69 NONAME +_YUV_blit_32abpp@52 @70 NONAME +_YUV_blit_32abpp_mask@60 @71 NONAME +_YUV_blit_32bpp@48 @72 NONAME +_YUV_blit_32bpp_mask@56 @73 NONAME +_YUV_blit_32rabpp@52 @74 NONAME +_YUV_blit_32rabpp_mask@60 @75 NONAME +_YUV_blit_32rbpp@48 @76 NONAME +_YUV_blit_32rbpp_mask@56 @77 NONAME +_YUV_blit_UYVY@48 @78 NONAME +_YUV_blit_UYVY_mask@56 @79 NONAME +_YUV_blit_YUY2@48 @80 NONAME +_YUV_blit_YUY2_mask@56 @81 NONAME +_YUV_blit_YV12@52 @82 NONAME +_YUV_init@4 @83 NONAME +_radfree@4 @84 NONAME +_radmalloc@4 @85 NONAME diff --git a/cmake/clang-toolchain.cmake b/cmake/clang-toolchain.cmake deleted file mode 100644 index e70a13752..000000000 --- a/cmake/clang-toolchain.cmake +++ /dev/null @@ -1,39 +0,0 @@ -set(CMAKE_SYSTEM_NAME Windows) - -set(CMAKE_C_COMPILER clang) -set(CMAKE_CXX_COMPILER clang++) -set(CMAKE_RC_COMPILER llvm-rc) - -set(triple i386-pc-win32-msvc) -set(CMAKE_C_COMPILER_TARGET ${triple}) -set(CMAKE_CXX_COMPILER_TARGET ${triple}) -set(CMAKE_RC_COMPILER_TARGET ${triple}) - -set(msvc_dir "$ENV{MSVC_DIRECTORY}") -if(msvc_dir STREQUAL "") - message(FATAL_ERROR "Please set a valid MSVC_DIRECTORY environment variable") -endif() - -# Don't remember which version of Visual Studio Build Tools this is. -# When updating this, make sure to pin the version so people can get it. -set(msvc_include - "${msvc_dir}/14.34.31933/include" - "${msvc_dir}/14.34.31933/ATLMFC/include" - "${msvc_dir}/include/10.0.22000.0/ucrt" - "${msvc_dir}/include/10.0.22000.0/um" - "${msvc_dir}/include/10.0.22000.0/shared" - "${msvc_dir}/include/10.0.22000.0/winrt" - "${msvc_dir}/include/10.0.22000.0/cppwinrt" -) -set(msvc_libraries - "${msvc_dir}/14.34.31933/ATLMFC/lib/x86" - "${msvc_dir}/14.34.31933/lib/x86" - "${msvc_dir}/lib/10.0.22000.0/ucrt/x86" - "${msvc_dir}/lib/10.0.22000.0/um/x86" -) - -foreach(LANG C CXX) - set(CMAKE_${LANG}_STANDARD_INCLUDE_DIRECTORIES ${msvc_include}) -endforeach() - -link_directories(${msvc_libraries}) diff --git a/cmake/presets/CMakePresets.json b/cmake/presets/CMakePresets.json index 381951d31..86202416c 100644 --- a/cmake/presets/CMakePresets.json +++ b/cmake/presets/CMakePresets.json @@ -4,12 +4,11 @@ { "name": "__userBase", "hidden": true, + "description": "Applications takes any of JA2, JA2MAPEDITOR, JA2UB, JA2UBMAPEDITOR, and builds all four when empty. Set CMAKE_RUNTIME_OUTPUT_DIRECTORY to a gamedir, e.g. C:/Games/JA2, to put the executables where they can be debugged.", "generator": "Ninja", "binaryDir": "${sourceDir}/build/${presetName}", "cacheVariables": { - // Valid choices: JA2;JA2MAPEDITOR;JA2UB;JA2UBMAPEDITOR. If empty (""), will configure all. "Applications": "JA2", - // For debugging: enter your desired gamedir. e.g. C:/Games/JA2 "CMAKE_RUNTIME_OUTPUT_DIRECTORY": "." }, "architecture": { diff --git a/cmake/toolchains/clang-cl.cmake b/cmake/toolchains/clang-cl.cmake new file mode 100644 index 000000000..f04d1c7f0 --- /dev/null +++ b/cmake/toolchains/clang-cl.cmake @@ -0,0 +1,91 @@ +# Usage: +# MSVC_SDK=/path/to/msvc cmake --toolchain cmake/toolchains/clang-cl.cmake ... +# +# MSVC_SDK is where the cross headers and libraries live: the directory holding +# vc/ and kits/, which for msvc-wine is the root it was installed to. It comes +# from the environment. + +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86) + +set(MSVC_SDK "$ENV{MSVC_SDK}") +if(NOT IS_DIRECTORY "${MSVC_SDK}/vc/tools/msvc" + OR NOT IS_DIRECTORY "${MSVC_SDK}/kits/10/include") + message(FATAL_ERROR + "MSVC_SDK (${MSVC_SDK}) does not hold vc/tools/msvc and kits/10/include. " + "Point it at the MSVC installation, e.g. MSVC_SDK=/home/you/msvc") +endif() + +# Both trees are versioned directories; take the newest of each rather than +# pinning a version that goes stale the next time the toolchain is updated. +function(_newest_subdirectory result dir) + file(GLOB versions "${dir}/*") + list(SORT versions) + list(POP_BACK versions newest) + set(${result} "${newest}" PARENT_SCOPE) +endfunction() + +_newest_subdirectory(_msvc "${MSVC_SDK}/vc/tools/msvc") +_newest_subdirectory(_sdk "${MSVC_SDK}/kits/10/include") +get_filename_component(_sdk_version "${_sdk}" NAME) +set(_sdk_lib "${MSVC_SDK}/kits/10/lib/${_sdk_version}") + +# Find every tool here rather than trusting PATH, so the choice of compiler, +# linker and rc is the toolchain file's alone. +foreach(tool clang-cl lld-link llvm-rc llvm-lib llvm-mt llvm-dlltool) + string(TOUPPER "${tool}" _variable) + string(REPLACE "-" "_" _variable "${_variable}") + # An absolute path: CMake resolves a bare name against the source directory. + find_program(${_variable} NAMES ${tool} REQUIRED) +endforeach() + +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}") + +# clang-cl does not read INCLUDE and LIB the way cl does, so every directory +# has to be named. -imsvc rather than -I marks them as system headers, which +# keeps warnings about the SDK out of the build log. +set(_includes + "${_msvc}/atlmfc/include" + "${_msvc}/include" + "${_sdk}/shared" + "${_sdk}/ucrt" + "${_sdk}/um" + "${_sdk}/winrt" + "${_sdk}/km" +) + +set(_flags "--target=i386-pc-windows-msvc") +foreach(directory ${_includes}) + string(APPEND _flags " -imsvc ${directory}") +endforeach() +set(CMAKE_C_FLAGS_INIT "${_flags}") +set(CMAKE_CXX_FLAGS_INIT "${_flags}") + +# The resource compiler is preprocessed by clang-cl, which needs the same +# headers again: ja2.rc includes windows.h. It takes -I, not -imsvc. +set(_rc_flags "") +foreach(directory ${_includes}) + string(APPEND _rc_flags " -I ${directory}") +endforeach() +set(CMAKE_RC_FLAGS_INIT "${_rc_flags}") + +set(CMAKE_EXE_LINKER_FLAGS_INIT + "/libpath:${_msvc}/lib/x86 /libpath:${_sdk_lib}/ucrt/x86 /libpath:${_sdk_lib}/um/x86") + +# lld-link cannot bind the Bink import library the game ships, so rebuild a +# bindable one from binkw32.def (which explains why) and hand it to the top-level +# CMakeLists through binkw32_lib. MSVC keeps using the shipped library. +execute_process( + COMMAND "${LLVM_DLLTOOL}" -m i386 --no-leading-underscore + -d "${CMAKE_CURRENT_LIST_DIR}/../../binkw32.def" + -l "${CMAKE_BINARY_DIR}/binkw32.lib" + RESULT_VARIABLE _binkw32DlltoolResult) +if(NOT _binkw32DlltoolResult EQUAL 0) + message(FATAL_ERROR "llvm-dlltool failed to build the binkw32 import library") +endif() +set(binkw32_lib "${CMAKE_BINARY_DIR}/binkw32.lib") diff --git a/cmake/mingw-toolchain.cmake b/cmake/toolchains/mingw.cmake similarity index 100% rename from cmake/mingw-toolchain.cmake rename to cmake/toolchains/mingw.cmake