Skip to content

Measure, Align and Wrap Text on Dragon 32 PMODE 4

Measure and align labels, choose character or word wrapping, and give narrow letters less room with proportional text.

Change a prompt and let the code find its position. A short label can sit in the middle of a panel; a longer message can continue on the next line. This component adds those calculations to the existing bitmap-text renderer.

An original READY TO PLAY prompt and PRESS FIRE TO BEGIN line centred within the declared panel on Dragon 32 PMODE 4.
The complete native reset caller: a 176-pixel panel beginning at x21, two centred lines, and a 12-scanline pitch. Original Code198x font and message; Emu198x 0.41.0.

The capture shows the emulator’s native raster, including its border and horizontal sampling. It does not establish a physical television aspect ratio. Layout uses logical bitmap pixels.

Include it, initialise it, call it

PMODE 4 uses a 256×192 one-bit bitmap at $0600 in this reset caller. Its linear rows keep the backend small. Public words are big-endian: a left coordinate of 21 is stored as $00,$15.

Reserve 64 bytes at TL=$0480, 96 at T=$0400 and 48 at BP=$0380 for this caller. Keep them separate from source, font, bitmap, code and stack. Include the unchanged bitmap-text component before text-layout/6809.inc; the layout uses its aliases and renderer. Keep the existing backend and its helper/table. The complete source shows every include and allocation.

T supplies the font pointer, first code/count, fallback, transparent/opaque mode and foreground/background pens. Layout takes ownership of T and BP while running, including the scissor. Initialise those font/pen choices as in caller.inc, then supply TL:

Offset Input
0, 2 Words: panel left, top
4, 6 Words: panel width, height
8 Byte: alignment, 0 left / 1 centre / 2 right
9 Byte: line pitch, 8..255 scanlines
10 Word: source address
12 Byte: source length, 0..255

Words are big-endian. Scratch needs no initialisation; inputs do. These are the maintained setup calls used by the working example:

Set the panel and call layout_block

prepare.inc
; BEGIN LAYOUT INPUTS
    ldd #PANEL_LEFT
    std TL+0
    ldd #PANEL_TOP
    std TL+2
    ldd #PANEL_WIDTH
    std TL+4
    ldd #PANEL_HEIGHT
    std TL+6
    ldd #label_data
    std TL+10
    lda #PANEL_ALIGN
    sta TL+8
    lda #LINE_PITCH
    sta TL+9
    lda #MESSAGE_LENGTH
    sta TL+12
    jsr layout_block
; END LAYOUT INPUTS

Use layout_line for one counted label or layout_block for LF-separated, character-wrapped text. TL+13 returns 1 when accepted and 0 when rejected. Inputs TL+0..12 survive; T/BP do not.

Measure before subtracting

layout_measure needs only TL+12. It returns 8*length as a word at TL+18, accepted=1, and zeroes the other documented outputs. It reads no string or font. A 255-code line therefore measures 2,040 pixels rather than wrapping into a byte.

Counted-line measurement without a string scan

6809.inc, excerpt
layout_measure
    jsr layout_reset
    ldd #$0000
    std l_measured
    lda l_length
    sta l_measured+1
    lsl l_measured+1
    rol l_measured
    lsl l_measured+1
    rol l_measured
    lsl l_measured+1
    rol l_measured
    lda #$01
    sta l_status
    rts

Five glyph cells need 40 pixels. A 177-pixel panel has 137 spare pixels. Centre alignment uses left + floor(137/2): at left 21, the first cell begins at 89, leaving the odd spare pixel on the right. Right alignment starts at 158. These are cell bounds; the visible ink can have blank columns inside them. Coordinates and remainders and glyph-local positions explain the calculations.

A line wider than its panel rejects before drawing. Do not subtract first and hope an unsigned result stays sensible. A block also plans every row before rendering: a message too tall for its panel leaves the previous bitmap intact. Supply readable, stable, non-overlapping source/font allocations and keep mappings unchanged throughout both passes.

Decide what a line break means

Block mode uses width div 8 cells per line. It keeps spaces and can split a word. Byte 10 is LF; byte zero remains a character code and uses the configured fallback if unsupported. Line mode instead treats every byte, including 10, as a glyph slot. This counted-line rule is also what layout_measure measures.

A full chunk consumes an immediately following LF as its own break. Consecutive LFs retain interior empty lines. A final LF ends its line without creating another trailing row. Empty input has zero lines and no drawing. The whole rectangle must fit the physical bitmap and be at least 8×8; an empty request still validates its declaration.

Pitch is the distance between row origins. Five rows at pitch 16 need (5-1)*16+8=72 scanlines. Opaque mode paints actual glyph cells, not the unused width or an empty line. Clear the panel you own before replacing a longer message, or old letters may remain. Use the word-aware variant below when words should stay together. Incremental reveal is a separate technique.

Make it your own

Build customise.asm with the same includes. It narrows the panel from 176 to 104 pixels, selects right alignment, changes pitch from 12 to 16 and supplies another original counted message. The result has five lines inside the existing 80-scanline height. PANEL splits at the 13-column boundary; that is the chosen character-wrap policy in action.

Change the panel, alignment, pitch and counted message

customise.asm
CUSTOM_LAYOUT equ 1
; SPDX-License-Identifier: MIT
; Complete caller: change layout inputs without changing the reusable renderer.
PANEL_LEFT equ 21
PANEL_TOP equ 37
PANEL_WIDTH equ 104
PANEL_HEIGHT equ 80
PANEL_ALIGN equ 2
LINE_PITCH equ 16
MESSAGE_LENGTH equ 44
    include "caller.inc"
The custom message wraps into five right-aligned lines inside a narrower panel on Dragon 32 PMODE 4.
Verified native customisation using the unchanged layout and drawing routines. Width changes the line count; pitch changes the vertical footprint.

Edit customise-message.inc and update MESSAGE_LENGTH together. The routine has no terminator scan to rescue an incorrect count. More rows or wider text can increase drawing work, even though code and context allocations stay the same. The proportional option below changes glyph advances. Another glyph size or video mode still needs its own implementation and verification.

For these exact original messages, native counters report:

Work Two-line demo (33 source bytes) Narrow custom panel (44 source bytes)
Logical lines 2 5
Backend spans 511 709
Destination-byte writes 767 1117
Added layout RAM 64 bytes 64 bytes

Both the message and layout changed. This compares those two workloads, not the isolated cost of right alignment or narrowing a panel. The code and font remain the same.

Common mistakes and their symptoms

The following are deliberately broken diagnostic fragments, kept outside the reusable implementation. Both complete bounded native calls. Their repairs also execute and assemble to the original working cartridge bytes.

The label moves right

A missing shift measures four pixels per code while the renderer still advances eight. The calculation leaves too much supposedly unused space on the left. This is a disagreement between measurement and rendering, rather than a font or video-address problem.

Deliberately wrong: stop multiplying at four

6809-width.inc, excerpt
; BEGIN WRONG
    lsl l_line_width+1
    rol l_line_width
    lsl l_line_width+1
    rol l_line_width
The diagnostic READY label is shifted right because its measured width is too small.
Native width-error symptom. The independent full-memory and pixel checks detect it.

Restore the third shift and check that five codes measure 40 pixels:

Correct: match the renderer's eight-pixel advance

6809-width.inc, excerpt
; BEGIN FIX
    lsl l_line_width+1
    rol l_line_width
    lsl l_line_width+1
    rol l_line_width
    lsl l_line_width+1
    rol l_line_width

The panel offset disappears

The alignment calculation returns an offset within the panel. Writing that offset as a screen coordinate forgets the panel’s left edge. Here the label moves 21 pixels left; other inputs can also make the scissor cut letters off.

Deliberately wrong: use a local offset as a screen coordinate

6809-origin.inc, excerpt
; BEGIN WRONG
    ldd l_temp
    std l_x
READY moves left because the diagnostic omits the panel's x21 origin.
The same source, font and panel, with the rectangle origin omitted.

Add the panel origin to the alignment offset:

Correct: translate from panel to screen coordinates

6809-origin.inc, excerpt
; BEGIN FIX
    ldd l_left
    std l_x
    ldd l_x
    addd l_temp
    std l_x
The corrected READY label begins at x89 within the panel.
Both repaired diagnostic callers reproduce this correct native frame.

Costs, ownership and results

