Skip to content

Separate Background Buffers for Overlapping Objects

Extend BBC MODE4 masked drawing beyond one save slot. Each object gets its own background buffer and old cell address, with a running two-object caller.

One object can use one saved cell. A second object needs its own saved bytes, particularly when the two occupy the same cell. These helpers take an explicit buffer pointer while reusing the existing masked drawing routine.

Two original BBC MODE4 software objects moving through a patterned strip

Original Code198x program captured in Emu198x. Objects are 8×8 and byte aligned.

Code status: maintained helpers and a running BBC Model B MOS1.20/BASICII caller. Execution record. See the shared lifecycle for the same restoration problem on Spectrum and Amiga.

Select the cell and its buffer

Include background.inc with the existing masked-sprites/masked.inc. screen_ptr (&70/&71) selects a complete byte-aligned character cell in the ordinary MODE4 bitmap. back_ptr (&74/&75) selects eight caller-owned saved bytes, separate from the bitmap, source, stack, code and other live buffers. pairs_ptr (&72/&73) still selects the interleaved keep-mask/image data for draw_masked_cell.

A/Y/flags change; X survives. IRQ code must preserve the working registers and owned zero-page. Separate buffers support sequential objects; shared pointer bytes still require exclusive use.

Copy a MODE4 cell to and from an explicit buffer

background.inc
; SPDX-License-Identifier: MIT
; Reuse screen_ptr=$70, pairs_ptr=$72 and draw_masked_cell from masked.inc.
; back_ptr=$74/$75 selects EIGHT caller-owned bytes, one buffer per live object.
; Whole MODE4 character cell, byte aligned. Buffer must not overlap bitmap,
; source, code or stack. Save/restore A/Y/flags change; X survives.
; No static save slot: caller retains both old screen address and buffer pointer.
back_ptr=$74
save_cell_to_buffer:
    ldy #7
save_cell_byte:
    lda (screen_ptr),y
    sta (back_ptr),y
    dey
    bpl save_cell_byte
    rts
restore_cell_from_buffer:
    ldy #7
restore_cell_byte:
    lda (back_ptr),y
    sta (screen_ptr),y
    dey
    bpl restore_cell_byte
    rts

The helpers remember neither an address nor an object. Keep the old cell address with the corresponding buffer. Restore at that address before selecting the new one.

Assembly
    ; Select A's cell and under_a buffer.
    jsr save_cell_to_buffer
    jsr draw_masked_cell
    ; Select B and under_b, then save/draw B.
    ; Next update: restore OLD B, then OLD A; only then choose new positions.

The earlier single-object entry keeps its original interface. Its one global save slot is not used by this caller; its masked drawing routine is. The unused single-slot helpers remain in that include, a small code/state cost of keeping the existing component intact.

Run two objects together

make produces build/loader.bas. Load or type it and RUN. The loader selects MODE4, reserves HIMEM=&3000 and installs the original code. MOS IRQ remains active; OSBYTE19 supplies vertical sync. Reset to leave.

The moving object crosses the fixed object’s cell. Save/draw A then B, restore B then A. When they share a cell, B’s buffer contains part of A; restoring in the opposite order brings those pixels back.

Two cells, two save buffers and explicit old addresses

demo.asm
*=$3000
OSBYTE=$fff4
start:
    cld
    cli
    lda #$6c
    sta screen_ptr+1
    lda #0
    sta screen_ptr
    ldy #0
paint:
    tya
    and #7
    bne paint_ready
    lda #$55
paint_ready:
    sta (screen_ptr),y
    iny
    bne paint
    jsr draw_objects
main:
    lda #19
    jsr OSBYTE
    inc tick
    lda tick
    and #7
    bne main
    ; Reverse draw order; restore at OLD addresses before moving either object.
    lda #$80
    sta screen_ptr
    lda #$6c
    sta screen_ptr+1
    lda #<under_b
    sta back_ptr
    lda #>under_b
    sta back_ptr+1
    jsr restore_cell_from_buffer
    lda old_a
    sta screen_ptr
    lda #<under_a
    sta back_ptr
    lda #>under_a
    sta back_ptr+1
    jsr restore_cell_from_buffer
    lda column
    clc
    adc #8
    cmp #$c0
    bcc next_column
    lda #$40
next_column:
    sta column
    jsr draw_objects
    inc updates
    jmp main
draw_objects:
    lda column
    sta old_a
    sta screen_ptr
    lda #$6c
    sta screen_ptr+1
    lda #<under_a
    sta back_ptr
    lda #>under_a
    sta back_ptr+1
    jsr save_cell_to_buffer
    lda #<shape_a
    sta pairs_ptr
    lda #>shape_a
    sta pairs_ptr+1
    jsr draw_masked_cell
    lda #$80
    sta screen_ptr
    lda #<under_b
    sta back_ptr
    lda #>under_b
    sta back_ptr+1
    jsr save_cell_to_buffer
    lda #<shape_b
    sta pairs_ptr
    lda #>shape_b
    sta pairs_ptr+1
    jsr draw_masked_cell
    rts
column: !byte $40
old_a: !byte 0
tick: !byte 0
updates: !byte 0
under_a: !fill 8,0
under_b: !fill 8,0
shape_a: !byte $e7,$18,$c3,$3c,$81,$7e,$24,$db,0,$ff,$a5,$5a,$db,$24,$ff,0
shape_b: !byte $81,$7e,$bd,$42,$bd,$42,$81,$7e,$e7,$18,$e7,$18,$e7,$18,$c3,$3c
!source "../masked-sprites/masked.inc"
!source "background.inc"

Three independent composition/restoration cases cover shared cells, adjacent cells and the bitmap end. The wrong-order version must fail the same byte comparison, and image sources must survive unchanged. A separate displayed caller check observes progression. This is not an original-hardware or deadline qualification.

The component remains 8×8 and byte aligned. It does not clip or translate a CRTC-scrolled screen. Different modes and arbitrary pixel positions need further implementations. A changing background also makes saved bytes stale: remove objects before changing the scenery, or rebuild damaged tiles from the world. The shared lifecycle explains those choices.

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.