A Fast Well-Behaved Pattern Flood Fill

Developer(s): Alvin Albrecht
Date: 2003
Type: Program
Platform(s): TS 2068

This program implements a patterned flood fill algorithm in C with an embedded Z80 assembly library, written by Alvin Albrecht in 2002–2003. The main routine draws ten random circles on a cleared screen, then picks a random starting point and one of eight hard-coded 8×8 fill patterns before calling the assembly routine `sp_PFill`. The flood fill operates byte-at-a-time using a breadth-first, three-block circular queue allocated on the Z80 stack, with the queue size controlled by the caller—the comment notes a maximum stack consumption of 3×300+30 bytes for the demo invocation. The queue is divided into “pattern,” “investigate,” and “new” blocks, maintaining a two-pixel-thick fill frontier so the expanding diamond wave can be patterned behind it without leaving unfilled gaps; if the queue overflows, a bail-out routine patterns any pending pixels and returns with the carry flag set. Pixel expansion uses a byte-widening loop in `bytefill` that fans out set bits left and right within each display byte before committing them to screen.


Program Analysis

Overall Structure

The program has two distinct layers: a short C driver (main()) and a hand-written Z80 assembly flood-fill engine (SPPFill). The C code uses the graphics.h and spritepack.h libraries and runs in an infinite loop. The assembly is a self-contained library module that exports SPPFill and imports helper routines (SPGetScrnAddr, SPPixelUp, SPPixelDown, SPCharLeft, SPCharRight).

C Driver

Each iteration of while(1) does the following:

  1. Clears the graphics screen with clg().
  2. Draws 10 random circles, each guaranteed to lie fully on-screen (the do…while loop rejects coordinates where the circle would clip any edge).
  3. Picks a random seed point (x, y) and a pattern offset r — a multiple of 8 in the range 0–56 — indexing one of the eight 8-byte patterns in the patterns[] array.
  4. Calls sp_PFill(x, y, patterns + r, 300), passing a queue depth of 300 (worst-case stack use ≈ 930 bytes plus 30 bytes of overhead).
  5. Waits for a keypress with sp_WaitForKey() before looping.

Fill Pattern Table

Eight 8×8 patterns are defined as inline assembly defb binary literals. Their visual meanings are:

IndexDescription
0Solid black (all bits set)
1Checkerboard (alternating 0xAA / 0x55)
2Outlined rectangle / hollow box
3Diagonal lines, top-left to bottom-right
4Diagonal lines, top-right to bottom-left
5Cross-hatch
6Sparse diagonal dots
7Border with interior diagonal line

Queue Architecture

The algorithm allocates its circular queue entirely on the Z80 hardware stack, growing downward. The queue is divided into three logically contiguous blocks, each terminated by a 3-byte sentinel whose screen-address MSB is below 0x40. The physical end of the queue is marked with a 0x80 byte, exploiting the fact that valid display addresses always have an MSB in the range 0x400x7F. Queue wrap-around is detected by comparing the current pointer’s MSB against 0x80; when wrap-around is needed, the pointer is reset to IX (the top of the queue).

Each queue entry is a 3-byte struct stored MSB-first:

  • Screen address (2 bytes, MSB first)
  • Fill byte (1 byte)

Six pointer variables are maintained in a fixed frame relative to IX:

IX offsetVariable
+1/+2New block pointer
+3/+4Investigate block pointer
+5/+6Pattern block pointer
+7/+8Available queue depth counter
+9/+10Fill pattern pointer
+11/+12Return address

Three-Block Wave Algorithm

The fill frontier advances as a Manhattan-distance diamond. At each iteration of pfloop:

  1. investigate expands the investigate block outward in four directions, adding successful expansions to the new block.
  2. applypattern writes the fill pattern to screen for every pixel in the pattern block, freeing that space in the queue.
  3. The investigate block logically becomes the new pattern block, and the new block becomes the new investigate block.