The layout adds 1,463 code bytes and 64 context bytes. The reused text renderer is 2,058 bytes and its backend 470; their 96+48 context bytes, the 512-byte sample font, messages, video allocation and stack are separate. Reserve a conservative 80-byte stack budget plus caller/interrupt depth. The 6809 keeps U and DP; D, X, Y and condition codes may change.

Block mode makes two planning passes, then the renderer adds source/font reads and glyph/span work. The second pass interleaves planning and rendering after the complete first pass has qualified the request. TL+26 reports total span calls; TL+28 reports a 32-bit destination-byte-write count. These are work counts, not cycle measurements. Byte alignment, pen packing, glyph shapes and opacity affect them. Static labels can be cheaper to position or render once.

Accepted calls return consumed bytes at TL+14, logical lines at +15, last-line glyph count at +16, maximum logical width at +18, first origin X at +20, last line end X at +22 and its origin Y at +24. Rejected calls return zero for all documented outputs and leave the bitmap untouched. T/BP scratch may change. Blank lines affect line count and position but cause no glyph/background writes.

The component is non-re-entrant. It supplies no frame wait, OS negotiation, interrupt protection or video synchronisation. The reset caller owns the machine; a real game must choose safe display access and coordinate other users of T/BP, video, banks and interrupts. Native completion does not establish stock-OS loading, live tearing behaviour, original-hardware timing or a frame budget.

Read the reusable layout component

Reusable layout over the existing text renderer

6809.inc
; SPDX-License-Identifier: MIT
; Bounded counted 8x8 text layout over the unchanged bitmap-text component.
; Include bitmap-text once; reserve TL=64, T=96 and BP=48 separate bytes.
; TL inputs 0..12 survive. T/BP are owned workspaces. See README.md.
l_left equ TL+0
l_top equ TL+2
l_width equ TL+4
l_height equ TL+6
l_align equ TL+8
l_pitch equ TL+9
l_source equ TL+10
l_length equ TL+12
l_status equ TL+13
l_consumed equ TL+14
l_lines equ TL+15
l_last_count equ TL+16
l_measured equ TL+18
l_first_x equ TL+20
l_last_x equ TL+22
l_last_y equ TL+24
l_calls equ TL+26
l_writes equ TL+28
l_mode equ TL+32
l_pass equ TL+33
l_i equ TL+34
l_start equ TL+35
l_count equ TL+36
l_columns equ TL+37
l_temp equ TL+38
l_right equ TL+40
l_bottom equ TL+42
l_y equ TL+44
l_x equ TL+46
l_pointer equ TL+48
l_code equ TL+50
l_line_width equ TL+52
l_work equ TL+54
layout_measure
    jsr layout_reset
    ldd #$0000
    std l_measured
    lda l_length
    sta l_measured+1
    lsl l_measured+1
    rol l_measured
    lsl l_measured+1
    rol l_measured
    lsl l_measured+1
    rol l_measured
    lda #$01
    sta l_status
    rts
layout_line
    lda #$00
    sta l_mode
    jmp layout_enter
layout_block
    lda #$01
    sta l_mode
    jmp layout_enter
layout_reset
    lda #$00
    sta l_status
    lda #$00
    sta l_consumed
    lda #$00
    sta l_lines
    lda #$00
    sta l_last_count
    ldd #$0000
    std l_measured
    ldd #$0000
    std l_first_x
    ldd #$0000
    std l_last_x
    ldd #$0000
    std l_last_y
    ldd #$0000
    std l_calls
    lda #$00
    sta l_writes+3
    lda #$00
    sta l_writes+2
    lda #$00
    sta l_writes+1
    lda #$00
    sta l_writes
    rts
layout_enter
    jsr layout_reset
; Validate geometry before additions, so huge unsigned values cannot wrap into range.
    lda #$00
    sta g_cmp
    lda l_width
    cmpa g_cmp
    lblo layout_reject
    lbne layout_branch_1
    lda #$08
    sta g_cmp
    lda l_width+1
    cmpa g_cmp
    lblo layout_reject
layout_branch_1
    lda #$00
    sta g_cmp
    lda l_height
    cmpa g_cmp
    lblo layout_reject
    lbne layout_branch_2
    lda #$08
    sta g_cmp
    lda l_height+1
    cmpa g_cmp
    lblo layout_reject
layout_branch_2
    ldd #TEXT_WIDTH
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_width
    cmpa g_cmp
    lblo layout_branch_3
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_width+1
    cmpa g_cmp
    lblo layout_branch_3
    lbne layout_reject
layout_branch_3
    ldd #TEXT_HEIGHT
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_height
    cmpa g_cmp
    lblo layout_branch_4
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_height+1
    cmpa g_cmp
    lblo layout_branch_4
    lbne layout_reject
layout_branch_4
    ldd #TEXT_WIDTH
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_left
    cmpa g_cmp
    lblo layout_branch_5
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_left+1
    cmpa g_cmp
    lblo layout_branch_5
    lbne layout_reject
layout_branch_5
    ldd #TEXT_HEIGHT
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_top
    cmpa g_cmp
    lblo layout_branch_6
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_top+1
    cmpa g_cmp
    lblo layout_branch_6
    lbne layout_reject
layout_branch_6
    ldd l_left
    std l_right
    ldd l_right
    addd l_width
    std l_right
    ldd #TEXT_WIDTH
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_right
    cmpa g_cmp
    lblo layout_branch_7
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_right+1
    cmpa g_cmp
    lblo layout_branch_7
    lbne layout_reject
layout_branch_7
    ldd l_top
    std l_bottom
    ldd l_bottom
    addd l_height
    std l_bottom
    ldd #TEXT_HEIGHT
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_bottom
    cmpa g_cmp
    lblo layout_branch_8
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_bottom+1
    cmpa g_cmp
    lblo layout_branch_8
    lbne layout_reject
layout_branch_8
    lda #$02
    sta g_cmp
    lda l_align
    cmpa g_cmp
    lblo layout_branch_9
    lbne layout_reject
layout_branch_9
    lda #$08
    sta g_cmp
    lda l_pitch
    cmpa g_cmp
    lblo layout_reject
layout_branch_10
    ldd #$0000
    std l_temp
    lda l_length
    sta l_temp+1
    ldd l_temp
    addd l_source
    std l_temp
    lda #$00
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lbne layout_branch_12
    lda #$00
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lbne layout_branch_12
    jmp layout_branch_11
layout_branch_12
    lda l_source
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lblo layout_reject
    lbne layout_branch_13
    lda l_source+1
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lblo layout_reject
layout_branch_13
layout_branch_11
    ldd l_width
    std l_temp
    lsr l_temp
    ror l_temp+1
    lsr l_temp
    ror l_temp+1
    lsr l_temp
    ror l_temp+1
    lda l_temp+1
    sta l_columns
; An empty renderer call validates font, pen and source declarations without drawing.
    ldd l_left
    std t_x
    ldd l_top
    std t_y
    ldd l_source
    std t_source
    ldd l_left
    std t_clip_l
    ldd l_top
    std t_clip_t
    ldd l_right
    std t_clip_r
    ldd l_bottom
    std t_clip_b
    lda #$00
    sta t_length
    jsr text_string
    lda #$00
    sta g_cmp
    lda t_status
    cmpa g_cmp
    lbne layout_branch_14
    jmp layout_reject
layout_branch_14
    lda #$00
    sta l_pass
layout_pass
    lda #$00
    sta l_i
    ldd l_top
    std l_y
    lda #$00
    sta l_lines
    ldd #$0000
    std l_measured
layout_next
    lda l_length
    sta g_cmp
    lda l_i
    cmpa g_cmp
    lblo layout_branch_15
    jmp layout_pass_done
layout_branch_15
    ldd l_y
    std l_temp
    ldd l_temp
    addd #$0008
    std l_temp
    lda l_bottom
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lblo layout_branch_16
    lbne layout_reject
    lda l_bottom+1
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lblo layout_branch_16
    lbne layout_reject
layout_branch_16
    lda l_i
    sta l_start
    lda #$00
    sta l_count
    lda #$00
    sta g_cmp
    lda l_mode
    cmpa g_cmp
    lbne layout_scan
layout_branch_17
    lda l_length
    sta l_count
    lda l_length
    sta l_i
    jmp layout_position
