The World Vol 2 · Drawing the World
ch 13 / 105
Chapter 13

Rectangles First, Then Lines

Fill line and clip

Ground tiles are small rectangles, the tick panel is a filled rectangle, and a cursor box is four straight runs of pixels. The drawing rule is the wall around all of them: every buffer holds a clip rectangle; no drawing operation writes a pixel outside it, and the check is made as early as the operation can make it: once for a whole rectangle, once per pixel for a line.

There is no third kind of drawing hiding in that list. Fill a rectangle, draw a line between two points, and the volume is those two calls with better arguments.

Chapter 12 left both of them one step away and one promise short. The loops that painted the rim and the pond said Set once per pixel, and Set quietly drops any coordinate that is not on the buffer.

The note attached to that decision made a commitment: the bounds check does not disappear, it moves up into the operations that draw, where the question can be asked once for a whole rectangle instead of once per pixel. Collecting that promise means writing drawing code that skips Set entirely.

The buffer's own edge is not enough. A minimap sits in a corner, a dialogue panel covers the bottom third, and a viewport shows the valley in the space left over; each needs drawing to stop at a border nowhere near the edge of the buffer.

In only ever knew one rectangle, the one with the buffer's own width and height. The wall stops being a hard-coded comparison and becomes a piece of data the buffer carries.

The {-4 7 4 15} rectangle

Start with the convention, because every rectangle bug in graphics starts with the other one. A rectangle here is half-open: it holds every pixel from X0 up to but not including X1, and from Y0 up to but not including Y1. So {3 2 9 6} covers columns 3 through 8 and rows 2 through 5, six pixels wide and four tall. Width becomes a plain subtraction with no stray +1 to forget, two rectangles side by side share an edge number without sharing a pixel, so tiles tile, and a rectangle holding nothing is one where X1 == X0.

▣ Build · stage 1: a rectangle type, and a wall made out of one
// internal/render/draw.go
package render

// Rect is a half-open rectangle of pixels: it holds every pixel with
// X0 <= x < X1 and Y0 <= y < Y1, so its width is X1-X0 and its height
// is Y1-Y0.
type Rect struct {
	X0, Y0, X1, Y1 int
}

// Empty reports whether the rectangle holds no pixels at all.
func (r Rect) Empty() bool { return r.X1 <= r.X0 || r.Y1 <= r.Y0 }

// Contains reports whether one pixel lies inside the rectangle.
func (r Rect) Contains(x, y int) bool {
	return x >= r.X0 && x < r.X1 && y >= r.Y0 && y < r.Y1
}

// Intersect returns the pixels the two rectangles share, which is
// empty if they overlap nowhere.
func (r Rect) Intersect(s Rect) Rect {
	if s.X0 > r.X0 {
		r.X0 = s.X0
	}
	if s.Y0 > r.Y0 {
		r.Y0 = s.Y0
	}
	if s.X1 < r.X1 {
		r.X1 = s.X1
	}
	if s.Y1 < r.Y1 {
		r.Y1 = s.Y1
	}
	if r.X1 < r.X0 {
		r.X1 = r.X0
	}
	if r.Y1 < r.Y0 {
		r.Y1 = r.Y0
	}
	return r
}
// internal/render/buffer.go — the buffer carries its wall
type Buffer struct {
	W, H int
	Pix  []Color

	clip Rect
}

// NewBuffer allocates a w-by-h buffer in a single allocation. Every
// pixel starts at the zero value, Clear, and drawing may write
// anywhere on it until somebody narrows the clip rectangle.
func NewBuffer(w, h int) *Buffer {
	b := &Buffer{W: w, H: h, Pix: make([]Color, w*h)}
	b.clip = b.Bounds()
	return b
}

// Set writes one pixel, if the clip rectangle allows it.
func (b *Buffer) Set(x, y int, c Color) {
	if !b.clip.Contains(x, y) {
		return
	}
	b.Pix[b.index(x, y)] = c
}
// internal/render/draw.go — the wall's controls, and the first user of it

// Bounds is every pixel the buffer owns: the widest the clip can be.
func (b *Buffer) Bounds() Rect { return Rect{0, 0, b.W, b.H} }

