Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/harness-native.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Native harnesses

# The test harnesses build with a native compiler, which is how a portability change is
# checked on a second platform. The engine target is not built: most of its sources still
# name Windows headers, and docs/BUILDING.md is the authority on what is supported.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "code/**"
- "cmake/**"
- "tests/**"
- "thirdparty/**"
- "CMakeLists.txt"
- ".gitmodules"
- ".github/workflows/harness-native.yml"
push:
branches: [main]
paths:
- "code/**"
- "cmake/**"
- "tests/**"
- "thirdparty/**"
- "CMakeLists.txt"
- ".gitmodules"
- ".github/workflows/harness-native.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: harness-native-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
harnesses:
name: Linux
# A draft pull request builds nothing; the ready_for_review trigger above starts this
# when the pull request is marked ready.
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
runs-on: ubuntu-24.04
# Clang is what the native options were written against. GCC accepts them too but
# reads __declspec differently, so it is a separate step to take, not an assumption.
env:
CC: clang
CXX: clang++
steps:
- name: Check out OpenTS
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
submodules: recursive

- name: Install Ninja
run: |
sudo apt-get update
sudo apt-get install --yes ninja-build

- name: Configure
run: >
cmake -S . -B build -G Ninja
-DCMAKE_BUILD_TYPE=Release
-DOPENTS_EXPERIMENTAL_NATIVE=ON

# The harnesses target carries whatever opents_add_test registered, which off Windows
# is the harnesses that do not name a Windows API.
- name: Build the harnesses
run: cmake --build build --target harnesses

- name: Test
run: ctest --test-dir build --output-on-failure
34 changes: 23 additions & 11 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ option(OPENTS_OFFICIAL_BUILD "Build as an official release of the declared versi

option(OPENTS_EXPERIMENTAL_CLANG_CL "Build with clang-cl using the MSVC ABI" OFF)
option(OPENTS_EXPERIMENTAL_X64 "Configure an unsupported 64-bit Windows build" OFF)
option(OPENTS_EXPERIMENTAL_NATIVE "Configure an unsupported native build for the host platform" OFF)

if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang"
AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
Expand All @@ -25,23 +26,34 @@ elseif(MSVC)
if(MSVC_VERSION LESS 1930)
message(FATAL_ERROR "OpenTS requires MSVC 19.30 or newer.")
endif()
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
# A save records pointer identities at a fixed width, but the members and raw
# structures around them still travel at the build's own widths, and the packed
# version stamp that saves and network packets carry is the same either way.
message(WARNING
"OpenTS: this build has ${CMAKE_SIZEOF_VOID_P}-byte pointers. Saved games it "
"writes are not interchangeable with a supported 32-bit build's, and nothing in "
"the version stamp distinguishes them.")
endif()
elseif(OPENTS_EXPERIMENTAL_NATIVE)
message(STATUS "OpenTS: configuring an unsupported native build for the host platform.")

# The tree declares imports and exports with __declspec throughout, which clang only
# accepts with the Microsoft extensions enabled.
add_compile_options(-fms-extensions)
else()
message(FATAL_ERROR
"OpenTS requires the Visual Studio 2022 MSVC toolchain. "
"For the unsupported clang-cl experiment, configure with "
"-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/clang-cl-msvc.cmake.")
"-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/clang-cl-msvc.cmake. "
"For the unsupported native build, configure with "
"-DOPENTS_EXPERIMENTAL_NATIVE=ON.")
endif()

enable_language(RC)
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
# A save records pointer identities at a fixed width, but the members and raw
# structures around them still travel at the build's own widths, and the packed
# version stamp that saves and network packets carry is the same either way.
message(WARNING
"OpenTS: this build has ${CMAKE_SIZEOF_VOID_P}-byte pointers. Saved games it "
"writes are not interchangeable with a supported 32-bit build's, and nothing in "
"the version stamp distinguishes them.")
endif()

if(WIN32)
enable_language(RC)
endif()

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand Down
33 changes: 29 additions & 4 deletions code/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set(CMAKE_CONFIGURATION_TYPES Debug Release CACHE STRING "" FORCE)

