Skip to content

Draw Bitmap Text at Pixel Positions on Amiga

Reusable 68000 glyph and counted-string routines over a single Chip RAM plane, with 32-bit source pointers, clipping and explicit advance.

Put a score above a moving enemy, slide a room name into view or label a point on a map. This component draws an 8×8 character or a counted string at any pixel position in an owned bitmap plane. It uses the CPU span backend, so shapes and labels share a clear definition of a pixel.

Native Amiga output with the original PIXEL TEXT label, a score, a clipped string and a fallback character
Original Code198x font and A500 PAL reset caller, captured unchanged in Emu198x 0.39.0. Whole plane memory and selected native frames are independently checked.

Include it and draw a label

Reserve 96 bytes at T plus 48 at BP, both even-aligned and disjoint from code, stack, source data and the plane. The complete caller uses T=$2200, BP=$2000, the 320×256 plane at $10000 and its Copper at $4000. Define TEXT_WIDTH/HEIGHT and PIXEL_WIDTH/HEIGHT as 320/256, set BITMAP_BASE, and include the text and CPU span components once. Scratch needs no initialisation; public fields do.

After the named display setup, this is a normal game call. It stops before the diagnostic marker/mailbox, which your project does not need.

Draw PIXEL TEXT at (21,37), using real font and string pointers

demo.asm, excerpt
; A normal game call: a counted label at an arbitrary pixel position.
    move.w #21,T
    move.w #37,T+2
    lea font_data(pc),a1
    move.l a1,T+4
    lea label_data(pc),a1
    move.l a1,T+8
    move.b #10,T+12
    move.b #65,T+13
    move.b #32,T+14
    move.b #63,T+15
    move.w #64,T+16
    move.b #1,T+18
    move.b #1,T+19
    clr.b T+20
    clr.w T+22
    clr.w T+24
    move.w #320,T+26
    move.w #256,T+28
    bsr text_string

Call text_string, then read T+21: one means accepted. Change the position and source pointer/length to draw another label. For a single glyph, set T+13 and call text_char; string pointer and length are then ignored. The routine owns no cursor, drawing list or game loop.

A pointer is wider; the contract is explicit

This Amiga context differs from the other text ports. It uses 32-bit source pointers; copying their 16-bit-address offsets would not describe the same fields. Words and longwords are big-endian. Contexts must be even-aligned; byte font/string sources may be odd.

T offset Supply or read
0,2 Signed origin X,Y, each −512..511
4 Font pointer longword
8 String pointer longword
12,13 String length byte 0..255; single character code byte
14,15 First code and fallback code bytes
16 Glyph-count word, 1..256; first+count ≤256
18 Mode byte: 0 transparent, 1 opaque
19,20 Foreground/background bit pens, each 0..1
21 Accepted 1, rejected 0
22,24,26,28 Scissor left,top,right,bottom words; right/bottom exclusive
30 Output signed word: next X
32,33 Output consumed/substitution bytes
34 Output word: backend span calls
36 Output longword: destination-byte stores
40–95 Private scratch

The origin is the cell’s top-left corner, Y downwards. The scissor must be nonempty and inside the physical plane. Each glyph advances by eight, including blank, substituted and completely clipped glyphs: nextX = startX + 8*consumed. A counted string has no hidden terminator, newline handling, wrapping or case conversion. A zero byte is a code. Empty strings are accepted without source reads or drawing after declaration checks.

Pointers are stored as longwords, but this 68000 contract accepts addresses only in $000000–$FFFFFF, with allocation ends no greater than $1000000. A last byte at $FFFFFF is arithmetically allowed. The source must actually be readable in the selected RAM/ROM mapping: arithmetic validation does not acquire memory or establish ownership. The default font is in the original reset ROM; the tests also use odd-addressed Chip RAM font and string buffers.

Invalid coordinates, mode, either pen, font range/fallback, scissor or pointer arithmetic reject before destination writes, returning the original X and zero counters. Inputs 0–20 and 22–29 survive. Keep the whole font/string allocation stable and separate from code, T/BP, stack and destination throughout the call.

Supply the shape; keep the advance

