Close

A Line Drive

A project log for Lunar Lander for the PDP-1

My PDP-1 Replica (PiDP-1) from Obsolescence Guaranteed has arrived and I want to do something cool with it.

michael-gardiMichael Gardi 12/07/2025 at 16:280 Comments

Before jumping into Lunar Lander I wanted to write a program that was a lot less ambitious but still useful moving forward.  Obsolescence Guaranteed's  PROGRAMMING THE DEC PDP-1 - A QUICK WAY TO GET STARTED guide has a simple draw a circle example.

CIRCLE
100/
go,     lac x
        lup,    
        cma        
        sar 4s        
        add y        
        dac y        
        lio y        
        sar 4s        
        add x        
        dac x        
        dpy        
        jmp lup

x,      200000
y,      0
start    go

Without going into a lot of detail:

In order to get this snippet of code to run here is what a PDP-1 programmer would have had to do:

  1. Mount the paper tape with an editor onto the reader and load it into the machine.  This would most likely have been ET (short for Expensive Typewriter or more formally 'the PDP-1 Symbolic Tape Editor'). ET is a modal editor much like VIM today.
  2. Once ET was loaded you could key in or edit your program using a hardcopy system console, often a Teletype Model 33 ASR
  3. When done editing you would instruct ET via a "p" command to punch your code onto a paper tape remembering to add a STOP code at the end ("s" command).
  4. Mount the assembler (MACRO) paper tape and load it by pressing the READ IN switch on the console. 
  5. Mount your program tape and load it by pressing the CONTINUE switch on the console. The console will type "Pass 1" (assuming there were no errors).
  6. Mount your program tape again and reload it by pressing CONTINUE again. "Pass 2" will be typed to the console and the paper tape punch will start to output the binary tape.
  7. Press CONTINUE one more time to punch a start block to the end of the tape.
  8. At this point you can remove the paper tape from the punch, load in into the tape reader, and run the code by pressing READ IN.

Easy right. To be fair when I was in high school in the late 60s I had to "keypunch" my programs onto cards and send them off to be run with 2 day turn around. So I would have welcomed the system described above. I sure hope that paper tape was cheap back then because they must have burned through a ton of it.

For my starter program I decided to make a line drawing routine based on Bresenham's algorithm since it is well suited to simple integer based systems with no floating point hardware like the PDP-1. This felt like it was:

Here is the pseudo code that I used as my template:

plotLine(x0, y0, x1, y1)    
    dx = abs(x1 - x0)    
    sx = x0 < x1 ? 1 : -1    
    dy = -abs(y1 - y0)    
    sy = y0 < y1 ? 1 : -1    
    error = dx + dy        
    while true        
        plot(x0, y0)        
        e2 = 2 * error        
        if e2 >= dy            
            if x0 == x1 break            
            error = error + dy            
            x0 = x0 + sx        
        end if        
        if e2 <= dx            
            if y0 == y1 break            
            error = error + dx            
            y0 = y0 + sy        
        end if    
    end while

The development progressed pretty well. Fortunately I did not have jump through as many hoops as the original PDP-1 programmers did to edit and run my code. On a Raspberry Pi with the PiDP-1 tools installed the steps I took were:

  1. Write the assembly code using a text editor. I just used the built in mousepad editor. It was fine. 
  2. From a terminal window assemble the code using macro1_1. This will produce a .rim “paper tape” file and a .lst file with assembly details. 
  3. From the desktop run the load tape application and select your .rim file. A virtual graphic of the tape will appear in the virtual tape reader. 
  4. Click the READ IN switch on either the virtual or physical PiDP-1 console. You will see an animation of the tape loading and when it stops if all went well your program will start running. 

As someone who has been using some kind of IDE to write programs for the past 35 years or so, writing code in this environment is a bit of a shock. You can't just view your program on a screen,  set breakpoints by clicking on lines of code, or even easily add a “print” statement.  There was a debugger available at the time, DDT (Dynamic Debugging Technique), that looks pretty useful. It was command driven and accessed through the teletype console. I had a few problems with my code but never had to make the effort to learn DDT (so far). 

Bresenham's  Line Drawing Algorithm

Here is the code:

