Example 1
Screen image compressor

 

What we shall do here is to jump straight in with an example program. Something that you will find useful (I mean - what is the point of writing "Hello World" a few thousand times? When you learn to code it is much better to learn on useful projects).

This project is a 16-colour screen mode compressor. It can be adapted to other modes, but I'll leave that as an exercise for the advanced. After all - ChangeFSI does a marvellous job of reducing the number of colours in an image, and for demos with 'real life screenshots', 16 colours is usually adequate (unless you are plugging the latest 16 million colour game you wrote - in which case you won't need this utility!).

Average savings depend on the image. It does badly with dithered images, it does well with images that don't have loads of fiddly things (a screenshot of a program running in an untextured Desktop, for example). It has been designed to work with MODE 12 and MODE 35. For others, it is a case of suck it and see - but there is no real reason why other 16 colour MODEs would not work.

Right. Well... In the old days when it was safe to leave your car unlocked and your daughter unchained, screen memory started at &2000000 and worked upwards. You needed to know a few modes such as 12 and 13 and 15... Like, who do you know that uses MODE 8?

Modern life, however, is not so simple. We would be foolish to assume the screen start position and/or size. We would also be foolish to assume the 'default' palette. So all of this information has to be examined.
16 colour modes use 4bpp, or 4 bits per pixel. Thus, one byte equals two pixels.

The compression system is appropriately crude. We will work on a "better" scheme later. However for now we shall be using a kind of Run-Length-Encoding. We treat the screen memory as a sequence of bytes, and we encode it into a set of byte counts and byte values. For example, assume screen memory is:

111111111142351111121111111222222222222222111111111111

This would be a bunch of 1's followed by a 4, a 2, a 3, a 5, more ones... Which becomes:
A114121315511271F2C1

This has reduced the stream to approximately a third of original size. The "A1" means 10 x 1 (A is 10 in base 16). Then 1 x 4, 1 x 2, 1 x 3, 1 x 5, 5 x 1, 1 x 2, 7 x 1, 15 x 2 and finally 12 x 1.

If you don't get it, study the above for a few moments. It is quite simple once you understand. By way of example, 11112246885788 becomes 4122141628151728; and you can see how it becomes inefficient with oft changing bytes (such as dithered images), the output is two bytes larger than the input.

 

 

Our format is to be defined as:

   Header:  "BudgieScrn (compressed 1)"+<newline>
            which is 26 bytes.

   Mode:    1 byte giving mode number.

   Palette: 16 entries, six bytes each. Giving red, green and
            blue values for first flash colour and second
            flash colour for each entry.

   Image:   Data as defined above.
We will leave the screen size validation up to the loader instead of encoding it into the image.

So... Let us begin:

REM >Ssaver BudgieSoft screen saver
REM V1.01 © 1997 Richard Murray
REM
REM Assembler programming example 1
REM Downloaded from: http://www.heyrick.co.uk/assembler/

:
ON ERROR PRINT REPORT$+" at "+STR$(ERL/10) : END
:
DIM code% &400
FOR pass% = 4 TO 6 STEP 2
  P%=0
  O%=code%
  [ OPT pass%
This is a fairly standard beginning. The thing to note is that we are using offset assembly. In this way, our code is compiled as a transient utility instead of an "absolute" application. It should also be loadable. The divide-by-ten in the error message is so that 'real lines' match up to !Edit lines, so if you get an error just press F5 and enter that number.
    MOV    R7, R14
We now 'save' the return address in register 7. This is so that it does not get overwritten by BL (Branch with Loop) stuff. Note, if you want to fiddle with the code - remember register 7 is special.
    MOV    R0, #&8C
    SWI    "XOS_Find"
    BVS    exit
    MOV    R6, R0
Our program is designed to be called as "SSaver <filename>". Conveniently the OS_Find call (opens/closes files) has a flag to look for the filename in the entry parameters. So this stuff attempts to open our file with write access. If it fails the V flag (oVerflow) is set. The BVS command (Branch if V Set) will carry us out to a safe exit. Otherwise we can assume all went well, and move our file handle into register 6 for safe keeping.

By now we have our file open and are ready to start building up our image data.

    MOV    R0, #2
    MOV    R1, R6
    ADR    R2, header_text
    MOV    R3, #26
    SWI    "XOS_GBPB"
    BVS    error
This sets up a call to OS_GBPB to write the header; which is defined at the end as ".header_text". The BVS takes us to an error handler in case we don't have write-access after all!
    MOV    R0, #&87
    SWI    "XOS_Byte"
    MOV    R0, R2
    MOV    R1, R6
    BL     write_byte
If you were to look up OS_Byte &87 in a manual, you might wonder about my sanity. After all - what relevance could "get character at cursor position" have?

Frankly - none. :-) However in register 2 it returns the current MODE number, and THAT is what we are looking for. We then set up a call to a routine that outputs a byte for us, defined later as ".write_byte".

    MOV    R5, #0
  .col_loop
    MOV    R0, R5
    MOV    R1, #16
    SWI    "OS_ReadPalette"
    MOV    R1, R6
    MOV    R0, R2, LSR #8    ; red 1st
    BL     write_byte
    MOV    R0, R2, LSR #16   ; green 1st
    BL     write_byte
    MOV    R0, R2, LSR #24   ; blue 1st
    BL     write_byte
    MOV    R0, R3, LSR #8    ; red 2nd
    BL     write_byte
    MOV    R0, R3, LSR #16   ; green 2nd
    BL     write_byte
    MOV    R0, R3, LSR #24   ; blue 2nd
    BL     write_byte
    ADD    R5, R5, #1
    CMP    R5, #16
    BLT    col_loop
