A pickup chime, a warning and an explosion may all request attention in one update. Before touching a sound channel or message panel, choose which request should win. This arbiter retains the first request at the highest priority.

Include, initialise and call
Bind PRESENT, ID, PRIORITY, REQUEST_ID, REQUEST_PRIORITY, ACCEPTED as 6 contiguous private bytes in that order. Include the helper once. Call priority_arbiter_init at the deliberate reset boundary: it clears all named bytes, including inputs. Set your starting values afterwards. Call priority_arbiter_step when this service is needed.
Initialise once at the start of each arbitration round, then offer all candidates. After the offers, inspect PRESENT and dispatch ID once. Keep playback duration, channel preemption and any running effect state outside this component. Starting a new round is different from restarting a sound.
PRESENT says whether a request has already been selected. ID and PRIORITY hold that selection. Offer REQUEST_ID and REQUEST_PRIORITY, then step. It accepts when no selection exists or the new unsigned priority is strictly greater; equal priority retains the first request. ACCEPTED is replaced on every offer. ID zero and priority zero are valid because PRESENT carries emptiness separately.
Reusable initialisation and service
; SPDX-License-Identifier: MIT
; Choose the Highest-priority Request. Bind these contiguous private bytes: PRESENT, ID, PRIORITY, REQUEST_ID, REQUEST_PRIORITY, ACCEPTED.
; init clears the whole block; step uses current inputs. No I/O, waits or IRQ owner.
; A/X/flags may change; Y and decimal mode survive. Binary arithmetic requires D=0.
priority_arbiter_init:
lda #0
sta PRESENT
sta ID
sta PRIORITY
sta REQUEST_ID
sta REQUEST_PRIORITY
sta ACCEPTED
rts
priority_arbiter_step:
lda #0
sta ACCEPTED
lda PRESENT
beq arbiter_accept
lda REQUEST_PRIORITY
cmp PRIORITY
bcc arbiter_done
beq arbiter_done
arbiter_accept:
lda REQUEST_ID
sta ID
lda REQUEST_PRIORITY
sta PRIORITY
lda #1
sta PRESENT
sta ACCEPTED
arbiter_done:
rts
component_end:State and ownership
The first three bytes retain the current selection; two request bytes are inputs; ACCEPTED reports whether this offer replaced the selection. Rejection leaves the selection untouched.
A/X/flags may change; Y and decimal mode survive. Clear decimal mode before binary arithmetic; the standalone caller uses CLD. A direct call pushes a two-byte return address.
This implementation has fixed symbol bindings and one mutation owner. Independent users need distinct state blocks and scoped/renamed bindings; including another copy with the same global labels is not sufficient. Sequential queries may reuse a scratch block only after the previous result has been consumed. Do not let an interrupt and the main loop mutate the same block without a separately designed ownership policy.
The standalone cartridge owns its background, CHR font and RAM layout. Its diagnostic packet is bounded below the end of the NES’s 2 KiB internal RAM; the component owns no PPU or audio registers.
A small caller you can adapt
The standalone program includes the helper and a separate client. Its read-only table exercises 96 valid inputs, then this client initialises once and retains state between calls. Replace the supplied input changes with your own game values; the display and packet machinery are diagnostics rather than dependencies of the component.
Buildable caller, bindings and include order
.segment "HEADER"
.byte "NES",$1a,2,1,0,0,0,0,0,0,0,0,0,0
PRESENT=$80
ID=$81
PRIORITY=$82
REQUEST_ID=$83
REQUEST_PRIORITY=$84
ACCEPTED=$85
STATE=$80
input_ptr=$70
cases_left=$72
ptr=$74
saved_y=$76
OUTPUT=$0200
preview=OUTPUT+1400
ready=OUTPUT+1408
CUSTOM=0
BROKEN=0
SCORE_CAP=65535
QUEUE_CAP=4
REPEAT_DELAY=4
REPEAT_RATE=2
.include "caller.inc"
.include "exercise.inc"
component_start:
.include "component.inc"
.include "cases.inc"
code_end:
.segment "VECTORS"
.word nmi,reset,irq
.segment "CHARS"
.include "../../input/controller-reading/edges-font.inc"
.res 8192-272,0A separate client retaining state between calls
preview_rules:
.if CUSTOM
lda #7
sta REQUEST_ID
lda #1
sta REQUEST_PRIORITY
.if BROKEN
lda #0
sta PRESENT
jsr checked_step
.else
jsr checked_step
.endif
jsr emit_state
lda #9
sta REQUEST_ID
lda #8
sta REQUEST_PRIORITY
.if BROKEN
lda #0
sta PRESENT
jsr checked_step
.else
jsr checked_step
.endif
jsr emit_state
lda #5
sta REQUEST_ID
lda #2
sta REQUEST_PRIORITY
.if BROKEN
lda #0
sta PRESENT
jsr checked_step
.else
jsr checked_step
.endif
jsr emit_state
.else
lda #7
sta REQUEST_ID
lda #8
sta REQUEST_PRIORITY
.if BROKEN
lda #0
sta PRESENT
jsr checked_step
.else
jsr checked_step
.endif
jsr emit_state
lda #9
sta REQUEST_ID
lda #8
sta REQUEST_PRIORITY
.if BROKEN
lda #0
sta PRESENT
jsr checked_step
.else
jsr checked_step
.endif
jsr emit_state
lda #5
sta REQUEST_ID
lda #2
sta REQUEST_PRIORITY
.if BROKEN
lda #0
sta PRESENT
jsr checked_step
.else
jsr checked_step
.endif
jsr emit_state
.endif
rtsThe eight display fields follow PRESENT, ID, PRIORITY, REQUEST_ID, REQUEST_PRIORITY, ACCEPTED; unused display fields are zero. Each field is a hexadecimal byte. The working row is 01 07 08 05 02 00 00 00. The output is a completed diagnostic state, not a recording of movement, input or sound hardware.
Make it your own
The custom caller lowers the first request priority from eight to one. The second request, ID 9 at priority eight, now wins. In the default trace it ties and ID 7 remains selected.
Maintained custom configuration or caller
.segment "HEADER"
.byte "NES",$1a,2,1,0,0,0,0,0,0,0,0,0,0
PRESENT=$80
ID=$81
PRIORITY=$82
REQUEST_ID=$83
REQUEST_PRIORITY=$84
ACCEPTED=$85
STATE=$80
input_ptr=$70
cases_left=$72
ptr=$74
saved_y=$76
OUTPUT=$0200
preview=OUTPUT+1400
ready=OUTPUT+1408
CUSTOM=1
BROKEN=0
SCORE_CAP=999
QUEUE_CAP=2
REPEAT_DELAY=6
REPEAT_RATE=3
.include "caller.inc"
.include "exercise.inc"
component_start:
.include "component.inc"
.include "cases.inc"
code_end:
.segment "VECTORS"
.word nmi,reset,irq
.segment "CHARS"
.include "../../input/controller-reading/edges-font.inc"
.res 8192-272,0The observed custom row is 01 09 08 05 02 00 00 00. The retained records qualify these named configurations; a new input domain or larger layout needs its own bounds and fresh execution checks.
Common mistake and repair
The wrong client clears PRESENT before every offer. Every request appears to be the first, so the final low-priority ID 5 replaces the earlier priority-eight choice. Clear once before the set of offers, then retain the winner until dispatch.
Deliberately wrong client selection
.segment "HEADER"
.byte "NES",$1a,2,1,0,0,0,0,0,0,0,0,0,0
PRESENT=$80
ID=$81
PRIORITY=$82
REQUEST_ID=$83
REQUEST_PRIORITY=$84
ACCEPTED=$85
STATE=$80
input_ptr=$70
cases_left=$72
ptr=$74
saved_y=$76
OUTPUT=$0200
preview=OUTPUT+1400
ready=OUTPUT+1408
CUSTOM=0
BROKEN=1
SCORE_CAP=65535
QUEUE_CAP=4
REPEAT_DELAY=4
REPEAT_RATE=2
.include "caller.inc"
.include "exercise.inc"
component_start:
.include "component.inc"
.include "cases.inc"
code_end:
.segment "VECTORS"
.word nmi,reset,irq
.segment "CHARS"
.include "../../input/controller-reading/edges-font.inc"
.res 8192-272,0
The wrong branch is in the displayed client above, selected by BROKEN. repaired.asm selects the original client again. Its executable matches demo exactly; Spectrum tape filenames can differ while loaded CODE is identical. The bad caller is a labelled experiment, not an alternative recommended implementation.
Cost and verification
The measured helper occupies 46 code bytes plus 6 named state bytes in this layout. Return addresses, alignment, tables and display diagnostics are additional. The sizes cover init and step together, not the whole standalone program.
Each offer compares one priority and may copy an ID/priority pair. A round over N requests costs N offers. There is no allocation, queue or hardware access; keeping the current winner is cheaper than sorting all candidates when only one result is needed.
A rotating tie-breaker can improve fairness but changes the stable first-wins contract. Multiple hardware voices may need one arbiter per eligible channel plus ownership and preemption rules. This logical selection does not assert that an effect played or that a device accepted it.
Four native runs use NTSC NES with a mapper-0 cartridge, CHR-ROM diagnostic font, NMI and DMC disabled; Asm198x 0.0.72 and Emu198x 0.44.2. Every run checks 96 table results, dirty initialisation and every state in a separate sequential trace against an independent integer model. This is boundary/varied-input qualification, not exhaustive coverage of every byte combination. Source/tool/firmware hashes, native startup and completion, preserved-register sentinels, immutable loaded code, actual helper extent, capture bytes and pixels, and exact repair are checked. Deliberately changed observations must fail. Firmware is hashed, never distributed.