Tricks with GET and PUT

The very basics of GET and PUT

QuickBASIC programmers learn very early how to use GET and PUT because it's by far the easiest, fastest, and most flexible graphic tool available right off the box. In short, we create a buffer (usually an INTEGER array) to hold the image, draw some stuff on the screen, then use the GET instruction to capture the image inside a buffer so it can be pasted again anywhere via the PUT instruction.

The GET instruction copies a rectangular area from the active video page into the specified buffer and takes two coordinates similarly to the LINE instruction plus an array that serves as a buffer. The PUT instruction pastes on the active page an image captured with GET. It only takes the coordinates to the top-left corner of the image, the buffer, and the drawing mode (here, it's PSET.) Beware though, as QuickBASIC won't allow the image to overflow the edges of the screen!

' set mode 0x13 (320x200, 8 bits per plane, 1 plane)
SCREEN 13

' reserve (too much) memory in form of an INTEGER array
DIM buffer(1023) AS INTEGER

' draw something on the screen
CIRCLE (7, 7), 7, 14
PAINT (7, 7), 14
PSET (5, 4), 0: PSET (9, 4), 0
PSET (5, 5), 0: PSET (9, 5), 0
CIRCLE (7, 7), 4, 0, 4.7

' capture the 16x16 image, starting at pixel 0, 0
' going 15 pixels to the right, and 15 pixels down.
GET (0, 0)-STEP(15, 15), buffer

' clear screen
CLS

' display the image we captured earlier at 142, 92
PUT (142, 92), buffer, PSET

SLEEP: END

Now, was this really worth a tutorial? No. Most people who learn about GET and PUT will write code similar to that... but there's room for improvement.

Get the buffer size (easy mode)

The first challenge is of course: how do we determine how big the array should be? It depends on the image dimensions and the video mode, more specifically the number of bits per plane and the number of planes. It is usually easy to find that value in mode 13 and 7... "usually" because there are exceptions we'll cover later.

To calculate the buffer size, we need to know that the image buffer contains two blocks: a 4-byte header (it contains the image width in bits and the image height in pixels) and then a larger block that contains pixel data. The size of that second block is (in general) the total number of pixels divided by the number of pixels per byte. Thus, for mode 13:

imgWidth% = 16                      ' image width, in pixels
imgHeight% = 16                     ' image height, in pixels
imgPixels% = imgWidth% * imgHeight% ' image size, in pixels

bufHeadLen% = 4                     ' header size, in bytes
bufPixelLen% = imgPixels% \ 1       ' image size, in bytes (1 pixel per byte)

bufferLen% = bufHeadLen% + bufPixelLen%      ' size of the buffer, in bytes
bufferLen% = bufferLen% + (bufferLen% AND 1) ' make the size even

DIM buffer(bufferLen% \ 2 - 1) AS INTEGER ' buffer for a 16x16 image

And for mode 7 (as long as the width in pixels is multiple of 4:)

imgWidth% = 16                      ' Image width, in pixels
imgHeight% = 16                     ' image height, in pixels
imgPixels% = imgWidth% * imgHeight% ' image size, in pixels

bufHeadLen% = 4                     ' header size, in bytes
bufPixelLen% = imgPixels% \ 2       ' image size, in bytes (2 pixels per byte)

bufferLen% = bufHeadLen% + bufPixelLen%      ' size of the buffer, in bytes
bufferLen% = bufferLen% + (bufferLen% AND 1) ' make the size even

DIM buffer(bufferLen% \ 2 - 1) AS INTEGER ' buffer for a 16x16 image

You may wonder why we subtract 1 from bufferLen% after dividing by two. INTEGERS are 2 bytes long, so we have to divide the length (in bytes) by two to obtain the number of integers we need (this is also why we round the size to the next even number if we have to.) Then, we subtract 1 because, by default, QuickBASIC initializes new arrays at index 0 (not 1) and when only one value is provided, it is assumed to be the upper boundary.

As a side note, the biggest advantage to using INTEGER arrays is that we can retrieve the width and height of the image if needed by simply reading the first and second elements of the array! For mode 13:

PRINT " Width:"; image(0) \ 8  ' divide by the number of bits per plane to get the width in pixels
PRINT "Height:"; image(1)      ' height, in pixels

And for mode 7:

PRINT " Width:"; image(0) \ 1  ' divide by the number of bits per plane to get the width in pixels
PRINT "Height:"; image(1)      ' height, in pixels

Now you may wonder what is that "bits per plane" thingy I keep rambling about... planes are like "layers" superposed on the screen, and they add up to each pixel to give them their color attribute... which leads us to...

Get the buffer size (hard mode)

The easy mode is straightforward as long as we respect two rules: no WINDOW trickery, and make sure the image width is multiple of 4 in mode 7... it sounds like a lot, but it's really simple and is my favorite way of doing things. Let's consider the alternative for a minute:

In mode 7, each pixel is described by a 4-bit value... that's how we have 16 colors (or "2 to the power of [4 bits per pixel]" colors.) Now here's the catch: those 4 bits are not contiguous in memory, they are spread through 4 planes. Each plane is its own individual thing, and thus the length (in bits) of each line for each plane has to be multiple of 8 to form a full byte.

So, if we were to save a 1x1 image in mode 7, here's how it would go: we first store 1 bit from the 1st plane and pad the remaining 7 bits, then we store 1 bit from the 2nd plane and pad 7 extra bits, then we do the same for plane 3 and 4... it means that to store 4 bits, we wasted 28. In other words: 4 bytes (that's our 4-bit pixel plus the 28 padding bits) are required to store the pixel data (without header) of a 1x1, 2x1, 3x1, 4x1, 5x1, 6x1, 7x1, or 8x1 image in mode 7.

