Sound FX on GS fixed.

This commit is contained in:
Scott Duensing 2026-09-28 21:35:48 -05:00
parent 70f67d232d
commit fe84be766f
8 changed files with 316 additions and 128 deletions

View file

@ -31,25 +31,7 @@ correct.
On the IIgs a Super Serial Card only interrupts with its interrupt switch On the IIgs a Super Serial Card only interrupts with its interrupt switch
(SW2-6) on; with it off the card still works, drained by `jlSerialPoll`, but (SW2-6) on; with it off the card still works, drained by `jlSerialPoll`, but
loses what arrives while the program is busy. The card's 6551 holds a single loses what arrives while the program is busy.
received byte, about 1 ms at 9600 baud, which is less than the IIgs's own
worst-case interrupt latency during a present: it runs loss-free at 2400 baud,
and above that relies on the protocol to recover a dropped byte. The built-in
modem and printer ports buffer three bytes and stay loss-free at 9600 through
full-screen presents, joystick reads and sound effects. To get there, JoeyLib
cuts its own long interrupts-off stretches into pieces of at most ~1.7 ms while
a port is open: the present and the full-surface clear, the joystick read
(~2.8 ms otherwise, with the stick at full deflection), and the sound-effect
calls into the NinjaTrackerPlus replayer (~2.4 ms to start an effect, and a
refill interrupt that tops up every streaming effect that is due at once). A
full-screen present gets about 5% slower; a joystick read costs about what it
does with no port open, up to ~0.8 ms more with the stick in a corner, and its
reading can differ from the same stick's reading with no port open by a count.
With no port open all of these run as before. Two stretches are not
JoeyLib's to cut and can still cost bytes: GS/OS disk access (a 3.5-inch
floppy holds interrupts off for 65 to 220 ms at a time), and starting a MOD
song with `jlAudioPlayMod`, which loads its instruments into the sound chip
with interrupts off, roughly 12 ms per KB of samples.
On the Amiga the port is `serial.device`, which AmigaOS loads from `DEVS:`: On the Amiga the port is `serial.device`, which AmigaOS loads from `DEVS:`:
every Workbench installation has it, but Kickstart 3.1 on its own does not, so every Workbench installation has it, but Kickstart 3.1 on its own does not, so
@ -60,6 +42,71 @@ to boot on its own must do the same.
A failed `jlSerialOpen` is non-fatal: it returns `false` and every other call A failed `jlSerialOpen` is non-fatal: it returns `false` and every other call
becomes a safe no-op, exactly like a failed audio init. becomes a safe no-op, exactly like a failed audio init.
=== What Can Lose Received Bytes
Every port receives into a small hardware buffer that an interrupt empties
into the ring. A byte is lost when that buffer fills before the interrupt
runs: when something keeps interrupts off, or keeps the CPU away from the
handler, for longer than the buffer lasts. A byte takes about 1 ms to arrive
at 9600 baud, so:
[cols="3,2,2", options="header"]
|===
|Port |Receive buffer |Lasts at 9600 baud
|IIgs modem and printer ports (Zilog 8530) |3 bytes |~3 ms
|IIgs Super Serial Card (6551) |1 byte |~1 ms
|DOS, 16550A UART |16 bytes, interrupt at 14 |~2 ms from the interrupt
|DOS, 8250 or 16450 UART |1 byte |~1 ms
|Amiga (Paula) |1 byte |~1 ms
|Atari ST (68901 MFP) |1 byte |~1 ms
|X68000 (Zilog 8530) |3 bytes |~3 ms
|===
At 19200 baud each lasts half as long, and so on.
JoeyLib's own work is safe while a port is open. On the IIgs, where the
display and sound code masks interrupts for long stretches, opening a port
makes JoeyLib cut them into pieces of at most ~1.7 ms: the present and the
full-surface clear, the joystick read (~2.8 ms otherwise, with the stick at
full deflection), and the sound-effect calls into the NinjaTrackerPlus
replayer (~2.4 ms to start an effect, and a refill interrupt that tops up
every streaming effect that is due at once). A full-screen present gets about
5% slower; a joystick read costs about what it does with no port open, up to
~0.8 ms more with the stick in a corner, and can read one count different
from the same stick with no port open. With no port open all of these run as
before. The other ports mask interrupts only across a few register writes.
These can still lose bytes, and are the program's to schedule around:
* *Disk access.* On the IIgs, GS/OS holds interrupts off while it reads or
writes a 3.5-inch floppy: 65 to 220 ms at a time, measured. That is every
file call -- `jlFile*`, `jlSave*`, loading assets and sprite banks, and
`jlAudioInit`, which loads the replayer from disk. The other ports' disk
access goes through the operating system and has not been measured; treat
it as a risk there too.
* *Starting a song on the IIgs.* `jlAudioPlayMod` loads the song's
instruments into the sound chip with interrupts off, roughly 12 ms per KB of
samples.
* *Music playing on the IIgs.* The replayer's tick runs as an interrupt
handler, and has not been measured with a port open.
* *A Super Serial Card above 2400 baud.* The IIgs's own interrupt handling can
outlast the 6551's single byte, so the card runs loss-free at 2400 and above
that relies on the protocol to recover. The built-in modem and printer ports
stay loss-free at 9600 through full-screen presents, clears, joystick reads
and sound effects.
* *The program's own code*, wherever it masks interrupts.
Ways to live with them:
* Keep file access and `jlAudioPlayMod` out of the middle of a transfer: do
them between messages, or while the other end is waiting for a reply.
* Ask the other end to pause first. With hardware flow control, drop RTS with
`jlSerialSetRts(false)`, allow a few character times for bytes already on
the way, do the slow operation, then raise RTS again. With software flow
control, send XOFF and later XON.
* For data that must arrive intact, use a protocol with checksums and
retransmission; XMODEM and its descendants exist for exactly this.
=== Types === Types
[source,c] [source,c]