// Clip is the rectangle drawing is currently allowed to write inside.
func (b *Buffer) Clip() Rect { return b.clip }

// SetClip narrows the writable area, and can only ever narrow it: the
// requested rectangle is intersected with the buffer's own bounds.
func (b *Buffer) SetClip(r Rect) { b.clip = r.Intersect(b.Bounds()) }

// FillRect paints every pixel of r that the clip rectangle allows.
func (b *Buffer) FillRect(r Rect, c Color) {
	r = r.Intersect(b.clip)
	if r.Empty() {
		return
	}
	for y := r.Y0; y < r.Y1; y++ {
		row := b.Pix[b.index(r.X0, y):b.index(r.X1, y)]
		for i := range row {
			row[i] = c
		}
	}
}
// cmd/worldc/main.go — three rectangles: one inside, one hanging off, one nowhere near
	b := render.NewBuffer(24, 12)
	b.Fill(render.Soil)

	pond := render.Rect{X0: 3, Y0: 2, X1: 9, Y1: 6}
	scree := render.Rect{X0: -4, Y0: 7, X1: 4, Y1: 15}

	fmt.Println("clip:  ", b.Clip())
	fmt.Println("pond:  ", pond, "->", pond.Intersect(b.Clip()))
	fmt.Println("scree: ", scree, "->", scree.Intersect(b.Clip()))

	b.FillRect(pond, render.Water)
	b.FillRect(scree, render.Rock)
	b.FillRect(render.Rect{X0: 30, Y0: 2, X1: 40, Y1: 4}, render.Rock)
	fmt.Print(b.Preview())
$ go run ./cmd/worldc
clip:   {0 0 24 12}
pond:   {3 2 9 6} -> {3 2 9 6}
scree:  {-4 7 4 15} -> {0 7 4 12}
........................
........................
...~~~~~~...............
...~~~~~~...............
...~~~~~~...............
...~~~~~~...............
........................
####....................
####....................
####....................
####....................
####....................

The scree is the one to study. It was asked for at {-4 7 4 15}, four columns west of the buffer and three rows below it, and Intersect turned it into {0 7 4 12} before a single pixel moved: four comparisons, one per side, each keeping whichever edge is further in. The clamps underneath handle the case where that leaves nothing, which is what the third call hit. {30 2 40 4} against a 24-wide buffer comes back as {30 2 30 4}, zero pixels wide, and Empty sends FillRect home before the loop starts. Nothing drawn, nothing said, because a rectangle entirely off the buffer is routine: in a scrolling map most of the world is off-screen every frame.

Look at what the fill loop does not do: it never calls Set. It takes a row slice, b.Pix[b.index(r.X0, y):b.index(r.X1, y)], and writes straight through it. Both ends of that slice are checked once, when the slice is made, and the for i := range row that follows writes through an index the compiler can prove is inside it. That is chapter 12's promise collected: 24 pixels of pond cost one intersection and four bounds checks instead of 24 coordinate tests, and the saving grows with the rectangle. A row is also contiguous in memory, so the fill walks forward instead of hopping.

Two smaller decisions carry weight across the drawing package. Set now asks clip instead of In, so every per-pixel write inherits the movable wall, and because NewBuffer starts the clip at Bounds(), nothing chapter 12 wrote behaves differently today. At keeps asking In: reading a pixel outside the clip is sensible, and the clip governs writing only.

Seven columns and three rows

Now the harder half. Draw a stream from {2 2} to {9 5}: seven columns east, three rows south. Every column the true line drops three sevenths of a row, and three sevenths of a pixel does not exist. Some columns have to take a whole row's drop and others none, and the question is which, answered the same way every time on every machine. Before any code, walk it with a pencil.

∑ Interlude: keeping the error honest, in whole numbers

Call the run dx = 7 and the rise dy = 3. Instead of tracking the true line's height as a fraction, track how far it has risen above the row being drawn on, measured in sevenths of a row and stored as a whole number of them. Call that number e and start it at 0, with the pen on row 2.

