diff --git a/include/priority_thread_pool_task.h b/include/priority_thread_pool_task.h new file mode 100644 index 0000000000..5cc62ea891 --- /dev/null +++ b/include/priority_thread_pool_task.h @@ -0,0 +1,131 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#pragma once + +#include + + +/** + * A helper class to execute tasks on a thread pool in priority order, with progress reporting. + */ +template +class PRIORITY_THREAD_POOL_TASK +{ +public: + using ItemT = typename ContainerT::value_type; + + PRIORITY_THREAD_POOL_TASK() : + m_reporter( nullptr ), + m_highestPriority( BS::pr::high ), + m_reporterInterval( std::chrono::milliseconds( 250 ) ) + { + } + + void SetReporter( PROGRESS_REPORTER* aReporter ) { m_reporter = aReporter; } + + /** + * Call this to execute the task on all items in aItems, using the thread pool + * and dispatching the tasks in order of descending priority as determined by + * comparePriority() implemented by the derived class. + */ + void Execute( ContainerT& aItems ) + { + thread_pool& tp = GetKiCadThreadPool(); + std::vector> returns; + + // Compute priority keys paired to item indices + using IndexedPriority = std::pair; + std::vector indexedKeys( aItems.size() ); + for( size_t i = 0; i < aItems.size(); ++i ) + { + indexedKeys[i] = { i, computePriorityKey( aItems[i] ) }; + } + + // Sort by descending priority key + std::sort( indexedKeys.begin(), indexedKeys.end(), + []( const IndexedPriority& a, const IndexedPriority& b ) + { + return a.second > b.second; + } ); + + // Dispatch largest first + const size_t numItems = aItems.size(); + for( size_t priorityRank = 0; priorityRank < numItems; ++priorityRank ) + { + const size_t itemIndex = indexedKeys[priorityRank].first; + ItemT& item = aItems[itemIndex]; + + // Earlier ranking -> higher key -> should be higher priority + const size_t priority = ( ( numItems - priorityRank - 1 ) * m_highestPriority ) / numItems; + + returns.emplace_back( tp.submit_task( + [this, &item] + { + return task( item ); + }, + priority ) ); + } + + for( const std::future& ret : returns ) + { + std::future_status status = ret.wait_for( m_reporterInterval ); + + while( status != std::future_status::ready ) + { + if( m_reporter ) + m_reporter->KeepRefreshing(); + + status = ret.wait_for( m_reporterInterval ); + } + } + } + +protected: + PROGRESS_REPORTER* m_reporter; + +private: + /** + * Implement this to compute a priority key for an item. + * + * Return a number representing priority, where a higher number means higher priority. + * The actual values returned don't matter, only their relative order. + * + * (A relational a < b comparator would work too, but a unary key lets us compute it + * once per item in O(n) and then sort indices cheaply, rather than calling it O(n log n) + * times inside std::sort.) + * + * If you'd like to test the effect of this priority ordering, you + * can return a constant value to disable sorting, or return the inverse + * to sort backwards. + */ + virtual int computePriorityKey( const ItemT& aItem ) const = 0; + + /** + * Process one item in the thread pool. Return the number of items processed. + */ + virtual size_t task( ItemT& item ) = 0; + + BS::priority_t m_highestPriority; + std::chrono::milliseconds m_reporterInterval; +}; diff --git a/pcbnew/pcb_io/allegro/allegro_builder.cpp b/pcbnew/pcb_io/allegro/allegro_builder.cpp index cc1e6d0be1..6dd833a29e 100644 --- a/pcbnew/pcb_io/allegro/allegro_builder.cpp +++ b/pcbnew/pcb_io/allegro/allegro_builder.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include #include @@ -3834,8 +3835,99 @@ static LSET getRuleAreaLayers( const LAYER_INFO& aLayerInfo, PCB_LAYER_ID aDefau } +/** + * Filled zones have their own outline and the fill itself comes from + * a bunch of "related" spaces. To convert this to a KiCad-ish ZONE, + * we need to chop out only the bit of the wider filled zone that applies + * to the outline (i.e. intersection). + * + * Then that fill has to be fractured. + * + * This is all repeated for each layer's separated filled areas. + * + * This takes ages, so this class handles the information you need to collect to do that + * later on in a thread pool, and does that. + */ +class BOARD_BUILDER::ZONE_FILL_HANDLER +{ +public: + /** + * This is all the info needed to do the fill of one layer of one zone. + */ + struct FILL_INFO + { + ZONE* m_Zone; + PCB_LAYER_ID m_Layer; + /// The wider filled area we will chop a piece out of for this layer of this zone + SHAPE_POLY_SET m_CombinedFill; + }; + + /** + * Priority task dispatcher for zone fills - we want to do the biggest ones first. + * + * On average, sorting the largest zones first is slightly faster (5-10%) on large boards. + * + * However, if you allow the large zones to be assigned at random, as they basically will be + * if they are handled later in the process, there's a chance the very biggest zones will end up + * consecutive in the same thread which could be a substantial penalty. + */ + class COMPLEX_FIRST_FILL_TASK: public PRIORITY_THREAD_POOL_TASK> + { + public: + COMPLEX_FIRST_FILL_TASK( bool aSimplify ) : m_simplify( aSimplify ) {} + + private: + int computePriorityKey( const FILL_INFO& a ) const override + { + return static_cast( a.m_CombinedFill.TotalVertices() ); + } + + size_t task( FILL_INFO& fillInfo ) override + { + SHAPE_POLY_SET finalFillPolys = *fillInfo.m_Zone->Outline(); + + finalFillPolys.ClearArcs(); + fillInfo.m_CombinedFill.ClearArcs(); + + // Intersect the zone outline with the combined fill that was assembled + // from all the related objects. + finalFillPolys.BooleanIntersection( fillInfo.m_CombinedFill ); + finalFillPolys.Fracture( m_simplify ); + + // This is already mutex-ed, so this is safe + fillInfo.m_Zone->SetFilledPolysList( fillInfo.m_Layer, finalFillPolys ); + return 1; + } + + bool m_simplify; + }; + + /** + * Process the polygons in a thread pool for more fans, more faster + */ + void ProcessPolygons( bool aSimplify ) + { + PROF_TIMER timer( "Zone fill processing" ); + + COMPLEX_FIRST_FILL_TASK fillTask( aSimplify ); + fillTask.Execute( m_FillInfos ); + + wxLogTrace( traceAllegroPerf, wxT( " Intersected and fractured zone fills in %.3f ms" ), timer.msecs() ); // format:allow + } + + void QueuePolygonForZone( ZONE& aZone, SHAPE_POLY_SET aFilledArea, PCB_LAYER_ID aLayer ) + { + m_FillInfos.emplace_back( &aZone, aLayer, std::move( aFilledArea ) ); + } + +private: + std::vector m_FillInfos; +}; + + std::unique_ptr BOARD_BUILDER::buildZone( const BLOCK_BASE& aBoundaryBlock, - const std::vector& aRelatedBlocks ) + const std::vector& aRelatedBlocks, + ZONE_FILL_HANDLER& aZoneFillHandler ) { int netCode = NETINFO_LIST::UNCONNECTED; const LAYER_INFO layerInfo = expectLayerFromBlock( aBoundaryBlock ); @@ -3927,7 +4019,7 @@ std::unique_ptr BOARD_BUILDER::buildZone( const BLOCK_BASE& { case 0x1B: { - auto it = m_netCache.find( block->GetKey() ); + const auto it = m_netCache.find( block->GetKey() ); if( it != m_netCache.end() ) { @@ -3950,7 +4042,6 @@ std::unique_ptr BOARD_BUILDER::buildZone( const BLOCK_BASE& SHAPE_POLY_SET fillPolySet = shapeToPolySet( shapeData ); combinedFill.Append( fillPolySet ); - m_usedZoneFillShapes.emplace( block->GetKey() ); break; } @@ -3968,18 +4059,23 @@ std::unique_ptr BOARD_BUILDER::buildZone( const BLOCK_BASE& // Add zone fills if( isCopperZone && !combinedFill.IsEmpty() ) { - SHAPE_POLY_SET zoneOutline = *zone->Outline(); + // We don't do this here, though it feels like we should. We collect the + // information for batch processing later on (which is conceptually + // easier to parallelise compared to this function). But there is room + // for improvement by threading more of this work: shapeToPolySet + // accounts for about 40% of the remaining single-threaded time in the + // building process. - combinedFill.ClearArcs(); - zoneOutline.ClearArcs(); - - combinedFill.BooleanIntersection( zoneOutline ); - combinedFill.Fracture( true ); - - zone->SetFilledPolysList( layer, combinedFill ); + // combinedFill.ClearArcs(); + // zoneOutline.ClearArcs(); + // combinedFill.BooleanIntersection( zoneOutline ); + // zone->SetFilledPolysList( layer, combinedFill ); zone->SetIsFilled( true ); zone->SetNeedRefill( false ); + + // Poke these relevant context in here for batch processing later on + aZoneFillHandler.QueuePolygonForZone( *zone, std::move( combinedFill ), layer ); } return zone; @@ -4081,6 +4177,8 @@ void BOARD_BUILDER::createZones() std::vector> boundaryZones; std::vector> keepoutZones; + ZONE_FILL_HANDLER zoneFillHandler; + // Walk m_LL_Shapes to find BOUNDARY shapes (zone outlines). // BOUNDARY shapes use class 0x15 with copper layer subclass indices. const LL_WALKER shapeWalker( m_brdDb.m_Header->m_LL_Shapes, m_brdDb ); @@ -4095,7 +4193,7 @@ void BOARD_BUILDER::createZones() if( shapeData.m_Layer.m_Class != LAYER_INFO::CLASS::BOUNDARY ) continue; - std::unique_ptr zone = buildZone( *block, getShapeRelatedBlocks( shapeData ) ); + std::unique_ptr zone = buildZone( *block, getShapeRelatedBlocks( shapeData ), zoneFillHandler ); if( zone ) { @@ -4127,7 +4225,7 @@ void BOARD_BUILDER::createZones() wxLogTrace( traceAllegroBuilder, " Processing %s rect %#010x", layerInfoDisplayName( rectData.m_Layer ), rectData.m_Key ); - zone = buildZone( *block, {} ); + zone = buildZone( *block, {}, zoneFillHandler ); break; } case 0x28: @@ -4140,7 +4238,7 @@ void BOARD_BUILDER::createZones() wxLogTrace( traceAllegroBuilder, " Processing %s shape %#010x", layerInfoDisplayName( shapeData.m_Layer ), shapeData.m_Key ); - zone = buildZone( *block, {} ); + zone = buildZone( *block, {}, zoneFillHandler ); break; } default: @@ -4153,6 +4251,9 @@ void BOARD_BUILDER::createZones() } } + // Deal with all the collected zone fill polygons now, all at once + zoneFillHandler.ProcessPolygons( true ); + int keepoutCount = keepoutZones.size(); int boundaryCount = boundaryZones.size(); diff --git a/pcbnew/pcb_io/allegro/allegro_builder.h b/pcbnew/pcb_io/allegro/allegro_builder.h index 9c07d25a51..b9c51961f9 100644 --- a/pcbnew/pcb_io/allegro/allegro_builder.h +++ b/pcbnew/pcb_io/allegro/allegro_builder.h @@ -163,13 +163,17 @@ private: std::vector> buildTrack( const BLK_0x05_TRACK& aBlock, int aNetcode ); std::unique_ptr buildVia( const BLK_0x33_VIA& aBlock, int aNetcode ); + class ZONE_FILL_HANDLER; + /** * Build a ZONE from an 0x0E, 0x24 or 0x28 block. - * + * * @param aRelatedBlocks are blocks to get net (0x1B) and fill (0x28) info from + * @param aZoneFillHandler is a management object for efficiently dealing with filled zones */ std::unique_ptr buildZone( const BLOCK_BASE& aBoundaryBlock, - const std::vector& aRelatedBlocks ); + const std::vector& aRelatedBlocks, + ZONE_FILL_HANDLER& aZoneFillHandler ); SHAPE_LINE_CHAIN buildOutline( const BLK_0x0E_RECT& aRect ) const; SHAPE_LINE_CHAIN buildOutline( const BLK_0x24_RECT& aRect ) const;