Inside geoWrite – 2: Screen Recovery

In the series about the internals of the geoWrite WYSIWYG text editor for the C64, this article discusses how the app manages to extend its usable RAM by 5 KB using a custom screen recovery solution.

Article Series

  1. The Overlay System
  2. Screen Recovery ← this article
  3. Font Management
  4. Zero Page
  5. Copy Protection
  6. Localization
  7. File Format and Pagination

Screen Recovery

Any graphical user interface that allows overlapping items has to deal with the problem of recovery: When dialogs and pull-down menus are shown, they cover parts of the screen, and when they are dismissed, the underlying UI needs to be revealed again.

There are two basic approaches to this:

  1. When the dialog or menu is dismissed, the system calls the same text or image rendering code again that drew it in the first place.

  2. Before the dialog or menu is drawn, the system saves the pixel data that will be overwritten, and after the item is dismissed, the saved pixels get written back onto the screen.

The latter solution requires additional memory, but is a lot faster and doesn’t create a potentially jarring redraw.

Screen Recovery in GEOS

GEOS uses the “restore the pixel data” approach, but with a twist.

Let’s look at the GEOS memory layout again:

-----------------------------------------  
$0000  Zero page, stack, system variables  
-----------------------------------------  
$0400




       Application memory





-----------------------------------------  
$6000  
       Background bitmap

-----------------------------------------  
$8000  System buffers and variables  
-----------------------------------------  
$9000  Disk driver  
-----------------------------------------  
$A000  
       Screen bitmap

-----------------------------------------  
$C000  GEOS KERNAL


-----------------------------------------

The 8 KB of 320×200 monochrome bitmap data resides at $A000-$BF40. In addition, GEOS statically reserves a whole 8 KB just for screen recovery: The “background bitmap” at $6000-$7F40 allows GEOS to recover the whole screen area.

 Imprint and Recover

This background bitmap is really just another (invisible) framebuffer with the same layout as the actual screen bitmap. When saving pixels, they get copied from the screen (“foreground”) bitmap to the same offset in the background bitmap and vice versa.

These two functions are the core of the API:

  • ImprintRectangle – copy rectangle from fg bitmap to bg bitmap
  • RecoverRectangle – copy rectangle from bg bitmap to fg bitmap

While applications are free to use the API this way, the system-suggested way is actually different.

Display Buffering

When using the core GEOS API, it’s not actually necessary to save the pixels (ImprintRectangle) before overwriting them. The background buffer already contains a copy!

That’s because all graphics code can be configured to either draw to the foreground bitmap, the background bitmap, or both.

To calculate the offset in the bitmap of a y coordinate, all internal drawing code calls GetScanLine. This function usually returns two pointers in virtual registers r5 and r6:

  • r5: pointer to the pixel line in the foreground screen
  • r6: pointer to the pixel line in the background screen

Any code in the GEOS drawing library then stores the pixel data in both the locations pointed to by r5 and r6:

    [...]  
    sta (r5),y  
    sta (r6),y

For as little as an extra 6 clock cycles for every byte to be stored (which is 8 pixels), the drawing code stores it into both bitmaps, so there is usually no need to copy data from the foreground to the background bitmap.

Now when the system draws a menu or a dialog, it only draws it into the foreground buffer. That’s because the GetScanLine call can actually return different kinds of pointers depending on the global system variable dispBufferOn:

dispBufferOn r5 r6
%11000000 (default) fg screen ptr bg screen ptr
%10000000 fg screen ptr fg screen ptr
%01000000 bg screen ptr bg screen ptr

By only setting one of bits 6 and 7, all drawing code can effectively be instructed to only draw to the foreground or the background bitmap. In this case, the two sta instructions will just store the pixel data to the same location twice.

The system default is to draw everything to both bitmaps – except menus and dialogs, which are only ever drawn to the foreground bitmap. There is therefore no need to call ImprintRectangle: All that the built-in code has to do is call RecoverRectangle on the rectangle that it overwrote.

Screen Recovery in geoWrite

geoWrite is a very complex application that is very tight on memory, so it was designed to reclaim as much as possible of the 8 KB normally used for screen recovery. Text rendering is very slow, so redrawing the page as a recovery strategy is out of the question.

Instead, geoWrite stores the saved pixels more efficiently: The buffer only really needs to be as big as is required for the largest rectangle ever saved while in the app. And for geoWrite, that’s dialogs, which are 200×104 pixels. So that’s 2600 bytes instead of 8000 bytes.

The code to save a screen rectangle is called with the following arguments in virtual registers:

  • r1: the pointer to the recovery buffer
  • r2L: x ÷ 8
  • r2H: y
  • r3L: width ÷ 8
  • r3H: height

X coordinates have to be divisible by 8, so that the pixels are at byte boundaries.

The Code

The following is the main code, slightly edited for readability. It iterates over all lines of the rectangle and, on save, appends the bitmap bytes to the array of saved data. On recover, it does the opposite.

@loop1: ldx     r2H         ; y coord  
        jsr     GetScanLine ; r5 := ptr to bitmap  
        lda     r2L         ; x coord / 8  
        asl     a  
        asl     a  
        asl     a           ; * 8  
        bcc     :+  
        inc     r5H  
:       tay  
        MoveB   r3L, r4L    ; copy byte count  
@loop2: bit     r4H         ; save or recover?  
        bpl     @1  
        jsr     @recv  
        bra     @2  
@1:     jsr     @save  
@2:     IncW    r1          ; advance buffer pointer  
        add     #8          ; account for quirky VIC-II memory layout  
        bcc     :+  
        inc     r5H  
:       tay  
        dec     r4L         ; dec byte counter  
        bne     @loop2      ; loop for horizontal bytes  
        inc     r2H  
        dec     r3H         ; dec line counter  
        bne     @loop1      ; loop for lines  
        rts

