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

The Pixel and the Number

A screen is a grid

The world you finished the last volume with is running, but every fact reached you as text. The first drawing rule is the same rule that made the terrain grid work: a screen is a grid whose cells are colors, held in one flat slice and indexed by the same y·W + x arithmetic the ground already uses.

You printed a map of glyphs, read a few heartbeat lines, diffed two files and got nothing back, then concluded there was a world in there. The conclusion was right. It was also secondhand.

worldc starts with somewhere to put pixels. A renderer decides what color each pixel should be and writes the color down; a window can only carry those numbers to the glass.

Before a line, a sprite, or a tile can exist, there has to be a place where a color at a coordinate can be stored and read back. That place is a framebuffer.

By the end of the chapter internal/render exists, cmd/worldc runs, and you can put a color at a coordinate and prove from the numbers that it landed there. The harder question is what a color is when a computer holds one.

One number, four bytes

A pixel on a modern screen is lit by three lamps, red, green and blue, each of which can be off, full, or any of 254 steps between. Three numbers from 0 to 255 therefore name any color the display can make. A fourth number rides along with them: alpha, how opaque the color is, 0 for invisible and 255 for solid. That number does nothing to a lamp. It says what should happen when this color is drawn on top of another one, and it will matter a great deal once anything in this book is drawn on top of anything else.

Four numbers, each fitting in one byte, is thirty-two bits of information, and a uint32 is thirty-two bits. So a color is one number. Take a concrete one: 0xFF4A90D2. Hexadecimal writes each byte as exactly two digits, so those eight digits pair off into the four bytes without any ambiguity about where one ends and the next begins. Convert them the slow way, remembering that a hex digit runs 0 to 9 then A to F for 10 to 15, and that the left digit of a pair counts sixteens:

FF is 15·16 + 15 = 255, so alpha is 255 and the color is fully opaque. 4A is 4·16 + 10 = 74: red, and not much of it. 90 is 9·16 + 0 = 144, a middling green. D2 is 13·16 + 2 = 210, blue, the loudest of the three. Much more blue than green and little red: a soft, slightly grey blue, water seen from above on an overcast day. That is the entire content of 0xFF4A90D2, and the first program of this volume does the same four conversions in code so you can check your arithmetic against a machine's.

▣ Build · stage 1: a color taken apart and put back together
// cmd/worldc/main.go — a throwaway main; the client's real one starts in stage 2
package main

import "fmt"

// show prints one packed color and the four bytes inside it.
func show(name string, c uint32) {
	a := (c >> 24) & 0xFF
	r := (c >> 16) & 0xFF
	g := (c >> 8) & 0xFF
	b := c & 0xFF
	fmt.Printf("%-14s 0x%08X = %10d   alpha %3d  red %3d  green %3d  blue %3d\n",
		name, c, c, a, r, g, b)
}

func main() {
	show("valley water", 0xFF4A90D2)
	show("no alpha byte", 0x004A90D2)
	show("pure red", 0xFFFF0000)

	// Take the water apart and put it back together.
	const water uint32 = 0xFF4A90D2
	a, r, g, b := (water>>24)&0xFF, (water>>16)&0xFF, (water>>8)&0xFF, water&0xFF
	packed := a<<24 | r<<16 | g<<8 | b
	fmt.Printf("repacked:      0x%08X   same number: %v\n", packed, packed == water)
}
$ go run ./cmd/worldc
valley water   0xFF4A90D2 = 4283076818   alpha 255  red  74  green 144  blue 210
no alpha byte  0x004A90D2 =    4886738   alpha   0  red  74  green 144  blue 210
pure red       0xFFFF0000 = 4294901760   alpha 255  red 255  green   0  blue   0
repacked:      0xFF4A90D2   same number: true

Two operators are doing all the work. c >> 16 shifts the whole number sixteen bits to the right, which slides the red byte down into the bottom eight bits and pushes green, blue and everything below them off the end. What is left above red is alpha, still sitting there, so & 0xFF follows: a bitwise AND against eight one-bits keeps the bottom eight bits and zeroes every bit above them. Shift to bring the byte you want to the bottom, mask to discard the rest. Packing runs the same two ideas backwards: shift each byte up to its own position and OR them together, since no two of them have a bit in common.