The two-pixel-thick boundary (pattern block + investigate block) ensures there are no holes in the frontier through which already-patterned areas could be re-entered by the fill.

Byte-Level Fill Expansion (bytefill)

Rather than working one pixel at a time, the algorithm operates on whole display bytes. The bytefill routine takes an incoming set of pixels (as a bitmask) and expands them left and right within the byte using a loop:

  • rra shifts right to probe the right neighbor bit.
  • add a,a (left shift) probes the left neighbor.
  • The two shifts are OR-ed with the current byte and then masked against already-set pixels in the display byte.
  • The loop repeats until the byte stabilizes, ensuring the full horizontal extent of the fill within that byte is determined before committing.

This is considerably faster than single-pixel processing and naturally handles byte-boundary cases for left/right expansion checks.

Left/Right Movement and Screen Boundaries

Horizontal expansion is gated: moving left requires the leftmost bit of the fill byte to be set (bit 7,b) and the screen column not to be zero (guarded by ld a,l / and 31 and a hi-res mode check via bit 5,h). Moving right requires the rightmost bit (bit 0,b) and checks for right-edge wrap by testing l AND 31 == 0 after the move.

Queue Overflow Bail-Out

When the available-depth counter reaches zero in addnew, the routine jumps to bail. The bail-out path clears the current pixel from the screen (to avoid a visible artifact), marks the end of the new block, then calls applypattern three times in succession to drain the pattern block, the investigate block, and the new block. It then restores SP from IX and returns with the carry flag set to signal the partial fill to the caller.

IX-Relative Variable Access and Undocumented Opcodes

The frame-pointer use of IX throughout is a deliberate C-calling-convention mimic in assembly. Two instances of defb 0xdd appear in endinv and annowrap to prefix a subsequent ld e,l or ld d,h instruction with the IX register, effectively encoding ld ixl,l and ld ixh,h — undocumented but widely used Z80 instructions not directly expressible in the assembler’s syntax. This is used to reload DE (the new-block pointer) with the value of IX when wrap-around is detected.

Pattern Application

In applypattern, the scan-line index (bits 0–2 of the screen address MSB, extracted with and 0x07) selects one of the eight bytes from the 8×8 pattern. The selected pattern byte is ANDed with the fill byte (to restrict the pattern to filled pixels) and then the complement of the fill byte is OR-ed in, ensuring unfilled pixels in the byte are cleared. This correctly handles partial bytes at fill boundaries.

Content

Appears On

Related Products

Related Articles

Tutorial on the display file and a machine code flood fill routine.

Related Content

Image Gallery