The optional original Code198x font supplies 64 ASCII glyphs, codes 32..95, under the sample repository’s MIT licence. It is not extracted from ROM. Each glyph has eight high-bit-first row bytes, a five-column/seven-row visible design inside an 8×8 cell. A font row lives at font + (code-first)*8 + row. Unsupported codes use the declared fallback, which must exist in the font.

Open the original 64-glyph font

Optional 512-byte sample font

font-m68000.inc
; SPDX-License-Identifier: MIT
; Original Code198x 8x8 diagnostic font, ASCII32..95.
    dc.b 0,0,0,0,0,0,0,0,16,16,16,16,16,0,16,0
    dc.b 40,40,40,0,0,0,0,0,40,124,40,40,124,40,0,0
    dc.b 16,60,80,56,20,120,16,0,100,104,8,16,32,44,76,0
    dc.b 48,72,80,32,84,72,52,0,16,16,32,0,0,0,0,0
    dc.b 8,16,32,32,32,16,8,0,32,16,8,8,8,16,32,0
    dc.b 0,84,56,124,56,84,0,0,0,16,16,124,16,16,0,0
    dc.b 0,0,0,0,0,16,32,0,0,0,0,124,0,0,0,0
    dc.b 0,0,0,0,0,0,16,0,4,8,8,16,32,32,64,0
    dc.b 56,68,76,84,100,68,56,0,16,48,16,16,16,16,56,0
    dc.b 56,68,4,8,16,32,124,0,120,4,4,56,4,4,120,0
    dc.b 8,24,40,72,124,8,8,0,124,64,64,120,4,4,120,0
    dc.b 56,64,64,120,68,68,56,0,124,4,8,16,32,32,32,0
    dc.b 56,68,68,56,68,68,56,0,56,68,68,60,4,4,56,0
    dc.b 0,16,0,0,16,0,0,0,0,16,0,0,16,16,32,0
    dc.b 4,8,16,32,16,8,4,0,0,0,124,0,124,0,0,0
    dc.b 64,32,16,8,16,32,64,0,56,68,4,8,16,0,16,0
    dc.b 56,68,92,84,92,64,56,0,56,68,68,124,68,68,68,0
    dc.b 120,68,68,120,68,68,120,0,56,68,64,64,64,68,56,0
    dc.b 120,68,68,68,68,68,120,0,124,64,64,120,64,64,124,0
    dc.b 124,64,64,120,64,64,64,0,56,68,64,92,68,68,60,0
    dc.b 68,68,68,124,68,68,68,0,56,16,16,16,16,16,56,0
    dc.b 28,8,8,8,72,72,48,0,68,72,80,96,80,72,68,0
    dc.b 64,64,64,64,64,64,124,0,68,108,84,84,68,68,68,0
    dc.b 68,100,100,84,76,76,68,0,56,68,68,68,68,68,56,0
    dc.b 120,68,68,120,64,64,64,0,56,68,68,68,84,72,52,0
    dc.b 120,68,68,120,80,72,68,0,60,64,64,56,4,4,120,0
    dc.b 124,16,16,16,16,16,16,0,68,68,68,68,68,68,56,0
    dc.b 68,68,68,68,68,40,16,0,68,68,68,84,84,108,68,0
    dc.b 68,68,40,16,40,68,68,0,68,68,40,16,16,16,16,0
    dc.b 124,4,8,16,32,64,124,0,56,32,32,32,32,32,56,0
    dc.b 64,32,32,16,8,8,4,0,56,8,8,8,8,8,56,0
    dc.b 16,40,68,0,0,0,0,0,0,0,0,0,0,0,124,0

Glyph-local (3,5) at origin (21,37) becomes (24,42). The next cell begins at X29, even if the first is a blank space. Move a glyph, keep its shape explains that translation, clipping and the distinction between ink, cell and advance.

Transparent mode sends foreground runs through bp_hspan, leaving zero font bits alone. Opaque first paints each clipped eight-pixel background row, then its foreground runs. Pens are stored bit values in this one plane. The caller chooses the palette and owns any other planes; this is not a hidden multi-plane colour replacement routine.

Make the cost visible