; Character wrapping: consume LF after a full chunk too; never invent a trailing empty row.
layout_scan
    lda l_length
    sta g_cmp
    lda l_i
    cmpa g_cmp
    lblo layout_branch_18
    jmp layout_after_scan
layout_branch_18
    lda l_columns
    sta g_cmp
    lda l_count
    cmpa g_cmp
    lblo layout_branch_19
    jmp layout_after_scan
layout_branch_19
    ldd #$0000
    std l_pointer
    lda l_i
    sta l_pointer+1
    ldd l_pointer
    addd l_source
    std l_pointer
    ldx l_pointer
    lda ,x
    sta l_code
    lda #$0a
    sta g_cmp
    lda l_code
    cmpa g_cmp
    lbne layout_branch_20
    jmp layout_after_scan
layout_branch_20
    lda l_count
    adda #$01
    sta l_count
    lda l_i
    adda #$01
    sta l_i
    jmp layout_scan
layout_after_scan
    lda l_length
    sta g_cmp
    lda l_i
    cmpa g_cmp
    lblo layout_branch_21
    jmp layout_position
layout_branch_21
    ldd #$0000
    std l_pointer
    lda l_i
    sta l_pointer+1
    ldd l_pointer
    addd l_source
    std l_pointer
    ldx l_pointer
    lda ,x
    sta l_code
    lda #$0a
    sta g_cmp
    lda l_code
    cmpa g_cmp
    lbne layout_position
layout_branch_22
    lda l_i
    adda #$01
    sta l_i
layout_position
    ldd #$0000
    std l_line_width
    lda l_count
    sta l_line_width+1
; BEGIN WIDTH FIX: eight pixels of advance for every counted glyph.
    lsl l_line_width+1
    rol l_line_width
    lsl l_line_width+1
    rol l_line_width
    lsl l_line_width+1
    rol l_line_width
; END WIDTH FIX
    lda l_width
    sta g_cmp
    lda l_line_width
    cmpa g_cmp
    lblo layout_branch_23
    lbne layout_reject
    lda l_width+1
    sta g_cmp
    lda l_line_width+1
    cmpa g_cmp
    lblo layout_branch_23
    lbne layout_reject
layout_branch_23
    lda l_measured
    sta g_cmp
    lda l_line_width
    cmpa g_cmp
    lblo layout_branch_24
    lbne layout_branch_25
    lda l_measured+1
    sta g_cmp
    lda l_line_width+1
    cmpa g_cmp
    lblo layout_branch_24
    lbeq layout_branch_24
layout_branch_25
    ldd l_line_width
    std l_measured
layout_branch_24
    ldd l_width
    std l_temp
    ldd l_temp
    subd l_line_width
    std l_temp
    lda #$00
    sta g_cmp
    lda l_align
    cmpa g_cmp
    lbne layout_branch_28
    jmp layout_branch_26
layout_branch_28
    lda #$02
    sta g_cmp
    lda l_align
    cmpa g_cmp
    lbne layout_branch_29
    jmp layout_branch_27
layout_branch_29
    lsr l_temp
    ror l_temp+1
    jmp layout_branch_27
layout_branch_26
    ldd #$0000
    std l_temp
layout_branch_27
; BEGIN ORIGIN FIX: the unused-space offset is relative to the rectangle.
    ldd l_left
    std l_x
    ldd l_x
    addd l_temp
    std l_x
; END ORIGIN FIX
    lda #$00
    sta g_cmp
    lda l_lines
    cmpa g_cmp
    lbne layout_branch_30
layout_branch_31
    ldd l_x
    std l_first_x
layout_branch_30
    ldd l_x
    std l_last_x
    ldd l_last_x
    addd l_line_width
    std l_last_x
    ldd l_y
    std l_last_y
    lda l_count
    sta l_last_count
    lda l_lines
    adda #$01
    sta l_lines
    lda #$00
    sta g_cmp
    lda l_pass
    cmpa g_cmp
    lbne layout_branch_32
    jmp layout_advance
layout_branch_32
    ldd l_x
    std t_x
    ldd l_y
    std t_y
    ldd #$0000
    std t_source
    lda l_start
    sta t_source+1
    ldd t_source
    addd l_source
    std t_source
    lda l_count
    sta t_length
    jsr text_string
    ldd l_calls
    addd t_calls
    std l_calls
    lda l_writes+3
    adda t_writes+3
    sta l_writes+3
    lda l_writes+2
    adca t_writes+2
    sta l_writes+2
    lda l_writes+1
    adca t_writes+1
    sta l_writes+1
    lda l_writes
    adca t_writes
    sta l_writes
layout_advance
    ldd #$0000
    std l_temp
    lda l_pitch
    sta l_temp+1
    ldd l_y
    addd l_temp
    std l_y
    jmp layout_next
layout_pass_done
    lda #$00
    sta g_cmp
    lda l_pass
    cmpa g_cmp
    lbne layout_branch_33
layout_branch_34
    lda #$01
    sta l_pass
    jmp layout_pass
layout_branch_33
    lda l_length
    sta l_consumed
    lda #$01
    sta l_status
    rts
layout_reject
    jsr layout_reset
    rts
layout_end

The source and retained records contain 48 positive native calls for this machine, the custom caller, two completed faults and both repairs. The checks compare the whole bitmap allocation, public results, guards, unchanged source/font and selected entire native frames. Asm198x 0.0.71 and released Emu198x 0.41.0 are pinned. Retained replay and corruption tests support CI; they do not replace fresh execution on another machine or configuration.

Compare the four implementations and use the existing bitmap-text entry for the font, backend and original hardware sources.

Keep words together

For a paragraph or message panel, word-aware wrapping can be easier to read. Choose the maintained word-wrap/6809.inc variant over the same bitmap-text renderer. It adds 101 code bytes (1,564 total) and keeps the same 64-byte TL allocation and public contract. The original character-wrap component above remains useful for compact labels and fixed grids.

In seven columns, ONE THREE FIVE becomes ONE / THREE / FIVE. In two columns, ABCDE still becomes AB / CD / E: a word wider than the panel has to split if the routine is to make progress.

The exact policy matters when preparing messages:

  • Fit at most width div 8 cells. If an LF or space follows a full line, consume that delimiter as the break.
  • Otherwise, break at the last space after the first byte of the current line, omitting that one separator. If there is none, split at the cell limit.
  • Preserve every other space. Leading, repeated and trailing spaces remain data; this is not whitespace normalisation. Three spaces in a one-column panel occupy two rows because the middle space becomes a break.
  • Byte 10 is LF in block mode. Consecutive LFs retain interior empty lines; a final LF adds no extra trailing row. Byte zero is a glyph code, not a terminator.

layout_line and layout_measure retain their counted-line behaviour: LF occupies a glyph cell there. Unsupported codes use the configured fallback. Empty input has zero rows and no drawing but still validates the declaration.

Both layout components deliberately export the same entry labels. Include one of them, after bitmap-text, rather than including both. The TL contract and single-line calls stay the same. Words can require more rows than character wrapping: the complete first pass checks the height before drawing, so a panel that is now too short rejects without partial text.

Pitch measures the distance between row origins. Five rows at pitch 16 need (5-1)*16+8=72 scanlines. Opaque mode paints actual glyph cells, not the unused panel width or empty rows. Clear the panel you own before replacing a longer message, or old letters may remain.

Try a word-aware panel

Use the complete word-aware caller. The include replaces text-layout; keep the same T/BP/font/backend setup. The normal call still looks like this:

Use the same TL contract with word-aware layout

prepare.inc
; BEGIN LAYOUT INPUTS
    ldd #PANEL_LEFT
    std TL+0
    ldd #PANEL_TOP
    std TL+2
    ldd #PANEL_WIDTH
    std TL+4
    ldd #PANEL_HEIGHT
    std TL+6
    ldd #label_data
    std TL+10
    lda #PANEL_ALIGN
    sta TL+8
    lda #LINE_PITCH
    sta TL+9
    lda #MESSAGE_LENGTH
    sta TL+12
    jsr layout_block
; END LAYOUT INPUTS

Build customise.asm with the same includes. It narrows the panel from 176 to 104 pixels, selects right alignment, changes pitch from 12 to 16 and supplies another original counted message. The result has five lines inside the existing 80-scanline height. The rows are A NARROW / PANEL / WRAPS THIS / MESSAGE / PRESS FIRE. PANEL and MESSAGE stay whole; character wrapping split both words in the earlier example.