The second line is a trap you will meet in real work. 0x004A90D2 holds the same red, green and blue as the water, and it is a different color: alpha 0, a color that is not there. Six-digit hex is the habit the web trained into everyone, and dropping into it here silently writes a fully transparent pixel that no amount of squinting at the red, green and blue values will explain. Eight digits, always, and the first pair is not optional. The third line is the sanity check every palette deserves: 0xFFFF0000 reads as opaque, red at maximum, green and blue at zero, and it is the reddest red the display can produce.

∑ Math Interlude: a color is a four-digit number in base 256

The decimal number 4,821 is 4·1000 + 8·100 + 2·10 + 1, and to pull out its hundreds digit you divide by 100 and take the remainder after dividing by 10: 4821 ÷ 100 = 48, and 48 mod 10 = 8. A packed color is the same trick with 256 in place of 10. Its four digits are alpha, red, green and blue, and the place values are 256³ = 16,777,216, then 65,536, then 256, then 1:

c = a·16777216 + r·65536 + g·256 + b

Check it against the run above. 255·16,777,216 = 4,278,190,080. 74·65,536 = 4,849,664. 144·256 = 36,864. Add 210 and the total is 4,283,076,818, the decimal the program printed for the water. Extraction is the hundreds-digit move: r = (c ÷ 65536) mod 256. And because 65,536 is 2 to the 16th and 256 is 2 to the 8th, both operations have a one-instruction form on the bits themselves: dividing by 65,536 is >> 16, and taking the remainder after dividing by 256 is & 0xFF. The arithmetic and the bit-twiddling are the same thing said two ways, so if the shifts ever confuse you, fall back to the division and you will still get the right byte.

cthe packed color: one 32-bit number holding four bytes
a, r, g, balpha, red, green, blue, each 0 to 255
0x…a number written in hexadecimal, base 16: two digits per byte
>> nshift right by n bits: divide by 2ⁿ and drop the remainder
& 0xFFkeep the low eight bits, zero the rest: the remainder after dividing by 256
|bitwise OR: merges numbers whose one-bits do not overlap

The 2·24 + 3 slot

Now the second question, and it answers itself. A screen is W pixels across and H pixels down, every pixel holds one color, and a slice is one-dimensional. That is the exact problem the terrain grid solved in chapter 3 with one line of arithmetic: skip y full rows, then walk x slots into the current one, so the cell at column x, row y lives at slot y·W + x. Nothing about that reasoning mentioned terrain. It works for a rectangle of anything, and here the anything is colors.

The new code lives in internal/render, a package that imports nothing from internal/sim and never will. The separation is deliberate: a sim.Coord is a place in the world, a pixel coordinate is a place on the screen, and the two are related by a transform that neither package should assume. So render speaks in plain int pairs and knows nothing about ponds.

▣ Build · stage 2: the color type and the buffer
// internal/render/color.go
// Package render owns pixels. Nothing in here knows what a world is,
// what a tick is, or that a package called sim exists.
package render

// Color is one pixel: alpha, red, green and blue, eight bits each,
// packed into one 32-bit number with alpha in the highest byte.
type Color uint32

// RGBA packs four components into one Color.
func RGBA(a, r, g, b uint8) Color {
	return Color(a)<<24 | Color(r)<<16 | Color(g)<<8 | Color(b)
}

// A, R, G and B are the four bytes back out again. Converting to
// uint8 keeps the low eight bits, which is the & 0xFF written shorter.
func (c Color) A() uint8 { return uint8(c >> 24) }
func (c Color) R() uint8 { return uint8(c >> 16) }
func (c Color) G() uint8 { return uint8(c >> 8) }
func (c Color) B() uint8 { return uint8(c) }

// The valley's first three colors, one per kind of ground, plus the
// fully transparent zero value a new buffer is full of.
const (
	Clear Color = 0x00000000
	Rock  Color = 0xFF5A5F63
	Soil  Color = 0xFF6B5A3E
	Water Color = 0xFF4A90D2
)
// internal/render/buffer.go
package render

// Buffer is a rectangle of pixels: W by H colors held in one flat
// slice, row after row, addressed with the same arithmetic the
// terrain grid uses on its cells.
type Buffer struct {
	W, H int
	Pix  []Color
}

// NewBuffer allocates a w-by-h buffer in a single allocation. Every
// pixel starts at the zero value, Clear.
func NewBuffer(w, h int) *Buffer {
	return &Buffer{W: w, H: h, Pix: make([]Color, w*h)}
}