This looks complicated and I'm not going to explain it to verbosely. However what we are trying to do here is to read the palette (care of OS_ReadPalette). It returns two registers for the six colour values. As we are using 32 bit words, we can cram several 8 bit values into one word. Hence the LSR, which is a Logical Shift Right. LSR takes a value (say %110110100) and shifts it (say two places) to produce an output (such as %001101101). We loop through the 16 colours and extract the six values for each, writing them to the file as we are going along.

The header is now complete.

    ADR    R0, vdu_block
    ADD    R1, R0, #12
    SWI    "OS_ReadVduVariables"
    LDR    R2, [R1]
    LDR    R3, [R1, #4]
    ADD    R3, R3, R2
    SWI    "XOS_RemoveCursors"
Even though we are not going to write the screen dimensions to the file; we do need to know where the screen starts and ends. This calls "OS_ReadVduVariables" asking for the screen base address and the size of the screen. Adding the two gives us our result, with the data held in the memory area defined later as ".vdu_block". While we are at it, we switch the cursor off.
    MOV    R1, R6
  .main_loop
    LDRB   R0, [R2]
    BL     write_byte
    MOV    R4, #1
  .loop2
    LDRB   R5, [R2, #1]!
    ADD    R4, R4, #1
    CMP    R5, R0
    BNE    skip
    CMP    R4, #&FF
    BLT    loop2
  .skip
    SUB    R0, R4, #1
    BL     write_byte
    CMP    R2, R3
    BLT    main_loop
Here is the main loop. What it does is it outputs the value of the current byte (ie: the colour) and then it reads through the memory looking to see how long it continues for. As a byte can only hold 256 different values, it loops up to a maximum of 255. Once a value has been established, it will write it out to the file and then branch back to the main loop; repeating until the end of screen memory is reached.
    MOV    R0, #&20
    BL     write_byte
    MOV    R0, #&20
    BL     write_byte
A really early version of this software messed up by trying to read a little beyond the end of the screen (and hence the end of the file), so it became practice to add two spaces to the end of the file - just in case - even though the <cough>bug</cough> was fixed.
    MOV    R0, #0
    MOV    R1, R6
    SWI    "XOS_Find"
    SWI    "XOS_RestoreCursors"
This wraps up, closing our file and restoring the cursor. It falls through to ".exit" which follows next.
  .exit
    MOV    PC, R7
Leave the program - places stored return address (in register 7) into the Program Counter (register 15).
  .write_byte
    SWI    "XOS_BPut"
    MOVVC  PC, R14
Simple routine to write a byte of data. Not terribly efficient, however you could change it to move the file handle here instead of beforehand (using MOV R1, R6). It returns by way of function returning (move return address into Program Counter); but only if the V flag is clear. If something went wrong, however, then we fall through to ".error"...
  .error
    MOV    R2, R0
    MOV    R1, R6
    MOV    R0, #0
    SWI    "XOS_Find"
    SWI    "XOS_RestoreCursors"
    MOV    R0, R2
    ORR    PC, R7, #1<<28
This closes the file, restores the cursor and exits with the V flag set (error condition). It is the 'standard' error handler for this program.
This is not 32-bit compliant.
  .vdu_block
    EQUD   149
    EQUD   7
    EQUD   -1
    EQUD   0
    EQUD   0
This is the block for the OS_ReadVduVariables command. The 149 and 7 are the codes for the values we wish to read. The -1 terminates the list. The two 0's are where the information is placed by the OS.
  .header_text
    EQUS   "BudgieScrn (compressed 1)"
    EQUB   10
    EQUB   0
    ALIGN
This is the header.
  ]
NEXT
:
OSCLI("Save <Obey$Dir>.Saver "+STR$~(code%)+" "+STR$~(O%))
OSCLI("SetType <Obey$Dir>.Saver &FFC")
:
END
And finally the denouement. Close the loop, save the file and exit.

There you have it!

To use it, try an Obey file similar to:

| Obey file to run SSaver
|
ScreenLoad <Obey$Dir>.mypiccy
|
<Obey$Dir>.SSaver <Obey$Dir>.comp_piccy
In the next section, we shall create a loader. Before looking at that, why not see if you can fiddle with the above to make it work in reverse?
Download example: ssaver.basic


Return to assembler index
Copyright © 1999 Richard Murray