Change the panel, alignment, pitch and counted message

customise.asm
CUSTOM_LAYOUT equ 1
; SPDX-License-Identifier: MIT
; Complete caller: change layout inputs without changing the reusable renderer.
PANEL_LEFT equ 21
PANEL_TOP equ 37
PANEL_WIDTH equ 104
PANEL_HEIGHT equ 80
PANEL_ALIGN equ 2
LINE_PITCH equ 16
MESSAGE_LENGTH equ 44
    include "caller.inc"
The custom message wraps into five right-aligned lines inside a narrower panel on Dragon 32 PMODE 4.
Verified native customisation using the unchanged layout and drawing routines. The narrow panel keeps words together; pitch controls the vertical footprint.

Edit customise-message.inc and update MESSAGE_LENGTH together. The routine has no terminator scan to rescue an incorrect count. More rows or wider text can increase drawing work, even though code and context allocations stay the same. The proportional option below changes glyph advances; different glyph sizes, whitespace rules or video modes need their own qualification.

A word breaks despite a usable space

The scan has found a space, but the caller discards that saved boundary and cuts at capacity. ONE THREE FIVE becomes ONE THR / EE FIVE in a seven-column panel. The count is bounded, yet the intended word policy is lost.

Deliberately wrong: discard the saved word boundary

6809-break.inc, excerpt
; BEGIN WRONG
    jmp layout_position ; WRONG: discard the saved word boundary
ONE THREE FIVE is cut into ONE THR and EE FIVE despite a usable word boundary.
The faulty word-break branch completes its native call but splits words unnecessarily.

Restore both the drawn count and the source resume position. The common delimiter step then consumes the selected space exactly once:

Correct: resume at the saved separator

6809-break.inc, excerpt
; BEGIN FIX
    lda l_break_count
    cmpa g_cmp
    lbne layout_branch_28
    jmp layout_position
layout_branch_28
    lda l_break_count
    sta l_count
    lda l_break_i
    sta l_i
ONE, THREE and FIVE occupy three complete centred lines.
The repaired native caller keeps each word whole. The full bitmap and outputs match the independent expectation.

What does word wrapping cost?

The extra work is scanning for a separator and possibly rescanning the start of a word on the next row, in both planning passes. Every row consumes at least one source byte; counted input is bounded at 255. There is no extra context or font allocation. This is not proportional text, kerning or whitespace normalisation.

For the same custom message and panel, changed line breaks and two omitted spaces reduce span calls from 709 to 693. Destination-byte writes change from 1,117 to 1,085. Those are observed drawing-work counts, not a total-speed comparison: planning costs more, and placement changes byte phase too. Measure the complete call before claiming a frame budget.

The word-aware component retains 63 positive native calls, the customisation and three faults with executed repairs on this machine. Full memory, inputs/outputs, guards, unchanged source/font and selected native frames match independent expectations. The four-machine total is 320 observations with Asm198x 0.0.71 and Emu198x 0.41.0. Source and records include the resource contract and reproduction commands.

Read the reusable word-aware component

Selectable word-aware variant; include instead of text-layout

6809.inc
; SPDX-License-Identifier: MIT
; Bounded counted 8x8 text layout over the unchanged bitmap-text component.
; Include bitmap-text once; reserve TL=64, T=96 and BP=48 separate bytes.
; TL inputs 0..12 survive. T/BP are owned workspaces. See README.md.
l_left equ TL+0
l_top equ TL+2
l_width equ TL+4
l_height equ TL+6
l_align equ TL+8
l_pitch equ TL+9
l_source equ TL+10
l_length equ TL+12
l_status equ TL+13
l_consumed equ TL+14
l_lines equ TL+15
l_last_count equ TL+16
l_measured equ TL+18
l_first_x equ TL+20
l_last_x equ TL+22
l_last_y equ TL+24
l_calls equ TL+26
l_writes equ TL+28
l_mode equ TL+32
l_pass equ TL+33
l_i equ TL+34
l_start equ TL+35
l_count equ TL+36
l_columns equ TL+37
l_temp equ TL+38
l_right equ TL+40
l_bottom equ TL+42
l_y equ TL+44
l_x equ TL+46
l_pointer equ TL+48
l_code equ TL+50
l_line_width equ TL+52
l_work equ TL+54
l_break_i equ TL+58
l_break_count equ TL+59
layout_measure
    jsr layout_reset
    ldd #$0000
    std l_measured
    lda l_length
    sta l_measured+1
    lsl l_measured+1
    rol l_measured
    lsl l_measured+1
    rol l_measured
    lsl l_measured+1
    rol l_measured
    lda #$01
    sta l_status
    rts
layout_line
    lda #$00
    sta l_mode
    jmp layout_enter
layout_block
    lda #$01
    sta l_mode
    jmp layout_enter
layout_reset
    lda #$00
    sta l_status
    lda #$00
    sta l_consumed
    lda #$00
    sta l_lines
    lda #$00
    sta l_last_count
    ldd #$0000
    std l_measured
    ldd #$0000
    std l_first_x
    ldd #$0000
    std l_last_x
    ldd #$0000
    std l_last_y
    ldd #$0000
    std l_calls
    lda #$00
    sta l_writes+3
    lda #$00
    sta l_writes+2
    lda #$00
    sta l_writes+1
    lda #$00
    sta l_writes
    rts
layout_enter
    jsr layout_reset
; Validate geometry before additions, so huge unsigned values cannot wrap into range.
    lda #$00
    sta g_cmp
    lda l_width
    cmpa g_cmp
    lblo layout_reject
    lbne layout_branch_1
    lda #$08
    sta g_cmp
    lda l_width+1
    cmpa g_cmp
    lblo layout_reject
layout_branch_1
    lda #$00
    sta g_cmp
    lda l_height
    cmpa g_cmp
    lblo layout_reject
    lbne layout_branch_2
    lda #$08
    sta g_cmp
    lda l_height+1
    cmpa g_cmp
    lblo layout_reject
layout_branch_2
    ldd #TEXT_WIDTH
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_width
    cmpa g_cmp
    lblo layout_branch_3
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_width+1
    cmpa g_cmp
    lblo layout_branch_3
    lbne layout_reject
layout_branch_3
    ldd #TEXT_HEIGHT
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_height
    cmpa g_cmp
    lblo layout_branch_4
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_height+1
    cmpa g_cmp
    lblo layout_branch_4
    lbne layout_reject
layout_branch_4
    ldd #TEXT_WIDTH
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_left
    cmpa g_cmp
    lblo layout_branch_5
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_left+1
    cmpa g_cmp
    lblo layout_branch_5
    lbne layout_reject
layout_branch_5
    ldd #TEXT_HEIGHT
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_top
    cmpa g_cmp
    lblo layout_branch_6
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_top+1
    cmpa g_cmp
    lblo layout_branch_6
    lbne layout_reject
layout_branch_6
    ldd l_left
    std l_right
    ldd l_right
    addd l_width
    std l_right
    ldd #TEXT_WIDTH
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_right
    cmpa g_cmp
    lblo layout_branch_7
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_right+1
    cmpa g_cmp
    lblo layout_branch_7
    lbne layout_reject
layout_branch_7
    ldd l_top
    std l_bottom
    ldd l_bottom
    addd l_height
    std l_bottom
    ldd #TEXT_HEIGHT
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_bottom
    cmpa g_cmp
    lblo layout_branch_8
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_bottom+1
    cmpa g_cmp
    lblo layout_branch_8
    lbne layout_reject
layout_branch_8
    lda #$02
    sta g_cmp
    lda l_align
    cmpa g_cmp
    lblo layout_branch_9
    lbne layout_reject
layout_branch_9
    lda #$08
    sta g_cmp
    lda l_pitch
    cmpa g_cmp
    lblo layout_reject
layout_branch_10
    ldd #$0000
    std l_temp
    lda l_length
    sta l_temp+1
    ldd l_temp
    addd l_source
    std l_temp
    lda #$00
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lbne layout_branch_12
    lda #$00
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lbne layout_branch_12
    jmp layout_branch_11
layout_branch_12
    lda l_source
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lblo layout_reject
    lbne layout_branch_13
    lda l_source+1
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lblo layout_reject
layout_branch_13
layout_branch_11
    ldd l_width
    std l_temp
    lsr l_temp
    ror l_temp+1
    lsr l_temp
    ror l_temp+1
    lsr l_temp
    ror l_temp+1
    lda l_temp+1
    sta l_columns
