Skip to content

Commit ba54f52

Browse files
authored
docs: mark idle cpu patch as legacy (#13)
1 parent 624fc43 commit ba54f52

10 files changed

Lines changed: 55 additions & 53 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,10 @@ Every variable below is consumed by the container entrypoint, so it applies iden
191191
| `FILES_WAIT_TIMEOUT_SECONDS` | `0` | 0 = wait forever for `WindowsServer/` to appear before launching. Only relevant when `WINDROSE_SERVER_SOURCE=files`. |
192192
| `PROTON_USE_XALIA` | `0` | Xalia crashes on headless Proton; leave off. |
193193

194-
**Idle-CPU patch**
194+
**Legacy idle-CPU patch**
195195
| Env var | Default | Purpose |
196196
|---|---|---|
197-
| `WINDROSE_PATCH_IDLE_CPU` | `0` | `1` → entrypoint builds a `WindroseServer-Win64-Shipping.patched.exe` sibling of the Steam-managed original on every start (caching by source md5), and launches that instead. The original is never modified, so SteamCMD's app_update has nothing to revert. **Experimental, no warranty** — see Caveats and the script header. Bare-Linux `install.sh` prompts for confirmation when flipping to `1`; set `WINDROSE_PATCH_ACK_RISK=1` to bypass for headless operators. |
197+
| `WINDROSE_PATCH_IDLE_CPU` | `0` | Legacy workaround for older/pinned server builds that predate Windrose's official idle-CPU fix. Current SteamCMD builds should leave this `0`. `1` builds a `WindroseServer-Win64-Shipping.patched.exe` sibling of the Steam-managed original and launches that instead. The original is never modified, so SteamCMD's app_update has nothing to revert. **Experimental, no warranty** — see Caveats and the script header. Bare-Linux `install.sh` prompts for confirmation when flipping to `1`; set `WINDROSE_PATCH_ACK_RISK=1` to bypass for headless operators. |
198198
| `WINDROSE_PATCH_OVERRIDE_FILE` | `$R5_DIR/.idle-patch-override` | Runtime override written by the admin UI's Idle-CPU card. `disabled` forces OFF (patched sibling is removed on next restart, game launches original); `enabled` forces ON regardless of env. Absent = follow `WINDROSE_PATCH_IDLE_CPU`. |
199199

200200
**Admin UI (`windrose-ui` sidecar)**
@@ -532,9 +532,9 @@ CI runs these plus JSON validation and YAML lint.
532532
--from-file=app.js=./ui/app.js
533533
```
534534
Point nginx's `root` or `alias` at the mount and `proxy_pass` the `/api/*` location to the windrose Service.
535-
- **Unpatched idle CPU is ~2 cores** — a known upstream spin-loop bug in `boost::asio::detail::socket_select_interrupter::reset()`, unreachable from Engine.ini / launch args / Proton env (see `memory/windrose_idle_cpu_known_bug.md`). Optional binary patch at `scripts/patch-idle-cpu.py` injects a `Sleep(1)` into the hot loop; ~97% reduction on the builds measured so far. Opt in via `WINDROSE_PATCH_IDLE_CPU=1` (Helm: `patchIdleCpu: "1"`). The script auto-derives offsets from a unique byte signature so it tolerates Windrose rebuilds — it refuses cleanly if the signature moves. The UI's Idle-CPU card has a runtime override for rollback without redeploying. **Offered as-is with no warranty and no guarantees.** Operators run it against their own SteamCMD-pulled copy; this project does not distribute modified binaries. Applying may conflict with the Windrose EULA or Steam Subscriber Agreement — review the terms before enabling; full risk rests with you.
535+
- **Legacy idle-CPU patch.** Older Windrose dedicated-server builds had an upstream spin-loop bug in `boost::asio::detail::socket_select_interrupter::reset()` that burned ~2 CPU cores while idle (see `memory/windrose_idle_cpu_known_bug.md`). Windrose's April 30, 2026 server update includes an official CPU fix, so current SteamCMD installs should keep `WINDROSE_PATCH_IDLE_CPU=0` (Helm: `patchIdleCpu: "0"`). The optional binary patch remains only for operators running older/pinned server files that still exhibit the historical spin. It injects a `Sleep(1)` into the old hot loop and refuses cleanly if the signature is not present. **Offered as-is with no warranty and no guarantees.** Operators run it against their own SteamCMD-pulled copy; this project does not distribute modified binaries. Applying may conflict with the Windrose EULA or Steam Subscriber Agreement — review the terms before enabling; full risk rests with you.
536536

537-
Sizing floors (validated on DigitalOcean bare-Linux droplets 2026-04-18/19, unpatched):
537+
Historical sizing floors (validated on DigitalOcean bare-Linux droplets 2026-04-18/19 before the official idle-CPU fix):
538538
- **1 vCPU / any RAM**: unplayable. Idle bug pegs the core, no headroom for the P2P handshake; client times out in `UePreloginVerified` and Coturn resets after ~180 s.
539539
- **2 vCPU / 2 GB**: unplayable for a different reason — a delayed ~500 MiB allocation at t≈9 min crashes page reclamation regardless of swap.
540540
- **2 vCPU / 4 GB, fresh world**: marginal. On slower shared vCPUs (typical DO / Hetzner / Linode small tiers) terrain generation saturates both cores during the first-connect handshake; Coturn times out at ~180 s and the client bounces to menu.

‎bare-linux/README.md‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ anywhere the [`Dockerfile`](../Dockerfile) deps are available.
77

88
## Sizing
99

10-
An upstream idle-CPU bug in the dedicated server eats ~1.82 cores
11-
before any player is connected (UE5 task-worker busy-spin, tracked on
12-
the [community thread](https://steamcommunity.com/app/3041230/discussions/0/807974232125564069/)).
13-
The community binary patch at `scripts/patch-idle-cpu.py` (opt in via
14-
`WINDROSE_PATCH_IDLE_CPU=1` in `/etc/windrose/windrose.env`) drops that
15-
to ~5% — if you apply it, a 1 vCPU host becomes viable. Without the
16-
patch the box needs real headroom above the idle floor:
10+
Windrose's April 30, 2026 dedicated-server update includes an official
11+
idle-CPU fix. Current SteamCMD installs should leave
12+
`WINDROSE_PATCH_IDLE_CPU=0`.
13+
14+
The sizing table below is historical, from older server builds where an
15+
upstream idle-CPU bug ate ~1.82 cores before any player connected. The
16+
community binary patch at `scripts/patch-idle-cpu.py` remains available
17+
for older/pinned server files that still show that behavior, but it is
18+
no longer the default recommendation for current SteamCMD installs.
1719

1820
| Box | Verdict |
1921
| ------------------ | ----------------------------------------------------------------------- |

‎bare-linux/install.sh‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,13 @@ if [ "${WINDROSE_PATCH_IDLE_CPU:-}" = "1" ]; then
8888
if [ "${_prev}" != "1" ] && [ "${WINDROSE_PATCH_ACK_RISK:-}" != "1" ]; then
8989
cat >&2 <<'EOF'
9090
91-
[install] ⚠️ You are enabling the idle-CPU binary patch.
91+
[install] ⚠️ You are enabling the legacy idle-CPU binary patch.
9292
93-
This is an EXPERIMENTAL community workaround that modifies the Windrose
94-
dedicated-server binary in place. It is provided AS IS, with NO warranty
95-
of any kind:
93+
Windrose's current SteamCMD server build includes an official CPU fix.
94+
This EXPERIMENTAL community workaround is only for older/pinned server
95+
builds that still show the historical idle spin. It modifies the
96+
Windrose dedicated-server binary in place and is provided AS IS, with
97+
NO warranty of any kind:
9698
9799
* It may break at any time — especially after a Windrose game update.
98100
* It may conflict with the Windrose EULA or Steam Subscriber Agreement.
@@ -355,7 +357,8 @@ DIRECT_CONNECTION_PROXY_ADDRESS=${DIRECT_CONNECTION_PROXY_ADDRESS:-0.0.0.0}
355357
NET_SERVER_MAX_TICK_RATE=${NET_SERVER_MAX_TICK_RATE:-60}
356358
PROTON_USE_XALIA=${PROTON_USE_XALIA:-0}
357359
FILES_WAIT_TIMEOUT_SECONDS=${FILES_WAIT_TIMEOUT_SECONDS:-0}
358-
# Opt in to the idle-CPU binary patch (scripts/patch-idle-cpu.py).
360+
# Legacy idle-CPU binary patch (scripts/patch-idle-cpu.py).
361+
# Current SteamCMD installs should keep this 0.
359362
# "1" -> entrypoint patches the EXE on every start (idempotent).
360363
# The UI Idle-CPU card can flip this per-host without editing this file.
361364
WINDROSE_PATCH_IDLE_CPU=${WINDROSE_PATCH_IDLE_CPU:-0}

‎docker-compose.yaml‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,9 @@ services:
4141
WINDROSE_LAUNCH_STRATEGY: ${WINDROSE_LAUNCH_STRATEGY:-shipping}
4242
FILES_WAIT_TIMEOUT_SECONDS: ${FILES_WAIT_TIMEOUT_SECONDS:-0}
4343
PROTON_USE_XALIA: ${PROTON_USE_XALIA:-0}
44-
# Apply the idle-CPU throttle patch at boot. Off by default because
45-
# the patch targets a specific Windrose build (md5-checked); leaving
46-
# it off means new operators aren't surprised by a no-op after the
47-
# game updates. Operators who want the 97% idle-CPU reduction right
48-
# now on the shipping 0.10.0 build opt in by setting this to 1.
44+
# Legacy idle-CPU workaround for older/pinned server builds before
45+
# Windrose's official CPU fix. Current SteamCMD installs should keep
46+
# this off.
4947
# See scripts/patch-idle-cpu.py for derivation and rollback.
5048
WINDROSE_PATCH_IDLE_CPU: ${WINDROSE_PATCH_IDLE_CPU:-0}
5149
DISPLAY: ":99"

‎helm/windrose/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ the chart is a thin wrapper around those vars plus Kubernetes-level knobs
278278
| `worldConfig.name` | `Default Windrose World` | |
279279
| `worldConfig.presetType` | `Medium` | `Easy` / `Medium` / `Hard` / `Custom`. |
280280
| `protonUseXalia` | `"0"` | Keep off; Xalia crashes on headless Proton. |
281-
| `patchIdleCpu` | `"0"` | **Experimental.** `"1"` enables the idle-CPU binary patch at boot. See [main README § Caveats](../../README.md) for the disclaimer. |
281+
| `patchIdleCpu` | `"0"` | Legacy workaround for older/pinned server builds before Windrose's official idle-CPU fix. Current SteamCMD installs should keep `"0"`. `"1"` enables the experimental binary patch at boot; see [main README § Caveats](../../README.md). |
282282
| `filesWaitTimeoutSeconds` | `"0"` | 0 = wait forever. |
283283
| `xvfb.enabled` | `true` | Keep on; the game container requires a display. |
284284
| `xvfb.display` | `99` | Canary releases should use a different number (:98, :97…) to avoid hostNetwork collisions. |

‎helm/windrose/values.yaml‎

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -266,15 +266,11 @@ worldConfig:
266266
presetType: Medium
267267

268268
protonUseXalia: "0"
269-
# Binary-patch the shipping EXE at boot to throttle the idle-CPU busy-
270-
# loop (boost::asio socket_select_interrupter::reset spinning under
271-
# Wine). When set to "1", the entrypoint runs `patch-idle-cpu.py`
272-
# against the game binary before launch. Drops idle CPU from ~200% to
273-
# ~5%. Safe to leave off for a conservative default — the patch is
274-
# tied to a specific build md5 and is a no-op on unknown builds, but
275-
# operators might still prefer to review the script (in
276-
# scripts/patch-idle-cpu.py) before enabling. See the same file for the
277-
# reverse-engineering rationale and rollback (--revert).
269+
# Legacy workaround for older/pinned server builds before Windrose's
270+
# official idle-CPU fix. Current SteamCMD installs should keep this "0".
271+
# When set to "1", the entrypoint runs `patch-idle-cpu.py` against the
272+
# game binary before launch. Read the disclaimer in the main README and
273+
# scripts/patch-idle-cpu.py before enabling.
278274
patchIdleCpu: "0"
279275
filesWaitTimeoutSeconds: "0"
280276

@@ -286,9 +282,10 @@ resources:
286282
memory: 4Gi
287283
limits:
288284
memory: 16Gi
289-
# Unpatched idle: ~2 cores (upstream spin bug). Mitigate with
290-
# patchIdleCpu: "1" (preferred) or a cgroup cap below. Gameplay
291-
# CPU scales ~125 mcpu per player (sublinear past the first);
285+
# Current SteamCMD builds include Windrose's official idle-CPU fix.
286+
# Older/pinned server builds may still need patchIdleCpu: "1" or a
287+
# cgroup cap below. Gameplay CPU scales ~125 mcpu per player
288+
# (sublinear past the first);
292289
# budget 300m for solo, 500m for 2-4, 1000m for 10. A CPU cap
293290
# is safe to layer on top of the patch as belt-and-braces.
294291
# cpu: 500m

‎scripts/entrypoint.sh‎

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -319,15 +319,14 @@ restore_legacy_sentry_plugin() {
319319
fi
320320
}
321321

322-
# Optional: maintain a PATCHED COPY of the shipping EXE alongside the
323-
# original, and launch that instead of the original. This throttles a
324-
# Boost.Asio drain-loop spin (see scripts/patch-idle-cpu.py for the
325-
# derivation) that otherwise burns ~2 CPU cores when no client is
326-
# connected. Sibling-file approach keeps Steam's manifest-managed
322+
# Optional legacy workaround for older/pinned server builds before
323+
# Windrose's official idle-CPU fix: maintain a PATCHED COPY of the
324+
# shipping EXE alongside the original, and launch that instead of the
325+
# original. The sibling-file approach keeps Steam's manifest-managed
327326
# original untouched — SteamCMD doesn't revert anything because there's
328-
# nothing to revert. On each boot we md5 the original and compare to
329-
# the source md5 recorded alongside the patched sibling; rebuild iff
330-
# the source changed (Windrose update) or the sibling is missing.
327+
# nothing to revert. On each boot we md5 the original and compare to the
328+
# source md5 recorded alongside the patched sibling; rebuild iff the
329+
# source changed (Windrose update) or the sibling is missing.
331330
#
332331
# Off by default. Enable via WINDROSE_PATCH_IDLE_CPU=1. UI override
333332
# via $WINDROSE_PATCH_OVERRIDE_FILE ("disabled" forces OFF, "enabled"
@@ -1001,11 +1000,9 @@ if [ ! -S "/tmp/.X11-unix/X${display_num}" ]; then
10011000
fi
10021001

10031002
# Extra launch args for the game binary. Default caps the engine tick to
1004-
# 60 FPS — the patch (scripts/patch-idle-cpu.py) is the real fix for the
1005-
# unpaced-main-loop spin; this cap is belt-and-braces for hosts that
1006-
# don't run the patch. Matches the Helm chart default so compose /
1007-
# plain-manifest / bare-Linux deployments all behave identically. Set
1008-
# empty to uncap, or to "-FPS=30" for the older behavior.
1003+
# 60 FPS. Matches the Helm chart default so compose / plain-manifest /
1004+
# bare-Linux deployments all behave identically. Set empty to uncap, or
1005+
# to "-FPS=30" for the older behavior.
10091006
: "${SERVER_LAUNCH_ARGS:=-FPS=60}"
10101007
read -r -a launch_args <<< "${SERVER_LAUNCH_ARGS}"
10111008

‎scripts/patch-idle-cpu.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
#!/usr/bin/env python3
22
# SPDX-License-Identifier: MIT
33
"""
4-
Patch `WindroseServer-Win64-Shipping.exe` to throttle the Boost.Asio
5-
`socket_select_interrupter::reset()` drain loop that busy-spins under
6-
Wine/Proton and burns ~2 CPU cores on an idle server.
4+
Legacy patch for older/pinned `WindroseServer-Win64-Shipping.exe` builds
5+
that predate Windrose's official idle-CPU fix. It throttles the
6+
Boost.Asio `socket_select_interrupter::reset()` drain loop that
7+
busy-spins under Wine/Proton and burns ~2 CPU cores on an idle server.
8+
9+
Current SteamCMD server builds should not need this patch.
710
811
Inserts a ~43-byte trampoline that calls `KERNEL32!Sleep(1)` on the
912
loop-continue tail; the loop exits via its normal branch as soon as

‎ui/app.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,9 +1302,11 @@ backupImportBtn.addEventListener("click", async () => {
13021302
});
13031303

13041304
const IDLE_PATCH_DISCLAIMER =
1305-
"You are enabling the EXPERIMENTAL idle-CPU binary patch.\n\n" +
1305+
"You are enabling the legacy EXPERIMENTAL idle-CPU binary patch.\n\n" +
1306+
"Current SteamCMD server builds include Windrose's official CPU fix. " +
1307+
"Only use this for older/pinned builds that still show the historical idle spin.\n\n" +
13061308
"It modifies the Windrose dedicated-server binary in place. NO warranty — " +
1307-
"it may break at any time (especially after a Windrose update), may conflict " +
1309+
"it may break at any time, may conflict " +
13081310
"with the Windrose EULA or Steam Subscriber Agreement, may corrupt saves, " +
13091311
"and the authors do not distribute or authorize redistributing modified " +
13101312
"binaries. Full risk rests with you.\n\n" +

‎ui/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ <h2>Maintenance mode</h2>
363363

364364
<div class="card admin-only hidden" id="idlePatchCard">
365365
<h2>Idle CPU patch</h2>
366-
<p class="muted">Community binary patch that throttles the Boost.Asio drain-loop spin in <code>socket_select_interrupter::reset()</code>. Drops idle CPU from ~200% to ~5% on a canary. Applied by the entrypoint on next container restart when enabled; revertable byte-for-byte. <strong>See <a href="https://github.com/shipstuff/windrose-self-hosted/blob/main/tools/patch-idle-cpu.py#L24" target="_blank" rel="noopener">the script docstring</a> for disclaimer and derivation details.</strong></p>
366+
<p class="muted">Legacy community binary patch for older/pinned server builds that predate Windrose's official idle-CPU fix. Current SteamCMD installs should keep it off. Applied by the entrypoint on next container restart when enabled; revertable byte-for-byte. <strong>See <a href="https://github.com/shipstuff/windrose-self-hosted/blob/main/scripts/patch-idle-cpu.py#L24" target="_blank" rel="noopener">the script docstring</a> for disclaimer and derivation details.</strong></p>
367367
<dl class="kv">
368368
<dt>Binary state</dt><dd id="ipBinaryState">-</dd>
369369
<dt>Binary MD5</dt><dd id="ipBinaryMd5" class="mono">-</dd>

0 commit comments

Comments
 (0)