/ Draw the line defined by the points X1,Y1 and X2,Y2. Called as subroutine.
drt,    jmp .
dln,    dap drt         / Save the return address.
        lac X1          / Get the starting coordinate.  
        dac x
        lac Y1
        dac y

        lac X2          / Calculate dx.
        sub X1          / X2 - X1
        spa             / Skip compliment if AC positive.
        cma             / Compliment AC to get positive value.
        dac dx          / Save absolute value in dx.
    
        lac Y2          / Calculate -dy.
        sub Y1          / Y2 - Y1
        spa i           / Skip compliment if AC negative.
        cma             / Compliment AC to get negative value.
        dac dy          / Save negative value in dy.

        lio PSLOPE      / Positive slope to start.  
        lac X2          / Calculate sx.
        sub X1          / X2 - X1
        spa             / Skip switch if AC positive.
        lio NSLOPE      / Negative slope.
        dio sx          / Save slope in sx.

        lio PSLOPE      / Positive slope to start.  
        lac Y2          / Calculate sy.
        sub Y1          / Y2 - Y1
        spa             / Skip switch if AC positive.
        lio NSLOPE      / Negative slope.
        dio sy          / Save slope in sy.

        lac dx          / Intitialize the error term.
        add dy
        dac err

/ Plot the points of the line.
lup,    plot x,y        / Draw the next point.

/ Figure out the next point to plot.
ctn,    lac err         / Calculate the e2 term.
        sal 1s          / e2 = err * 2
        dac e2

/ Check for y change.
        lac e2          / Is (e2 > dy)?
        sub dy          / e2 - dy
        sma             / Skip if negative.
        jmp doy         / Process y change.
        jmp dox         / Check for x change.

doy,    lac X2          / Is x done?
        sub x           / x2 - x 
        sza i           / Skip if not zero.
        jmp dne         / Line done. 

        lac err         / err = err + dy;
        add dy
        dac err         / err + dy

        lac x           / x = x + sx
        add sx
        dac x           / x + sx

/ Check for x change.
dox,    lac dx          / Is (e2 < dx)?
        sub e2          / dx - e2
        sma             / Skip if minus.
        jmp prx         / Process y.
        jmp lup         / Keep drawing line.

prx,    lac Y2          / Is y done?
        sub y           / y2 - y
        sza i           / Skip if not zero.
        jmp dne         / Line done.

        lac err         / err = err + dx
        add dx  
        dac err         / err + dx

        lac y           / y = y + sy
        add sy
        dac y           / y + sy
        jmp lup         / Keep drawing the line.
dne,    jmp drt

/ Temporary variables used by drawline.
x,  0                   / Used to "walk" the line.
y,  0
dx, 0                   / Amount to change x and y on each step.    
dy, 0 
sx, 0                   / Direction of x and y movement.
sy, 0
err, 0                  / Error terms used in algorithm.
e2, 0

/ Line coordinates.
X1, 333                 / Starting coordinate.
Y1, 333
X2, 1333                / Ending coordinate.
Y2, 1333

/ Constants.
PSLOPE,   1             / Line has positive slope.
NSLOPE,   -1            / Line has negative slope.

This is pretty much a direct implementation of the pseudo code listed above. The two biggest problems that I had getting this code to run had nothing to do with the code itself but are I think worth mentioning.

Problem No. 1 Screen Coordinates

There is an actual opcode, coded as dpy, used to plot a point on the Type 30 Display. When dpy is executed it uses the value of the AC (accumulator) register as the X coordinate and the value of the IO (input-output) register as the Y coordinate to draw a dot on the CRT.  Note that the hardware does this directly, there is no screen buffer so after a few seconds the dot will fade and be gone. This means that to draw a persistent image on the screen the program itself must continually refresh it.  Problem No. 1A for me was that I had failed to read the fine print that only the upper 10-bits of each register was used to plot.  Sigh. 

Problem No. 1B was equally as stupid on my part. Another point I missed was that the origin of the screen coordinates is in the center of the screen.  Like this:

          +511

-511       0/0      +511      

          -511

I had assumed with my 21st century programming bias that the screen origin was in the lower left corner with increasing values going up and to the right. The annoying thing about both these problems, other than the fact that they were completely avoidable had I RTFM more carefully, was that I was getting dots on the screen, just not where I expected them. So I kept looking for bugs in my line code.