// index turns a pixel coordinate into a position in the flat slice.
func (b *Buffer) index(x, y int) int { return y*b.W + x }

// In reports whether a pixel coordinate is on the buffer at all.
func (b *Buffer) In(x, y int) bool {
	return x >= 0 && x < b.W && y >= 0 && y < b.H
}

// Set writes one pixel. A coordinate off the buffer is dropped: a
// drawing operation that runs off an edge is ordinary, not an error.
func (b *Buffer) Set(x, y int, c Color) {
	if !b.In(x, y) {
		return
	}
	b.Pix[b.index(x, y)] = c
}

// At returns the color at a pixel coordinate, or Clear if the
// coordinate names no pixel.
func (b *Buffer) At(x, y int) Color {
	if !b.In(x, y) {
		return Clear
	}
	return b.Pix[b.index(x, y)]
}

// Fill paints every pixel one color, walking the flat slice straight
// through without computing a single coordinate.
func (b *Buffer) Fill(c Color) {
	for i := range b.Pix {
		b.Pix[i] = c
	}
}
// cmd/worldc/main.go — the client's real main, one buffer deep
package main

import (
	"fmt"

	"theworld/internal/render"
)

const version = "0.0.1"

func main() {
	fmt.Println("worldc", version)

	b := render.NewBuffer(24, 12)
	fmt.Printf("buffer %dx%d: %d pixels, %d bytes of pixel data\n",
		b.W, b.H, len(b.Pix), len(b.Pix)*4)
	fmt.Printf("a fresh pixel at (3,2): 0x%08X\n", uint32(b.At(3, 2)))

	b.Fill(render.Soil)
	b.Set(3, 2, render.Water)
	b.Set(30, 2, render.Rock) // off the right edge: dropped

	fmt.Printf("slot for (3,2) is 2*24 + 3 = %d\n", 2*b.W+3)
	fmt.Printf("Pix[%d] = 0x%08X, and At(3,2) = 0x%08X\n",
		2*b.W+3, uint32(b.Pix[2*b.W+3]), uint32(b.At(3, 2)))
	fmt.Print("row 2, slots 51 through 58:")
	for x := 3; x < 11; x++ {
		fmt.Printf(" %08X", uint32(b.Pix[2*b.W+x]))
	}
	fmt.Println()
	fmt.Printf("after the off-edge write: %d pixels, At(30,2) = 0x%08X\n",
		len(b.Pix), uint32(b.At(30, 2)))
}
$ go run ./cmd/worldc
worldc 0.0.1
buffer 24x12: 288 pixels, 1152 bytes of pixel data
a fresh pixel at (3,2): 0x00000000
slot for (3,2) is 2*24 + 3 = 51
Pix[51] = 0xFF4A90D2, and At(3,2) = 0xFF4A90D2
row 2, slots 51 through 58: FF4A90D2 FF6B5A3E FF6B5A3E FF6B5A3E FF6B5A3E FF6B5A3E FF6B5A3E FF6B5A3E
after the off-edge write: 288 pixels, At(30,2) = 0x00000000

Read the fourth and fifth lines together, because they are the whole claim of the chapter. The program computed the slot by hand, 2·24 + 3 = 51, reached into the flat slice at that number, and found the water it had asked Set to put at column 3, row 2. Then At was asked the same question in coordinates and answered with the same eight hex digits. Two ways of naming one location, agreeing. The row of slots after it shows the neighbours: one water pixel, then soil running east, laid out end to end in memory exactly as they are laid out on the row.

A fresh buffer prints 0x00000000, and that number is not black. Alpha 0 makes it a pixel with no color at all, the same zero-value trick the terrain grid used when a new world came out solid rock: make hands back zeroed memory, and the type decides what zero means. Here it means nothing has been drawn yet.

◆ Note: why Set does not return an error

The grid was taught to refuse a bad coordinate by returning ErrOutOfBounds, and the buffer just dropped a write on the floor. The difference is what the caller is doing. A write to the ground is a world event, and one aimed at a cell that does not exist is a bug in a law that somebody has to fix. A drawing operation off the edge of the screen is Tuesday: half a sprite hanging past the border, a line whose endpoint is outside the window. Nothing is wrong and nobody needs telling, and a per-pixel error value on a buffer this program will eventually fill two million pixels at a time would cost more than the drawing. The bounds check does not disappear, though. It moves up into the operations that draw, where the question can be asked once for a whole rectangle instead of once per pixel.