View file

@ -210,7 +210,7 @@ static bool sfxStart(void) {
for (i = 0u; i < SFX_WAVE_BYTES; i++) { for (i = 0u; i < SFX_WAVE_BYTES; i++) {
gSfxWave[i] = (uint8_t)(i == 0u ? 1u : i); // 0x00 would stop the DOC gSfxWave[i] = (uint8_t)(i == 0u ? 1u : i); // 0x00 would stop the DOC
} }
// The first trigger of a sample converts it for the DOC (~0.2 s for this one); do it before // The first trigger of a sample converts it for the DOC (~15 ms for this one); do it before
// the port opens, so the loop's triggers are the cheap repeats and a slow first frame cannot // the port opens, so the loop's triggers are the cheap repeats and a slow first frame cannot
// overflow the receive ring. // overflow the receive ring.
jlAudioPlaySfx((uint8_t)SFX_SHOT_SLOT, (const uint8_t *)gSfxShot, SFX_SHOT_BYTES, SFX_SHOT_RATE); jlAudioPlaySfx((uint8_t)SFX_SHOT_SLOT, (const uint8_t *)gSfxShot, SFX_SHOT_BYTES, SFX_SHOT_RATE);

View file

@ -23,6 +23,19 @@
// ring, and the hardware drain where it does not, so calling it every frame is // ring, and the hardware drain where it does not, so calling it every frame is
// always correct. Transmit is a bounded poll. // always correct. Transmit is a bounded poll.
// //
// Received bytes CAN be lost. Each port's receive hardware holds 1 to 3 bytes
// (16 on a 16550), ~1-3 ms at 9600 baud, and anything that keeps interrupts off
// longer drops what arrives meanwhile. JoeyLib's own work is safe with a port
// open (on the IIgs it cuts its long interrupts-off stretches to ~1.7 ms while
// one is), but these are not: IIgs disk access (GS/OS holds interrupts off
// 65-220 ms per 3.5" floppy access, so every file call, asset load and
// jlAudioInit), IIgs jlAudioPlayMod (~12 ms per KB of samples), a Super Serial
// Card above 2400 baud, and the program's own interrupt masking. Disk access
// on the other ports is unmeasured. Schedule them between messages, pause the
// peer first (jlSerialSetRts(false) or XOFF), or use a protocol that
// retransmits. docs/ApiSerial.adoc, "What Can Lose Received Bytes", has the
// per-port table.
//
// A failed jlSerialOpen is non-fatal: it returns false and every other call is // A failed jlSerialOpen is non-fatal: it returns false and every other call is
// a safe no-op (Available 0, Read 0), exactly like a failed audio init. // a safe no-op (Available 0, Read 0), exactly like a failed audio init.

View file

@ -418,6 +418,9 @@ unsigned char gCallStub[16];
// here rather than shared because only the holds below need it. // here rather than shared because only the holds below need it.
extern uint16_t iigsGetTickWord(void); extern uint16_t iigsGetTickWord(void);
// sfxCopyUnsigned's byte loop (sfxCopy.s).
extern void iigsSfxCopyUnsigned(unsigned char *dst, const uint8_t *src, uint16_t length, uint16_t padTo);
// ----- Internal helpers ----- // ----- Internal helpers -----
@ -721,27 +724,19 @@ static void sfxApplyPan(unsigned char *sfx, uint8_t slot, uint8_t vol) {
// declare, so the bytes past a short sample get played too. They are // declare, so the bytes past a short sample get played too. They are
// whatever the slot held before, very often zeros. 0x80 is unsigned // whatever the slot held before, very often zeros. 0x80 is unsigned
// silence and plays harmlessly if it is ever reached. // silence and plays harmlessly if it is ever reached.
// Safe in place (dst == src), which is how streamChunk uses it. // Safe in place (dst == src), which is how streamChunk uses it. The bytes
// themselves are iigsSfxCopyUnsigned's (sfxCopy.s): 57 ms for a full slot,
// where the C loop it replaces took 0.73 s.
// length never exceeds SFX_SAMPLE_BYTES (both callers bound it), so it
// and padTo, an even page multiple or SFX_SAMPLE_BYTES itself, fit a word.
static void sfxCopyUnsigned(unsigned char *dst, const uint8_t *src, uint32_t length) { static void sfxCopyUnsigned(unsigned char *dst, const uint8_t *src, uint32_t length) {
uint32_t padTo; uint32_t padTo;
uint32_t i;
for (i = 0; i < length; i++) {
unsigned char v = (unsigned char)(src[i] ^ 0x80u);
if (v == 0u) {
v = 1u;
}
dst[i] = v;
}
padTo = (length + SFX_STREAM_PAGE - 1u) & ~(SFX_STREAM_PAGE - 1u); padTo = (length + SFX_STREAM_PAGE - 1u) & ~(SFX_STREAM_PAGE - 1u);
if (padTo > (uint32_t)SFX_SAMPLE_BYTES) { if (padTo > (uint32_t)SFX_SAMPLE_BYTES) {
padTo = (uint32_t)SFX_SAMPLE_BYTES; padTo = (uint32_t)SFX_SAMPLE_BYTES;
} }
for (i = length; i < padTo; i++) { iigsSfxCopyUnsigned(dst, src, (uint16_t)length, (uint16_t)padTo);
dst[i] = 0x80u;
}
} }
@ -1095,9 +1090,8 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
// Cache check: same sample, length, and rate as the prior trigger // Cache check: same sample, length, and rate as the prior trigger
// for this slot? Then the slot's DOC sample bytes and stream // for this slot? Then the slot's DOC sample bytes and stream
// struct are already correct -- skip the 4 KB byte-XOR loop and // struct are already correct -- skip the sample conversion (~7 us
// the 15-byte struct rebuild, both of which together can run // a byte) and the struct rebuild.
// ~50 ms per call.
if (sample == gSfxSlotSample[slot] && if (sample == gSfxSlotSample[slot] &&
length == gSfxSlotLength[slot] && length == gSfxSlotLength[slot] &&
rateHz == gSfxSlotRateHz[slot]) { rateHz == gSfxSlotRateHz[slot]) {

150
src/iigs/sfxCopy.s Normal file
View file

@ -0,0 +1,150 @@
; ====================================================================
; sfxCopy.s - stage a sound effect's sample for the DOC (llvm-mc w65816)
;
; iigsSfxCopyUnsigned(unsigned char *dst, const uint8_t *src,
; uint16_t length, uint16_t padTo)
;
; The inner loop of audio_full.c's sfxCopyUnsigned, which says why: the
; sample is signed 8-bit (the public contract) and DOC RAM wants
; unsigned, so every byte gets its sign bit flipped, a byte that comes
; out 0x00 (the ES5503's wave-end marker) becomes 0x01, and the bytes
; from length up to padTo become 0x80, unsigned silence. padTo must be
; even and at least length; dst == src (in place) is fine, since every
; word is read before it is written. Either pointer may cross a bank.
;
; A word at a time through [dp],y with 16-bit Y, four words a pass with X
; counting the passes, and the 0x00 fix-ups off the straight line.
; Measured under MAME at 2.8 MHz: 7.0 us a byte, 57 ms for a full 8172-
; byte slot, against 90 us a byte for the compiled C loop, whose 32-bit
; index and bound became materialised compares on every byte.
;
; ABI: arg0 dst in A:X (A = low16, X = high16). src (4-byte ptr), length
; and padTo on the stack: src @4,s, length @8,s, padTo @10,s on JSL
; entry, +3 after PHP+PHD. They are copied into a stack (bank-0)
; D-frame: [0] = dst, [4] = src, 8 = length (then its odd-byte flag),
; 10 = padTo, 12 = the four-word pass count.
; A, X and Y are clobbered; P, D and DBR are preserved.
; ====================================================================
SFX_FRAME_BYTES = 14
; One word: flip both sign bits, then turn a 0x00 byte into 0x01 through
; the fix-up stubs SC_FIX places after each loop, off its straight line.
.macro SC_WORD n
lda [4], y
eor #0x8080
bit #0x00FF
beq scFixLo\n
scBackLo\n:
bit #0xFF00
beq scFixHi\n
scBackHi\n:
sta [0], y
iny
iny
.endm
.macro SC_FIX n
scFixLo\n:
ora #0x0001
bra scBackLo\n
scFixHi\n:
ora #0x0100
bra scBackHi\n
.endm
.section .text.iigsSfxCopyUnsigned,"ax"
.globl iigsSfxCopyUnsigned
iigsSfxCopyUnsigned:
php
phd
rep #0x30
.a16
.i16
tay ; park dst low16
pha ; the pass-count slot (12)
lda 15,s ; padTo
pha
lda 15,s ; length (each push moves the next arg up to 15,s)
pha
lda 15,s ; src high16
pha
lda 15,s ; src low16
pha
phx ; dst high16
phy ; dst low16
tsc
inc a
tcd ; D -> the frame (pushed data starts at SP+1)
lda 8
lsr a ; A = whole words, C = an odd last byte
tax
lda #0
rol a
sta 8 ; 8 = 1 when there is an odd last byte
txa
lsr a
lsr a
sta 12 ; 12 = passes of four words
txa
ldy #0
and #0x0003
beq scPasses
tax ; the words left over from the passes, first
scSingle:
SC_WORD 0
dex
bne scSingle
bra scPasses
SC_FIX 0
scPasses:
ldx 12
beq scWordsDone
scPass:
SC_WORD 1
SC_WORD 2
SC_WORD 3
SC_WORD 4
dex
bne scPass
bra scWordsDone
SC_FIX 1
SC_FIX 2
SC_FIX 3
SC_FIX 4
scWordsDone:
lda 8
beq scPad
; The odd last byte, then one pad byte: padTo is even, so the
; word pad below starts on an even index again.
sep #0x20
.a8
lda [4], y
eor #0x80
bne scOddStore
lda #0x01
scOddStore:
sta [0], y
iny
lda #0x80
sta [0], y
iny
rep #0x20
.a16
scPad:
cpy 10
bcs scDone
lda #0x8080
scPadWord:
sta [0], y
iny
iny
cpy 10
bcc scPadWord
scDone:
tsc
clc
adc #SFX_FRAME_BYTES
tcs ; drop the D-frame
pld
plp
rtl

View file

@ -1,47 +0,0 @@
# Generated by install.sh -- do not edit. For a shell:
# source toolchains/env.sh
export JOEY_TOOLCHAINS='/home/scott/claude/joeylib/toolchains'
export AMIGA_PREFIX='/home/scott/claude/joeylib/toolchains/opt/amigaGcc'
export AMIGA_CC='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin/m68k-amigaos-gcc'
export AMIGA_AR='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin/m68k-amigaos-ar'
export AMIGA_LD='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin/m68k-amigaos-ld'
export AMIGA_AS='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin/vasmm68k_mot'
export AMIGA_NDK='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/m68k-amigaos/sys-include'
export AMIGA_SERIAL_DEVICE='/home/scott/claude/joeylib/toolchains/firmware/amiga/Devs/serial.device'
export CADIUS='/home/scott/claude/joeylib/toolchains/opt/cadius/bin/cadius'
export DOS_CWSDPMI='/home/scott/claude/joeylib/toolchains/opt/cwsdpmi/bin'
export DOS_CWSDSTUB='/home/scott/claude/joeylib/toolchains/opt/cwsdpmi/bin/CWSDSTUB.EXE'
export DOS_PREFIX='/home/scott/claude/joeylib/toolchains/opt/djgpp'
export DOS_CC='/home/scott/claude/joeylib/toolchains/opt/djgpp/bin/i586-pc-msdosdjgpp-gcc'
export DOS_AR='/home/scott/claude/joeylib/toolchains/opt/djgpp/bin/i586-pc-msdosdjgpp-ar'
export DOS_LD='/home/scott/claude/joeylib/toolchains/opt/djgpp/bin/i586-pc-msdosdjgpp-ld'
export DOS_STUBEDIT='/home/scott/claude/joeylib/toolchains/opt/djgpp/i586-pc-msdosdjgpp/bin/stubedit'
export DOS_STUBIFY='/home/scott/claude/joeylib/toolchains/opt/djgpp/i586-pc-msdosdjgpp/bin/stubify'
export DOS_EXE2COFF='/home/scott/claude/joeylib/toolchains/opt/djgpp/i586-pc-msdosdjgpp/bin/exe2coff'
export DOS_EMBED_DPMI='/home/scott/claude/joeylib/make/support/dos/embed-dpmi.sh'
export DOSBOX='/home/scott/claude/joeylib/toolchains/opt/dosbox/dosbox'
export X68K_ROOT='/home/scott/claude/joeylib/toolchains/opt/elf2x68k'
export EMUTOS_IMG='/home/scott/claude/joeylib/toolchains/firmware/atarist/emutos-512k.img'
export FSUAE='/usr/bin/fs-uae'
export HATARI='/usr/bin/hatari'
export IIGS_ROMPATH='/home/scott/claude/joeylib/toolchains/firmware/iigs/roms'
export GSOS_SYSTEM_DISK='/home/scott/claude/joeylib/toolchains/firmware/iigs/gsos-system.po'
export KICKSTART_ROM='/home/scott/claude/joeylib/toolchains/firmware/amiga/kickstart.rom'
export LIBXMP_DIR='/home/scott/claude/joeylib/toolchains/opt/libxmp'
export LLVM816_ROOT='/home/scott/claude/joeylib/toolchains/opt/llvm816'
export IIGS_CLANG_BUILD='/home/scott/claude/joeylib/make/support/iigs/clang-build.sh'
export MAME='/home/scott/claude/joeylib/toolchains/opt/mame/bin/mame'
export IIGS_AS='/home/scott/claude/joeylib/toolchains/opt/merlin32/bin/merlin32'
export ST_PREFIX='/home/scott/claude/joeylib/toolchains/opt/mintGcc'
export ST_CC='/home/scott/claude/joeylib/toolchains/opt/mintGcc/bin/m68k-atari-mint-gcc'
export ST_AR='/home/scott/claude/joeylib/toolchains/opt/mintGcc/bin/m68k-atari-mint-ar'
export ST_LD='/home/scott/claude/joeylib/toolchains/opt/mintGcc/bin/m68k-atari-mint-ld'
export DOS_AS='/usr/bin/nasm'
export IIGS_NTP_DIR='/home/scott/claude/joeylib/toolchains/opt/ntp'
export AMIGA_PTPLAYER='/home/scott/claude/joeylib/toolchains/opt/ptplayer'
export X68K_DIR='/home/scott/claude/joeylib/toolchains/firmware/x68000'
case ":$PATH:" in *":/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin:"*) ;; *) PATH='/home/scott/claude/joeylib/toolchains/opt/amigaGcc/bin':"$PATH" ;; esac
case ":$PATH:" in *":/home/scott/claude/joeylib/toolchains/opt/djgpp/bin:"*) ;; *) PATH='/home/scott/claude/joeylib/toolchains/opt/djgpp/bin':"$PATH" ;; esac
case ":$PATH:" in *":/home/scott/claude/joeylib/toolchains/opt/elf2x68k/bin:"*) ;; *) PATH='/home/scott/claude/joeylib/toolchains/opt/elf2x68k/bin':"$PATH" ;; esac
case ":$PATH:" in *":/home/scott/claude/joeylib/toolchains/opt/mintGcc/bin:"*) ;; *) PATH='/home/scott/claude/joeylib/toolchains/opt/mintGcc/bin':"$PATH" ;; esac
export PATH

