From b7630a040a1d6db3b2ad951a41ed362f1064ad6c Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:55:37 +0000 Subject: [PATCH 1/2] perf(pathfinder): Add PathfindCell::getTotalCostDifference(), ::getPrevOpen() and extend PathfindCellList with tail tracking and PathfindCellList::canReverseSort() (#2450) --- .../GameEngine/Include/GameLogic/AIPathfind.h | 14 ++++++++++-- .../Source/GameLogic/AI/AIPathfind.cpp | 22 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/Core/GameEngine/Include/GameLogic/AIPathfind.h b/Core/GameEngine/Include/GameLogic/AIPathfind.h index 3f65888391b..25326f7c6fd 100644 --- a/Core/GameEngine/Include/GameLogic/AIPathfind.h +++ b/Core/GameEngine/Include/GameLogic/AIPathfind.h @@ -254,16 +254,23 @@ class PathfindCellList friend class PathfindCell; public: - PathfindCellList() : m_head(nullptr) {} + PathfindCellList() : m_head(nullptr), m_tail(nullptr) {} - void reset(PathfindCell* newHead = nullptr) { m_head = newHead; } +#if RETAIL_COMPATIBLE_PATHFINDING + void reset(PathfindCell* newHead = nullptr) { m_head = newHead; m_tail = nullptr; } +#else + void reset() { m_head = nullptr; m_tail = nullptr; } +#endif PathfindCell* getHead() const { return m_head; } Bool empty() const { return m_head == nullptr; } + Bool canReverseSort(PathfindCell& currentCell) const; + private: PathfindCell* m_head; + PathfindCell* m_tail; }; /** @@ -353,6 +360,7 @@ class PathfindCell static Int releaseOpenList( PathfindCellList &list ); inline PathfindCell *getNextOpen() {return m_info->m_nextOpen?m_info->m_nextOpen->m_cell: nullptr;} + inline PathfindCell *getPrevOpen() {return m_info->m_prevOpen?m_info->m_prevOpen->m_cell: nullptr;} inline UnsignedShort getXIndex() const {return m_info->m_pos.x;} inline UnsignedShort getYIndex() const {return m_info->m_pos.y;} @@ -365,6 +373,8 @@ class PathfindCell inline UnsignedInt getCostSoFar() const {return m_info->m_costSoFar;} inline UnsignedInt getTotalCost() const {return m_info->m_totalCost;} + inline UnsignedInt getTotalCostDifference(PathfindCell& other) const; + inline void setCostSoFar(UnsignedInt cost) { if( m_info ) m_info->m_costSoFar = cost;} inline void setTotalCost(UnsignedInt cost) { if( m_info ) m_info->m_totalCost = cost;} diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index 0da5bc24120..47e7bb62a9f 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -1235,6 +1235,16 @@ void PathfindCellInfo::releaseACellInfo(PathfindCellInfo *theInfo) //----------------------------------------------------------------------------------- +Bool PathfindCellList::canReverseSort(PathfindCell& currentCell) const +{ + if (m_head && m_tail) + return m_head->getTotalCostDifference(currentCell) > m_tail->getTotalCostDifference(currentCell); + + return false; +} + +//----------------------------------------------------------------------------------- + /** * Constructor */ @@ -1338,6 +1348,18 @@ inline void PathfindCell::setBlockedByAlly(Bool blocked) #endif } +/** + * Determine absolute total path cost difference between two cells. + * Returns UINT_MAX if used with an uninitialised cell, so will be sorted as maximally dissimilar. + */ +inline UnsignedInt PathfindCell::getTotalCostDifference(PathfindCell& other) const +{ + if (m_info && other.m_info) + return abs(m_info->m_totalCost - other.m_info->m_totalCost); + + return UINT_MAX; +} + /** * Set the parent pointer. */ From c88b44e4755b0da26635546a33d2debd1c94c494 Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:57:42 +0000 Subject: [PATCH 2/2] perf(pathfinder): Extend PathfindCell::putOnSortedOpenList() with conditional reverse insertion sorting (#2450) --- .../GameEngine/Include/GameLogic/AIPathfind.h | 3 + .../Source/GameLogic/AI/AIPathfind.cpp | 72 +++++++++++++++++-- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/Core/GameEngine/Include/GameLogic/AIPathfind.h b/Core/GameEngine/Include/GameLogic/AIPathfind.h index 25326f7c6fd..efc60330b28 100644 --- a/Core/GameEngine/Include/GameLogic/AIPathfind.h +++ b/Core/GameEngine/Include/GameLogic/AIPathfind.h @@ -341,6 +341,9 @@ class PathfindCell // Forward insertion sort, in ascending cost order void forwardInsertionSort(PathfindCellList& list); + // Reverse insertion sort, in ascending cost order + void reverseInsertionSort(PathfindCellList& list); + /// put self on "open" list in ascending cost order void putOnSortedOpenList( PathfindCellList &list ); diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index 47e7bb62a9f..d8a934ecf51 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -1355,7 +1355,7 @@ inline void PathfindCell::setBlockedByAlly(Bool blocked) inline UnsignedInt PathfindCell::getTotalCostDifference(PathfindCell& other) const { if (m_info && other.m_info) - return abs(m_info->m_totalCost - other.m_info->m_totalCost); + return abs((Int)m_info->m_totalCost - (Int)other.m_info->m_totalCost); return UINT_MAX; } @@ -1756,7 +1756,7 @@ void PathfindCell::forwardInsertionSortRetailCompatible(PathfindCellList& list) } #endif -// Forward insertion sort, returns early if the list is being initialised or we are prepending the list +// Forward insertion sort, returns early if the list is being initialized or we are prepending the list void PathfindCell::forwardInsertionSort(PathfindCellList& list) { DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); @@ -1770,6 +1770,7 @@ void PathfindCell::forwardInsertionSort(PathfindCellList& list) m_info->m_prevOpen = nullptr; m_info->m_nextOpen = nullptr; list.m_head = this; + list.m_tail = this; return; } @@ -1793,10 +1794,60 @@ void PathfindCell::forwardInsertionSort(PathfindCellList& list) if (current->m_info->m_nextOpen != nullptr) { current->m_info->m_nextOpen->m_prevOpen = this->m_info; } + else { + list.m_tail = this; + } + current->m_info->m_nextOpen = this->m_info; m_info->m_prevOpen = current->m_info; } +// Reverse insertion sort, returns early if the list is being initialized or we are appending the list +void PathfindCell::reverseInsertionSort(PathfindCellList& list) +{ + DEBUG_ASSERTCRASH(m_info, ("Has to have info.")); + DEBUG_ASSERTCRASH(m_info->m_closed == FALSE && m_info->m_open == FALSE, ("Serious error - Invalid flags. jba")); + + // mark the new cell as being on the open list + m_info->m_open = true; + m_info->m_closed = false; + + if (list.m_tail == nullptr) { + m_info->m_prevOpen = nullptr; + m_info->m_nextOpen = nullptr; + list.m_tail = this; + list.m_head = this; + return; + } + + // If the node needs inserting after the current list tail + if (m_info->m_totalCost >= list.m_tail->m_info->m_totalCost) { + m_info->m_prevOpen = list.m_tail->m_info; + list.m_tail->m_info->m_nextOpen = this->m_info; + m_info->m_nextOpen = nullptr; + list.m_tail = this; + return; + } + + // Traverse the list to find correct position + PathfindCell* current = list.m_tail; + while (current->m_info->m_prevOpen && current->m_info->m_prevOpen->m_totalCost > m_info->m_totalCost) { + current = current->getPrevOpen(); + } + + // Insert the new node in the correct position + m_info->m_prevOpen = current->m_info->m_prevOpen; + if (current->m_info->m_prevOpen != nullptr) { + current->m_info->m_prevOpen->m_nextOpen = this->m_info; + } + else { + list.m_head = this; + } + + current->m_info->m_prevOpen = this->m_info; + m_info->m_nextOpen = current->m_info; +} + /// put self on "open" list in ascending cost order, return new list void PathfindCell::putOnSortedOpenList( PathfindCellList &list ) { @@ -1807,7 +1858,15 @@ void PathfindCell::putOnSortedOpenList( PathfindCellList &list ) } #endif - forwardInsertionSort(list); + // TheSuperHackers @performance Mauller 20/03/2026 Implement reverse insertion sorting. + // Long and complex paths often append PathfindCell's, with high total path costs, to the open list. + // Appending and reverse traversal allow faster insertion of these cells, reducing pathfinding overhead by 50 - 66%. + if (list.canReverseSort(*this)) { + reverseInsertionSort(list); + } + else { + forwardInsertionSort(list); + } } /// remove self from "open" list @@ -1817,6 +1876,9 @@ void PathfindCell::removeFromOpenList( PathfindCellList &list ) DEBUG_ASSERTCRASH(m_info->m_closed==FALSE && m_info->m_open==TRUE, ("Serious error - Invalid flags. jba")); if (m_info->m_nextOpen) m_info->m_nextOpen->m_prevOpen = m_info->m_prevOpen; + else { + list.m_tail = getPrevOpen(); + } if (m_info->m_prevOpen) m_info->m_prevOpen->m_nextOpen = m_info->m_nextOpen; @@ -1854,7 +1916,7 @@ Int PathfindCell::releaseOpenList( PathfindCellList &list ) if (curInfo->m_nextOpen) { list.m_head = curInfo->m_nextOpen->m_cell; } else { - list.m_head = nullptr; + list.reset(); } DEBUG_ASSERTCRASH(cur == curInfo->m_cell, ("Bad backpointer in PathfindCellInfo")); curInfo->m_nextOpen = nullptr; @@ -1889,7 +1951,7 @@ Int PathfindCell::releaseClosedList( PathfindCellList &list ) if (curInfo->m_nextOpen) { list.m_head = curInfo->m_nextOpen->m_cell; } else { - list.m_head = nullptr; + list.reset(); } DEBUG_ASSERTCRASH(cur == curInfo->m_cell, ("Bad backpointer in PathfindCellInfo")); curInfo->m_nextOpen = nullptr;