; An empty renderer call validates font, pen and source declarations without drawing.
    ldd l_left
    std t_x
    ldd l_top
    std t_y
    ldd l_source
    std t_source
    ldd l_left
    std t_clip_l
    ldd l_top
    std t_clip_t
    ldd l_right
    std t_clip_r
    ldd l_bottom
    std t_clip_b
    lda #$00
    sta t_length
    jsr text_string
    lda #$00
    sta g_cmp
    lda t_status
    cmpa g_cmp
    lbne layout_branch_14
    jmp layout_reject
layout_branch_14
    lda #$00
    sta l_pass
layout_pass
    lda #$00
    sta l_i
    ldd l_top
    std l_y
    lda #$00
    sta l_lines
    ldd #$0000
    std l_measured
layout_next
    lda l_length
    sta g_cmp
    lda l_i
    cmpa g_cmp
    lblo layout_branch_15
    jmp layout_pass_done
layout_branch_15
    ldd l_y
    std l_temp
    ldd l_temp
    addd #$0008
    std l_temp
    lda l_bottom
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lblo layout_branch_16
    lbne layout_reject
    lda l_bottom+1
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lblo layout_branch_16
    lbne layout_reject
layout_branch_16
    lda l_i
    sta l_start
    lda #$00
    sta l_count
    lda #$00
    sta l_break_count
    lda #$00
    sta g_cmp
    lda l_mode
    cmpa g_cmp
    lbne layout_scan
layout_branch_17
    lda l_length
    sta l_count
    lda l_length
    sta l_i
    jmp layout_position
; Scan a word-aware row: consume LF after a full chunk too; never invent a trailing empty row.
layout_scan
    lda l_length
    sta g_cmp
    lda l_i
    cmpa g_cmp
    lblo layout_branch_18
    jmp layout_after_scan
layout_branch_18
    lda l_columns
    sta g_cmp
    lda l_count
    cmpa g_cmp
    lblo layout_branch_19
    jmp layout_after_scan
layout_branch_19
    ldd #$0000
    std l_pointer
    lda l_i
    sta l_pointer+1
    ldd l_pointer
    addd l_source
    std l_pointer
    ldx l_pointer
    lda ,x
    sta l_code
    lda #$0a
    sta g_cmp
    lda l_code
    cmpa g_cmp
    lbne layout_branch_20
    jmp layout_after_scan
layout_branch_20
    lda #$20
    sta g_cmp
    lda l_code
    cmpa g_cmp
    lbne layout_branch_21
layout_branch_22
    lda #$00
    sta g_cmp
    lda l_count
    cmpa g_cmp
    lbne layout_branch_23
    jmp layout_branch_21
layout_branch_23
    lda l_i
    sta l_break_i
    lda l_count
    sta l_break_count
layout_branch_21
    lda l_count
    adda #$01
    sta l_count
    lda l_i
    adda #$01
    sta l_i
    jmp layout_scan
layout_after_scan
    lda l_length
    sta g_cmp
    lda l_i
    cmpa g_cmp
    lblo layout_branch_24
    jmp layout_position
layout_branch_24
    ldd #$0000
    std l_pointer
    lda l_i
    sta l_pointer+1
    ldd l_pointer
    addd l_source
    std l_pointer
    ldx l_pointer
    lda ,x
    sta l_code
    lda #$0a
    sta g_cmp
    lda l_code
    cmpa g_cmp
    lbne layout_branch_26
    jmp layout_branch_25
layout_branch_26
    lda #$20
    sta g_cmp
    lda l_code
    cmpa g_cmp
    lbne layout_branch_27
    jmp layout_branch_25
layout_branch_27
; Backtrack to the last eligible space; otherwise split an overlong word.
    lda #$00
    sta g_cmp
; BEGIN BREAK FIX
    lda l_break_count
    cmpa g_cmp
    lbne layout_branch_28
    jmp layout_position
layout_branch_28
    lda l_break_count
    sta l_count
    lda l_break_i
    sta l_i
; END BREAK FIX
layout_branch_25
    lda l_i
    adda #$01
    sta l_i
layout_position
    ldd #$0000
    std l_line_width
    lda l_count
    sta l_line_width+1
; BEGIN WIDTH FIX: eight pixels of advance for every counted glyph.
    lsl l_line_width+1
    rol l_line_width
    lsl l_line_width+1
    rol l_line_width
    lsl l_line_width+1
    rol l_line_width
; END WIDTH FIX
    lda l_width
    sta g_cmp
    lda l_line_width
    cmpa g_cmp
    lblo layout_branch_29
    lbne layout_reject
    lda l_width+1
    sta g_cmp
    lda l_line_width+1
    cmpa g_cmp
    lblo layout_branch_29
    lbne layout_reject
layout_branch_29
    lda l_measured
    sta g_cmp
    lda l_line_width
    cmpa g_cmp
    lblo layout_branch_30
    lbne layout_branch_31
    lda l_measured+1
    sta g_cmp
    lda l_line_width+1
    cmpa g_cmp
    lblo layout_branch_30
    lbeq layout_branch_30
layout_branch_31
    ldd l_line_width
    std l_measured
layout_branch_30
    ldd l_width
    std l_temp
    ldd l_temp
    subd l_line_width
    std l_temp
    lda #$00
    sta g_cmp
    lda l_align
    cmpa g_cmp
    lbne layout_branch_34
    jmp layout_branch_32
layout_branch_34
    lda #$02
    sta g_cmp
    lda l_align
    cmpa g_cmp
    lbne layout_branch_35
    jmp layout_branch_33
layout_branch_35
    lsr l_temp
    ror l_temp+1
    jmp layout_branch_33
layout_branch_32
    ldd #$0000
    std l_temp
layout_branch_33
; BEGIN ORIGIN FIX: the unused-space offset is relative to the rectangle.
    ldd l_left
    std l_x
    ldd l_x
    addd l_temp
    std l_x
; END ORIGIN FIX
    lda #$00
    sta g_cmp
    lda l_lines
    cmpa g_cmp
    lbne layout_branch_36
layout_branch_37
    ldd l_x
    std l_first_x
layout_branch_36
    ldd l_x
    std l_last_x
    ldd l_last_x
    addd l_line_width
    std l_last_x
    ldd l_y
    std l_last_y
    lda l_count
    sta l_last_count
    lda l_lines
    adda #$01
    sta l_lines
    lda #$00
    sta g_cmp
    lda l_pass
    cmpa g_cmp
    lbne layout_branch_38
    jmp layout_advance
layout_branch_38
    ldd l_x
    std t_x
    ldd l_y
    std t_y
    ldd #$0000
    std t_source
    lda l_start
    sta t_source+1
    ldd t_source
    addd l_source
    std t_source
    lda l_count
    sta t_length
    jsr text_string
    ldd l_calls
    addd t_calls
    std l_calls
    lda l_writes+3
    adda t_writes+3
    sta l_writes+3
    lda l_writes+2
    adca t_writes+2
    sta l_writes+2
    lda l_writes+1
    adca t_writes+1
    sta l_writes+1
    lda l_writes
    adca t_writes
    sta l_writes
layout_advance
    ldd #$0000
    std l_temp
    lda l_pitch
    sta l_temp+1
    ldd l_y
    addd l_temp
    std l_y
    jmp layout_next
layout_pass_done
    lda #$00
    sta g_cmp
    lda l_pass
    cmpa g_cmp
    lbne layout_branch_39
layout_branch_40
    lda #$01
    sta l_pass
    jmp layout_pass
layout_branch_39
    lda l_length
    sta l_consumed
    lda #$01
    sta l_status
    rts
layout_reject
    jsr layout_reset
    rts
layout_end

Captures preserve native borders and horizontal sampling. Use logical pixel coordinates for layout calculations; these are not calibrated CRT aspect-ratio demonstrations.

Give narrow letters less room

An I does not need the same space as a W. The proportional variant crops our original font to its ink and supplies one width byte per glyph. Add the chosen letter spacing to get the advance: the distance to the next glyph. At spacing 1, six Is need 24 pixels and six Ws need 36. Both needed 48 in the fixed-cell version.

