Skip to content

Place BBC MODE4 Sprites at Pixel Positions

Use eight prepared mask phases with signed pixel coordinates, normal forty-column MODE4 mapping and a bounded three-byte drawing routine.

Original bbc sprite gallery with prepared pixel phases on a sparse background grid, including cropped screen edges

Original Code198x program and artwork captured in Emu198x.

Code status: maintained routines and an emulator-executed phase gallery. Execution record.

Pixel X can belong to the object even though MODE4 memory is organised in byte columns and eight-row character cells. The phase wrapper selects a prepared source, then the drawing routine deals with that bitmap layout and viewport clipping.

Set up the call

Include phase.inc, draw.inc, the existing clipped-sprites/clip.inc and one generated bank. Set clip_ptr ($70/$71) to 32 exclusive caller bytes. Signed pixel X goes at +0, signed pixel-row Y at +14, and an eight-entry pointer table at +28.

The wrapper consumes X/table fields, replacing them with byte-column X and the selected source pointer. Reload those inputs before each call; keep the object’s logical coordinates separately. Use another generated table to draw a different image with the same routine.

A/X/Y/P change, and decimal mode must be clear. The routine owns zero-page $70..$78 and context; interrupt users must preserve them. Table, sources, context, code and stack must be separate from the bitmap and one another. It sets up no IRQs, screen mode or display timing.

Select the phase and divide signed pixel X towards negative infinity

phase.inc
; SPDX-License-Identifier: MIT
; draw_pixel_sprite: clip_ptr -> 32 bytes, signed PIXEL X +0, Y +14,
; pointer to generated eight-entry phase table +28. Reload before every call:
; X/table become byte X/source pointer. A/X/Y/P change; D=0.
; Owns $70..$78 plus context. Preserve these in interrupt users. No IRQ setup.
draw_pixel_sprite:
    ldy #0
    lda (clip_ptr),y
    and #7
    asl
    sta phase_mask_byte
    ldx #3
phase_divide:
    ldy #1
    lda (clip_ptr),y
    cmp #$80
    ror
    sta (clip_ptr),y
    dey
    lda (clip_ptr),y
    ror
    sta (clip_ptr),y
    dex
    bne phase_divide
    ldy #28
    lda (clip_ptr),y
    sta phase_source_ptr
    iny
    lda (clip_ptr),y
    sta phase_source_ptr+1
    ldy phase_mask_byte
    lda (phase_source_ptr),y
    pha
    iny
    lda (phase_source_ptr),y
    ldy #29
    sta (clip_ptr),y
    pla
    dey
    sta (clip_ptr),y
    jmp phase_draw_clipped

Follow the prepared source through MODE4

Every prepared phase has three byte columns and sixteen rows: ninety-six interleaved keep-mask/image bytes. The mask retains background bits where it is 1; the corresponding image bits are zero. The bank contains eight phases and sixteen pointer bytes, totalling 784 bytes rather than 64 unshifted bytes.

The bounded renderer keeps the original six-byte source stride after cropping. It maps the normal unscrolled forty-column MODE4 display, including page carries within and between 320-byte character-row bands. A changed CRTC start needs a different mapper.

Draw the visible prepared mask/image pairs in MODE4

draw.inc
; SPDX-License-Identifier: MIT
; phase_draw_clipped: clip_ptr -> 32-byte context; +0 signed X in byte columns,
; +14 signed Y in pixel rows, +28 source pointer (96 interleaved KEEP/image
; bytes for a 24x16 sprite). Remaining context is work and clip outputs.
; MODE4 standard unscrolled $5800..$7fff. No attributes or OS/display setup.
; A/X/Y/P change. D=0. Own ZP $70..$78 and context, exclude interrupt users.
; This implementation pushes TWO source bytes per
; row, in addition to JSR returns and IRQ use. No static background storage.
phase_source_ptr=$72
phase_screen_ptr=$74
phase_row_ptr=$76
phase_mask_byte=$78
phase_draw_clipped:
    ldy #2
    lda #3
    sta (clip_ptr),y
    iny
    lda #0
    sta (clip_ptr),y
    iny
    lda #40
    sta (clip_ptr),y
    iny
    lda #0
    sta (clip_ptr),y
    jsr clip_axis
    ldy #10
    lda (clip_ptr),y
    bne phase_draw_clip_y
    rts
phase_draw_clip_y:
    clc
    lda clip_ptr
    adc #14
    sta clip_ptr
    bcc phase_draw_y_pointer
    inc clip_ptr+1
phase_draw_y_pointer:
    ldy #2
    lda #16
    sta (clip_ptr),y
    iny
    lda #0
    sta (clip_ptr),y
    iny
    sta (clip_ptr),y
    iny
    lda #1
    sta (clip_ptr),y
    jsr clip_axis
    sec
    lda clip_ptr
    sbc #14
    sta clip_ptr
    bcs phase_draw_context_restored
    dec clip_ptr+1