Source Code

 /* Alvin Albrecht 01.2003 */
 
 #include <stdlib.h>
 #include <graphics.h>
 #include <spritepack.h>
 
 extern uchar patterns[];
 #asm
 ._patterns
 
 defb @11111111
 defb @11111111
 defb @11111111
 defb @11111111
 defb @11111111
 defb @11111111
 defb @11111111
 defb @11111111
 
 defb @10101010
 defb @01010101
 defb @10101010
 defb @01010101
 defb @10101010
 defb @01010101
 defb @10101010
 defb @01010101
 
 defb @00000000
 defb @01111110
 defb @01100110
 defb @01100110
 defb @01100110
 defb @01100110
 defb @01111110
 defb @00000000
 
 defb @10001000
 defb @01000100
 defb @00100010
 defb @00010001
 defb @10001000
 defb @01000100
 defb @00100010
 defb @00010001
 
 defb @00010001
 defb @00100010
 defb @01000100
 defb @10001000
 defb @00010001
 defb @00100010
 defb @01000100
 defb @10001000
 
 defb @10011001
 defb @01100110
 defb @01100110
 defb @10011001
 defb @10011001
 defb @01100110
 defb @01100110
 defb @10011001
 
 defb @00100010
 defb @01010101
 defb @10001000
 defb @00000000
 defb @00100010
 defb @01010101
 defb @10001000
 defb @00000000
 
 defb @11111111
 defb @10000000
 defb @10100010
 defb @10010100
 defb @10001000
 defb @10010100
 defb @10100010
 defb @10000000
 #endasm
 
 
 main()
 {
   int x,y,r,n;
 
   while (1) {
      clg();
      for (n=0; n!=10; n++) {
         do {
            x = rand() % 256;
            y = rand() % 192;
            r = rand() % 40;
         } while (((x-r)<0) || ((y-r)<0) || ((x+r)>255) || ((y+r)>191));
         circle(x,y,r,1);
      }
      x = rand() % 256;
      y = rand() % 192;
      r = (rand() % 8)*8;
      sp_PFill(x, y, patterns + r, 300);   /* max stack space used = 3*300+30 bytes */
      sp_WaitForKey();
   }
 }
 
 
 ;
 ; SPFill
 ;
 
 INCLUDE "SPconfig.def"
 XLIB SPPFill
 LIB SPGetScrnAddr, SPPixelUp, SPPixelDown, SPCharLeft, SPCharRight
 
 ; Patterned Flood Fill
 ; Alvin Albrecht 2002
 ;
 ;
 ; This subroutine does a byte-at-a-time breadth-first patterned flood fill.
 ; It works by allocating a circular queue on the stack, with the size of the
 ; queue determined by an input parameter.  The queue is divided into three
 ; contiguous blocks: the pattern block, the investigate block and the new block.
 ; The queue contains 3-byte structures (described below) with a special structure
 ; delimiting each block.  The physical end of the queue is marked with a special
 ; byte.  The contents of the queue grow down in memory.
 ;
 ; The pattern block holds pixels that have been blackened on screen and are
 ; only waiting to have the pattern applied to them before they are removed
 ; from the queue.
 ;
 ; The investigate block holds pixels that have been blackened on screen and
 ; are waiting to be investigated before they become part of the pattern
 ; block.  'Investigating' a pixel means trying to expand the fill in all
 ; four directions.  Any successful expansion causes the new pixel to be
 ; added to the new block.
 ;
 ; The new block holds pixels that have been blackened on screen and are
 ; waiting to become part of the investigate block.  The new block expands
 ; as the investigate block is investigated.
 ;
 ; The pattern fill algorithm follows these steps:
 ; 1. Examine the investigate block.  Add new pixels to the new block if an
 ;    expansion is possible.
 ; 2. Pattern the pattern block.  All pixels waiting to be patterned are
 ;    patterned on screen.
 ; 3. The investigate block becomes the pattern block.
 ;    The new block becomes the investigate block.
 ; 4. Repeat until the investigate block is empty.
 ;
 ; The algorithm may bail out prematurely if the queue fills up.  Bailing
 ; out causes any pending pixels in the queue to be patterned before the
 ; subroutine exits.  If PFILL would continue to run by refusing to enter
 ; new pixels when the queue is full, there would be no guarantee that
 ; the subroutine would ever return.
 ;
 ; In English, the idea behind the patterned flood fill is simple.  The
 ; start pixel grows out in a circular shape (actually a diamond).  A
 ; fill boundary two pixels thick is maintained in that circular shape.
 ; The outermost boundary is the frontier, and is where the flood fill
 ; is growing from (ie the investigate block).  The inner boundary is
 ; the pattern block, waiting to be patterned.  A solid inner boundary
 ; is necessary to prevent the flood-fill frontier pixels from growing
 ; back toward the starting pixel through holes in the pattern.  Once
 ; the frontier pixels are investigated, the innermost boundary is
 ; patterned.  The newly investigated pixels become the outermost
 ; boundary (the investigate block) and the old investigate block becomes
 ; the new pattern block.
 ;
 ; Each entry in the queue is a 3-byte struct that grows down in memory:
 ;       screen address      (2-bytes, MSB first)
 ;       fill byte           (1-byte)
 ; A screen address with MSB < 0x40 is used to indicate the end of a block.
 ; A screen address with MSB >= 0x80 is used to mark the physical end of the Q.
 ;
 ; The fill pattern is a typical 8x8 pixel character, stored in 8 bytes.
 
 ; enter: h = y coord, l = x coord, bc = max stack depth, de = address of fill pattern
 ;        In hi-res mode, carry flag is most significant bit of x coord
 ; used : ix, af, bc, de, hl
 ; exit : no carry = success, carry = had to bail queue was too small
 ; stack: 3*bc+30 bytes, not including the call to PFILL or interrupts
 
 .SPPFill
   push de			; save (pattern pointer) variable
   dec bc			; we will start with one struct in the queue
   push bc			; save max stack depth variable
 
   ld a,h
   call SPGetScrnAddr	; de = screen address, b = pixel byte
   ex de,hl			; hl = screen address
   call bytefill		; b = fill byte
   jr c, viable
   pop bc
   pop de
   ret
 
 .viable
   ex de,hl			; de = screen address, b = fill byte
   ld hl,-7
   add hl,sp
   push hl			; create pattern block pointer = top of queue
   push hl
   pop ix			; ix = top of queue
   dec hl
   dec hl
   dec hl
   push hl			; create investigate block pointer
   ld hl,-12
   add hl,sp
   push hl			; create new block pointer
 
   xor a
   push af
   dec sp			; mark end of pattern block
   push de			; screen address and fill byte are
   push bc			;   first struct in investigate block
   inc sp
   push af
   dec sp			; mark end of investigate block
 
   ld c,(ix+7)
   ld b,(ix+8)		; bc = max stack depth - 1
   inc bc
   ld l,c
   ld h,b
   add hl,bc		; space required = 3*BC (max depth) + 10
   add hl,bc		; but have already taken 9 bytes
   ld c,l
   ld b,h			; bc = # uninitialized bytes in queue
   ld hl,0
   sbc hl,bc		; negate hl, additions above will not set carry
   add hl,sp
   ld (hl),0		; zero last byte in queue
   ld sp,hl			; move stack below queue
   ld a,$80
   push af			; mark end of queue with $80 byte
   inc sp
   ld e,l
   ld d,h
   inc de
   dec bc
   ldir			; zero the uninitialized bytes in queue
 
 ; NOTE: Must move the stack before clearing the queue, otherwise if an interrupt
 ; occurred, garbage could overwrite portions of the (just cleared) queue.
 
 ; ix = top of queue, bottom of queue marked with 0x80 byte
 
 ; Variables indexed by ix, LSB first:
 ;   ix + 11/12    return address
 ;   ix + 09/10    fill pattern pointer
 ;   ix + 07/08    max stack depth
 ;   ix + 05/06    pattern block pointer
 ;   ix + 03/04    investigate block pointer
 ;   ix + 01/02    new block pointer
 
 ; A picture of memory at this point:
 ;
 ;+-----------------------+   higher addresses
 ;|                       |         |
 ;|-   return address    -|        |/
 ;|                       |         V
 ;+-----------------------+   lower addresses
 ;|        fill           |
 ;|-  pattern pointer    -|
 ;|                       |
 ;+-----------------------+
 ;|                       |
 ;|-  max stack depth    -|
 ;|                       |
 ;+-----------------------+
 ;|                       |
 ;|-   pattern block     -|
 ;|                       |
 ;+-----------------------+
 ;|                       |
 ;|- investigate block   -|
 ;|                       |
 ;+-----------------------+
 ;|                       |
 ;|-     new block       -|
 ;|                       |
 ;+-----------------------+
 ;|  end of block marker  |  <- ix = pattern block = top of queue
 ;|          ?            |
 ;|          ?            |
 ;+-----------------------+
 ;|  screen address MSB   |  <- investigate block
 ;|  screen address LSB   |
 ;|      fill byte        |
 ;+-----------------------+
 ;|  end of block marker  |
 ;|          ?            |
 ;|          ?            |
 ;+-----------------------+
 ;|          0            |  <- new block
 ;|          0            |
 ;|          0            |
 ;+-----------------------+
 ;|                       |
 ;|        ......         |  size is a multiple of 3 bytes
 ;|     rest of queue     |
 ;|      all zeroed       |
 ;|        ......         |
 ;|                       |
 ;+-----------------------+
 ;|         0x80           |  <- sp, special byte marks end of queue
 ;+-----------------------+
 
 .pfloop
   ld l,(ix+3)
   ld h,(ix+4)		; hl = investigate block
   ld e,(ix+1)
   ld d,(ix+2)		; de = new block
   call investigate
   ld (ix+1),e
   ld (ix+2),d		; save new block
   ld (ix+3),l
   ld (ix+4),h		; save investigate block
 
   ld l,(ix+5)
   ld h,(ix+6)		; hl = pattern block
   ld c,(ix+7)
   ld b,(ix+8)		; bc = max stack depth (available space)
   call applypattern
   ld (ix+7),c
   ld (ix+8),b		; save stack depth
   ld (ix+5),l
   ld (ix+6),h		; save pattern block
 
   ld a,(hl)		; done if the investigate block was empty
   cp 0x40
   jp nc, pfloop
 
 .endpfill
   ld de,11			; return address is at ix+11
   add ix,de
   ld sp,ix
   or a			; make sure carry is clear, indicating success
   ret
 
 ; IN/OUT: hl = investigate block, de = new block
 
 .investigate
   ld a,(hl)		
   cp 0x80			; bit 15 of screen addr set if time to wrap		
   jp c, inowrap
   push ix
   pop hl			; hl = ix = top of queue
   ld a,(hl)
 
 .inowrap
   cp 0x40			; screen address < 0x4000 marks end of block
   jp c, endinv		; are we done yet?
   ld b,a
   dec hl
   ld c,(hl)		; bc = screen address
   dec hl
   ld a,(hl)		; a = fill byte
   dec hl
   push hl			; save spot in investigate block
   ld l,c
   ld h,b			; hl = screen address
   ld b,a			; b = fill byte
 
 .goup
   push hl			; save screen address
   call SPPixelUp		; move screen address up one pixel
   jr c, updeadend		; if went off-screen
   push bc			; save fill byte
   call bytefill
   call c, addnew		; if up is not dead end, add this to new block
   pop bc			; restore fill byte
 
 .updeadend
   pop hl			; restore screen address
 
 .godown
   push hl			; save screen address
   call SPPixelDown		; move screen address down one pixel
   jr c, downdeadend
   push bc			; save fill byte
   call bytefill
   call c, addnew		; if down is not dead end, add this to new block
   pop bc			; restore fill byte
 
 .downdeadend
   pop hl			; restore screen address
 
 .goleft
   bit 7,b			; can only move left if leftmost bit of fill byte set
   jr z, goright
   ld a,l
   and 31
   jr nz, okleft
   bit 5,h              ; for hi-res mode: column = 1 if l=0 and bit 5 of h is set
   jr z, goright
 
 .okleft
   push hl			; save screen address
   call SPCharLeft
   push bc			; save fill byte
   ld b,0x01		; set rightmost pixel for incoming byte
   call bytefill
   call c, addnew		; if left is not dead end, add this to new block
   pop bc			; restore fill byte
   pop hl			; restore screen address
 
 .goright
   bit 0,b			; can only move right if rightmost bit of fill byte set
   jr z, nextinv
   or a			; clear carry
   call SPCharRight
   jr c, nextinv     	; went off screen
   ld a,l
   and 31
   jr z, nextinv  	; wrapped around line
   ld b,0x80		; set leftmost pixel for incoming byte
   call bytefill
   call c, addnew		; if right is not dead end, add this to new block
 
 .nextinv
   pop hl			; hl = spot in investigate block
   jp investigate
 
 .endinv
   dec hl
   dec hl
   dec hl			; investigate block now points at new block
 
   ld a,(de)		; check if new block is at end of queue
   cp 0x80
   jr c, nowrapnew
   defb 0xdd
   ld e,l
   defb 0xdd
   ld d,h			; de = ix = top of queue
 
 .nowrapnew
   xor a
   ld (de),a		; store end marker for new block
   dec de
   dec de
   dec de
   ret
 
 ; enter b = incoming byte, hl = screen address
 ; exit  b = fill byte, screen blackened with fill byte
 
 .bytefill
   ld a,b
   xor (hl)			; zero out incoming pixels that
   and b			; run into set pixels in display
   ret z
 
 .bfloop
   ld b,a
   rra			; expand incoming pixels
   ld c,a			; to the right and left
   ld a,b			; within byte
   add a,a
   or c
   or b
   ld c,a
   xor (hl)			; zero out pixels that run into
   and c			; set pixels on display
   cp b
   jp nz, bfloop		; keep going until incoming byte does not change
 
   or (hl)
   ld (hl),a		; fill byte on screen
   scf			; indicate that this was a viable step
   ret
 
 ; add incoming fill byte and screen address to new block
 ; enter b = incoming byte, hl = screen address, de = new block
 
 .addnew
   push hl			; save screen address
   ld l,(ix+7)
   ld h,(ix+8)		; hl = max stack depth
   ld a,h
   or l
   jr z, bail		; no space in queue so bail!
   dec hl			; available queue space decreases by one struct
   ld (ix+7),l
   ld (ix+8),h
   pop hl			; hl = screen address
 
   ld a,(de)		; check if new block is at end of queue
   cp 0x80
   jr c, annowrap
   defb 0xdd
   ld e,l
   defb 0xdd
   ld d,h               ; de = ix = top of queue
 
 .annowrap
   ex de,hl
   ld (hl),d		; make struct, store screen address (2 bytes)
   dec hl
   ld (hl),e
   dec hl
   ld (hl),b		; store fill byte (1 byte)
   dec hl
   ex de,hl
   ret
 
 ; if the queue filled up, we need to bail.  Bailing means patterning any set pixels
 ; which may still be on the display.  If we didnt bail and tried to trudge along,
 ; there is no guarantee the fill would ever return.
 
 .bail
   pop hl			; hl = screen address, b = fill byte
   ld a,b
   xor (hl)
   ld (hl),a		; clear this byte on screen
 
   xor a
   ld (de),a		; mark end of new block
 
   ld l,(ix+5)
   ld h,(ix+6)		; hl = pattern block
   call applypattern	; for pattern block
   call applypattern	; for investigate block
   call applypattern	; for new block
 
   ld de,11			; return address is at ix+11
   add ix,de
   ld sp,ix
   scf			; indicate we had to bail
   ret
 
 ; hl = pattern block, bc = max stack depth (available space)
 
 .applypattern
   ld a,(hl)
   cp 0x80			; bit 15 of screen addr set if time to wrap
   jp c, apnowrap
   push ix
   pop hl			; hl = ix = top of queue
   ld a,(hl)
 
 .apnowrap
   cp 0x40			; screen address < 0x4000 marks end of block
   jr c, endapply		; are we done yet?
 
   and 0x07			; use scan line 0..7 to index pattern
   add a,(ix+9)
   ld e,a
   ld a,0
   adc a,(ix+10)
   ld d,a			; de points into fill pattern
   ld a,(de)		; a = pattern 
 
   ld d,(hl)
   dec hl
   ld e,(hl)		; de = screen address
   dec hl
 
   and (hl)			; and pattern with fill byte
   sub (hl)			; or in complement of fill byte
   dec a
   ex de,hl
   and (hl)			; apply pattern to screen
   ld (hl),a
   ex de,hl
   dec hl
   inc bc			; increase available queue space
   jp applypattern
 
 .endapply
   dec hl
   dec hl
   dec hl			; pattern block now pts at investigate block
   ret

Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.

Scroll to Top