Skip to content

perf(pathfinder): Optimize pathfind snippets for better performance - #3198

Merged
xezon merged 3 commits into
TheSuperHackers:mainfrom
Skyaero42:skyaero/optimize-pathfind
Sep 15, 2026
Merged

xezon merged 3 commits into
TheSuperHackers:mainfrom
Skyaero42:skyaero/optimize-pathfind

Conversation

@Skyaero42

@Skyaero42 Skyaero42 commented Aug 25, 2026

Copy link
Copy Markdown

This PR optimizes three code snippets in the pathfinding algorithm to improve its performance. In end-games like FFA's with high number of units, pathfinding can take up to 80% of all CPU time.

For convenience, each snippet optimization is its own commit. PR can be merged by either squash or rebase.

Commits

refactor(pathfind): Optimize appending node to end of the path.
PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed pathNode::appendToList() as it is not used anywhere else.

refactor(pathfind): Take parents cell's position outside of for-loop for optimization.
The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.

refactor(pathfind): Remove redundant isCrusher recomputation for optimization.
The parameter isCrusher is calculated in the ExamineCellsStruct and then recalculated in theexamineCellsCallback. By caching the result in the ExamineCellStruct it reduced the number of evaluations needed.

Performance

VS's performance analyser was used.
The appending node commit reduced 3.1% in absolute CPU time for pathfinding.
The parent's cells position and isCrusher optimizations combined reduced 1.2% in absolute CPU time for pathfinding.

Testing

This PR has been tested against 50 normal replays and 1 replay with the pathfind failover activated.

Disclaimer

This PR and its description was fully made by a human.

Skyaero42 added a commit to Skyaero42/GeneralsGameCode that referenced this pull request Aug 25, 2026
…perHackers#3198)

PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
Skyaero42 added a commit to Skyaero42/GeneralsGameCode that referenced this pull request Aug 25, 2026
…for optimization (TheSuperHackers#3198)

The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
Skyaero42 added a commit to Skyaero42/GeneralsGameCode that referenced this pull request Aug 25, 2026
@Skyaero42
Skyaero42 force-pushed the skyaero/optimize-pathfind branch from 7df663c to c95c628 Compare August 25, 2026 06:36
@Skyaero42 Skyaero42 self-assigned this Aug 25, 2026
@Skyaero42 Skyaero42 added Performance Is a performance concern Refactor Edits the code with insignificant behavior changes, is never user facing labels Aug 25, 2026
@Skyaero42
Skyaero42 marked this pull request as ready for review August 25, 2026 07:00
@qodo-free-for-open-source-projects

This comment was marked as low quality.

@qodo-free-for-open-source-projects

This comment was marked as low quality.

Comment thread Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp
Comment thread Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp
Comment thread Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp
Comment thread Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp Outdated
Skyaero42 added a commit to Skyaero42/GeneralsGameCode that referenced this pull request Sep 14, 2026
…perHackers#3198)

PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
Skyaero42 added a commit to Skyaero42/GeneralsGameCode that referenced this pull request Sep 14, 2026
…for optimization (TheSuperHackers#3198)

The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
Skyaero42 added a commit to Skyaero42/GeneralsGameCode that referenced this pull request Sep 14, 2026
@Skyaero42
Skyaero42 force-pushed the skyaero/optimize-pathfind branch from c95c628 to 12e8fd4 Compare September 14, 2026 11:24
@Skyaero42
Skyaero42 requested a review from Mauller September 14, 2026 11:25
@greptile-apps

greptile-apps Bot commented Sep 14, 2026

Copy link
Copy Markdown

Greptile Summary

This PR optimizes pathfinding by making path appends constant-time and caching loop-invariant movement calculations.

  • Appends new path nodes through the tracked tail instead of traversing the list.
  • Caches crusher capability in ExamineCellsStruct.
  • Computes the parent cell’s world position once before examining neighbors.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
Core/GameEngine/Include/GameLogic/AIPathfind.h Removes the obsolete PathNode::appendToList declaration after replacing its only use.
Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp Implements constant-time tail appends and reuses previously computed pathfinding values; the previously malformed Path::prependNode definition is now valid.

Reviews (4): Last reviewed commit: "perf(pathfinder): Remove redundant isCru..." | Re-trigger Greptile

Comment thread Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp Outdated
Skyaero42 added a commit to Skyaero42/GeneralsGameCode that referenced this pull request Sep 14, 2026
…perHackers#3198)

PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
Skyaero42 added a commit to Skyaero42/GeneralsGameCode that referenced this pull request Sep 14, 2026
…for optimization (TheSuperHackers#3198)

The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
Skyaero42 added a commit to Skyaero42/GeneralsGameCode that referenced this pull request Sep 14, 2026
@Skyaero42
Skyaero42 force-pushed the skyaero/optimize-pathfind branch from 12e8fd4 to fc87c74 Compare September 14, 2026 11:31

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

Comment thread Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp
@Caball009

Copy link
Copy Markdown

Seems like a good idea to run this against a large number of replays before merging.

Skyaero42 added a commit to Skyaero42/GeneralsGameCode that referenced this pull request Sep 14, 2026
…perHackers#3198)

PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
Skyaero42 added a commit to Skyaero42/GeneralsGameCode that referenced this pull request Sep 14, 2026
…for optimization (TheSuperHackers#3198)

The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
Skyaero42 added a commit to Skyaero42/GeneralsGameCode that referenced this pull request Sep 14, 2026
@Skyaero42
Skyaero42 force-pushed the skyaero/optimize-pathfind branch from fc87c74 to 65c9a50 Compare September 14, 2026 18:45
Comment thread Core/GameEngine/Include/GameLogic/AIPathfind.h

@Mauller Mauller left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just needs commit titles updating

…rHackers#3198)

PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
…r optimization (TheSuperHackers#3198)

The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
@Skyaero42
Skyaero42 force-pushed the skyaero/optimize-pathfind branch from 65c9a50 to 9e7c160 Compare September 15, 2026 10:11
@Skyaero42 Skyaero42 changed the title refactor(pathfind): Optimize pathfind snippets for higher performance perf(pathfinder): Optimize pathfind snippets for higher performance Sep 15, 2026
@Skyaero42 Skyaero42 changed the title perf(pathfinder): Optimize pathfind snippets for higher performance perf(pathfinder): Optimize pathfind snippets for better performance Sep 15, 2026
@Skyaero42

Skyaero42 commented Sep 15, 2026

Copy link
Copy Markdown
Author

Seems like a good idea to run this against a large number of replays before merging.

Checking now

Update 20260915 1541: looks like a mismatch was introduced between 5th and 12th September release in Generals - which hampers running large amount replays for this PR - Currently investigating.

ZH is clear (1500 replays, no mismatches)

@Skyaero42

Copy link
Copy Markdown
Author

Replay check complete. No issues by this PR.

@xezon xezon added Major Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour labels Sep 15, 2026
@xezon
xezon merged commit 288a3ea into TheSuperHackers:main Sep 15, 2026
23 checks passed
xezon pushed a commit that referenced this pull request Sep 15, 2026
PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
xezon pushed a commit that referenced this pull request Sep 15, 2026
…r optimization (#3198)

The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
@Mauller

Mauller commented Sep 15, 2026

Copy link
Copy Markdown

Seems like a good idea to run this against a large number of replays before merging.

Checking now

Update 20260915 1541: looks like a mismatch was introduced between 5th and 12th September release in Generals - which hampers running large amount replays for this PR - Currently investigating.

ZH is clear (1500 replays, no mismatches)

The generals problem is okay, #3296 is there to help fix one of the mismatch issues. But there is also a mismatch introduced by #2793.

fbraz3 added a commit to fbraz3/GeneralsX that referenced this pull request Sep 16, 2026
* bugfix(gamewindow): Remove destroyed windows from the modal stack and prevent duplicate modals for the same window (TheSuperHackers#3224)

* feat(commandline): Add working directory command line options (TheSuperHackers#3149)

Append -useCwd to apply the startup working directory, -setCwd "path" to apply a custom working directory, otherwise it falls back to the default executable working directory

* bugfix(neutronmissile): Fix and improve Nuke Missile damage for large objects inside the outer blast radius (TheSuperHackers#3161)

* bugfix(dozeraiupdate): Fix issue where builders could resume completed tasks after being disabled (TheSuperHackers#2793)

* refactor(milesaudiomanager): Use consistent variable names for PlayingAudio in MilesAudioManager (TheSuperHackers#3254)

* refactor(milesaudiomanager): Simplify MilesAudioManager::notifyOfAudioCompletion() (TheSuperHackers#3254)

* refactor(milesaudiomanager): Simplify MilesAudioManager::findLowestPrioritySound() (TheSuperHackers#3254)

* bugfix(milesaudiomanager): Fix premature 2d and 3d sound cancellations from MilesAudioManager::stopAudioEvent() (TheSuperHackers#3254)

* bugfix(milesaudiomanager): No longer use stopped audio in queries and updates (TheSuperHackers#3254)

* refactor(bink): Replace the Bink SDK stub with a Bink runtime loader (TheSuperHackers#3272)

The Bink SDK stub was linked as an import library, so binkw32.dll had to be
resolvable while the process image was still loading, long before WinMain and
therefore long before the command line was parsed. That is why -setCwd could not
point a build at a retail installation: the working directory it selects is set
far too late to influence how the library is found.

BinkLoader loads binkw32.dll explicitly once BinkVideoPlayer is initialized, at
which point the working directory is final. The Bink functions declared in bink.h
are now ordinary functions that forward to the matching export of the loaded
module, so no call site changes. An unresolved function returns the same neutral
value the stub library returned, which means a missing binkw32.dll disables video
playback instead of preventing the game from starting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(miles): Replace the Miles SDK stub with a Miles runtime loader (TheSuperHackers#3272)

The Miles SDK stub was linked as an import library, so mss32.dll had to be
resolvable while the process image was still loading, long before WinMain and
therefore long before the command line was parsed. That is why -setCwd could not
point a build at a retail installation: the working directory it selects is set
far too late to influence how the library is found.

MilesLoader loads mss32.dll explicitly once the audio device is opened, at which
point the working directory is final. The Miles functions declared in mss/mss.h
are now ordinary functions that forward to the matching export of the loaded
module, so no call site changes. An unresolved function returns the same neutral
value the stub library returned, which means a missing mss32.dll turns audio off
instead of preventing the game from starting.

Nine declarations were dropped along the way, because the retail mss32.dll does
not export them and nothing has called them since they were replaced by their
volume_pan counterparts: AIL_sample_volume, AIL_set_sample_volume, AIL_sample_pan,
AIL_set_sample_pan and the four stream equivalents, plus AIL_open_stream_by_sample.
The MSS_auto_cleanup hook was dropped as well, because its atexit handler would
have called AIL_shutdown after the module was already freed. All 92 remaining
exports were verified to resolve against the retail mss32.dll.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(miles): Fix the written primitive types in mss.h and all its call sites; no ABI changes (TheSuperHackers#3272)

* perf(productionupdate): Simplify and correct implementations of cancel functions in ProductionUpdate (TheSuperHackers#3270)

* chore(gamememory): Compile out the memory link tester in Release (TheSuperHackers#3266)

* perf(gamememory): Early exit delete and free functions on null pointer (TheSuperHackers#3266)

* perf(gamememory): Inline preMainInitMemoryManager (TheSuperHackers#3266)

* perf(gamememory): Add overloads for the deletes with size_t argument (TheSuperHackers#3266)

* chore(gamememory): Remove superfluous extern keywords from operator overloads (TheSuperHackers#3266)

* perf(gamememory): Remove unnecessary calls to preMainInitMemoryManager from delete and free functions and make freeBytes noexcept to get rid of EH frame (TheSuperHackers#3266)

* fix(gamefont): Ceil font glyph buffer size to the actual glyph size to prevent a buffer write overflow (TheSuperHackers#3268)

* refactor(particlesys): Parse IsGroundAligned as an enum instead of a boolean (TheSuperHackers#3265)

* ci(release): Stop requesting permissions from the reusable workflow (TheSuperHackers#3276)

* refactor(basetype): Add utility functions to Region and Coord types (TheSuperHackers#3271)

New functions are:
intersectWith, uniteWith for IRegion3D, IRegion2D, Region3D, Region2D
updateMin, updateMax for ICoord3D, ICoord2D, Coord3D, Coord2D
asICoord2D, asCoord2D for ICoord3D, Coord3D

* build(cmake): Add retail compatibility option in CMake config (TheSuperHackers#2379)

RTS_BUILD_OPTION_RETAIL_COMPATIBLE_GAME=DEFAULT/ON/OFF

* bugfix(meshmatdesc): Fix mesh material color processing (TheSuperHackers#3246)

* ci: Restore CI workflow permission compatibility (TheSuperHackers#3286)

* chore: Remove trailing commas in braced initializers that break clang-format's compact layout (TheSuperHackers#3274)

Scoped to comment-free array/struct literals (BorderColors, TeamGeneric,
BezierSegment, GameMemoryInitPools, BFISH, Properties, Scripts) where
clang-format explodes each element onto its own line without this.

* chore(license): Add SPDX-License-Identifier to LICENSE.md (TheSuperHackers#3290)

Helps github detect the license version

* bugfix(pathfinder): Restore Generals retail compatibility after crash fix changes to Pathfinder::findAttackPath (TheSuperHackers#3289)

* feat(recorder): Play a replay file from the command line (TheSuperHackers#3227)

Use -loadreplay <file> as a command line argument to load the replay with full game context

* fix(audio): Copy SoundSceneObjClass state safely (TheSuperHackers#3247)

* fix(hash): Fix initialization of HashTableIteratorClass and make it work with an empty HashTableClass (TheSuperHackers#3284)

* chore(pathfinder): Remove superfluous CPOP_STARTS_FROM_PREV_SEG macro (TheSuperHackers#3295)

* fix(milesaudiomanager): Prevent heap-buffer-overflow read in MilesAudioManager::selectProvider() (TheSuperHackers#3281)

* bugfix(filesystem): Preserve write paths with missing directories (TheSuperHackers#3104)

* perf(pathfinder): Optimize appending node to end of the path (TheSuperHackers#3198)

PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.

* perf(pathfinder): Take parents cell's position outside of for-loop for optimization (TheSuperHackers#3198)

The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.

* perf(pathfinder): Remove redundant isCrusher recomputation for optimization (TheSuperHackers#3198)

* ci(windows): make bink and miles runtime stubs optional in build artifacts

* fix(platform): preserve POSIX startup working directory and set Flatpak asset paths

* docs(worklog): document CI fixes and verification for upstream sync PR 304

---------

Co-authored-by: ArcticDolphin <5984296+tintinhamans@users.noreply.github.com>
Co-authored-by: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com>
Co-authored-by: xezon <4720891+xezon@users.noreply.github.com>
Co-authored-by: Stubbjax <11547761+Stubbjax@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: stm <14291421+stephanmeesters@users.noreply.github.com>
Co-authored-by: mirelle7 <115191165+mirelle7@users.noreply.github.com>
Co-authored-by: Caball009 <82909616+Caball009@users.noreply.github.com>
Co-authored-by: Bobby Battista <bobtista@gmail.com>
Co-authored-by: SkyAero <21192585+Skyaero42@users.noreply.github.com>
gamezerve pushed a commit to gamezerve/Reborn-Omega that referenced this pull request Sep 17, 2026
…rHackers#3198)

PathNode::appendToList() walks the entire list from the head to find the tail on every call, making repeated appendNode() calls O(n^2) in path length. Path already tracks m_pathTail, so append directly onto it in O(1) instead. Removed PathNode::appendToList() as it is not used anywhere else.
gamezerve pushed a commit to gamezerve/Reborn-Omega that referenced this pull request Sep 17, 2026
…r optimization (TheSuperHackers#3198)

The parent cell's world position fromPos never changes across the neighbour loop, so compute it once instead.
gamezerve pushed a commit to gamezerve/Reborn-Omega that referenced this pull request Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Gen Relates to Generals Major Severity: Minor < Major < Critical < Blocker Performance Is a performance concern Refactor Edits the code with insignificant behavior changes, is never user facing ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants