Skip to content

Draw a Bitmap Region on Acorn Atom

Redraw a room, changed patch or single tile with reusable 6502 code for Acorn Atom's 256×192 monochrome bitmap.

A door opens. Its map cell changes, and now its picture needs to change too. This routine takes a rectangle of byte-map IDs and paints their 8×8 pictures into the bitmap. Use it for one tile, a whole room or the strip of scenery revealed by a camera.

Acorn Atom native bitmap output: a small patterned region, an erased cell and an isolated bottom-right tile
Original diagnostic running in Emu198x 0.39.0. The 5×3 patch comes from a larger map; a single-cell erase and the display’s bottom-right corner are checked separately.

Make room for the renderer

Define the map, tile pictures, two scanline address tables and a 32-byte context, NR, in your own memory layout. The allocation constants shown here belong to the reset diagnostic; choose addresses which fit your project. The entire context must fit in zero page, and decimal mode must be clear.

The caller supplies the map, bitmap dimensions and private context

demo.asm, excerpt
NR=112
RESULT=768
MAP=1264
MAP_W=12
MAP_H=8
TILE_COUNT=4
CELL_BYTES=1
SCREEN_COLS=32
SCREEN_ROWS=24
X_STEP=1

MAP is a row-major byte map, twelve cells wide and eight high in this example. TILES contains four pictures, each with eight scanlines and 1 byte(s) per scanline. ID zero is an opaque empty picture, so drawing it erases a cell. It does not mean “skip this tile”.

The two tables, bitmap_lo and bitmap_hi, hold the low and high address byte for every pixel scanline. Keep the tables, tiles and map stable throughout a call. Include the shared 6502.inc routine once, after defining its symbols. The complete caller linked below supplies working tables and tile data as well as video setup.

Draw a rectangle

Set NR+0..5 to source column, source row, destination column, destination row, width and height. Call region_draw; NR+6 is one when it completes and zero when it rejects the request. All six request bytes survive.

Copy a 5×3 patch from map cell (2,1) to display cell (4,3)

demo.asm, excerpt
case_0:
    lda #2
    sta NR+0
    lda #1
    sta NR+1
    lda #4
    sta NR+2
    lda #3
    sta NR+3
    lda #5
    sta NR+4
    lda #3
    sta NR+5
    jsr region_draw
    lda NR+6
    ora #128
    sta RESULT+1

The extra result store belongs to the diagnostic. In a game, inspect NR+6 and continue with your own logic. No separate initialisation call is needed for the context scratch.

The routine rejects zero sizes, out-of-range starts, rectangles which overrun either edge and IDs outside the tile table. It checks every selected ID before painting the first byte. Bounds are checked by comparing the requested size with the space remaining, so byte arithmetic cannot wrap a large request into an apparently small one. This variant rejects rather than clips.

How this bitmap is laid out

The Atom’s highest monochrome mode gives you 256×192 pixels, or thirty-two by twenty-four 8×8 cells. It needs the full 6 KB graphics RAM at $8000–$97FF. The presence of the graphics commands in an unexpanded Atom does not supply that RAM.

Rows are linear: the next pixel scanline starts thirty-two bytes later. Each byte contains eight pixels, most significant bit on the left. The next cell is one byte across, so CELL_BYTES=1 and X_STEP=1.

The original driver configures the 8255 and selects mode 4 through $B000. That register also selects a keyboard row in its lower nibble. The drawing routine only writes bitmap RAM; your input and video setup must agree on the port value. The separate guard lives after the map because the address immediately below video RAM is unmapped.

The reusable routine

Checked bitmap-region drawing

6502.inc
; Caller defines NR (32 zero-page bytes), MAP/MAP_W/MAP_H, TILES,
; TILE_COUNT (1..16), CELL_BYTES (1/2), SCREEN_COLS/ROWS, X_STEP (1/2/8),
; and bitmap_lo/bitmap_hi (one address per pixel scanline).
; Decimal mode must be clear. A/X/Y/flags and NR+6..31 clobbered.
region_draw:
    lda #0
    sta NR+6
    lda NR+4
    bne bounds_nonzero_0
    rts
bounds_nonzero_0:
    lda NR+0
    cmp #MAP_W
    bcc bounds_start_0
    rts
bounds_start_0:
    lda #MAP_W
    sec
    sbc NR+0
    cmp NR+4
    bcs bounds_fit_0
    rts
bounds_fit_0:
    lda NR+5
    bne bounds_nonzero_1
    rts
bounds_nonzero_1:
    lda NR+1
    cmp #MAP_H
    bcc bounds_start_1
    rts
bounds_start_1:
    lda #MAP_H
    sec
    sbc NR+1
    cmp NR+5
    bcs bounds_fit_1
    rts
bounds_fit_1:
    lda NR+4
    bne bounds_nonzero_2
    rts
bounds_nonzero_2:
    lda NR+2
    cmp #SCREEN_COLS
    bcc bounds_start_2
    rts
