Investigating MacPaint's Source Code

MacPaint is a monochromatic raster image painting program that introduced many people to mouse-driven controls, tool palettes, and copy and paste integration with other applications. One of two launch applications for the Apple Macintosh in 1984, MacPaint is emblematic of the Macintosh’s early quirky revolutionary branding, focus on ease of use, and appeal to artistic customers. Using the source code, we examine the design and implementation of the application. We find that the buffer management and bucket filling algorithms demonstrate mechanical empathy with the 68k platform and leverage the limitations of the domain as a means to improve performance. We also find positive and negative aspects in the code style and architecture and its pliability for change. Finally, we dispute some claimed novel aspects of the program while also arguing for its significance and impact on the development of digital graphic systems.

Screenshot of MacPaint 1.5 with a drawing

MacPaint 1.5 (1985)

Table of Contents

  1. Background
  2. Timeline
  3. Developer: Bill Atkinson
  4. Development and Testing
  5. Design and Source Code
  6. Interesting Algorithms and Designs
  7. Alternative Paths and Competitors
  8. Post Release
  9. Conclusion
  10. Special Recognition
  11. References

Background

At the Boston Computer Society’s general meeting on January 30th 1984, Steve Jobs laid out his rationale for why Apple’s newest product, the Macintosh computer, was the third milestone product of the computer industry after Apple’s own Apple II and the IBM PC. The Macintosh would be the “computer for the rest of us.” The Macintosh used the same software as the Lisa bringing the same ease of use from a point-and-click interface and pull down menus as well as sharing the same fast Motorola 68000 processor.

After highlighting hardware features such as portability, the 3.5" diskette drive, and the AppleBus support, Jobs let the computer do a demo on its own. Booting the Mac from a diskette, the Mac displayed its name and the “Insanely Great” logo, and then showed the first real, application image: a MacPaint screenshot showing a woodcut of a Japanese lady.

Screenshot of MacPaint with Japanese lady

MacPaint screenshot with Japanese Lady (1984)

Apple used the screenshot heavily within their advertisements and most of the audience would have seen static screenshots of MacPaint and MacWrite in the months beforehand, but the audience was still delighted.

Twenty-nine minutes in, Jobs introduces a panel of the Macintosh development team and Bill Atkinson starts the first demo, a demo of MacPaint that he developed.

Over the next seven minutes, Atkinson demonstrates how to make art with the program. The audience’s first applause comes when he uses an eraser to erase little chunks of previously drawn lines and rectangles. The applause is due to both how easy and quick the tool is to use but also that it demonstrates the program is working with actual pixels; he isn’t creating new clipping regions or restricted to deleting entire shapes. The audience also applauded when Bill draw lines filled with patterns and then again when, by holding down the spray can, the paint grows denser. Heralding the feature’s value, the fourth applause comes when Bill zooms in and manipulates individual pixels using the Fat Bits mode. The audience similarly loves the many ways areas of the image can be selected, moved and copied.

Atkinson finishes the demo by copying an image of a fish that will soon be pasted into a MacWrite document. In an unspoken nod to the limits of the Macintosh’s hardware, Atkinson closes MacPaint so the memory can be used by MacWrite.

Randy Wigginton, who is about to demo MacWrite, pauses to praise Bill’s work on QuickDraw, the foundational graphical library for the Lisa and the Macintosh. “Without Bill, none of us would be up here on stage.”

Bill Atkinson’s contributions at Apple, including foundational user interface contributions for the Lisa and Macintosh computers, and one of the two application programs that shipped with the “Insanely Great” Macintosh as the advertisements claimed, have been well-recorded. With the release of the QuickDraw and MacPaint source code in 2010, we have an opportunity to examine the technical design and implementation of his work. This article examines the MacPaint application, how it was built, what are some of the interesting algorithms and engineering trade offs, and how we might measure its impact against the larger industry trends around image painting and rastering technology.

Timeline

The development of MacPaint is intertwined with the development of the mouse and the graphical user interface, the Lisa and Macintosh computers, and QuickDraw, the foundational graphics library used by both the Lisa and the Macintosh. This timeline focuses on MacPaint and contemporary competitor painting programs, not on the overall history of raster drawing programs. See (Smith 2001) for a history of early raster drawing programs and their commercial applications and development.

1982

Over a period of six weeks, Atkinson develops a prototype painting program that “sort-of worked” (Young 1985, pg 315). The source code file MyTools.text, later renamed to MyTools.a, states it was created October 31st.

1983

In January, Apple announces the Lisa Computer, although no units are shipped until June. The Lisa includes Atkinson’s QuickDraw library.

Microsoft broadens the development of mouse-based applications by releasing their first Microsoft Mouse. The package includes a color raster drawing program program called ‘Doodle.’ Doug Wolfgram releases perhaps the first third-party drawing program with ‘Mouse Draw,’ which uses the Microsoft Mouse.

Atkinson resumes work on MacPaint, at this point called MacSketch. The set of palettes and tools is already very close to the eventual MacPaint UI. The image in the screenshot (below) is celebrating ROM 2.0; based on MyTools.a, this would date the image between February 13th and March 16th, when the file was regenerated for ROM 2.0 but before ROM 2.4.

Screenshot of MacSketch with UI similar to MacPaint

MacSketch (MacPaint c. 1983; Source folklore.org)

MacSketch is renamed MacPaint in April. Between then and October, Atkinson iterates on the program adding features and improving performance. The last entry in MyTools.a is dated September 1983.

In December, Apple advertises the Macintosh with a full-color brochure. MacPaint is featured prominently and is used to educate the public on how tool palettes, menus, and copy-paste work. The ad also mentions that the content area can be scrolled for more work space.

1984

With great fanfare, the Apple Macintosh is shown at the Boston Computer Society January 30th General Meeting. Bill Atkinson demos MacPaint (and implicitly, QuickDraw) to the crowd. The original Macintosh 128k comes with two applications: MacWrite and MacPaint.

In May, MacPaint 1.3 is released as part of a free software update to customers. This version adds the ability to lasso an object and repeatedly fill it (via the Fill item in the Edit menu) with a pattern.

In September, MacPaint 1.4 is released along with the Macintosh 512k.

