Skip to content

Draw a Native Region on Amiga

Reusable Amiga code for drawing a room, changed tile or strip of scenery, with explicit display ownership.

Open a door, break a wall, reveal the next stretch of scenery. The map changes first; this routine makes the picture catch up. Give it a rectangle of byte-map IDs and a destination, and it draws just that part of the world.

Amiga native output showing a patterned map patch and a single-cell redraw
Original diagnostic in Emu198x 0.39.0. A 5×3 patch and a single-cell erase are visible; a separate logical bottom-right write is also checked in memory.

One picture, two bitplanes

This A500 PAL example draws a 320×256 picture from two separate bitplanes. A cell is 16×8 pixels, so the logical display has twenty columns and thirty-two rows. Each tile takes 32 bytes: eight words of plane 0, then eight words of plane 1. Combining their bits selects one of four colours.

Each destination plane has a forty-byte scanline pitch and takes 10,240 bytes. The original caller places them at $10000 and $12800, provides its Copper list and colours, and draws before enabling display DMA. Choose Chip RAM allocations which fit your own project; the source map and two planes are separate resources.

Give the routine its context

Pass A0 pointing to an even-aligned, private 32-byte context. Bytes 0–5 are the request and byte 6 is the completion flag. Byte 7 selects backend 0 CPU, 1 blitter. Longwords at 8, 12, 16 and 20 point to the byte map, tile pictures, plane 0 and plane 1. Bytes 24–26 hold map width, height and tile count; 27–31 are scratch.

Initialise the pointers, dimensions and selected backend

demo.asm, excerpt
    lea NR,a0
    move.b #BACKEND,7(a0)
    move.l #MAP,8(a0)
    move.l #TILES,12(a0)
    move.l #PLANE0,16(a0)
    move.l #PLANE1,20(a0)
    move.b #12,24(a0)
    move.b #8,25(a0)
    move.b #4,26(a0)

The map may be byte-aligned. Tile and plane pointers must be even; misalignment and unknown backend values are rejected before painting. The caller guarantees allocation sizes, pointer ranges and Chip RAM visibility. No routine can infer those allocations from a bare address.

A0 and the six request bytes survive. D0–D7, A1–A5 and CCR are clobbered. Allow eight stack bytes on the CPU path or sixteen on the blitter path, including the direct BSR, beyond your own caller and interrupt needs. Include m68000.inc with its adjacent blit-plane.inc helper available at the maintained relative path.

Choose how to copy

The CPU path copies eight words per plane. The blitter path calls the separate Blitter Copy component once per plane. Both write 32 bytes per cell, or 480 bytes for the 5×3 patch, in addition to preflight work.

The blitter helper copies one word across eight lines. Source pitch is two bytes; destination pitch is forty, so BLTDMOD=38. BLTSIZE=$0201 starts the copy. It waits before setting registers and again before returning. This is synchronous: the example demonstrates the hardware path, not useful CPU/blitter overlap or an automatic speed win for small tiles.

Your caller owns the blitter exclusively, enables master/blitter DMA and keeps all buffers stable. Neither backend changes interrupt state. An OS application needs an ownership protocol; the reset diagnostic owns the machine. Bigger spans, asynchronous submission, masks and clipping are useful further variants, each with different obligations.

Draw the patch

These are the example caller’s allocations and constants. Use addresses which fit your own memory plan.

The caller owns its memory and display configuration

demo.asm, excerpt
NR equ $2000
RESULT equ $2040
MAP equ $20f0
TILES equ $6000
PLANE0 equ $10000
PLANE1 equ $12800
SCREEN_COLS equ 20
SCREEN_ROWS equ 32
BACKEND equ 0

Supply six unsigned bytes: source column, source row, destination column, destination row, width and height. Then call region_draw. 6(A0) is one for a completed draw or zero for rejection. The six request bytes survive.

Draw a 5×3 patch from map cell (2,1) at display cell (4,3)

demo.asm, excerpt
case_0:
    move.b #2,0(a0)
    move.b #1,1(a0)
    move.b #4,2(a0)
    move.b #3,3(a0)
    move.b #5,4(a0)
    move.b #3,5(a0)
    bsr region_draw
    move.b 6(a0),d0
    or.b #128,d0
    move.b d0,RESULT+1

The result store after the call belongs to the diagnostic. In a game, inspect the completion flag and continue with your own logic. The routine rejects zero sizes, invalid starts, source/destination overruns and IDs outside the configured tile set. It checks every selected ID before writing any destination bytes, so a bad ID cannot leave half a patch behind. It rejects rather than clips.

Keep the reusable code

Checked native region drawing

m68000.inc
; A0 -> 32-byte even-aligned context. Six request bytes, valid +6,
; backend +7 (0 CPU, 1 blitter), long map/tiles/plane0/plane1 +8/+12/+16/+20,
; byte map width/height/tile count +24/+25/+26; scratch +27..31.
; A0 preserved; D0-D7/A1-A5/CCR clobbered. Caller owns custom chips and DMA.
region_draw:
    clr.b 6(a0)
    cmp.b #1,7(a0)
    bls region_backend_ok
    rts
region_backend_ok:
    move.l 12(a0),d0
    btst #0,d0
    beq region_tiles_aligned
    rts
region_tiles_aligned:
    move.l 16(a0),d0
    btst #0,d0
    beq region_plane0_aligned
    rts
