Prevent lockup when tuning differential pair length

Fix freezes caused excessive branching.  Replaced the recursive
branching tracking with iterative joint mapping and a timeout fail-safe.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/22041

(cherry picked from commit bc37f184bb)
This commit is contained in:
Seth Hillbrand
2026-02-05 23:01:37 -08:00
parent 737e6de4c5
commit c85e130ea5
5 changed files with 141 additions and 37 deletions
+5
View File
@@ -133,6 +133,7 @@ static const wxChar ExcludeFromSimulationLineWidth[] = wxT( "ExcludeFromSimulati
static const wxChar GitIconRefreshInterval[] = wxT( "GitIconRefreshInterval" );
static const wxChar MaxPastedTextLength[] = wxT( "MaxPastedTextLength" );
static const wxChar PNSProcessClusterTimeout[] = wxT( "PNSProcessClusterTimeout" );
static const wxChar FollowBranchTimeout[] = wxT( "FollowBranchTimeoutMs" );
} // namespace KEYS
@@ -322,6 +323,7 @@ ADVANCED_CFG::ADVANCED_CFG()
m_MaxPastedTextLength = 100;
m_PNSProcessClusterTimeout = 100; // Default: 100 ms
m_FollowBranchTimeout = 500; // Default: 500 ms
loadFromConfigFile();
}
@@ -631,6 +633,9 @@ void ADVANCED_CFG::loadSettings( wxConfigBase& aCfg )
configParams.push_back( new PARAM_CFG_INT( true, AC_KEYS::PNSProcessClusterTimeout,
&m_PNSProcessClusterTimeout, 100, 10, 10000 ) );
configParams.push_back( new PARAM_CFG_INT( true, AC_KEYS::FollowBranchTimeout,
&m_FollowBranchTimeout, 500, 50, 5000 ) );
// Special case for trace mask setting...we just grab them and set them immediately
// Because we even use wxLogTrace inside of advanced config
wxString traceMasks;
+12
View File
@@ -805,6 +805,18 @@ public:
*/
int m_PNSProcessClusterTimeout;
/**
* Timeout for the PNS router's followBranch path search, in milliseconds.
*
* This limits how long the router will spend searching for the longest path
* through a complex track topology before returning the best path found so far.
*
* Setting name: "FollowBranchTimeoutMs"
* Valid values: 50 to 5000
* Default value: 500
*/
int m_FollowBranchTimeout;
///@}
private:
+12
View File
@@ -185,6 +185,18 @@ bool DP_MEANDER_PLACER::Move( const VECTOR2I& aP, ITEM* aEndItem )
m_originPair.CP().Split( m_currentStart, aP, preP, tunedP, postP );
m_originPair.CN().Split( m_currentStart, aP, preN, tunedN, postN );
// Bail out early if the tuned sections are empty (issue #22041). This can happen when the
// split points are too close together or outside the line chain.
if( tunedP.PointCount() == 0 || tunedN.PointCount() == 0 )
{
m_finalShapeP = m_originPair.CP();
m_finalShapeN = m_originPair.CN();
m_lastLength = origPathLength();
m_lastStatus = TOO_SHORT;
return false;
}
auto updateStatus =
[&]()
{
+111 -36
View File
@@ -19,6 +19,13 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <wx/log.h>
#include <chrono>
#include <stack>
#include <advanced_config.h>
#include "pns_line.h"
#include "pns_segment.h"
#include "pns_arc.h"
@@ -200,68 +207,136 @@ ITEM* TOPOLOGY::NearestUnconnectedItem( const JOINT* aStart, int* aAnchor, int a
}
TOPOLOGY::PATH_RESULT TOPOLOGY::followBranch( const JOINT* aJoint, LINKED_ITEM* aPrev,
TOPOLOGY::PATH_RESULT TOPOLOGY::followBranch( const JOINT* aStartJoint, LINKED_ITEM* aPrev,
std::set<ITEM*>& aVisited,
bool aFollowLockedSegments )
{
using clock = std::chrono::steady_clock;
PATH_RESULT best;
best.m_end = aStartJoint;
ITEM* via = nullptr;
ITEM_SET links( aJoint->CLinks() );
const int timeoutMs = ADVANCED_CFG::GetCfg().m_FollowBranchTimeout;
auto startTime = clock::now();
for( ITEM* link : links )
// State for iterative DFS: current joint, previous item, accumulated path items,
// accumulated length, and the set of visited joints for this path
struct STATE
{
if( link->OfKind( ITEM::VIA_T ) && !aVisited.contains( link ) )
via = link;
}
const JOINT* joint;
LINKED_ITEM* prev;
ITEM_SET pathItems;
int pathLength;
std::set<const JOINT*> visitedJoints;
ITEM* via;
};
for( ITEM* link : links )
std::stack<STATE> stateStack;
// Initialize with starting state
STATE initial;
initial.joint = aStartJoint;
initial.prev = aPrev;
initial.pathLength = 0;
initial.visitedJoints.insert( aStartJoint );
initial.via = nullptr;
stateStack.push( std::move( initial ) );
while( !stateStack.empty() )
{
if( link->OfKind( ITEM::SEGMENT_T | ITEM::ARC_T )
&& link != aPrev && !aVisited.contains( link ) )
// Check timeout
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
clock::now() - startTime ).count();
if( elapsed > timeoutMs )
{
wxLogTrace( wxT( "PNS_TUNE" ),
wxT( "followBranch: timeout after %lld ms, returning best path found" ),
elapsed );
break;
}
STATE current = std::move( stateStack.top() );
stateStack.pop();
const JOINT* joint = current.joint;
ITEM_SET links( joint->CLinks() );
// Check for via at this joint
ITEM* via = nullptr;
for( ITEM* link : links )
{
if( link->OfKind( ITEM::VIA_T ) && !aVisited.contains( link ) )
{
via = link;
break;
}
}
// Find all unvisited branches from this joint
bool foundBranch = false;
for( ITEM* link : links )
{
if( !link->OfKind( ITEM::SEGMENT_T | ITEM::ARC_T ) )
continue;
if( link == current.prev )
continue;
if( aVisited.contains( link ) )
continue;
LINE l = m_world->AssembleLine( static_cast<LINKED_ITEM*>( link ), nullptr,
false, aFollowLockedSegments );
if( l.CPoint( 0 ) != aJoint->Pos() )
if( l.CPoint( 0 ) != joint->Pos() )
l.Reverse();
const JOINT* next = m_world->FindJoint( l.CPoint( -1 ), &l );
const JOINT* nextJoint = m_world->FindJoint( l.CPoint( -1 ), &l );
for( LINKED_ITEM* ll : l.Links() )
aVisited.insert( ll );
// Skip if we've already visited this joint in the current path
if( current.visitedJoints.count( nextJoint ) )
continue;
foundBranch = true;
// Build new state for this branch
STATE nextState;
nextState.joint = nextJoint;
nextState.prev = l.Links().back();
nextState.pathItems = current.pathItems;
nextState.pathLength = current.pathLength + l.CLine().Length();
nextState.visitedJoints = current.visitedJoints;
nextState.visitedJoints.insert( nextJoint );
nextState.via = via;
// Add via and line to path
if( via )
aVisited.insert( via );
nextState.pathItems.Add( via );
PATH_RESULT sub = followBranch( next, l.Links().back(), aVisited, aFollowLockedSegments );
nextState.pathItems.Add( l );
ITEM_SET tmp;
if( via )
tmp.Add( via );
tmp.Add( l );
for( ITEM* it : sub.m_items )
tmp.Add( it );
stateStack.push( std::move( nextState ) );
}
int len = l.CLine().Length() + sub.m_length;
if( len > best.m_length )
// If no branches found, this is a terminal joint - check if it's the best path
if( !foundBranch )
{
if( current.pathLength > best.m_length )
{
best.m_length = len;
best.m_end = sub.m_end;
best.m_items = tmp;
best.m_length = current.pathLength;
best.m_end = joint;
best.m_items = current.pathItems;
}
for( LINKED_ITEM* ll : l.Links() )
aVisited.erase( ll );
if( via )
aVisited.erase( via );
}
}
if( !best.m_end )
best.m_end = aJoint;
wxLogTrace( wxT( "PNS_TUNE" ),
wxT( "followBranch: completed with best path length=%d, %d items" ),
best.m_length, best.m_items.Size() );
return best;
}
+1 -1
View File
@@ -113,7 +113,7 @@ private:
PATH_RESULT() : m_end( nullptr ), m_length( 0 ) {}
};
PATH_RESULT followBranch( const JOINT* aJoint, LINKED_ITEM* aPrev,
PATH_RESULT followBranch( const JOINT* aStartJoint, LINKED_ITEM* aPrev,
std::set<ITEM*>& aVisited, bool aFollowLockedSegments );
ITEM_SET followTrivialPath( LINE* aLine, const JOINT** aTerminalJointA,