Competitors quickly adopt the MacPaint interface. In June, Mouse Systems ships PC Paint 1.0 bundled with a mouse in competition with Microsoft. PC Paint is based on Mouse Draw, which they purchased from Wolfgram, but with a MacPaint-like interface. Similarly, ZSoft Corporation ships their PC Paintbrush, also DOS-based but with an interface derived from MacPaint.

1985

Microsoft releases a new version of their mouse. They drop their ‘Doodle’ program and replace it with a rebranded version of PC Paintbrush, licensed from ZSoft Corporation. In April, Apple releases System Software 2.0 which includes MacPaint 1.5 (https://archive.org/details/mac_Paint_2). This is the last version until 1988, when MacPaint 2.0 is released by a new developer. There are no more official releases of MacPaint.

Developer: Bill Atkinson

When you start MacPaint, the name Bill Atkinson briefly flashes in the credits. You can also find Atkinson’s name and a small portrait in the About menu.

Screenshot of About window from MacPaint showing Bill Atkinson as author with portrait

MacPaint 1.5 About Window (1985)

Ironically for someone who would rise to be an Apple Fellow, Bill Atkinson did not receive classical training in computing. His undergraduate education was in chemistry and biochemistry with graduate training in neuroscience. However, he was not disconnected from the computing scene. He built both an IMSAI and an Altair computer (Atkinson 2004, pg 4). Further, his college work emphasized using computers and he made contacts that would prove highly influential to his career.

While at UC San Diego, Atkinson met Jef Raskin and was introduced to Raskin’s unconventional computer lab which emphasized direct and real-time connections with computers. He also met Guy Bud Tribble at UCSD, who would later live with Atkinson and help develop the Macintosh. Atkinson’s mentor at the University of Washington, Kent Wilson, introduced him to computer graphics and innovations in the field, such as Ivan Sutherland’s work on Sketchpad.

Lured to Apple in 1978 by Jef Raskin, Atkinson became the company’s first application software developer. His first project was a stock portfolio evaluator because, while Apple featured one in an advertisement, they did not have any in the catalog (Atkinson 2004, pg 7). His second major project was helping port the UCSD Pascal system to the Apple II. With no other structured programming options, Lisa development adopted this version of Pascal (Atkinson 2004, pg 9).

In 1979, Steve Jobs made visits to Xerox Parc along with a small group of Apple employees, including Atkinson. At Parc they were shown the Alto computer, Smalltalk programming language, and (likely) the Bravo text editor (Atkinson 2004, pg 18). Transferred to the Lisa project, Atkinson was responsible for LisaGraf, the foundational graphics library (later named QuickDraw), as well as the original window manager, menu manager, and event manager (Atkinson 2004, pg 20).

Since the Macintosh re-used software from the Lisa, in particular Atkinson’s user interface and graphics code, he moved onto the Macintosh team where he started to work on MacPaint in earnest in 1983.

Unlike the founders of Adobe, Atkinson did not come from the computer graphics research world, but was familiar with research advances through his academic mentors and had met luminaries such as Douglas Engelbart. Similar to the Adobe founders who worked on the same problems multiple times, he had time to iterate, with several years of experiments as Lisa developed from a research effort into a product. Intentionally straddling both foundational and application development, Atkinson had the viewpoint of a “vertical integrator,” able to control where functionality should go and how the interfaces should work.

Development and Testing

Finding life in the Apple office too “busy” (Atkinson 2004, pg 20), he worked out of a home laboratory using a prototype Apple Lisa. The Lisa had a “Workshop” mode which featured a graphical editor and a command-line environment for compiling and other development activities.

Atkinson took Polaroids of the user interface as it evolved and drove into work to share them with the team. Fortunately for posterity, Atkinson saved the Polaroids and we have a detailed visual history of the evolution of the Lisa interface and QuickDraw capabilities as shown in the 2022 CHM interview below. (SketchPad is briefly shown and discussed, starting at 9:45.)

The Macintosh team used the ‘Monkey’ as a durability and robustness test mechanism. Developed by Steve Capps, the Monkey would randomly type keys, move objects, and interact with menus (Atkinson 2010, pg 14-15). The team used a computer running in Monkey mode to effectively stress-test an application. MacPaint was able to survive two weeks without crashing. Monkey mode can be seen in the source code:

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

To prevent Monkey mode from quitting the program and thus ending the test prematurely, the Pascal code calls this function to selectively disable the Apple menu, File menu, and the Quit Program command.

Susan Kare, who served as the graphics designer for the Macintosh, was the main customer of MacPaint. Atkinson watched her use MacPaint and “see what she stumbled on or wished she had” (Atkinson 2004, pg 47). As the only true artist on the team, and someone who used MacPaint as a tool for their job, Kare’s feedback was invaluable. Andy Hertzfeld characterized her impact as “I think a lot of the refinement of MacPaint came from watching an actual user, an actual artist use the program on a day-to-day basis.” (Atkinson 2010, pg 9). Similarly, Atkinson states “I would credit Susan Kare as a co-designer of MacPaint because she used it as I was trying to write it.” (ibid).

Design and Source Code

Physical Description

The MacPaint 1.3 distribution consists of five files:

  1. MacPaint.p, 4,688 lines of Pascal (Lisa Pascal variant)
  2. MacPaint.rsrc, resource description for the program containing icons, strings, and other localizable attributes. The version string identifies itself as version 1.3.
  3. MyHeapAsm.a, 67 lines of assembly for calls into system memory management routines
  4. MyTools.a, 300 lines of assembly defining traps or external calls into QuickDraw. A comment states this file was mostly generated via MakeTTraps. This is the only file with a change log and content attributed to someone other than Bill Atkinson.
  5. PaintAsm.a, 1,809 lines of assembly containing application code called from the Pascal code

We counted physical lines of code using the pascal_count and asm_count programs, both part of David A. Wheeler’s SLOCCount suite.

Data Types and Structures

MacPaint defines very few datatypes for its own use, leveraging instead types from QuickDraw such as Point, Rect (Rectangle), Pattern, and BitMap.

The Pascal code heavily uses global variables; the list extends from lines 212 through 370, with white-space used to group them by commonality. Most of the global variables are used to store various flags or interface state, such as the current font specification. This list is distinct from global constants (lines 27 through 190). Most of the constants are used to specify menu items, buttons, or other interface elements.

The application is a set of tools that, ultimately, modify the document which is a fixed size 1-bit bitmap stored as an array of integers. The document lacks dynamic metadata. As pixels are pretty simple, few abstractions or data types are required.

Pascal vs. Assembly

Roughly one-third of MacPaint’s lines of code are in Motorola 68000 assembly, while two-thirds are in Pascal. In (Young 1985, pg 316), Atkinson explains the rationale and benefit of both languages:

By frequency of working on it, I would bring up the Pascal file 20 or 30 times for every one time I brought up the assembly language file. Basically, the information in assembly language doesn’t really need a lot of maintenance. The assembly-language portion contains things that are there for speed, or that were small and I knew wouldn’t need a lot of maintenance. I put them in assembly language just to reduce the code size. By keeping the main control, flow, and logic in Pascal, the program was more pliable.

Atkinson’s rationale is supported by the list of procedures in each language. Performance-critical code, such as that which manipulates the buffers directly, is in assembly. User interface control logic is in Pascal, as well as code that handles initial setup or rare operations. Operating system calls, such as checking the amount of spare space in the disk drive or invoking the system beep, are in assembly. Since MacPaint was developed concurrently with the operating system, some functionality may have existed in the ROM but had not yet been exposed by Pascal system libraries.

As an example of Atkinson’s assembly style and quality, we present the NearPt function:

 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 returns true if two points are “close enough,” even if they are not truly equal. Since points use integral values, this code is not for handling floating point error, but for human imprecision. For instance, if a user was trying to close a polygon by clicking on a previous point, the code allows the polygon to be closed if the user clicks within a few pixels of an earlier point. Similarly, the mouse may slip a small distance during a double click. While this is not particularly performance critical code as it is called a relatively small number of times, it is code that is unlikely to need to change.

As is typical for his assembly code, the function is documented with the Pascal calling convention. This function is further documented with a mathematical definition; few functions or procedures warranted descriptive comments. Each line is commented semantically. Almost every assembly line in PaintAsm.a is similarly commented.

In contrast, HVConstain is a Pascal procedure. We chose it as an example because it is relatively short and self-enclosed. This procedure is used to constrain or trap an anchor while drawing to a particular direction or a 45 degree angle. For example, the user can hold down shift while drawing a rectangle to force the drawing of a square. Or, while drawing a line, force it to be parallel with the edges of the screen.

	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 has a very consistent naming style for his variables and the code is usually highly readable. In contrast to his assembly code, comments are rare and terse in the Pascal code, although still focused on explaining the semantic purpose of the line.

HVConstrain uses one global variable, shiftFlag, to track the user mode, and three global variables to store state: hConstrain, vConstrain, and ptConstrain. Although global variables, there is only one other procedure that accesses the latter three variables directly: InitConstrain. The three variables are commented as belonging to HVConstrain. The design allows a developer to use the variables without proper initialization or accidentally modifying them. Although this is a violation of the design principle of encapsulation, we believe this was an effective trade-off of code complexity and memory resources.

Message Loop

MacPaint is an early event-driven program. The core of the program is:

	REPEAT
			[...]
		    IF GetNextEvent(everyEvent,theEvent) THEN ProcessTheEvent;
			[...]
	UNTIL quitFlag;

GetNextEvent is a Toolbox Event Manager function that fetches the next event, if it exists, from the event queue. ProcessTheEvent is an application procedure that is a long CASE statement that maps event locations to buttons or, more accurately rectangular areas of the screen. ProcessTheEvent calls other application procedures that control tool-specific modes. ProcessTheEvent is sufficiently low-level it has to measure the time since the last click to differentiate between single and double clicks. Since the user interface is fixed (windows cannot be moved), the code is tedious but easy to follow.

Example: Straight Line Tool

The Straight Line tool is an exemplar of the software design and the intermix of Pascal, assembly, and QuickDraw functionality. For the StraightLine procedure to be invoked, the user will have previously selected the straight line tool from the tool palette and then clicked (and held) the mouse within the content area. While in this procedure (the straight line “mode”), one end of the line will be anchored at the initial point (startPt) while the other end will follow the cursor until the user releases the mouse button. The mode-driven interface gives the user continuous feedback on what the eventual line will look like within the painting.

	{$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;

The first line {$S } is a compiler directive stating that this procedure should live in the default main segment (for more about segments, see the section below Allocation Failure and Segment Anti-Fragmentation)

JamLine resets some QuickDraw state using PenNormal and then sets the pen’s size, pattern, and mode (depending on keys being depressed) to the current palette settings.

PinGridMouse sets the passed in variable to the current position of the mouse as modified by various modes (e.g. snap to grid, fat bits). Similarly, Constrain sets newPt to a 45-degree constrained point value if the user is holding down the shift key.

The REPEAT block tests the StillDown condition. StillDown is a Toolbox Event Manager function that will return false even if the user released and then quickly pressed the mouse button again between invocations.

If a line exists (IF NOT EqualPt), the main buffer will be copied into the alt(ernate) buffer. (QuickDraw uses integral values for a Point’s coordinates, so EqualPt does not need a tolerance parameter.) MainToAlt calls BufToBuf, which is an assembly routine that includes the same MOVEM optimization as BufToScrn (see section Fast Buffer to Screen Copy). AltBufLine writes, to the alt buffer, the line from startPt to newPt using the MoveTo and LineTo QuickDraw routines. The contents from the alt buffer are then sent to the screen buffer using a boundary box that includes the oldPt, thus eliminating (by redrawing) any previous line sent to the screen, but also reducing the amount of written data. (BandToScrn also takes care to hide and show the cursor.)

The lineTop and lineBot variables are declared but unused. Our assumption is that part of the functionality of AltBufLine originally was part of the StraightLine as it is the only other block to use variables with the same names. The program also uses the constant lineTop to describe the top of the line size palette window, so the compiler must not have considered duplicate lineTop declarations as an error.

Interesting Algorithms and Designs

Fast Buffer to Screen Copy

In an interview with the Macintosh team (Lemmon 1984), the team explains their process for optimizing code size and processing time. After arguing that code must be first made correct before being made fast, Atkinson brings up register allocation. He states:

This little baby, the 68000, has sixteen 32-bit registers sitting there, and the way you get performance out of that is to keep them full. Keep the registers full of important stuff all the time. That’s the way you make this processor sing.

The buffer copying code, which is performance critical, illustrates this technique.

MacPaint uses two off-screen buffers for rendering which are then copied to the screen buffer for display. Within the pascal code, the two buffer’s storage are declared as:

   mainBuf:            ARRAY[0..239,0..12] OF LongInt;     
   altBuf:             ARRAY[0..239,0..12] OF LongInt;  

In Pascal, range definitions are inclusive, so each buffer contains 240 rows of 13 LongInts each. The content area is fixed at 416 pixels by 240 pixels. Since a LongInt contains 32 bits, each row stores 416 single-bit pixels.

While MacPaint’s user interface looks well-proportioned to the Macintosh’s display, Atkinson could have designed the content area to be larger or adjusted the design for a portrait arrangement. However, the need to keep registers full suggests the technical reason for the given layout. If we look at the BufToScrn assembly code:

 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

The NXTROW loop is executed 240 times with each execution copying 13 LongInts from the buffer. The Motorola 68k supports a “move multiple” (MOVEM) instruction that accepts up to 13 registers as the source or destination. (The .L informs the assembler we are copying long values.) According to (Motorola 1993), a MOVEM instruction requires \(12 + 4n\) clock periods to move memory from an address stored in an A register to registers and \(8 + 8n\) clock periods to move values stored in registers to a memory location (Table 9-16). Thus, memory transfers require \(20 + 12n\) (where \(n\) is the number of registers) or 176 clock periods. In contrast, if the transfers were performed via MOVE instructions, each transfer requires 12 clock periods or 312 clock periods total (Table 9-18). By maximizing the throughput possible with MOVEM by populating all (relevant) registers, each buffer to screen transfer saves 136 clock periods.

Bucket Fill (Seed Fill)

Bucket Fill is an algorithm that, starting from a given pixel, travels along all adjacent pixels sharing a base color and transforms those pixels to a target color. MacPaint implements a variant of this function by filling the space with a pattern, rather than just a single color. As the boundaries can be arbitrarily complex, this can be an expensive computation. In the worst-case, filling an empty screen, this requires 416 by 240 pixel checks or 99,840 checks in total. Through various tricks, Atkinson’s implementation reduces the amount of work and makes the operation feel fast.

(This algorithm is not used when the boundaries are known, such as painting a filled in rectangle. QuickDraw supported drawing filled in polygons directly.)

By 1983, several researchers had investigated and published algorithms for bucket filling. SuperPaint, in the mid 1970s, was likely the first implementation of the idea (Glassner 2001); (Lieberman 1978) and (Smith 1979) represent early research papers. The MacPaint algorithm is similar to Lieberman’s algorithm as the algorithm travels along vertical and horizontal pixel paths and supports filling in with patterns as well as colors. MacPaint’s code has been optimized for the restrictions in the domain, i.e. one-bit bitmaps.

Within the source code, the Bucket Fill tool is called SeedFill. The SeedFill procedure contains the top-level business logic for the tool:

 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(altBits);
  PenPat(thePat);
  IF firstBlack THEN
    BEGIN
      PenMode(patBic);
      PaintRect(altBits.bounds);
      InvertBuf(@altBuf);
      BufAndBuf(@mainBuf,@altBuf);
    END
  ELSE
    BEGIN
      PenMode(notPatBic);
      PaintRect(altBits.bounds);
      BufOrBuf(@mainBuf,@altBuf);
    END;

  PenNormal;
  AltToScrn;
  clickTime := TickCount;
  GetMouse(clickLoc);
  LocalToGlobal(clickLoc);
  killDouble := TRUE;
END;

Based on the position of the mouse click (startPt) and the current state of the image (mainBits), the procedure determines if the clicked pixel is black (firstBlack is true) or white. The assembly CalcMask procedure travels the image in mainBits, from the startPt, and computes a mask for the eventual pattern, storing this in altBits.

After the boundaries for the fill are determined by CalcMask, the graphics port is set to the alternate buffer and the pattern is set to the currently selected pattern. The pattern transfer mode (patBic or notPatBic) is set to effectively erase the destinations content. The pattern is then filled within the bounding box, inverting the color depending on if the object’s boundary is a black or white pixel. Afterwards, the alternate buffer is sent to the screen and the procedures performs some cleanup. If the result is not what the user wishes, they can undo the action, whereupon the screen switches to the other buffer which contains the content prior to the fill.

CalcMask contains the complicated parts of the program. CalcMask is also shared with the Lasso tool, which similarly needs to match arbitrary shapes.

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
PROCEDURE CalcMask(* srcBits,dstBits: BitMap;
                     limitRect: Rect;
                     firstPt: Point;
                     firstBlack: BOOLEAN;
                     invertDst: BOOLEAN *);

{ given data in srcBits, compute mask into dstBits }
{ WARNING: srcBits and dstBits must be the wired-in size }

LABEL 9;

CONST row  = 52;         { rowbytes }
      queueSize = 400;   { how many entries big }

VAR aQueue,bQueue: ARRAY[0..queueSize] OF QueueEntry;
    readPtr:   EntryPtr;
    writePtr:  EntryPtr;
    donePtr:   EntryPtr;
    dstStart,dstLimit,srcOffset: LongInt;
    firstByte,firstMask: INTEGER;
    firstPtr: ^INTEGER;
    dh,dv:    INTEGER;
    leftByte,rightByte:     INTEGER;
    wordsWide,height: INTEGER;

BEGIN
  ZeroBuf(dstBits.baseAddr);

  IF NOT firstBlack THEN InvertBuf(srcBits.baseAddr);

  dh := -dstBits.bounds.left;
  dv := -dstBits.bounds.top;
  OffsetRect(limitRect,dh,dv);  { make global }
  firstPt.h := firstPt.h + dh;
  firstPt.v := firstPt.v + dv;

  limitRect.left := BitAnd(limitRect.left,$FFF0);       { round down to word }
  limitRect.right := BitAnd(limitRect.right+15,$FFF0);  { round up to word }

  leftByte  := limitRect.left DIV 8;
  rightByte := limitRect.right DIV 8;

  dstStart := ORD(dstBits.baseAddr) + limitRect.top * row + leftByte;
  dstLimit := ORD(dstBits.baseAddr) + limitRect.bottom * row + leftByte;
  srcOffset := ORD(srcBits.BaseAddr) - ORD(dstBits.baseAddr);

  firstByte := 2*(firstPt.h DIV 16);
  firstPtr := Pointer(ORD(srcBits.baseAddr) + firstPt.v * row + firstByte);

  firstMask := 0;
  BitSet(@firstMask,BitAnd(firstPt.h,15));
  firstMask := VertSeed(firstMask,firstPtr^);
  IF firstMask = 0 THEN GOTO 9;

  { Prime "aQueue" with seed at first point }

  writePtr := @aQueue;
  WITH writePtr^ DO
    BEGIN
      addr := ORD(firstPtr)-srcOffset;
      bump := 2;
      twoH := firstByte - leftByte;
      mask := firstMask;
    END;
  writePtr := Pointer(ORD(writePtr) + SizeOf(queueEntry));

 { Ping pong between the two Queues.  Read each entry from one queue }
 { and push all the untried ones it spawns onto the other queue.     }

  REPEAT

    donePtr := writePtr;
    readPtr := @aQueue;                         { read from queue A }
    writePtr := @bQueue;                        { push into queue B }
    MaskIt(dstStart,dstLimit,srcOffset,
           rightByte-leftByte,row,readPtr,donePtr,writePtr);

    donePtr := writePtr;
    readPtr := @bQueue;                         { read from queue B }
    writePtr := @aQueue;                        { push into queue A }
    MaskIt(dstStart,dstLimit,srcOffset,
           rightByte-leftByte,row,readPtr,donePtr,writePtr);

  UNTIL writePtr = @aQueue;                     { until aQueue is empty }

9: IF NOT firstBlack THEN InvertBuf(srcBits.baseAddr);   { restore src }

  IF invertDst THEN
    BEGIN
      height := limitRect.bottom - limitRect.top;
      wordsWide := (limitRect.right - limitRect.left) DIV 16;
      InvertChunk(dstStart,wordsWide,height);
    END;
END;

At a high-level, CalcMask is very similar to (Lieberman 1978)’s fill algorithm. The initial point, the seed, is expanded left and right until a boundary is found, and then the program checks up and down. Lieberman’s version uses a queue, while Atkinson’s uses two arrays as queues. Because a queue can be completed consumed before ‘ping pong’ing to the other queue, iteration and appending items are simpler than using a single queue.

The MaskIt procedure scans right and left from an initial position, and then seeks to expand up and down the image. Based on the movement direction, the algorithm is looking for boundary conditions. Two lower-level algorithms implement much of the bit-level logic: HSeed and VSeed. Both are “local functions,” they are effectively inlined functions within the assembly rather than functions callable (and requiring a stack frame) from Pascal. We will focus on VSeed because it is simpler.

Conceptually, VSeed is trying to extrude the seed up (or down, the directions are equivalent) through the “grill” of the data. If a slice of the seed can flow into an open space of the grill, it will then also flow right and left within the grill to the edge or to a boundary. For clarity of visualization, in the table below we use a 3-bit word size. The one bits in the mask represent the set bits of the target color being “pushed” into the data. The data may be full of the target color, in which case we push into the entire “word”. However, we might encounter a wall, such as “data empty”, in which case we are unable to push to any location, resulting in zero. In the case of a hook, we flow both up and to the open left.

Open Blocked Same Offset Hook Left Hook Right
Data 111 000 010 100 110 011
Mask 010 010 010 010 010 010
Result 111 000 010 000 110 011

The assembly code implementation:

 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
34
35
36
37
38
39
40
41
42
;---------------------------------------------------
;
;  LOCAL FUNCTION VSeed(mask,data: INTEGER ): INTEGER;
;
;  ENTER WITH:  D4 MASK
;               D1 DATA
;               D3 ALL ONES
;
;  RETURNS      D6 SEED
;
;  CLOBBERS     D0,D1,D2
;
VSEED   MOVE    D4,D6                           ;COPY MASK
        AND     D1,D6                           ;SEED := MASK AND DATA
        BNE.S   NOTZERO                         ;CONTINUE IF RESULT NONZERO
        RTS                                     ;ELSE RETURN ZERO

NOTZERO CMP     D3,D1                           ;IS DATA ALL ONES ?
        BNE.S   NOTONES                         ;NO, CONTINUE
        MOVE    D1,D6                           ;YES, RESULT = ONES
        RTS                                     ;AND QUIT

NOTONES CMP     D1,D6                           ;IS SEED = DATA ?
        BNE.S   RIGHTLP                         ;NO, CONTINUE
        RTS                                     ;YES WE'RE DONE

RIGHTLP MOVE    D6,D2                           ;REMEMBER OLDSEED
        MOVE    D6,D0                           ;COPY SEED
        LSR     #1,D0                           ;SHIFT SEED RIGHT
        OR      D0,D6                           ;LEAK SEED TO RIGHT
        AND     D1,D6                           ;AND WITH DATA TO LIMIT
        CMP     D6,D2                           ;IS SEED SAME AS OLD SEED ?
        BNE     RIGHTLP                         ;NO TRY SOME MORE

LEFTLP  MOVE    D6,D2                           ;REMEMBER OLDSEED
        MOVE    D6,D0                           ;COPY SEED
        ADD     D0,D0                           ;SHIFT SEED LEFT
        OR      D0,D6                           ;LEAK SEED TO LEFT
        AND     D1,D6                           ;AND WITH DATA TO LIMIT
        CMP     D6,D2                           ;IS SEED SAME AS OLD SEED ?
        BNE     LEFTLP                          ;NO TRY SOME MORE
        RTS                                     ;AND RETURN

The first half of VSeed is equivalent to the pseudo-code:

switch (mask bitand data)
	case 0: return 0
	case -1: return -1
	case mask /* mask = data */: return mask

The last half performs iterative bit-twiddling to flow the seed into adjoining spaces of data.

The test of the seed against the earlier data also demonstrates the value of ‘playing the odds.’ Although boundaries may be arbitrary, they are often regular in shape – e.g. a boundary being a vertical line. Expensive computations can be reused if a relatively simple check is made first. Atkinson relates this technique to a similar domain in (Lemmons 1984, pg 76):

So play your odds. People draw characters in OR mode a whole lot, and OR mode is about twice as fast as the other modes, so 95 percent of all characters are drawn in OR mode. Statistical measuring of the use of the thing allows you to get much more performance on your average throughput than you can if you don’t go back and measure.”

In cases where the fillable area is open, or edges are regular, the algorithm can effectively process a 32 pixels at a time with much higher throughput than checking each individual pixel.

FatBits

One of the “Goodies” – additional MacPaint features selectable via the menubar – FatBits is a mode that magnifies the image. When in this mode, a small picture-in-picture provides the artist context of the full image while the rest of the content area expands to make it easier to manipulate individual pixels (below). Originally MacPaint only allowed the Pencil tool to be used in FatBits mode, but Atkinson extended support for all the tools (Young 1985, pg 315).

Screenshot of MacPaint 1.5 with magnified content

MacPaint 1.5 in FatBits Mode (1985)

As suggested by the title of an early manual Inside MacPaint: Sailing through the Sea of FatBits on a Single-Pixel Raft, FatBits was a killer feature for MacPaint. For Susan Kare, Apple’s in-house graphic designer, the mode accelerated the development of Macintosh’s small bitmap icons where every pixel mattered.

Marketing pushed to rename “FatBits” “Magnify”, but Atkinson won the argument, arguing that the name gave the program some personality (Young 1985).

FatBits is restricted to a single zoom level and, within the code, is handled via many special-cased changes to the input and output. The single zoom level restriction was due to the limited hardware; an arbitrary scaling factor would have required too much CPU (Atkinson 2010, pg 10). The contemporary PostScript program supported arbitrary scaling via a transformation matrix but operated with looser performance requirements and greater hardware capabilities.

While an important and impactful feature, IF fatFlag is called 36 times in the Pascal code. A cross-cutting feature, it requires special handling when mapping input coordinates, screen rendering, and buffer manipulation. Thus, we consider it an expensive feature because it violates the open-closed design principle — existing code needed to be modified to support it and future tools need special-cased code to support the mode. Functional composition is used to reduce the impact in certain code paths (e.g. GetFatMouse calls GetMouse and internally performs any necessary conversion), but there are too few abstractions overall to keep the cross-cutting nature of the feature constrained.

Allocation Failure and Segment Anti-Fragmentation

As a way to work within the limited memory of the Macintosh, developers could divide their application code into multiple segments. A segment could be unloaded when not needed, for example, printing routines (Inside-Vol2 1985, pg II-55). In the Pascal code, the compiler directive {$S SegPrint } places the procedure or function into the SegPrint segment. An empty name (e.g, {$S } denotes the default, main segment.) In the assembly code, .SEG is used.

Other than the main segment, MacPaint has the following eleven segments: SegBrush, SegFlip, SegHelp, SegInit, SegPage, SetPaste, SegPatEdit, SegPrint, SegScrap, SegSym, and SegUpdate.

Within the main event loop, each of these segments is unloaded after each loop. Per (Inside-Vol2 1985, II-57), this is recommended practice as unloading a segment that is not loaded is a no-op and presumably cheap.

Although a program may have sufficient free space to work, the layout of allocated memory might not leave a block large enough for an allocation to succeed. MacPaint’s design was robust to a certain level of memory fragmentation.

Within the main event loop, the code tests for heapJam to be set and, if it is, calls the MaxMem system routine. This routine compacts and frees unused memory, acting as a kind of garbage collector and compactor. Once fragmentation is reduced, the next memory allocation is likely to succeed.

1
2
3
4
5
    IF heapJam THEN
      BEGIN
        tempLong := MaxMem(tempLong);   { purge heap }
        heapJam := FALSE;
      END;

MacPaint’s implementation largely avoids dynamic memory allocation, but there are eight calls to NewHandle (which allocates memory). Of these, only AllocMask tests the return value of NewHandle to verify it is non-Nil and sets the heapJam global variable if it is. The reason why AllocMask is the only path that checks for a memory allocation failure may be due to its unusually large allocation request. AllocMask asks for 12,480 bytes to store a mask which is nearly 10% of the Macintosh’s 128k memory in total. Other than some calls that are used for initialization, the next largest called to NewHandle is 3,024 bytes and the rest below 100 bytes. As these other calls are much less likely to fail, it is perhaps acceptable that they lacked similar error detection.

All callers of AllocMask check the return status and return early if the call failed. Although the user would need to retry their operation, the program would not suffer a crash due to the failed memory allocation.

Atkinson recounted this general technique in (Atkinson 2010, pg 15):

When code segments were loaded, you needed some code to do this job and that job. They would be loaded sort of at the first available place. But if you needed another code segment and this one would go out, it might leave a hole there that wasn’t quite big enough for the next one that you needed but now you had sort of what we called memory fragmentation. That even though you had enough memory total, you couldn’t load the pieces of code that you needed. And I developed a little technique for this which is setting a flag at the top of the event loop, saying that we failed and as I went to load code segments, if I failed to load one then I would beep and let it go back to the top of the event loop without doing anything. And it would say uh-oh, there’s a failure here. Whereas if I succeeded and got to the right part of the code and got everything in, then I would set the flag to say we succeeded. The net result was the user would go to draw something and it would beep and they would try it again and it would work, and they’d shrug and they’d never know that they just avoided crashing the program.

Alternative Paths and Competitors

As a counter-factual, if MacPaint had not shipped with the Macintosh, Apple may instead have shipped a vector drawing program. The Lisa shipped with LisaDraw, a vector drawing program which was the basis for MacDraw. However, MacDraw was likely not available in time for the January launch, as the September/October issue of MacWorld states MacDraw is “soon-to-be-released”. Since MacPaint was finished several months prior to the Macintosh’s release date, we are unaware of any historical “Plan B”.

Competively, there were several digital painting programs released in parallel. The year prior to the release of MacPaint, 1983, saw the release of the first Microsoft Mouse and two drawing programs that used it: Microsoft’s Doodle program (below) and Doug Wolfgram’s Mouse Draw. Notably, both programs supported color but the user interfaces can be charitably treated as primitive.

Screenshot of Microsoft Doodle showing lack of tool and pattern palettes

Microsoft Doodle (1983)

After the release of MacPaint and the Macintosh at the beginning of 1984, competitors quickly adopted the user interface. Mouse Systems purchased the rights to Mouse Draw, modified the interface to be similar to MacPaint, and then resold it as PC Paint 1.0 along with their own mice. ZSoft Corporation released their own PC Paintbrush, also with an interface modeled after MacPaint. Both programs were DOS-based and supported color. A year later, Microsoft updated their mouse and dropped Doodle, replacing it with a licensed but rebranded version of PC Paintbrush.

Thus, the demand for painting programs, tied to sales of mice, was recognized independently of the Macintosh and multiple vendors were attempting to fill that need. That said, within the consumer market, MacPaint seems to be the first “great” painting program and highly influential.

Technologically, was MacPaint novel? The site macpaint.org claims the following aspects of MacPaint were novel:

The marching ants around a selection; the palette of drawing tools; the (rudimentary) ability to zoom in; the spray can; the paint bucket; copying and pasting images between programs; just moving the mouse and drawing: we take these for granted in the 21st century, but Macpaint did them first, with only 128k of RAM available.

While we concede the marching ants and maybe the spray can were novel to MacPaint, MacPaint has historical antecedents in research and commercial systems. As a contrary example, SuperPaint, which displayed its first picture in 1973, supported color, and featured a tool palette and the following functions: Paint, Shrink 2x, Expand 2x, Move, Copy, Store and Load, Text, Video In, Make Brush, Draw Lines, Gridding, Area Fill, and various color table animation tools (Shoup 2001). Originally aimed at television graphics use cases, this research system was rapidly supplanted by commercial offerings. For example, the Quantel Paintbox, a dedicated computer for compositing broadcast video and graphics, was first released in 1981 and was quickly adopted by the major TV networks.

Although the Macintosh was opening up new markets for consumer and small-scale digital production, broadcasting and commercial computer animation firms had been operating for several years. In Alvy Ray Smith’s article “Digital paint systems: an anecdotal and historical overview” (Smith 2001), he focuses on “systems” rather than “programs” and eliminates 1-bit and 3-bit systems from his article’s scope because of their lack of influence. MacPaint is thus eliminated both due to its limited functionality and its 1-bit nature. Is this restriction fair?

Certainly, the professional market was developing on its own, without regard for the hobbyist market. The Macintosh was not capable until the late 80s to support the monied use cases which required color and higher-performance. However, consumer technology eventually caught up and the existence of consumer-level tools provided a path for artists to “graduate” to professional-grade tools. Photoshop is an example of this progression.

The Knoll brothers, who developed Photoshop in 1988, were both exposed to the original Macintosh when their father bought one in mid-1984. Thomas Knoll used a Macintosh Plus in 1987 to assist in his PhD work, which also came with MacPaint. The Plus did not support gray scale, so Thomas started working on image manipulation routines. John, who was working at Industrial Lights and Magic at the time, saw connections between what Thomas was developing and the features in the Pixar Image Computer. Combining the use case insights from professional animators with a graphical user interface led to Photoshop.

Photoshop was widely adopted both by professionals but also by aspiring students and hobbyists. It represents a joining of two pathways, one from the personal, consumer world, and the other from the research and commercial world.

Impact of Source Code?

In 1984, event-driven programming was a relatively new paradigm in software design. A possible additional influence of MacPaint was the program being used to train developers on how to develop an event-driven program and how to program for the mouse. Atkinson believes Apple gave the source to some developers [Atkinson 2010, pg 8], although we have not seen evidence for this elsewhere. (At least among external developers; the Macintosh team certainly had access to the code and there are three programmers attested in MyTools.a, including Andy Hertzfeld.) Inside Macintosh, the official developer documentation, includes an Example Program that demonstrates basic window management and handling. Comparing the Example Program to MacPaint, we find there are similarities in naming and structure, which suggests MacPaint may have been a reference when writing the documentation.

Post Release

MacPaint saw two more releases in 1984, the first as a free update with System Software 1.1 and the second with the release of the Macintosh 512K model. In 1985, System Software was updated to version 2.0 and included version 1.5 of MacPaint.

Under pressure from third-party developers for less competition from system shipped software, in 1987 Apple spun off their internally built applications to the Claris company, including MacPaint. Claris released version 2.0 of MacPaint in 1988 which featured tear off palettes, support for multiple documents open at once, the magic eraser, and other features.

The release seemed also perfunctory, as evidenced by the developer working on the 2.0 release to fill time and the lack of any product specs. By April 1988, as listed by Macworld, MacPaint was in competition with an array of graphics programs, including PixelPaint, Modern Artist, Aldus Freehand, and Adobe Illustrator. Furthermore, the Macintosh II had been announced in 1987 with support for 8-bit color, limiting the appeal of a monochrome only application. No more versions were officially released and Claris discontinued the product in 1998.

Conclusion

(Knuth Shustek, 2021) has this to say about MacPaint and QuickDraw’s source code:

They are brilliant programs, beautifully organized and structured, that are a treat to read and deserve to be annotated and studied.

This article does not cover QuickDraw, but do we agree with these claims as they relate to MacPaint?

Is it brilliant?

It is uncontroversial that MacPaint strongly influenced the user interfaces of painting and drawing programs. Contemporary competitors changed their UIs to be similar to MacPaint and much of interface design live on in Photoshop, Illustrator, and other programs. Many of the tool modifiers, such as the shift key to constrain boxes and lines, became informally standardized. Although MacPaint was not the earliest painting program and did not invent all it is often credited with, it was the first “great” implementation, was widely available, and helped cement Macintosh’s reputation as a computer for creative people.

Is it beautifully organized and structured? A treat to read?

This is subjective, so we’ll examine certain aspects of the code.

If we look at the physical sequencing of functions and procedures within the code, we find that the first five callables within the PaintAsm.a file are:

  1. EjectReset (eject disks)
  2. PixelTrue (return state of specific pixel within bitmap)
  3. Monkey (test for reliability testing mechanism)
  4. Stretch2x (re-samples buffer for higher resolution printing)
  5. MySetItemStyle (type coercion utility)

Within the Pascal MacPaint.p file, after a long list of EXTERNAL declarations, we find:

  1. KeyIsDown (function to test if a specific key is pressed)
  2. GetFatMouse (get mouse position in fat bit coordinates)
  3. GridPoint (modify point to a grid or truncated position)
  4. GetGridMouse (get mouse position in grid coordinates)
  5. PinGridMouse (clamp grid mouse position within rectangle)

We reject the idea that the physical organization of the code is “beautifully organized”; if someone wanted to understand the core functionality and design of the program, they should not read it linearly. However, we suspect this was not the foundation of Knuth and Shustek’s claim, as outside of very short or a few literate programs, programs are not read linearly.

Readers will often start with the entry point to the program. In this case, the reader will find a significant number of lines dedicated to UnloadSeg and MoreMaster, both efforts to mitigate the constrained memory. Outside of that, the master block contains sequence of Init... calls, followed by the main loop. The naming and structure facilitate distinguishing between the business logic and the system interfaces. So, we find that the main block is not necessarily beautiful, since it interleaves both kinds of logic, but the organization and structure are praise-worthy.

Within a procedure or function, Pascal mandates some aspects of the ordering, but we find that the code does not fight language idioms. In general, in terms of code style, we find it is easy to read and follow. Anecdotally, while we were writing this article, another programmer saw the Pascal code on our laptop screen (main method) and noted that it appeared clean. (They also thought it looked “old” and certainly procedural code has a different visual appearance than functional or OOP code.)

Does it deserve to be annotated and studied?

There are three primary audiences for a historical source code artifact: 1) historians, interested in the context and impact of an artifact, 2) software practitioners, who can see it as a way to improve their craft, and 3) students, who may be introduced to techniques, practices, and paradigms and use it as training material.

