A picture is not stored as pixels. It is a program of a few hundred ops -- move the pen, draw a line, flood-fill, paint colours -- for RUN_PICTURE, which draws it into the top 128 scanlines of the screen while the game does nothing else. This page follows the first picture a game shows, tunnel like hall (Bag End), op by op through the game's own interpreter, times every op in every picture, and draws the one picture the game changes as it runs, both ways.

The stream

DRAW_LOCATION_PICTURE looks the location up in PICTURE_TABLE and hands the stream to RUN_PICTURE, which walks it with IY and tests each op in this order (read):

OpBytesDoes
header2the border colour, and the attribute the canvas starts as (CLEAR_CANVAS)
$001the end
$08 x y3move the pen
bit 7 set2a line (DRAW_LINE): bits 0-2 its direction (bit 0 the vertical axis leads, bit 1 down, bit 2 left), the second byte's bits 0-5 its length less one, and the minor-axis step -- one step every n pixels -- split between the first byte's bits 2-5 and the second's bits 6-7
bit 6 set3a flood fill (FLOOD_FILL) in the ink of bits 0-2, from x, y; the pen does not move
bit 5 set3+paint attribute cells: bits 0-2 the colour, then an attribute address, high byte first, then path bytes of two bits of direction and six of count, to $FF

x runs 0-255 across and y 0-127 up from the bottom of the canvas; a stream that draws before it moves starts at (127, 63). Every pixel plotted also sets its cell's ink (PLOT_PIXEL), flipped if it would be the paper's colour, so a line is never invisible. The 22 pictures, with the ops each stream holds and how long the game took to draw it here:

LocationNameStreamBytesLinesMovesFillsPaintsDrawing
1tunnel like hallLOC1_TUNNEL_LIKE_HALL_PIC56416268616.7 s
49great riverLOC49_GREAT_RIVER_PIC31510425616.5 s
6trolls pathLOC6_TROLLS_PATH_PIC6682444613011.7 s
11narrow placeLOC11_NARROW_PLACE_PIC2818527516.3 s
37dragons desolationLOC37_DRAGONS_DESOLATION_PIC590187561501.8 s
43smooth straight passageLOC43_SMOOTH_STRAIGHT_PASSAGE_PIC35014220100.3 s
38dale valleyLOC38_DALE_VALLEY_PIC53515063625.6 s
7trolls caveLOC7_TROLLS_CAVE_PIC638192502631.5 s
24forest gateLOC24_FOREST_GATE_PIC46514152802.4 s
35lake townLOC35_LAKE_TOWN_PIC691185100608.2 s
13goblins dungeonLOC13_GOBLINS_DUNGEON_PIC2787043206.6 s
31dark dungeonLOC31_DARK_DUNGEON_PIC2496041106.4 s
5trolls clearingLOC5_TROLLS_CLEARING_PIC70024857713.1 s
28levelled elvish clearingLOC28_LEVELLED_ELVISH_CLEARING_PIC60721851212.7 s
4lonelandsLOC4_LONELANDS_PIC12435152011.5 s
32elvenkings cellarLOC32_ELVENKINGS_CELLAR_PIC61821652902.0 s
16big goblins cavernLOC16_BIG_GOBLINS_CAVERN_PIC778284521701.3 s
25bewitched gloomy placeLOC25_BEWITCHED_GLOOMY_PLACE_PIC592215401116.4 s
8running riverLOC8_RUNNING_RIVER_PIC51719034526.3 s
41lower hallsLOC41_LOWER_HALLS_PIC4461323616110.3 s
26spider threads placeLOC26_SPIDER_THREADS_PLACE_PIC48517535428.1 s
39front gateLOC39_FRONT_GATE_PIC3731141613410.4 s

126.1 s of drawing in all. Two streams do not end: the last private op of location 13's and of location 5's takes the next picture's two header bytes as its operands and runs on into it (13 into 31; 5 into 28), so each of those is another picture with a few more ops first.

Bag End, op by op

DRAW_LOCATION_PICTURE run for location 1 on a game at its first prompt, with a probe at $7FBC, the top of RUN_PICTURE's loop, taking the canvas each time an op begins -- so each picture below is the canvas after the ops named, with the time they took. Clearing the canvas came first: 0.03 s. Then 230 moves and lines, shown in 4 pictures, and each fill and paint on its own:

Ops 1-58 ($CC45): 9 moves, 49 lines. 0.12 s.

Ops 1-58 ($CC45): 9 moves, 49 lines. 0.12 s.

Ops 59-116 ($CCC2): 45 lines, 13 moves. 0.10 s.

Ops 59-116 ($CCC2): 45 lines, 13 moves. 0.10 s.

Ops 117-174 ($CD43): 28 moves, 30 lines. 0.11 s.

Ops 117-174 ($CD43): 28 moves, 30 lines. 0.11 s.