Each column east, the line rises dy sevenths, so add 3 to e. Once the accumulated rise passes half a row, the nearest pixel is the next row down: step down, and take a whole row, dx sevenths, back out of e. Half a row is dx/2, and halves are exactly what integer arithmetic hates, so double both sides of the question. Instead of asking whether e has reached dx/2, ask whether 2e has reached dx. Same question, no fractions, no division anywhere.

xe2e2e ≥ 7?y
200no2
336no2
4612yes3
524no3
6510yes4
712no4
848yes5
900no5

Read the row for x = 4. e was 3, adding dy makes it 6, and 12 has reached 7, so the pen drops to row 3 and e becomes 6 − 7 = −1. Negative is not a mistake, it is the point: after the step the true line sits one seventh of a row above the pixel row, so e is a signed distance from the row being drawn on, never more than half a row either way.

The last table row holds the property that makes this exact. Over seven columns the total added is 7·3 = 21 sevenths and the total taken back out is 3·7 = 21, so e returns to 0 and y arrives at 5, the endpoint asked for. It could not come out otherwise: e is pinned within half a row of zero, so after dx columns the number of rows stepped can only be dy. A line drawn this way cannot miss its own endpoint.

dx
the run: how many columns the line covers, x1 − x0, a whole number of pixels
dy
the rise: how many rows the line covers, y1 − y0, a whole number of pixels
e
the error: how far the true line sits above the row being drawn, counted in units of one dx-th of a row, always a whole number
2e ≥ dx
the doubled question "has the line risen at least half a row?", asked without fractions
▣ Build · stage 2: the same walk, in a loop
// cmd/worldc/main.go — a throwaway: lines that only run east and south

// lineShallow draws a line running left to right whose rise is no
// greater than its run, keeping the error as a whole number.
func lineShallow(b *render.Buffer, x0, y0, x1, y1 int, c render.Color) {
	dx, dy := x1-x0, y1-y0
	e, y := 0, y0
	for x := x0; x <= x1; x++ {
		b.Set(x, y, c)
		e += dy
		if 2*e >= dx {
			y++
			e -= dx
		}
	}
}
$ go run ./cmd/worldc   (the walk printed, then the pixels it chose)
  x     e    2e  2e >= 7   y
  2     0     0    false   2
  3     3     6    false   2
  4     6    12     true   3
  5     2     4    false   3
  6     5    10     true   4
  7     1     2    false   4
  8     4     8     true   5
  9     0     0    false   5
........................
........................
..~~....................
....~~..................
......~~................
........~~..............
........................
........................
........................
........................
........................
........................

Eight columns, eight pixels, and the staircase steps down exactly where the pencil said it would. Per pixel the method costs one addition, one comparison and occasionally one subtraction, and the division that would have produced 0.428571… never happens: it was replaced by the decision to count in sevenths. That trick is much older than graphics and turns up wherever a rate must be tracked without drift, from audio resampling to scheduling. Multiply the units up until the fractions disappear, then stay in whole numbers.

Which invites the obvious objection. Computers have floating-point hardware, and a loop that adds 3/7 to a float64 is shorter than the one above. Write that version, point it at two lines, and look at what comes out.

▣ Build · stage 3: the same idea in floating point
// cmd/worldc/main.go — add the slope, cut the result down to a pixel

func lineFloat(b *render.Buffer, x0, y0, x1, y1 int, c render.Color) {
	slope := float64(y1-y0) / float64(x1-x0)
	y := float64(y0)
	for x := x0; x <= x1; x++ {
		b.Set(x, int(y), c)
		y += slope
	}
}

func main() {
	b := render.NewBuffer(24, 12)
	b.Fill(render.Soil)
	lineFloat(b, 2, 2, 9, 5, render.Water)
	lineFloat(b, 2, 8, 12, 9, render.Water)
	fmt.Print(b.Preview())

	slope := 1.0 / 10.0
	y := 8.0
	for x := 2; x < 12; x++ {
		y += slope
	}
	fmt.Printf("asked for   (12,9)\n")
	fmt.Printf("y ends at   %.17f\n", y)
	fmt.Printf("as a pixel  %d\n", int(y))
	fmt.Printf("exact?      %v\n", y == 9.0)
}
$ go run ./cmd/worldc
........................
........................
..~~~...................
.....~~.................
.......~~...............
.........~..............
........................
........................
..~~~~~~~~~~~...........
........................
........................
........................
asked for   (12,9)
y ends at   8.99999999999999645
as a pixel  8
exact?      false