A hex dump proves the arithmetic and shows nothing. The buffer needs a way to be looked at, and no window exists yet, so render gets a debugging aid built on a trick the ground already used: one character per pixel.

▣ Build · stage 3: a picture in a terminal, painted a pixel at a time
// internal/render/buffer.go — a debug view, four colors wide
import "strings"

// glyphs pairs each palette color with a one-byte stand-in.
var glyphs = []struct {
	c Color
	g byte
}{{Clear, ' '}, {Rock, '#'}, {Soil, '.'}, {Water, '~'}}

// Preview draws the buffer as one character per pixel, so a terminal
// can show what is in it before anything can display it properly. A
// color outside the palette prints as ?.
func (b *Buffer) Preview() string {
	var s strings.Builder
	for y := 0; y < b.H; y++ {
		for x := 0; x < b.W; x++ {
			c := b.Pix[b.index(x, y)]
			ch := byte('?')
			for _, k := range glyphs {
				if k.c == c {
					ch = k.g
				}
			}
			s.WriteByte(ch)
		}
		s.WriteByte('\n')
	}
	return s.String()
}
// cmd/worldc/main.go — the body of main paints something
	b := render.NewBuffer(24, 12)
	b.Fill(render.Soil)

	// The rim: the top and bottom rows, then the left and right columns.
	for x := 0; x < b.W; x++ {
		b.Set(x, 0, render.Rock)
		b.Set(x, b.H-1, render.Rock)
	}
	for y := 0; y < b.H; y++ {
		b.Set(0, y, render.Rock)
		b.Set(b.W-1, y, render.Rock)
	}
	// The pond: every pixel inside one rectangle.
	for y := 4; y <= 7; y++ {
		for x := 3; x <= 14; x++ {
			b.Set(x, y, render.Water)
		}
	}
	// The spring: exactly one pixel, at column 19, row 2.
	b.Set(19, 2, render.Water)

	fmt.Print(b.Preview())

	n := 0
	for _, c := range b.Pix {
		if c == render.Water {
			n++
		}
	}
	fmt.Println("water pixels in the slice:", n)
$ go run ./cmd/worldc
worldc 0.0.1
########################
#......................#
#..................~...#
#......................#
#..~~~~~~~~~~~~........#
#..~~~~~~~~~~~~........#
#..~~~~~~~~~~~~........#
#..~~~~~~~~~~~~........#
#......................#
#......................#
#......................#
########################
water pixels in the slice: 49

Four loops and a single write, and the rectangle came out where the arithmetic said it would: twelve columns of pond spanning rows 4 through 7, one lone spring pixel high on the right at column 19, row 2. Count the water: forty-eight pixels of pond plus one, and the count came from a single pass over the flat slice with no coordinates involved at all, the same loop Fill uses. That is the flat layout paying rent again. Ask a spatial question and you compute an index; ask "how many of these are there" and you walk the run.

Everything about this picture is hand-placed, exactly like the first pond you tiled by hand before a seed grew one. The terrain grid is right there in the same module and the buffer has never heard of it. Connecting them takes a tileset and a mapping from terrain to tile. What matters today is that a color put at a coordinate is at that coordinate.

One index function serving two different grids Two stacked rows of six slots. The upper row is the terrain grid's flat slice, slots 24 through 29, holding the words soil and water, with slot 27 highlighted. Between the rows sits a box holding the formula i equals y times W plus x. The lower row is the framebuffer's flat slice, slots 48 through 53, holding packed hex colors, with slot 51 highlighted. Both rows are addressed by the same formula. THE GROUND: sim.Grid, W = 12 soil soil soil water water soil 24 25 26 27 28 29 x = 3, y = 2 lands in slot 2·12 + 3 = 27 i = y·W + x one index function, two grids THE SCREEN: render.Buffer, W = 24 FF6B5A3E FF6B5A3E FF6B5A3E FF4A90D2 FF6B5A3E FF6B5A3E 48 49 50 51 52 53 x = 3, y = 2 lands in slot 2·24 + 3 = 51

Figure 12.1: the same arithmetic over two runs of memory. Only the width and what a slot holds are different.