Ops 175-230 ($CDD3): 38 lines, 18 moves. 0.07 s.

Ops 175-230 ($CDD3): 38 lines, 18 moves. 0.07 s.

Op 231 ($CE55): 1 fill: black from 191, 81. 0.02 s.

Op 231 ($CE55): 1 fill: black from 191, 81. 0.02 s.

Op 232 ($CE58): 1 fill: black from 191, 67. 0.02 s.

Op 232 ($CE58): 1 fill: black from 191, 67. 0.02 s.

Op 233 ($CE5B): 1 fill: black from 196, 80. 0.01 s.

Op 233 ($CE5B): 1 fill: black from 196, 80. 0.01 s.

Op 234 ($CE5E): 1 fill: black from 196, 68. 0.00 s.

Op 234 ($CE5E): 1 fill: black from 196, 68. 0.00 s.

Op 235 ($CE61): 1 fill: black from 156, 68. 0.89 s.

Op 235 ($CE61): 1 fill: black from 156, 68. 0.89 s.

Op 236 ($CE64): 1 paint: green from row 7, column 19. 0.00 s.

Op 236 ($CE64): 1 paint: green from row 7, column 19. 0.00 s.

Op 237 ($CE73): 1 fill: magenta from 127, 24. 5.37 s.

Op 237 ($CE73): 1 fill: magenta from 127, 24. 5.37 s.

The outline is almost free; the fills are the picture's time. The Locations page has every picture animated at the speed the game draws it.

Where the time goes

The same run, the T-states from each op's start to the next's, by kind:

OpHow manyT-statesTimeShareEach
clear-97,9460.03 s0.4%97,946
move688,6360.00 s0.0%127
line1621,406,6010.40 s6.0%8,682
fill622,075,7506.31 s93.5%3,679,291
paint19,1000.00 s0.0%9,100
end11510.00 s0.0%151

And the same over all 22 pictures:

OpHow manyT-statesTimeShareEach
clear-2,167,0550.62 s0.5%2,167,055
move979124,3330.04 s0.0%127
line354929,909,3188.55 s6.8%8,427
fill181408,843,178116.81 s92.7%2,258,802
paint20156,2220.04 s0.0%7,811
end223,3220.00 s0.0%151

Inside the ops, the time is in addressing. Every instruction of Bag End's drawing, its T-states put down to the routine it is in (probed at every instruction):

RoutineT-statesShare
PIXEL_ADDRESS12,282,71652.0%
FLOOD_FILL3,632,75115.4%
PLOT_PIXEL3,068,60013.0%
PIXEL_SET1,820,8857.7%
INC_Y1,641,4437.0%
DEC_Y534,9122.3%
DRAW_LINE240,2751.0%
INC_X176,1850.7%

DRAW_LINE and FLOOD_FILL step their point a pixel at a time, so they always know where the next pixel is -- and then PIXEL_ADDRESS works its screen address out again from x and y, rotating the mask into place one bit at a time. The fill does it for every pixel it tests (PIXEL_SET), above, below and beside the sweep, as well as for every pixel it plots, and keeps its queue of seeds on the machine stack.

The fast-draw patch

The patch on Patches keeps every algorithm and every seed and carries the address and mask along with the point instead, working them out from scratch only where a line or a sweep begins; then it takes the fill's own bookkeeping down too. Nine times faster over the 22 pictures, and the same pixels.

The same measurement with the patch from Patches laid over the same machine (its ranges copied from the patched snapshot the build makes): the ops are the same ones, at the same places in the stream, and the finished canvas is identical to the original's. Only the lines and fills get faster: the moves, the paints and clearing the canvas are untouched code.

OpOriginalPatchedFaster by
clear0.03 s0.03 s1.0x
move0.00 s0.00 s1.0x
line0.40 s0.21 s1.9x
fill6.31 s0.31 s20.5x
paint0.00 s0.00 s1.0x
the whole picture6.74 s0.55 s12.2x

The trolls' clearing, by night and by day

The one picture the game changes. On the tape the first two bytes of location 5's stream are $00 $00: black border, black paper, black ink: night. When day dawns and the trolls turn to stone, TROLLS_TURN_TO_STONE writes $05 $28: cyan border, cyan paper, black ink, and START (NEW_GAME) writes the night back before every game. The drawing is the same; drawn both ways here by DRAW_LOCATION_PICTURE, the second with the two bytes the dawn writes:

By night, as the tape has it: the lines come out white, because black ink on black paper is flipped by PLOT_PIXEL.

By night, as the tape has it: the lines come out white, because black ink on black paper is flipped by PLOT_PIXEL.

By day, after the dawn: the same ops on cyan.

By day, after the dawn: the same ops on cyan.

Location 5's stream runs on into location 28's -- levelled elvish clearing -- so the clearing is that picture with the trolls' ops first, and its colour bytes are the only ones in the game with a writer.

What is confirmed, and what is not