Two failures, one of them fatal. The first stream sags: its runs are three pixels, then two, two, one, where the integer walk laid down an even two, two, two, two. That is int(y) cutting the fraction off, which discards up to a whole pixel of rise every column and always in the same direction, so the line rides low and its final column arrives alone.

The second stream never arrives at all. It was told to run from {2 8} to {12 9}, a slope of exactly one tenth, and the preview shows it flat along row 8 with row 9 untouched. The printed arithmetic says why: one tenth has no exact form in binary, ten additions of it land on 8.99999999999999645 instead of 9, and cutting the fraction off reads that as row 8. The line missed its own endpoint by a pixel. Rounding to nearest would rescue this particular line, at the price of an extra operation per pixel and a patch laid over a method that is approximate by construction. The integer walk needs no rescue: its error is a whole number that provably returns to zero.

lineShallow only knows one direction, though. Real calls will run west, north, and straight up, and eight copies of the loop is not an answer. The trick is to carry one error term for both axes and let each axis decide separately whether this iteration is its turn to step.

▣ Build · stage 4: one loop, every direction
// internal/render/draw.go

// Line draws the pixels nearest the straight line from (x0,y0) to
// (x1,y1), in integers only.
func (b *Buffer) Line(x0, y0, x1, y1 int, c Color) {
	dx, sx := x1-x0, 1
	if dx < 0 {
		dx, sx = -dx, -1
	}
	dy, sy := y1-y0, 1
	if dy < 0 {
		dy, sy = -dy, -1
	}
	dy = -dy
	err := dx + dy
	for {
		b.Pix[b.index(x0, y0)] = c // straight into the slice, like FillRect
		if x0 == x1 && y0 == y1 {
			return
		}
		e2 := 2 * err
		if e2 >= dy {
			err += dy
			x0 += sx
		}
		if e2 <= dx {
			err += dx
			y0 += sy
		}
	}
}
// cmd/worldc/main.go — the hand-walked line, then a fan of twelve
	b.Line(2, 2, 9, 5, render.Water)
	fmt.Print(b.Preview())

	f := render.NewBuffer(24, 12)
	f.Fill(render.Soil)
	cx, cy := 11, 5
	for _, d := range [][2]int{{9, 0}, {9, 4}, {4, 5}, {0, 5}, {-4, 5}, {-9, 4},
		{-9, 0}, {-9, -4}, {-4, -5}, {0, -5}, {4, -5}, {9, -4}} {
		f.Line(cx, cy, cx+d[0], cy+d[1], render.Water)
	}
	fmt.Print(f.Preview())
$ go run ./cmd/worldc
........................
........................
..~~....................
....~~..................
......~~................
........~~..............
........................
........................
........................
........................
........................
........................
.......~...~...~........
..~~....~..~..~....~~...
....~~...~.~.~...~~.....
......~~.~.~.~.~~.......
........~~~~~~~.........
..~~~~~~~~~~~~~~~~~~~...
........~~~~~~~.........
......~~.~.~.~.~~.......
....~~...~.~.~...~~.....
..~~....~..~..~....~~...
.......~...~...~........
........................

Check the first preview first: those are the same eight pixels the pencil chose and lineShallow drew, produced by a loop that never heard of octants. The fan is the general case, twelve spokes shallow and steep across all four quadrants, every one of them ending on the pixel it was aimed at.

The signs come off at the top: dx and dy are made positive with their directions kept in sx and sy, so the arithmetic is about magnitudes and the direction of travel is a separate fact. Negating dy afterwards is what lets one error term serve both axes. err carries the x-error and the y-error against each other, and e2, its double, plays the part 2*e played in the interlude, asking each axis in turn whether the true line has moved far enough to justify a step. On a shallow line the x test passes every iteration and the y test occasionally; on a steep line it is the other way round; at 45 degrees both pass every time and the walk goes diagonally. The loop stops on the coordinate test and not on a counter, so both endpoints are drawn exactly.