For historians, MacPaint exhibits the engineering trade-offs necessary to bring forth an application within the limitations of the Macintosh hardware, as well as an example of how a graphical program was expected to be designed by someone who also wrote one of the largest, most foundational libraries. Historians of algorithms can place the use of multiple buffers and the seed fill algorithm into the published history of graphics algorithms and how that informs the interrelation of commercial and academic research.

Unless a practitioner is working within extreme resource limits or is working on legacy devices, we suspect the MacPaint source code has less to offer them. User interfaces are now asynchronous and callback based, languages are far richer than Pascal in their support of abstractions and encapsulation, and few use cases are restricted to 1-bit displays.

However, we see many advantages as an artifact of study for students of computer science. First, MacPaint is not a toy, but a full-featured program, yet is quite small so there is little to distract the student. MacPaint’s domain — painting — is readily understood and does not require students to understand unfamiliar domains such as finance or physics. Students are expected to understand multiple languages. Pascal and M68k assembly test a student’s ability to learn a new language while neither is particularly difficult nor obscure.

Although MacPaint runs in emulation and students can easily play with it, a disadvantage is that students would have difficulty modifying the source code and running their own versions. MacPaint doesn’t even run on late series Macintoshes as it was too tightly bound to the hardware. That said, there are many potential assignments that do not require modifying and running the source code:

