探究 MacPaint 的源代码
探究 MacPaint 的源代码
2025-03-25
MacPaint 是一款单色栅格图像绘画程序,它向许多人介绍了鼠标驱动的控件、工具调色板以及与其他应用程序的复制粘贴集成。作为 1984 年 Apple Macintosh 的两个首发应用程序之一,MacPaint 象征着 Macintosh 早期古怪的革命性品牌、对易用性的关注以及对艺术客户的吸引力。通过研究其源代码,我们来考察该应用程序的设计和实现。我们发现,缓冲区管理和桶填充算法体现了对 68k 平台的机械同理心,并利用了该领域的局限性来提高性能。我们还在代码风格和架构及其变更灵活性方面发现了积极和消极的方面。最后,我们对该程序的一些声称的新颖之处提出异议,同时也论证了其对数字图形系统发展的重要性和影响。
MacPaint 1.5 (1985)
目录
背景
在 1984 年 1 月 30 日波士顿计算机协会的全体会议上,Steve Jobs 阐述了他认为 Apple 的最新产品 Macintosh 计算机是计算机行业继 Apple 自己的 Apple II 和 IBM PC 之后的第三个里程碑式产品的原因。Macintosh 将成为“为我们其他人提供的计算机”。Macintosh 使用与 Lisa 相同的软件,从点击界面和下拉菜单中带来了相同的易用性,并共享相同的快速 Motorola 68000 处理器。
在强调了便携性、3.5 英寸软盘驱动器和 AppleBus 支持等硬件功能后,Jobs 让计算机自己进行演示。从软盘启动 Mac 后,Mac 显示了其名称和“Insanely Great”徽标,然后显示了第一个真实的应用程序图像:MacPaint 的屏幕截图,显示了日本女士的木刻。
带有日本女士的 MacPaint 屏幕截图 (1984)
Apple 在其广告中大量使用了该屏幕截图,并且大多数观众在此之前的几个月中都看到了 MacPaint 和 MacWrite 的静态屏幕截图,但观众仍然感到高兴。
在 29 分钟后,Jobs 介绍了 Macintosh 开发团队的成员,Bill Atkinson 开始了第一个演示,即他开发的 MacPaint 的演示。
在接下来的七分钟内,Atkinson 演示了如何使用该程序进行艺术创作。当他使用橡皮擦擦除先前绘制的线条和矩形的小块时,观众第一次鼓掌。鼓掌的原因既在于该工具的易用性和速度,还在于它表明该程序正在处理实际像素;他没有创建新的裁剪区域或仅限于删除整个形状。当 Bill 绘制填充有图案的线条时,观众也鼓掌了,当按住喷枪时,油漆变得更浓稠时,他们再次鼓掌。预示着该功能的价值,第四次鼓掌是在 Bill 放大并使用 Fat Bits 模式操纵单个像素时。观众同样喜欢可以选中、移动和复制图像区域的多种方式。
Atkinson 通过复制一条鱼的图像来结束演示,该图像将很快粘贴到 MacWrite 文档中。为了不言而喻地表示 Macintosh 硬件的局限性,Atkinson 关闭了 MacPaint,以便 MacWrite 可以使用该内存。
即将演示 MacWrite 的 Randy Wigginton 停下来赞扬 Bill 在 QuickDraw 上的工作,QuickDraw 是 Lisa 和 Macintosh 的基础图形库。“没有 Bill,我们都不会站在这里。”
Bill Atkinson 在 Apple 的贡献,包括 Lisa 和 Macintosh 计算机的基础用户界面贡献,以及随“Insanely Great” Macintosh 附带的两个应用程序之一(正如广告所宣称的那样),已被充分记录。随着 QuickDraw 和 MacPaint 源代码在 2010 年的发布,我们有机会研究他的工作的技术设计和实现。本文考察了 MacPaint 应用程序,它是如何构建的,有哪些有趣的算法和工程权衡,以及我们如何衡量其对围绕图像绘画和栅格化技术的更大行业趋势的影响。
时间线
MacPaint 的开发与鼠标和图形用户界面、Lisa 和 Macintosh 计算机以及 QuickDraw(Lisa 和 Macintosh 使用的基础图形库)的开发交织在一起。此时间线侧重于 MacPaint 和当代的竞争对手绘画程序,而不是栅格绘图程序的整体历史。有关早期栅格绘图程序及其商业应用和开发的历史,请参见(Smith 2001)。
1982
在六周的时间里,Atkinson 开发了一个“大致有效”的绘画程序原型(Young 1985, pg 315)。源代码文件 MyTools.text
(后来重命名为 MyTools.a
)声明它是 10 月 31 日创建的。
1983
1 月,Apple 宣布推出 Lisa 计算机,尽管直到 6 月才发货。Lisa 包括 Atkinson 的 QuickDraw 库。
Microsoft 通过发布他们的第一个 Microsoft Mouse 来扩大了基于鼠标的应用程序的开发。该软件包包括一个名为“Doodle”的彩色栅格绘图程序。Doug Wolfgram 发布了可能是第一个第三方绘图程序“Mouse Draw”,该程序使用 Microsoft Mouse。
Atkinson 恢复了 MacPaint 的工作,此时称为 MacSketch。调色板和工具集已经非常接近最终的 MacPaint UI。屏幕截图(如下)中的图像是为了庆祝 ROM 2.0;根据 MyTools.a,这将使该图像的日期介于 2 月 13 日和 3 月 16 日之间,当时为 ROM 2.0 重新生成了该文件,但在 ROM 2.4 之前。
MacSketch(MacPaint c. 1983;来源 folklore.org)
MacSketch 在四月份更名为 MacPaint。从那时到十月,Atkinson 反复迭代该程序,添加功能并提高性能。MyTools.a 中的最后一个条目可追溯到 1983 年 9 月。
12 月,Apple 通过 全彩色小册子 宣传 Macintosh。MacPaint 得到了突出展示,并用于向公众介绍工具调色板、菜单和复制粘贴的工作方式。该广告还提到可以滚动内容区域以获得更多工作空间。
1984
1 月 30 日,在波士顿计算机协会全体会议上,Apple Macintosh 大张旗鼓地亮相。Bill Atkinson 向人群演示了 MacPaint(并隐含地演示了 QuickDraw)。最初的 Macintosh 128k 附带了两个应用程序:MacWrite 和 MacPaint。
5 月,MacPaint 1.3 作为免费软件更新的一部分发布给客户。此版本添加了套索对象并使用图案重复填充对象(通过“编辑”菜单中的“填充”项)的功能。
9 月,MacPaint 1.4 与 Macintosh 512k 一起发布。
竞争对手迅速采用了 MacPaint 界面。6 月,Mouse Systems 发布了 PC Paint 1.0,与 Microsoft 竞争,并将其与鼠标捆绑在一起。PC Paint 基于他们从 Wolfgram 购买的 Mouse Draw,但具有类似于 MacPaint 的界面。同样,ZSoft Corporation 发布了他们的 PC Paintbrush,它也是基于 DOS 的,但具有从 MacPaint 派生的界面。
1985
Microsoft 发布了其鼠标的新版本。他们放弃了他们的“Doodle”程序,并将其替换为从 ZSoft Corporation 获得许可的 PC Paintbrush 的更名版本。4 月,Apple 发布了 System Software 2.0,其中包括 MacPaint 1.5 (https://archive.org/details/mac_Paint_2)。这是 1988 年之前的最后一个版本,当时 MacPaint 2.0 由 新的开发者 发布。MacPaint 没有更多官方版本。
开发者:Bill Atkinson
当您启动 MacPaint 时,Bill Atkinson 的名字会在片头中短暂闪烁。您还可以在“关于”菜单中找到 Atkinson 的名字和一个小肖像。
MacPaint 1.5“关于”窗口 (1985)
具有讽刺意味的是,对于一位将晋升为 Apple Fellow 的人来说,Bill Atkinson 并没有接受过计算机方面的经典培训。他的本科教育是化学和生物化学,研究生培训是神经科学。但是,他并没有与计算领域脱节。他构建了 IMSAI 和 Altair 计算机(Atkinson 2004, pg 4)。此外,他的大学工作强调使用计算机,并且他建立的联系对他的职业生涯产生了高度影响。
在 UC San Diego 时,Atkinson 遇到了 Jef Raskin,并被介绍到 Raskin 的非常规计算机实验室,该实验室强调与计算机的直接和实时连接。他还遇到了 UCSD 的 Guy Bud Tribble,他后来与 Atkinson 同住并帮助开发了 Macintosh。Atkinson 在华盛顿大学的导师 Kent Wilson 向他介绍了计算机图形学和该领域的创新,例如 Ivan Sutherland 在 Sketchpad 上的工作。
在 1978 年被 Jef Raskin 吸引到 Apple 后,Atkinson 成为该公司的第一位应用程序软件开发人员。他的第一个项目是股票投资组合评估器,因为尽管 Apple 在广告中介绍了该评估器,但他们的目录中没有任何评估器(Atkinson 2004, pg 7)。他的第二个主要项目是帮助将 UCSD Pascal 系统移植到 Apple II。由于没有其他结构化编程选项,Lisa 开发采用了此版本的 Pascal(Atkinson 2004, pg 9)。
1979 年,Steve Jobs 与包括 Atkinson 在内的一小群 Apple 员工一起参观了 Xerox Parc。在 Parc,他们看到了 Alto 计算机、Smalltalk 编程语言和(可能)Bravo 文本编辑器(Atkinson 2004, pg 18)。调到 Lisa 项目后,Atkinson 负责 LisaGraf(基础图形库,后来命名为 QuickDraw),以及原始窗口管理器、菜单管理器和事件管理器(Atkinson 2004, pg 20)。
由于 Macintosh 重用了 Lisa 的软件,特别是 Atkinson 的用户界面和图形代码,他转到了 Macintosh 团队,并在 1983 年开始认真开发 MacPaint。
与 Adobe 的创始人不同,Atkinson 并非来自计算机图形研究领域,但他通过他的学术导师熟悉了研究进展,并且会见了 Douglas Engelbart 等知名人士。与多次处理相同问题的 Adobe 创始人类似,他有时间进行迭代,Lisa 从研究工作发展到产品期间进行了几年的实验。Atkinson 有意跨越基础和应用程序开发,拥有“垂直集成商”的视角,能够控制功能应去向何处以及接口应如何工作。
开发和测试
由于发现 Apple 办公室的生活过于“繁忙”(Atkinson 2004, pg 20),他使用 Apple Lisa 原型在他自己的家庭实验室工作。Lisa 具有“Workshop”模式,该模式具有 图形编辑器和命令行环境,用于编译和其他开发活动。
Atkinson 拍摄了用户界面演变的宝丽来照片,并开车去上班与团队分享。幸运的是,为了后代,Atkinson 保存了宝丽来照片,并且我们拥有 Lisa 界面和 QuickDraw 功能演变的详细视觉历史,如 2022 年 CHM 采访中所示。(SketchPad 在 9:45 开始短暂地显示和讨论。)
Macintosh 团队使用“Monkey”作为耐用性和稳健性测试机制。Monkey 由 Steve Capps 开发,它会随机键入键、移动对象并与菜单交互(Atkinson 2010, pg 14-15)。该团队使用以 Monkey 模式运行的计算机来有效地对应用程序进行压力测试。MacPaint 能够在不崩溃的情况下生存两周。可以在源代码中看到 Monkey 模式:
1
2
3
4
5
6
7
8
9
| ``` .FUNC Monkey ;--------------------------------------------------------------------- ; ; FUNCTION Monkey: BOOLEAN; ; TST MonkeyLives ;IS THE MONKEY ACTIVE ? SGE 4(SP) ;YES IF >= ZERO NEG.B 4(SP) ;CONVERT TO PASCAL BOOLEAN RTS
---|---
为了防止 Monkey 模式退出程序并因此过早结束测试,Pascal 代码调用此函数以选择性地禁用 Apple 菜单、文件菜单和“退出程序”命令。
Susan Kare 担任 Macintosh 的图形设计师,是 MacPaint 的主要客户。Atkinson 观察了她使用 MacPaint,“看看她绊倒或希望拥有什么”(Atkinson 2004, pg 47)。作为团队中唯一真正的艺术家,并且是使用 MacPaint 作为工作工具的人,Kare 的反馈非常宝贵。Andy Hertzfeld 将她的影响描述为“我认为 MacPaint 的很多改进都来自于观察实际用户,实际艺术家每天使用该程序。”(Atkinson 2010, pg 9)。同样,Atkinson 说:“我会将 Susan Kare 视为 MacPaint 的共同设计师,因为她在我想编写它时使用了它。”(同上)。
## 设计和源代码
### 物理描述
MacPaint 1.3 发行版包含五个文件:
1. `MacPaint.p`,4,688 行 Pascal 代码([Lisa Pascal 变体](https://ztoz.blog/posts/macpaint-source-code/<http:/pascal.hansotten.com/ucsd-p-system/apple-pascal/>))
2. `MacPaint.rsrc`,程序的资源描述,包含图标、字符串和其他可本地化的属性。版本字符串将其标识为 1.3 版。
3. `MyHeapAsm.a`,67 行汇编代码,用于调用系统内存管理例程
4. `MyTools.a`,300 行汇编代码,用于定义陷阱或外部调用到 QuickDraw。一条注释说明此文件主要通过 `MakeTTraps` 生成。这是唯一一个具有更改日志和内容归因于 Bill Atkinson 以外的人的文件。
5. `PaintAsm.a`,1,809 行汇编代码,包含从 Pascal 代码调用的应用程序代码
我们使用 `pascal_count` 和 `asm_count` 程序(均为 David A. Wheeler 的 SLOCCount 套件的一部分)计算了物理代码行。
### 数据类型和结构
MacPaint 为自己的使用定义的数据类型很少,而是利用了 QuickDraw 中的类型,例如 Point、Rect(矩形)、Pattern 和 BitMap。
Pascal 代码大量使用全局变量;该列表从第 212 行到第 370 行,并使用空格按共同点对其进行分组。大多数全局变量用于存储各种标志或界面状态,例如当前字体规范。此列表与全局常量(第 27 行到第 190 行)不同。大多数常量用于指定菜单项、按钮或其他界面元素。
该应用程序是一组工具,最终会修改文档,该文档是一个固定大小的 1 位位图,存储为整数数组。该文档缺少动态元数据。由于像素非常简单,因此需要的抽象或数据类型很少。
### Pascal 与汇编
大约三分之一的 MacPaint 代码行是用 Motorola 68000 汇编编写的,而三分之二是 Pascal。在 (Young 1985, pg 316) 中,Atkinson 解释了两种语言的基本原理和好处:
> 通过工作频率,我每次打开汇编语言文件时都会打开 Pascal 文件 20 或 30 次。基本上,汇编语言中的信息实际上不需要大量维护。汇编语言部分包含用于提高速度或很小的东西,我知道不需要大量维护。我将它们放在汇编语言中只是为了减少代码大小。通过将主控制、流程和逻辑保留在 Pascal 中,该程序更易于修改。
Atkinson 的基本原理得到了每种语言的过程列表的支持。性能关键型代码(例如直接操作缓冲区的代码)是用汇编编写的。用户界面控制逻辑是用 Pascal 编写的,以及处理初始设置或罕见操作的代码。操作系统调用(例如检查磁盘驱动器中的可用空间量或调用系统蜂鸣声)是用汇编编写的。由于 MacPaint 是与操作系统同时开发的,因此某些功能可能存在于 ROM 中,但尚未通过 Pascal 系统库公开。
作为 Atkinson 的汇编风格和质量的示例,我们展示了 `NearPt` 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| ```
.FUNC NearPt
;-----------------------------------------------------------
;
; FUNCTION NearPt(pt1,pt2: Point; tol: INTEGER): BOOLEAN;
;
; NearPt:=((ABS(pt1.h-pt2.h) < tol) AND (ABS(pt1.v-pt2.v) < tol));
;
MOVE.L (SP)+,A0 ;pop return addr
MOVE (SP)+,D0 ;pop tolerance
MOVE.L (SP)+,D1 ;pop pt2
MOVE.L (SP)+,D2 ;pop pt1
CLR.B (SP) ;assume result FALSE
SUB.W D1,D2 ;calc delta horiz
BGE.S DHPOS ;continue if dh positive
NEG.W D2 ;else negate for abs value
DHPOS CMP.W D0,D2 ;is ABS(dh) < tol ?
BGE.S FALSE ;no, return false
SWAP D1 ;get pt2.v
SWAP D2 ;get pt1.v
SUB.W D1,D2 ;calc delta vert
BGE.S DVPOS ;continue if dv positive
NEG.W D2 ;else negate for abs value
DVPOS CMP.W D0,D2 ;is ABS(dv) < tol ?
BGE.S FALSE ;no, return FALSE
MOVE.B #1,(SP) ;result := TRUE
FALSE JMP (A0) ;and return
---|---
如果两个点“足够接近”(即使它们并不完全相等),NearPt
将返回 true。由于点使用整数值,因此此代码不用于处理浮点错误,而是用于处理人为的不精确性。例如,如果用户试图通过单击先前的点来关闭多边形,则如果用户在较早的点的几个像素内单击,则该代码允许关闭该多边形。同样,鼠标在双击期间可能会滑动一小段距离。虽然这不是特别性能关键的代码,因为它被调用的次数相对较少,但它是不太可能需要更改的代码。
与他的汇编代码的典型情况一样,该函数记录了 Pascal 调用约定。该函数还记录了一个数学定义;很少有函数或过程需要描述性注释。每一行都进行了语义注释。PaintAsm.a
中的几乎每一行汇编代码都进行了类似的注释。
相比之下,HVConstain
是一个 Pascal 过程。我们选择它作为示例是因为它相对较短且独立。此过程用于在绘制时将锚点约束或捕获到特定方向或 45 度角。例如,用户可以在绘制矩形时按住 shift 键以强制绘制正方形。或者,在绘制线条时,强制其与屏幕边缘平行。
PROCEDURE HVConstrain(VAR newPt: Point);
VAR dh,dv: INTEGER;
BEGIN
IF shiftFlag THEN { constrain to horiz or vert }
BEGIN
IF hConstrain AND vConstrain THEN { still chosing direction }
BEGIN
dh := ABS(newPt.h-ptConstrain.h);
dv := ABS(newPt.v-ptConstrain.v);
IF (dh > dv) AND (dh > 1) THEN vConstrain := FALSE;
IF (dv > dh) AND (dv > 1) THEN hConstrain := FALSE;
END;
IF hConstrain THEN newPt.v := ptConstrain.v; { horiz }
IF vConstrain THEN newPt.h := ptConstrain.h; { vert }
END;
END;
Atkinson 为他的变量使用了非常一致的命名风格,并且该代码通常具有很高的可读性。与他的汇编代码相比,Pascal 代码中的注释很少且简洁,但仍侧重于解释该行的语义目的。
HVConstrain
使用一个全局变量 shiftFlag
来跟踪用户模式,并使用三个全局变量来存储状态:hConstrain
、vConstrain
和 ptConstrain
。尽管是全局变量,但只有另一个过程可以直接访问后三个变量:InitConstrain
。这三个变量被注释为属于 HVConstrain
。该设计允许开发人员使用变量而不进行正确的初始化或意外地修改它们。尽管这违反了封装的设计原则,但我们认为这是代码复杂性和内存资源的有效权衡。
消息循环
MacPaint 是一个早期的事件驱动程序。程序的核心是:
REPEAT
[...]
IF GetNextEvent(everyEvent,theEvent) THEN ProcessTheEvent;
[...]
UNTIL quitFlag;
GetNextEvent
是一个 Toolbox Event Manager 函数,用于从事件队列中获取下一个事件(如果存在)。ProcessTheEvent
是一个应用程序过程,它是一个很长的 CASE 语句,用于将事件位置映射到按钮或更准确地说是屏幕的矩形区域。ProcessTheEvent
调用控制工具特定模式的其他应用程序过程。ProcessTheEvent
的级别足够低,必须测量自上次单击以来的时间才能区分单击和双击。由于用户界面是固定的(无法移动窗口),因此代码很繁琐但易于遵循。
示例:直线工具
直线工具是软件设计以及 Pascal、汇编和 QuickDraw 功能混合的典范。为了调用 StraightLine
过程,用户将先前从工具调色板中选择了直线工具,然后在内容区域中单击(并按住)鼠标。在此过程(直线“模式”)中,线条的一端将固定在初始点 (startPt
),而另一端将跟随光标,直到用户释放鼠标按钮。模式驱动的界面为用户提供了有关最终线条在绘画中外观的持续反馈。
{$S }
PROCEDURE StraightLine;
VAR newPt,oldPt,startPt: Point;
lineTop,lineBot: INTEGER;
BEGIN
JamLine;
PinGridMouse(startPt);
oldPt.h := 1000; { force first time }
REPEAT
PinGridMouse(newPt);
IF shiftFlag THEN Constrain(startPt,newPt,TRUE);
IF NOT EqualPt(newPt,oldPt) THEN
BEGIN
MainToAlt; { erase old }
AltBufLine(startPt,newPt,oldPt);
oldPt := newPt;
END;
UNTIL NOT StillDown;
END;
第一行 {$S }
是一个编译器指令,指示此过程应位于默认主段中(有关段的更多信息,请参见下面的_分配失败和段反碎片_部分)
JamLine
使用 PenNormal
重置一些 QuickDraw 状态,然后将笔的大小、图案和模式(取决于按下的键)设置为当前调色板设置。
PinGridMouse
将传入的变量设置为鼠标的当前位置,该位置已由各种模式修改(例如,捕捉到网格、粗体)。同样,如果用户按住 shift 键,则 Constrain
会将 newPt
设置为 45 度约束的点值。
REPEAT
块测试 StillDown
条件。即使在调用之间用户释放然后快速再次按下鼠标按钮,StillDown
也是一个 Toolbox Event Manager 函数,它将返回 false。
如果存在一条线 (IF NOT EqualPt
),则会将主缓冲区复制到备用(替代)缓冲区。(QuickDraw 使用整数值作为 Point
的坐标,因此 EqualPt
不需要容差参数。)MainToAlt
调用 BufToBuf
,这是一个汇编例程,其中包含与 BufToScrn
相同的 MOVEM
优化(请参见_快速缓冲区到屏幕复制_部分)。AltBufLine
使用 MoveTo
和 LineTo
QuickDraw 例程将从 startPt
到 newPt
的线写入备用缓冲区。然后,使用包含 oldPt
的边界框将备用缓冲区的内容发送到屏幕缓冲区,从而消除(通过重绘)发送到屏幕的任何先前线条,但也减少了写入的数据量。(BandToScrn
也会注意隐藏和显示光标。)
声明了 lineTop
和 lineBot
变量,但未使用。我们的假设是,AltBufLine
的部分功能最初是 StraightLine
的一部分,因为它是唯一使用具有相同名称变量的另一个块。该程序还使用常量 lineTop
来描述线条大小调色板窗口的顶部,因此编译器一定没有将重复的 lineTop
声明视为错误。
有趣的算法和设计
快速缓冲区到屏幕复制
在对 Macintosh 团队的采访中 (Lemmon 1984),该团队解释了他们优化代码大小和处理时间的过程。在争辩必须先使代码正确然后再使其快速之后,Atkinson 提出了寄存器分配。他说:
这个小东西,68000,有 16 个 32 位寄存器坐在那里,而您从中获得性能的方式是保持它们充满。始终保持寄存器充满重要内容。这就是你让这个处理器唱歌的方式。
缓冲区复制代码(性能至关重要)说明了这种技术。
MacPaint 使用两个屏幕外缓冲区进行渲染,然后将其复制到屏幕缓冲区进行显示。在 Pascal 代码中,两个缓冲区的存储声明为:
mainBuf: ARRAY[0..239,0..12] OF LongInt;
altBuf: ARRAY[0..239,0..12] OF LongInt;
在 Pascal 中,范围定义是包含性的,因此每个缓冲区包含 240 行,每行 13 个 LongInt。内容区域固定为 416 像素 x 240 像素。由于 LongInt 包含 32 位,因此每行存储 416 个单位像素。
虽然 MacPaint 的用户界面看起来与 Macintosh 的显示器非常协调,但 Atkinson 本可以设计更大的内容区域或调整设计以适应纵向排列。但是,需要保持寄存器充满表明了给定布局的技术原因。如果我们看一下 BufToScrn
汇编代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| ``` .PROC BufToScrn,2 ;-------------------------------------------------------- ; ; PROCEDURE BufToScrn(bufPtr,scrnPtr: Ptr; top,bottom: INTEGER); ; ; top and bottom coords are relative to start of buffer ; ; cursor has already been hidden. ; MOVE.L (SP)+,D0 ;POP RETURN ADDR MOVE (SP)+,D1 ;POP BOTTOM MOVE (SP)+,D2 ;POP TOP MOVE.L (SP)+,A1 ;POP SCRNPTR MOVE.L (SP)+,A0 ;POP BUFPTR MOVE.L D0,-(SP) ;PUSH RETURN ADDR MOVEM.L D3-D7/A2-A6,-(SP) ;SAVE REGS SUB D2,D1 ;CALC HEIGHT BLE.S GOHOME ;QUIT IF COUNT <= 0 MOVE D1,-(SP) ;INIT ROW COUNT MOVE D2,D1 ;COPY TOP COORD MULU #52,D1 ;CALC SRC OFFSET ADD.L D1,A0 ;OFFSET SRCPTR MULU screenRow,D2 ;CALC SCRN OFFSET ADD.L D2,A1 ;OFFSET SCRNPTR NXTROW MOVEM.L (A0),D0-D7/A2-A6 ;SUCK UP 13 LONGS FROM BUF MOVEM.L D0-D7/A2-A6,(A1) ;SPIT THEM OUT TO SCREEN ADD #52,A0 ;BUMP SRCPTR ADD screenRow,A1 ;BUMP SCREENPTR SUB #1,(SP) ;DECREMENT ROWCOUNT BNE NXTROW ;LOOP 240 ROWS TST (SP)+ ;POP ROW COUNT GOHOME MOVEM.L (SP)+,D3-D7/A2-A6 ;RESTORE REGS RTS ;AND RETURN
---|---
`NXTROW` 循环执行 240 次,每次执行从缓冲区复制 13 个 LongInt。Motorola 68k 支持 [“移动多个” (MOVEM) 指令,该指令接受多达 13 个寄存器](https://ztoz.blog/posts/macpaint-source-code/<https:/www.looksgoodworkswell.com/elegance-of-macpaint-code/>) 作为源或目标。(`.L` 通知汇编程序我们正在复制长值。)根据(Motorola 1993),MOVEM 指令需要 \\(12 + 4n\\) 个时钟周期才能将内存从存储在 A 寄存器中的地址移动到寄存器,并且需要 \\(8 + 8n\\) 个时钟周期才能将存储在寄存器中的值移动到内存位置(表 9-16)。因此,内存传输需要 \\(20 + 12n\\)(其中 \\(n\\) 是寄存器的数量)或 176 个时钟周期。相比之下,如果通过 MOVE 指令执行传输,则每次传输需要 12 个时钟周期或总共 312 个时钟周期(表 9-18)。通过使用 MOVEM 尽可能地提高吞吐量(通过填充所有(相关的)寄存器),每次缓冲区到屏幕的传输可节省 136 个时钟周期。
### 桶填充(种子填充)
桶填充是一种算法,从给定的像素开始,沿着共享基础颜色的所有相邻像素传播,并将这些像素转换为目标颜色。MacPaint 通过用图案填充空间来实现此函数的变体,而不仅仅是单一颜色。由于边界可能非常复杂,因此这可能是一项昂贵的计算。在最坏的情况下,填充空屏幕,这需要 416 x 240 像素检查或总共 99,840 次检查。通过各种技巧,Atkinson 的实现减少了工作量,并使操作感觉很快。
(当边界已知时,不会使用此算法,例如绘制填充的矩形。QuickDraw 支持直接绘制填充的多边形。)
到 1983 年,几位研究人员已经调查并发布了用于桶填充的算法。20 世纪 70 年代中期的 _SuperPaint_ 可能是该想法的第一个实现(Glassner 2001);(Lieberman 1978)和(Smith 1979)代表了早期的研究论文。MacPaint 算法与 Lieberman 的算法类似,因为该算法沿垂直和水平像素路径传播,并支持用图案和颜色填充。MacPaint 的代码已针对域中的限制进行了优化,即 1 位位图。
在源代码中,桶填充工具称为 `SeedFill`。`SeedFill` 过程包含该工具的顶层业务逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| ```
PROCEDURE SeedFill(startPt: Point);
VAR firstBlack: BOOLEAN;
BEGIN
firstBlack := PixelTrue(startPt.h,startPt.v,mainBits);
CalcMask(mainBits,altBits,altBits.bounds,startPt,firstBlack,FALSE);
SetPortBits(alt