The original two-line prompt uses narrower, proportional letter spacing.
The same 33-byte prompt, with glyph-specific advances. Original Code198x font, cropped from the maintained MIT design; verified native reset capture.

Choose proportional-text/6809.inc instead of either fixed-width layout include. Keep the existing bitmap-text component and this machine’s backend. Reserve 80 bytes for TL, rather than 64; T and BP still need 96 and 48 bytes. Their addresses and video obligations remain as above. Use the complete proportional caller for the new font and metric includes as well as the ordinary setup call.

The first 13 TL input bytes and output offsets remain familiar. Add a native-order word metric pointer at TL+64 and byte letter spacing at TL+66. The table has one width byte per declared glyph, indexed by resolved code − first code. Resolve an unsupported code to the configured fallback before looking up its width. Each width is 1..8, spacing is 0..2, and their sum must be at most 8. Inputs TL+0..12 and +64..66 survive; the rest of the 80-byte context is scratch or output.

Supply the metric table and letter spacing before layout_block

prepare.inc
; BEGIN LAYOUT INPUTS
    ldd #PANEL_LEFT
    std TL+0
    ldd #PANEL_TOP
    std TL+2
    ldd #PANEL_WIDTH
    std TL+4
    ldd #PANEL_HEIGHT
    std TL+6
    ldd #label_data
    std TL+10
    lda #PANEL_ALIGN
    sta TL+8
    lda #LINE_PITCH
    sta TL+9
    lda #MESSAGE_LENGTH
    sta TL+12
    ldd #metric_data
    std TL+64
    lda #LETTER_SPACING
    sta TL+66
    jsr layout_block
; END LAYOUT INPUTS

Measurement now reads the string, fallback and widths. Initialise the whole declaration before layout_measure; the old count-only call cannot measure this font. It returns the sum at TL+18, even if that sum exceeds panel width. layout_line checks fit before subtracting for alignment. The same advances decide where a word fits in block mode, then place its glyphs during rendering. This is an advance sum, not optical centring: the final spacing slot is included too.

The source is still counted bytes, the glyph is still eight rows, and top is a cell origin rather than a baseline. LF only breaks in block mode; elsewhere it is a glyph or fallback. Word wrapping keeps the explicit space/LF policy described above, but tests pixel advances rather than width div 8. No kerning pairs, shaping, bearings or case conversion are supplied.

Change the spacing deliberately

The maintained custom caller uses spacing 2 instead of 1, a 104-pixel right-aligned panel, pitch 16 and the 44-byte message used above. It produces 4 rows, with a maximum advance of 94 pixels. The glyphs keep their shapes; their origins move further apart. I! WIDE measures 32 pixels at spacing 1 and 39 at spacing 2.

Change letter spacing alongside the panel and message

customise.asm
LETTER_SPACING equ 2
CUSTOM_LAYOUT equ 1
; SPDX-License-Identifier: MIT
; Complete caller: change layout inputs without changing the reusable renderer.
PANEL_LEFT equ 21
PANEL_TOP equ 37
PANEL_WIDTH equ 104
PANEL_HEIGHT equ 80
PANEL_ALIGN equ 2
LINE_PITCH equ 16
MESSAGE_LENGTH equ 44
    include "caller.inc"
The custom message uses wider letter spacing in the narrow right-aligned panel.
Executed customisation: the width table stays the same; spacing, panel and message change.

Edit a glyph’s eight row bytes and its width together. Keep ink inside the declared width and width + spacing ≤ 8. A smaller width crops pixels; it does not shrink the letter. A space has width 3 in this original font. A different font needs readable, stable rows and a matching table for its declared range. Those allocations must not overlap the bitmap, scratch, code or stack.

Letters spread beyond their measured end

This next fragment is deliberately wrong. Measurement uses proportional advances, but drawing still moves eight pixels after each glyph. I! WIDE starts at the planned position, then spreads out. Changing the panel origin would hide one symptom while leaving the disagreement in place.

Deliberately wrong: retain a fixed eight-pixel movement

6809-advance.inc, excerpt
; BEGIN WRONG
    ldd l_x
    addd #8
    std l_x
I! WIDE spreads across fixed eight-pixel positions despite its smaller measured width.
The faulty native call completes. Its whole bitmap differs from the correct proportional expectation.

Restore the very advance used by the planner:

Correct: advance by this glyph's width plus spacing

6809-advance.inc, excerpt
; BEGIN FIX
    ldd l_x
    addd l_advance
    std l_x
The repaired I! WIDE label fits its measured proportional advance.
The repaired caller executes and reproduces the original working fixture bytes.

What the smaller letters cost

This layout variant uses 2,230 code bytes and 80 TL bytes, compared with the fixed alternatives above. The cropped font still uses 512 bytes; its width table adds 64. The unchanged renderer and backend still cost 2,058 and 470 code bytes. BBC row tables, video, source and stack remain separate allocations. Keep the conservative 80-byte stack budget plus your caller/interrupt depth.

Exact workload Normal prompt Narrow custom panel
Source bytes 33 44
Logical rows 2 4
Maximum line advance 104 pixels 94 pixels
Backend spans 511 701
Destination-byte writes 682 952

Smaller advances can reduce opaque background work, but also change byte phase and line breaks. The component validates the table, reads glyph widths, may rescan words and calls the baseline renderer once per glyph. These drawing counts do not establish a speed gain or frame budget. Measure static labels once where useful; pre-rendering trades another asset for less runtime work.

The complete planning pass validates the declaration and height before drawing. Rejection leaves zero public results and the bitmap intact. Source, font, metrics, pens and mappings must remain stable through both passes. Opaque text clears each glyph’s advance, including spacing, for eight rows; unused panel space and blank lines still need your own clearing. Transparent zero bits preserve the picture underneath.

The component owns T/BP and their scissor, is non-re-entrant, and supplies no OS or interrupt protection, bank switching or safe display interval. CPU clobbers remain those of the renderer above. The 88 positive native calls, custom caller and wrong/fixed observations compare advance sums, row placement, complete destination memory, neighbours, unchanged source/font/metrics and selected whole frames. Tests cover all eight horizontal phases, empty/255-code strings, fallback, spacing limits, malformed metrics and atomic rejection. The exact dragon reset/display configuration is qualified with pinned Asm198x 0.0.71 and Emu198x 0.41.0; stock-OS loading, live tearing, original hardware and other modes remain separate checks.

Read the reusable proportional component

Proportional measurement and word-aware layout over the existing renderer

6809.inc
; SPDX-License-Identifier: MIT
; Bounded proportional layout over the unchanged eight-row bitmap-text component.
; Include bitmap-text once; reserve TL=80, T=96 and BP=48 separate bytes.
; TL inputs 0..12 survive. TL+64 metric pointer, TL+66 spacing are additional preserved inputs. T/BP are owned workspaces. See README.md.
l_left equ TL+0
l_top equ TL+2
l_width equ TL+4
l_height equ TL+6
l_align equ TL+8
l_pitch equ TL+9
l_source equ TL+10
l_length equ TL+12
l_status equ TL+13
l_consumed equ TL+14
l_lines equ TL+15
l_last_count equ TL+16
l_measured equ TL+18
l_first_x equ TL+20
l_last_x equ TL+22
l_last_y equ TL+24
l_calls equ TL+26
l_writes equ TL+28
l_mode equ TL+32
l_pass equ TL+33
l_i equ TL+34
l_start equ TL+35
l_count equ TL+36
l_columns equ TL+37
l_temp equ TL+38
l_right equ TL+40
l_bottom equ TL+42
l_y equ TL+44
l_x equ TL+46
l_pointer equ TL+48
l_code equ TL+50
l_line_width equ TL+52
l_work equ TL+54
l_break_i equ TL+58
l_break_count equ TL+59
l_metrics equ TL+64
l_spacing equ TL+66
l_advance equ TL+68
l_scan_width equ TL+70
l_break_width equ TL+72
l_glyph equ TL+74
l_metric_i equ TL+75
l_draw_i equ TL+77
layout_measure
    lda #$02
    sta l_mode
    jmp layout_enter
layout_line
    lda #$00
    sta l_mode
    jmp layout_enter
layout_block
    lda #$01
    sta l_mode
    jmp layout_enter
