Original Code198x program and artwork captured in Emu198x.
Code status: maintained routines and an emulator-executed caller. Execution record.
MODE4 packs eight pixel rows into each character cell. Clipping a sprite therefore needs two pieces of care: selecting the surviving source pixels and finding the matching rows in that interleaved bitmap. This wrapper does both for a byte-aligned 16×16 object.
Give the wrapper its workspace
Include clip.inc and draw.inc once. Set clip_ptr ($70/$71) to 32 caller-owned bytes. Store signed X at +0, signed Y at +14 and source pointer at +28. X is measured in eight-pixel byte columns; the normal 320-pixel MODE4 screen has forty. Y is measured in pixel rows, from zero to 255 when visible.
Source data contains 64 interleaved keep-mask/image bytes, four bytes per original row. Keep-mask 1 preserves scenery; corresponding image bits must be zero. Both source and context must be separate from code, stack and the bitmap.
The wrapper owns zero-page $70..$78 and its context. A/X/Y/P change; decimal mode must be clear. Interrupt users must preserve that workspace. Each axis uses a 14-byte block: input position +0, extent +2, limit +4; outputs destination +6, source skip +8 and count +10. +12..13 is scratch, and context +30 is a row counter. The wrapper supplies its own fixed extents and viewport limits.
Crop and draw a 16×16 image in the MODE4 bitmap
; SPDX-License-Identifier: MIT
; draw_clipped: clip_ptr -> 32-byte context; +0 signed X in byte columns,
; +14 signed Y in pixel rows, +28 source pointer (64 interleaved KEEP/image
; bytes for a 16x16 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.
source_ptr=$72
screen_ptr=$74
row_ptr=$76
mask_byte=$78
draw_clipped:
ldy #2
lda #2
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 draw_clip_y
rts
draw_clip_y:
clc
lda clip_ptr
adc #14
sta clip_ptr
bcc draw_y_pointer
inc clip_ptr+1
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 draw_context_restored
dec clip_ptr+1
draw_context_restored:
ldy #24
lda (clip_ptr),y
bne draw_visible
rts
draw_visible:
ldy #30
sta (clip_ptr),y
; source = original + skipped_rows*4 + skipped_columns*2.
ldy #22
lda (clip_ptr),y
asl
asl
tax
ldy #8
lda (clip_ptr),y
asl
sta mask_byte
txa
clc
adc mask_byte
ldy #28
adc (clip_ptr),y
sta source_ptr
iny
lda (clip_ptr),y
adc #0
sta source_ptr+1
; MODE4: base + (y/8)*320 + x*8 + (y mod 8).
lda #0
sta row_ptr+1
ldy #20
lda (clip_ptr),y
and #7
sta mask_byte
ldy #6
lda (clip_ptr),y
asl
rol row_ptr+1
asl
rol row_ptr+1
asl
rol row_ptr+1
ora mask_byte
sta row_ptr
lda row_ptr+1
clc
adc #$58
sta row_ptr+1
ldy #20
lda (clip_ptr),y
lsr
lsr
lsr
tax
beq draw_clipped_row
draw_cell_row:
clc
lda row_ptr
adc #64
sta row_ptr
lda row_ptr+1
adc #1
sta row_ptr+1
dex
bne draw_cell_row
draw_clipped_row:
lda row_ptr
sta screen_ptr
lda row_ptr+1
sta screen_ptr+1
lda source_ptr+1
pha
lda source_ptr
pha
ldy #10
lda (clip_ptr),y
tax
draw_clipped_byte:
ldy #0
lda (source_ptr),y
sta mask_byte
lda (screen_ptr),y
and mask_byte
iny
ora (source_ptr),y
dey
sta (screen_ptr),y
clc
lda source_ptr
adc #2
sta source_ptr
bcc draw_source_ready
inc source_ptr+1
draw_source_ready:
clc
lda screen_ptr
adc #8
sta screen_ptr
bcc draw_screen_ready
inc screen_ptr+1
draw_screen_ready:
dex
bne draw_clipped_byte
pla
sta source_ptr
pla
sta source_ptr+1
ldy #30
lda (clip_ptr),y
sec
sbc #1
sta (clip_ptr),y
bne draw_next_row
rts
draw_next_row:
clc
lda source_ptr
adc #4
sta source_ptr
bcc draw_source_row
inc source_ptr+1
draw_source_row:
inc row_ptr
bne draw_row_carry_ready
inc row_ptr+1
draw_row_carry_ready:
lda row_ptr
and #7
bne draw_clipped_row
clc
lda row_ptr
adc #$38
sta row_ptr
lda row_ptr+1
adc #1
sta row_ptr+1
jmp draw_clipped_rowThe address calculation uses forty character columns and 320 bytes per eight-row band. It handles the page carry when a sprite crosses a character-row boundary. It assumes the normal unscrolled $5800..$7fff mapping; a changed CRTC screen start needs a different address mapper. The BBC Microcomputer User Guide describes MODE4’s 320×256 display and forty-column text layout.
The axis helper can also serve other jobs within its contract: signed 16-bit position, extent 0..256 and viewport limit 1..256. It returns all-zero results when invisible and preserves the input fields. It does not validate values outside that contract.
Signed interval clipping with explicit carry and borrow
; SPDX-License-Identifier: MIT
; clip_ptr=$70/$71 -> 14 caller-owned bytes, little-endian words:
; +0 signed position, +2 extent 0..256, +4 limit 1..256.
; +6 destination, +8 source skip, +10 count; +12 scratch capacity.
; All three result words zero when empty. Inputs survive. A/X/Y/P change.
; NMOS 6502: D must be clear. Pointer and workspace exclusively owned;
; interrupt users must preserve them. No hardware access or static save slot.
clip_ptr=$70
clip_axis:
lda #0
ldy #6
clip_clear:
sta (clip_ptr),y
iny
cpy #12
bne clip_clear
ldy #2
lda (clip_ptr),y
iny
ora (clip_ptr),y
bne clip_nonzero
rts
clip_nonzero:
ldy #1
lda (clip_ptr),y
bpl clip_positive
; Compute unsigned magnitude into skip (including $8000).
ldy #0
sec
lda #0
sbc (clip_ptr),y
ldy #8
sta (clip_ptr),y
ldy #1
lda #0
sbc (clip_ptr),y
ldy #9
sta (clip_ptr),y
; count = extent - skip; <=0 means fully outside.
ldy #2
lda (clip_ptr),y
ldy #8
sec
sbc (clip_ptr),y
ldy #10
sta (clip_ptr),y
ldy #3
lda (clip_ptr),y
ldy #9
sbc (clip_ptr),y
ldy #11
sta (clip_ptr),y
bcc clip_empty
dey
ora (clip_ptr),y
beq clip_empty
jmp clip_capacity
clip_positive:
; Copy positive position to destination, extent to count.
ldy #0
lda (clip_ptr),y
ldy #6
sta (clip_ptr),y
ldy #1
lda (clip_ptr),y
ldy #7
sta (clip_ptr),y
ldy #2
lda (clip_ptr),y
ldy #10
sta (clip_ptr),y
ldy #3
lda (clip_ptr),y
ldy #11
sta (clip_ptr),y
clip_capacity:
; capacity = limit - destination; <=0 means fully outside.
ldy #4
lda (clip_ptr),y
ldy #6
sec
sbc (clip_ptr),y
ldy #12
sta (clip_ptr),y
ldy #5
lda (clip_ptr),y
ldy #7
sbc (clip_ptr),y
ldy #13
sta (clip_ptr),y
bcc clip_empty
dey
ora (clip_ptr),y
beq clip_empty
; Compare count with capacity, high then low (unsigned).
ldy #11
lda (clip_ptr),y
ldy #13
cmp (clip_ptr),y
bcc clip_return
bne clip_trim
ldy #10
lda (clip_ptr),y
ldy #12
cmp (clip_ptr),y
bcc clip_return
clip_trim:
ldy #12
lda (clip_ptr),y
ldy #10
sta (clip_ptr),y
ldy #13
lda (clip_ptr),y
ldy #11
sta (clip_ptr),y
clip_return:
rts
clip_empty:
lda #0
ldy #6
clip_clear_empty:
sta (clip_ptr),y
iny
cpy #12
bne clip_clear_empty
rtsRun the complete scene
Build with make ASM198X=/path/to/asm198x. Type the generated build/loader.bas into Model B MOS1.20/BASICII. It selects MODE4, reserves machine-code memory with HIMEM=&3000 and enters the caller. The caller owns that memory and the bitmap, keeps MOS IRQs active and waits through OSBYTE19 after drawing. Reset to leave.
Complete MODE4 caller with edge and corner crops
*=$3000
start:
cld
cli
lda #0
sta $74
lda #$58
sta $75
ldy #0
paint:
lda #$55
sta ($74),y
iny
bne paint
inc $75
lda $75
cmp #$80
bne paint
lda #<65535
sta context
lda #>65535
sta context+1
lda #<65528
sta context+14
lda #>65528
sta context+15
lda #<shape_a
sta context+28
lda #>shape_a
sta context+29
lda #<context
sta clip_ptr
lda #>context
sta clip_ptr+1
jsr draw_clipped
lda #<39
sta context
lda #>31
sta context+1
lda #<65528
sta context+14
lda #>65528
sta context+15
lda #<shape_a
sta context+28
lda #>shape_a
sta context+29
lda #<context
sta clip_ptr
lda #>context
sta clip_ptr+1
jsr draw_clipped
lda #<65535
sta context
lda #>65535
sta context+1
lda #<248
sta context+14
lda #>248
sta context+15
lda #<shape_a
sta context+28
lda #>shape_a
sta context+29
lda #<context
sta clip_ptr
lda #>context
sta clip_ptr+1
jsr draw_clipped
lda #<39
sta context
lda #>31
sta context+1
lda #<248
sta context+14
lda #>248
sta context+15
lda #<shape_a
sta context+28
lda #>shape_a
sta context+29
lda #<context
sta clip_ptr
lda #>context
sta clip_ptr+1
jsr draw_clipped
lda #<15
sta context
lda #>15
sta context+1
lda #<120
sta context+14
lda #>120
sta context+15
lda #<shape_a
sta context+28
lda #>shape_a
sta context+29
lda #<context
sta clip_ptr
lda #>context
sta clip_ptr+1
jsr draw_clipped
lda #<0
sta context
lda #>0
sta context+1
lda #<80
sta context+14
lda #>80
sta context+15
lda #<shape_a
sta context+28
lda #>shape_a
sta context+29
lda #<context
sta clip_ptr
lda #>context
sta clip_ptr+1
jsr draw_clipped
lda #<38
sta context
lda #>30
sta context+1
lda #<160
sta context+14
lda #>160
sta context+15
lda #<shape_a
sta context+28
lda #>shape_a
sta context+29
lda #<context
sta clip_ptr
lda #>context
sta clip_ptr+1
jsr draw_clipped
lda #<65534
sta context
lda #>65534
sta context+1
lda #<40
sta context+14
lda #>32
sta context+15
lda #<shape_a
sta context+28
lda #>shape_a
sta context+29
lda #<context
sta clip_ptr
lda #>context
sta clip_ptr+1
jsr draw_clipped
lda #<40
sta context
lda #>32
sta context+1
lda #<40
sta context+14
lda #>32
sta context+15
lda #<shape_a
sta context+28
lda #>shape_a
sta context+29
lda #<context
sta clip_ptr
lda #>context
sta clip_ptr+1
jsr draw_clipped
lda #$ca
sta ready
main:
lda #19
jsr $fff4
inc updates
jmp main
ready: !byte 0
updates: !byte 0
context: !fill 32,0
!source "clip.inc"
!source "draw.inc"
!source "shapes.inc"Keep the old footprint when moving
The wrapper pushes two source-pointer bytes per drawn row, in addition to call returns and interrupt needs. It keeps no saved background. Use separate background buffers when objects move over scenery, remembering the old clipped dimensions as well as the old address.
The shared clipping entry explains source stride, invisible objects and restoration order. This implementation deliberately leaves out horizontal bit shifts, CRTC wraparound and automatic erasure. Those are useful further components, with their own costs and contracts.
For individual pixel X positions, select prepared source phases and keep the padded footprint and original source stride explicit.