This is the subroutine for copying a byte from the screen to the buffer…

@save:  lda     (r5),y      ; read screen byte  
        tax  
        tya  
        pha                 ; save offset  
        ldy     #0  
        txa  
        sta     (r1),y      ; write into buffer  
        pla                 ; restore offset  
        rts

…and the code for copying a byte from the buffer to the screen:

@recv:  tya                 ; save offset  
        pha  
        ldy     #0  
        lda     (r1),y      ; read from buffer  
        tax  
        pla  
        tay                 ; restore offset  
        txa  
        sta     (r5),y      ; write onto screen  
        tya  
        rts

The Tables

geoWrite uses a table with the buffer pointer, x, y, width and height values for each use case, so only an offset within the table has to be passed:

;              r1L/r1H  r2L  r2H  r3L  r3H  
;                ptr      x    y wdth hght  
scrrecvtabs:  
scrrecvtab_geos:  
        .word   MEM_SCRREST  
        .byte             0,  15,  10, 128  
scrrecvtab_file:  
        .word   MEM_SCRREST  
        .byte             3,  15,   7, 100  
[...]

The first item in the table is for saving and restoring the rectangle under the “geos” menu, and so on.

MEM_SCRREST is the address of the start of the recover buffer. The buffer pointer isn’t always MEM_SCRREST though:

scrrecvtab_font:  
        .word   MEM_SCRREST  
        .byte            17,  15,  10, 114  
scrrecvtab_fontsize:  
        .word   MEM_SCRREST + 1408  
        .byte            26,  15,   9, 114

This is because the font menu has a sub-menu for the point size:

When the font menu is open, the rectangle covered by it is already saved in the buffer. The rectangle under the point size menu has to be saved in the area following the already occupied data.

Memory Layout

Conveniently, the RAM area for the background bitmap immediately follows the application’s memory. Apps are free to just not use the background bitmap at all. By setting dispBufferOn to %10000000 globally, the system will never touch the $6000-$7FFF area.

Here’s a mostly to scale overview of the geoWrite memory map compared to the GEOS default:

       GEOS Default                    geoWrite  
-------------------------      -------------------------  
$0400                                              $0400

                                     Main Code


       Application memory      -------------------------  
                                     Overlay Code  $3244  
                               -------------------------  
                                                   $4310  
                                     Page Data

-------------------------      -------------------------  
$6000                                Font Heap     $5E68  
       Background bitmap       _________________________  
                                     Recovery Data $75D8  
-------------------------      -------------------------

geoWrite puts the recovery buffer at the topmost 2600 bytes of the $0400-$8000 window that is available to the application if it doesn’t use the background screen.

The reason for this location is because the range $7900-$7F40 is where GEOS requires the printer driver to be loaded once the application wants to print. It overlaps the background screen on purpose: As long as the application is not printing, so no space for the driver is wasted. And once it is printing, it just can’t use the background buffer any more. The same is true in geoWrite’s model.

Triggers

Aside from setting dispBufferOn to just the foreground, using one’s own save/recover logic requires the app to make sure the save and recover code gets called at the right times.

For dialogs, this is easy: Apps have to explicitly show them by calling the doDlgBox API, so before calling it, geoWrite saves the respective rectangle, and once the API returns, it recovers it.

For menus, it’s more tricky. The application has to trigger on the open event of a sub-menu.

Usually, the data structure of a menu for the doMenu API contains pointers to sub-menu data structures, forming the complete menu tree, like this:

menu_main:  
        .byte   0, 14, 0, 191 ; pos & size  
        .byte   HORIZONTAL | UN_CONSTRAINED | 3 ; #items

        .word   txt_geos  ; string "geos"  
        .byte   SUB_MENU  ; =below is a sub-menu ptr  
        .word   menu_geos ; sub-menu data structure

        .word   txt_file  
        .byte   SUB_MENU  
        .word   menu_file

        .word   txt_edit  
        .byte   SUB_MENU  
        .word   menu_edit

This way, an application can describe a complete menu tree with just data structures and no code.

But instead of a pointer to a sub-menu (code SUB_MENU), the data structure can alternatively contain a pointer to code that returns a sub-menu data structure (code DYN_SUB_MENU):

        .word   txt_geos     ; string "geos"  
        .byte   DYN_SUB_MENU ; =below is a code ptr  
        .word   callbackGeos ; code, called on sub-menu open

In this example, whenever the “geos” item is clicked, the callbackGeos function is called, which saves the rectangle that will be covered by the “geos” sub-menu, and returns a pointer to the sub-menu to be presented (edited for clarity):

callbackGeos:  
        ldx     #scrrecvtab_geos-scrrecvtabs  
        stx     a2L  
        jsr     screenSave  
        LoadW   r0, menu_geos  
        rts

For the recovery of the rectangle, there is actually a GEOS system variable supporting this use case: If the application sets the RecoverVector pointer to anything but 0, closing a menu will call that code instead of doing its own RecoverRectangle-based recovery.

This is where it points in geoWrite (again edited for clarity):

appRecoverVector:  
        ldx     a2L  
        jsr     screenRecover  
        rts

The function reuses the previously set offset in the screen recovery table (in virtual register a2L) for buffer location, the origin and size of the rectangle.

Conclusion

Berkeley Softworks wrote the GEOS operating system as well as many major applications like geoWrite, geoPaint, geoPublish and geoCalc, yet they implemented two different methods for screen recovery: The system way of doing it wastes space, but is very simple to use. And the way they do it in the applications is very much tied to the specific use case, but very optimized.

1 thought on “Inside geoWrite – 2: Screen Recovery”

Leave a Comment