region_plane0_aligned:
    move.l 20(a0),d0
    btst #0,d0
    beq region_plane1_aligned
    rts
region_plane1_aligned:
    tst.b 4(a0)
    bne bounds_nonzero_0
    rts
bounds_nonzero_0:
    move.b 24(a0),d0
    cmp.b 0(a0),d0
    bhi bounds_start_0
    rts
bounds_start_0:
    sub.b 0(a0),d0
    cmp.b 4(a0),d0
    bhs bounds_fit_0
    rts
bounds_fit_0:
    tst.b 5(a0)
    bne bounds_nonzero_1
    rts
bounds_nonzero_1:
    move.b 25(a0),d0
    cmp.b 1(a0),d0
    bhi bounds_start_1
    rts
bounds_start_1:
    sub.b 1(a0),d0
    cmp.b 5(a0),d0
    bhs bounds_fit_1
    rts
bounds_fit_1:
    tst.b 4(a0)
    bne bounds_nonzero_2
    rts
bounds_nonzero_2:
    move.b #SCREEN_COLS,d0
    cmp.b 2(a0),d0
    bhi bounds_start_2
    rts
bounds_start_2:
    sub.b 2(a0),d0
    cmp.b 4(a0),d0
    bhs bounds_fit_2
    rts
bounds_fit_2:
    tst.b 5(a0)
    bne bounds_nonzero_3
    rts
bounds_nonzero_3:
    move.b #SCREEN_ROWS,d0
    cmp.b 3(a0),d0
    bhi bounds_start_3
    rts
bounds_start_3:
    sub.b 3(a0),d0
    cmp.b 5(a0),d0
    bhs bounds_fit_3
    rts
bounds_fit_3:
    clr.b 30(a0)
region_pass:
    clr.b 27(a0)
region_row:
    clr.b 28(a0)
region_cell:
    moveq #0,d0
    move.b 1(a0),d0
    add.b 27(a0),d0
    moveq #0,d1
    move.b 24(a0),d1
    mulu.w d1,d0
    moveq #0,d1
    move.b (a0),d1
    add.b 28(a0),d1
    add.l d1,d0
    movea.l 8(a0),a1
    move.b 0(a1,d0.l),d1
    move.b d1,29(a0)
    cmp.b 26(a0),d1
    blo region_id_ok
    rts
region_id_ok:
    tst.b 30(a0)
    beq region_next
    bsr region_paint
region_next:
    addq.b #1,28(a0)
    move.b 28(a0),d0
    cmp.b 4(a0),d0
    beq region_row_done
    bra region_cell
region_row_done:
    addq.b #1,27(a0)
    move.b 27(a0),d0
    cmp.b 5(a0),d0
    beq region_pass_done
    bra region_row
region_pass_done:
    tst.b 30(a0)
    bne region_success
    move.b #1,30(a0)
    bra region_pass
region_success:
    move.b #1,6(a0)
    rts
region_paint:
    moveq #0,d0
    move.b 29(a0),d0
    lsl.l #5,d0
    movea.l 12(a0),a1
    adda.l d0,a1
    moveq #0,d0
    move.b 3(a0),d0
    add.b 27(a0),d0
    mulu.w #320,d0
    moveq #0,d1
    move.b 2(a0),d1
    add.b 28(a0),d1
    add.w d1,d1
    add.l d1,d0
    movea.l 16(a0),a2
    adda.l d0,a2
    movea.l 20(a0),a3
    adda.l d0,a3
    tst.b 7(a0)
    bne region_blitter
    moveq #7,d4
region_cpu_plane0:
    move.w (a1)+,(a2)
    lea 40(a2),a2
    dbra d4,region_cpu_plane0
    moveq #7,d4
region_cpu_plane1:
    move.w (a1)+,(a3)
    lea 40(a3),a3
    dbra d4,region_cpu_plane1
    rts
region_blitter:
    lea $dff000,a5
    bsr region_blit_plane
    lea 16(a1),a1
    movea.l a3,a2
    bsr region_blit_plane
    rts
    include "../../../../../cross-platform/patterns/rendering/region-architectures/blit-plane.inc"

The context is non-re-entrant. Your caller owns its memory, video setup and interrupt policy. Keep all inputs and display resources stable throughout the call. Drawing an opaque zero tile erases a cell; transparent sprites need masking or background restoration instead.

What was checked

All 22 calls completed per backend in the original reset diagnostic. Fifteen rejected map/bounds/ID requests were also executed in isolation, each after the same three accepted draws. The complete destination and both mapped guards were compared with an independent model, and every native capture pixel was compared with a separate composition. Completed wrong-layout and unsafe-destination controls fail those checks.

Both CPU and blitter outputs match byte for byte. Backend and alignment rejection cases complete on each path; a deliberately shortened hardware blit fails the independent checks.

These results qualify the selected Emu198x 0.39.0 configuration and Asm198x 0.0.71 fixture. They do not establish original-hardware timing, stock-OS loading, interrupt coexistence or a whole-game frame budget. The complete caller keeps reset setup separate from reusable drawing. Copy counts above are not measured timing claims.

CPU pixels, spans and boxes handle arbitrary pixel edges in one plane. Bitmap text adds glyph lookup, clipping and counted layout over that backend. These have their own contexts and costs rather than silently inheriting this aligned region contract.