layout_reset
    lda #$00
    sta l_status
    lda #$00
    sta l_consumed
    lda #$00
    sta l_lines
    lda #$00
    sta l_last_count
    ldd #$0000
    std l_measured
    ldd #$0000
    std l_first_x
    ldd #$0000
    std l_last_x
    ldd #$0000
    std l_last_y
    ldd #$0000
    std l_calls
    lda #$00
    sta l_writes+3
    lda #$00
    sta l_writes+2
    lda #$00
    sta l_writes+1
    lda #$00
    sta l_writes
    rts
layout_enter
    jsr layout_reset
; Validate geometry before additions, so huge unsigned values cannot wrap into range.
    lda #$00
    sta g_cmp
    lda l_width
    cmpa g_cmp
    lblo layout_reject
    lbne layout_branch_1
    lda #$08
    sta g_cmp
    lda l_width+1
    cmpa g_cmp
    lblo layout_reject
layout_branch_1
    lda #$00
    sta g_cmp
    lda l_height
    cmpa g_cmp
    lblo layout_reject
    lbne layout_branch_2
    lda #$08
    sta g_cmp
    lda l_height+1
    cmpa g_cmp
    lblo layout_reject
layout_branch_2
    ldd #TEXT_WIDTH
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_width
    cmpa g_cmp
    lblo layout_branch_3
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_width+1
    cmpa g_cmp
    lblo layout_branch_3
    lbne layout_reject
layout_branch_3
    ldd #TEXT_HEIGHT
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_height
    cmpa g_cmp
    lblo layout_branch_4
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_height+1
    cmpa g_cmp
    lblo layout_branch_4
    lbne layout_reject
layout_branch_4
    ldd #TEXT_WIDTH
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_left
    cmpa g_cmp
    lblo layout_branch_5
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_left+1
    cmpa g_cmp
    lblo layout_branch_5
    lbne layout_reject
layout_branch_5
    ldd #TEXT_HEIGHT
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_top
    cmpa g_cmp
    lblo layout_branch_6
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_top+1
    cmpa g_cmp
    lblo layout_branch_6
    lbne layout_reject
layout_branch_6
    ldd l_left
    std l_right
    ldd l_right
    addd l_width
    std l_right
    ldd #TEXT_WIDTH
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_right
    cmpa g_cmp
    lblo layout_branch_7
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_right+1
    cmpa g_cmp
    lblo layout_branch_7
    lbne layout_reject
layout_branch_7
    ldd l_top
    std l_bottom
    ldd l_bottom
    addd l_height
    std l_bottom
    ldd #TEXT_HEIGHT
    std l_temp
    lda l_temp
    sta g_cmp
    lda l_bottom
    cmpa g_cmp
    lblo layout_branch_8
    lbne layout_reject
    lda l_temp+1
    sta g_cmp
    lda l_bottom+1
    cmpa g_cmp
    lblo layout_branch_8
    lbne layout_reject
layout_branch_8
    lda #$02
    sta g_cmp
    lda l_align
    cmpa g_cmp
    lblo layout_branch_9
    lbne layout_reject
layout_branch_9
    lda #$08
    sta g_cmp
    lda l_pitch
    cmpa g_cmp
    lblo layout_reject
layout_branch_10
    ldd #$0000
    std l_temp
    lda l_length
    sta l_temp+1
    ldd l_temp
    addd l_source
    std l_temp
    lda #$00
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lbne layout_branch_12
    lda #$00
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lbne layout_branch_12
    jmp layout_branch_11
layout_branch_12
    lda l_source
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lblo layout_reject
    lbne layout_branch_13
    lda l_source+1
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lblo layout_reject
layout_branch_13
layout_branch_11
    ldd l_width
    std l_temp
    lsr l_temp
    ror l_temp+1
    lsr l_temp
    ror l_temp+1
    lsr l_temp
    ror l_temp+1
    lda l_temp+1
    sta l_columns
; An empty renderer call validates font, pen and source declarations without drawing.
    ldd l_left
    std t_x
    ldd l_top
    std t_y
    ldd l_source
    std t_source
    ldd l_left
    std t_clip_l
    ldd l_top
    std t_clip_t
    ldd l_right
    std t_clip_r
    ldd l_bottom
    std t_clip_b
    lda #$00
    sta t_length
    jsr text_string
    lda #$00
    sta g_cmp
    lda t_status
    cmpa g_cmp
    lbne layout_branch_14
    jmp layout_reject
layout_branch_14
    lda #$02
    sta g_cmp
    lda l_spacing
    cmpa g_cmp
    lblo layout_branch_15
    lbne layout_reject
layout_branch_15
    ldd l_metrics
    std l_temp
    ldd l_temp
    addd t_count
    std l_temp
    lda #$00
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lbne layout_branch_17
    lda #$00
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lbne layout_branch_17
    jmp layout_branch_16
layout_branch_17
    lda l_metrics
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lblo layout_reject
    lbne layout_branch_18
    lda l_metrics+1
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lblo layout_reject
layout_branch_18
layout_branch_16
    ldd #$0000
    std l_metric_i
layout_validate_metric
    lda t_count
    sta g_cmp
    lda l_metric_i
    cmpa g_cmp
    lblo layout_branch_19
    lbne layout_metrics_ready
    lda t_count+1
    sta g_cmp
    lda l_metric_i+1
    cmpa g_cmp
    lblo layout_branch_19
    jmp layout_metrics_ready
layout_branch_19
    ldd l_metrics
    std l_pointer
    ldd l_pointer
    addd l_metric_i
    std l_pointer
    jsr layout_read_pointer
    sta l_glyph
    lda #$01
    sta g_cmp
    lda l_glyph
    cmpa g_cmp
    lblo layout_reject
layout_branch_20
    ldd #$0000
    std l_temp
    lda l_glyph
    sta l_temp+1
    ldd #$0000
    std l_advance
    lda l_spacing
    sta l_advance+1
    ldd l_temp
    addd l_advance
    std l_temp
    lda #$00
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lblo layout_branch_21
    lbne layout_reject
    lda #$08
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lblo layout_branch_21
    lbne layout_reject
layout_branch_21
    ldd l_metric_i
    addd #$0001
    std l_metric_i
    jmp layout_validate_metric
layout_metrics_ready
    lda #$00
    sta l_pass
layout_pass
    lda #$00
    sta l_i
    ldd l_top
    std l_y
    lda #$00
    sta l_lines
    ldd #$0000
    std l_measured
layout_next
    lda l_length
    sta g_cmp
    lda l_i
    cmpa g_cmp
    lblo layout_branch_22
    jmp layout_pass_done
layout_branch_22
    ldd l_y
    std l_temp
    ldd l_temp
    addd #$0008
    std l_temp
    lda l_bottom
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lblo layout_branch_23
    lbne layout_reject
    lda l_bottom+1
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lblo layout_branch_23
    lbne layout_reject
layout_branch_23
    lda l_i
    sta l_start
    lda #$00
    sta l_count
    lda #$00
    sta l_break_count
    ldd #$0000
    std l_line_width
layout_scan
    lda l_length
    sta g_cmp
    lda l_i
    cmpa g_cmp
    lblo layout_branch_24
    jmp layout_after_scan
layout_branch_24
    ldd #$0000
    std l_pointer
    lda l_i
    sta l_pointer+1
    ldd l_pointer
    addd l_source
    std l_pointer
    ldx l_pointer
    lda ,x
    sta l_code
    lda #$01
    sta g_cmp
    lda l_mode
    cmpa g_cmp
    lbne layout_branch_25
layout_branch_26
    lda #$0a
    sta g_cmp
    lda l_code
    cmpa g_cmp
    lbne layout_branch_27
    jmp layout_after_scan
layout_branch_27
layout_branch_25
    jsr layout_glyph_advance
    ldd l_line_width
    std l_scan_width
    ldd l_scan_width
    addd l_advance
    std l_scan_width
    lda #$01
    sta g_cmp
    lda l_mode
    cmpa g_cmp
    lbne layout_branch_28
layout_branch_29
    lda l_width
    sta g_cmp
    lda l_scan_width
    cmpa g_cmp
    lblo layout_branch_30
    lbne layout_after_scan
    lda l_width+1
    sta g_cmp
    lda l_scan_width+1
    cmpa g_cmp
    lblo layout_branch_30
    lbne layout_after_scan
layout_branch_30
layout_branch_28
    lda #$20
    sta g_cmp
    lda l_code
    cmpa g_cmp
    lbne layout_branch_31
