Skip to content

fix point_template ForceSpawn getting slower until map change - #475

Open
cjujn wants to merge 2 commits into
Source2ZE:devfrom
cjujn:spawngroup-release
Open

cjujn wants to merge 2 commits into
Source2ZE:devfrom
cjujn:spawngroup-release

Conversation

@cjujn

@cjujn cjujn commented Oct 20, 2026

Copy link
Copy Markdown

Summary

Every point_template ForceSpawn gets slower than the one before it, and the cost only resets on map change. This happens even if everything spawned is removed right away. On maps that spawn a lot, such as ZE maps with procedural or repeated spawns (for example ze_diddle extreme and ze_collective), this adds up to a noticeable server hitch within one map.

This PR adds an opt-in cvar, cs2f_fix_forcespawn_slowdown (default 0). With it on, ForceSpawn cost stays flat for the whole map.

Cause

From reverse engineering engine2 and server on the 1.41.9.1 Windows and Linux binaries:

  1. Each ForceSpawn loads a new spawn group. When the spawn finishes, CBaseSpawnGroup merges that group into its owner spawn group. For the default INSERT_INTO_POINT_TEMPLATE_SPAWN_GROUP the owner is the map's spawn group. The merge is the vfunc that logs "%s: Transfering creation sequence %d to owner spawn group %s".
  2. The merge appends the child's IGameResourceManifest list (the CUtlVector at +0x228) to the owner's list.
  3. The owner keeps those manifests until it is destroyed on map change. In its destructor it calls IGameResourceService (+0x220), vfunc 31, on each one. So the map spawn group gains one manifest per ForceSpawn, and per-spawn engine work grows with that list.

I confirmed this by checking the owner's list during a stress run. With the fix off it grows by exactly one per ForceSpawn (111k+ entries after one 10-minute run). With the fix on it stays at the map's own count.

Fix

src/spawngroupfix.cpp hooks CBaseSpawnGroup_MergeIntoOwner:

  • Pre: record the owner's manifest count.
  • Post: shrink the owner's list back to that count first, then destroy the manifests the merge handed over. They are destroyed with the same IGameResourceService call the spawn group destructor uses.

Details:

  • Gamedata: a new signature, CBaseSpawnGroup_MergeIntoOwner (Windows and Linux), and three offsets: CBaseSpawnGroup_ResourceManifests (552), CBaseSpawnGroup_ResourceService (544) and IGameResourceService_DestroyManifest (31). All four have the same values on Windows and Linux.
  • Fails soft: a missing signature or offset only makes the cvar unavailable, with a warning. It does not fail plugin load.
  • Sanity checks: if the vector layout looks wrong or changes unexpectedly during a merge, the feature disables itself.
  • Must be set in cs2fixes.cfg. That file is re-executed on every map load, so a value set from the console is overwritten on the next changelevel.

Results

Test setup: a public repro map, ForceSpawn Stress Test (workshop 3823325930). Chat !stress spawns a template and removes it right away, for 6 ms of every Think, and reports ms per call every 5 s.

Linux dedicated server, 10 min each, external measurement with pidstat

"CPU ms per ForceSpawn" is process CPU time, from pidstat at 10 s intervals, divided by the ForceSpawn calls in the same interval.

linux_pidstat_ab
Metric cs2f_fix_forcespawn_slowdown 0 cs2f_fix_forcespawn_slowdown 1
ForceSpawn calls in 10 min 200,603 385,700
CPU ms per ForceSpawn (pidstat), minute 1 → 10 0.340 → 1.501 (4.41x) 0.232 → 0.231 (1.00x)
Script-timed ms per ForceSpawn, minute 1 → 10 0.710 → 1.810 (2.55x) 0.595 → 0.599 (1.01x)
ForceSpawn calls/s, minute 1 → 10 544 → 225 645 → 641
Average process CPU % 28.0 15.0
Crash no no

AI disclosure

I used Claude Code for the reverse engineering, the code, and polishing this PR description.

cjujn and others added 2 commits October 20, 2026 22:05
Every point_template ForceSpawn loads a new spawn group and merges it into
the owner spawn group (the map's group by default). The merge moves the
child's resource manifests into the owner, which keeps them until map
change, so each ForceSpawn adds one manifest and later ForceSpawns get
slower (0.53 ms -> 1.47 ms per call after 65k calls, nothing left alive).

With the new experimental cvar enabled, the manifests a merge hands over
are destroyed right away, the same way the spawn group destructor does it.
ForceSpawn cost then stays flat (0.49 ms per call after 141k calls).

The hook fails soft: a missing signature or offset only disables the cvar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Name it after the symptom server operators see, matching cs2f_fix_game_bans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant