/** * 将各个节点转换为 elknode 的数据结构 */ import { globalLookup } from "../global"; import { pinkLog } from "../utils"; import { Cell, dotConnect, ModuleView } from "./yosys"; // 线段的宽度 export const LINE_WIDTH = 2; // 分组最小 ID export const MIN_PARTITION_INDEX = 1; // 分组最大 ID export const MAX_PARTITION_INDEX = 10000000; // 每一个例化最多能占据的列数 export const MAX_INSTANCE_COL_NUM = 1000; // 每一列最多的同类器件数量 export const MAX_SAME_TYPE_INSTANCE_NUM = 5; // 其他常量 export const LAYOUT_CONSTANT = { // port PORT_WIDTH: 50, PORT_HEIGHT: 25, // 例化模块(这只是最小,height 会根据端口数量进行调整) INSTANTIATION_WIDTH: 80, INSTANTIATION_HEIGHT: 80, // port 顶部的空余,一般用来放置标签的 PORT_TOP_MARGIN: 30, // port 内部的 padding,用于绘制 PORT_INNER_PADDING: 20, // 例化模块上部分的空缺区域 INSTANCE_TITLE_HEIGHT: 10, INSTANCE_TOP_PADDING: 20, INSTANCE_LEFT_MARGIN: 10, INSTANCE_RIGHT_MARGIN: 10, // 常数 CONSTANT_WIDTH: 50, CONSTANT_HEIGHT: 30, // 器件的端口 CELL_PORT_HEIGHT: 1, CELL_PORT_WIDTH: 1, CELL_LEFT_MARGIN: 10, CELL_RIGHT_MARGIN: 10 }; export const ELK_DIRECTION = { LEFT: 'WEST', RIGHT: 'EAST', TOP: 'NORTH', BOTTOM: 'SOUTH' } export function makeEdgeId(fromId, toId) { return fromId + '-' + toId; } export class Module { /** * @param {string} name * @param {YosysNetModule} module */ constructor(name, module) { this.module = module; this.name = name; /** * @description * @type {ModuleView} */ this.view = new ModuleView(name, module); this.commonPartitionIndex = 10; this.partitionIndex = 11; /** * @description 统计一个例化模块 name 出现的次数,用来分配布局分组索引 * @type {Map} */ this.instanceNameCounter = new Map(); } /** * @description 从 ports 中创建所有变量相关的节点 * @returns {ElkNode[]} */ makeNetsElkNodes() { const nodes = []; // 绘制 ports for (const name of this.view.nameToPort.keys()) { const port = this.view.nameToPort.get(name); // 根据长度估算 name 的渲染长度 const estimateLength = Math.max(12 * 0.6 * name.length + 10, LAYOUT_CONSTANT.PORT_WIDTH); if (port.direction === 'input') { // 为 port 设置连接点 const portConnection = { id: dotConnect(port.id, '0'), renderType: 'portConnection', source: 'port', // 对于 input 的 port 而言,它的端口只有一个且为输出 direction: 'output', width: LAYOUT_CONSTANT.CELL_PORT_WIDTH, height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT, x: estimateLength, y: LAYOUT_CONSTANT.PORT_HEIGHT / 2 }; const node = { id: port.id, name: port.name, renderName: name, renderType: 'port', source: 'port', // 对于 input 的 port 而言,它的端口只有一个且为输入 direction: 'input', type: port.direction, width: estimateLength, height: LAYOUT_CONSTANT.PORT_HEIGHT, ports: [portConnection], layoutOptions: { 'partitioning.partition': MIN_PARTITION_INDEX, 'org.eclipse.elk.portConstraints': 'FIXED_POS' } }; nodes.push(node); } else { // 为 port 设置连接点 const portConnection = { id: dotConnect(port.id, '0'), renderType: 'portConnection', width: LAYOUT_CONSTANT.CELL_PORT_WIDTH, height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT, x: 0, y: LAYOUT_CONSTANT.PORT_HEIGHT / 2 }; const node = { id: port.id, name: port.name, renderName: name, renderType: 'port', type: port.direction, width: estimateLength, height: LAYOUT_CONSTANT.PORT_HEIGHT, ports: [portConnection], layoutOptions: { 'partitioning.partition': MAX_PARTITION_INDEX, 'org.eclipse.elk.portConstraints': 'FIXED_POS' } }; nodes.push(node); } } // 非 port 的其他内部变量的节点不需要绘制,因为它们总能被表示为中间变量 // 它们的操作可以表示为一系列器件的组合或者直连 return nodes; } /** * @description 从 cells(器件) 中创建节点 * @returns {[ElkNode[], ElkEdge[]]} */ makeCellsElkNodes() { const nodes = []; const edges = []; const skinManager = globalLookup.skinManager; for (const name of this.view.nameToCell.keys()) { const cell = this.view.nameToCell.get(name); const skin = skinManager.querySkin(cell.type); if (skin) { const node = this.makeCellNode(cell, skin); nodes.push(node); } else { const node = this.makeInstanceNode(cell); nodes.push(node); } } return nodes; } /** * @description 从 cells.connections 中创建边 * @returns {[ElkNode[], ElkEdge[]]} */ makeConnectionElkNodes() { const nodes = []; // 完成去重 const id2EdgeCount = new Map(); const edges = []; const tree = this.view; for (const cellName of tree.nameToCell.keys()) { const cell = tree.nameToCell.get(cellName); for (const connectionName of cell.nameToConnection.keys()) { const connection = cell.nameToConnection.get(connectionName); // 遍历器件端口的每一个连接点 // 比如对于端口 input [31:0] data ,它的32个位不一定是完全导向一个变量(虽然我们愿意认为,大部分情况下是这样) // 大部分情况下,👇的 wireIds.length 都是 1 for (const wire of connection.wires) { const wireId = wire.wireId; const id = wire.id; if (typeof wireId === 'string') { // 常数 // 如果是常数,需要先创建代表常数的节点,常数一定是器件的输入,而非输出 // 估算常量的宽度 let renderString = parseInt(wireId, 2); if (isNaN(renderString)) { if (wireId.toString().startsWith('x')) { renderString = `${wireId.toString().length}{1'bx}`; } else { renderString = wireId.toString(); } } const constantWidth = Math.max(12 * 0.6 * renderString.toString().length + 10, LAYOUT_CONSTANT.CONSTANT_WIDTH); const constantConnection = { id: dotConnect(id, '0'), renderType: 'portConnection', source: 'port', // 对于 constant 的 port 而言,它的端口只有一个且为输出 direction: 'output', width: LAYOUT_CONSTANT.CELL_PORT_WIDTH, height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT, x: constantWidth, y: LAYOUT_CONSTANT.CONSTANT_HEIGHT / 2 }; const node = { id, name: wireId, renderType: 'constant', width: constantWidth, height: LAYOUT_CONSTANT.CONSTANT_HEIGHT, ports: [constantConnection], layoutOptions: { 'partitioning.partition': this.commonPartitionIndex, 'org.eclipse.elk.portConstraints': 'FIXED_POS' } }; nodes.push(node); // 创建常数到器件的连线 const edge = { id: makeEdgeId(cell.id, id), source: id, sourcePort: constantConnection.id, target: cell.id, targetPort: connection.id }; edges.push(edge); } else { // 如果不是一个常数连接,那么存在两种可能 // 1. 当前的器件的这个端口和某一个 port 连接 // 2. 当前的器件的这个端口和另一个器件的一个端口连接 if (tree.wireIdToPort.has(wireId)) { // 当前的器件的这个端口和某一个 port 连接 const port = tree.wireIdToPort.get(wireId); // 器件的端口的方向有比 port 更高的优先级。 // 器件当前的口为 input,那么所连接的 port 就必须是 input,即便这个 port 是 output if (connection.direction === 'input') { const edgeId = makeEdgeId(port.id, connection.id); if (!id2EdgeCount.has(edgeId)) { id2EdgeCount.set(edgeId, 0); const edge = { // id 遵循 sourcePort-targetPort id: edgeId, source: port.id, sourcePort: dotConnect(port.id, '0'), target: cell.id, targetPort: connection.id }; edges.push(edge); } const counter = id2EdgeCount.get(edgeId); id2EdgeCount.set(edgeId, counter + 1); } else { const edgeId = makeEdgeId(connection.id, port.id); if (!id2EdgeCount.has(edgeId)) { id2EdgeCount.set(edgeId, 0); const edge = { id: edgeId, source: cell.id, sourcePort: connection.id, target: port.id, targetPort: dotConnect(port.id, '0') }; edges.push(edge); } const counter = id2EdgeCount.get(edgeId); id2EdgeCount.set(edgeId, counter + 1); } } else if (tree.wireIdToConnection.has(wireId)) { // 当前的器件的这个端口和另一个器件的一个端口连接 const conn = tree.wireIdToConnection.get(wireId); // 防止查询到自己 if (conn.id === connection.id && conn.cell.id === cell.id) { continue; } if (connection.direction === 'output') { const edgeId = makeEdgeId(conn.id, connection.id); if (!id2EdgeCount.has(edgeId)) { id2EdgeCount.set(edgeId, 0); const edge = { // id 遵循 sourcePort-targetPort id: edgeId, source: cell.id, sourcePort: connection.id, target: conn.cell.id, targetPort: conn.id }; edges.push(edge); } const counter = id2EdgeCount.get(edgeId); id2EdgeCount.set(edgeId, counter + 1); } else { const edgeId = makeEdgeId(connection.id, conn.id); if (!id2EdgeCount.has(edgeId)) { id2EdgeCount.set(edgeId, 0); const edge = { id: edgeId, source: conn.cell.id, sourcePort: conn.id, target: cell.id, targetPort: connection.id }; edges.push(edge); } const counter = id2EdgeCount.get(edgeId); id2EdgeCount.set(edgeId, counter + 1); } } } } } } pinkLog('#edge: ' + edges.length); this.id2EdgeCount = id2EdgeCount; return [nodes, edges]; } /** * @description 制作器件对应的 node * @param {Cell} cell * @param {import("../skin").SkinResource} skin */ makeCellNode(cell, skin) { // 内置器件 // 创建器件节点的 port, port 和 connection 一一对应 const meta = skin.meta; const height = meta.height; const width = meta.width + LAYOUT_CONSTANT.CELL_LEFT_MARGIN + LAYOUT_CONSTANT.CELL_RIGHT_MARGIN; const ports = []; // 统计分配到左右两侧的 port const leftSideConnections = []; const rightSideConnections = []; const topSideConnections = []; for (const connection of cell.nameToConnection.values()) { const yOffset = meta.getPortYOffset(connection.name); if (yOffset === 0) { topSideConnections.push(connection); } else if (connection.direction === 'input') { leftSideConnections.push(connection); } else { rightSideConnections.push(connection); } } // 计算上侧的 for (let i = 0; i < topSideConnections.length; ++ i) { const connection = topSideConnections[i]; const xOffset = meta.getPortXOffset(connection.name); ports.push({ id: connection.id, renderName: connection.name, renderType: 'cellPort', direction: 'input', source: 'cell', isVertical: true, width: LAYOUT_CONSTANT.CELL_PORT_WIDTH, height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT, x: xOffset, y: 0 }); } // 计算左侧的 for (let i = 0; i < leftSideConnections.length; ++ i) { const connection = leftSideConnections[i]; const yOffset = meta.getPortYOffset(connection.name); ports.push({ id: connection.id, renderName: connection.name, renderType: 'cellPort', direction: 'input', source: 'cell', width: LAYOUT_CONSTANT.CELL_PORT_WIDTH, height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT, x: 0, y: yOffset }); } // 计算右侧的 for (let i = 0; i < rightSideConnections.length; ++ i) { const connection = rightSideConnections[i]; const yOffset = meta.getPortYOffset(connection.name); ports.push({ id: connection.id, renderName: connection.name, renderType: 'cellPort', direction: 'output', source: 'cell', width: LAYOUT_CONSTANT.CELL_PORT_WIDTH, height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT, x: width, y: yOffset }); } const node = { id: cell.id, name: cell.name, renderName: cell.type, renderType: 'cell', width, height, ports, layoutOptions: { 'org.eclipse.elk.portConstraints': 'FIXED_POS', 'partitioning.partition': this.commonPartitionIndex } }; return node; } /** * @description 制作例化模块对应的 node * @param {Cell} cell */ makeInstanceNode(cell) { // 统计分配到左右两侧的 port const leftSideConnections = []; const rightSideConnections = []; for (const conn of cell.nameToConnection.values()) { if (conn.direction === 'input') { leftSideConnections.push(conn); } else { rightSideConnections.push(conn); } } // 根据一侧的 port 数量来决定 height const maxPortNum = Math.max(leftSideConnections.length, rightSideConnections.length); const instanceHeight = Math.max( // 这是例化模块高度的最小值 LAYOUT_CONSTANT.INSTANTIATION_HEIGHT, // 每一个 connection 占据的空间为:【上方空余区域(用来防止文本的)】 + 【本身的高度】 (LAYOUT_CONSTANT.PORT_TOP_MARGIN + LAYOUT_CONSTANT.CELL_PORT_HEIGHT) * (maxPortNum + 1) ); // 宽度等于预设宽度 + 两倍的 inner padding + left margin + right margin const instanceWidth = LAYOUT_CONSTANT.INSTANTIATION_WIDTH + 2 * LAYOUT_CONSTANT.PORT_INNER_PADDING + LAYOUT_CONSTANT.INSTANCE_LEFT_MARGIN + LAYOUT_CONSTANT.INSTANCE_RIGHT_MARGIN; // 创建器件节点的 port, port 和 connection 一一对应 const ports = []; let inputCount = 0; let outputCount = 0; for (const connectionName of cell.nameToConnection.keys()) { const connection = cell.nameToConnection.get(connectionName); const portSide = connection.direction === 'input' ? ELK_DIRECTION.LEFT: ELK_DIRECTION.RIGHT; if (connection.direction === 'input') { inputCount ++; const offsetY = inputCount * LAYOUT_CONSTANT.PORT_TOP_MARGIN + (inputCount - 1) * LAYOUT_CONSTANT.CELL_PORT_HEIGHT + LAYOUT_CONSTANT.INSTANCE_TITLE_HEIGHT + LAYOUT_CONSTANT.INSTANCE_TOP_PADDING; ports.push({ id: connection.id, renderName: connectionName, renderType: 'cellPort', direction: 'input', source: 'instance', width: LAYOUT_CONSTANT.CELL_PORT_WIDTH, height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT, x: 0, y: offsetY }); } else { outputCount ++; const offsetY = outputCount * LAYOUT_CONSTANT.PORT_TOP_MARGIN + (outputCount - 1) * LAYOUT_CONSTANT.CELL_PORT_HEIGHT + LAYOUT_CONSTANT.INSTANCE_TITLE_HEIGHT + LAYOUT_CONSTANT.INSTANCE_TOP_PADDING; ports.push({ id: connection.id, renderName: connectionName, renderType: 'cellPort', direction: 'output', source: 'instance', width: LAYOUT_CONSTANT.CELL_PORT_WIDTH, height: LAYOUT_CONSTANT.CELL_PORT_HEIGHT, x: instanceWidth, y: offsetY }); } } const node = { id: cell.id, name: cell.name, parentName: this.name, renderName: cell.type, renderType: 'instance', width: instanceWidth, height: instanceHeight + LAYOUT_CONSTANT.INSTANCE_TITLE_HEIGHT + LAYOUT_CONSTANT.INSTANCE_TOP_PADDING, ports, layoutOptions: { // 强制固定 port 的方向 'org.eclipse.elk.portConstraints': 'FIXED_POS', // TODO: 同名例化模块对齐 'partitioning.partition': this.commonPartitionIndex } }; return node; } } /** * * @param {import('../jsdoc').ElkPort} port * @returns {ConnectionMarginParameter} */ export function getMarginParamter(port) { if (!port) { // 如果是例化模块,那么当前的 port 就是空 // 因为例化模块内部的变量一定和外部直连,所以例化模块内部是没有 port 的 return { leftMargin: 0, rightMargin: 0 } } switch (port.source) { case 'instance': return { leftMargin: LAYOUT_CONSTANT.INSTANCE_LEFT_MARGIN, rightMargin: LAYOUT_CONSTANT.INSTANCE_RIGHT_MARGIN } case 'cell': return { leftMargin: LAYOUT_CONSTANT.CELL_LEFT_MARGIN, rightMargin: LAYOUT_CONSTANT.CELL_RIGHT_MARGIN } default: return { leftMargin: 0, rightMargin: 0 } } } /** * @typedef ConnectionMarginParameter * @property {number} leftMargin * @property {number} rightMargin */