diff --git a/.classpath b/.classpath index 90ab0ce0c..69d07f8ed 100644 --- a/.classpath +++ b/.classpath @@ -56,5 +56,21 @@ + + + + + + + + + + + + + + + + diff --git a/.github/workflows/test-rapidwright-wrapper.yml b/.github/workflows/test-rapidwright-wrapper.yml index a9b79aa39..91e97777a 100644 --- a/.github/workflows/test-rapidwright-wrapper.yml +++ b/.github/workflows/test-rapidwright-wrapper.yml @@ -13,11 +13,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup JDK 1.11 + - name: Setup JDK 1.17 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '11' + java-version: '17' cache: 'gradle' - name: Compile diff --git a/LICENSE.TXT b/LICENSE.TXT index 738e6771a..e32dd5a06 100755 --- a/LICENSE.TXT +++ b/LICENSE.TXT @@ -105,6 +105,8 @@ RapidWright (Apache 2.0) - Apache Commons IO 2.20.0 (Apache 2.0) - Zstd-jni 1.5.5-1 (2-clause BSD) - Zstandard 1.5.5 (3-clause BSD or GPLv2) + - Google Guava 33.5.0 (Apache 2.0) + - ELK (Eclipse Layout Kernel) 0.10.0 (Eclipse Public License 2.0) - Google or-tools-java 9.14.6206 (Apache 2.0) - Java Native Access jna-5.14.0 (Apache 2.0) @@ -3237,7 +3239,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ********************************* END LICENSE ********************************* -******************* google-guava 28 - Apache 2.0 ******************************** +******************* google-guava 28, Google Guava 33.5.0 - Apache 2.0 ******************************** Copyright (C) 2013 The Guava Authors @@ -9073,6 +9075,274 @@ library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. +********************************* END LICENSE ********************************* + +********************** ELK (Eclipse Layout Kernel) (Eclipse Public License v 2.0) ***************** +# Eclipse Public License - v 2.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE +PUBLIC LICENSE (“AGREEMENT”). ANY USE, REPRODUCTION OR DISTRIBUTION OF +THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +## 1. DEFINITIONS + +“Contribution” means: + +- a\) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and +- b\) in the case of each subsequent Contributor: + - i\) changes to the Program, and + - ii\) additions to the Program; + + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + “originates” from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's + behalf. Contributions do not include changes or additions to the + Program that are not Modified Works. + +“Contributor” means any person or entity that Distributes the Program. + +“Licensed Patents” mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +“Program” means the Contributions Distributed in accordance with this +Agreement. + +“Recipient” means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +“Derivative Works” shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +“Modified Works” shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +“Distribute” means the acts of a) distributing or b) making available in +any manner that enables the transfer of a copy. + +“Source Code” means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +“Secondary License” means either the GNU General Public License, Version +2.0, or any later versions of that license, including any exceptions or +additional permissions as identified by the initial Contributor. + +## 2. GRANT OF RIGHTS + +- a\) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. +- b\) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent license + under Licensed Patents to make, use, sell, offer to sell, import and + otherwise transfer the Contribution of such Contributor, if any, in + Source Code or other form. This patent license shall apply to the + combination of the Contribution and the Program if, at the time the + Contribution is added by the Contributor, such addition of the + Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. +- c\) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. Each + Contributor disclaims any liability to Recipient for claims brought by + any other entity based on infringement of intellectual property rights + or otherwise. As a condition to exercising the rights and licenses + granted hereunder, each Recipient hereby assumes sole responsibility to + secure any other intellectual property rights needed, if any. For + example, if a third party patent license is required to allow Recipient + to Distribute the Program, it is Recipient's responsibility to acquire + that license before distributing the Program. +- d\) Each Contributor represents that to its knowledge it has sufficient + copyright rights in its Contribution, if any, to grant the copyright + license set forth in this Agreement. +- e\) Notwithstanding the terms of any Secondary License, no Contributor + makes additional grants to any Recipient (other than those set forth in + this Agreement) as a result of such Recipient's receipt of the Program + under the terms of a Secondary License (if permitted under the terms of + Section 3). + +## 3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + +- a\) the Program must also be made available as Source Code, in accordance + with section 3.2, and the Contributor must accompany the Program with a + statement that the Source Code for the Program is available under this + Agreement, and informs Recipients how to obtain it in a reasonable + manner on or through a medium customarily used for software exchange; + and +- b\) the Contributor may Distribute the Program under a license different + than this Agreement, provided that such license: + - i\) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including warranties or + conditions of title and non-infringement, and implied warranties or + conditions of merchantability and fitness for a particular purpose; + - ii\) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, incidental + and consequential damages, such as lost profits; + - iii\) does not attempt to limit or alter the recipients' rights in the + Source Code under section 3.2; and + - iv\) requires any subsequent distribution of the Program by any party to + be under a license that satisfies the requirements of this section 3. + +3.2 When the Program is Distributed as Source Code: + +- a\) it must be made available under this Agreement, or if the Program (i) + is combined with other material in a separate file or files made + available under a Secondary License, and (ii) the initial Contributor + attached to the Source Code the notice described in Exhibit A of this + Agreement, then the Program may be made available under the terms of + such Secondary Licenses, and +- b\) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability (‘notices’) contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +## 4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, the +Contributor who includes the Program in a commercial product offering +should do so in a manner which does not create potential liability for +other Contributors. Therefore, if a Contributor includes the Program in +a commercial product offering, such Contributor (“Commercial +Contributor”) hereby agrees to defend and indemnify every other +Contributor (“Indemnified Contributor”) against any losses, damages and +costs (collectively “Losses”) arising from claims, lawsuits and other +legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the +Program in a commercial product offering. The obligations in this +section do not apply to any claims or Losses relating to any actual or +alleged intellectual property infringement. In order to qualify, an +Indemnified Contributor must: a) promptly notify the Commercial +Contributor in writing of such claim, and b) allow the Commercial +Contributor to control, and cooperate with the Commercial Contributor +in, the defense and any related settlement negotiations. The Indemnified +Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those +performance claims and warranties, and if a court requires any other +Contributor to pay any damages as a result, the Commercial Contributor +must pay those damages. + +## 5. NO WARRANTY {#warranty} + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN “AS IS” +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs or +equipment, and unavailability or interruption of operations. + +## 6. DISCLAIMER OF LIABILITY {#disclaimer} + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +## 7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further action +by the parties hereto, such provision shall be reformed to the minimum +extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including +a cross-claim or counterclaim in a lawsuit) alleging that the Program +itself (excluding combinations of the Program with other software or +hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails +to comply with any of the material terms or conditions of this Agreement +and does not cure such failure in a reasonable period of time after +becoming aware of such noncompliance. If all Recipient's rights under +this Agreement terminate, Recipient agrees to cease use and distribution +of the Program as soon as reasonably practicable. However, Recipient's +obligations under this Agreement and any licenses granted by Recipient +relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and may +only be modified in the following manner. The Agreement Steward reserves +the right to publish new versions (including revisions) of this +Agreement from time to time. No one other than the Agreement Steward has +the right to modify this Agreement. The Eclipse Foundation is the +initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is +published, Contributor may elect to Distribute the Program (including +its Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +## Exhibit A – Form of Secondary Licenses Notice {#exhibit-a} + +“This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}.” + +> Simply including a copy of this Agreement, including this Exhibit A is +> not sufficient to license the Source Code under Secondary Licenses. +> +> If it is not possible or desirable to put the notice in a particular +> file, then You may include the notice in a location (such as a LICENSE +> file in a relevant directory) where a recipient would be likely to +> look for such a notice. +> +> You may add additional accurate notices of copyright ownership. + ********************************* END LICENSE ********************************* ********************** Jacl 1.4.1 (multiple licenses) **************** @@ -9284,4 +9554,4 @@ CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. -********************************* END LICENSE ********************************* \ No newline at end of file +********************************* END LICENSE ********************************* diff --git a/common.gradle b/common.gradle index 3531f40c3..90fa8094f 100644 --- a/common.gradle +++ b/common.gradle @@ -48,6 +48,14 @@ dependencies { api 'commons-io:commons-io:2.20.0' api 'com.xilinx.rapidwright:qtjambi-'+os+':4.5.2_01' api 'com.xilinx.rapidwright:jupyter-kernel-jsr223:1.0.1' + if (JavaVersion.current() < JavaVersion.VERSION_17) { + // Eclipse ELK doesn't compile to support JDK8 anymore + api 'com.xilinx.rapidwright:org.eclipse.elk-deps-jdk8:0.10.0-rc1' + } else { + api 'org.eclipse.elk:org.eclipse.elk.core:0.10.0' + api 'org.eclipse.elk:org.eclipse.elk.graph:0.10.0' + api 'org.eclipse.elk:org.eclipse.elk.alg.layered:0.10.0' + } api 'com.xilinx.rapidwright:jacl:1.4.1' testFixturesApi 'org.junit.jupiter:junit-jupiter-api:5.7.1' testFixturesApi 'org.junit.jupiter:junit-jupiter-engine:5.7.1' diff --git a/src/com/xilinx/rapidwright/gui/HierCellInstTreeWidgetItem.java b/src/com/xilinx/rapidwright/gui/HierCellInstTreeWidgetItem.java index a0b53b830..7d76391d5 100644 --- a/src/com/xilinx/rapidwright/gui/HierCellInstTreeWidgetItem.java +++ b/src/com/xilinx/rapidwright/gui/HierCellInstTreeWidgetItem.java @@ -36,6 +36,10 @@ public HierCellInstTreeWidgetItem(QTreeWidgetItem parent) { super(parent); } + public HierCellInstTreeWidgetItem(NetlistTreeWidget netlistTreeWidget) { + super(netlistTreeWidget); + } + public void setInst(EDIFHierCellInst inst) { this.inst = inst; } diff --git a/src/com/xilinx/rapidwright/gui/NetlistBrowser.java b/src/com/xilinx/rapidwright/gui/NetlistBrowser.java index 044c98bdc..d7d13db46 100644 --- a/src/com/xilinx/rapidwright/gui/NetlistBrowser.java +++ b/src/com/xilinx/rapidwright/gui/NetlistBrowser.java @@ -22,20 +22,30 @@ */ package com.xilinx.rapidwright.gui; +import com.trolltech.qt.core.QModelIndex; import com.trolltech.qt.core.Qt.DockWidgetArea; import com.trolltech.qt.gui.QApplication; import com.trolltech.qt.gui.QDockWidget; import com.trolltech.qt.gui.QDockWidget.DockWidgetFeature; import com.trolltech.qt.gui.QMainWindow; import com.trolltech.qt.gui.QTreeWidget; +import com.trolltech.qt.gui.QTreeWidgetItem; import com.trolltech.qt.gui.QWidget; import com.xilinx.rapidwright.design.Design; +import com.xilinx.rapidwright.edif.EDIFCell; +import com.xilinx.rapidwright.edif.EDIFCellInst; +import com.xilinx.rapidwright.edif.EDIFHierCellInst; +import com.xilinx.rapidwright.edif.EDIFNet; import com.xilinx.rapidwright.edif.EDIFNetlist; +import com.xilinx.rapidwright.edif.EDIFPort; import com.xilinx.rapidwright.edif.EDIFTools; public class NetlistBrowser extends QMainWindow { - private QTreeWidget treeWidget; + private NetlistTreeWidget treeWidget; + private QDockWidget schematicWidget; + private SchematicScene schematicScene; + private SchematicView schematicView; private Design design; @@ -90,12 +100,44 @@ private void init() { resize(1280, 1024); treeWidget = new NetlistTreeWidget("Netlist", netlist); - // treeWidget.doubleClicked.connect(this,"showPart(QModelIndex)"); + treeWidget.clicked.connect(this, "selectNetlistItem(QModelIndex)"); QDockWidget dockWidget = new QDockWidget(tr("Design"), this); dockWidget.setWidget(treeWidget); dockWidget.setFeatures(DockWidgetFeature.DockWidgetMovable); addDockWidget(DockWidgetArea.LeftDockWidgetArea, dockWidget); + + // Add schematic viewer on the right + schematicScene = new SchematicScene(netlist); + schematicView = new SchematicView(schematicScene); + schematicWidget = new QDockWidget(tr("Schematic"), this); + schematicWidget.setWidget(schematicView); + schematicWidget.setFeatures(DockWidgetFeature.DockWidgetMovable); + addDockWidget(DockWidgetArea.RightDockWidgetArea, schematicWidget); + + schematicScene.objectSelected.connect(this, "selectFromSchematic(String)"); + schematicScene.cellDrawn.connect(schematicView, "zoomToFit()"); + } + + public void selectNetlistItem(QModelIndex index) { + QTreeWidgetItem item = treeWidget.getItemFromIndex(index); + if (item instanceof HierCellInstTreeWidgetItem) { + EDIFHierCellInst cellInst = ((HierCellInstTreeWidgetItem) item).getInst(); + schematicScene.drawCell(cellInst, true); + } else if (item.data(0, 0) instanceof EDIFPort) { + EDIFPort port = (EDIFPort) item.data(0, 0); + schematicScene.selectObject(item.data(1, 0).toString(), true); + } else if (item.data(0, 0) instanceof EDIFNet) { + schematicScene.selectObject(item.data(1, 0).toString(), true); + } + } + + public void selectFromSchematic(String lookup) { + QTreeWidgetItem item = treeWidget.getItemByStringLookup(lookup); + if (item != null) { + treeWidget.setCurrentItem(item); + treeWidget.scrollToItem(item); + } } /** diff --git a/src/com/xilinx/rapidwright/gui/NetlistTreeWidget.java b/src/com/xilinx/rapidwright/gui/NetlistTreeWidget.java index a10204ae9..8995db00a 100644 --- a/src/com/xilinx/rapidwright/gui/NetlistTreeWidget.java +++ b/src/com/xilinx/rapidwright/gui/NetlistTreeWidget.java @@ -24,7 +24,9 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import com.trolltech.qt.core.QModelIndex; import com.trolltech.qt.gui.QTreeWidget; @@ -32,6 +34,8 @@ import com.xilinx.rapidwright.edif.EDIFCell; import com.xilinx.rapidwright.edif.EDIFCellInst; import com.xilinx.rapidwright.edif.EDIFHierCellInst; +import com.xilinx.rapidwright.edif.EDIFHierNet; +import com.xilinx.rapidwright.edif.EDIFHierPortInst; import com.xilinx.rapidwright.edif.EDIFNet; import com.xilinx.rapidwright.edif.EDIFNetlist; import com.xilinx.rapidwright.edif.EDIFPort; @@ -45,13 +49,16 @@ public class NetlistTreeWidget extends QTreeWidget { private QTreeWidgetItem rootItem; + private Map objectLookup = new HashMap<>(); + private static final String DUMMY = "_*DUMMY*_"; public NetlistTreeWidget(String header, EDIFNetlist netlist) { this.netlist = netlist; setColumnCount(1); setHeaderLabel(header); - QTreeWidgetItem root = new QTreeWidgetItem(this); + HierCellInstTreeWidgetItem root = new HierCellInstTreeWidgetItem(this); + root.setInst(netlist.getTopHierCellInst()); QTreeWidgetItem dummy = new QTreeWidgetItem(root); dummy.setText(0, DUMMY); rootItem = populateCellInst(root, netlist.getTopHierCellInst()); @@ -68,14 +75,18 @@ public QTreeWidgetItem populateCellInst(QTreeWidgetItem curr, EDIFHierCellInst i EDIFCell cell = inst.getCellType(); curr.setText(0, inst.getInst().getName() + " (" + cell.getName() + ")"); + boolean isTop = inst.isTopLevelInst(); QTreeWidgetItem ports = new QTreeWidgetItem(curr); ports.setText(0, "Ports (" + cell.getPorts().size() + ")"); - List edifPorts = new ArrayList<>(cell.getPorts()); - Collections.sort(edifPorts); - for (EDIFPort port : edifPorts) { + + for (EDIFHierPortInst portInst : inst.getHierPortInsts()) { QTreeWidgetItem n = new QTreeWidgetItem(ports); - n.setText(0, port.getName() + " (" + port.getDirection() + ")"); + n.setData(0, 0, portInst); + String portLookup = "PORT:" + (isTop ? portInst.getPortInst().getName() : portInst.toString()); + n.setData(1, 0, portLookup); + n.setText(0, portInst.getPortInst().getName() + " (" + portInst.getPortInst().getDirection() + ")"); + objectLookup.put(portLookup, n); } ports.setExpanded(false); @@ -86,16 +97,21 @@ public QTreeWidgetItem populateCellInst(QTreeWidgetItem curr, EDIFHierCellInst i Collections.sort(edifNets); for (EDIFNet net : edifNets) { QTreeWidgetItem n = new QTreeWidgetItem(nets); + EDIFHierNet hierNet = inst.getNet(net.getName()); + n.setData(0, 0, hierNet); + String netLookup = "NET:" + hierNet.toString(); + n.setData(1, 0, netLookup); n.setText(0, net.getName()); + objectLookup.put(netLookup, n); } nets.setExpanded(false); - List leaves = new ArrayList<>(); + List leaves = new ArrayList<>(); List nonLeaves = new ArrayList<>(); for (EDIFCellInst child : cell.getCellInsts()) { if (child.getCellType().isLeafCellOrBlackBox()) { - leaves.add(child); + leaves.add(inst.getChild(child)); } else { nonLeaves.add(inst.getChild(child)); } @@ -105,9 +121,13 @@ public QTreeWidgetItem populateCellInst(QTreeWidgetItem curr, EDIFHierCellInst i QTreeWidgetItem leafCells = new QTreeWidgetItem(curr); leafCells.setText(0, "Leaf Cells (" + leaves.size() + ")"); - for (EDIFCellInst i : leaves) { + for (EDIFHierCellInst i : leaves) { QTreeWidgetItem leaf = new QTreeWidgetItem(leafCells); - leaf.setText(0, i.getName() + " (" + i.getCellName() + ")"); + leaf.setText(0, i.getInst().getName() + " (" + i.getCellName() + ")"); + leaf.setData(0, 0, i); + String leafLookup = "INST:" + i.toString(); + leaf.setData(1, 0, leafLookup); + objectLookup.put(leafLookup, leaf); } leafCells.setExpanded(false); @@ -116,6 +136,9 @@ public QTreeWidgetItem populateCellInst(QTreeWidgetItem curr, EDIFHierCellInst i HierCellInstTreeWidgetItem cellInst = new HierCellInstTreeWidgetItem(curr); cellInst.setText(0, i.getInst().getName() + " (" + i.getCellName() + ")"); cellInst.setInst(i); + String instLookup = "INST:" + i.toString(); + cellInst.setData(1, 0, instLookup); + objectLookup.put(instLookup, cellInst); QTreeWidgetItem dummy = new QTreeWidgetItem(cellInst); dummy.setText(0, DUMMY); } @@ -144,4 +167,12 @@ public EDIFNetlist getNetlist() { public QTreeWidgetItem getRootItem() { return rootItem; } + + public QTreeWidgetItem getItemFromIndex(QModelIndex index) { + return this.itemFromIndex(index); + } + + public QTreeWidgetItem getItemByStringLookup(String lookup) { + return objectLookup.get(lookup); + } } diff --git a/src/com/xilinx/rapidwright/gui/SchematicScene.java b/src/com/xilinx/rapidwright/gui/SchematicScene.java new file mode 100644 index 000000000..83a0b5969 --- /dev/null +++ b/src/com/xilinx/rapidwright/gui/SchematicScene.java @@ -0,0 +1,767 @@ +/* + * + * Copyright (c) 2025, Advanced Micro Devices, Inc. + * All rights reserved. + * + * Author: Chris Lavin, AMD Advanced Research and Development. + * + * This file is part of RapidWright. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.xilinx.rapidwright.gui; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; + +import org.eclipse.elk.core.IGraphLayoutEngine; +import org.eclipse.elk.core.RecursiveGraphLayoutEngine; +import org.eclipse.elk.core.math.ElkPadding; +import org.eclipse.elk.core.options.CoreOptions; +import org.eclipse.elk.core.options.NodeLabelPlacement; +import org.eclipse.elk.core.options.PortConstraints; +import org.eclipse.elk.core.options.PortSide; +import org.eclipse.elk.core.util.BasicProgressMonitor; +import org.eclipse.elk.core.util.IElkProgressMonitor; +import org.eclipse.elk.graph.ElkBendPoint; +import org.eclipse.elk.graph.ElkEdge; +import org.eclipse.elk.graph.ElkEdgeSection; +import org.eclipse.elk.graph.ElkGraphElement; +import org.eclipse.elk.graph.ElkGraphFactory; +import org.eclipse.elk.graph.ElkLabel; +import org.eclipse.elk.graph.ElkNode; +import org.eclipse.elk.graph.ElkPort; + +import com.trolltech.qt.core.QPointF; +import com.trolltech.qt.core.QRectF; +import com.trolltech.qt.core.QSizeF; +import com.trolltech.qt.core.Qt.KeyboardModifier; +import com.trolltech.qt.gui.QAbstractGraphicsShapeItem; +import com.trolltech.qt.gui.QBrush; +import com.trolltech.qt.gui.QColor; +import com.trolltech.qt.gui.QFont; +import com.trolltech.qt.gui.QFontMetrics; +import com.trolltech.qt.gui.QGraphicsItemInterface; +import com.trolltech.qt.gui.QGraphicsLineItem; +import com.trolltech.qt.gui.QGraphicsPathItem; +import com.trolltech.qt.gui.QGraphicsPolygonItem; +import com.trolltech.qt.gui.QGraphicsRectItem; +import com.trolltech.qt.gui.QGraphicsScene; +import com.trolltech.qt.gui.QGraphicsSceneMouseEvent; +import com.trolltech.qt.gui.QGraphicsSimpleTextItem; +import com.trolltech.qt.gui.QPainterPath; +import com.trolltech.qt.gui.QPen; +import com.trolltech.qt.gui.QPolygonF; +import com.xilinx.rapidwright.edif.EDIFCell; +import com.xilinx.rapidwright.edif.EDIFCellInst; +import com.xilinx.rapidwright.edif.EDIFHierCellInst; +import com.xilinx.rapidwright.edif.EDIFHierPortInst; +import com.xilinx.rapidwright.edif.EDIFNet; +import com.xilinx.rapidwright.edif.EDIFNetlist; +import com.xilinx.rapidwright.edif.EDIFPort; +import com.xilinx.rapidwright.edif.EDIFPortInst; + +public class SchematicScene extends QGraphicsScene { + + private EDIFNetlist netlist; + + private EDIFHierCellInst currCellInst; + + private ElkNode elkRoot; + + private Map portInstMap = new HashMap<>(); + private Map elkNodeTopPortMap = new HashMap<>(); + private Map elkNodeCellMap = new HashMap<>(); + private Map> lookupMap = new HashMap<>(); + private Set expandedCellInsts = new HashSet<>(); + private Set selectedObjects = new HashSet<>(); + + public Signal1 objectSelected = new Signal1<>(); + public Signal0 cellDrawn = new Signal0(); + + private static QFont FONT = new QFont("Arial", 8); + private static QFont BUTTON_TEXT_FONT = new QFont("Arial", 10, QFont.Weight.Bold.value()); + + private static final QBrush BLACK_BRUSH = new QBrush(QColor.black); + private static final QPen BLACK_PEN = new QPen(QColor.black); + private static final QPen CLICK_PEN = new QPen(new QColor(0, 0, 0, 0), 10); + private static final QBrush CLICK_BRUSH = new QBrush(new QColor(0, 0, 0, 0)); + + private static QFontMetrics fm = new QFontMetrics(FONT); + private static QBrush canvasBackgroundBrush = new QBrush(QColor.white); + private static final QBrush PORT_BRUSH = new QBrush(QColor.white); + private static final QPen PORT_PEN = BLACK_PEN; + private static final QPen NET_PEN = new QPen(new QColor(91, 203, 75)); + private static final QPen NET_CLICK_PEN = CLICK_PEN; + private static final QPen SELECTED_PEN = new QPen(new QColor(0, 0, 255), 3); + + private static final QBrush CELL_BRUSH = new QBrush(new QColor(255, 255, 210)); + private static final QPen CELL_PEN = new QPen(QColor.black); + private static final QBrush HIER_CELL_BRUSH = new QBrush(new QColor(173, 216, 230)); + private static final QPen HIER_CELL_PEN = new QPen(new QColor(100, 149, 237)); + private static final QBrush EXPANDED_HIER_CELL_BRUSH = new QBrush(new QColor(255, 255, 255, 0)); + private static final QPen EXPANDED_HIER_CELL_PEN = new QPen(new QColor(100, 149, 237), 2); + + private static final QBrush BUTTON_BRUSH = new QBrush(new QColor(72, 61, 139)); + private static final QPen BUTTON_PEN = new QPen(new QColor(72, 61, 139)); + private static final QBrush BUTTON_TEXT_BRUSH = new QBrush(QColor.white); + + private static final double PORT_SIZE = 6.0; + private static final double MIN_NODE_HEIGHT = 30.0; + private static final double MIN_NODE_WIDTH = 40.0; + + private static final double PORT_HEIGHT = 20.0; + private static final double PORT_NAME_BUFFER = 40.0; + private static final double TOP_PORT_WIDTH = 20.0; + private static final double TOP_PORT_HEIGHT = 14.0; + private static final double PORT_LABEL_SPACING = 4.0; + + private static final double NODE_TO_NODE_SPACING = 40.0; + private static final double EDGE_TO_NODE_SPACING = 20.0; + private static final double SIDE_PADDING = 10.0; + + + private static final double POINT_DIST = TOP_PORT_HEIGHT * 0.2; // Pointy part of the port + + private static final double BUTTON_SIZE = 16.0; + private static final double BUTTON_RADIUS = 3.0; + private static final double LABEL_BUFFER = 2.0; + private static final double PIN_LINE_LENGTH = 10.0; + + private static final String HIER_BUTTON = "HIER_BUTTON"; + private static final String CLICK = "CLICK"; + + public SchematicScene(EDIFNetlist netlist) { + super(); + this.netlist = netlist; + setBackgroundBrush(canvasBackgroundBrush); + } + + private ElkNode createElkRoot(EDIFHierCellInst cellInst) { + ElkNode root = ElkGraphFactory.eINSTANCE.createElkNode(); + root.setIdentifier(cellInst.toString()); + applyElkNodeProperties(root); + return root; + } + + private void applyElkNodeProperties(ElkNode root) { + root.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered"); + root.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_TO_NODE_SPACING); + root.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_TO_NODE_SPACING); + } + + public void drawCell(EDIFHierCellInst cellInst, boolean zoomFit) { + clear(); + portInstMap.clear(); + elkNodeTopPortMap.clear(); + elkNodeCellMap.clear(); + lookupMap.clear(); + selectedObjects.clear(); + this.currCellInst = cellInst; + elkRoot = createElkRoot(cellInst); + populateCellContent(cellInst, elkRoot, ""); + elkNodeCellMap.put(elkRoot, cellInst); + + IGraphLayoutEngine engine = new RecursiveGraphLayoutEngine(); + IElkProgressMonitor monitor = new BasicProgressMonitor(); + engine.layout(elkRoot, monitor); + + renderSchematic(); + if (zoomFit) { + cellDrawn.emit(); + } + } + + public void renderSchematic() { + double extraWidthBuffer = elkRoot.getWidth() * 0.25 + 100; + double extraHeightBuffer = elkRoot.getHeight() * 0.25 + 100; + QSizeF size = new QSizeF(elkRoot.getWidth() + extraWidthBuffer, elkRoot.getHeight() + extraHeightBuffer); + + setSceneRect(new QRectF(new QPointF(0, 0), size)); + + // We create arrow-shaped top port ElkNodes to serve as targets for top ports + for (ElkNode topPort : elkRoot.getChildren()) { + EDIFHierPortInst hierPortInst = elkNodeTopPortMap.get(topPort); + if (hierPortInst != null) { + QPolygonF portShape = createPortShape(topPort, hierPortInst.isOutput()); + QGraphicsPolygonItem port = addPolygon(portShape, PORT_PEN, PORT_BRUSH); + String lookup = "PORT:" + hierPortInst.toString(); + port.setData(0, lookup); + String portInstName = hierPortInst.getPortInst().getName(); + port.setToolTip(portInstName + (hierPortInst.isOutput() ? "(Output)" : "(Input)")); + port.setAcceptsHoverEvents(true); + lookupMap.computeIfAbsent(lookup, l -> new ArrayList<>()).add(port); + + QGraphicsSimpleTextItem portLabel = addSimpleText(portInstName); + portLabel.setBrush(BLACK_BRUSH); + portLabel.setFont(FONT); + double portShapeX = topPort.getX() + (topPort.getWidth() - TOP_PORT_WIDTH) / 2.0; + double portShapeY = topPort.getY() + (topPort.getHeight() - TOP_PORT_HEIGHT) / 2.0; + double labelX = portShapeX; + if (hierPortInst.isOutput()) { + // Position label to the right of top ports + labelX += TOP_PORT_WIDTH + PORT_LABEL_SPACING; + } else { + // To the left for inputs + labelX -= portLabel.boundingRect().width() + PORT_LABEL_SPACING; + } + double labelY = portShapeY + (TOP_PORT_HEIGHT - portLabel.boundingRect().height()) / 2.0; + portLabel.setPos(labelX, labelY); + portLabel.setZValue(4); + } + } + + renderNode(elkRoot, 0, 0, ""); + renderEdges(elkRoot, 0, 0); + } + + private void renderNode(ElkNode parent, double xOffset, double yOffset, String prefix) { + EDIFCell cell = elkNodeCellMap.get(parent).getCellType(); + for (ElkNode child : parent.getChildren()) { + if (elkNodeTopPortMap.containsKey(child)) { + // Rendered in renderSchematic() + continue; + } + String relCellInstName = child.getIdentifier(); + if (relCellInstName.startsWith(parent.getIdentifier()) && parent.getIdentifier().length() > 0) { + // Remove hierarchical reference + relCellInstName = relCellInstName.substring(parent.getIdentifier().length() + 1); + } + EDIFCellInst eci = cell.getCellInst(relCellInstName); + + boolean isLeaf = true; + String cellInstName = prefix + relCellInstName; + boolean isExpanded = expandedCellInsts.contains(cellInstName); + if (child.getChildren().size() > 0) { + isLeaf = false; + } else if (eci != null && !eci.getCellType().isLeafCellOrBlackBox()) { + isLeaf = false; + } + + double x = xOffset + child.getX(); + double y = yOffset + child.getY(); + + QGraphicsRectItem rect = null; + QGraphicsRectItem clickRect = addRect(x, y, child.getWidth(), child.getHeight(), CLICK_PEN, CLICK_BRUSH); + ; + if (isLeaf) { + rect = addRect(x, y, child.getWidth(), child.getHeight(), CELL_PEN, CELL_BRUSH); + clickRect.setZValue(6); + } else { + if (isExpanded) { + rect = addRect(x, y, child.getWidth(), child.getHeight(), EXPANDED_HIER_CELL_PEN, EXPANDED_HIER_CELL_BRUSH); + clickRect.setZValue(4); + } else { + rect = addRect(x, y, child.getWidth(), child.getHeight(), HIER_CELL_PEN, HIER_CELL_BRUSH); + clickRect.setZValue(6); + } + createHierButton(child, isExpanded, cellInstName, xOffset, yOffset); + } + String instLookup = "INST:" + child.getIdentifier(); + clickRect.setData(0, instLookup); + lookupMap.computeIfAbsent(instLookup, l -> new ArrayList<>()).add(rect); + + ElkLabel instNameLabel = child.getLabels().get(0); // instance name + ElkLabel cellTypeLabel = child.getLabels().get(1); // cell type + + // Instance name centered above cell rectangle + QGraphicsSimpleTextItem instLabel = addSimpleText(instNameLabel.getText()); + instLabel.setBrush(BLACK_BRUSH); + instLabel.setFont(FONT); + double instLabelX = x + (child.getWidth() - instLabel.boundingRect().width()) / 2.0; + double instLabelY = y - instLabel.boundingRect().height(); + instLabel.setPos(instLabelX, instLabelY - LABEL_BUFFER); + instLabel.setZValue(5); + + // Cell type centered below cell rectangle + QGraphicsSimpleTextItem cellLabel = addSimpleText(cellTypeLabel.getText()); + cellLabel.setBrush(BLACK_BRUSH); + cellLabel.setFont(FONT); + double cellLabelX = x + (child.getWidth() - cellLabel.boundingRect().width()) / 2.0; + double cellLabelY = y + child.getHeight(); + cellLabel.setPos(cellLabelX, cellLabelY + LABEL_BUFFER); + cellLabel.setZValue(5); + + for (ElkPort port : child.getPorts()) { + double yPort = y + port.getY() + port.getHeight() / 2.0; + PortSide side = port.getProperty(CoreOptions.PORT_SIDE); + drawPin(child, port, yPort, side, isExpanded, xOffset, child.getIdentifier()); + + QGraphicsSimpleTextItem pinLabel = addSimpleText(port.getIdentifier()); + pinLabel.setBrush(BLACK_BRUSH); + pinLabel.setFont(FONT); + pinLabel.setZValue(5); + double textWidth = pinLabel.boundingRect().width(); + double textHeight = pinLabel.boundingRect().height(); + double labelX = x; + double labelY = y; + + if (!isLeaf) { + labelX += side == PortSide.EAST ? child.getWidth() + LABEL_BUFFER : - textWidth - LABEL_BUFFER; + labelY = yPort - textHeight + LABEL_BUFFER; + } else { + labelX += side == PortSide.EAST ? child.getWidth() - textWidth - 2*LABEL_BUFFER : 2*LABEL_BUFFER; + labelY = yPort - textHeight / 2.0; + } + pinLabel.setPos(labelX, labelY); + } + + if (child.getChildren().size() > 0) { + renderNode(child, x, y, prefix + relCellInstName + "/"); + } + } + } + + private void drawPin(ElkNode cell, ElkPort port, double y, PortSide side, boolean isExpanded, double xOffset, String parentInst) { + double x1 = cell.getX() + xOffset + (side == PortSide.EAST ? cell.getWidth() : -PIN_LINE_LENGTH); + double x2 = cell.getX() + xOffset + (side == PortSide.EAST ? cell.getWidth() + PIN_LINE_LENGTH : 0); + + // Draw outer pins + QGraphicsLineItem pinLine = addLine(x1, y, x2, y, BLACK_PEN); + pinLine.setZValue(2); + String lookup = "PORT:" + parentInst + "/" + port.getIdentifier(); + pinLine.setData(0, lookup); + lookupMap.computeIfAbsent(lookup, l -> new ArrayList<>()).add(pinLine); + // Add a thick invisible area to make them easier to click on + QGraphicsLineItem clickLine = addLine(x1, y, x2, y, CLICK_PEN); + clickLine.setZValue(3); + clickLine.setData(0, lookup); + + if (isExpanded) { + // Draw inner pins + x1 = cell.getX() + xOffset + (side == PortSide.EAST ? cell.getWidth() - PIN_LINE_LENGTH : 0); + x2 = cell.getX() + xOffset + (side == PortSide.EAST ? cell.getWidth() : PIN_LINE_LENGTH); + QGraphicsLineItem innerPinLine = addLine(x1, y, x2, y, BLACK_PEN); + innerPinLine.setZValue(2); + // Add a thick invisible area to make them easier to click on + QGraphicsLineItem innerClickLine = addLine(x1, y, x2, y, CLICK_PEN); + innerClickLine.setZValue(2); + } + } + + private QGraphicsPathItem createHierButton(ElkNode node, boolean isExpanded, String expandedCellName, double xOffset, double yOffset) { + double buttonX = xOffset + node.getX() + BUTTON_SIZE / 2; + double buttonY = yOffset + node.getY() + BUTTON_SIZE / 2; + QPainterPath path = new QPainterPath(); + path.addRoundedRect(buttonX, buttonY, BUTTON_SIZE, BUTTON_SIZE, BUTTON_RADIUS, BUTTON_RADIUS); + + // Create a rounded rectangle for the hierarchy button + QGraphicsPathItem button = addPath(path, BUTTON_PEN, BUTTON_BRUSH); + String data = HIER_BUTTON + ": " + expandedCellName + " : " + (isExpanded ? "COLLAPSE" : "EXPAND"); + button.setData(0, data); + String tooltip = isExpanded ? "Collapse" : "Expand"; + button.setToolTip(tooltip); + button.setZValue(10); + + // Add text to the button + QGraphicsSimpleTextItem buttonText = addSimpleText(isExpanded ? "-" : "+"); + buttonText.setBrush(BUTTON_TEXT_BRUSH); + buttonText.setFont(BUTTON_TEXT_FONT); + double textX = buttonX + (BUTTON_SIZE - buttonText.boundingRect().width()) / 2.0; + double textY = buttonY + (BUTTON_SIZE - buttonText.boundingRect().height()) / 2.0; + buttonText.setPos(textX, textY); + buttonText.setZValue(11); + buttonText.setData(0, data); + buttonText.setToolTip(tooltip); + + return button; + } + + private void renderEdges(ElkNode parent, double xOffset, double yOffset) { + for (ElkEdge e : parent.getContainedEdges()) { + if (e.getSections().isEmpty()) continue; + ElkEdgeSection s = e.getSections().get(0); + + double startX = xOffset + s.getStartX(); + double startY = yOffset + s.getStartY(); + double endX = xOffset + s.getEndX(); + double endY = yOffset + s.getEndY(); + + if (!e.getSources().isEmpty()) { + ElkPort srcPort = (ElkPort) e.getSources().get(0); + ElkNode portParent = (ElkNode) srcPort.getParent(); + EDIFHierPortInst portInst = elkNodeTopPortMap.get(portParent); + if (portInst != null && portInst.getPortInst().isTopLevelPort()) { + QPointF topPortLoc = getTopPortConnectionPoint(portParent, portInst.isOutput()); + startX = topPortLoc.x(); + startY = topPortLoc.y(); + } + } + + if (!e.getTargets().isEmpty()) { + ElkPort snkPort = (ElkPort) e.getTargets().get(0); + ElkNode portParent = (ElkNode) snkPort.getParent(); + EDIFHierPortInst portInst = elkNodeTopPortMap.get(portParent); + if (portInst != null && portInst.getPortInst().isTopLevelPort()) { + QPointF topPortLoc = getTopPortConnectionPoint(portParent, portInst.isOutput()); + endX = topPortLoc.x(); + endY = topPortLoc.y(); + } + } + + double lastX = startX; + double lastY = startY; + String id = e.getIdentifier(); + String lookup = "NET:" + (id == null ? "" : id); + for (ElkBendPoint bp : s.getBendPoints()) { + double bpX = bp.getX() + xOffset; + double bpY = bp.getY() + yOffset; + drawSegment(lastX, lastY, bpX, bpY, lookup); + lastX = bpX; + lastY = bpY; + } + + // Draw final segment + drawSegment(lastX, lastY, endX, endY, lookup); + } + + for (ElkNode child : parent.getChildren()) { + if (child.getChildren().size() > 0) { + renderEdges(child, child.getX() + xOffset, child.getY() + yOffset); + } + } + } + + private void drawSegment(double lastX, double lastY, double endX, double endY, String lookup) { + QGraphicsLineItem line = addLine(lastX, lastY, endX, endY, NET_PEN); + line.setData(0, lookup); + line.setZValue(0); + lookupMap.computeIfAbsent(lookup, l -> new ArrayList<>()).add(line); + QGraphicsLineItem clickLine = addLine(lastX, lastY, endX, endY, NET_CLICK_PEN); + clickLine.setData(0, lookup); + clickLine.setData(1, CLICK); + clickLine.setZValue(5); + lookupMap.computeIfAbsent(lookup, l -> new ArrayList<>()).add(clickLine); + } + + private QPointF getTopPortConnectionPoint(ElkNode port, boolean isOutput) { + double portX = port.getX() + (port.getWidth() - TOP_PORT_WIDTH) / 2.0; + double portY = port.getY() + (port.getHeight() - TOP_PORT_HEIGHT) / 2.0; + return new QPointF(portX + (isOutput ? -POINT_DIST : POINT_DIST) + TOP_PORT_WIDTH, portY + TOP_PORT_HEIGHT / 2); + } + + private static QPolygonF createPortShape(ElkNode topPort, boolean isOutput) { + double portX = topPort.getX(); + double portY = topPort.getY(); + double height = topPort.getHeight(); + double width = topPort.getWidth(); + double x = portX + (width - TOP_PORT_WIDTH) / 2.0; + double y = portY + (height - TOP_PORT_HEIGHT) / 2.0; + + QPolygonF portShape = new QPolygonF(); + + if (isOutput) { + // Point to the left + portShape.add(new QPointF(x + POINT_DIST, y)); + portShape.add(new QPointF(x + TOP_PORT_WIDTH, y)); + portShape.add(new QPointF(x + TOP_PORT_WIDTH, y + TOP_PORT_HEIGHT)); + portShape.add(new QPointF(x + POINT_DIST, y + TOP_PORT_HEIGHT)); + portShape.add(new QPointF(x - POINT_DIST, y + (TOP_PORT_HEIGHT / 2))); + } else { + // Input points to the right + portShape.add(new QPointF(x, y)); + portShape.add(new QPointF(x + TOP_PORT_WIDTH - POINT_DIST, y)); + portShape.add(new QPointF(x + TOP_PORT_WIDTH + POINT_DIST, y + (TOP_PORT_HEIGHT / 2))); + portShape.add(new QPointF(x + TOP_PORT_WIDTH - POINT_DIST, y + TOP_PORT_HEIGHT)); + portShape.add(new QPointF(x, y + TOP_PORT_HEIGHT)); + } + + return portShape; + } + + private void populateCellContent(EDIFHierCellInst cellInst, ElkNode parent, String prefix) { + ElkGraphFactory f = ElkGraphFactory.eINSTANCE; + EDIFCell cell = cellInst.getCellType(); + + // Only create top level port shapes if this is the top cell + if (prefix.isEmpty()) { + for (EDIFPort topPort : cell.getPorts()) { + for (int i : (topPort.isBus() ? topPort.getBitBlastedIndicies() : new int[] { 0 })) { + String portInstName = topPort.getPortInstNameFromPort(i); + ElkNode elkTopPortNode = f.createElkNode(); + EDIFHierPortInst hierPortInst = cellInst.getPortInst(portInstName); + elkNodeTopPortMap.put(elkTopPortNode, hierPortInst); + elkTopPortNode.setDimensions(TOP_PORT_WIDTH + POINT_DIST, TOP_PORT_HEIGHT); + elkTopPortNode.setIdentifier(portInstName); + elkTopPortNode.setParent(parent); + parent.getChildren().add(elkTopPortNode); + labelElkNode(elkTopPortNode, portInstName); + elkTopPortNode.setProperty(CoreOptions.NODE_LABELS_PLACEMENT, + EnumSet.of(topPort.isOutput() ? NodeLabelPlacement.H_RIGHT : NodeLabelPlacement.H_LEFT)); + ElkPort elkTopPort = f.createElkPort(); + elkTopPort.setParent(elkTopPortNode); + elkTopPort.setIdentifier(portInstName); + elkTopPort.setProperty(CoreOptions.PORT_SIDE, topPort.isOutput() ? PortSide.EAST : PortSide.WEST); + elkTopPort.setProperty(CoreOptions.PORT_INDEX, 1); + elkTopPort.setDimensions(PORT_SIZE, PORT_SIZE); + portInstMap.put(cellInst.getPortInst(portInstName), elkTopPort); + elkTopPortNode.getPorts().add(elkTopPort); + } + } + } + + Map instNodeMap = new HashMap<>(); + for (EDIFCellInst inst : cell.getCellInsts()) { + ElkNode elkInst = f.createElkNode(); + elkInst.setParent(parent); + parent.getChildren().add(elkInst); + EDIFHierCellInst childInst = cellInst.getChild(inst); + elkInst.setIdentifier(childInst.toString()); + elkInst.setProperty(CoreOptions.PORT_CONSTRAINTS, PortConstraints.FIXED_ORDER); + instNodeMap.put(childInst, elkInst); + elkNodeCellMap.put(elkInst, childInst); + + boolean isHierCell = !inst.getCellType().isLeafCellOrBlackBox(); + + // Create labels + elkInst.setProperty(CoreOptions.NODE_LABELS_PLACEMENT, NodeLabelPlacement.outsideTopCenter()); + labelElkNode(elkInst, inst.getName()); + labelElkNode(elkInst, inst.getCellName()); + + // Create ports + Map westPorts = new TreeMap<>(); + Map eastPorts = new TreeMap<>(); + double longestWestName = 0; + double longestEastName = 0; + // for (EDIFPort port : inst.getCellPorts()) { + for (EDIFHierPortInst hierPortInst : childInst.getHierPortInsts()) { + String name = hierPortInst.getPortInst().getName(); + if (hierPortInst.isInput()) { + westPorts.put(name, hierPortInst); + longestWestName = Math.max(longestWestName, fm.width(name)); + } else { + eastPorts.put(name, hierPortInst); + longestEastName = Math.max(longestEastName, fm.width(name)); + } + } + + int portStartIndex = 0; + portStartIndex = createElkPorts(portStartIndex, isHierCell, elkInst, westPorts, PortSide.WEST); + portStartIndex = createElkPorts(portStartIndex, isHierCell, elkInst, eastPorts, PortSide.EAST); + + // Calculate cell rectangle size + int maxPins = Math.max(westPorts.size(), eastPorts.size()); + double height = Math.max(MIN_NODE_HEIGHT, PORT_HEIGHT * maxPins); + double width = Math.max(MIN_NODE_WIDTH, longestWestName + longestEastName + PORT_NAME_BUFFER); + elkInst.setDimensions(width, height); + + if (isHierCell && expandedCellInsts.contains(prefix + inst.getName())) { + applyElkNodeProperties(elkInst); + // Extra spacing for button placement + elkInst.setProperty(CoreOptions.PADDING, new ElkPadding( + BUTTON_SIZE * 2, // Top + SIDE_PADDING, // Side + fm.height() + LABEL_BUFFER * 2, // Bottom + SIDE_PADDING // Side + )); + createExpandedCellInnerPorts(childInst); + populateCellContent(childInst, elkInst, prefix + inst.getName() + "/"); + } + } + + for (EDIFNet net : cell.getNets()) { + List drivers = new ArrayList<>(); + List sinks = new ArrayList<>(); + + for (EDIFPortInst p : net.getPortInsts()) { + EDIFHierPortInst hierPortInst = new EDIFHierPortInst(cellInst, p); + if (p.isTopLevelPort()) { + if (p.isOutput()) { + sinks.add(hierPortInst); + } else { + drivers.add(hierPortInst); + } + } else { + if (p.isOutput()) { + drivers.add(hierPortInst); + } else { + sinks.add(hierPortInst); + } + } + } + + for (EDIFHierPortInst d : drivers) { + ElkPort driver = getOrCreateElkPort(d, prefix, instNodeMap, cellInst); + for (EDIFHierPortInst s : sinks) { + ElkPort sink = getOrCreateElkPort(s, prefix, instNodeMap, cellInst); + if (driver == null || sink == null) + continue; + + ElkEdge edge = ElkGraphFactory.eINSTANCE.createElkEdge(); + edge.setContainingNode(parent); + edge.setIdentifier(cellInst.toString() + "/" + net.getName()); + edge.getSources().add(driver); + edge.getTargets().add(sink); + parent.getContainedEdges().add(edge); + } + } + } + } + + private void createExpandedCellInnerPorts(EDIFHierCellInst inst) { + for (EDIFPort port : inst.getCellType().getPorts()) { + for (int i : (port.isBus() ? port.getBitBlastedIndicies() : new int[] { 0 })) { + EDIFPortInst outerPortInst = inst.getInst().getPortInst(port.getPortInstNameFromPort(i)); + ElkPort outerElkPort = portInstMap.get(outerPortInst); + // Map the inner port inst to the outer one so nets are aligned + if (outerElkPort != null) { + EDIFPortInst innerPortInst = port.getInternalPortInstFromIndex(i); + if (innerPortInst != null) { + portInstMap.put(inst.getPortInst(innerPortInst.getName()), outerElkPort); + } + } + } + } + } + + private ElkPort getOrCreateElkPort(EDIFHierPortInst p, String prefix, Map instNodeMap, + EDIFHierCellInst cellInst) { + ElkPort port = portInstMap.get(p); + if (port == null) { + port = ElkGraphFactory.eINSTANCE.createElkPort(); + if (p.getPortInst().isTopLevelPort()) { + p = cellInst.getPortInst(p.getPortInst().getName()); + port = portInstMap.get(p); + } else { + ElkNode inst = instNodeMap.get(p.getFullHierarchicalInst()); + port.setParent(inst); + inst.getPorts().add(port); + port.setIdentifier(p.getPortInst().getName()); + port.setDimensions(PORT_SIZE, PORT_SIZE); + port.setProperty(CoreOptions.PORT_SIDE, p.isOutput() ? PortSide.EAST : PortSide.WEST); + } + + portInstMap.put(p, port); + } + return port; + } + + private int createElkPorts(int startIdx, boolean isHierCell, ElkNode parent, Map portNames, + PortSide side) { + for (Entry e : portNames.entrySet()) { + ElkPort port = ElkGraphFactory.eINSTANCE.createElkPort(); + portInstMap.put(e.getValue(), port); + port.setParent(parent); + parent.getPorts().add(port); + port.setIdentifier(e.getKey()); + port.setProperty(CoreOptions.PORT_SIDE, side); + port.setProperty(CoreOptions.PORT_INDEX, startIdx++); + port.setDimensions(PORT_SIZE, PORT_SIZE); + + if (isHierCell) { + labelElkNode(port, e.getKey()); + } + + } + return startIdx; + } + + private void labelElkNode(ElkGraphElement n, String name) { + ElkLabel label = ElkGraphFactory.eINSTANCE.createElkLabel(); + label.setText(name); + label.setParent(n); + n.getLabels().add(label); + label.setDimensions(fm.width(label.getText()), fm.height()); + } + + public void mousePressEvent(QGraphicsSceneMouseEvent event) { + QGraphicsItemInterface item = itemAt(event.scenePos()); + if (item != null && item.data(0) != null) { + String data = item.data(0).toString(); + if (data.startsWith(HIER_BUTTON)) { + String[] parts = data.split(":"); + toggleCellInstExpansion(parts[1].trim()); + } else if (data.startsWith("INST:") || data.startsWith("NET:") || data.startsWith("PORT:")) { + boolean ctrlPressed = event.modifiers().isSet(KeyboardModifier.ControlModifier); + toggleSelection(data, ctrlPressed); + } + } + super.mousePressEvent(event); + } + + private void toggleCellInstExpansion(String cellInstName) { + if (expandedCellInsts.contains(cellInstName)) { + expandedCellInsts.remove(cellInstName); + } else { + expandedCellInsts.add(cellInstName); + } + boolean zoomFit = false; + drawCell(currCellInst, zoomFit); + } + + private void toggleSelection(String lookup, boolean multipleSelection) { + if (!multipleSelection) { + clearSelections(); + } + + if (selectedObjects.contains(lookup)) { + updateSelectionHighlight(lookup, false); + selectedObjects.remove(lookup); + } else { + updateSelectionHighlight(lookup, true); + selectedObjects.add(lookup); + } + + objectSelected.emit(lookup); + } + + private void clearSelections() { + for (String unselected : selectedObjects) { + updateSelectionHighlight(unselected, false); + } + selectedObjects.clear(); + } + + public void selectObject(String lookup, boolean clearPreviousSelections) { + if (clearPreviousSelections) { + clearSelection(); + } + selectedObjects.remove(lookup); + toggleSelection(lookup, !clearPreviousSelections); + } + + private void updateSelectionHighlight(String lookup, boolean isSelected) { + List guiObjects = lookupMap.get(lookup); + for (Object guiObject : guiObjects == null ? Collections.EMPTY_LIST : guiObjects) { + if (guiObject instanceof QAbstractGraphicsShapeItem) { + QAbstractGraphicsShapeItem shape = (QAbstractGraphicsShapeItem) guiObject; + if (isSelected) { + if (shape.data(1) == null || !shape.data(1).toString().equals(CLICK)) { + shape.setPen(SELECTED_PEN); + } + } else { + if (lookup.startsWith("INST:")) { + shape.setPen(CELL_PEN); + } else if (lookup.startsWith("PORT:")) { + shape.setPen(PORT_PEN); + } + } + } else if (guiObject instanceof QGraphicsLineItem) { + QGraphicsLineItem line = (QGraphicsLineItem) guiObject; + if (line.data(1) == null || !line.data(1).toString().equals(CLICK)) { + line.setPen(isSelected ? SELECTED_PEN : NET_PEN); + } + } + + } + } +} diff --git a/src/com/xilinx/rapidwright/gui/SchematicView.java b/src/com/xilinx/rapidwright/gui/SchematicView.java new file mode 100644 index 000000000..866a2dde8 --- /dev/null +++ b/src/com/xilinx/rapidwright/gui/SchematicView.java @@ -0,0 +1,181 @@ +/* + * + * Copyright (c) 2025, Advanced Micro Devices, Inc. + * All rights reserved. + * + * Author: Chris Lavin, AMD Advanced Research and Development. + * + * This file is part of RapidWright. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.xilinx.rapidwright.gui; + +import com.trolltech.qt.core.QPoint; +import com.trolltech.qt.core.QPointF; +import com.trolltech.qt.core.QRectF; +import com.trolltech.qt.core.Qt; +import com.trolltech.qt.core.Qt.AspectRatioMode; +import com.trolltech.qt.core.Qt.CursorShape; +import com.trolltech.qt.core.Qt.Key; +import com.trolltech.qt.gui.QCursor; +import com.trolltech.qt.gui.QGraphicsScene; +import com.trolltech.qt.gui.QGraphicsView; +import com.trolltech.qt.gui.QKeyEvent; +import com.trolltech.qt.gui.QMouseEvent; +import com.trolltech.qt.gui.QWheelEvent; + +public class SchematicView extends QGraphicsView { + + private boolean rightPressed; + private QPoint lastPan; + + /** The maximum value to which we can zoom out */ + protected static double zoomMin = 0.05; + /** The maximum value to which we can zoom in */ + protected static double zoomMax = 30; + /** The rate at which we zoom */ + protected static double scaleFactor = 1.15; + + public SchematicView(QGraphicsScene scene) { + super(scene); + } + + /** + * This method is called when any mouse button is pressed. In this case, a right + * click will allow the user to pan the array of tiles. + */ + public void mousePressEvent(QMouseEvent event) { + if (event.button().equals(Qt.MouseButton.RightButton)) { + // For panning the view + rightPressed = true; + lastPan = event.pos(); + setCursor(new QCursor(CursorShape.ClosedHandCursor)); + } + super.mousePressEvent(event); + } + + /** + * This method is called when any mouse button is released. In this case, this + * will disallow the user to pan. + */ + public void mouseReleaseEvent(QMouseEvent event) { + if (event.button().equals(Qt.MouseButton.RightButton)) { + rightPressed = false; + setCursor(new QCursor(CursorShape.ArrowCursor)); + } + super.mouseReleaseEvent(event); + } + + /** + * This method is called when the mouse moves in the window. This will reset the + * window based on the mouse panning. + */ + public void mouseMoveEvent(QMouseEvent event) { + if (rightPressed) { + if (lastPan != null && !lastPan.isNull()) { + // Get how much we panned + QPointF s1 = mapToScene(new QPoint((int) lastPan.x(), (int) lastPan.y())); + QPointF s2 = mapToScene(new QPoint((int) event.pos().x(), (int) event.pos().y())); + QPointF delta = new QPointF(s1.x() - s2.x(), s1.y() - s2.y()); + lastPan = event.pos(); + // Scroll the scrollbars ie. do the pan + double zoom = this.matrix().m11(); + this.horizontalScrollBar().setValue((int) (this.horizontalScrollBar().value() + zoom * delta.x())); + this.verticalScrollBar().setValue((int) (this.verticalScrollBar().value() + zoom * delta.y())); + } + } + super.mouseMoveEvent(event); + } + + /** + * This method is called when the mouse wheel or scroll is used. In this case, + * it allows the user to zoom in and out of the array of tiles. + */ + public void wheelEvent(QWheelEvent event) { + // Get the position of the mouse before scaling, in scene coords + QPointF pointBeforeScale = mapToScene(event.pos()); + + // Scale the view ie. do the zoom + double zoom = this.matrix().m11(); + if (event.delta() > 0) { + // Zoom in (if not at limit) + if (zoom < zoomMax) + scale(scaleFactor, scaleFactor); + } else { + // Zoom out (if not at limit) + if (zoom > zoomMin) + scale(1.0 / scaleFactor, 1.0 / scaleFactor); + } + + // Read the new zoom value + zoom = this.matrix().m11(); + + // Get the position after scaling, in scene coords + QPointF pointAfterScale = mapToScene(event.pos()); + + // Get the offset of how the screen moved + QPointF offset = new QPointF(pointBeforeScale.x() - pointAfterScale.x(), + pointBeforeScale.y() - pointAfterScale.y()); + this.horizontalScrollBar().setValue((int) (this.horizontalScrollBar().value() + zoom * offset.x())); + this.verticalScrollBar().setValue((int) (this.verticalScrollBar().value() + zoom * offset.y())); + } + + /** + * This method gets called when a key on the keyboard is pressed. In this case, + * if the '=' key is pressed, it zooms in. If the '-' key is pressed, it zooms + * out. + */ + public void keyPressEvent(QKeyEvent event) { + double scaleFactor = 1.15; + if (event.key() == Key.Key_Equal.value()) { + // Zoom in (if not at limit) + if (this.matrix().m11() < zoomMax) + scale(scaleFactor, scaleFactor); + } else if (event.key() == Key.Key_Minus.value()) { + // Zoom out (if not at limit) + if (this.matrix().m11() > zoomMin) + scale(1.0 / scaleFactor, 1.0 / scaleFactor); + } + } + + public void zoomIn() { + // Zoom in (if not at limit) + if (this.matrix().m11() < zoomMax) + scale(scaleFactor, scaleFactor); + } + + public void zoomOut() { + // Zoom out (if not at limit) + if (this.matrix().m11() > zoomMin) + scale(1.0 / scaleFactor, 1.0 / scaleFactor); + } + + public void zoomToFit() { + QRectF sceneRect = scene().sceneRect(); + if (sceneRect != null) { + fitInView(sceneRect, AspectRatioMode.KeepAspectRatio); + double zoom = this.matrix().m11(); + if (zoom > zoomMax) { + resetMatrix(); + scale(zoomMax, zoomMax); + fitInView(sceneRect, AspectRatioMode.KeepAspectRatio); + } else if (zoom < zoomMin) { + resetMatrix(); + scale(zoomMin, zoomMin); + fitInView(sceneRect, AspectRatioMode.KeepAspectRatio); + } + } + } +}