Skip to content

Save, Draw and Restore Amiga Bobs

Move word-aligned blitter objects over a persistent background. Reusable chip-RAM save, masked drawing and restore routines with a complete two-object ADF caller.

These objects are drawn into the playfield by the blitter. Moving one means putting its previous background back, then saving and drawing at the new address. The second object overlaps it, so the order matters too.

Two original Amiga blitter objects over a patterned background

Original Code198x A500 caller captured in Emu198x. It uses one bitplane and word-aligned motion.

Code status: maintained routines, executed on the A500 OCS PAL/Kickstart1.3 configuration. Execution record. The shared lifecycle explains restoration order across machines. No original-hardware or whole-game frame-budget claim is made.

Include it and provide the buffers

Include bob.inc once. Each live object needs its previous rectangle address and a packed background buffer. The module allocates no save slots.

For all three calls, D0 is width in 16-bit words, D1 is height, and D2 is the screen stride in bytes. Zero width or height does nothing. Otherwise width is 1–64, height 1–1024, and the even stride must be at least 2×width and at most 32766. Every accessed address must be even, in owned chip RAM and within its valid allocation; buffers must not overlap code, stack, sources or each other.

Routine Pointers
bob_save A0=screen rectangle, A1=packed save buffer
bob_restore A0=packed save buffer, A1=screen rectangle at its old position
bob_draw A0=opaque mask, A1=image, A2=screen rectangle

Packed source/save rows contain 2×width bytes. The mask uses 1 for the image and 0 for the background, unlike the Spectrum/BBC keep-mask convention. Images and masks are separate sources, not interleaved byte pairs.

D0, D3–D5, A5 and flags change; D1/D2/A0–A2 survive. Reload D0 before the next operation rather than chaining calls on its previous value. Each non-empty operation waits before programming the blitter and before returning. The caller enables DMA and excludes other blitter users; separate buffers do not make concurrent calls safe.

Synchronous save, masked draw and restoration

bob.inc
; SPDX-License-Identifier: MIT
; Synchronous OCS word-aligned save/draw/restore. No static state.
; D0=width words 0..64, D1=height 0..1024, D2=screen row stride bytes.
; Zero width/height no-op. For other sizes: stride even, 2*width<=stride<=32766.
; Every pointer even, every byte touched in owned chip RAM; buffers do not
; overlap code/stack or one another. Source/saved rows are packed (2*width).
; All routines change D0,D3-D5,A5,CCR; D1,D2,A0-A2 survive.
; Caller owns/enables blitter DMA and excludes other blitter users until return.
; Each operation waits before programming AND before return. No shifts/clipping.
; Save: A0=screen rectangle, A1=packed save buffer.
bob_save:
    tst.w d0
    beq _bob_return
    tst.w d1
    beq _bob_return
    move.w d2,d3
    sub.w d0,d3
    sub.w d0,d3
    moveq #0,d4
    bra _bob_copy
; Restore: A0=packed save buffer, A1=screen rectangle.
bob_restore:
    tst.w d0
    beq _bob_return
    tst.w d1
    beq _bob_return
    moveq #0,d3
    move.w d2,d4
    sub.w d0,d4
    sub.w d0,d4
_bob_copy:
    lea $dff000,a5
    bsr _bob_wait
    move.w #$09f0,$040(a5)
    move.w #0,$042(a5)
    move.w #$ffff,$044(a5)
    move.w #$ffff,$046(a5)
    move.w d3,$064(a5)
    move.w d4,$066(a5)
    move.l a0,$050(a5)
    move.l a1,$054(a5)
    bra _bob_start
; Draw: A0=opaque mask (1=image), A1=image, A2=screen rectangle.
; D = (A AND B) OR (NOT A AND C), minterm $CA. No image-data restriction on
; transparent bits: A chooses whether B or the old C appears.
bob_draw:
    tst.w d0
    beq _bob_return
    tst.w d1
    beq _bob_return
    lea $dff000,a5
    bsr _bob_wait
    move.w #$0fca,$040(a5)
    move.w #0,$042(a5)
    move.w #$ffff,$044(a5)
    move.w #$ffff,$046(a5)
    move.w #0,$064(a5)
    move.w #0,$062(a5)
    move.w d2,d3
    sub.w d0,d3
    sub.w d0,d3
    move.w d3,$060(a5)
    move.w d3,$066(a5)
    move.l a0,$050(a5)
    move.l a1,$04c(a5)
    move.l a2,$048(a5)
    move.l a2,$054(a5)
_bob_start:
    move.w d1,d5
    lsl.w #6,d5
    and.w #63,d0
    or.w d0,d5
    move.w d5,$058(a5)
_bob_wait:
    btst #6,$002(a5)
_bob_busy:
    btst #6,$002(a5)
    bne.s _bob_busy
_bob_return:
    rts

The copy path transfers A to D with minterm $F0. Drawing uses A=mask, B=image, C=old screen and D=screen with $CA. Modulos bridge packed rows and the screen stride. Software zero-size checks happen before encoding the blitter’s maximum-size zero fields. See the Amiga Hardware Reference Manual, chapter 6, for channels, modulos and size encoding.

A complete two-object caller

make creates build/demo.adf. Boot it on the named A500 configuration. The caller owns the custom chips, copper list, bitplane and chip-RAM assets, uses the existing VERTB polling wait, and resets to leave. It is a takeover demo; integration with AmigaOS needs its blitter ownership protocol instead of this startup.

A and B are 32×16 objects. Main restores old B, then old A, changes A’s position, saves/draws A, then saves/draws B. Every call reloads the geometry. Movement is in 16-pixel steps because this version is word aligned.

Original ADF caller with two overlapping bobs

demo.asm
; A500 OCS PAL/KS1.3, all data in chip RAM. Owns chips; reset to leave.
    section code,code_c
start:
    lea $dff000,a5
    move.w #$7fff,$09a(a5)
    move.w #$7fff,$09c(a5)
    move.w #$7fff,$096(a5)
    lea plane,a0
    move.w #255,d7
paint_row:
    move.w #19,d6
paint_word:
    move.w d7,d0
    and.w #7,d0
    bne.s paint_zero
    move.w #$5555,d0
paint_zero:
    move.w d0,(a0)+
    dbra d6,paint_word
    dbra d7,paint_row
    lea plane,a0
    move.l a0,d0
    lea copbpl,a1
    move.w d0,6(a1)
    swap d0
    move.w d0,2(a1)
    lea copperlist,a0
    move.l a0,$080(a5)
    move.w #0,$088(a5)
    move.w #$83c0,$096(a5)
    bsr draw_objects
    move.w #$cafe,ready
main:
    bsr wait_frame
    move.l old_b,a1
    lea under_b,a0
    bsr geometry
    bsr bob_restore
    move.l old_a,a1
    lea under_a,a0
    bsr geometry
    bsr bob_restore
    addq.w #1,tick
    move.w tick,d0
    and.w #3,d0
    bne.s no_move
    addq.w #2,column
    cmp.w #32,column
    bcs.s no_move
    move.w #4,column
no_move:
    bsr draw_objects
    addq.w #1,updates
    bra main
geometry:
    moveq #2,d0
    moveq #16,d1
    moveq #40,d2
    rts
draw_objects:
    lea plane,a0
    adda.l #104*40,a0
    adda.w column,a0
    move.l a0,old_a
    lea under_a,a1
    bsr geometry
    bsr bob_save
    lea mask,a0
    lea image_a,a1
    move.l old_a,a2
    bsr geometry
    bsr bob_draw
    lea plane+112*40+16,a0
    move.l a0,old_b
    lea under_b,a1
    bsr geometry
    bsr bob_save
    lea mask,a0
    lea image_b,a1
    move.l old_b,a2
    bsr geometry
    bsr bob_draw
    rts
signature: dc.b "198XLIFECYCLE!",0
    even
ready: dc.w 0
updates: dc.w 0
tick: dc.w 0
column: dc.w 4
old_a: dc.l 0
old_b: dc.l 0
    include "bob.inc"
    include "shapes.inc"
    include "../../framework/game-loop/frame.inc"
    even
copperlist:
    dc.w $08e,$2c81,$090,$2cc1,$092,$0038,$094,$00d0
    dc.w $100,$1200,$102,0,$104,0,$108,0,$10a,0
    dc.w $180,$012,$182,$6ed
copbpl:
    dc.w $0e0,0,$0e2,0
    dc.w $ffff,$fffe
under_a: dcb.b 64,0
under_b: dcb.b 64,0
plane: dcb.b 10240,0

What is qualified, and what comes next

Checks cover empty operations, ordinary rectangles, different strides, width 64 and height 1024 as separate small-memory cases, surrounding guards, packed saves and independent expected restoration. A two-object fixture compares the full composed and restored region and checks unchanged image/mask sources. Broken-mask and wrong-order variants must fail those same comparisons. The complete displayed caller progresses separately.

The normal screen example is one bitplane. A colour bob needs a save/draw/restore operation and saved background per changed plane; the silhouette mask can be shared. This is extra work and storage, not a colour flag you can turn on for free.

There is no clipping, arbitrary-X shift or asynchronous queue here. The existing cookie-cut listing explores shifting and remains illustrative. Saved backgrounds also become stale if scenery changes: remove objects before updating or scrolling their background, or redraw dirty tiles from world state. See the shared lifecycle before expanding the object count.

When an object can leave the screen, clip its visible rectangle before saving or drawing it. Keep its old clipped address and dimensions for the next restoration.