This method is Bresenham's line algorithm, worked out at IBM in 1962 to drive a plotter. It survived into every framebuffer renderer since for the reasons that fan demonstrates. Integers only, a couple of adds and compares per pixel, endpoints exact, and the same answer everywhere.

The {-1 6} write

Look again at the write in the middle of that loop. FillRect earned the right to skip Set by clipping its rectangle first, and the line above copied the style without the earning: it indexes Pix directly, and every pixel in that fan happened to be inside the buffer. Point a line at a coordinate that is not.

⚠ Worked failure: a stream drawn off the west edge
// cmd/worldc/main.go
	b := render.NewBuffer(24, 12)
	b.Fill(render.Soil)

	// the stream, running west off the edge of the buffer
	b.Line(20, 4, -6, 7, render.Water)
	fmt.Print(b.Preview())

	// the same mistake, made on row 0
	b.Line(3, 0, -5, 0, render.Water)
	fmt.Println("still drawing")
$ go run ./cmd/worldc
........................
........................
........................
........................
................~~~~~...
........~~~~~~~~.......~
~~~~~~~~..........~~~~~.
........................
........................
........................
........................
........................
panic: runtime error: index out of range [-1]

goroutine 1 [running]:
theworld/internal/render.(*Buffer).Line(...)
	/home/you/theworld/internal/render/draw.go:80
main.main()
	/home/you/theworld/cmd/worldc/main.go:18 +0x150
exit status 2

The stream is drawn correctly for as long as it is on the map, down and west from {20 4} to column 0. Then look at the two clusters with no business existing: one lone pixel at the very end of row 5, past a gap of dry soil, and five more at columns 18 through 22 of row 6, east of a line that was travelling west.

Do the arithmetic on the first stray. The walk wanted a pixel at {-1 6}, so index computed 6·24 + (−1) = 143. That is a legal slot in a 288-pixel slice, and 143 is also 5·24 + 23, which is the pixel at {23 5}: the far east end of the row above. The other five wanted columns −2 through −6 on row 7 and landed on slots 166 down to 162, which is columns 22 through 18 of row 6. A line that walked off the west edge came back on the east edge one row up, the same wraparound chapter 6's walker suffered when it asked for {-1 5} and was handed the terrain at {11 4}, for the same reason. The flattening formula is arithmetic, not geography. It has no idea that x = −1 means "outside"; it just returns a smaller number.

Then the second call kills the process. On row 0 there is no row above to wrap into, so {-1 0} computes 0·24 + (−1) = −1, the slice bounds check fires, and the whole client dies with its picture half drawn. Two faces of one bug, and the loud one is the lucky one: everywhere except row 0 this mistake corrupts the image quietly, in a place on screen unrelated to where the drawing went wrong. Note what did not save it. Chapter 12's per-pixel guard is still sitting in Set, working perfectly for every caller that uses it, and Line walked around it.

A negative column lands on the row above A request to write the pixel at x equals minus one, y equals six, shown as a box, with a dashed arrow leading down into a strip of eight slice slots numbered 140 to 147. A divider between slot 143 and slot 144 marks where row 5 ends and row 6 begins. Each slot is labelled with the pixel it really holds, and the highlighted slot 143 is the pixel at column 23 of row 5. WHAT THE WRITE MEANT x = −1, y = 6 one pixel off the west edge THE SLICE 140 141 142 143 144 145 146 147 (20,5) (21,5) (22,5) (23,5) (0,6) (1,6) (2,6) (3,6) row 5 ends row 6 begins y·24 + x = 6·24 + (−1) = 143 the write is legal, lands on the row above, and nothing objects

Figure 13.1: the slice has no edges, only slots: a column of −1 is a subtraction, and the write lands wherever that subtraction points.

The repair is one word. A line discovers its coordinates one at a time, so it has no larger unit to clip, and the test goes where the pixels are.

