简介:OwlVision GDSII Viewer是一款基于Java的开源版图查看工具,面向集成电路设计工程师与版图验证人员,用于高效浏览和分析GDSII格式的芯片几何布局。资源包共255个文件,压缩后约1.56MB,包含131个class字节码、104个java源文件、7个bat构建脚本以及少量txt说明、示例gdsii文件等,便于用户直接阅读源码、重新编译或对照学习。已有386人下载学习。包内还配有jar打包脚本和运行脚本,可快速搭建本地查看环境。借助这份开源代码,读者能了解GDSII解析、分层渲染、缩放平移等交互功能的实现思路,也可以基于现有工程扩展自定义测量与层次过滤工具,适合作为IC设计工具开发入门或相关课程设计参考。 刚接触版图相关工作时,我最大的困扰不是画版图,而是“看版图”。GDSII格式的文件从流片厂、设计公司、封装测试厂转一圈回来,尺寸动辄几百MB甚至几个GB,但身边合用的查看工具要么贵得离谱,要么启动一次要等几分钟。很多时候我只是想确认一下某个图层的位置、量一下几个点的坐标、截个图发到群里跟同事对齐信息,实在没必要把重型EDA工具搬出来。所以我自己动手做了一个开源的GDSII Viewer项目——OwlVision GDSII Viewer,今天就把这个项目的完整设计思路、实现细节和踩坑过程分享出来。
这个项目能干什么?一句话总结:纯本地运行、开源免费、启动快、可定制的GDSII只读查看器。它能解析GDSII二进制文件,渲染多边形、路径、文本、单元引用,支持缩放平移、图层显隐、单元树浏览,还能把当前视图导出成PNG或把坐标信息导出成CSV。适合版图工程师做快速审查、工艺和封装工程师核对图形信息、学生和科研人员学习版图数据结构,也适合想在自己工具链里嵌入一个轻量版图浏览器的开发者。
1. 项目设计与整体思路
1.1 为什么不自研轮子,还要再写一个查看器
说到开源GDSII查看器,很多人第一反应是KLayout。KLayout功能确实非常强大,支持脚本、宏、DRC、LVS等一大堆功能,是开源EDA工具链里绕不开的存在。但正因为功能太全,它的体量、启动速度、交互复杂度对只读快速预览这个场景来说反而成了负担。我在实际工作中频繁遇到以下几种情况:
- 打开一个400MB的GDS文件,只是确认顶层cell图层的叠放顺序;
- 需要把某个局部区域截图贴到文档或群里;
- 需要快速读出某条path的坐标序列,或者数一数有多少个cell实例;
- 想做一个自动化脚本,在CI流程里把版图导出成缩略图;
- 想在课堂上给学生讲GDSII格式,但用完整的商业工具讲起来太重。
这些场景的共同特点是:不需要编辑能力,不需要验证能力,只需要“快速、清晰地看到内容”。与其在功能复杂的工具里翻找入口,不如自己写一个专注的、代码量可控的查看器。这也是OwlVision项目最原始的出发点——回归查看这个动作本身。
1.2 技术选型:为什么是Python + PySide6
浏览器的技术路线,我先后对比了三套方案,各有取舍:
| 方案 | 优势 | 劣势 |
|---|---|---|
| Python + PySide6 + QPainter | 开发快、跨平台、GUI代码简单、与解析器天然解耦 | CPU渲染,超大版图性能有限 |
| C++ + Qt + OpenGL | 渲染性能最强、能扛极大文件 | 开发周期长、构建环境复杂、对新手不友好 |
| Web (Canvas/WebGL) | 免安装、易分享部署 | 大文件IO和内存管理受限,交互体验不如桌面 |
我最终选了Python + PySide6,核心原因有两点。第一,解析GDSII本身是纯逻辑计算,瓶颈主要在IO和对象创建,Python配合内置的内存优化完全能应付大多数场景,而图形显示用QPainter做CPU渲染,在中等规模版图上帧率表现足够好;第二,Python生态里做二进制定制解析非常顺手,struct模块可以直接处理各种字节序转换,配合numpy还能做批量坐标变换。当然如果你是追求极限性能的开发者,完全可以把OwlVision的解析器作为参考实现,再用C++或Rust重写渲染层。
1.3 整体架构:解析、模型、渲染、交互四层分离
我在设计时就定了一个规矩:OwlVision的每个核心模块必须能被单独拿来复用,绝对不能跟GUI绑死。整个项目分成了四层:
- 解析层(parser):负责把GDSII二进制流转成内存中的对象列表;
- 模型层(model):定义Cell、Boundary、Path、SRef、ARef等数据类,把二进制数据和几何语义对应起来;
- 渲染层(renderer):把模型层的对象绘制到QPainter上,负责坐标变换、裁剪、绘制批次合并;
- 交互层(ui):负责鼠标键盘操作、图层树、单元树、状态栏显示等。
这样的分层带来的直接好处是:GUI和解析逻辑互不干扰,即使某天你决定不用Qt,换一个命令行脚本批量导出PNG,也只需要调用解析层和渲染层就好了,不用动任何UI代码。我在命令行模式里就是直接复用这两层,一个--export-png参数就实现了无头导出功能。
2. GDSII格式解析的细节与关键实现
2.1 二进制记录结构:理解GDSII的“积木”
GDSII格式非常有意思,它和我们现在常见的XML、JSON、PROTOBUF都不一样。整个文件是一串“记录”(record)按顺序排列而成的,每条记录都是一个变长的数据块。记录头固定是8字节,构成如下:
- 4字节记录长度(包含记录头自身长度);
- 2字节记录类型(record type),比如边界、路径、文本、单元定义等;
- 2字节数据类型(data type),说明后面数据体里装的是整数、浮点还是字符串。
所有多字节整数和浮点数都用大端序存储。这一点极其关键,如果解析时没注意字节序,读到的坐标数据会错得离谱。这里也顺带提醒一句:GDSII不是严格对齐的二进制格式,记录体长度经常是奇数,只要你老老实实按长度字段跳转就不会出错,但别假设后面紧跟的一定是偶数地址。
记录类型和数据类型组合起来,就能描述一条条的几何语义。下面是我实现时用到的最核心的记录类型表:
| 记录类型 | 十六进制值 | 含义 |
|---|---|---|
| HEADER | 0x00 | 文件头,版本号 |
| BGNLIB | 0x01 | 库定义开始,含时间戳 |
| LIBNAME | 0x02 | 库名称 |
| UNITS | 0x03 | 单位定义,数据库单位与用户单位 |
| BGNSTR | 0x05 | 单元(cell)定义开始 |
| STRNAME | 0x06 | 单元名称 |
| BOUNDARY | 0x08 | 多边形边界 |
| PATH | 0x09 | 路径 |
| SREF | 0x0A | 单元引用(单个实例) |
| AREF | 0x0B | 单元阵列引用 |
| TEXT | 0x0C | 文本标注 |
| LAYER | 0x0D | 图层号 |
| DATATYPE | 0x0E | 数据类型号 |
| XY | 0x10 | 坐标序列 |
有一个细节容易忽略:GDSII里“层”不是一个字段一次搞定,它的图层号(LAYER)和数据类型号(DATATYPE)是两条独立的记录,一个图形对象必须同时具备这两个值才能确定它属于哪个工艺层。很多新手把DATATYPE当成填充图案编号来看待,其实不是,它和LAYER共同构成了一个复合键。在渲染时,我会把(layer, datatype)组合映射成一种颜色,这样版图看起来才直观。
2.2 Cell层次关系:看懂引用树
GDSII最核心的概念是Cell(单元)。一个库文件里可以定义很多Cell,每个Cell内部包含实际的几何元素(边界、路径、文本),也可以通过SREF或AREF引用其他Cell。这种引用和现代编程语言里的“函数调用”非常像:一个Cell内部可以多次调用另一个Cell,调用时可以附加旋转、镜像、缩放等变换。
举例来说,一个SRAM模块可能由存储阵列的多行多列构成,每一行是一个Cell实例,行里再引用基本的bitcell单元。渲染这种文件时,如果把所有引用递归展开,图形数量会呈指数级增长。所以我设计了两套显示模式:
- 完全展开模式:所有引用的子单元都递归渲染成实际图形,适合看最终合并效果;
- 单Cell模式:只显示当前选中Cell的直接几何元素,不递归引用,适合排查层次归属。
在展开时有一个必须处理的坑:SREF会附带一个变换矩阵(包括镜像、旋转角度、缩放比例),AREF除了变换矩阵还带行列数和行列间距。坐标变换如果漏掉缩放比例,渲染出来的图形比例就会全乱。OwlVision里我把这个变换矩阵统一抽象成transform(x, y) -> (x', y')的函数,所有引用在计算时都走同一个函数,大大降低了出错概率。
2.3 坐标单位与UNITS记录:为什么图会“消失”
在我收到的反馈中,最常见的现象是“打开文件后什么都看不到”。排查下来,十有八九是坐标单位没有处理好。GDSII中所有坐标都是整数,它的单位是“数据库单位”(database unit),而实际上的物理尺寸是由UNITS记录换算的。UNITS记录包含两个双精度浮点数:第一个是数据库单位对应的用户单位数,第二个固定表示数据库单位对应的米数。
举个具体的例子:如果UNITS记录里的值是 0.001,表示数据库单位等于0.001微米(也就是1纳米),那么一个坐标值为10000的点,实际位置是10微米。如果你的代码忽略这个换算,直接把坐标当成微米来渲染,那么一个设计尺寸为1mm的芯片,会被画成1km的巨幅视图,自然什么都看不见;反过来,如果设计单位是微米而解析器误以为单位是纳米,图形就会小到几乎消失。因此我在解析器里把“用户单位”默认固定为微米,并在状态栏上实时显示鼠标位置的实际物理坐标,这一步对调试定位问题帮助特别大。
3. 渲染核心:画得快才能用得爽
3.1 坐标变换与缩放锚点
渲染第一步是把模型坐标变成屏幕坐标。我的实现里维护了一个简单视图矩阵,包含三个参数:平移量offsetX/offsetY、缩放比例scale、DPI缩放因子。绘制每个多边形时,把模型坐标依次做平移、缩放,然后交给QPainter绘制。坐标变换的公式并不复杂,但有一个交互细节很容易被忽略:滚轮缩放时,缩放锚点一定要是鼠标当前位置,而不是视图中心。这是所有地图软件和版图工具的通用习惯,如果锚点是视图中心,用户滚轮放大时需要不断调整鼠标位置,体验非常糟糕。
为了实现锚点缩放,每次收到滚轮事件时,要先把鼠标位置对应的模型坐标算出来,然后按缩放系数更新scale,最后重新调整平移量,让这个模型坐标在缩放后仍然保持在同一屏幕坐标位置。这段逻辑初次实现时容易被绕晕,但写一次之后永久受益。如果你也在做类似的图形查看器,建议把“屏幕坐标与模型坐标互转”收敛成两个函数,所有交互都基于这两个函数,避免在事件处理里散落大量的手写换算。
3.2 视口裁剪:先看一半再画,省掉千万次绘制
对于几十万级别的多边形,QPainter全量绘制其实也不算太慢,但文件一上百万图形,每帧全量重绘必然卡顿。这里最有效的一招是视口裁剪:在渲染前,把当前视图矩形范围(矩形角点转换回模型坐标系)传给遍历函数,只保留和这个矩形有交集的图形。
实现上最简单的方式是先做一次包围盒(bbox)判断。每个Boundary或Path在解析时就算好它的最小X、最小Y、最大X、最大Y,渲染时先用矩形相交快速过滤,再调用QPainter真正绘制。这个操作看起来不起眼,但对性能的提升近乎质变。我做过一个对比测试:对于一个包含200万个图形的版图,不裁剪时每帧刷新要接近1秒,裁剪到视口只占全图5%的区域后,刷新耗时降到了30毫秒以内。
当然,视口裁剪只解决了“画多少”的问题,没解决“画什么”的开销。如果你的缩略图级别一眼望到整个芯片,那所有图形都在视口内,还是得全部绘制。这时就需要结合图层合并与简化,把相邻的小图形在绘制前合并成一个多边形批次,或者根据当前缩放比例决定是否要对复杂多边形做顶点抽稀。这两招属于进阶优化,在1.2GB大文件的实测里,配合“仅显示当前Cell”模式,基本能做到交互不卡顿。
3.3 图层颜色映射与显示控制
版图查看器里颜色不是随便分配的。GDSII里的图层号可能与工艺层的物理含义强相关,比如METAL1、VIA1、POLY等层。我的实现里内置了一个默认的工艺层颜色表:金属层偏暖色、多晶硅偏绿色、有源区偏绿色、过孔偏蓝色,这样打开文件的第一眼就能大致判断每个区域是什么层。同时,图层管理面板支持按(layer, datatype)组合控制显隐,也可以按住鼠标中键呼出临时放大镜圈选某个图层并高亮显示。
一个容易犯糊涂的地方是:GDSII的图层号本身并没有行业统一标准,每家晶圆厂或设计公司都可以自定义自己的图层编号方案。所以千万不要把颜色表写死,一定要让它可通过配置文件覆盖。OwlVision支持一个简单的映射表文件,用户可以按"LAYER 10, DATATYPE 0 -> #FF8800"的格式自定义配色,方便对接自家公司的版图标准。
4. 构建、使用与二次开发扩展
4.1 环境准备与快速启动
OwlVision依赖项非常少,只有Python 3.9+、PySide6和numpy。在Ubuntu和Windows上我都验证过,安装过程基本一致:
git clone https://github.com/owlvision/gdsii-viewer.git cd gdsii-viewer python -m venv .venv source .venv/bin/activate # Windows下为 .venv\Scripts\activate pip install -r requirements.txt python owlvision.py启动后直接把.gds文件拖进窗口即可打开。命令行模式也支持直接传路径:
python owlvision.py path/to/design.gds程序第一次启动时会解析文件,并生成一份解析缓存(pickle格式),下次打开相同文件会直接加载缓存,速度提升巨大。我拿一个1.2GB的生产文件做过测试:首次解析耗时约18秒,内存峰值约2.1GB;第二次打开因为加载缓存,耗时降到3秒左右。缓存文件默认存放在系统临时目录,用户可以直接删除来强制重新解析。
4.2 核心操作路径:看图、量坐标、导出
我平时使用这个查看器的核心流程是这样的:
- 打开文件后先看全图,确认顶层cell和大致结构;
- 切换到图层管理面板,关闭几层显眼但暂时不关心的金属层,把关心的图层置顶加亮;
- 用滚轮缩放到目标区域,按住鼠标左键拖拽平移;
- 单击某个图形,状态栏会显示它的layer、datatype、bbox坐标和当前鼠标位置的真实物理坐标;
- 在单元树面板里跳到某个具体cell,用“仅显示当前Cell”模式检查层次归属;
- 按下Ctrl+E导出当前视图为PNG,或者按下Ctrl+C导出当前选中图形到剪贴板。
这套操作路径覆盖了我日常80%的看版图需求。最常用的其实是“全图概览 + 局部缩放 + 快速截图”三步,整个过程用不了10秒,比打开商业工具再等授权高效得多。
4.3 为二次开发预留的扩展接口
如果你想把OwlVision的能力嵌入自己的工具链,我特意把解析和渲染模块做成了相对独立的库。调用示例:
from owlvision.parser import GDSIIDatabase from owlvision.renderer import Renderer # 解析文件 db = GDSIIDatabase.parse("design.gds") # 访问顶层单元 top_cell = db.top_cell() # 遍历所有图形 for shape in top_cell.iter_shapes(): print(shape.layer, shape.datatype, shape.bbox)渲染层也支持把任意坐标转换函数传入Renderer实例,这样你可以方便地叠加网格、芯片边框、探针位置等自定义标注层。我另外封装了一个无头导出命令:
python owlvision.py --export-png design.gds --output preview.png --scale 4这条命令不需要打开GUI窗口,从解析到渲染到保存PNG一气呵成,我在CI脚本里就是用它定时生成版图快照,直接贴到发布说明里当配图。如果你想做更复杂的批量处理,比如按图层分别导出图片,在代码里循环调用Renderer即可。
5. 常见问题、踩坑记录与解决建议
5.1 文件打不开,或者解析直接报错
最常见的原因是指定的文件根本不是GDSII格式。有些人会把OASIS、DXF甚至文本文件改个后缀当作GDSII传给工具,解析器读到无效记录类型自然就崩了。我加了一层文件头校验,GDSII文件开头一般是版本记录结构,如果前几个字节连合法的记录长度都对不上,直接弹出友好提示,而不是刷一堆traceback。另一个容易出问题的是文件指针偏移没按记录长度跳转,导致后续所有记录都错位。解析器里所有跳转都基于记录头字段,绝不假设任何固定对齐。
5.2 打开后图形为空,或者坐标全在几千亿的位置
这类问题90%出在单位换算上。建议先确认UNITS记录读到的浮点数是否符合预期。你可以临时加一段调试代码,打印数据库单位和用户单位的换算比,如果数据极其悬殊,就需要检查是大端序没调对导致浮点数字节序错误,还是记录长度算错导致UNITS解析位置错乱。实测中,字节序错误会让UNITS读出来的值变成天文数字,进而导致所有坐标缩放后跑到屏幕外面。
5.3 大文件的渲染卡顿和内存占用
对于超过1GB的GDS文件,解析阶段内存占用很高是正常的,因为要建立全部的对象模型。但渲染卡顿是可以优化的,按前文说的视口裁剪是第一步;如果还是卡,就开启“仅显示当前Cell”模式,尽量减少单帧绘制数量。假如你本地内存紧张,还可以在解析时通过参数忽略非目标层的所有图形数据,只保留你需要的那几个图层,这样内存能降一个量级。这个功能我是在分析一个超大模拟版图时临时加上去的,没想到后来成了很多用户最常用的功能之一。
如果你遇到了KLayout能打开、OwlVision打开就卡死的情况,建议先用命令行模式做一次导出测试,通过导出的PNG确认是解析还是渲染环节的问题。从实际排查经历看,90%的卡顿都来自绘制环节,解析阶段一般都能在合理时间内完成。
写在最后的一点体会
做OwlVision这个项目,最大的收获不是代码本身,而是对GDSII格式从“恐惧”变成“熟悉”的过程。刚开始我也觉得GDSII是一团乱麻,可当你逐条记录拆解、逐个字段验证之后,会发现它其实是一个极其优雅的格式——用最简单的序列化方式,承载了集成电路设计几十年的工业成果。如果你也想学习版图数据格式,或者只是需要一个趁手的轻量查看器,强烈建议自己动手写一遍解析器,哪怕只支持Polygon渲染,对理解芯片数据模型的帮助也是巨大的。
之后我还打算给OwlVision加上OASIS格式的只读支持、更智能的图层自动命名,以及一个简单的DRC快速规则检查模块(只做几何间距和宽度的粗查,不做完整验证)。如果你也有类似的想法,欢迎把项目fork过去,按你自己的需求往下扩展。开源项目最迷人的地方就在这里——它不一定比商业工具完美,但它永远是你自己的工具,你可以让它按照你的习惯生长。
本文还有配套的精品资源,点击获取