The +48 carry

Flat blue is a poor pond. Shallow water near a bank reads paler than deep water, and the cheapest way to say that is to take the water color and brighten it by a fixed amount for the shallow rows. Brightening means adding the same number to red, green and blue, leaving alpha alone. There is an obvious one-liner for it, and the obvious one-liner is wrong in a way that teaches the packing better than any correct code would.

⚠ Worked failure: the pond that turned green
// internal/render/color.go — brighten every channel at once

// Lighten brightens a color by adding the same amount to red, green
// and blue.
func Lighten(c Color, n uint8) Color {
	step := Color(n)<<16 | Color(n)<<8 | Color(n)
	return c + step
}
// cmd/worldc/main.go — brighten the water and look at the bytes
func show(name string, c render.Color) {
	fmt.Printf("%-10s 0x%08X   alpha %3d  red %3d  green %3d  blue %3d\n",
		name, uint32(c), c.A(), c.R(), c.G(), c.B())
}

func main() {
	show("water", render.Water)
	show("+48", render.Lighten(render.Water, 48))
}
$ go run ./cmd/worldc
water      0xFF4A90D2   alpha 255  red  74  green 144  blue 210
+48        0xFF7AC102   alpha 255  red 122  green 193  blue   2

Red did what it was told: 74 + 48 = 122. Everything else went strange. Blue was 210, the highest of the three, and brightening it produced 2, very nearly nothing. Green was asked for 48 and got 49.

Follow the one channel that misbehaved most. 210 + 48 = 258, and 258 does not fit in a byte, whose largest value is 255. In binary 258 is nine bits: 1 0000 0010. The low eight bits are 2, and the ninth bit is a 1 sitting one place above the top of the blue byte. The place one bit above the top of blue is the bottom of green, so green received a carry of 1 on top of its own 48, and 144 + 48 + 1 = 193. That accounts for every number on the line.

Name the mistake exactly, because the name is the fix. The four channels are separate only as an agreement between the code that packs them and the code that unpacks them. The processor was handed two 32-bit integers and added them as 32-bit integers; it has never heard of a byte boundary and would have carried straight into alpha just as happily. Any arithmetic that can push a channel past 255 has to be done on channels one at a time, in numbers wide enough to hold the overflow, with the decision about what to do when a channel maxes out made on purpose instead of by carry.

▣ Build · stage 4: separate, add, clamp, repack
// internal/render/color.go — the fix

// Lighten adds n to red, green and blue, each channel stopping at 255
// instead of running into its neighbour. Alpha is left alone.
func Lighten(c Color, n uint8) Color {
	return RGBA(c.A(), addTo255(c.R(), n), addTo255(c.G(), n), addTo255(c.B(), n))
}

// addTo255 adds two bytes in arithmetic wide enough to hold the answer,
// then clamps.
func addTo255(v, n uint8) uint8 {
	sum := int(v) + int(n)
	if sum > 255 {
		return 255
	}
	return uint8(sum)
}
// cmd/worldc/main.go — three depths of one water color
	show("water", render.Water)
	show("+24", render.Lighten(render.Water, 24))
	show("+48", render.Lighten(render.Water, 48))
	show("+72", render.Lighten(render.Water, 72))
$ go run ./cmd/worldc
water      0xFF4A90D2   alpha 255  red  74  green 144  blue 210
+24        0xFF62A8EA   alpha 255  red  98  green 168  blue 234
+48        0xFF7AC0FF   alpha 255  red 122  green 192  blue 255
+72        0xFF92D8FF   alpha 255  red 146  green 216  blue 255

Three steadily paler blues, and the interesting line is the middle one. Blue lands on 255 at +48, the same addition that wrapped to 2 before, because int(210) + int(48) is computed in a 64-bit register where 258 fits comfortably and only then gets a decision applied to it. Green comes out 192, not 193; the phantom carry is gone with the mechanism that produced it. At +72 blue is stuck at 255 while red and green keep climbing, so the color drifts toward white instead of overshooting into nonsense. That drift is a real property of clamping and something to watch: brighten hard enough and every color converges on white, so a palette built by lightening one base color runs out of distinct steps at the top.

The PPM file