layout_branch_32
    lda #$00
    sta g_cmp
    lda l_count
    cmpa g_cmp
    lbne layout_branch_33
    jmp layout_branch_31
layout_branch_33
    lda l_i
    sta l_break_i
    lda l_count
    sta l_break_count
    ldd l_line_width
    std l_break_width
layout_branch_31
    ldd l_scan_width
    std l_line_width
    lda l_count
    adda #$01
    sta l_count
    lda l_i
    adda #$01
    sta l_i
    jmp layout_scan
layout_after_scan
    lda #$01
    sta g_cmp
    lda l_mode
    cmpa g_cmp
    lbne layout_position
layout_branch_34
    lda l_length
    sta g_cmp
    lda l_i
    cmpa g_cmp
    lblo layout_branch_35
    jmp layout_position
layout_branch_35
    ldd #$0000
    std l_pointer
    lda l_i
    sta l_pointer+1
    ldd l_pointer
    addd l_source
    std l_pointer
    ldx l_pointer
    lda ,x
    sta l_code
    lda #$0a
    sta g_cmp
    lda l_code
    cmpa g_cmp
    lbne layout_branch_37
    jmp layout_branch_36
layout_branch_37
    lda #$20
    sta g_cmp
    lda l_code
    cmpa g_cmp
    lbne layout_branch_38
    jmp layout_branch_36
layout_branch_38
; Backtrack to the last eligible space; otherwise split an overlong word.
    lda #$00
    sta g_cmp
    lda l_break_count
    cmpa g_cmp
    lbne layout_branch_39
    jmp layout_position
layout_branch_39
    lda l_break_count
    sta l_count
    lda l_break_i
    sta l_i
    ldd l_break_width
    std l_line_width
layout_branch_36
    lda l_i
    adda #$01
    sta l_i
layout_position
    lda l_start
    sta g_cmp
    lda l_i
    cmpa g_cmp
    lbne layout_branch_40
layout_branch_41
    lda #$00
    sta g_cmp
    lda l_count
    cmpa g_cmp
    lbne layout_branch_42
    jmp layout_reject
layout_branch_42
layout_branch_40
    lda #$02
    sta g_cmp
    lda l_mode
    cmpa g_cmp
    lbne layout_branch_43
    jmp layout_measure_done
layout_branch_43
    lda l_width
    sta g_cmp
    lda l_line_width
    cmpa g_cmp
    lblo layout_branch_44
    lbne layout_reject
    lda l_width+1
    sta g_cmp
    lda l_line_width+1
    cmpa g_cmp
    lblo layout_branch_44
    lbne layout_reject
layout_branch_44
    lda l_measured
    sta g_cmp
    lda l_line_width
    cmpa g_cmp
    lblo layout_branch_45
    lbne layout_branch_46
    lda l_measured+1
    sta g_cmp
    lda l_line_width+1
    cmpa g_cmp
    lblo layout_branch_45
    lbeq layout_branch_45
layout_branch_46
    ldd l_line_width
    std l_measured
layout_branch_45
    ldd l_width
    std l_temp
    ldd l_temp
    subd l_line_width
    std l_temp
    lda #$00
    sta g_cmp
    lda l_align
    cmpa g_cmp
    lbne layout_branch_49
    jmp layout_branch_47
layout_branch_49
    lda #$02
    sta g_cmp
    lda l_align
    cmpa g_cmp
    lbne layout_branch_50
    jmp layout_branch_48
layout_branch_50
    lsr l_temp
    ror l_temp+1
    jmp layout_branch_48
layout_branch_47
    ldd #$0000
    std l_temp
layout_branch_48
; BEGIN ORIGIN FIX: the unused-space offset is relative to the rectangle.
    ldd l_left
    std l_x
    ldd l_x
    addd l_temp
    std l_x
; END ORIGIN FIX
    lda #$00
    sta g_cmp
    lda l_lines
    cmpa g_cmp
    lbne layout_branch_51
layout_branch_52
    ldd l_x
    std l_first_x
layout_branch_51
    ldd l_x
    std l_last_x
    ldd l_last_x
    addd l_line_width
    std l_last_x
    ldd l_y
    std l_last_y
    lda l_count
    sta l_last_count
    lda l_lines
    adda #$01
    sta l_lines
    lda #$00
    sta g_cmp
    lda l_pass
    cmpa g_cmp
    lbne layout_branch_53
    jmp layout_advance
layout_branch_53
    lda l_start
    sta l_draw_i
layout_draw_glyph
    ldd #$0000
    std l_temp
    lda l_draw_i
    sta l_temp+1
    ldd #$0000
    std l_pointer
    lda l_start
    sta l_pointer+1
    ldd #$0000
    std l_advance
    lda l_count
    sta l_advance+1
    ldd l_pointer
    addd l_advance
    std l_pointer
    lda l_pointer
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lblo layout_branch_54
    lbne layout_advance
    lda l_pointer+1
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lblo layout_branch_54
    jmp layout_advance
layout_branch_54
    ldd l_source
    std l_pointer
    ldd l_pointer
    addd l_temp
    std l_pointer
    jsr layout_read_pointer
    sta l_code
    jsr layout_glyph_advance
    ldd l_x
    std t_x
    ldd l_y
    std t_y
    ldd l_x
    std t_clip_l
    ldd l_x
    std t_clip_r
    ldd t_clip_r
    addd l_advance
    std t_clip_r
    lda l_glyph
    sta t_code
    jsr text_char
    ldd l_calls
    addd t_calls
    std l_calls
    lda l_writes+3
    adda t_writes+3
    sta l_writes+3
    lda l_writes+2
    adca t_writes+2
    sta l_writes+2
    lda l_writes+1
    adca t_writes+1
    sta l_writes+1
    lda l_writes
    adca t_writes
    sta l_writes
; BEGIN ADVANCE FIX: move by this glyph metric plus letter spacing.
    ldd l_x
    addd l_advance
    std l_x
; END ADVANCE FIX
    lda l_draw_i
    adda #$01
    sta l_draw_i
    jmp layout_draw_glyph
layout_advance
    ldd #$0000
    std l_temp
    lda l_pitch
    sta l_temp+1
    ldd l_y
    addd l_temp
    std l_y
    jmp layout_next
layout_pass_done
    lda #$00
    sta g_cmp
    lda l_pass
    cmpa g_cmp
    lbne layout_branch_55
layout_branch_56
    lda #$01
    sta l_pass
    jmp layout_pass
layout_branch_55
    lda l_length
    sta l_consumed
    lda #$01
    sta l_status
    rts
layout_measure_done
    ldd l_line_width
    std l_measured
    lda #$01
    sta l_status
    rts
layout_reject
    jsr layout_reset
    rts
layout_glyph_advance
    lda l_code
    sta l_glyph
    lda t_first
    sta g_cmp
    lda l_glyph
    cmpa g_cmp
    lblo layout_branch_58
layout_branch_59
    ldd #$0000
    std l_temp
    lda l_glyph
    sta l_temp+1
    ldd #$0000
    std l_pointer
    lda t_first
    sta l_pointer+1
    ldd l_temp
    subd l_pointer
    std l_temp
    lda t_count
    sta g_cmp
    lda l_temp
    cmpa g_cmp
    lblo layout_branch_57
    lbne layout_branch_60
    lda t_count+1
    sta g_cmp
    lda l_temp+1
    cmpa g_cmp
    lblo layout_branch_57
layout_branch_60
layout_branch_58
    lda t_fallback
    sta l_glyph
    ldd #$0000
    std l_temp
    lda l_glyph
    sta l_temp+1
    ldd #$0000
    std l_pointer
    lda t_first
    sta l_pointer+1
    ldd l_temp
    subd l_pointer
    std l_temp
layout_branch_57
    ldd l_metrics
    std l_pointer
    ldd l_pointer
    addd l_temp
    std l_pointer
    jsr layout_read_pointer
    sta l_advance+1
    lda #$00
    sta l_advance
    ldd #$0000
    std l_temp
    lda l_spacing
    sta l_temp+1
    ldd l_advance
    addd l_temp
    std l_advance
    rts
layout_read_pointer
    ldx l_pointer
    lda ,x
    rts
layout_end

Advance sums and rounding explain why measuring and drawing must agree; local-to-screen coordinates explain how those advances become glyph origins. Integer enlargement and other transforms remain further variants.