So, yes, MacPaint deserves to be studied.

Special Recognition

Releasing the source code was surprisingly long and difficult. I thank those involved for their dogged persistence that gave me the opportunity to study this source code.

References

(Atkinson 2004) Atkinson, Bill, and Andy Hertzfeld. 2004. MacPaint oral history with Bill Atkinson and Andy Hertzfeld Interview by Grady Booch. Oral history collection. https://www.computerhistory.org/collections/catalog/102658007.

(Atkinson 2010) Atkinson, Bill, and Andy Hertzfeld. 2010. MacPaint Interview and Demonstration with Bill Atkinson and Andy Herzfeld. https://www.computerhistory.org/collections/catalog/102743021.

(Glassner 2001) Glassner, Andrew. 2001. “Fill ’er up! [Graphics Filling Algorithms].” IEEE Computer Graphics and Applications 21 (1): 78–85.

(Inside-Vol2 1985) Apple Computer, Inc. 1985. Inside Macintosh. Vol. 2. Addison-Wesley Publishing Company, Inc. https://vintageapple.org/inside_o/pdf/Inside_Macintosh_Volume_I_1985.pdf.

(Knuth Shustek, 2021) Knuth, Donald, and Len Shustek. 2021. “Let’s Not Dumb down the History of Computer Science.” Communications of the ACM 64 (2): 33–35. https://doi.org/10.1145/3442377.

(Lemmons 1984) Lemmons, Phil. 1984. “An Interview: The Macintosh Design Team.” BYTE, February 1984.

(Lieberman 1978) Lieberman, Henry. 1978. How to color in a coloring book. In Proceedings of the 5th annual conference on Computer graphics and interactive techniques (SIGGRAPH ‘78). Association for Computing Machinery, New York, NY, USA, 111–116. https://doi.org/10.1145/800248.807380

(Shoup 2001) Shoup, Richard. 2001. “SuperPaint: An Early Frame Buffer Graphics System.” IEEE Annals of the History of Computing 23 (2): 32–37.

(Smith 1979) Smith, Alvy Ray. 1979. Tint fill. SIGGRAPH Comput. Graph. 13, 2 (August 1979), 276–283. https://doi.org/10.1145/965103.807456

(Smith 2001) Smith, Alvy Ray. 2001. “Digital Paint Systems: An Anecdotal and Historical Overview.” IEEE Annals of the History of Computing 23 (2): 4–30.

(Young 1985) Young, Jeffrey S. 1985. Inside MacPaint: Sailing through the Sea of FatBits on a Single-Pixel Raft. Microsoft Press.