diff --git a/package-lock.json b/package-lock.json index ab4dfd1..13271bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "elkjs": "^0.9.3", "mitt": "^3.0.1", "vue": "^3.2.13", - "vue-i18n": "10.0.5" + "vue-i18n": "10.0.5", + "web-worker": "^1.3.0" }, "devDependencies": { "@babel/core": "^7.12.16", @@ -11345,6 +11346,11 @@ "defaults": "^1.0.3" } }, + "node_modules/web-worker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index c77471c..7e5bea2 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "elkjs": "^0.9.3", "mitt": "^3.0.1", "vue": "^3.2.13", - "vue-i18n": "10.0.5" + "vue-i18n": "10.0.5", + "web-worker": "^1.3.0" }, "devDependencies": { "@babel/core": "^7.12.16", diff --git a/src/App.vue b/src/App.vue index a73b719..ae46305 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,6 +8,7 @@ import { useI18n } from 'vue-i18n'; import RightNav from '@/components/right-nav.vue'; import { onMounted } from 'vue'; import { setDefaultCss } from './hook/css'; +import { NetlistRender } from './hook/render'; const { t } = useI18n(); @@ -18,7 +19,8 @@ onMounted(async () => { // 初始化载入 netlist 的 json 文件 const [ netJson ] = await window.readNetFile(); - console.log(netJson); + const render = new NetlistRender(netJson); + await render.createLayout(); }); diff --git a/src/hook/jsdoc.js b/src/hook/jsdoc.js index 5c80c9b..977b28d 100644 --- a/src/hook/jsdoc.js +++ b/src/hook/jsdoc.js @@ -68,10 +68,15 @@ /** * @typedef ModuleAttribute - * @property {string} top - 表示该模块是否为顶层模块(1 表示是,0 表示否)。 - * @property {string} src - 模块定义的源文件和行号。 - * @property {string} [dynports] - 动态端口标志(1 表示是,0 表示否)。 - * @property {string} [hdlname] - HDL 名称。 + * @property {string} [top] 表示该模块是否为顶层模块(1 表示是,0 表示否)。 + * @property {string} src 模块定义的源文件和行号。 + * @property {string} [cells_not_processed] 代表当前模块是否没有被处理 + * + * 比如对于一个 main.v 而言,它内部定义了两个模块 A 和 B。如果只综合了 A,那么 B 也会出现在 yosys json 中 + * 但是 B 的会额外出现 `cells_not_processed` 这个字段,且值为 "00000000000000000000000000000001",代表没有处理 + * + * @property {string} [dynports] 动态端口标志(1 表示是,0 表示否)。 + * @property {string} [hdlname] HDL 名称。 */ /** @@ -142,7 +147,11 @@ /** * @typedef ElkNode - * @property {string} id - 节点的唯一标识符。 + * @property {string} id 节点的唯一标识符。 + * @property {ElkNode[]} children 当前节点的内部 + * @property {ElkPort[]} ports 当前节点的端口 + * @property {ElkEdge[]} edges 当前节点的连线 + * @property {string} type 节点的类型 * @property {number} [x] - 节点的 X 坐标(可选,由布局算法生成)。 * @property {number} [y] - 节点的 Y 坐标(可选,由布局算法生成)。 * @property {number} [width] - 节点的宽度(可选)。 @@ -175,5 +184,5 @@ * @property {string} [elk.direction] - 布局方向(可选,如 "RIGHT", "DOWN" 等)。 * @property {number} [elk.spacing.nodeNode] - 节点之间的间距(可选)。 * @property {number} [elk.spacing.edgeNode] - 边与节点之间的间距(可选)。 - * @property {string} [elk.port.side] - 端口的位置(可选,如 "NORTH", "SOUTH", "EAST", "WEST")。 + * @property {'NORTH' | 'SOUTH' | 'EAST' | 'WEST'} [elk.port.side] - 端口的位置(可选,如 "NORTH", "SOUTH", "EAST", "WEST")。 */ \ No newline at end of file diff --git a/src/hook/render/index.js b/src/hook/render/index.js index 40484b4..c6d69d6 100644 --- a/src/hook/render/index.js +++ b/src/hook/render/index.js @@ -1,7 +1,10 @@ import * as d3 from 'd3'; -import { ModuleLayout } from './layout'; +import ELK from 'elkjs'; -class NetlistRender { + +import { Module } from './layout'; + +export class NetlistRender { /** * * @param {YosysRawNet} rawNet @@ -21,6 +24,11 @@ class NetlistRender { edges: [] }; + /** + * @type {Map} + */ + this.nameToModule = new Map(); + this.loadYosysRawNet(rawNet); } @@ -29,11 +37,36 @@ class NetlistRender { * @param {YosysRawNet} rawNet */ loadYosysRawNet(rawNet) { - // 转换为 elkjs 格式的 graph - for (const [moduleName, module] of Object.entries(rawNet.modules)) { - + for (const [moduleName, rawModule] of Object.entries(rawNet.modules)) { + const top = parseInt(rawModule.attributes.top); + // 一开始只渲染 top 模块 + if (top) { + const module = new Module(moduleName, rawModule); + const portNodes = layout.makeNetsElkNodes(); + const cellNodes = layout.makeCellsElkNodes(); + const [constantNodes, connectionEdges] = layout.makeConnectionElkNodes(); + + this.elkGraph.children.push(...portNodes); + this.elkGraph.children.push(...cellNodes); + this.elkGraph.children.push(...constantNodes); + this.elkGraph.edges.push(...connectionEdges); + + this.nameToModule.set(moduleName, module); + } } } + /** + * @description 根据信息创建布局对象 + * @returns {Promise} + */ + async createLayout() { + const elk = new ELK(); + const graph = this.elkGraph; + const layoutGraph = await elk.layout(graph, {}); + console.log(layoutGraph); + + return layoutGraph + } } \ No newline at end of file diff --git a/src/hook/render/layout.js b/src/hook/render/layout.js index b8093cb..5adba8e 100644 --- a/src/hook/render/layout.js +++ b/src/hook/render/layout.js @@ -10,9 +10,19 @@ export const LAYOUT_CONSTANT = { INSTANTIATION_WIDTH: 50, INSTANTIATION_HEIGHT: 50, CONSTANT_WIDTH: 50, - CONSTANT_HEIGHT: 50 + CONSTANT_HEIGHT: 50, + + CELL_PORT_HEIGHT: 10, + CELL_PORT_WIDTH: 5 }; +export const ELK_DIRECTION = { + LEFT: 'WEST', + RIGHT: 'EAST', + TOP: 'NORTH', + BOTTOM: 'SOUTH' +} + function getCellSize(cellType) { if (CELL_LIBS[cellType]) { return { @@ -31,7 +41,7 @@ function makeEdgeId(fromId, toId) { return 'edge-' + fromId + '-' + toId; } -export class ModuleLayout { +export class Module { /** * @param {string} name * @param {YosysNetModule} module @@ -44,7 +54,7 @@ export class ModuleLayout { * @description * @type {ModuleTree} */ - this.moduleTree = new ModuleTree(name, moduleTree); + this.moduleTree = new ModuleTree(name, module); } /** @@ -57,15 +67,11 @@ export class ModuleLayout { // 绘制 ports for (const name of this.moduleTree.nameToPort.keys()) { const port = this.moduleTree.nameToPort.get(name); - const side = port.direction === 'input' ? 'WEST' : 'EAST'; const node = { id: port.id, width: LAYOUT_CONSTANT.PORT_WIDTH, height: LAYOUT_CONSTANT.PORT_HEIGHT, - layoutOptions: { - 'elk.port.side': side - } }; nodes.push(node); @@ -78,23 +84,38 @@ export class ModuleLayout { } /** - * @description 从 cells 中创建节点 - * @returns {ElkNode[]} + * @description 从 cells(器件) 中创建节点 + * @returns {[ElkNode[], ElkEdge[]]} */ makeCellsElkNodes() { const nodes = []; + const edges = []; for (const name of this.moduleTree.nameToCell.keys()) { const cell = this.moduleTree.nameToCell.get(name); const { height, width } = getCellSize(cell.type); - const side = 'NORTH'; + + // 创建器件节点的 port, port 和 connection 一一对应 + const ports = []; + for (const connectionName of cell.nameToConnection) { + const connection = cell.nameToConnection.get(connectionName); + const portSide = connection.direction === 'input' ? ELK_DIRECTION.LEFT: ELK_DIRECTION.RIGHT; + ports.push({ + id: connection.id, + width: LAYOUT_CONSTANT.CELL_PORT_WIDTH, + height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT, + properties: { + 'port.side': portSide, + 'allowNonFlowPortsToSwitchSides': true + } + }) + } const node = { id: cell.id, - width, height, - layoutOptions: { - 'elk.port.side': side - } + width, + height, + ports } nodes.push(node); @@ -120,9 +141,6 @@ export class ModuleLayout { for (const connectionName of cell.nameToConnection.keys()) { const connection = cell.nameToConnection.get(connectionName); - // 器件当前的端口的 ID,为了区别与外层 module 的端口,起名为 connection - const connectionId = connection.id; - // 遍历器件端口的每一个连接点 // 比如对于端口 input [31:0] data ,它的32个位不一定是完全导向一个变量(虽然我们愿意认为,大部分情况下是这样) // 大部分情况下,👇的 wireIds.length 都是 1 @@ -185,38 +203,3 @@ export class ModuleLayout { } - -/** - * @description 使用 . 把各个部分连接起来 - * @param {string[]} args - */ -export function dotConnect(...args) { - const stringArgs = args.map(arg => arg.toString()); - return stringArgs.join('.'); -} - -export namespace ElkNodeId { - export function module(moduleName) { - return dotConnect(moduleName); - } - - export function port(moduleName, portName) { - return dotConnect(moduleName, portName); - } - - export function cell(moduleName, cellName) { - return dotConnect(moduleName, cellName); - } - - export function connection(moduleName, cellName, connectionName) { - return dotConnect(moduleName, cellName, connectionName); - } - - export function constantBit(moduleName, cellName, connectionName, orderId, constant) { - return dotConnect(moduleName, cellName, connectionName, orderId, 'constant-' + constant); - } - - export function commonBit(moduleName, cellName, connectionName, wireId) { - return dotConnect(moduleName, cellName, connectionName, wireId); - } -} \ No newline at end of file diff --git a/src/hook/render/yosys.js b/src/hook/render/yosys.js index e1aef1b..339f75a 100644 --- a/src/hook/render/yosys.js +++ b/src/hook/render/yosys.js @@ -60,8 +60,8 @@ export class ModuleTree { */ this._wireIdToConnection = new Map(); - for (const [portName, port] of Object.entries(rawModule.ports)) { - const port = new Port(this, portName, port); + for (const [portName, rawPort] of Object.entries(rawModule.ports)) { + const port = new Port(this, portName, rawPort); this._nameToPort.set(portName, port); // 构建映射 for (const id of port.bits) { @@ -71,8 +71,8 @@ export class ModuleTree { } } - for (const [cellName, cell] of Object.entries(rawModule.cells)) { - const cell = new Cell(this, cellName, cell); + for (const [cellName, rawCell] of Object.entries(rawModule.cells)) { + const cell = new Cell(this, cellName, rawCell); this._nameToCell.set(cellName, cell); // 构建映射 for (const connection of cell.nameToConnection.values()) { @@ -181,6 +181,7 @@ export class Cell { } } + export class Connection { /** * @description connection 的抽象,一个 connection 代表一个器件的一个端口是如何与外界连接的 @@ -193,6 +194,11 @@ export class Connection { this.name = name; this.wireIds = wireIds; + /** + * @type {PortDirection} + */ + this.direction = cell.rawCell.port_directions[name]; + /** * @type {Wire[]} */ @@ -200,7 +206,7 @@ export class Connection { for (let i = 0; i < wireIds.length; ++ i) { const wire = new Wire(this, i, wireIds[i]); - this.wireIds.push(wire); + this.wires.push(wire); } }