Episode 14: Loading a background from disk

Episode 14: Loading a background from disk

In Episode 12 we achieved fast, flicker-free character movement over a solid-color background via double-buffering. In Episode 13 we added a third video buffer, allowing us to have any arbitrary background. We demonstrated that by drawing a solid blue background with two rectangles over it. In this lesson we will give our app the ability to load backgrounds from a file!

As you are well aware by now, we are working with a 320x200 screen with 16 colors. So let's draw a compatible image, then use a little Python to convert it to a format that can be read directly into our background buffer. My approach is to draw the 320x200 picture in any image editor using the 16 available colors, save it as PNG, then write a Python script to read it and convert it to the 2-pixels-per-byte format we need for the app.

I'm very comfortable drawing in Inkscape, so I imported a picture I had handy and traced it with vector graphics. I then loaded the SVG into GIMP (Inkscape can export to PNG but it aliases (smoothly blends) the resulting pixels, and we must keep the drawing in 16 colors) and exported it to a PNG. I've included a a 16-color .GPL palette to be used with GIMP (coincidentally the same file format also works with Inkscape).

My source image:

My result (minus bird, plus firepit... also guessed a bit about the bottom since the source image wasn't quite tall enough):

A couple important things in GIMP:

  • Import the CGA 16-color palette (Windows -> Dockable Dialogs -> Palettes, right-click Palette Editor, choose Import Palette), then double-click to activate it.
  • Set the image to indexed color mode (Image -> Mode -> Indexed), select Use Custom Palette, and select the CGA 16-color palette.
  • Remove the alpha channel from the image (under Layers, right-click on the layer and select Remove Alpha Channel). Without this it won't export as indexed.

Assuming your .PNG image is infile and the resulting file is outfile, the Python script really boils down to a two-liner:

pixels = list( Image.open( infile ).getdata())
outfile.write( bytes( (pxpair[0] << 4) | pxpair[1] for pxpair in zip( pixels[::2], pixels[1::2] ) ) )

The PNG is indexed so each pixel is represented by a number 1-15. We're taking the pixels in pairs, and for each pair we shift the first pixel to the high 4 bits and sum in the second pixel. Then we write it all to an output file. The whole script is in the repo linked at the end; Pillow (Python Imaging Library) is the only external requirement.

Now that we've got our image in PCjr video format (I named mine room1.bin), let's write some code to read it.

Remove the part of code that draws the background color and two rectangles into the background buffer. In its place we'll call a read_file routine which we'll write later. The routine will need the name of the file, the number of bytes to read, and the memory location in which to store the file (our BACKGROUND_SEG).

The path can be stored in our data section:

path_room1: db "room1.bin", 0

Thus we can push the arguments, in order, like this:

push word [BACKGROUND_SEG]
mov ax, [room_width_px]
mov bx, [room_height_px]
mul bx
shr ax, 1
push ax
mov ax, path_room1
push ax
call read_file  ; read "room1.bin" into BACKGROUND_SEG

Hopefully you've picked up enough ASM knowledge to be able to see that the number of bytes to read is calculated as room_width_px * room_height_px / 2.

The DOS subsystem gives us some subroutines for handling files, available at INT 21h. The ones we're interested in are 3D (open file), 3E (close file) and 3F (read file). Let's check out our read_file subroutine which I've put into stdio.asm:

; read_file( path, size, destination )
; Reads bytes from a file into a buffer.
; Args:
;   bp+4 = path, bp+6 = size,
;   bp+8 = destination
read_file:
  push bp
  mov bp, sp

  mov ax, 0x3d00   ; Call INT 21h, 3D (open file)
  mov dx, [bp+4]   ; with DX as the path
  int 21h

  mov bx, ax       ; Move newly-opened file handle to BX
  mov ax, 0x3f00   ; Call INT 21h, 3F (read from file)
  mov cx, [bp+6]   ; with CX = file size...
  xor dx, dx
  push ds          ; (save DS first)
  mov di, [bp+8]
  mov ds, di       ; and DS:DX as the read buffer
  int 21h
  pop ds

  mov ax, 0x3e00  ; Call INT21h, 3E to close the file
  int 21h         ; (file handle still in BX)

  pop bp
  ret 6

Pretty simple, huh? In a nutshell:

  • We move the file path to DX and call INT 21h 3D to open it
  • We move the returned file handle to BX the size to CX, and the destination DS:DX, then call INT 21h 3F to read the file
  • Finally, we call INT 21h 3E to close the file

Let's see it in action:

Great! But it would be nice to 1) keep the player from running off the screen edges and 2) don't let him walk on water. We'll tackle that, and more, in the next episode!

Full source for this episode is available on GitHub: https://github.com/josh2112/pcjr-asm-game/tree/episode-14