bounds_start_2:
    lda #SCREEN_COLS
    sec
    sbc NR+2
    cmp NR+4
    bcs bounds_fit_2
    rts
bounds_fit_2:
    lda NR+5
    bne bounds_nonzero_3
    rts
bounds_nonzero_3:
    lda NR+3
    cmp #SCREEN_ROWS
    bcc bounds_start_3
    rts
bounds_start_3:
    lda #SCREEN_ROWS
    sec
    sbc NR+3
    cmp NR+5
    bcs bounds_fit_3
    rts
bounds_fit_3:
    lda #0
    sta NR+29
region_pass:
    lda #0
    sta NR+16
region_row:
    lda #0
    sta NR+17
region_cell:
    lda #<MAP
    sta NR+20
    lda #>MAP
    sta NR+21
    lda NR+1
    clc
    adc NR+16
    tax
    beq source_column
source_row:
    clc
    lda NR+20
    adc #MAP_W
    sta NR+20
    bcc source_no_carry
    inc NR+21
source_no_carry:
    dex
    bne source_row
source_column:
    lda NR
    clc
    adc NR+17
    tay
    lda (NR+20),y
    sta NR+19
    cmp #TILE_COUNT
    bcc region_id_ok
    rts
region_id_ok:
    lda NR+29
    beq region_next
    jsr region_paint
region_next:
    inc NR+17
    lda NR+17
    cmp NR+4
    beq region_row_done
    jmp region_cell
region_row_done:
    inc NR+16
    lda NR+16
    cmp NR+5
    beq region_pass_done
    jmp region_row
region_pass_done:
    lda NR+29
    bne region_success
    inc NR+29
    jmp region_pass
region_success:
    lda #1
    sta NR+6
    rts
region_paint:
    lda NR+19
    asl
    asl
    asl
!if CELL_BYTES = 2 {
    asl
}
    clc
    adc #<TILES
    sta NR+24
    lda #>TILES
    adc #0
    sta NR+25
    lda #0
    sta NR+18
region_scanline:
    lda NR+3
    clc
    adc NR+16
    asl
    asl
    asl
    clc
    adc NR+18
    tay
    lda bitmap_lo,y
    sta NR+22
    lda bitmap_hi,y
    sta NR+23
    lda #0
    sta NR+28
    lda NR+2
    clc
    adc NR+17
!if X_STEP >= 2 {
    asl
    rol NR+28
}
!if X_STEP = 8 {
    asl
    rol NR+28
    asl
    rol NR+28
}
    clc
    adc NR+22
    sta NR+22
    lda NR+23
    adc NR+28
    sta NR+23
    ldy #0
    lda (NR+24),y
    sta (NR+22),y
!if CELL_BYTES = 2 {
    iny
    lda (NR+24),y
    sta (NR+22),y
}
    clc
    lda NR+24
    adc #CELL_BYTES
    sta NR+24
    bcc paint_no_carry
    inc NR+25
paint_no_carry:
    inc NR+18
    lda NR+18
    cmp #8
    bne region_scanline
    rts

A, X, Y and flags are clobbered. The context scratch may change; only the six request bytes are preserved. Allow four stack bytes for the direct call and its paint helper, plus whatever your caller and interrupt handlers need. The routine is not re-entrant.

Your renderer owns the bitmap, display configuration, palette and interrupt policy. This routine neither sets video registers nor changes interrupt state. An interrupt may run only if it preserves the registers and cannot change the request, map, tables, tiles or affected display memory during the call.

Choose the right cost

One accepted cell copies 8 bitmap bytes. A 5×3 patch copies 120; there is also a full ID preflight and source address work. The two scanline tables cost 384 bytes, trading space for a readable address lookup. These are storage and copy counts, not measured cycle or VBlank budgets.

For a large moving scene, cache source-row pointers, copy longer spans or queue a limited amount of work per frame. For a sprite, consider masking and background restoration instead of opaque tile replacement. A camera at a map edge needs clipping of source and destination together. Those are useful variants; they each change the contract.

What was checked

All 18 calls completed in this machine’s reset diagnostic: three accepted draws and fifteen rejected requests. Each rejection was also executed in isolation after the accepted draws, and left the entire allocated bitmap and guard unchanged. The non-square source selection crosses a CPU page boundary.

The observer compares complete bitmap memory with an independent expected layout, then compares every native capture pixel with a separate composition. Both completed wrong-layout and unsafe-destination controls fail. The captures include the emulator’s borders and scaling; they are not photographs of a CRT.

This qualifies the selected Emu198x 0.39.0 configuration with Asm198x 0.0.71. The publishing assembler, 0.0.65, produces byte-identical fixtures. It does not establish stock-OS loading, interrupt coexistence, original-hardware timing, tearing-free animation or a whole-game frame budget.

For individual dots and shapes rather than tile copies, pixels, spans and boxes supply separately verified reusable bitmap routines.