# OpenTS supports 32-bit (x86) builds. A 64-bit build is an unsupported experiment.
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4 AND NOT OPENTS_EXPERIMENTAL_X64)
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4 AND NOT OPENTS_EXPERIMENTAL_X64 AND NOT OPENTS_EXPERIMENTAL_NATIVE)
message(FATAL_ERROR
"OpenTS must be built as 32-bit x86. Reconfigure with -A Win32. "
"For the unsupported 64-bit experiment, configure with "
Expand Down Expand Up @@ -95,6 +95,8 @@ set_target_properties(OpenTS PROPERTIES
#
message(STATUS "${PROJECT_NAME}: Applying compiler flags...")

if(MSVC)

set(OPENTS_COMPILE_OPTIONS

# ================= DEBUG =================
Expand Down Expand Up @@ -132,6 +134,29 @@ set(OPENTS_COMPILE_OPTIONS
>
)

else()

# The native equivalents of the options above. /RTC1 and the static runtime library have
# none, and a native compiler needs no help with UTF-8 sources or with __cplusplus.
set(OPENTS_COMPILE_OPTIONS
-g
$<$<CONFIG:Debug>:-O0>
$<$<CONFIG:Debug>:-fno-omit-frame-pointer>
$<$<CONFIG:Release>:-O2>

# /fp:precise forbids contracting the engine's accumulations into fused operations,
# which a native compiler does within a statement by default.
-ffp-contract=off
)

# 32-bit x86 keeps x87 excess precision unless the SSE unit is selected, which is what
# /arch:SSE2 is doing above.
if(CMAKE_SIZEOF_VOID_P EQUAL 4 AND CMAKE_SYSTEM_PROCESSOR MATCHES "[xi][3-8]86")
list(APPEND OPENTS_COMPILE_OPTIONS -msse2 -mfpmath=sse)
endif()

endif()

target_compile_options(OpenTS PRIVATE ${OPENTS_COMPILE_OPTIONS})

#
Expand Down Expand Up @@ -160,7 +185,7 @@ set_source_files_properties("${CMAKE_CURRENT_SOURCE_DIR}/bgfxbackend.cpp" PROPER
"${BGFX_ROOT}/include;${CMAKE_SOURCE_DIR}/thirdparty/bgfx.cmake/bx/include;${BGFX_ROOT}/examples/common/imgui"
COMPILE_DEFINITIONS
"BX_CONFIG_DEBUG=$<IF:$<CONFIG:Debug>,1,$<BOOL:${BX_CONFIG_DEBUG}>>"
COMPILE_OPTIONS "/Zc:preprocessor"
COMPILE_OPTIONS "$<$<BOOL:${MSVC}>:/Zc:preprocessor>"
)

# bx rewrites __stdcall while its headers are being parsed by clang-cl. Force the
Expand All @@ -174,8 +199,8 @@ endif()

message(STATUS "${PROJECT_NAME}: Adding compilier definitions...")
target_compile_definitions(OpenTS PRIVATE
WIN32
_WINDOWS
$<$<BOOL:${WIN32}>:WIN32>
$<$<BOOL:${WIN32}>:_WINDOWS>
NOMINMAX

# Compiles Blowfish into the binary instead of reaching it through the COM object in
Expand Down
31 changes: 31 additions & 0 deletions docs/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,37 @@ version stamp that saves and network packets carry is the same for both, so
nothing rejects a save or a peer on that basis. Configuring the build warns
about it.

## Test harnesses on a native toolchain

The test harnesses build with a native compiler, which is how portability work is
checked without a Windows machine. The engine target does not link: most of its
translation units compile, and the rest stop on the Windows headers they name.

```bash
cmake -S . -B build/native -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DOPENTS_EXPERIMENTAL_NATIVE=ON
cmake --build build/native --target harnesses
ctest --test-dir build/native
```

Continuous integration runs exactly that on Linux, so a change that breaks the
native harnesses is caught rather than discovered later.

A harness that does not build off Windows yet declares `UNPORTED` in its
`opents_add_test` call and is configured on Windows alone, so a native build
registers the rest and nothing else. Sixteen of the thirty-seven build and pass
on macOS at the time of writing. The other twenty-one reach `windows.h`,
`io.h`, `comdef.h` or the MSVC spellings of the C library, most of them through
an include and not by their own subject, as do about a third of the engine's own
sources. Each one that loses the keyword is a subsystem that has been ported.

The options the engine builds with are MSVC's, and the native build passes the
equivalents rather than the same spellings: `-O0` and `-O2` for `/Od` and `/O2`,
`-ffp-contract=off` for `/fp:precise`, and `-msse2 -mfpmath=sse` for `/arch:SSE2`
where the target is 32-bit x86. `/RTC1` and the static runtime library have no
equivalent and are not replaced.

## Build from Visual Studio Code

With the recommended extensions installed, the repository provides:
Expand Down
62 changes: 48 additions & 14 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,27 @@
# runs. Set before the subdirectories so every target below inherits it.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/test-bin")

# Everything opents_add_test registers, so a build that cannot finish the engine can still
# ask for the harnesses by name.
add_custom_target(harnesses)

# Builds one test harness and registers it with CTest.
#
# A harness compiles the engine sources it exercises instead of linking the game, so ENGINE
# names them per test, relative to code/. SOURCES are relative to the test's own directory.
# FLOAT adds the float semantics the engine builds with, so a harness measuring numeric
# results matches it. UTF8 is for a harness whose own source holds non-ASCII text. STAMP is
# for a harness that reaches the generated version headers.
# for a harness that reaches the generated version headers. UNPORTED is for a harness that
# does not build off Windows yet, whether because it reaches a Windows API or because
# something it compiles still names a Windows header; it is configured on Windows alone.
function(opents_add_test target)
cmake_parse_arguments(TEST "FLOAT;UTF8;STAMP" "NAME"
cmake_parse_arguments(TEST "FLOAT;UTF8;STAMP;UNPORTED" "NAME"
"SOURCES;ENGINE;INCLUDES;DEFINITIONS;LIBRARIES" ${ARGN})

if(TEST_UNPORTED AND NOT WIN32)
return()
endif()

set(sources "")
foreach(source IN LISTS TEST_SOURCES)
list(APPEND sources "${CMAKE_CURRENT_SOURCE_DIR}/${source}")
Expand All @@ -30,34 +40,58 @@ function(opents_add_test target)
)

if(TEST_DEFINITIONS)
if(NOT WIN32)
# These three name the platform a harness is built for, which a native build
# is not. NOMINMAX only reaches a Windows header and costs nothing here.
list(REMOVE_ITEM TEST_DEFINITIONS WIN32 _WINDOWS _MBCS)
endif()
target_compile_definitions(${target} PRIVATE ${TEST_DEFINITIONS})
endif()

target_compile_options(${target} PRIVATE
$<$<CONFIG:Debug>:/MTd>
$<$<CONFIG:Release>:/MT>
/EHsc
/Zc:__cplusplus
)
if(TEST_UTF8)
target_compile_options(${target} PRIVATE /utf-8)
if(MSVC)
target_compile_options(${target} PRIVATE
$<$<CONFIG:Debug>:/MTd>
$<$<CONFIG:Release>:/MT>
/EHsc
/Zc:__cplusplus
)
if(TEST_UTF8)
target_compile_options(${target} PRIVATE /utf-8)
endif()
else()
# A native compiler reads UTF-8 sources and enables exceptions already, and has no
# runtime library to choose. The Microsoft extensions the engine headers need come
# from the top-level build.
endif()

# /arch:SSE2 is the default beyond 32-bit x86 and is rejected as an unknown option
# there, so it is only passed where it applies.
if(TEST_FLOAT)
target_compile_options(${target} PRIVATE /fp:precise)
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
target_compile_options(${target} PRIVATE /arch:SSE2)
if(MSVC)
target_compile_options(${target} PRIVATE /fp:precise)
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
target_compile_options(${target} PRIVATE /arch:SSE2)
endif()
else()
# /fp:precise forbids contracting the engine's accumulations into fused
# operations. A native compiler contracts them within a statement by default.
target_compile_options(${target} PRIVATE -ffp-contract=off)
if(CMAKE_SIZEOF_VOID_P EQUAL 4 AND CMAKE_SYSTEM_PROCESSOR MATCHES "[xi][3-8]86")
target_compile_options(${target} PRIVATE -msse2 -mfpmath=sse)
endif()
endif()
endif()

target_link_libraries(${target} PRIVATE kernel32 user32 shell32 ${TEST_LIBRARIES})
if(WIN32)
target_link_libraries(${target} PRIVATE kernel32 user32 shell32)
endif()
target_link_libraries(${target} PRIVATE ${TEST_LIBRARIES})

if(TEST_STAMP)
add_dependencies(${target} OpenTSBuildStamp)
endif()

add_dependencies(harnesses ${target})
add_test(NAME ${TEST_NAME} COMMAND ${target})
endfunction()

Expand Down
1 change: 1 addition & 0 deletions tests/autosave/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ opents_add_test(Autosave
ENGINE
autosave.cpp
DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX
UNPORTED
)
1 change: 1 addition & 0 deletions tests/cpudetect/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ opents_add_test(CpuDetect
mpu.cpp
DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX
FLOAT
UNPORTED
)
1 change: 1 addition & 0 deletions tests/cstream/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ opents_add_test(CStreamContract
DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX
LIBRARIES lzo
FLOAT
UNPORTED
)
1 change: 1 addition & 0 deletions tests/deploymentconfig/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ opents_add_test(DeploymentConfig
DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX
UTF8
STAMP
UNPORTED
)
1 change: 1 addition & 0 deletions tests/gamedirs/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ opents_add_test(GameDirs
DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX
UTF8
STAMP
UNPORTED
)
1 change: 1 addition & 0 deletions tests/ini/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ opents_add_test(IniContract
DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX
UTF8
STAMP
UNPORTED
)
1 change: 1 addition & 0 deletions tests/lcwblock/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ opents_add_test(LcwBlock
xstraw.cpp
DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX
FLOAT
UNPORTED
)
1 change: 1 addition & 0 deletions tests/logstress/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ opents_add_test(LogStress
INCLUDES ${OPENTS_GENERATED_DIR}
DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX
STAMP
UNPORTED
)
1 change: 1 addition & 0 deletions tests/lzoblock/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ opents_add_test(LzoBlock
DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX
LIBRARIES lzo
FLOAT
UNPORTED
)
1 change: 1 addition & 0 deletions tests/netpacket/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ opents_add_test(NetContract
INCLUDES ${OPENTS_GENERATED_DIR}
DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX
STAMP
UNPORTED
)
1 change: 1 addition & 0 deletions tests/scenfile/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ opents_add_test(ScenFile
ENGINE
scenfile.cpp
DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX
UNPORTED
)
Loading