View file

@ -30,34 +30,43 @@
#define JOEY_START_PATH "/JOEYLIB/STAXI" #define JOEY_START_PATH "/JOEYLIB/STAXI"
#endif #endif
// Fixed scratch in bank $E1: the $E1:9DC8-$9DFD window that survives a // The harness's marker, in the $E1:9DC8-$9DFD window that survives a
// present (SCB proper ends at $9DC7). The asm stubs hard-code the two // present (SCB proper ends at $9DC7).
// parameter-block addresses, so these must agree with iigsStartQuit.s.
// 9DC8 mailbox 9DCC QuitGS parm (8)
// 9DD4 pathname string 9DE4 SetPrefix parm (8)
// 9DEC prefix string
#define QUIT_MAILBOX ((volatile uint8_t *)0xE19DC8L) #define QUIT_MAILBOX ((volatile uint8_t *)0xE19DC8L)
#define QUIT_PARM ((volatile uint8_t *)0xE19DCCL)
#define QUIT_PATH ((volatile uint8_t *)0xE19DD4L)
#define PREFIX_PARM ((volatile uint8_t *)0xE19DE4L)
#define PREFIX_PATH ((volatile uint8_t *)0xE19DECL)
#define QUIT_PATH_ADDR 0x00E19DD4UL
#define PREFIX_PATH_ADDR 0x00E19DECUL
// A GS/OS class-1 input string is a length WORD followed by the characters. // A GS/OS class-1 input string is a length WORD followed by the characters.
#define GSOS_STRING_HEADER 2u #define GSOS_STRING_HEADER 2u
// The parameter blocks: SetPrefixGS {pCount, prefixNum, prefix} and
// QuitGS {pCount, pathname, flags}.
#define PREFIX_PARM_BYTES 8u
#define PREFIX_PARM_PATH 4u
#define QUIT_PARM_BYTES 8u
#define QUIT_PARM_PATH 2u
#define GSOS_PCOUNT_2 2u
#define PREFIX_CURRENT 0u // prefix 0 = the current directory
// Room for the whole path, which is the longest either string gets.
#define PATH_STRING_BYTES (GSOS_STRING_HEADER + sizeof(JOEY_START_PATH))
// Markers for the harness. // Markers for the harness.
#define QUIT_REACHED 0xAAu #define QUIT_REACHED 0xAAu
#define QUIT_RETURNED 0xEEu #define QUIT_RETURNED 0xEEu
void iigsStartQuit(void); static void gsosString(uint8_t *dst, const char *src, uint16_t len);
void iigsStartSetPrefix(void); void iigsStartQuit(uint8_t *parm);
void iigsStartSetPrefix(uint8_t *parm);
static void putLong(uint8_t *dst, uint32_t v);
// Write a GS/OS input string (length word + characters) and return its length. // In the launcher's own data, where GS/OS reads them during each call.
static uint16_t gsosString(volatile uint8_t *dst, const char *src, uint16_t len) { static uint8_t gPrefixParm[PREFIX_PARM_BYTES];
static uint8_t gPrefixPath[PATH_STRING_BYTES];
static uint8_t gQuitParm[QUIT_PARM_BYTES];
static uint8_t gQuitPath[PATH_STRING_BYTES];
// Write a GS/OS input string: length word, then the characters.
static void gsosString(uint8_t *dst, const char *src, uint16_t len) {
uint16_t i; uint16_t i;
dst[0] = (uint8_t)(len & 0xFFu); dst[0] = (uint8_t)(len & 0xFFu);
@ -65,11 +74,10 @@ static uint16_t gsosString(volatile uint8_t *dst, const char *src, uint16_t len)
for (i = 0; i < len; i++) { for (i = 0; i < len; i++) {
dst[GSOS_STRING_HEADER + i] = (uint8_t)src[i]; dst[GSOS_STRING_HEADER + i] = (uint8_t)src[i];
} }
return len;
} }
static void putLong(volatile uint8_t *dst, uint32_t v) { static void putLong(uint8_t *dst, uint32_t v) {
dst[0] = (uint8_t)(v & 0xFFu); dst[0] = (uint8_t)(v & 0xFFu);
dst[1] = (uint8_t)((v >> 8) & 0xFFu); dst[1] = (uint8_t)((v >> 8) & 0xFFu);
dst[2] = (uint8_t)((v >> 16) & 0xFFu); dst[2] = (uint8_t)((v >> 16) & 0xFFu);
@ -94,28 +102,27 @@ int main(void) {
} }
} }
gsosString(PREFIX_PATH, path, dir); gsosString(gPrefixPath, path, dir);
PREFIX_PARM[0] = 2u; // pCount gPrefixParm[0] = GSOS_PCOUNT_2;
PREFIX_PARM[1] = 0u; gPrefixParm[1] = 0u;
PREFIX_PARM[2] = 0u; // prefixNum 0 = current directory gPrefixParm[2] = PREFIX_CURRENT;
PREFIX_PARM[3] = 0u; gPrefixParm[3] = 0u;
putLong(PREFIX_PARM + 4, PREFIX_PATH_ADDR); putLong(gPrefixParm + PREFIX_PARM_PATH, (uint32_t)gPrefixPath);
iigsStartSetPrefix(); iigsStartSetPrefix(gPrefixParm);
gsosString(QUIT_PATH, path, len); gsosString(gQuitPath, path, len);
QUIT_PARM[0] = 2u; // pCount gQuitParm[0] = GSOS_PCOUNT_2;
QUIT_PARM[1] = 0u; gQuitParm[1] = 0u;
putLong(QUIT_PARM + 2, QUIT_PATH_ADDR); // pathname putLong(gQuitParm + QUIT_PARM_PATH, (uint32_t)gQuitPath);
QUIT_PARM[6] = 0u; // flags gQuitParm[6] = 0u; // flags
QUIT_PARM[7] = 0u; gQuitParm[7] = 0u;
*QUIT_MAILBOX = QUIT_REACHED; *QUIT_MAILBOX = QUIT_REACHED;
iigsStartQuit(); iigsStartQuit(gQuitParm);
*QUIT_MAILBOX = QUIT_RETURNED; *QUIT_MAILBOX = QUIT_RETURNED;
// QuitGS only returns on failure. Nothing useful is left to do and the // QuitGS only returns on failure. Nothing useful is left to do, so
// llvm-mos post-main exit path is broken on GS/OS, so spin rather than // spin: the harness reads the mailbox to see what happened.
// crash: the harness reads the mailbox to see what happened.
for (;;) { for (;;) {
} }
} }

