joeylib2/make/support/iigs/ntpIrqWindow.py
2026-10-13 14:01:46 -05:00

123 lines
5.5 KiB
Python
Executable file

#!/usr/bin/env python3
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# ntpIrqWindow.py SRC DST - bound NinjaTrackerPlus's interrupts-off stretches.
#
# NinjaTrackerPlus moves sample data into the DOC with interrupts masked,
# ~4 us a byte (measured under MAME), and the Z8530 SCC's receive FIFO only
# lasts ~3.1 ms at 9600 baud (~2.3 ms under MAME). Two stretches outlast it:
# - NTPstreamsound fills a stream's 512-byte DOC page with two unrolled
# 256-byte copies under its own SEI: ~2.4 ms. stream_window goes between
# the halves: pending interrupts run there, then the DOC's mode and
# address are put back for the second half. The stream's oscillators are
# already halted, and its job is cleared first, so a refill interrupt left
# pending by the slot's previous sound cannot stream into the page.
# - The DOC interrupt handler refills every streaming oscillator that is
# due before it returns, 256 bytes each: ~1 ms apiece, ~2.5 ms for two.
# It returns after one instead; the DOC keeps its IRQ up for the rest, so
# the handler is re-entered at once, after anything the ROM's interrupt
# manager puts first (the SCC is on its fast path).
# Both only while the word at offset 27 of the replayer (irq_window, right
# after the documented jump table) is nonzero. audio_full.c sets it from
# hal.c's tight interrupt-latency bound before every NTPstreamsound, so with
# the bound loose the replayer does exactly what it always did.
#
# The source is the installer's pinned ntpsources.zip (checksummed), so
# every anchor below is exact; a miss means NTP changed and this script
# must be revisited, so it fails the build instead of guessing.
import sys
TABLE_END = """ jmp streamsound
"""
TABLE_END_NEW = """ jmp streamsound
; irq_window (JoeyLib, offset 27): nonzero bounds the interrupts-off
; stretches -- see stream_window and interrupt_handler_exit.
irq_window dw 0
"""
FILL = """ ldy streamsound_osc2
jsr stream_copy ;fill buffer with 512 bytes for first play
jsr stream_copy_no_init
"""
FILL_NEW = """ ldy streamsound_osc2
jsr stream_copy ;fill buffer with 512 bytes for first play
jsr stream_window ;JoeyLib: interrupts in between the halves, if asked
jsr stream_copy_no_init
"""
COPY_HELPERS = """ mx %10
streamsound_copy = * ;copy a byte from stream structure to multiple doc registers
"""
COPY_HELPERS_NEW = """; stream_window (JoeyLib): between the two halves of streamsound's first
; DOC fill, when irq_window is nonzero, let pending interrupts run, then
; point the DOC back at the second half (a handler may have used it).
; In and out: mx %00, Y = the stream's track * 2.
mx %00
stream_window = *
lda irq_window
beq stream_window_1
sep #$20
mx %10
ldx streamsound_osc ;no refill job left pending from the slot's last sound
stz oscillator_job_tbl,x
cli
nop ;a pending interrupt is taken here
sei
jsr access_doc_ram
rep #$20
mx %00
lda stream_doc_page,y ;the page's second half
inc
xba
sta sound_address
stream_window_1 = *
rts
mx %10
streamsound_copy = * ;copy a byte from stream structure to multiple doc registers
"""
HANDLER_JOB = """ ldx interrupt_job
jsr (interrupt_jump_tbl,x)
]loop lda sound_control
"""
HANDLER_JOB_NEW = """ ldx interrupt_job
jsr (interrupt_jump_tbl,x)
lda irq_window ;JoeyLib: one oscillator an interrupt when asked;
bne interrupt_handler_exit ;the DOC keeps its IRQ up for any others
]loop lda sound_control
"""
HANDLER_END = """ bpl interrupt_handler_loop
sep #$30
"""
HANDLER_END_NEW = """ bpl interrupt_handler_loop
interrupt_handler_exit = *
sep #$30
"""
def patchOnce(text, old, new):
if text.count(old) != 1:
sys.exit("ntpIrqWindow: anchor not found exactly once -- NinjaTrackerPlus changed:\n" + old)
return text.replace(old, new)
def main():
if len(sys.argv) != 3:
sys.exit("usage: ntpIrqWindow.py SRC DST")
with open(sys.argv[1], "r", encoding="ascii") as f:
text = f.read()
text = patchOnce(text, TABLE_END, TABLE_END_NEW)
text = patchOnce(text, FILL, FILL_NEW)
text = patchOnce(text, COPY_HELPERS, COPY_HELPERS_NEW)
text = patchOnce(text, HANDLER_JOB, HANDLER_JOB_NEW)
text = patchOnce(text, HANDLER_END, HANDLER_END_NEW)
with open(sys.argv[2], "w", encoding="ascii") as f:
f.write(text)
main()