The preview knows four colors. The pond now has four blues in it, three of them made by arithmetic, and the terminal will print ? for every pixel it has no word for. Hit that limit on purpose, because it forces the question of how a buffer gets out of the program at all. The answer is a file format, and there is one simple enough to write in twenty lines: PPM, plain text, a header naming the size and the brightest possible value, then the pixels as decimal numbers.

▣ Build · stage 5: the buffer leaves the program
// internal/render/ppm.go
package render

import (
	"bufio"
	"fmt"
	"io"
)

// WritePPM writes a buffer as a plain PPM image: three header lines,
// then one line per row holding three decimal numbers per pixel. PPM
// has no alpha channel, so the alpha byte is dropped on the way out.
func WritePPM(w io.Writer, b *Buffer) error {
	out := bufio.NewWriter(w)
	fmt.Fprintf(out, "P3\n%d %d\n255\n", b.W, b.H)
	for y := 0; y < b.H; y++ {
		for x := 0; x < b.W; x++ {
			c := b.Pix[b.index(x, y)]
			if x > 0 {
				out.WriteByte(' ')
			}
			fmt.Fprintf(out, "%d %d %d", c.R(), c.G(), c.B())
		}
		out.WriteByte('\n')
	}
	return out.Flush()
}
// cmd/worldc/main.go — stage 3's loops lifted into paint, plus depth and a file
package main

import (
	"flag"
	"fmt"
	"os"

	"theworld/internal/render"
)

const version = "0.0.1"

// paint fills a buffer with a rim, soil, and a pond that pales toward
// its northern edge.
func paint(b *render.Buffer) {
	b.Fill(render.Soil)
	for x := 0; x < b.W; x++ {
		b.Set(x, 0, render.Rock)
		b.Set(x, b.H-1, render.Rock)
	}
	for y := 0; y < b.H; y++ {
		b.Set(0, y, render.Rock)
		b.Set(b.W-1, y, render.Rock)
	}
	// Four rows of pond, the shallowest lightened most.
	shallow := []uint8{72, 48, 24, 0}
	for i, n := range shallow {
		c := render.Lighten(render.Water, n)
		for x := 3; x <= 14; x++ {
			b.Set(x, 4+i, c)
		}
	}
	b.Set(19, 2, render.Water)
}

func main() {
	path := flag.String("ppm", "valley.ppm", "where to write the picture")
	flag.Parse()

	fmt.Println("worldc", version)

	b := render.NewBuffer(24, 12)
	paint(b)
	fmt.Print(b.Preview())

	// A four-by-three buffer, small enough to read the file format off
	// the screen.
	demo := render.NewBuffer(4, 3)
	demo.Fill(render.Soil)
	demo.Set(1, 1, render.Water)
	demo.Set(2, 1, render.Rock)
	if err := render.WritePPM(os.Stdout, demo); err != nil {
		fmt.Fprintln(os.Stderr, "worldc:", err)
		os.Exit(1)
	}

	f, err := os.Create(*path)
	if err != nil {
		fmt.Fprintln(os.Stderr, "worldc:", err)
		os.Exit(1)
	}
	defer f.Close()
	if err := render.WritePPM(f, b); err != nil {
		fmt.Fprintln(os.Stderr, "worldc:", err)
		os.Exit(1)
	}
	info, err := f.Stat()
	if err != nil {
		fmt.Fprintln(os.Stderr, "worldc:", err)
		os.Exit(1)
	}
	fmt.Printf("wrote %s: %dx%d, %d bytes\n", *path, b.W, b.H, info.Size())
}
$ go run ./cmd/worldc -ppm /tmp/valley.ppm
worldc 0.0.1
########################
#......................#
#..................~...#
#......................#
#..????????????........#
#..????????????........#
#..????????????........#
#..~~~~~~~~~~~~........#
#......................#
#......................#
#......................#
########################
P3
4 3
255
107 90 62 107 90 62 107 90 62 107 90 62
107 90 62 74 144 210 90 95 99 107 90 62
107 90 62 107 90 62 107 90 62 107 90 62
wrote /tmp/valley.ppm: 24x12, 2898 bytes

The four-by-three image is the format with nothing hidden. P3 says plain-text color pixels, 4 3 is width then height, 255 is the largest value a channel will use, and then twelve pixels arrive in reading order: three rows of soil at 107, 90, 62, with the water and the rock side by side in the middle row. Those are the same decimal bytes stage 1 pulled out of 0xFF4A90D2, and any image viewer on your machine will open the 24-by-12 file and show you the pond, pale rows and all. Alpha does not survive the trip; PPM has nowhere to put it, so the number stays in the buffer and the file carries three channels instead of four.