For ten-code PIXEL TEXT, transparent drawing takes 77 / 77 byte stores at X24/X31; opaque takes 157 / 237. Foreground uses 77 spans; opaque adds 80 backgrounds. Gaps and partial edge bytes matter. Opaque drawing revisits foreground bytes after the background, buying a clear composable baseline at a cost.

The text component is 888 assembled bytes, plus 480 for the CPU backend and 96+48 RAM context bytes. The sample font takes 512 bytes; ten digits need only 80, and 256 contiguous glyphs need 2,048. Reset setup, bitmap, string, stack and the 72-byte Copper list are additional. These are storage and store counts, not cycles or a frame-rate promise.

A masked word/longword blit could combine ink and background in fewer passes. Pre-shifted Amiga sprites trade more asset storage for simpler positioning. The CPU/blitter region example provides an aligned two-plane comparison. General blitter text would also need masks, alignment, Chip RAM, DMA ownership and completion handling; it is not supplied by this CPU component.

Own the resources

T/BP are non-re-entrant. Clobbers D0–D7/A1/CCR; SR interrupt-mask bits survive. Reserve a conservative 64-byte stack budget beyond your other calls/IRQs. The routine changes no interrupt enables, waits for no frame, acquires no OS display and synchronises no renderer. Keep other CPU, blitter and IRQ writers from racing its read/modify/write operations. An AmigaOS project needs its own loading, allocation and display ownership arrangement.

Open the reusable 68000 text component

Include this text component once alongside its span backend

m68000.inc
; SPDX-License-Identifier: MIT
; 8x8 fixed-width bitmap text over bp_hspan. Native big-endian, 32-bit pointers.
; T:96 even-aligned private bytes; BP:48 separate bytes. No hidden scene/OS/IRQ.
; +0/2 signed origin; +4 font long; +8 string long; +12 length; +13 char;
; +14 first; +15 fallback; +16 glyph count word; +18 mode; +19/20 fg/bg;
; +21 accepted; +22/24/26/28 scissor L/T/R/B words (exclusive ends);
; +30 nextX word; +32 consumed; +33 substitutions; +34 span word; +36 stores long.
; +40..95 scratch. Public 0..20 and 22..29 survive. D0-D7/A1/CCR clobbered.
; Caller supplies readable stable allocations and owns plane, banks and DMA/IRQ access.
text_char:
    move.b #1,T+60
    moveq #1,d0
    bra text_begin
text_string:
    clr.b T+60
    moveq #0,d0
    move.b T+12,d0
text_begin:
    move.w d0,T+40
    clr.b T+21
    move.w T,T+30
    clr.b T+32
    clr.b T+33
    clr.w T+34
    clr.l T+36
    cmp.w #-512,T
    blt text_return
    cmp.w #511,T
    bgt text_return
    cmp.w #-512,T+2
    blt text_return
    cmp.w #511,T+2
    bgt text_return
    cmp.b #1,T+18
    bhi text_return
    cmp.b #1,T+19
    bhi text_return
    cmp.b #1,T+20
    bhi text_return
    move.w T+22,d0
    cmp.w T+26,d0
    bhs text_return
    cmp.w #TEXT_WIDTH,T+26
    bhi text_return
    move.w T+24,d0
    cmp.w T+28,d0
    bhs text_return
    cmp.w #TEXT_HEIGHT,T+28
    bhi text_return
    moveq #0,d1
    move.w T+16,d1
    beq text_return
    cmp.w #256,d1
    bhi text_return
    moveq #0,d0
    move.b T+14,d0
    add.w d0,d1
    cmp.w #256,d1
    bhi text_return
    moveq #0,d2
    move.b T+15,d2
    cmp.w d0,d2
    blo text_return
    cmp.w d1,d2
    bhs text_return
    move.l T+4,d0
    cmp.l #$ffffff,d0
    bhi text_return
    moveq #0,d1
    move.w T+16,d1
    lsl.l #3,d1
    add.l d0,d1
    cmp.l #$1000000,d1
    bhi text_return
    tst.b T+60
    bne text_valid
    move.l T+8,d0
    cmp.l #$ffffff,d0
    bhi text_return
    moveq #0,d1
    move.w T+40,d1
    add.l d0,d1
    cmp.l #$1000000,d1
    bhi text_return