Both of these were corrected in my plot(x ,y) routine.

/ Swap values between AC and IO.
define swap
    rcl 9s            
    rcl 9s
    term

/ Draw a point at X,Y on the display. Note X and Y are positive integers 
/ between 0 and 1023 that are converted to screen coordinates by this macro.
define plot X,Y
    lac Y            / Get the y coordibate.
    add SOFFSET      / Convert to screen coordinates.
    sal 8s           / Move y to high bits.
    swap             / Move value from AC to IO register.        
    lac X            / Get the x coordinate.
    add SOFFSET      / Convert to screen coordinates.    
    sal 8s           / Move x to high AC bits.
    dpy-i            / Draw pixel. Don't wait.
    term

/ Constants.
SOFFSET,  -777       / Convert 0-1023 to screen coordinates.

This a macro definition. As a todo list item I would like to get the line drawing algorithm to work directly with screen coordinates avoiding all of this extra overhead, making the routine much faster.

Testing and Problem No. 2

I wrote a couple of programs to test the line drawing code.  The first just continuously generates random end point coordinates and draws a line between them.

... line and plot routines ...

/ Generate a random number.
define random 
    lac ran
    rar 1s
    xor (355670
    add (355670
    dac ran 
    term
  
/ Random number goes here.
ran,    0

/ Install sequence break routine.
3/      jmp sbf     / Ignore sequence breaks.
        jmp go      / Start address of program.

/ Routine to flush sequence breaks if they occur.
sbf,    tyi         / Reset console typewriter.
        lio 2       / Restore IO from address 2.
        lac 0       / Restore AC from address 0.
        lsm         / Leave sequence break mode.
        jmp i 1     / Jump indirect through address 1.
    
/ Mainline.
go,     jsp dln     / Draw the line.

        random      / Set new starting coordinate.
        and LOWBITS
        dac X1
        random
        and LOWBITS
        dac Y1
    
        random      / Set new ending coordinate.
        and LOWBITS
        dac X2          
        random
        and LOWBITS
        dac Y2
    
        jmp go      / Do the next line.
    
/ Constants.
LOWBITS,  1777      / Mask off the upper 8 bits leaving the lower 10 bits.
    
/ Insert constants and variables, end of program.

    constants
    variables

start 4

At the top there is a random number macro that when called leaves an 18-bit random number in the AC register.  This code is lifted directly from Norbert Landsteiner's ICSS document and can be found verbatim in the Spacewar code.  Random will produce 262,144 unique 18-bit random numbers before repeating.

Just below random there is code to "flush sequence breaks if they occur". Again from ICSS this is part of the Computer Space program and Spacewar. Sequence breaks are interrupts. If someone presses a key on the console teletype a sequence break is initiated. This code basically just ignores such interrupts while the game is being played.  I am following the conventions used in ICSS and Spacewar closely because I hope one day to run my code on a real PDP-1 and thus want to follow "best practices".

Problem No. 2 occurred when I tried running this program. Instead of seeing ever changing lines being drawn, the sequence of lines would start repeating after a small number of iterations, overwriting lines that could still be seen on the screen due to the (virtual) persistence. It's highly unlikely that random was broken. I posted this issue to the PiDP-1 Google Group.  Finally I took a closer look at the Computer Space code and realized that I had not included the "constants" and "variables" directives as seen above. The simple CIRCLE program did not have them so I assumed they were optional (another bad assumption on my part).  When I posted this finding Norbert Landsteiner replied:

Whenever the CONSTANTS keyword is encountered, any previously assembled 
code is dumped onto a data block on tape and memory is allocated 
in-place for any newly gathered constant expressions.

So the working theory is that in the absence of the CONSTANTS keyword the ran variable that random uses was not being initialized to 0 properly, and that the value being randomly ;-) picked up resulted in a short cycle.  Tricky.

Here is the demo program running. I call it cloud chamber. 



I like the way new lines get piled on top of the old lines as they fade to black. The long persistence of the Type 30 CRT phosphors (expertly rendered by the virtual PiDP-1 screen) can be used to great effect when creating demos. 

Discussions