Step back from the pixels and the mechanism generalizes cleanly. A rectangle of anything is one flat array plus one function from a coordinate to an offset. Change what a slot holds and you get a different structure with identical addressing: terrain kinds make a map, 32-bit colors make a screen, and other things in slots make other rectangles, all of them indexed by the arithmetic in figure 12.1. That is why the ground was built this way in the first place, and why the framebuffer needed no design work today. You are reusing a decision, not making one.

The packing generalizes too, and its lesson is about agreement. Nothing in memory marks a byte as red. 0xFF4A90D2 is a number, and alpha-red-green-blue in that order is a convention this book picked once and now keeps everywhere: in RGBA, in the four readers, in the palette constants, in the PPM writer. Other orders exist and are equally defensible, so the bugs live at the seams where two pieces of code disagree about which byte means what. Blue arriving where you wanted red is almost never a broken calculation; it is two agreements meeting.

The flat layout throws in speed you did not have to ask for. make([]Color, 288) is one allocation of one contiguous block, walking Pix in order walks memory in order, and Fill is a single pass with no multiplication in it. At 288 pixels none of that is measurable. At a window's worth of pixels, sixty times a second, it is the difference between a client that keeps up and one that does not.

Checkpoint

✓ Checkpoint: what you can now do
  • Handed 0xFF4A90D2, name its four bytes in decimal without running anything, and pull any one of them out with a shift and a mask.
  • Explain why 0x004A90D2 is invisible even though its red, green and blue are unchanged.
  • Compute the slot a pixel occupies in Pix from its coordinate, and check the answer against At.
  • Say why the terrain grid returns an error for a coordinate off the map while the buffer quietly drops the write, and defend both.
  • Shown a color that gained green and lost blue after being brightened, look for arithmetic done on the packed number instead of on its channels.
  • Write a buffer out as a PPM and open the result in an image viewer.
⚡ Exercises: try first, then reveal
Exercise 1: a fifth color. Pick a green for shrubs, add it to the palette and to the glyph table, paint a few pixels of it into the buffer, and open the PPM. Then brighten it by 40 and check the three channels moved together.

Any green works; a muted 0xFF4E7A3C sits well against the soil. Add Shrub Color = 0xFF4E7A3C to the constant block and {Shrub, 'v'} to glyphs, and the preview grows a fifth word. Print the parts and you get alpha 255, red 78, green 122, blue 60, and render.RGBA(255, 78, 122, 60) packs back to the identical number, which is the round trip from stage 1 done on a color you chose. Lightened by 40 it becomes 0xFF76A264: 78+40 = 118, 122+40 = 162, 60+40 = 100, every channel up by exactly 40 and nothing borrowed from a neighbour. Open the PPM and the shrubs are there.

Exercise 2: find one pixel in the file. Without opening the image, work out which line of /tmp/valley.ppm and which three numbers on it describe the spring pixel at column 19, row 2. Then check with a one-line command.

The header is three lines, so image row y is file line y+4, putting row 2 on line 6. Pixels run left to right, three numbers each, so column 19 is the twentieth triple: fields 58, 59 and 60. awk 'NR==6 {print $58, $59, $60}' /tmp/valley.ppm prints 74 144 210, the water bytes exactly. The same reasoning finds the shallowest pond row on line 8, where field 10 onward reads 146 216 255: the +72 blue from stage 4, clamped at the top. A file you can navigate with arithmetic is a file you can debug.

Exercise 3: swap the terms. Change index to return x*b.H + y and run the preview. Predict what breaks before you look, then explain the result you actually get.

Nothing breaks. The preview prints exactly the picture it printed before, and that is the lesson. Set, At, Preview and WritePPM all address the slice through the same broken function, so they agree with each other perfectly and the picture is self-consistent. The damage is in memory, and you can see it by reading the slice directly: print slots 0 to 23 as glyphs and the correct build gives ########################, the whole top rim, while the swapped build gives #############..........#, which is column 0 followed by column 1. The buffer is stored in column order and every consumer that walks the flat run in reading order will draw the world on its side. The one that matters is the window itself, which takes the run and never asks a coordinate. Put the terms back.