text_valid:
    move.b #1,T+21
    clr.w T+42
    tst.w T+40
    beq text_return
text_glyph:
    moveq #0,d0
    tst.b T+60
    beq text_from_string
    move.b T+13,d0
    bra text_lookup
text_from_string:
    move.l T+8,a1
    moveq #0,d1
    move.w T+42,d1
    adda.l d1,a1
    move.b (a1),d0
text_lookup:
    moveq #0,d1
    move.b T+14,d1
    cmp.w d1,d0
    blo text_fallback
    add.w T+16,d1
    cmp.w d1,d0
    blo text_have_code
text_fallback:
    move.b T+15,d0
    addq.b #1,T+33
text_have_code:
    moveq #0,d1
    move.b T+14,d1
    sub.w d1,d0
    lsl.w #3,d0
    move.w d0,T+44
    clr.w T+46
text_row:
    move.w T+2,d2
    add.w T+46,d2
    move.w d2,T+52
    tst.b T+18
    beq text_load_row
    move.w T+30,d0
    move.w d0,d1
    addq.w #8,d1
    moveq #0,d3
    move.b T+20,d3
    bsr text_emit
text_load_row:
    move.l T+4,a1
    moveq #0,d0
    move.w T+44,d0
    add.w T+46,d0
    adda.l d0,a1
    move.b (a1),T+48
    clr.b T+49
    move.w #-1,T+50
text_column:
    moveq #0,d0
    move.b T+48,d0
    andi.b #$80,d0
    beq text_zero
    tst.w T+50
    bpl text_shift
    moveq #0,d0
    move.b T+49,d0
    move.w d0,T+50
    bra text_shift
text_zero:
    tst.w T+50
    bmi text_shift
    move.w T+30,d0
    add.w T+50,d0
    moveq #0,d1
    move.b T+49,d1
    add.w T+30,d1
    move.w T+52,d2
    moveq #0,d3
    move.b T+19,d3
    bsr text_emit
    move.w #-1,T+50
text_shift:
    move.b T+48,d0
    add.b d0,d0
    move.b d0,T+48
    addq.b #1,T+49
    cmp.b #8,T+49
    blo text_column
    tst.w T+50
    bmi text_next_row
    move.w T+30,d0
    add.w T+50,d0
    move.w T+30,d1
    addq.w #8,d1
    move.w T+52,d2
    moveq #0,d3
    move.b T+19,d3
    bsr text_emit
text_next_row:
    addq.w #1,T+46
    cmp.w #8,T+46
    blo text_row
    addq.w #8,T+30
    addq.b #1,T+32
    addq.w #1,T+42
    subq.w #1,T+40
    bne text_glyph
text_return:
    rts
text_emit:
    cmp.w T+24,d2
    blt text_return
    cmp.w T+28,d2
    bge text_return
    cmp.w T+22,d0
    bge text_left_inside
    move.w T+22,d0
text_left_inside:
    cmp.w T+26,d1
    ble text_right_inside
    move.w T+26,d1
text_right_inside:
    cmp.w d1,d0
    bge text_return
    move.w d0,BP
    move.w d1,BP+4
    move.w d2,BP+2
    clr.b BP+8
    tst.b d3
    bne text_pen_set
    move.b #1,BP+8
text_pen_set:
    bsr bp_hspan
    addq.w #1,T+34
    moveq #0,d0
    move.w BP+40,d0
    add.l d0,T+36
    rts

What was verified

81 calls completed using Asm198x 0.0.71 and native Emu198x 0.39.0. Every call compares the full plane, guards, public inputs, source/font preservation, acceptance, advance, consumption/substitution and work counts against an independent logical pixel model. Cases include every phase, pens, clipping edges, negative origins, empty/255-code strings, fallback, 256-code fonts, odd buffers, wide pointers and the 24-bit arithmetic boundary. Selected whole native frames match independently composed images; the Copper list stays unchanged.

Wrong glyph-bit and advance executables each complete accepted calls and fail both memory and picture checks. Exact source/tool bindings and evidence-corruption tests are retained. This qualifies the original A500 PAL reset fixture, not stock Kickstart/AmigaOS loading, original-hardware timing or tear-free display coexistence.