phase_draw_context_restored:
    ldy #24
    lda (clip_ptr),y
    bne phase_draw_visible
    rts
phase_draw_visible:
    ldy #30
    sta (clip_ptr),y
    ; source = original + skipped_rows*6 + skipped_columns*2.
    ldy #22
    lda (clip_ptr),y
    asl
    sta phase_mask_byte
    asl
    clc
    adc phase_mask_byte
    tax
    ldy #8
    lda (clip_ptr),y
    asl
    sta phase_mask_byte
    txa
    clc
    adc phase_mask_byte
    ldy #28
    adc (clip_ptr),y
    sta phase_source_ptr
    iny
    lda (clip_ptr),y
    adc #0
    sta phase_source_ptr+1
    ; MODE4: base + (y/8)*320 + x*8 + (y mod 8).
    lda #0
    sta phase_row_ptr+1
    ldy #20
    lda (clip_ptr),y
    and #7
    sta phase_mask_byte
    ldy #6
    lda (clip_ptr),y
    asl
    rol phase_row_ptr+1
    asl
    rol phase_row_ptr+1
    asl
    rol phase_row_ptr+1
    ora phase_mask_byte
    sta phase_row_ptr
    lda phase_row_ptr+1
    clc
    adc #$58
    sta phase_row_ptr+1
    ldy #20
    lda (clip_ptr),y
    lsr
    lsr
    lsr
    tax
    beq phase_draw_clipped_row
phase_draw_cell_row:
    clc
    lda phase_row_ptr
    adc #64
    sta phase_row_ptr
    lda phase_row_ptr+1
    adc #1
    sta phase_row_ptr+1
    dex
    bne phase_draw_cell_row
phase_draw_clipped_row:
    lda phase_row_ptr
    sta phase_screen_ptr
    lda phase_row_ptr+1
    sta phase_screen_ptr+1
    lda phase_source_ptr+1
    pha
    lda phase_source_ptr
    pha
    ldy #10
    lda (clip_ptr),y
    tax
phase_draw_clipped_byte:
    ldy #0
    lda (phase_source_ptr),y
    sta phase_mask_byte
    lda (phase_screen_ptr),y
    and phase_mask_byte
    iny
    ora (phase_source_ptr),y
    dey
    sta (phase_screen_ptr),y
    clc
    lda phase_source_ptr
    adc #2
    sta phase_source_ptr
    bcc phase_draw_source_ready
    inc phase_source_ptr+1
phase_draw_source_ready:
    clc
    lda phase_screen_ptr
    adc #8
    sta phase_screen_ptr
    bcc phase_draw_screen_ready
    inc phase_screen_ptr+1
phase_draw_screen_ready:
    dex
    bne phase_draw_clipped_byte
    pla
    sta phase_source_ptr
    pla
    sta phase_source_ptr+1
    ldy #30
    lda (clip_ptr),y
    sec
    sbc #1
    sta (clip_ptr),y
    bne phase_draw_next_row
    rts
phase_draw_next_row:
    clc
    lda phase_source_ptr
    adc #6
    sta phase_source_ptr
    bcc phase_draw_source_row
    inc phase_source_ptr+1
phase_draw_source_row:
    inc phase_row_ptr
    bne phase_draw_row_carry_ready
    inc phase_row_ptr+1
phase_draw_row_carry_ready:
    lda phase_row_ptr
    and #7
    bne phase_draw_clipped_row
    clc
    lda phase_row_ptr
    adc #$38
    sta phase_row_ptr
    lda phase_row_ptr+1
    adc #1
    sta phase_row_ptr+1
    jmp phase_draw_clipped_row

The loop pushes two source-pointer bytes per row, plus JSR returns and interrupt requirements. It shares the existing axis clipping helper while leaving the old fixed-size renderer intact.

Build with make ASM198X=/path/to/asm198x and type build/loader.bas on Model B MOS1.20/BASICII. Its compact hexadecimal DATA loader reserves HIMEM=&4000 and enters machine code there, below the bitmap at &5800. The caller uses OSBYTE19 with MOS IRQs active after drawing. Reset to leave.

Complete MODE4 caller with all eight phases and cropped edges

demo.asm
*=$4000
start:
    cld
    cli
    lda #0
    sta $74
    lda #$58
    sta $75
    ldy #0
paint:
    tya
    and #7
    beq paint_grid
    lda #0
    jmp paint_value
paint_grid:
    lda #$11