View file

@ -1,12 +1,16 @@
; GS/OS inline-call stubs for iigsStart. ; GS/OS inline-call stubs for iigsStart.
; ;
; Same shape as iigsQuitGS in src/iigs/joeyDraw.s and for the same reason: ; The same inline form and the same self-patch as the llvm816 runtime's
; pure constants, no relocations, so the cross-bank DBR trap that bit the ; gsosDispatch (runtime/src/iigsGsos.s): the caller passes the parameter
; first attempt at that routine cannot apply here either. Each parameter ; block's address in A:X (A = offset, X = bank), and the stub writes it
; block lives at a FIXED address in the $E1:9DC8-$9DFD scratch window and ; into its own inline pBlock operand before the call. The stores target
; the C caller fills it in before calling. ; the stub's own bank (phk/plb), not the caller's DBR. The blocks used to
; sit at fixed addresses in the $E1:9DC8-$9DFD scratch window, which is 54
; bytes: a program name of 8 characters or more ran the QuitGS pathname
; over the SetPrefix string, prefix 0 was never set, and the program could
; not open "ntpplayer" or its DATA folder.
; iigsStartSetPrefix -- SetPrefixGS ($2009), parm block at $E1:9DE4. ; iigsStartSetPrefix(parm) -- SetPrefixGS ($2009).
; ;
; GS/OS does NOT point prefix 0 at the program named in a QuitGS launch: it ; GS/OS does NOT point prefix 0 at the program named in a QuitGS launch: it
; was measured still pointing at the BOOT volume, so the launched example's ; was measured still pointing at the BOOT volume, so the launched example's
@ -17,12 +21,22 @@
.section .text.iigsStartSetPrefix,"ax" .section .text.iigsStartSetPrefix,"ax"
.globl iigsStartSetPrefix .globl iigsStartSetPrefix
iigsStartSetPrefix: iigsStartSetPrefix:
phb
phk
plb
sta spPblock
sep #0x20
txa
sta spPblock+2
rep #0x20
plb
jsl 0xE100A8 jsl 0xE100A8
.word 0x2009 .word 0x2009
.long 0x00E19DE4 spPblock:
.long 0
rtl rtl
; iigsStartQuit -- QuitGS ($2029), parm block at $E1:9DCC. ; iigsStartQuit(parm) -- QuitGS ($2029).
; ;
; Unlike jlpShutdown's pCount=0 "quit to my parent", this is called with ; Unlike jlpShutdown's pCount=0 "quit to my parent", this is called with
; pCount=2 and a pathname, which tells GS/OS to LAUNCH that program in our ; pCount=2 and a pathname, which tells GS/OS to LAUNCH that program in our
@ -30,7 +44,17 @@ iigsStartSetPrefix:
.section .text.iigsStartQuit,"ax" .section .text.iigsStartQuit,"ax"
.globl iigsStartQuit .globl iigsStartQuit
iigsStartQuit: iigsStartQuit:
phb
phk
plb
sta qtPblock
sep #0x20
txa
sta qtPblock+2
rep #0x20
plb
jsl 0xE100A8 jsl 0xE100A8
.word 0x2029 .word 0x2029
.long 0x00E19DCC qtPblock:
.long 0
rtl rtl