If we want to play with exotic image dimensions, we'll have to take that "quirk" into account, consider the number of bits per plane and the number of planes of the current graphic mode. The full buffer size in bytes can be obtained with the following formula:

CONST bpp = 1    ' 1 bit per plane
CONST planes = 4 ' 4 planes

w% = INT(((PMAP(x2%, 0) - PMAP(x1%, 0) + 1) * bpp + 7) / 8) * planes ' width (all planes,) in bytes
h% = PMAP(y2%, 1) - PMAP(y1%, 1) + 1                                 ' height, in pixels
Size% = 4 + w% * h%                                                  ' buffer size, in bytes

Size2% = (Size% + (Size% AND 1)) \ 2                                 ' buffer size, in INTEGERS

The values of "bpp" (bits per plane) and "planes" depend directly on the graphic mode, so remember to change the constants above accordingly:

Side note: the information above is taken from the QuickBASIC documentation and I suspect the specifications for modes 8, 12 and 9 (if more than 64Kb of video memory is present on the graphic card) are wrong: it's actually 1 bit per plane, 4 planes - like mode 7.)

Note that the graphic mode must be set before calling PMAP and the result is influenced by the active WINDOW dimensions (you may ignore PMAP if you don't use the WINDOW instruction...) now, if you know what screen mode you're using, the size of your images and the WINDOW size, you can eyeball the buffer size and skip the distressing formula.

While we're talking about bits per plane, planes, and graphic modes: it is possible to share graphics between screen modes that have the same number of bits per plane, the same number of planes and the same width in pixels. For instance, GET something in mode 2 and then PUT it in mode 4.

Stick it in there!

Now that we got the basics out of the way, did you know that you can tell GET and PUT where to start writing and reading data? All we have to do is include the number of the first element of the array to write to (or read from.) This way, we can reserve one buffer that would contain two (or let's be crazy, three, or EVEN MORE) images. We only have to keep track of the location of each image in the buffer. The easiest way to do that is to have images that are the exact same size (in INTEGERS,) and then multiply that size by the number of images we want to skip:

' set mode 13 (320x200, 8 bits per plane, 1 plane)
SCREEN 13

' in mode 13, a 16x16 image is 256 pixels or 256 bytes. We need an extra
' 4 bytes for the header, so that's 260 bytes per image. But since we use
' integers, not bytes, one image will "only" take 130 elements (INTEGERS.)
CONST imgSize = 130

' we want to store two images in this example, so let's write it down.
CONST imgCount = 2

' and now, we can declare our array (1st element is 0, last element is 259.)
DIM image(imgCount * imgSize - 1) AS INTEGER

' draw image 1
LINE(0, 0)-(15, 15), 13, B
LINE(0, 0)-(15, 15), 15
LINE(15, 0)-(0, 15), 1

' save image 0, starting at element 0
GET(0, 0)-STEP(15, 15), image(0 * imgSize)

' draw image 2
LINE(0, 0)-(15, 15), 4, BF
LINE(0, 7)-(15, 7), 10
LINE(7, 0)-(7, 15), 2

' save image 1, starting at element 130
GET(0, 0)-STEP(15, 15), image(1 * imgSize)

' clear all
CLS

' paste both images in the middle of the screen
PUT(152, 84), image(0 * imgSize), PSET
PUT(152, 100), image(1 * imgSize), PSET

SLEEP: END

That's pretty neat, because this offers a convenient way to store many images in one place and we can retrieve each image by simply providing the proper offset. This allows for some fancy graphic text with very little code:

' set mode 13 (320x200, 8 bits per plane, 1 plane)
SCREEN 13

' in mode 13, a 8x8 character is 64 pixels (64 bytes). We need an extra
' 4 bytes for the header, so that's 68 bytes per character. We divide by
' 2 to get obtain: 34 INTEGERS per character. Remember this number.
CONST imgSize = 34

' we're going to store 128 characters:
CONST imgCount = 128

' and we reserve enough memory to store it all:
DIM gfxCar(imgCount * imgSize - 1) AS INTEGER

' display standard 128-character ASCII table
COLOR 15
FOR i% = 0 TO 127
  IF (i% <> 12) THEN ' skip "clear screen" code
    LOCATE 1 + (i% \ 16), 1 + (i% AND 15): PRINT CHR$(i%)
  END IF
NEXT i%

' make it fancy!
FOR y% = 0 TO 63
  FOR x% = 0 TO 127
    IF POINT(x%, y%) THEN PSET (x%, y%), 32 + (y% AND 7)
  NEXT x%
NEXT y%

' capture each character individually
FOR y% = 0 TO 7
  FOR x% = 0 TO 15
    GET (x% * 8, y% * 8)-STEP(7, 7), gfxCar(carOfs%)
    carOfs% = carOfs% + imgSize ' here's that number
  NEXT x%
NEXT y%

LOCATE 10, 1: PRINT "Done. Press any key to continue..."
SLEEP: CLS

' display a test string on screen
PAINT(0, 0), 7
TestString$ = "Now, this is FANCY!"
FOR i% = 1 TO LEN(TestString$)
  carOfs% = ASC(MID$(TestString$, i%, 1)) ' get ASCII code for this character
  PUT (8 + (i% - 1) * 8, 32), gfxCar(carOfs% * imgSize), PSET ' here's that number again
NEXT i%

SLEEP: END

This type of organization is great when rendering tile-based levels: you only need to multiply a tile index by a constant to draw the proper graphic for each tile. It's not only easier to maintain, it's also much faster than working with multiple arrays and condition blocks. The other obvious usage would be animation frames: replace the tile index by a frame number, multiply by a constant and you obtain the proper graphic without painstakingly checking each case manually.

Drawing sprites with boolean black magic

We slapped together graphic fonts in the previous section. It's fancy and all, but it still suffers from the old PRINT issue: the graphics located under the text is erased. Hopefuly, there is a way to preserve the background and it only requires a little bit of boolean magic.

You can PUT images on screen using five techniques: PSET, PRESET, AND, OR and XOR. Up until now, we only used PSET, which overwrites existing pixels on the screen with whatever is in the image buffer. It's sufficiant for opaque tiles, but it doesn't work for sprites because they have see-through parts. To draw sprites, we'll first draw a picture that will only clear (set to 0) pixels that needs to be overwritten, and then we'll draw a second picture that only replaces cleared pixels. This is done with the boolean operators AND and OR. An in-depth explanation of how booleans work would require its very own article so we'll keep it simple for now:

First, we create a mask with only two color indices: 0 (binary 00000000) and 255 (11111111,) and we PUT it on the screen with the AND operator. Target pixels under index 0 will be cleared (because 00000000 AND anything will always return 00000000,) while pixels under index 255 will remain intact (because 11111111 AND anything will always return "anything".) In other words, see-through pixels on the mask are index 255 and opaque pixels are index 0.

Then, we create a color image that has its see-through pixels set to index 0 (because 00000000 OR anything will always return "anything",) while the solid part is whatever color we want to display: the previously applied mask changed the target pixel to 00000000, and 00000000 OR anything (our color image) will always return "anything:"

' set mode 13 (320x200, 8 bits per plane, 1 plane)
SCREEN 13

' in mode 13, a 8x8 character is 64 pixels (64 bytes). We need an extra
' 4 bytes for the header, so that's 68 bytes per character. We divide by
' 2 to get obtain: 34 INTEGERS per character. However, this time, we need
' to store a color image AND a mask, so there will be 68 INTEGERS between
' two characters...
CONST imgSize = 68

' ...and we'll keep track of the offset to the mask here:
CONST imgMask = 34

' we still need to store 128 characters:
CONST imgCount = 128

' and we reserve enough memory to store it all:
DIM gfxCar(imgCount * imgSize - 1) AS INTEGER

' by default, attribute 255 is black, so let's replace it
' with something more visible for the demo.
PALETTE 255, &h3F003F

' display standard 128-character ASCII table
COLOR 15
FOR i% = 0 TO 127
  IF (i% <> 12) THEN ' skip "clear screen" code
    LOCATE 1 + (i% \ 16), 1 + (i% AND 15): PRINT CHR$(i%)
  END IF
NEXT i%

' make it fancy (and masky!)
FOR y% = 0 TO 63
  FOR x% = 0 TO 127
    IF POINT(x%, y%) THEN ' opaque pixel
      PSET (x%, y%), 32 + (y% AND 7) ' to color image
      PSET (x%, y% + 64), 0 ' to mask image
    ELSE ' see-through pixel
      PSET (x%, y%), 0 ' to color image
      PSET (x%, y% + 64), 255 ' to mask image
    END IF
  NEXT x%
NEXT y%

' capture each character individually
FOR y% = 0 TO 7
  FOR x% = 0 TO 15
    GET (x% * 8, y% * 8)-STEP(7, 7), gfxCar(carOfs%) ' colors
    GET (x% * 8, y% * 8 + 64)-STEP(7, 7), gfxCar(carOfs% + imgMask) ' mask
    carOfs% = carOfs% + imgSize
  NEXT x%
NEXT y%

LOCATE 10, 1: PRINT "Done. Press any key to continue..."
SLEEP: CLS

' display a test string on screen.
PAINT(0, 0), 7
TestString$ = "Now, this is EXTRA FANCY!"
FOR i% = 1 TO LEN(TestString$)
  carOfs% = ASC(MID$(TestString$, i%, 1)) * imgSize
  PUT (8 + (i% - 1) * 8, 32), gfxCar(carOfs% + imgMask), AND ' mask
  PUT (8 + (i% - 1) * 8, 32), gfxCar(carOfs%), OR ' colors
NEXT i%

SLEEP: END

This is for mode 13... in mode 7 (which only has 16 colors,) the mask will use index 15 instead of 255 for the see-through pixels, otherwise the technique is identical: create a mask, PUT it on the screen with AND, then create a color image and PUT it at the same location with OR.

Save it, load it!

QuickBASIC has two build-in instructions designed to save and load huge chunks of memory. And it's very fast! More often than not, these instructions are used to save image data stored in arrays. They both require to change the default memory segment and also demand the memory offset to the source (or target) buffer. We can save an image:

' set mode 0x13 (320x200, 8 bits per plane, 1 plane)
SCREEN 13

' reserve memory in form of an INTEGER array
DIM buffer(129) AS INTEGER

' draw something on the screen
CIRCLE (7, 7), 7, 14
PAINT (7, 7), 14
PSET (5, 4), 0: PSET (9, 4), 0
PSET (5, 5), 0: PSET (9, 5), 0
CIRCLE (7, 7), 4, 0, 4.7

' capture the 16x16 image
GET (0, 0)-STEP(15, 15), buffer

' jump to the array's memory segment...
DEF SEG = VARSEG(array(0))
' ...then save 260 bytes of memory starting at the offset provided by VARPTR into "DUMMY.BSV"
BSAVE "DUMMY.BSV", VARPTR(array(0)), 260

PRINT "Saved"

SLEEP: END

And then, we can reload it:

' set mode 0x13 (320x200, 8 bits per plane, 1 plane)
SCREEN 13

' reserve memory in form of an INTEGER array
DIM buffer(129) AS INTEGER

' jump to the array's memory segment...
DEF SEG = VARSEG(array(0))
' ...and load the content of "DUMMY.BSV" into memory, starting at the offset provided by VARPTR
BLOAD "DUMMY.BSV", VARPTR(array(0))

' display on screen
PUT (0, 0), buffer, PSET

SLEEP: END

You'll notice that we don't need to tell BLOAD how many bytes of information must to be recalled: the file contains a short header that includes the number of bytes we saved. Interestingly, it is possible to append data at the end of the file without corrupting anything...

Well, that's about it I think. Unless you want to dissect the content of the image buffer... I know you do. Come on. It's going to be fun.

Sweet Jesus, Pooh! That's not honey! That's 1 bit per plane, 4 planes!

It was a jest... and yet here we are; looking at the BSV format for kicks and giggles. Well BSV is a memory dump, so it's more the internal pixel organization for graphic modes rather than something specific to the BSV format. That said, the stuff is somewhat straightforward... somewhat.

The first byte contains a magic number to identify the file as a BSV format. It must be &hFD. Then we have two integers for the memory segment and memory offset where the data was copied from. Then we have another integer (I suspect it is meant to be unsigned, so some trickery will be required here: read two bytes as a string and convert to a long integer with CVL()) that contains the number of bytes copied from the buffer.

And now back on topic: the actual image buffer (the thing you obtain when you GET an image from video memory.) The first integer is the width (in pixels) of the image multiplied by the number of bits per plane (check the table in earlier chapters.) The next integer is the height (in pixels) of the image. The rest is of course the pixel data.

Maybe, for some reason, you need to convert BSV files captured in one mode to another mode and you may not be certain how to proceed. The easiest way is to PUT the graphic in its native mode, then POINT each pixel and write their value into another file. That's the easy way. Uncle Mike like to do things dirty, so we'll just get in there and...

DIM magic AS STRING *1, memSeg AS INTEGER, memOfs AS INTEGER
DIM memLen AS INTEGER, imgW AS INTEGER, imgH AS INTEGER
DIM planeOfs AS INTEGER, planeLen AS INTEGER
DIM pixelsPerByte AS INTEGER, pxlData AS INTEGER, oneByte AS STRING * 1
DIM bpp AS INTEGER, numPlanes AS INTEGER

' While it's possible to check if the data is valid under a certain mode,
' it's not possible to guess what mode was used to generate the BSV file.
' Here's, we'll assume it was made in mode 7
bpp = 1       ' One bit per plane
numPlanes = 4 ' Four planes

' Enter mode 13, that's more colors than we'll need (most of the time.)
SCREEN 13

' Open file. No check! I assume it's a proper mode 7 graphic file.
OPEN "MYFILE.BSV" FOR BINARY AS #1
GET #1, , magic
GET #1, , memSeg
GET #1, , memOfs
GET #1, , memLen
GET #1, , imgW
GET #1, , imgH

imgW = imgW \ bpp       ' Image width divided by the number of bits per plane (see table)
planeLen = 2 ^ bpp      ' Length of a plane
pixelsPerByte = 8 \ bpp ' Pixels stored in each byte

' That's a given: loop through each row
FOR y% = 0 TO imgH - 1
  ' Loop through each plane (find numPlanes in the table)
  FOR p% = 0 TO numPlanes - 1
    ' Value written for each bit set on this plane
    planeOfs = 2 ^ p%
    ' Read as many bytes as we need to complete a row
    FOR x% = 0 TO imgW - 1 STEP pixelsPerByte
      GET #1, , oneByte
      pxlData = ASC(oneByte)
      ' Read all bits within the byte, distribute value to pixels
      FOR b% = pixelsPerByte - 1 TO 0 STEP - 1
        ' Planes are layered on top of one another, hence the POINT and OR
        PSET (x% + b%, y%), POINT(x% + b%, y%) OR ((pxlData AND (planeLen - 1)) * planeOfs)
        ' Push aside the bits we read to obtain the bits we need.
        pxlData = pxlData \ planeLen
      NEXT b%
    NEXT x%
  NEXT p%
NEXT y%

' And that's it.
CLOSE #1

Well, that was unexpected. The code above can be used in Mode 13 to convert any image buffer from any graphic mode. I picked Mode 13 because it's the easiest way to visualize the buffer content and understand how planes work. Just add a SLEEP instruction near the end of the "b" or "p" loop... Of course, you don't have to enter Mode 13: it's entirely possible to reserve another buffer elsewhere and dump the decoded information there.

By the way, thanks to this silly endeavor, I noticed that Microsoft may have made a mistake in their documentation (or was it IBM?) During testing, the code perfectly decoded images from mode 1, 2, 7, 10, 11, and 13. And yet, mode 9 failed again and again. Then I realized that if Mode 9 has 2 bits per plane and 4 planes (as the QuickBASIC documentation claims,) it would be 256 colors, not 16. After testing a few combinations, I came to the conclusion that modes 8, 12 and 9 work much like Mode 7 (1 bit per plane, 4 planes.)

The more you know.