paint_value:
    sta ($74),y
    iny
    bne paint
    inc $75
    lda $75
    cmp #$80
    bne paint
    lda #<8
    sta context
    lda #>8
    sta context+1
    lda #<40
    sta context+14
    lda #>40
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<33
    sta context
    lda #>33
    sta context+1
    lda #<40
    sta context+14
    lda #>40
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<58
    sta context
    lda #>58
    sta context+1
    lda #<40
    sta context+14
    lda #>40
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<83
    sta context
    lda #>83
    sta context+1
    lda #<40
    sta context+14
    lda #>40
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<108
    sta context
    lda #>108
    sta context+1
    lda #<40
    sta context+14
    lda #>40
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<133
    sta context
    lda #>133
    sta context+1
    lda #<40
    sta context+14
    lda #>40
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<158
    sta context
    lda #>158
    sta context+1
    lda #<40
    sta context+14
    lda #>40
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<183
    sta context
    lda #>183
    sta context+1
    lda #<40
    sta context+14
    lda #>40
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<8
    sta context
    lda #>8
    sta context+1
    lda #<65528
    sta context+14
    lda #>65528
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<33
    sta context
    lda #>33
    sta context+1
    lda #<65528
    sta context+14
    lda #>65528
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<58
    sta context
    lda #>58
    sta context+1
    lda #<65528
    sta context+14
    lda #>65528
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<83
    sta context
    lda #>83
    sta context+1
    lda #<65528
    sta context+14
    lda #>65528
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<108
    sta context
    lda #>108
    sta context+1
    lda #<65528
    sta context+14
    lda #>65528
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<133
    sta context
    lda #>133
    sta context+1
    lda #<65528
    sta context+14
    lda #>65528
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<158
    sta context
    lda #>158
    sta context+1
    lda #<65528
    sta context+14
    lda #>65528
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<183
    sta context
    lda #>183
    sta context+1
    lda #<65528
    sta context+14
    lda #>65528
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<8
    sta context
    lda #>8
    sta context+1
    lda #<248
    sta context+14
    lda #>248
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<33
    sta context
    lda #>33
    sta context+1
    lda #<248
    sta context+14
    lda #>248
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<58
    sta context
    lda #>58
    sta context+1
    lda #<248
    sta context+14
    lda #>248
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<83
    sta context
    lda #>83
    sta context+1
    lda #<248
    sta context+14
    lda #>248
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<108
    sta context
    lda #>108
    sta context+1
    lda #<248
    sta context+14
    lda #>248
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<133
    sta context
    lda #>133
    sta context+1
    lda #<248
    sta context+14
    lda #>248
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<158
    sta context
    lda #>158
    sta context+1
    lda #<248
    sta context+14
    lda #>248
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<183
    sta context
    lda #>183
    sta context+1
    lda #<248
    sta context+14
    lda #>248
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<65528
    sta context
    lda #>65528
    sta context+1
    lda #<80
    sta context+14
    lda #>80
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<65529
    sta context
    lda #>65529
    sta context+1
    lda #<90
    sta context+14
    lda #>90
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<65530
    sta context
    lda #>65530
    sta context+1
    lda #<100
    sta context+14
    lda #>100
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<65531
    sta context
    lda #>65531
    sta context+1
    lda #<110
    sta context+14
    lda #>110
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<65532
    sta context
    lda #>65532
    sta context+1
    lda #<120
    sta context+14
    lda #>120
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<65533
    sta context
    lda #>65533
    sta context+1
    lda #<130
    sta context+14
    lda #>130
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<65534
    sta context
    lda #>65534
    sta context+1
    lda #<140
    sta context+14
    lda #>140
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<65535
    sta context
    lda #>65535
    sta context+1
    lda #<150
    sta context+14
    lda #>150
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<312
    sta context
    lda #>312
    sta context+1
    lda #<80
    sta context+14
    lda #>80
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<313
    sta context
    lda #>313
    sta context+1
    lda #<90
    sta context+14
    lda #>90
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<314
    sta context
    lda #>314
    sta context+1
    lda #<100
    sta context+14
    lda #>100
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<315
    sta context
    lda #>315
    sta context+1
    lda #<110
    sta context+14
    lda #>110
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<316
    sta context
    lda #>316
    sta context+1
    lda #<120
    sta context+14
    lda #>120
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<317
    sta context
    lda #>317
    sta context+1
    lda #<130
    sta context+14
    lda #>130
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<318
    sta context
    lda #>318
    sta context+1
    lda #<140
    sta context+14
    lda #>140
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #<319
    sta context
    lda #>319
    sta context+1
    lda #<150
    sta context+14
    lda #>150
    sta context+15
    lda #<sprite_phase_table
    sta context+28
    lda #>sprite_phase_table
    sta context+29
    lda #<context
    sta clip_ptr
    lda #>context
    sta clip_ptr+1
    jsr draw_pixel_sprite
    lda #$ca
    sta ready
main:
    lda #19
    jsr $fff4
    inc updates
    jmp main
ready: !byte 0
updates: !byte 0
context: !fill 32,0
!source "phase.inc"
!source "draw.inc"
!source "../clipped-sprites/clip.inc"
!source "phases.inc"

The gallery is static and keeps servicing frame waits. It verifies placement and masking, rather than an animation deadline. Run make regenerate after editing the original artwork; make check-assets rejects a stale bank.

For movement, provide a backing store for the old padded/clipped rectangle or redraw from tile/world data. The existing 8×8 saved-cell routine is too small for a 24×16 prepared footprint. The shared article explains the memory choice and alternatives.