← Back to Courses
module
36

Appendix VI - Drawing a line

Optional. The course's own path builds games from tiles and sprites, not freehand pixels - S17 explains why. This is for the times you still want a line: a diagonal beam, a trajectory, a debug overlay, a title screen with a curve in it.

The part that is not new

Setting one pixel is already fully covered: S16 gives the bitmap address of pixel (x, y), and this course has since confirmed that a pixel can be set without disturbing its seven neighbours - read the byte, OR the bit in, write it back. Nothing below invents a new hardware fact; it is all built on that.

; --- set pixel (x, y): compute the address, then read-modify-write the bit
; H = ((y/64) * 8) or ((y/8) mod 8)
; L = ((x/8) * 8) or (y mod 8)
; bit = 80h >> (x mod 8)
;
setpx:  ; HL holds the address, B holds the bit, per the formula above
        LD A,L
        OUT (9),A
        LD A,H              ; no 40h - a read
        OUT (9),A
        IN A,(8)
        OR B
        LD C,A
        LD A,L
        OUT (9),A
        LD A,H
        ADD A,40h           ; the write bit
        OUT (9),A
        LD A,C
        OUT (8),A
        RET

The rest is not a hardware fact - it is arithmetic

Given setpx, drawing a line between two points is a question with nothing Einstein-specific left in it: which pixels lie between (x1, y1) and (x2, y2)? That is Bresenham's line algorithm, published in 1965 and reprinted in essentially every graphics text since - general computer science, not something this project's discipline has any business "confirming" against the machine, any more than it would test whether 2 + 2 still equals 4 here.

The shape of it: walk from one endpoint to the other along whichever axis covers more ground, and carry a running error term that decides, pixel by pixel, whether the other axis needs to step too.

; --- drawline: (x1,y1) to (x2,y2), all four in the named bytes below
;
; dx, dy: the absolute distances. sx, sy: +1 or -1, the direction of travel
; on each axis. err: dx - dy to start, adjusted every step. This is the
; textbook Bresenham loop - nothing here is Einstein-specific.

drawline:
        LD A,(x1)
        LD (px),A
        LD A,(y1)
        LD (py),A

dl_loop:
        ; --- plot the current point
        ; (compute HL/B for (px,py) as in S16, then CALL setpx)

        LD A,(px)
        LD B,A
        LD A,(x2)
        CP B
        JR NZ,dl_cont
        LD A,(py)
        LD B,A
        LD A,(y2)
        CP B
        RET Z               ; reached the end point - done

dl_cont:
        LD A,(err)
        ADD A,A             ; e2 = 2*err
        LD B,A

        LD A,(dy)
        NEG
        CP B
        JR Z,dl_stepx        ; e2 == -dy: step x only
        JR NC,dl_stepx       ; e2 > -dy: step x

        LD A,(dx)
        CP B
        JR Z,dl_stepy         ; e2 == dx: step y only
        JR C,dl_stepy         ; e2 < dx: step y (else fall through: step both)

dl_stepx:
        LD A,(err)
        LD B,A
        LD A,(dy)
        NEG
        ADD A,B
        LD (err),A
        LD A,(px)
        LD B,A
        LD A,(sx)
        ADD A,B
        LD (px),A

dl_stepy:
        LD A,(err)
        LD B,A
        LD A,(dx)
        ADD A,B
        LD (err),A
        LD A,(py)
        LD B,A
        LD A,(sy)
        ADD A,B
        LD (py),A

        JR dl_loop

This is deliberately a skeleton rather than a ready-to-assemble program: dx/dy/sx/sy/err need setting up from the two endpoints first (the usual subtract-and-take-the-sign a reader who has reached S10 already knows how to write), and the branch structure above needs the two "step both" paths falling through correctly rather than being sketched as comments. The point of showing it at all is the shape of the algorithm - error accumulation trading a division for an addition every step, which is exactly why it suits a chip with no divide instruction - not a listing to type in unchanged.

What it costs

Every step is a setpx, and setpx is a VRAM read, a VRAM write, and the address arithmetic for both. A line of a hundred pixels is on the order of a hundred read-modify-write cycles - considerably more work than moving a sprite, which is two bytes regardless of how far it travels. This is the same trade S15 introduced for the whole bitmap: total control over every pixel, paid for one pixel at a time. For a game's moving parts, S19's sprites almost always win. For the parts that hold still - a diagram, a decorative curve, a line only ever drawn once - the cost is paid once, and this is the right tool.

Where this material came from

The algorithm's shape here was cross-checked against a published Z80 implementation for VDP-based machines (AFW Software's Power Graphics booklet, itself citing A. Redfern, "Draw the Line", Personal Computer World, March 1989, and H. Schildt, C: The Complete Reference) - useful for the algorithm, not as evidence about the Einstein. That source's actual code is written for a different machine's port numbers and needs adapting before it would run here at all; nothing from it was carried over unverified, and the pixel-address and read-modify-write mechanics above are this project's own confirmed facts, not borrowed ones.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.