▣ Build · stage 5: the guarded write, and the same two calls
// internal/render/draw.go — Line's loop, one line different
	for {
		b.Set(x0, y0, c)
		if x0 == x1 && y0 == y1 {
			return
		}
$ go run ./cmd/worldc
........................
........................
........................
........................
................~~~~~...
........~~~~~~~~........
~~~~~~~~................
........................
........................
........................
........................
........................
still drawing

Same two calls, same coordinates, and the stream now stops dead at column 0 with the east side of rows 5 and 6 untouched. The row 0 line draws its four on-map pixels and returns, and the client is still running to say so. The fix does not shorten the walk or leave the loop early. The error term keeps stepping through coordinates nobody will ever see, out to {-6 7}, and Set throws each one away. That matters for correctness, because the pixels a clipped line draws must be the pixels it would have drawn unclipped. A line that changed its staircase depending on where the window edge fell would shift by a pixel every time the camera moved.

◆ Note: the faster clip, and why it is not here

Four comparisons per pixel is a real cost, and there is a known way to avoid most of it: compute where the line crosses the clip rectangle, move the endpoints onto those crossings, and run an unguarded loop between them. The catch is that moving an endpoint moves the error term with it, and an adjustment that is slightly wrong shifts the whole staircase by a pixel, in a bug that appears only when something is half off screen. At the sizes this volume draws, the guarded write is fast enough and provably identical to the unclipped line, so it comes first.

One thing is still untested. The clip rectangle has been the whole buffer this entire chapter, and its reason for existing is being something smaller.

▣ Build · stage 6: a window in the middle of the buffer
// cmd/worldc/main.go — draw far too much, inside a small window

// border draws the four sides of a rectangle, one pixel wide.
func border(b *render.Buffer, r render.Rect, c render.Color) {
	b.Line(r.X0, r.Y0, r.X1-1, r.Y0, c)
	b.Line(r.X0, r.Y1-1, r.X1-1, r.Y1-1, c)
	b.Line(r.X0, r.Y0, r.X0, r.Y1-1, c)
	b.Line(r.X1-1, r.Y0, r.X1-1, r.Y1-1, c)
}

func main() {
	b := render.NewBuffer(24, 12)
	b.Fill(render.Soil)

	view := render.Rect{X0: 6, Y0: 3, X1: 18, Y1: 10}
	full := b.Clip()

	b.SetClip(view)
	b.FillRect(render.Rect{X0: 2, Y0: 6, X1: 22, Y1: 8}, render.Water)
	b.Line(-4, 1, 27, 12, render.Rock)
	b.SetClip(full)

	border(b, view, render.Rock)
	fmt.Print(b.Preview())

	wide := render.Rect{X0: -100, Y0: -100, X1: 900, Y1: 900}
	b.SetClip(wide)
	fmt.Println("asked to clip to", wide)
	fmt.Println("clip is        ", b.Clip())
}
$ go run ./cmd/worldc
........................
........................
........................
......############......
......#..........#......
......###........#......
......#~~###~~~~~#......
......#~~~~~###~~#......
......#........###......
......############......
........................
........................
asked to clip to {-100 -100 900 900}
clip is         {0 0 24 12}

The pond was asked for at columns 2 through 21 and appears only between 7 and 16. The rock line was told to run from {-4 1}, off the buffer entirely, to {27 12}, off the other side, and only its middle survives, entering and leaving the window on the pixels it would have used anyway: clipping removed pixels without moving any. The border exists because it is drawn afterwards with the clip restored, four calls to Line, each using X1-1 because half-open means the last pixel is one short of the edge number.

The last two printed lines are what makes this a wall and not a suggestion. Asked to open the clip to a thousand pixels in every direction, the buffer reports {0 0 24 12}. Because SetClip intersects with Bounds(), the writable area can be cut down by any caller and widened by none, and that guarantee holds no matter what a camera or a panel layout computes.

Pix is written in two places

The arrangement is the one chapter 6 settled on for the simulation, moved down a layer. There, every route into the world's state ran through a handful of methods that stopped trusting their callers, so other systems could not corrupt the grid however confused its arithmetic became. The framebuffer has that property now for the same reason: Pix is written by two functions, and both consult clip before anything else happens. Sprites, tilemaps and text will draw through those two, so none of them has to remember the check and none can forget it. Callers multiply. Writers do not.

What changed between the layers is what the check may cost, and that is where the rule splits in two. FillRect knows its whole job before it starts, so it tests the description of the work: four comparisons decide the fate of thousands of pixels. Line cannot, because its coordinates are produced as it goes, so the test moves inside the loop and is paid per pixel. The principle under both: clip the largest unit you can name in advance, and pay per item only for work whose extent you learn while doing it. Every renderer here sorts into one of those two cases.

The integer arithmetic settles a different debt. Volume 1 proved a run correct by replaying it and diffing the log, and that discipline is only available to code whose output is decided instead of measured. A staircase computed from accumulated floating-point error is decided by rounding, and stage 3 showed the endpoint that method can lose. Bresenham's walk is a sequence of integer additions: the same call draws the same pixels, in any run, on any machine, which is the precondition for a picture two runs can be compared by. Exact endpoints pay off more plainly as well. Tiles and panels are drawn edge to edge, and a renderer that missed an endpoint now and then would leave one-pixel seams between them, obvious on screen and unexplainable to anyone who did not know the line code was approximate.

Checkpoint

✓ Checkpoint: what you can now do
  • Intersect two half-open rectangles by hand and say why {-4 7 4 15} becomes {0 7 4 12} against a 24 by 12 buffer, and why {30 2 40 4} comes back empty.
  • Run the error walk for a line on paper, explain what 2*e >= dx asks about the true line, and argue why the walk cannot finish anywhere except on its endpoint.
  • Explain why the accumulated-slope stream ended on row 8 when it was told to end on row 9, and read 8.99999999999999645 as the cause.
  • Take a stray pixel out of the wrapped preview, compute the slot it wrote to, and name the pixel that slot really belongs to.
  • Say where the clip test goes for a rectangle, where it goes for a line, and why the two answers differ.
  • Narrow the clip to a window, draw something twice its size through it, and predict every pixel that survives.
⚡ Exercises: try first, then reveal
Exercise 1: rectangles that hold nothing. Call FillRect four times on a fresh buffer, with {3 2 4 6}, {3 2 3 6}, {9 2 3 6} and {30 2 40 4}. Predict how many pixels each one paints, then count them.

Four, zero, zero, zero. {3 2 4 6} is one column wide, since half-open makes the width 4 − 3, and it paints rows 2 through 5. {3 2 3 6} has zero width and Empty catches it. The backwards one is the case to sit with: Intersect leaves X1 below X0, the clamp lifts X1 up to 9, and {9 2 9 6} is empty. Without that clamp the row slice would be asked for a negative length and the client would die, so those four lines turn a swapped pair of arguments into nothing drawn.

Exercise 2: draw it backwards. For every pair of points on a 24 by 12 buffer, draw the line one way, draw it the other way into a second buffer, and compare the two previews. How many of the 82,944 pairs disagree?

26,560 of them, close to a third. The cause is the tie: when e2 lands exactly on the boundary, two pixels are equally close to the true line, and the loop resolves that in favour of the direction it is walking, so starting from the other end resolves it the other way. The smallest example is {0 0} to {1 2}, where one direction bends late and the other early. Neither is wrong, and the property matters: if two parts of a renderer draw the same wall from opposite ends, they will disagree about its pixels. The usual answer is to order the endpoints first, always starting from the smaller x, so one geometric line has exactly one drawing.

Exercise 3: count the corners. Write border from stage 6 yourself and draw the outline of {6 3 18 10}. Before running it, work out how many pixels it should paint, then count them in Pix.

34, and the arithmetic is 2·(12 + 7) − 4 for a 12 by 7 rectangle: two rows plus two columns, minus the four corners each counted twice. Counting that colour in Pix gives exactly 34, because drawing a pixel twice paints it once, and the agreement between prediction and count is the exercise. The formula survives a rectangle one pixel wide, where 2·(1 + 7) − 4 = 12 describes a single column drawn twice with its two ends shared.