Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a43ebcd
SolLua: use sol::lua_nil instead of the sol::nil alias
MarkKropf Jun 18, 2026
3e83e20
float3: drop redundant direct streflop_cond.h include
tomjn Jun 18, 2026
517608f
SafeUtil: include <type_traits> and <memory> directly (#3025)
tomjn Jun 18, 2026
6318fb6
LuaTextures: log glTexImage failures instead of returning nil silently
iamaperson000 Jun 18, 2026
54348ff
fix: guard against already-dead reclaim targets (#3020)
bruno-dasilva Jun 19, 2026
e91a58f
CUtils: use const dirent* for the scandir selector on all platforms
tomjn Jun 20, 2026
580100a
legacy: only require X11 on non-Apple UNIX
tomjn Jun 20, 2026
d13dcd3
FindSevenZip: also accept the 7zz binary name
tomjn Jun 20, 2026
0da81b4
macOS: remove dead SDL 1.2-era SDLMain.{m,h}
tomjn Jun 19, 2026
83642b0
Fix SplinterFaction card link format
ScarylePoo Jun 21, 2026
13b6804
fix: update section min-level in place instead of appending duplicate…
bruno-dasilva Jun 22, 2026
1caa3c5
Convert `u8string_view` to `string` using explicit size
Jun 23, 2026
2186ee4
Fix inconsistent class/struct forward declarations
Jun 5, 2026
a854046
macOS: guard X11/Xlib.h include for non-X11 platforms
tomjn Jun 25, 2026
ab68d6f
fix: address sse2neon/streflop macro redefinition warning (#2999)
bruno-dasilva Jun 29, 2026
52b55cf
glad: do not build the GLX loader on macOS
tomjn Jun 29, 2026
4034d5a
docs: add ENGINE_PERFORMANCE.md with some notes on engine internals (…
bruno-dasilva Jun 29, 2026
7e07543
System: add missing <algorithm> includes for libc++
tomjn Jun 29, 2026
cf3b083
DemoTool: link nowide and fmt for their include directories
tomjn Jun 29, 2026
f898efc
Fix truncated sync checksum in demotool dump
burnhamrobertp Jun 19, 2026
8141bf2
Portability in archive handlers
tomjn Jun 30, 2026
898dd6c
docs: add BACKWARDS_COMPATIBILITY.md guidance doc for coding agents
bruno-dasilva Apr 11, 2026
487fd43
docs: improve running/testing instructions in AGENTS.md (#2917)
bruno-dasilva Jul 1, 2026
d313449
Move `isHeadless` from `Engine` to `Platform`. (#3067)
sprunk Jul 2, 2026
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
164 changes: 111 additions & 53 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,91 +8,159 @@ RecoilEngine is an open-source real-time strategy (RTS) game engine written in C

## Build Commands

### Submodules

The repo uses git submodules for vendored libraries (`rts/lib/*`, `tools/pr-downloader`, AI skirmish bots, etc.). If you cloned without `--recurse-submodules`, initialize them before building:
```bash
git submodule update --init --recursive
```

### Building the Engine

**Using Docker (Recommended):**
```bash
# Build for Linux
# Full build (default: RELWITHDEBINFO, -O3 -g -DNDEBUG, Ninja)
# Output lands in build-<arch>-<os>/ (e.g. build-amd64-linux/) and the
# ready-to-use install in build-amd64-linux/install/
docker-build-v2/build.sh linux

# Build for Windows
# Parallelism
docker-build-v2/build.sh -j 8 linux

# Windows cross-build
docker-build-v2/build.sh windows

# Build with custom CMake options
# Change optimization level — trailing -D… is forwarded to configure.sh and
# overrides the baked-in RELWITHDEBINFO default.
docker-build-v2/build.sh linux -DCMAKE_BUILD_TYPE=DEBUG
docker-build-v2/build.sh linux -DCMAKE_BUILD_TYPE=RELEASE
docker-build-v2/build.sh linux -DCMAKE_BUILD_TYPE=PROFILE

# Combine cmake options (configure phase)
docker-build-v2/build.sh linux -DBUILD_spring-headless=OFF -DTRACY_ENABLE=ON

# List all available cmake options and their current values
docker-build-v2/build.sh --configure linux -LH

# Build a specific target — use --compile so args flow to `cmake --build`,
# not to configure. Without --compile, `-t …` would be rejected by configure.
docker-build-v2/build.sh --compile linux -t engine-headless
docker-build-v2/build.sh --compile linux -t engine-legacy
docker-build-v2/build.sh --compile linux -t tests --verbose

# Split the phases
docker-build-v2/build.sh --configure linux # configure only
docker-build-v2/build.sh --compile linux # compile only (reuses existing config)
```

**Without Docker:**
```bash
# Create build directory
mkdir -p build && cd build

# Configure
# Configure — project requires C++23 (clang ≥ 17 or gcc ≥ 13 on PATH).
# CMAKE_BUILD_TYPE defaults to RELWITHDEBINFO when omitted.
cmake ..

# Build specific target
cmake --build . --target engine-headless -j$(nproc)

# Build all
cmake --build . -j$(nproc)
# Optional: Default generator is Unix Makefiles; add `-G Ninja` for faster builds if ninja is installed.
cmake -G Ninja ..

# Optional: pin to gcc-13 + gold linker via the in-repo toolchain file
# (tracked under docker-build-v2/; same compiler the docker build uses).
cmake \
-DCMAKE_TOOLCHAIN_FILE=../docker-build-v2/images/all-linux/toolchain.cmake ..

# Optional: speed up incremental builds with ccache
cmake \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache ..

# Change optimization level by re-running cmake (no wipe required):
cmake -DCMAKE_BUILD_TYPE=DEBUG .. # no optimization, full symbols
cmake -DCMAKE_BUILD_TYPE=RELEASE .. # optimized, no debug info
cmake -DCMAKE_BUILD_TYPE=RELWITHDEBINFO .. # optimized + debug info (default)
cmake -DCMAKE_BUILD_TYPE=PROFILE .. # optimized + profiling hooks

# Build (generator-agnostic — works under Ninja or Make)
cmake --build .

# Build a specific target
cmake --build . --target engine-headless
cmake --build . --target engine-legacy
cmake --build . --target engine-dedicated
cmake --build . --target tests
```

> The docker flow writes to **`build-<arch>-<os>/`** (e.g. `build-amd64-linux/`
> or `build-amd64-windows/`), which is a different directory than the `build/`
> used by this flow. When running tests, point commands at whichever build
> directory you populated.

### Build Types
- `DEBUG` - Debug build with full symbols and no optimization
- `RELEASE` - Optimized release build
- `RELWITHDEBINFO` - Release with debug info (default)
- `PROFILE` - Profiling build

### Build Targets
- `engine-legacy` - Main engine build
- `engine-headless` - Headless server build
- `engine-dedicated` - Dedicated server build
- `tests` - Build all test executables
- `check` - Build and run all tests
- `spring-content` - Build game content packages
- `engine-legacy` — main interactive engine build
- `engine-headless` — headless engine (no graphics)
- `engine-dedicated` — dedicated server
- `unitsync` — unitsync shared library
- `pr-downloader` — content downloader tool
- `tests` — phony; builds every `test_*` executable under `build/test/`
- `check` — phony; depends on `engine-headless` + all `test_*` executables, then runs ctest with `--output-on-failure -V`
- `install` — install into `CMAKE_INSTALL_PREFIX`

## Testing

### Test Framework
The project uses **Catch2** for unit testing. Test files are located in the `test/` directory.
### Writing Tests
See `test/AGENTS.md` for details on writing tests, available compile flags, patterns, and test helpers.

### Running Tests

**Build and run all tests:**
```bash
# From build directory
make tests # Build all test executables
make check # Build and run all tests via CTest
make test # Alternative: run via CTest
# From build/ — ctest / check recipes below assume a non-docker build.
# For a docker build, run `docker-build-v2/build.sh --compile linux -t check`
# (runs ctest inside the container) or invoke the binaries in
# build-amd64-linux/test/ directly.
cmake --build . --target tests # build all test executables (no run)
ctest # run all tests (does not rebuild)
# OR
cmake --build . --target check # rebuild engine-headless + all tests, then run ctest -V
```

**Run a single test:**
`check` is the safe default when iterating; bare `ctest` is faster when nothing relevant has changed since the last build.

**Run a single test (from repo root):**
```bash
# Tests are built as executable binaries in the build directory
# Pattern: test_<TestName>
# Tests are built as executable binaries under <build-folder>/test/
# Pattern: <build-folder>/test/test_<TestName>
# where <build-folder> depends on if you built in docker or not (see above).

# Run specific test executable
./test_Float3
./test_Matrix44f
./test_SyncedPrimitive
./test_UDPListener
./build/test/test_Float3
./build/test/test_Matrix44f
./build/test/test_SyncedPrimitive
./build/test/test_UDPListener

# Run with verbose output
./test_Float3 -s
# Catch2: show passing assertions too
./build/test/test_Float3 -s

# Run specific test case
./test_Float3 "TestSection"
# Run a specific test case by name (positional arg matches TEST_CASE name, supports wildcards)
./build/test/test_Float3 "Float3"
./build/test/test_Float3 "Float34_*"
```

**Run via CTest:**
**Run via CTest (from inside build/):**
```bash
# Run specific test by name
ctest -R Float3 -V
# Filter by regex, show output only on failure
ctest -R Float3 --output-on-failure

# Run with regex pattern
ctest -R Matrix -V
# Same, but verbose (full stdout regardless of result)
ctest -R Float3 -V

# List all available tests
# List all registered tests without running
ctest -N
```

Expand Down Expand Up @@ -325,20 +393,6 @@ Use preprocessor directives for platform-specific code:
- Use tabs for indentation in CMake files
- Keep lines reasonably short

### Adding Tests
In `test/CMakeLists.txt`:
```cmake
set(test_name TestName)
set(test_src
"${CMAKE_CURRENT_SOURCE_DIR}/path/to/TestFile.cpp"
${test_Common_sources}
)
set(test_libs
library_name
)
add_spring_test(${test_name} "${test_src}" "${test_libs}" "${test_flags}")
```

## Project Structure

- `rts/` - Main engine source code
Expand Down Expand Up @@ -388,6 +442,10 @@ The engine uses custom thread pools. See `THREADPOOL` define and related code.
4. Follow the workflow in `contributing.md`
5. Disclose any AI assistance used

### Additional docs
Please see @coding-agents/ for additional documentation:
- coding-agents/ENGINE_PERFORMANCE.md — notes on scale targets and engine performance internals. Useful for performance related changes.
- coding-agents/BACKWARDS_COMPATIBILITY.md - notes on when we should strive to be backwards compatible. Reference it for any major reworks or api changes.
## Additional Resources

- Official website: https://recoilengine.org
Expand Down
4 changes: 0 additions & 4 deletions AI/Wrappers/CUtils/Util.c
Original file line number Diff line number Diff line change
Expand Up @@ -487,11 +487,7 @@ static void util_initFileSelector(const char* suffix) {
fileSelectorSuffix = suffix;
}

#if defined(__APPLE__)
static int util_fileSelector(struct dirent* fileDesc) {
#else
static int util_fileSelector(const struct dirent* fileDesc) {
#endif
return util_endsWith(fileDesc->d_name, fileSelectorSuffix);
}

Expand Down
25 changes: 25 additions & 0 deletions coding-agents/BACKWARDS_COMPATIBILITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Backwards compatibility

Recoil isn't 100% beholden to backwards compatibility, but breaking changes are weighed carefully against their benefit. Backwards compatiblity is a constraint, not a veto.

## Why the bar is high

Recoil games aren't short-cycle Unreal/Unity titles — they're lifetime hobby projects. There is no steady stream of new games picking up the latest engine; the games we have are the games we have. They fall into two camps, and neither absorbs churn well:

- **Mature games** need stability above all else.
- **Games still in active development** have the flexibility, but rarely the volunteer bandwidth to chase significant engine breakage.

## How to weigh a change

- Quantify the benefit (perf, correctness, maintainability) concretely, not in the abstract.
- Identify which games or content would break, and how mechanical the fix is on their side. "Rename a call site" is very different from "rearchitect your gadget."
- Prefer changes whose blast radius is contained or whose adaptation is mechanical. Avoid changes that force games to rethink core logic with no real mitigation path.

## Precedents

- **Multi-threaded unit movement & collision** — landed with a large perf win and effectively no game-side impact (ignoring incidentally-fixed bugs). This is the shape of change to look for.
- **Multi-threading `Unit::Update`, `Unit::SlowUpdate`, or projectiles** — don't. The impact on games would be huge and there isn't much that can be done to mitigate it. Not a path worth proposing.

## The upshot

Backwards compatiblity constraints don't close the door on performance work — they just point it at the areas where the blast radius is small. Plenty of wins are still on the table; pick the ones games don't have to pay for.
55 changes: 55 additions & 0 deletions coding-agents/ENGINE_PERFORMANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Engine performance

Recoil is an RTS engine built for large-scale games — designed to handle thousands of units at once.

## Scale target

- **Target:** ~10k concurrent units, *including buildings*.
- Mobile units tend to be ~40% of that late-game total.
- Largest seen in a real game: ~17.7k units. That's a data point, not a design target.

## Sim, draw, and update frames

The main loop is `Update → Draw`, repeating — see the diagram and table below for the per-phase breakdown. Each iteration first drains any queued sim-frame packets (0..N per iteration), then renders one draw frame. `CGame::Update` dispatches `SimFrame()` calls as `NETMSG_NEWFRAME` packets arrive; `CGame::Draw` runs the unsynced update phase and then renders. The sim burst is capped at ~500 ms (`minDrawFPS`) so draw always gets to run, and it's all one thread — sim and rendering are **not concurrent**; parallelism only happens *inside* a phase.

Conversely, if no sim frames are in the queue the main loop runs `Draw`/`UpdateUnsynced` as fast as possible — many draw iterations can pass between successive sim frames, with visuals interpolating smoothly in between via `globalRendering->timeOffset`.

```
main-loop iteration (repeats as fast as possible)
├── CGame::Update (mostly synced)
│ └── SimFrame × 0..N ← processes queued sim frames capped at
| ~500ms per iteration
└── CGame::Draw (unsynced)
├── UpdateUnsynced ← unsynced update phase
└── render world + screen ← Draw::World + Draw::Screen
```

| Phase | Rate | Synced? | Responsibility |
|---|---|---|---|
| **Sim frame** — `CGame::SimFrame` | fixed 30 Hz (`GAME_SPEED`) | mostly yes | advance deterministic state: units, pathing, projectiles, line-of-sight, scripts, Lua `GameFrame` |
| **Draw frame** — `CGame::Draw` | variable | no | update phase (see below) + render world/screen |
| **Update phase** — `CGame::UpdateUnsynced` *(inside draw frame)* | per draw frame | no | timings, interpolation, camera, GUI, sound, world-drawer prep |

### Profiler buckets

The engine `CTimeProfiler` (and the `benchmark` tool) report three peer buckets: `Sim` (the whole synced step), `Update` (`CGame::UpdateUnsynced`), and `Draw` (rendering only, *excluding* the Update that runs first).

`Sim` is **"mostly" synced**: it also bills unsynced work that runs inline during `SimFrame`.
- **Explicit Lua callins** — `GameFrame`/`GameFramePost` run near the start of each sim frame.
- **Event-driven Lua callins** — unsynced widgets can subscribe to synced game events, so their handlers run inline as those events fire during the frame.
- **C++-only unsynced sections** — e.g. the MT projectile visual pass (`Sim::Projectiles::UpdateUnsyncedMT`) and ghosted-building updates (`CUnitDrawer::UpdateGhostedBuildings`).

### Scheduling and CPU budget

- Sim has a target rate set by the server; draw is as fast as the hardware allows. The sim target is `30 Hz × speedFactor`; at a speed factor of 1x, in-game time tracks real-world time 1:1, and at 2x speed the server fires twice as many sim frames per real-world second so the world evolves twice as fast.
- **Zero, one, or many** sim frames per draw frame — if the client falls behind, pending sim frames burst in the next iteration to catch up.
- Visuals interpolate between sim frames, so draw rate can exceed sim rate without stutter.
- Sim time is carefully budgeted and scheduled against draw frames (because they run serially) so there's always a minimum fps for the player

## Multi-threading

The engine runs one **main thread** plus a pool of **worker threads**, all pinned to distinct cores. We typically aim for 6-8 worker threads. The main thread drives the sim/draw loop; workers pick up parallel work dispatched from the main thread (via `for_mt` and friends in `rts/System/Threading/ThreadPool.h`). The main thread also participates in draining the task queue while it waits.

Most parallel work in the engine is **homogeneous** — the same operation applied over many items (unit updates, projectile steps, etc.) via `for_mt`. Keeping parallel work homogeneous is a deliberate discipline: it makes determinism easier to reason about and keeps sim output independent of how work happens to land across threads.

**QTPFS is the one heterogeneous exception.** The quad-tree pathfinder maintains its own per-worker search state (`SearchThreadData`, `SparseData`) independent of engine sim state, which lets it safely run path searches on the worker pool *in the background* via `for_mt_background`. Background tasks yield to higher-priority work by rescheduling themselves when other jobs arrive, so QTPFS soaks up idle worker capacity without preempting foreground parallelism.
2 changes: 1 addition & 1 deletion doc/site/content/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ draft = false
{{< /cards >}}
{{< cards >}}
{{< card title="Tech Annihilation" image="/showcase/ta.jpeg" link="https://github.com/techannihilation/TA" >}}
{{< card title="SplinterFaction" image="showcase/splinter_faction.jpg" link="splinterfaction.info" >}}
{{< card title="SplinterFaction" image="showcase/splinter_faction.jpg" link="https://splinterfaction.info" >}}
{{< card title="Mechcommander: Legacy" image="/showcase/mcl.jpg" link="https://github.com/SpringMCLegacy/SpringMCLegacy/wiki" >}}
{{< /cards >}}

Expand Down
2 changes: 1 addition & 1 deletion doc/site/content/changelogs/_index.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ This is the bleeding-edge changelog since version 2025.06, for **pre-release 202
- add `Spring.GetClosestEnemyUnit(x, y, z, range = inf, allyTeamID, bool useLoS = true, bool spherical = false, bool requireEnemyToSeePos = false) → unitID?` to LuaRules.
- large QTPFS perf improvements.
- always output logs to stdout.
- add `Engine.isHeadless`, available in unsynced only.
- add boolean `Platform.isHeadless`.
- archive cache version 20 → 21.

## Fixes
Expand Down
11 changes: 10 additions & 1 deletion docker-build-v2/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,16 @@ if [[ "$GIT_DIR" != "$GIT_COMMON_DIR" ]]; then
WORKTREE_MOUNTS="-v $GIT_COMMON_DIR:$GIT_COMMON_DIR:ro"
fi

$RUNTIME run --platform=linux/$ARCH -it --rm \
# Docker's -t requires stdin AND stdout to be TTYs; in CI, pipes, or agent
# contexts one or both are missing and docker errors out with "the input
# device is not a TTY". Only add -t when it's safe; -i is harmless either
# way (non-interactive stdin just sees EOF).
TTY_FLAG=
if [[ -t 0 && -t 1 ]]; then
TTY_FLAG=-t
fi

$RUNTIME run --platform=linux/$ARCH -i $TTY_FLAG --rm \
-v "$CWD${P}":/build/src:z,ro \
-v "$CWD${P}.cache${P}ccache-$PLATFORM":/build/cache:z,rw \
-v "$CWD${P}build-$PLATFORM":/build/out:z,rw \
Expand Down
3 changes: 0 additions & 3 deletions rts/Lua/LuaConstEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,6 @@ bool LuaConstEngine::PushEntries(lua_State* L)
LuaPushNamedString(L, "buildFlags" , SpringVersion::GetAdditional());
LuaPushNamedNumber(L, "wordSize", (!CLuaHandle::GetHandleSynced(L))? Platform::NativeWordSize() * 8: 0);

if (!CLuaHandle::GetHandleSynced(L))
LuaPushNamedBool(L, "isHeadless", SpringVersion::IsHeadless());

LuaPushNamedNumber(L, "gameSpeed", GAME_SPEED);
LuaPushNamedNumber(L, "maxCustomPaletteID", MAX_CUSTOM_COLORS - 1);

Expand Down
4 changes: 4 additions & 0 deletions rts/Lua/LuaConstPlatform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "LuaConstPlatform.h"
#include "LuaUtils.h"
#include "Game/GameVersion.h"
#include "System/Platform/Hardware.h"
#include "System/Platform/Misc.h"
#include "Rendering/GlobalRendering.h"
Expand Down Expand Up @@ -147,5 +148,8 @@ bool LuaConstPlatform::PushEntries(lua_State* L)
/*** @field Platform.macAddrHash string */
LuaPushNamedString(L, "macAddrHash", Platform::GetMacAddrHash());

/*** @field Platform.isHeadless boolean Is this a headless build which only simulates and doesnt offer interactive IO? */
LuaPushNamedBool(L, "isHeadless", SpringVersion::IsHeadless());

return true;
}
4 changes: 3 additions & 1 deletion rts/Lua/LuaTextures.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ std::string LuaTextures::Create(const Texture& tex)
} break;
}

if (glGetError() != GL_NO_ERROR) {
if (const GLenum texErr = glGetError(); texErr != GL_NO_ERROR) {
LOG_L(L_ERROR, "[LuaTextures::%s] glTexImage failed: target=0x%x size=%dx%d fmt=0x%x dataFmt=0x%x dataType=0x%x border=%d glError=0x%x",
__func__, tex.target, tex.xsize, tex.ysize, tex.format, dataFormat, dataType, tex.border, texErr);
glDeleteTextures(1, &texID);
glBindTexture(tex.target, currentBinding);
return "";
Expand Down
Loading
Loading