Lots more fixes to make each target behave more like the others.

This commit is contained in:
Scott Duensing 2026-10-16 15:51:28 -05:00
parent d33d9c9c25
commit 484d22691b
136 changed files with 6803 additions and 2454 deletions

View file

@ -29,6 +29,16 @@ silent, `jlAudioTickRegister` refuses, and the log says so. The tone and noise
voices, and so `jlMusicPlay`, still play where they drive a sound chip of voices, and so `jlMusicPlay`, still play where they drive a sound chip of
their own: on the Amiga, the ST, DOS, and the X68000. their own: on the Amiga, the ST, DOS, and the X68000.
The mix is the same on every port, within 3 dB: a sound effect at full scale,
and a module channel at full volume playing a full-scale sample, are each as
loud as a tone voice at attenuation 0 (a square wave sampled at full scale
peaks where the voice does), and the noise at attenuation 0 is as loud as a
voice, so modules, effects, voices, and `jlMusicPlay` keep their balance from
one machine to the next. The ports that mix in software put that peak at a
quarter of full scale, so the three voices and the noise together, or a
module's four channels, fit without clipping (the plain ST is the exception,
<<portsDifferAudio>>).
=== Constants === Constants
[cols="3,1,4", options="header"] [cols="3,1,4", options="header"]
@ -113,7 +123,11 @@ void jlAudioStopMod(void);
---- ----
Stops the current module and resets the playhead, so the next `jlAudioPlayMod` Stops the current module and resets the playhead, so the next `jlAudioPlayMod`
starts from the top. starts from the top. It goes silent at once on every port: the ports that mix
ahead of the sound chip (DOS and the X68000) take it back out of what they
have mixed, leaving the sound effects there playing on, as they start a new
module, or a new sound effect, in what is mixed already rather than after
it.
*Parameters:* none. *Returns:* nothing. *Parameters:* none. *Returns:* nothing.
@ -148,7 +162,9 @@ new sample replaces it.
|`sample` |`const uint8_t *` |Raw signed 8-bit PCM. |`sample` |`const uint8_t *` |Raw signed 8-bit PCM.
|`length` |`uint32_t` |Sample count in bytes. |`length` |`uint32_t` |Sample count in bytes.
|`rateHz` |`uint16_t` |The rate the sample was recorded at; the engine |`rateHz` |`uint16_t` |The rate the sample was recorded at; the engine
re-pitches to its own output rate. re-pitches to its own output rate. Any rate plays at its pitch: one faster
than the Amiga's or the IIgs's chip skips samples there
(<<portsDifferAudio>>).
|=== |===
*Returns:* nothing. *Returns:* nothing.
@ -203,7 +219,7 @@ with no caller-side allocation.
|`fill` |`jlAudioStreamFillT` |Refill callback. |`fill` |`jlAudioStreamFillT` |Refill callback.
|`ctx` |`void *` |Opaque pointer handed back to the callback. |`ctx` |`void *` |Opaque pointer handed back to the callback.
|`rateHz` |`uint16_t` |Rate your callback produces; the engine resamples with |`rateHz` |`uint16_t` |Rate your callback produces; the engine resamples with
linear interpolation. linear interpolation. Any rate plays at its pitch, as for `jlAudioPlaySfx`.
|=== |===
*Returns:* nothing. *Returns:* nothing.
@ -232,9 +248,10 @@ void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill,
`jlAudioPlaySfxStreamRaw` is `jlAudioPlaySfxStream` except your callback `jlAudioPlaySfxStreamRaw` is `jlAudioPlaySfxStream` except your callback
delivers bytes *already in that format*, so the port skips its per-byte delivers bytes *already in that format*, so the port skips its per-byte
conversion of every chunk. On the IIgs that conversion costs about 7 conversion of every chunk. On the IIgs that conversion costs about 7
microseconds a byte, some 50 ms for each 7 KB chunk. Every other port's raw microseconds a byte, some 50 ms for each 7 KB chunk. DVX's raw format is the
format is signed PCM, so there this is simply the plain call (DVX still flips IIgs's too, since its sound library's 8-bit PCM is unsigned, and it skips its
each byte for its sound library). flip of every byte. Every other port's raw format is signed PCM, so there this
is simply the plain call.
If your producer cannot make the port's format, use `jlAudioPlaySfxStream`. If your producer cannot make the port's format, use `jlAudioPlaySfxStream`.
@ -279,7 +296,12 @@ What each port can actually do:
PTPlayer picks. PTPlayer picks.
|X68000 |Ignored. Everything mixes into one ADPCM stream, so panning would move |X68000 |Ignored. Everything mixes into one ADPCM stream, so panning would move
all audio at once. all audio at once.
|DOS, ST, STe |Ignored. Output is mono. |DOS |All three on an SB16, which plays stereo: the module's channels on Paula's
sides, as on the Amiga, and each effect on its side. Ignored on the Sound
Blasters before it, which play mono.
|ST |Ignored. Output is mono.
|STe build |All three: the DMA chip plays stereo, the module's channels on
Paula's sides, as on the Amiga, and each effect on its side.
|DVX |All three, through DVX's stereo mixer (on a stereo card). |DVX |All three, through DVX's stereo mixer (on a stereo card).
|SDL3, web, Win32, PSP, Haiku, BeOS |All three: the port mixes in stereo. |SDL3, web, Win32, PSP, Haiku, BeOS |All three: the port mixes in stereo.
|Win16 |Ignored. Output is mono. |Win16 |Ignored. Output is mono.
@ -349,10 +371,10 @@ channels A, B, and C (on the plain ST voice 0 is silent while digital sound
plays); the Amiga uses Paula channels 0..2 looping a two-byte square sample; plays); the Amiga uses Paula channels 0..2 looping a two-byte square sample;
the IIgs uses Ensoniq DOC oscillators playing a resident square on sound the IIgs uses Ensoniq DOC oscillators playing a resident square on sound
effect slots 0..2; the X68000 uses YM2151 channels 0..2, one operator each, effect slots 0..2; the X68000 uses YM2151 channels 0..2, one operator each,
so its tones are sine waves; DVX plays square tones of its sound library, so its tones are sine waves; DVX and the hosted ports mix square waves in
which puts them on the FM chip when there is a free voice and mixes them software (DVX through a stream of its sound library's mixer; on a machine
otherwise; the hosted ports mix square waves in software. The OPL2's waves whose only sound is the PC speaker, DVX's own square tones instead). The
are not square either. OPL2's waves are not square either.
[IMPORTANT] [IMPORTANT]
==== ====
@ -387,9 +409,10 @@ every port as far as its chip goes. Mapping: the ST uses the YM2149 noise
generator mixed onto channel C; the X68000 the YM2151's noise, at the same generator mixed onto channel C; the X68000 the YM2151's noise, at the same
rates exactly; the Amiga Paula channel 3 looping an LFSR noise sample, whose rates exactly; the Amiga Paula channel 3 looping an LFSR noise sample, whose
fastest rate, 28.6 kHz, pitches 0 to 4 all play at; the IIgs a resident noise fastest rate, 28.6 kHz, pitches 0 to 4 all play at; the IIgs a resident noise
sample on slot 3, up to 26 kHz, likewise for pitches 0 to 4; DVX a noise tone sample on slot 3, up to 26 kHz, likewise for pitches 0 to 4; and DVX and the
of its sound library; and the hosted ports mix the YM2149's own noise in hosted ports mix the YM2149's own noise in software (its 17-bit generator),
software (its 17-bit generator), so it sounds as on the ST. DOS uses the OPL2 so it sounds as on the ST (DVX with only the PC speaker: a noise tone of its
sound library). DOS uses the OPL2
rhythm-mode snare, leaving the three tone channels untouched: a tuned drum, rhythm-mode snare, leaving the three tone channels untouched: a tuned drum,
not free-running noise, whose pitch spreads over the snare's range rather than not free-running noise, whose pitch spreads over the snare's range rather than
following the YM2149's rate. following the YM2149's rate.
@ -516,9 +539,8 @@ frame, so the chip only hears the last write and short notes vanish.
|=== |===
*Returns:* `true` if installed. `false` when `hz` is 0, when audio is off or *Returns:* `true` if installed. `false` when `hz` is 0, when audio is off or
did not start, or when the port cannot take the rate (above 1000 Hz on DVX, or did not start, or when the port cannot take the rate (above the mixer's rate
above the mixer's rate on the hosted ports). Uninstalling always returns on DVX and the hosted ports). Uninstalling always returns `true`.
`true`.
[WARNING] [WARNING]
==== ====
@ -529,10 +551,10 @@ allocation, no `printf`, nothing blocking -- and share data with it through
==== ====
On every port the calls average exactly `hz` a second; where they fall On every port the calls average exactly `hz` a second; where they fall
differs. DOS, the Amiga, and the hosted ports place each one at its own time. differs. DOS, the Amiga, DVX, and the hosted ports place each one at its own
The ST and X68000 deliver them from the system's own timer, 200 and 100 times a time. The ST and X68000 deliver them from the system's own timer, 200 and 100
second, so the calls due between two of its ticks come together. The IIgs and times a second, so the calls due between two of its ticks come together. The
DVX deliver them in program time from `jlAudioFrameTick`, so a frame's calls IIgs delivers them in program time from `jlAudioFrameTick`, so a frame's calls
come together at it. come together at it.
On DOS this reprograms PIT counter 0 and hooks INT 8, keeping the BIOS tick On DOS this reprograms PIT counter 0 and hooks INT 8, keeping the BIOS tick
@ -548,17 +570,15 @@ On the IIgs `fn` runs in program time, at `jlAudioFrameTick`, as many times as
the display's beats since the last frame have made due, so the average is the display's beats since the last frame have made due, so the average is
exact whatever the program's frame rate. exact whatever the program's frame rate.
On DVX a timer of DVX's (at most 1000 Hz) counts the ticks, and `fn` runs in On DVX and the hosted ports the sound is mixed in program time, a few frames
program time, once per counted tick, at the next `jlAudioFrameTick`: DVX's ahead (on DVX by its sound library's pump, which DVX also runs while the
sound library, which `jlAudioVoice` and the rest go through there, may not be desktop has the processor), and `fn` is called by the mixer between exactly
called from an interrupt. `fn` may therefore do anything, but what it does the two samples where its tick falls: in program time, so it may do anything,
lands on the frame, not between frames. yet a note it starts begins at the right place in the sound. `hz` may be
anything up to the mixer's rate: 44100 on the hosted ports (22222, or 22050
On the hosted ports the sound is mixed in program time, a few frames ahead, where the card refuses that, on Win16), and on DVX the sound card's mixing
and `fn` is called by the mixer between exactly the two samples where its tick rate. DVX's ticks keep time on a machine with no sound card too: its null
falls: in program time, so it may do anything, yet a note it starts begins at driver runs the mixer as a card would.
the right place in the sound. `hz` may be anything up to the mixer's rate,
44100 (22050 on Win16).
=== jlAudioCriticalEnter / jlAudioCriticalExit === jlAudioCriticalEnter / jlAudioCriticalExit

View file

@ -19,14 +19,15 @@ typedef struct {
|`codegenBytes` |`codegenBytes`
|`uint32_t` |`uint32_t`
|Size of the runtime compiled-sprite cache. `0` selects a per-platform |Size of the runtime compiled-sprite cache. `0` selects a per-platform
default: 8 KB on the IIgs, 64 KB on the Amiga and ST, and 32 KB on the default: 8 KB on the IIgs, 64 KB on the Amiga, ST, and X68000, and 32 KB on
X68000, DOS, DVX, and Win16. The other hosted ports interpret every sprite DOS, DVX, and Win16. The other hosted ports interpret every sprite and have
and never use the cache, but still allocate it (32 KB by default). The no cache: they allocate none, whatever is asked. Every port takes as much of
cache is one block, and `jlInit` fails if it cannot be had -- except on the the request as the machine has, and `jlInit` fails only if it can have none
IIgs, where it is up to four Memory Manager blocks of at most 64 KB, each of it; `jlSpriteCodegenBytesTotal()` says what it got. On the IIgs the cache
inside one 64 KB bank, so at most 256 KB; a bank that is refused is tried is up to four Memory Manager blocks of at most 64 KB, each inside one 64 KB
again at 60 KB, and `jlInit` takes whatever part of the request it was bank, so at most 256 KB (a bank that is refused is tried again at 60 KB);
granted. Size it against `jlSpriteCompiledSize()`. elsewhere it is one block, halved until the heap has room, down to 4 KB.
Size it against `jlSpriteCompiledSize()`.
|`noAudio` |`noAudio`
|`bool` |`bool`
@ -60,9 +61,9 @@ sound chip needs no engine: the Amiga, the ST, DOS, and the X68000.
|=== |===
*Returns:* `true` on success. On failure returns `false` and `jlLastError()` *Returns:* `true` on success. On failure returns `false` and `jlLastError()`
describes the reason. Where the port's own start-up fails without saying describes the reason, on every port: for example, the plain ST refuses a
why, as the IIgs, Amiga, ST, X68000, and DVX ports' can, the reason is just monochrome monitor, and the Amiga says when chip RAM cannot hold the two
`jlpInit failed`. screen pages.
=== jlShutdown === jlShutdown
@ -74,8 +75,10 @@ void jlShutdown(void);
Shuts the library down: stops audio first, then restores the display mode, Shuts the library down: stops audio first, then restores the display mode,
releases the library's own resources, and un-hooks any interrupt vectors the releases the library's own resources, and un-hooks any interrupt vectors the
port installed. Surfaces and sprites the program created are not released: port installed. Surfaces and sprites the program created are not released:
destroy them with `jlSurfaceDestroy` and `jlSpriteDestroy` first. Call it on destroy them with `jlSurfaceDestroy` and `jlSpriteDestroy` first. A program
every exit path; skipping it can leave the machine unusable. that ends without calling it, by returning from `main` or calling `exit`, has
it called then: the first successful `jlInit` registers it with `atexit`.
Nothing can run it after a crash or an `abort`.
*Parameters:* none. *Returns:* nothing. *Parameters:* none. *Returns:* nothing.
@ -191,7 +194,11 @@ Blocks until the next display vertical blank, pacing a game loop to the
display's native refresh rate. On the machine ports it waits for the display's native refresh rate. On the machine ports it waits for the
display's own vertical blank. The hosted ports, and DVX in a window, have no display's own vertical blank. The hosted ports, and DVX in a window, have no
vertical blank to wait for: there it waits for the next tick of a virtual vertical blank to wait for: there it waits for the next tick of a virtual
frame clock, 60 Hz on the hosted ports and 70 Hz on DVX, and nothing stops a frame clock, 60 Hz on the hosted ports and 70 Hz on DVX. A hosted window shows
one frame a tick, as a display shows one a refresh (a present in a tick
already shown waits for the next); a desktop that composites its windows, as
every current one does, puts only whole frames on the screen, but on one that
does not (Windows 3.1, a bare X11 desktop) and in a DVX window nothing stops a
present from tearing. present from tearing.
*Parameters:* none. *Returns:* nothing. *Parameters:* none. *Returns:* nothing.
@ -243,9 +250,15 @@ seconds.
uint32_t jlMillisElapsed(void); uint32_t jlMillisElapsed(void);
---- ----
Monotonic millisecond counter since `jlInit`. It may advance a frame at a Monotonic millisecond counter since `jlInit`, read from a hardware timer to
time. Two calls separated by a long render report the true elapsed time, not the millisecond on every port: the IIgs from the frame count and where the
the number of vertical blanks that fired. beam is, the ST and the X68000 from their system tick and the MFP timer
counting down to the next, the Amiga from the E-clock, and DOS from the PIT.
A machine whose timer is taken (an X68000 whose Timer C a resident program
has set to another rate, an Amiga with no timer.device) falls back to the
frame count and advances a frame at a time. Two calls separated by a long
render report the true elapsed time, not the number of vertical blanks that
fired.
*Parameters:* none. *Parameters:* none.

View file

@ -2,13 +2,13 @@
Declared in `<joey/debug.h>`. Declared in `<joey/debug.h>`.
A crash-tracing logger that writes `joeylog.txt` in the current directory A crash-tracing logger that writes `joeylog.txt` in the program's folder,
(on DVX, in the program's settings folder, beside `DATA/` and `SAVES/` (<<_files>>: on the ST, the current directory; on
`CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`). Debugging a program on an emulated DVX, the program's settings folder, `CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`).
1988 machine is awkward, and this is usually the fastest way to find out what Debugging a program on an emulated 1988 machine is awkward, and this is usually
it did before it stopped doing it. The headless run scripts retrieve the log the fastest way to find out what it did before it stopped doing it. The
automatically. The library itself writes to it only when something fails, such headless run scripts retrieve the log automatically. The library itself writes
as sound that does not start. to it only when something fails, such as sound that does not start.
How the file is written differs: How the file is written differs:
@ -20,21 +20,19 @@ How the file is written differs:
`jlLogFlush`, which replaces the file with the last 8 KB logged. A program `jlLogFlush`, which replaces the file with the last 8 KB logged. A program
that never calls it leaves no log. that never calls it leaves no log.
* *The web* writes the file into the page's memory, where it is lost when the * *The web* writes the file into the page's memory, where it is lost when the
page closes. page closes, so every line also goes to the browser's console.
* *The hosted ports* make the program's folder the current directory in
`jlInit`: a line logged before that goes to a log in the directory the
program was started from, and the rest of the run's lines follow it there.
[NOTE] [NOTE]
==== ====
The logger's buffer is linked into every program, since the library logs its The logger's buffer is linked into every program, since the library logs its
own failures: the 8 KB ring on the IIgs, and a 16 KB file buffer on every own failures: the 8 KB ring on the IIgs, and a 512-byte line buffer on every
other port but DVX. A memory-tight program can still leave its own log calls other port but DVX. A memory-tight program can still leave its own log calls
out and observe hardware state through the emulator instead. out and observe hardware state through the emulator instead.
==== ====
On DOS the library also creates `CRASH.LOG` in the current directory at On DOS the library also opens `CRASH.LOG` beside the program at `jlInit`, and
`jlInit`, and writes the processor's registers there if the program faults. writes the processor's registers there if the program faults; a run that shuts
down cleanly deletes it, so it is only there after a crash.
=== jlLog === jlLog
@ -61,8 +59,8 @@ void jlLogF(const char *fmt, ...);
---- ----
Appends a formatted message, `printf` style, as one line: like `jlLog`, the Appends a formatted message, `printf` style, as one line: like `jlLog`, the
library ends the line, so the format needs no trailing `\n`. On the IIgs a library ends the line, so the format needs no trailing `\n`. A formatted line
line is cut at 199 characters. is cut at 255 characters, on every port.
[cols="1,1,4", options="header"] [cols="1,1,4", options="header"]
|=== |===

View file

@ -8,24 +8,29 @@ packager:
* `DATA/` -- game content: levels, sprites, audio. * `DATA/` -- game content: levels, sprites, audio.
* `SAVES/` -- writable saves and preferences. * `SAVES/` -- writable saves and preferences.
Where the calls look for them: The calls find them in the program's own folder, wherever the program was
started from, and before `jlInit` as well as after (the log, `joeylog.txt`,
goes there too):
* *The hosted ports* make the program's own folder the current directory in * *DOS* takes the folder from the path it started the program by, *the Amiga*
`jlInit`, so both are found beside the program wherever it was started uses `PROGDIR:` (Kickstart 2.0 and later; under 1.3, the current
from. directory), *the X68000* reads it from the program's process block, *the
* *DOS, the ST, the Amiga, and the X68000* look in the current directory. A IIgs* uses GS/OS prefix 1 (the application's folder), and *the hosted ports*
program started from its own folder (as a desktop starts it) finds them; take it from their executable's path. The hosted ports also make it the
one started from a command line elsewhere finds no `DATA/` and makes its current directory in `jlInit`, for the program's own relative paths.
`SAVES/` there. * *The ST* looks in the current directory: TOS tells a program nothing of
* *The IIgs* looks for `DATA/` under GS/OS prefix 0 and puts `SAVES/` in the where it was started from. A program the desktop starts runs in its own
application's folder (prefix 1). When the two differ, data and saves come folder; one started from a command line elsewhere finds no `DATA/` and makes
from different folders. its `SAVES/` there.
* *DVX* is the exception, because its programs share one current directory: * *DVX* keeps the two apart, because its programs share one current
there `DATA/` is beside the program, in its folder under DVX's `APPS`, and directory: there `DATA/` is beside the program, in its folder under DVX's
`SAVES/` is in the program's own settings folder, `APPS`, and `SAVES/` is in the program's own settings folder,
`CONFIG\APPS\JOEYLIB\NAME`, which DVX keeps apart from the program files so `CONFIG\APPS\JOEYLIB\NAME`, which DVX keeps apart from the program files so
a reinstall never overwrites it. The calls below find both there by a reinstall never overwrites it.
themselves. * *The web* keeps them apart too: `DATA/` is packed into the program, and
`SAVES/` and the log are in a folder named after the page
(`games/hero.html` has `/games/hero/`), its `SAVES/` kept in the browser's
storage for that page alone (<<_web_programs>>).
Both are addressed by a *bare name relative to their directory*. The prefix is Both are addressed by a *bare name relative to their directory*. The prefix is
added for you, so never hard-code `"DATA/"` or `"SAVES/"` yourself. added for you, so never hard-code `"DATA/"` or `"SAVES/"` yourself.
@ -97,9 +102,12 @@ whole-file blob prefer `jlSaveWrite` and `jlSaveRead`.
bool jlSaveWrite(const char *name, const void *buf, uint32_t len); bool jlSaveWrite(const char *name, const void *buf, uint32_t len);
---- ----
Writes a whole save file, replacing any existing contents. The write is not Writes a whole save file, replacing any existing contents. The bytes go to a
atomic: if it fails part way (a full disk, say) the save is left short. A temporary file first, and only once all of them are written does it take the
`NULL` `buf` with a non-zero `len` returns `false` and leaves the save alone. save's place, so a write that fails part way (a full disk, say, or the machine
switched off) leaves the old save as it was. A `NULL` `buf` with a non-zero
`len` returns `false` and leaves the save alone. A save written with
`jlSaveOpen` is written in place, as `fopen` writes.
[cols="1,1,4", options="header"] [cols="1,1,4", options="header"]
|=== |===

View file

@ -97,9 +97,9 @@ characters are dropped until you pop some. A chord types nothing on any port:
Ctrl+M is not Return and Ctrl+1 is not 1, and Alt chords type nothing where Ctrl+M is not Return and Ctrl+1 is not 1, and Alt chords type nothing where
Alt is a command key (DOS, DVX, Windows, the Be ports' Command key, the PSP's Alt is a command key (DOS, DVX, Windows, the Be ports' Command key, the PSP's
on-screen keyboard); the Amiga's Alt and Windows' AltGr type their layout's on-screen keyboard); the Amiga's Alt and Windows' AltGr type their layout's
characters, and so does the ST's Alt where TOS keeps its keyboard's Alt characters, and so does the ST's Alt, the characters its national
layer in its tables (TOS 4.00 and later, and EmuTOS: a German keyboard's keyboard's Alt layer gives (a German keyboard's @ [ ] { } \;
@ [ ] { } \), and otherwise nothing (<<portsDifferInput>>). A held key <<portsDifferInput>>). A held key
repeats its character on every port (the ST at the rate TOS is set to); repeats its character on every port (the ST at the rate TOS is set to);
repeat policy is yours to manage. 7-bit ASCII only -- no Unicode, no input repeat policy is yours to manage. 7-bit ASCII only -- no Unicode, no input
methods. methods.

View file

@ -23,15 +23,15 @@ the ST goes through the BIOS Aux channel; the X68000 transmits through IOCS
and receives on its own SCC interrupt; DVX goes through DVX's serial library, and receives on its own SCC interrupt; DVX goes through DVX's serial library,
which owns the COM ports for every program on the desktop (a port another which owns the COM ports for every program on the desktop (a port another
program has open cannot be opened); Win32 and Win16 go through Windows' own program has open cannot be opened); Win32 and Win16 go through Windows' own
communications calls, whose driver owns the ports the same way. SDL3, the web, communications calls, whose driver owns the ports the same way; SDL3 on Linux,
the PSP, Haiku, and BeOS have no serial port: `jlSerialOpen` fails there. Haiku, and BeOS open the port as a terminal device through termios. The web and
the PSP have no serial port: `jlSerialOpen` fails there.
The model is *non-blocking and buffered*. Received bytes accumulate in a The model is *non-blocking and buffered*. Received bytes accumulate in a
background ring -- a hardware interrupt on DOS, the X68000, and the IIgs, the background ring -- a hardware interrupt on DOS, the X68000, and the IIgs, the
OS on Amiga, ST, DVX, and Windows -- and reads never block. Call `jlSerialPoll` OS on Amiga, ST, DVX, Windows, Linux, Haiku, and BeOS -- and reads never
once per frame. (The X68000 takes its interrupt only when the port is opened in block. Call `jlSerialPoll`
supervisor mode, which `jlInit` puts every program in; opened without it, the once per frame.
port is drained from RSDRV.SYS's own 64-byte buffer by the serial calls.)
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
@ -70,9 +70,10 @@ at 9600 baud, so:
At 19200 baud each lasts half as long, and so on. At 19200 baud each lasts half as long, and so on.
A byte that arrives with a framing, parity, or overrun error is dropped on the A byte that arrives with a framing, parity, or overrun error is delivered as
X68000 and delivered as it came on the IIgs and DOS; on the other ports the it came on the IIgs, DOS, the X68000, Linux, Haiku, and BeOS (the port does
system's driver decides. not ask the driver to check parity); on the other ports the system's driver
decides.
JoeyLib's own work is safe while a port is open, with one exception on the JoeyLib's own work is safe while a port is open, with one exception on the
X68000 (below). On the IIgs, where the X68000 (below). On the IIgs, where the
@ -106,6 +107,14 @@ These can still lose bytes, and are the program's to schedule around:
that relies on the protocol to recover. The built-in modem and printer ports 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, stay loss-free at 9600 through full-screen presents, clears, joystick reads,
and sound effects. and sound effects.
* *The IIgs's built-in ports above 9600 baud.* At 19200 the present's ~1.7 ms
stretches outlast the chip's 3 bytes, and a stream received through
full-screen presents loses about one byte in 70. From 38400 up a steady
stream keeps the 65816 in its interrupt handler until it ends, even with
nothing drawn: the program gets no time until then, and the 512-byte receive
buffer keeps the first 511 bytes. Those rates suit bursts that fit the
buffer, a protocol that paces its blocks (XMODEM's acknowledgements do), and
sending, which they do at full speed.
* *Per-scanline palettes on the X68000.* The port reloads its one hardware * *Per-scanline palettes on the X68000.* The port reloads its one hardware
palette as the beam scans, and a run of palette changes on consecutive lines palette as the beam scans, and a run of palette changes on consecutive lines
holds serial receive off for the whole run, about 32 microseconds a line. holds serial receive off for the whole run, about 32 microseconds a line.
@ -209,8 +218,9 @@ A field left 0 takes a default: `baud` 9600, `dataBits` 8, and `stopBits` 1.
|Port |Rates |Port |Rates
|IIgs modem and printer ports |IIgs modem and printer ports
|115200 divided by a whole number, up to 57600; a rate between two of those |115200 divided by a whole number: 115200 itself straight from the ports'
runs at the one above it, and anything faster at 57600. crystal, the rest from the SCC's baud rate generator. A rate between two of
those runs at the one above it, and anything faster at 115200.
|IIgs Super Serial Card |IIgs Super Serial Card
|The 6551's fifteen rates, 50 to 19200; any other runs at the fastest one |The 6551's fifteen rates, 50 to 19200; any other runs at the fastest one
@ -240,12 +250,21 @@ A field left 0 takes a default: `baud` 9600, `dataBits` 8, and `stopBits` 1.
|Win16 |Win16
|The rate as asked up to 57600; above that, see below. |The rate as asked up to 57600; above that, see below.
|SDL3 (Linux), Haiku, BeOS
|The nearest of 300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, and
115200, handed to the system's driver.
|=== |===
The IIgs is the machine with three device choices; elsewhere `JL_SERIAL_MODEM` The IIgs is the machine with three device choices; elsewhere `JL_SERIAL_MODEM`
and `JL_SERIAL_PRINTER` both mean the default port. `JL_SERIAL_DEFAULT` selects and `JL_SERIAL_PRINTER` both mean the default port. `JL_SERIAL_DEFAULT` selects
the IIgs modem port; COM1 on DOS, DVX, Win32, and Win16; the ST Modem port; the IIgs modem port; COM1 on DOS, DVX, Win32, and Win16; the ST Modem port;
Amiga unit 0; or the X68000's RS-232C port. Amiga unit 0; the X68000's RS-232C port; on Linux `/dev/ttyS0`; or on Haiku and
BeOS the first entry of `/dev/ports`. On Linux, Haiku, and BeOS the
`JOEYLIB_SERIAL` environment variable names another device for the default
(and the modem and printer) port: a USB adapter, `JOEYLIB_SERIAL=/dev/ttyUSB0`.
Haiku's and BeOS's drivers take 7 or 8 data bits only, so 5 or 6 fail the
open there.
On Win16 a `baud` above 57600 runs at 115200 through KPCOMM.DRV, the On Win16 a `baud` above 57600 runs at 115200 through KPCOMM.DRV, the
communications driver JoeyLib's Windows runs with in place of Windows 3.1's communications driver JoeyLib's Windows runs with in place of Windows 3.1's
@ -262,8 +281,16 @@ the usual tolerance of a UART at the far end.
`unit` disambiguates `JL_SERIAL_SLOT` and is ignored otherwise: an IIgs Super `unit` disambiguates `JL_SERIAL_SLOT` and is ignored otherwise: an IIgs Super
Serial Card slot 1..7, a DOS or DVX COM number 1..4, a Win16 COM number Serial Card slot 1..7, a DOS or DVX COM number 1..4, a Win16 COM number
1..9, a Win32 COM number 1..255, or an Amiga `serial.device` unit. The ST and 1..9, a Win32 COM number 1..255, an Amiga `serial.device` unit, or on Linux
X68000 have one port, and open it for every device, `JL_SERIAL_SLOT` included. `/dev/ttyS<unit - 1>` and on Haiku and BeOS the unit'th entry of `/dev/ports`
in name order. On the ST family it is a port in TOS's Bconmap order: 1 the
Modem port, and on a Mega STe, TT, or Falcon 2 and up its other ports (a
Mega STe's SCC channel B, Modem 2, is 2 and its LAN port 3; a TT adds its
second MFP's port as 3 and has the LAN port at 4). The open maps TOS's AUX
device to that port and the close maps it back; a plain ST has unit 1 only.
The other devices open AUX wherever TOS has it, the Modem port unless the
user mapped it elsewhere. The X68000 has one port, and opens it for every
device, `JL_SERIAL_SLOT` included.
=== jlSerialOpen === jlSerialOpen
@ -300,9 +327,11 @@ open when the program exits is closed then: the first successful
`jlSerialOpen` registers an `atexit` handler, because on DOS, the X68000, and `jlSerialOpen` registers an `atexit` handler, because on DOS, the X68000, and
the IIgs the port's receive interrupt vector points into the program until it the IIgs the port's receive interrupt vector points into the program until it
is closed. Bytes `jlSerialWrite` accepted are still sent, so a program can is closed. Bytes `jlSerialWrite` accepted are still sent, so a program can
write a farewell and close straight away. DOS, the ST, and the X68000 put back write a farewell and close straight away. DOS, the ST, the X68000, Linux,
the speed and framing the port had before the open; the IIgs (whose serial Haiku, and BeOS put back the speed and framing the port had before the open
chip's settings cannot be read back) and DVX leave it as JoeyLib set it. (Linux, Haiku, and BeOS with HUPCL off, so the system's own last close keeps
DTR up); the IIgs (whose serial chip's settings cannot be read back) and DVX
leave it as JoeyLib set it.
Closing does *not* hang up: `jlSerialClose` leaves DTR exactly as it is, so Closing does *not* hang up: `jlSerialClose` leaves DTR exactly as it is, so
a modem call or a BBS caller survives the program handing the line back: a a modem call or a BBS caller survives the program handing the line back: a
@ -310,9 +339,11 @@ door returning to its BBS, or a terminal program exiting while the call stays
up. RTS is not part of that promise; on the Amiga, `serial.device` puts back up. RTS is not part of that promise; on the Amiga, `serial.device` puts back
whatever RTS was before the open. whatever RTS was before the open.
Windows cannot make the promise. On Win32 and Win16 the close gives the port Win32 cannot make the promise: the close gives the port back to Windows'
back to Windows' driver, and neither has a call that keeps DTR up past it. A driver, which has no call that keeps DTR up past it, so a Win32 program that
Windows program that must not hang up the line keeps the port open. must not hang up the line keeps the port open. Win16 keeps it through
KPCOMM.DRV, the communications driver JoeyLib's Windows runs with; with
Windows 3.1's own COMM.DRV it drops too.
To hang up, drop DTR first: call `jlSerialSetDtr(false)`, wait longer than To hang up, drop DTR first: call `jlSerialSetDtr(false)`, wait longer than
the modem needs to notice (a Hayes modem's S25 setting, 50 ms by default), the modem needs to notice (a Hayes modem's S25 setting, 50 ms by default),
@ -385,10 +416,10 @@ uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len);
---- ----
Sends bytes without waiting long. Most ports feed the bytes to the line as it Sends bytes without waiting long. Most ports feed the bytes to the line as it
takes them and give up after a bounded wait when it stalls (a peer holding takes them, and give up once it has taken none for 100 ms (a peer holding
transmit off, say); the Amiga queues them in two 1 KB buffers it sends from in transmit off, say). The Amiga queues them in two 1 KB buffers it sends from in
the background, and Win16 in Windows' driver's queue, so neither waits at the background, DVX in its serial library's 2 KB ring, and Win16 in Windows'
all. driver's queue, so those never wait at all.
[cols="1,1,4", options="header"] [cols="1,1,4", options="header"]
|=== |===
@ -465,6 +496,11 @@ Two ports tie DTR to more than the line:
whenever it runs, so the next `jlSerialOpen` -- or another program setting whenever it runs, so the next `jlSerialOpen` -- or another program setting
the line -- raises it again. the line -- raises it again.
On a Mega STe's, TT's, or Falcon's ports other than the Modem port (a
`JL_SERIAL_SLOT` open, or AUX mapped there) DTR stays as TOS set it: TOS has
no call for those ports' lines, and JoeyLib does not reach past its driver to
the SCC.
Closing after a hang-up keeps DTR down, but on two ports it rises briefly on Closing after a hang-up keeps DTR down, but on two ports it rises briefly on
the way: on the Amiga for under a millisecond (`serial.device` puts back the the way: on the Amiga for under a millisecond (`serial.device` puts back the
lines as they were at its open) and on the X68000 for about 0.2 ms (close puts lines as they were at its open) and on the X68000 for about 0.2 ms (close puts
@ -487,7 +523,8 @@ pause actually needs. Under `JL_SERIAL_FLOW_RTSCTS` on DOS, the X68000, and a
IIgs Super Serial Card, RTS is up only while both the port's handshake and IIgs Super Serial Card, RTS is up only while both the port's handshake and
this call want it; elsewhere the driver may change it again (see Flow this call want it; elsewhere the driver may change it again (see Flow
Control). The IIgs modem and printer ports have no RTS line on their Control). The IIgs modem and printer ports have no RTS line on their
connector, so nothing reaches the peer there. connector, so nothing reaches the peer there, and a Mega STe's, TT's, or
Falcon's ports other than the Modem port keep RTS as TOS sets it (as DTR).
[cols="1,1,4", options="header"] [cols="1,1,4", options="header"]
|=== |===

View file

@ -70,7 +70,7 @@ static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)];
==== ====
Give the backing array *at least 2-byte alignment*. On the Amiga, ST, and Give the backing array *at least 2-byte alignment*. On the Amiga, ST, and
X68000 an odd buffer address sends the save and restore calls down a much X68000 an odd buffer address sends the save and restore calls down a much
slower path, and on the Amiga and ST it makes the trusted variants crash; the slower path, and it makes the trusted variants crash; the
other ports do not mind. Declaring it `uint16_t`, as above, guarantees the other ports do not mind. Declaring it `uint16_t`, as above, guarantees the
alignment. alignment.
==== ====
@ -361,8 +361,8 @@ following hold:
16-pixel-aligned x and width on the Amiga, ST, and X68000, even on the 16-pixel-aligned x and width on the Amiga, ST, and X68000, even on the
other ports, and a height equal to the sprite's pixel height (on the IIgs, other ports, and a height equal to the sprite's pixel height (on the IIgs,
to the rows the compiled save keeps, those holding an opaque pixel); to the rows the compiled save keeps, those holding an opaque pixel);
* on the Amiga and ST, `+backup->bytes+` is at least 2-byte aligned, for the * on the Amiga, ST, and X68000, `+backup->bytes+` is at least 2-byte aligned,
trusted save and save-and-draw as well as the restore; for the trusted save and save-and-draw as well as the restore;
* the recorded region is fully on the destination surface. * the recorded region is fully on the destination surface.
Use the plain calls anywhere those invariants are not statically guaranteed. Use the plain calls anywhere those invariants are not statically guaranteed.
@ -424,9 +424,8 @@ Arena introspection, for judging whether to compact or to raise
*Parameters:* none. *Parameters:* none.
*Returns:* bytes in use, and total arena size. Free space is the difference, *Returns:* bytes in use, and total arena size. Free space is the difference,
though it may be fragmented across holes until `jlSpriteCompact` runs. On the though it may be fragmented across holes until `jlSpriteCompact` runs. The
IIgs the total is what `jlInit` was granted, which can be less than total is what `jlInit` was granted, which can be less than `codegenBytes`
`codegenBytes` asked for, and it is split into blocks of at most 64 KB that asked for on a machine short of memory. On the IIgs it is split into blocks
compacting never merges: one sprite's code must fit in one block. On the of at most 64 KB that compacting never merges: one sprite's code must fit in
ports that interpret every sprite the total is the arena they allocated one block. The ports that interpret every sprite have no arena: both are 0.
anyway, and the bytes in use stay 0.

View file

@ -40,8 +40,9 @@ jlSurfaceT *jlSurfaceCreate(void);
---- ----
Allocates a new offscreen surface. Each one costs about 32 KB. On the Amiga Allocates a new offscreen surface. Each one costs about 32 KB. On the Amiga
that comes from chip RAM, because the blitter works on the planes there, so on that comes from chip RAM, where the blitter can work on its planes; once chip
a 512 KB machine, beside the display's own pages, only a few fit. RAM is full, a surface comes from Fast RAM instead, and its fills and copies
run on the CPU, more slowly, so make the surfaces drawn on most first.
*Parameters:* none. *Parameters:* none.
@ -228,6 +229,14 @@ Drawing primitives mark per-row dirty ranges as a side effect, so you only need
to call this at the end of a frame and everything you drew appears. Rows to call this at the end of a frame and everything you drew appears. Rows
nothing touched since the last present are skipped. nothing touched since the last present are skipped.
The Amiga, the ST, DOS, and the X68000 draw into a second screen page and flip
to it at the next vertical blank, so the picture never tears. A present that comes before
that blank, right after another one, waits for it: the page it would draw into
is still on the screen. Present once a frame, after `jlWaitVBL`, and it never
waits. On DOS, a present that changes colours also waits: VGA loads them in
the blank that shows the new page, and the `jlWaitVBL` after it returns at
once, so the loop keeps its pace.
*Parameters:* none. *Returns:* nothing. *Parameters:* none. *Returns:* nothing.
[NOTE] [NOTE]

View file

@ -103,7 +103,7 @@ depending on the machine, and nothing warns you.
*Align your backup buffers.* Give any `jlSpriteBackupT` backing array 2-byte *Align your backup buffers.* Give any `jlSpriteBackupT` backing array 2-byte
alignment, by declaring it `uint16_t` or with an alignment attribute. On the alignment, by declaring it `uint16_t` or with an alignment attribute. On the
Amiga, ST, and X68000 an odd buffer address sends save and restore down a much Amiga, ST, and X68000 an odd buffer address sends save and restore down a much
slower path, and on the Amiga and ST it crashes the trusted variants. slower path, and it crashes the trusted variants.
[source,c] [source,c]
---- ----
@ -257,8 +257,8 @@ The API is uniform; these are the places where what you get differs.
|Stereo pan |Stereo pan
|all three |all three
|all three (centred on a free channel each side) |all three (centred on a free channel each side)
|ignored (mono) |ignored (mono); all three in the STe build
|ignored (mono) |all three on an SB16; ignored (mono) before it
|ignored |ignored
|all three |all three
|all three |all three
@ -313,7 +313,7 @@ The API is uniform; these are the places where what you get differs.
|COM1..COM4 |COM1..COM4
|RS-232C (needs RSDRV.SYS) |RS-232C (needs RSDRV.SYS)
|COM1..COM4 (DVX's library) |COM1..COM4 (DVX's library)
|Win32: COM1 and up; SDL3, web, PSP, Haiku, BeOS: none |Win32: COM1 and up; SDL3 (Linux), Haiku, BeOS: terminal devices; web, PSP: none
|COM1..COM9 |COM1..COM9
|=== |===
@ -366,27 +366,29 @@ and 56 under MAME, whose X68000 display runs slightly fast (56.07 Hz).
timer to the millisecond. timer to the millisecond.
Per-scanline palettes cost CPU time on the ST and the X68000, whose displays Per-scanline palettes cost CPU time on the ST and the X68000, whose displays
have one 16-colour palette, and a stall on the Amiga when the bands move. The have one 16-colour palette. Both reload it as the beam scans, and store each
ST takes a timer interrupt at every change of palette, every frame, and copies band's 16 colours in the horizontal blanking before the band's first line, so
the next band's 16 colours there. The X68000 reloads its palette as the beam the line shows them from its first pixel. The ST takes a timer interrupt a
scans: an interrupt stores the next band's colours in the horizontal blanking line above every change of palette, every frame, and waits in it for that
before the band's first line. A screen with one palette costs nothing on line's display to end: about one line of CPU time (64 microseconds) a change.
either and arms no interrupt. On the X68000 each change of palette between one A screen with one palette costs nothing on either and arms no interrupt. On
the X68000 each change of palette between one
line and the next costs about one line of CPU time (32 microseconds); a band line and the next costs about one line of CPU time (32 microseconds); a band
more than two lines from the one before costs about two. A screen that changes more than two lines from the one before costs about two. A screen that changes
palette on every line therefore spends a third of each frame in that palette on every line therefore spends a third of each frame in that
interrupt, during which serial and mouse interrupts wait (see the serial interrupt, during which serial and mouse interrupts wait (see the serial
chapter for how long a run can be before received bytes are lost). On the chapter for how long a run can be before received bytes are lost). The ST
Amiga the copper changes the palettes at no CPU cost, but an SCB change that does the same for palettes one line apart, and spends those lines in the
moves where a band starts or ends rebuilds the copper list, which holds up interrupt. On the Amiga the copper changes the palettes at no CPU cost: a
that `jlStagePresent` for about three frames on an A500; a change of colours, present that changes palettes or the SCB writes a list of the bands' copper
or of which palette an unmoved band uses, costs nothing. On the ST a instructions, a few hundred bytes. On the Amiga, the ST, DOS, and the X68000
present's colours and pixels show from the same frame: it hands the a present's colours and pixels show from the same frame: the Amiga's VBL
interrupts their new colours with the page it flips to. On the X68000, a switches the copper to the new list with the page it flips to, the ST and
screen of two or more bands takes palette and SCB changes at the start of the the X68000 hand their interrupts the new colours with the page they flip to,
next frame rather than mid-frame, so a fade can lag its pixels by one frame and DOS loads changed colours in the vertical blank that shows the new page.
but never tears; a screen of one palette has its colours written at the A present that changes colours right after another, before the vertical
present. blank that shows the first, waits for it on the Amiga and DOS (and on DOS
the `jlWaitVBL` after it returns at once).
=== Where the Ports Still Differ === Where the Ports Still Differ
@ -422,21 +424,11 @@ names the ports, says how they differ, and gives the reason.
it among all its programs, so Win32 and a DVX window show each colour as the it among all its programs, so Win32 and a DVX window show each colour as the
nearest one the display has, and Win16 shows them exactly only while the 236 nearest one the display has, and Win16 shows them exactly only while the 236
entries left are enough (<<_windows_programs>>). entries left are enough (<<_windows_programs>>).
* *Amiga and X68000: colours a frame off their pixels.* A present's new
colours and new pixels show from the same frame on the other ports (the IIgs
and DOS change both at once, and can tear). The Amiga's pixels change at the
next frame, when the copper loads the new bitplane pointers, but the bands
below the beam take their new colours in the frame the present runs: the
copper list is graphics.library's, one per screen, and JoeyLib patches its
colours in place. The X68000 draws straight into the displayed text plane,
which has no second page, while a screen of two or more bands takes its new
colours at the start of the next frame, so there the colours come a frame
after the pixels.
* *ST and X68000: per-scanline palettes cost time.* Both displays have one * *ST and X68000: per-scanline palettes cost time.* Both displays have one
16-colour palette at a time, so the CPU loads each band's colours as the beam 16-colour palette at a time, so the CPU loads each band's colours as the beam
reaches it: the ST in a timer interrupt at every change of palette, every reaches it: the ST in a timer interrupt at every change of palette, every
frame, and the X68000 in a raster interrupt. A screen with one palette costs frame (about a line of CPU time each), and the X68000 in a raster interrupt.
nothing (<<_per_platform_behaviour_summary>>). A screen with one palette costs nothing (<<_per_platform_behaviour_summary>>).
[[portsDifferInput]] [[portsDifferInput]]
==== Input ==== Input
@ -454,11 +446,14 @@ names the ports, says how they differ, and gives the reason.
(Delete is the backspace key), and one Shift as far as JoeyLib can tell: (Delete is the backspace key), and one Shift as far as JoeyLib can tell:
`JL_KEY_LSHIFT` and `JL_KEY_RSHIFT` both read either. Option is `JL_KEY_LSHIFT` and `JL_KEY_RSHIFT` both read either. Option is
`JL_KEY_LALT`. `JL_KEY_LALT`.
* *ST keyboard: the Alt layer on TOS before 4.00.* A national keyboard types * *ST keyboard: no Alt layer on two countries' older TOS.* A national keyboard
some ASCII with Alt (a German one's @ [ ] { } \). TOS 4.00 and later, and types some ASCII with Alt (a German one's @ [ ] { } \). TOS 4.00 and later,
EmuTOS, keep that layer in the keyboard tables JoeyLib reads, so Alt types and EmuTOS, keep that layer in the keyboard tables JoeyLib reads; older TOS
it; older TOS keeps it in the keyboard code JoeyLib replaces, so there Alt keeps it in the keyboard code JoeyLib replaces, so JoeyLib carries its own
with a key types nothing, as on the ports where Alt is a command key. copies of those layers, by the ROM's country: Germany, France, Spain,
Italy, Sweden, Finland, Norway, and German Switzerland. On the French Swiss
and Danish TOS before 4.00, whose layers it does not know, Alt with a key
types nothing.
* *X68000 keyboard: one Shift.* Both Shift keys send the same code, so * *X68000 keyboard: one Shift.* Both Shift keys send the same code, so
`JL_KEY_LSHIFT` and `JL_KEY_RSHIFT` both read either. OPT.1 and OPT.2 are `JL_KEY_LSHIFT` and `JL_KEY_RSHIFT` both read either. OPT.1 and OPT.2 are
`JL_KEY_LALT`. `JL_KEY_LALT`.
@ -491,7 +486,13 @@ names the ports, says how they differ, and gives the reason.
and its noise is mixed into channel C, voice 2's. Author noise hits and and its noise is mixed into channel C, voice 2's. Author noise hits and
voice-2 notes apart; `songbake` warns when they overlap. voice-2 notes apart; `songbake` warns when they overlap.
* *ST: volume steps.* The YM2149 has 16 volume levels about 3.5 dB apart, so a * *ST: volume steps.* The YM2149 has 16 volume levels about 3.5 dB apart, so a
2 dB attenuation step is matched to within 1.6 dB. 2 dB attenuation step is matched to within 1.4 dB on the plain ST and 1.9 dB
on the STe, whose voices start a level lower to balance its louder digital
sound.
* *Plain ST: a loud module clips.* The plain ST plays all its digital sound
through one sound chip channel, so the mix gives a module channel half of
it: a module whose three or four channels peak at the same moment clips.
Everywhere else all four fit.
* *Amiga: four channels for everything.* Paula has four sound channels, two * *Amiga: four channels for everything.* Paula has four sound channels, two
on each side, shared by the module, the effects, the three tone voices, and on each side, shared by the module, the effects, the three tone voices, and
the noise. At most four sounds play at once, an effect can take a channel a the noise. At most four sounds play at once, an effect can take a channel a
@ -506,18 +507,47 @@ names the ports, says how they differ, and gives the reason.
voices or `jlMusicPlay` sound. voices or `jlMusicPlay` sound.
* *IIgs: mono.* A stock IIgs mixes its stereo channels into one speaker, so * *IIgs: mono.* A stock IIgs mixes its stereo channels into one speaker, so
panning is heard only with a stereo card. panning is heard only with a stereo card.
* *IIgs: tone pitch in steps.* The DOC sets a voice's pitch in steps of
1.6 Hz for notes up to 346 Hz and 6.4 Hz above, so a note can be up to
half a step off: about 12 cents at 110 Hz, 13 at 440 Hz, and 6 at 880 Hz.
* *Plain ST and X68000: no panning.* Each has one output for digital sound: * *Plain ST and X68000: no panning.* Each has one output for digital sound:
the ST its channel A, the X68000 its one ADPCM channel, so `jlAudioSetPan` the ST its channel A, the X68000 its one ADPCM channel, so `jlAudioSetPan`
is ignored. is ignored. The STe build plays stereo through the DMA chip: the module's
channels on Paula's sides, as on the Amiga, and each sound effect where
`jlAudioSetPan` puts it.
* *DOS: stereo on an SB16 only.* An SB16 plays the module's channels on
Paula's sides and each sound effect where `jlAudioSetPan` puts it; the
Sound Blasters before it play mono (an SB Pro's stereo needs the DSP's
high-speed mode, which takes no commands while it runs). Mono cards also
play 22050 Hz as 22222 Hz, the rate their time constant gives, which
JoeyLib mixes for, so pitch is right on both.
* *DOS: the mix needs a mixer.* The OPL2's loudest voice is about 5 dB under
the quarter of the Sound Blaster's digital full scale where JoeyLib mixes a
sound effect or a module channel (each keeps 6 of its 8 bits, so the four
channels fit), so on an SB Pro or SB16 JoeyLib sets the
card's mixer, FM at full and digital sound about 4 dB under it, and
`jlShutdown` puts both back. The cards before the SB Pro have no mixer, and
there sound effects and modules play about 5 dB over the voices.
* *DOS: the noise is a drum.* The OPL2 has no free-running noise generator; its * *DOS: the noise is a drum.* The OPL2 has no free-running noise generator; its
rhythm-mode snare is the nearest, a tuned drum whose pitch cannot follow the rhythm-mode snare is the nearest, a tuned drum whose pitch cannot follow the
YM2149's noise rate. YM2149's noise rate.
* *X68000: digital sound at 7812 Hz.* Modules and effects are mixed at 7812 Hz
and encoded to the MSM6258's 4-bit ADPCM, so nothing above 3.9 kHz is
heard. The chip can play 15625 Hz, but the mix and the encode run on the
10 MHz 68000 for every sample: twice the rate would take twice the
~13% of the machine they take now.
* *DOS and X68000: tones are not square.* The OPL2 and the YM2151 make their * *DOS and X68000: tones are not square.* The OPL2 and the YM2151 make their
waves from sines and have no square wave, so their tone voices (DOS's when it waves from sines and have no square wave, so their tone voices (DOS's when it
has an OPL2) are not the square waves the other ports play. has an OPL2) are not the square waves the other ports play.
* *Amiga and IIgs: the brightest noise.* Paula plays a sample no faster than * *Amiga and IIgs: the brightest noise.* Paula plays a sample no faster than
28.6 kHz and the IIgs's DOC no faster than 26 kHz, so noise pitches 0 to 4 28.6 kHz and the IIgs's DOC no faster than 26 kHz, so noise pitches 0 to 4
sound alike there. sound alike there.
* *Amiga and IIgs: fast effects lose their top.* For the same reason, an effect
or stream faster than 28.6 kHz plays every second sample on the Amiga (every
third above 57.2 kHz), and one faster than the DOC's 26 kHz output steps over
samples on the IIgs, both at its own pitch and length: 44.1 kHz plays as
about 22 kHz, so what it holds above 11 kHz is gone, where the other ports
keep it up to their own output's limit.
* *Where tick callbacks fall.* The calls average exactly `hz` a second on every * *Where tick callbacks fall.* The calls average exactly `hz` a second on every
port. The ST and X68000 deliver them from the system's own timer, at 200 and port. The ST and X68000 deliver them from the system's own timer, at 200 and
100 Hz, so the calls due between two of its interrupts come together. DVX 100 Hz, so the calls due between two of its interrupts come together. DVX
@ -528,8 +558,15 @@ names the ports, says how they differ, and gives the reason.
the SFX overlay run too long for an interrupt, so their sound buffers are the SFX overlay run too long for an interrupt, so their sound buffers are
refilled at `jlAudioFrameTick`: a program that stops calling it for longer refilled at `jlAudioFrameTick`: a program that stops calling it for longer
than the buffer lasts stops or repeats its sound. than the buffer lasts stops or repeats its sound.
* *Win16: a module's end is reported early.* `jlAudioIsPlayingMod` turns false
up to about 0.15 s before the module's last notes are heard. Windows 3.1's
Sound Blaster driver counts a buffer as played as soon as it takes it into
its own DMA buffer, and reports the same through `waveOutGetPosition`, so
nothing tells what has been heard.
* *Win16 with a Sound Blaster 2.0: 8-bit sound.* Its driver, SNDBLST2.DRV, takes * *Win16 with a Sound Blaster 2.0: 8-bit sound.* Its driver, SNDBLST2.DRV, takes
only 8-bit samples, so the quietest attenuation steps are coarser there. only 8-bit samples, so the quietest attenuation steps are coarser there, and
sound effects and modules, mixed at a quarter of full scale like every
software mix, keep 6 of their 8 bits.
[[portsDifferSerial]] [[portsDifferSerial]]
==== Serial ==== Serial
@ -538,7 +575,11 @@ names the ports, says how they differ, and gives the reason.
IIgs Super Serial Card at 19200 (the 6551's), the X68000 at 38400 (RSDRV.SYS's IIgs Super Serial Card at 19200 (the 6551's), the X68000 at 38400 (RSDRV.SYS's
table; really 39062), and Win16 with Windows' own COMM.DRV at 57600. table; really 39062), and Win16 with Windows' own COMM.DRV at 57600.
* *One port.* The base ST has one RS-232 port, and so has the X68000, whose * *One port.* The base ST has one RS-232 port, and so has the X68000, whose
second serial channel is the mouse's. second serial channel is the mouse's. A Mega STe, TT, or Falcon has more,
which `JL_SERIAL_SLOT` opens through TOS's Bconmap; their DTR and RTS
stay as TOS sets them.
* *Haiku and BeOS: 7 or 8 data bits.* Their termios cannot ask for 5 or 6,
so an open that does fails.
* *IIgs modem and printer ports: no RTS.* Their mini-DIN-8 connector's one * *IIgs modem and printer ports: no RTS.* Their mini-DIN-8 connector's one
handshake output is DTR, so under RTS/CTS nothing can tell the peer to handshake output is DTR, so under RTS/CTS nothing can tell the peer to
pause: receive needs XON/XOFF or a protocol that paces itself. The printer pause: receive needs XON/XOFF or a protocol that paces itself. The printer
@ -559,8 +600,13 @@ names the ports, says how they differ, and gives the reason.
* *Name rules.* ProDOS names on the IIgs are at most 15 characters and start * *Name rules.* ProDOS names on the IIgs are at most 15 characters and start
with a letter; DOS, the ST, and Win16 use 8.3 names; and Linux, Haiku, BeOS, with a letter; DOS, the ST, and Win16 use 8.3 names; and Linux, Haiku, BeOS,
and the web tell upper and lower case apart. Keep to the portable rule and the web tell upper and lower case apart. The save calls refuse a name
(<<_files>>). outside the portable rule on every port (<<_files>>); a `DATA/` name is
passed through, so keep those to it too.
* *ST: the current directory.* Every other port finds `DATA/`, `SAVES/`, and
the log in the program's own folder however it was started; TOS tells a
program nothing of where it was started from, so the ST uses the current
directory, which is the program's folder when the desktop starts it.
* *Pre-compiled sprites per processor.* A `.spc` holds machine code, so it is * *Pre-compiled sprites per processor.* A `.spc` holds machine code, so it is
baked per port: DVX and Win16 run DOS's x86 code, the STe the ST's, and the baked per port: DVX and Win16 run DOS's x86 code, the STe the ST's, and the
ports that interpret sprites use only its pixels. ports that interpret sprites use only its pixels.
@ -583,6 +629,11 @@ names the ports, says how they differ, and gives the reason.
is one Memory Manager block inside one bank, 32 KB to 48 KB, and a `jlAlloc` is one Memory Manager block inside one bank, 32 KB to 48 KB, and a `jlAlloc`
block is a Memory Manager handle, so `jlFree` and `free` must never be given block is a Memory Manager handle, so `jlFree` and `free` must never be given
each other's blocks. each other's blocks.
* *ST: a `<math.h>` of five functions.* The ST's C library has no maths
library, so JoeyLib brings `ceil`, `floor`, `log`, `pow`, and `trunc` (and
their `float` forms), good to a few hundred parts per million, and nothing
else: a program that calls `sin` or `sqrt` does not link there. Every
other port has its C library's whole `<math.h>`.
[[portsDifferTiming]] [[portsDifferTiming]]
==== Timing ==== Timing
@ -646,9 +697,11 @@ Nothing in the API differs between them:
|Digital sound (`.MOD`, SFX, streams) |Digital sound (`.MOD`, SFX, streams)
|PWM through the YM2149's channel A volume, driven by a 12288 Hz timer |PWM through the YM2149's channel A volume, driven by a 12288 Hz timer
interrupt; tone voice 0 is silent while it plays interrupt, each sample the nearest of the YM's sixteen logarithmic levels
|The 8-bit DMA sound chip, mono at 12517 Hz, with no interrupt; all three tone (so about three bits near full scale); tone voice 0 is silent while it
voices keep playing plays, and for a fifth of a second after
|The 8-bit DMA sound chip, stereo at 12517 Hz, with no interrupt; all three
tone voices keep playing
|Fills and copies |Fills and copies
|The CPU |The CPU
@ -687,10 +740,14 @@ MS-DOS port's -- the same chunky stage and the same x86 code, so UBER's
hashes are DOS's -- and the rest behaves like this: hashes are DOS's -- and the rest behaves like this:
* *A window, or the whole screen.* The window's content is the 320x200 stage * *A window, or the whole screen.* The window's content is the 320x200 stage
at the largest whole scale, up to 3x, that fits the desktop: 1x at at the largest whole scale, up to 3x, that fits the desktop, in 4:3 as a
640x480, 2x at 800x600, 3x at 1024x768. The stage reaches the desktop CRT shows it and as the SDL3 window does (each stage row a fifth taller:
through one colour table per palette, so SCB palettes are exact on 15-, 1x is 320x240), or with Game > Square Pixels, square pixels (16:10, as
16-, and 32-bit desktops. On a 256-colour desktop every colour is the `JOEYLIB_ASPECT=square` gives the SDL3 window). In 4:3 that is 1x at
640x480 and 2x at 800x600 and 1024x768; with square pixels 1x, 2x, and
3x. The choice is kept with the full-screen one. The stage reaches the
desktop through one colour table per palette, so SCB palettes are exact on
15-, 16-, and 32-bit desktops. On a 256-colour desktop every colour is the
nearest one in DVX's palette, which all programs share: gradients band. nearest one in DVX's palette, which all programs share: gradients band.
* *Full screen.* Game > Full Screen, or Alt+Enter, puts the program on the * *Full screen.* Game > Full Screen, or Alt+Enter, puts the program on the
whole screen in DVX's full-screen mode, 320x200 in 256 colours (VGA mode whole screen in DVX's full-screen mode, 320x200 in 256 colours (VGA mode
@ -716,7 +773,8 @@ hashes are DOS's -- and the rest behaves like this:
screen, every key goes to the program, F1, F10, and Alt included, except screen, every key goes to the program, F1, F10, and Alt included, except
Alt+Tab (switch programs), Ctrl+Esc (Task Manager) and Alt+F4 (close the Alt+Tab (switch programs), Ctrl+Esc (Task Manager) and Alt+F4 (close the
window), and Alt+Enter, which the port keeps for full screen. The window's window), and Alt+Enter, which the port keeps for full screen. The window's
Game menu has Full Screen, Help (the program's help file) and Exit. Typed Game menu has Full Screen, Square Pixels, Help (the program's help file),
and Exit. Typed
characters come through DVX's keyboard layout. characters come through DVX's keyboard layout.
* *Closing ends the program.* Alt+F4, the close box, and Game > Exit end the * *Closing ends the program.* Alt+F4, the close box, and Game > Exit end the
program at its next `jlWaitVBL`, `jlInputPoll`, `jlStagePresent`, or program at its next `jlWaitVBL`, `jlInputPoll`, `jlStagePresent`, or
@ -733,10 +791,13 @@ hashes are DOS's -- and the rest behaves like this:
folder, `CONFIG\APPS\JOEYLIB\NAME`, which DVX keeps apart from the program folder, `CONFIG\APPS\JOEYLIB\NAME`, which DVX keeps apart from the program
files so a reinstall never overwrites them. files so a reinstall never overwrites them.
* *Sound* goes through DVX's sound and music libraries, mixed with every * *Sound* goes through DVX's sound and music libraries, mixed with every
other program's: `.MOD` music, SFX in stereo, and the tone voices on the FM other program's: `.MOD` music, SFX in stereo, and the tone voices and the
chip when there is one. A `jlAudioTickRegister` callback runs in program noise as the hosted ports make them, square waves and the YM2149's noise
time, at the next `jlAudioFrameTick`, because DVX's sound library may not mixed in software into a stream of DVX's mixer, so their levels and
be called from an interrupt. balance are the same on every card (a machine whose only sound is the PC
speaker plays DVX's own tones instead). A `jlAudioTickRegister` callback
runs in program time from that mixing, between exactly the samples where
its tick falls, as on the hosted ports.
* *Joysticks* are DVX's, calibrated in its Control Panel: real analog axes. * *Joysticks* are DVX's, calibrated in its Control Panel: real analog axes.
Two sticks on one game port (a Y cable, or a four-axis stick in DVX's Two sticks on one game port (a Y cable, or a four-axis stick in DVX's
default two-stick mode) are joysticks 0 and 1. An axis that was never default two-stick mode) are joysticks 0 and 1. An axis that was never
@ -808,7 +869,11 @@ this:
exactly the samples it falls between, so its notes start in the right exactly the samples it falls between, so its notes start in the right
place in the sound. Every sound needs audio started (the default), the place in the sound. Every sound needs audio started (the default), the
tone voices included. tone voices included.
* *No serial port.* * *Serial* through termios on Linux: `/dev/ttyS0`, or the device the
`JOEYLIB_SERIAL` environment variable names (a USB adapter:
`JOEYLIB_SERIAL=/dev/ttyUSB0`), and `/dev/ttyS<unit - 1>` for a slot. The
user running the program needs the device's permissions (on most
distributions, the `dialout` group).
`make sdl3` builds every example as a native program, each in `make sdl3` builds every example as a native program, each in
`build/sdl3/bin/` (a program that ships in a folder, in a folder of its own `build/sdl3/bin/` (a program that ships in a folder, in a folder of its own
@ -835,12 +900,18 @@ are DOS's here too. Everything in <<_sdl3_programs>> holds, except:
`jlInputPoll` alone, as `jlWaitForAnyKey` is). `jlInputPoll` alone, as `jlWaitForAnyKey` is).
* *Sound starts with the first click or key press*, as browsers require; the * *Sound starts with the first click or key press*, as browsers require; the
page says so. page says so.
* *Saves persist.* `SAVES/` is kept in the browser's IndexedDB, read back * *Saves persist, page by page.* `SAVES/` is kept in the browser's
when the program starts and written back as each save changes, so a save IndexedDB, read back the first time the program asks for it and written
outlives the page however it ends. The store is the site's, not the page's: back as each save changes, so a save outlives the page however it ends.
every JoeyLib program served from the same site shares one `SAVES/`, so give Each page keeps its own, as each program keeps its own `SAVES/` folder on
each game's saves names of their own. `jlDiskFree` there is the browser's a machine: `games/hero.html` and `games/maze.html` on one site never see
own estimate of what the site may still store. each other's saves, and moving or renaming a page leaves its saves behind
under the old address. `jlDiskFree` there is the browser's own estimate of
what the whole site may still store.
* *No serial port.* A page reaches one only through Web Serial, which only
Chromium-based browsers have and which opens a port only when the person
picks it from the browser's own chooser, opened by a click: nothing a
program calls can do that for it.
* *No full screen by default and no closing.* Alt+Enter asks the browser for * *No full screen by default and no closing.* Alt+Enter asks the browser for
the whole screen; the program ends when the page is closed. the whole screen; the program ends when the page is closed.
@ -862,6 +933,8 @@ is the same chunky one, so UBER's hashes are DOS's. Everything in
and `joeylog.txt` are there. It runs on a PSP with custom firmware (it is and `joeylog.txt` are there. It runs on a PSP with custom firmware (it is
not signed) and in PPSSPP. The ELF it is packed from stays in not signed) and in PPSSPP. The ELF it is packed from stays in
`build/psp/elf/`, for `psp-addr2line`. `build/psp/elf/`, for `psp-addr2line`.
* *No serial port.* The PSP's one UART, on the remote connector, is
reached only from kernel mode, which a homebrew program does not run in.
* *The screen.* The PSP's 480x272 screen shows the stage at 1:1 in its * *The screen.* The PSP's 480x272 screen shows the stage at 1:1 in its
middle, black around it. The scale and aspect settings do not apply (a PSP middle, black around it. The scale and aspect settings do not apply (a PSP
has no environment), and there is no full screen to switch to. has no environment), and there is no full screen to switch to.
@ -995,8 +1068,10 @@ The 2x window has nearly five times the IIgs's pixels to fill (640x480 for
its 320x200): a 386 keeps up with that for sprites, but not for a whole new its 320x200): a 386 keeps up with that for sprites, but not for a whole new
screen every frame, so a game that redraws everything every frame wants screen every frame, so a game that redraws everything every frame wants
`JOEYLIB_SCALE=1` there. `JOEYLIB_SCALE=1` there.
* *Sound* is 16-bit mono at 22.05 kHz through waveOut, what a Sound Blaster * *Sound* is 16-bit mono at 22.222 kHz through waveOut, what a Sound Blaster
and a 386 handle well, or 8-bit on a card whose driver has no 16-bit (a and a 386 handle well (a Sound Blaster plays 22.05 kHz as 22.222, 0.8%
sharp, so JoeyLib asks for the rate it plays exactly; a card that refuses
it gets 22.05 kHz), or 8-bit on a card whose driver has no 16-bit (a
Sound Blaster 2.0, whose SNDBLST2.DRV is 8-bit only, which makes the quiet Sound Blaster 2.0, whose SNDBLST2.DRV is 8-bit only, which makes the quiet
attenuation steps coarser); stereo pan is ignored. attenuation steps coarser); stereo pan is ignored.
* *Joysticks* are the first two MMSYSTEM sticks, through `joyGetPos`, with * *Joysticks* are the first two MMSYSTEM sticks, through `joyGetPos`, with
@ -1078,7 +1153,11 @@ Their stage is the same chunky one, so UBER's hashes are DOS's. Everything in
across from its resources (as `mimeset -F` does), so Tracker shows it by across from its resources (as `mimeset -F` does), so Tracker shows it by
its icon once it has run; that is the one time a program writes to its own its icon once it has run; that is the one time a program writes to its own
file. file.
* *No serial port.* * *Serial* through termios: the first entry of `/dev/ports` (Haiku's
`pc_serial0`, BeOS's `serial1`), or the device the `JOEYLIB_SERIAL`
environment variable names, and the unit'th entry in name order for a
slot; 7 or 8 data bits only. The gates run it on Haiku; BeOS R5 shares the
code but not the test, since the gates' BeOS VM has no serial ports.
* *BeOS R5's loader* refuses any program with a dynamic tag it does not * *BeOS R5's loader* refuses any program with a dynamic tag it does not
know, so `make beos` links without combined relocations (`-z know, so `make beos` links without combined relocations (`-z
nocombreloc`). Build for R5 with `make beos`, not `make haiku32`. nocombreloc`). Build for R5 with `make beos`, not `make haiku32`.

View file

@ -26,19 +26,20 @@ native sprite blitter code into. Getting it wrong is the most common cause of
sprites drawing slowly, so it is worth understanding. sprites drawing slowly, so it is worth understanding.
* `0` selects a per-platform default: 8 KB on the IIgs, 64 KB on the * `0` selects a per-platform default: 8 KB on the IIgs, 64 KB on the
Amiga and ST, and 32 KB on the X68000, DOS, DVX, and Win16. The other Amiga, ST, and X68000, and 32 KB on DOS, DVX, and Win16. The other hosted
hosted ports interpret every sprite and never use it, though they still ports interpret every sprite and allocate no arena at all.
allocate it.
* The 68000 and x86 emitters produce code roughly four to eight times less * The 68000 and x86 emitters produce code roughly four to eight times less
dense than the 65816 one, so the same sprite needs far more arena space on dense than the 65816 one, so the same sprite needs far more arena space on
those ports. A 32x32 sprite can need 8 to 16 KB there. those ports. A 32x32 sprite can need 8 to 16 KB there.
* Measure, do not guess: after compiling a sprite, `jlSpriteCompiledSize()` * Measure, do not guess: after compiling a sprite, `jlSpriteCompiledSize()`
reports its exact cost on the current platform. reports its exact cost on the current platform.
* On the IIgs a compiled sprite cannot cross a 64 KB bank boundary, so the * Every port takes as much of the request as the machine has, and `jlInit`
arena is up to four Memory Manager blocks of at most 64 KB, each inside one fails only if it can have none of it; `jlSpriteCodegenBytesTotal()` says
bank: 256 KB at most. A bank that is refused is tried again at 60 KB, and what it got. On the IIgs a compiled sprite cannot cross a 64 KB bank
`jlInit` takes whatever part of the request it was granted. Elsewhere the boundary, so the arena is up to four Memory Manager blocks of at most
arena is one block, and `jlInit` fails if it cannot be had. 64 KB, each inside one bank: 256 KB at most (a bank that is refused is
tried again at 60 KB). Elsewhere it is one block, halved until the heap
has room, down to 4 KB.
If the arena is too small, sprites still draw: they fall back to an If the arena is too small, sprites still draw: they fall back to an
interpreter that is *two to six times slower*, depending on the machine. interpreter that is *two to six times slower*, depending on the machine.
@ -357,8 +358,8 @@ jlSpriteSaveAndDraw(stage, sprite, x, y, &backup);
Size the buffer with `jlSpriteBackupLength`, and declare it as `uint16_t` (or Size the buffer with `jlSpriteBackupLength`, and declare it as `uint16_t` (or
otherwise 2-byte aligned): on the Amiga, ST, and X68000 an odd address sends otherwise 2-byte aligned): on the Amiga, ST, and X68000 an odd address sends
save and restore down a much slower path, and on the Amiga and ST it crashes save and restore down a much slower path, and it crashes the trusted
the trusted variants. variants.
==== The Clean-Buffer Pattern ==== The Clean-Buffer Pattern

View file

@ -8,8 +8,9 @@
// port played and tools/gradeAudioConformance.py can measure it against the // port played and tools/gradeAudioConformance.py can measure it against the
// API's contract: tone and voice pitch, 2 dB attenuation steps, every voice, // API's contract: tone and voice pitch, 2 dB attenuation steps, every voice,
// the noise generator's rate and steps, sound effects at their rates and // the noise generator's rate and steps, sound effects at their rates and
// pans, a module that does not loop ending (and jlAudioIsPlayingMod saying // pans, streamed ones (signed, and in the port's raw format), a module that
// so), and tick callbacks at their rates. // does not loop ending (and jlAudioIsPlayingMod saying so), one that loops
// stopping when it is told, and tick callbacks at their rates.
// //
// The log, one line per event, "AUDPAR <ms> <event> <values>", ms counted // The log, one line per event, "AUDPAR <ms> <event> <values>", ms counted
// from the first sync tone's start: // from the first sync tone's start:
@ -18,9 +19,17 @@
// tone <hz> jlAudioTone // tone <hz> jlAudioTone
// voice <v> <hz> <atten> jlAudioVoice // voice <v> <hz> <atten> jlAudioVoice
// noise <pitch> <atten> jlAudioNoise // noise <pitch> <atten> jlAudioNoise
// sfx <slot> <rate> <hz> <pan> a one-shot sine sounding <hz> at <rate> // sfx <slot> <rate> <hz> <pan> <samples>
// a one-shot sine sounding <hz> at <rate>,
// <samples> long (44100 Hz: above the IIgs's
// and the Amiga's chips, which decimate)
// stream <slot> <rate> <hz> <pan> <raw> <samples>
// a sine streamed (raw 1: through
// jlAudioPlaySfxStreamRaw)
// mod start / mod end jlAudioPlayMod with loop false, and the // mod start / mod end jlAudioPlayMod with loop false, and the
// first jlAudioIsPlayingMod false after it // first jlAudioIsPlayingMod false after it
// mod loop / mod stop jlAudioPlayMod with loop true, and the
// jlAudioStopMod after it
// tick <hz> <ok> <calls> <ms> jlAudioTickRegister's result, the calls // tick <hz> <ok> <calls> <ms> jlAudioTickRegister's result, the calls
// that came, and the time they had // that came, and the time they had
// silence everything stopped // silence everything stopped
@ -31,6 +40,7 @@
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include <joey/joey.h> #include <joey/joey.h>
#include <joey/file.h> #include <joey/file.h>
@ -57,10 +67,17 @@
#define VOICE_HZ 660u #define VOICE_HZ 660u
#define TICK_MS 2000u #define TICK_MS 2000u
#define MOD_WAIT_MS 12000u #define MOD_WAIT_MS 12000u
#define MOD_STOP_MS 1500u // a looping module plays this long, then stops
#define SFX_SLOT 0u #define SFX_SLOT 0u
#define SFX_SAMPLES 4000u #define SFX_SAMPLES 4000u
#define SFX_PERIOD 8u // samples a cycle: the tone is rate / 8 #define SFX_PERIOD 8u // samples a cycle: the tone is rate / 8
// Faster than the IIgs's DOC and the Amiga's Paula play, so they decimate:
// the tone rate / 16, as the 22050 Hz step's, as long as that step.
#define SFX_FAST_RATE 44100u
#define SFX_FAST_PERIOD 16u
#define SFX_FAST_SAMPLES 8000u
#define SFX_AMPLITUDE 100 #define SFX_AMPLITUDE 100
#define RAW_BIAS 0x80 // JL_SFX_RAW_UNSIGNED8_NOZERO's zero
#define MS_PER_SECOND 1000u #define MS_PER_SECOND 1000u
#define EVENTS_MAX 128u #define EVENTS_MAX 128u
@ -70,7 +87,12 @@ typedef enum {
STEP_VOICE, STEP_VOICE,
STEP_NOISE, STEP_NOISE,
STEP_SFX, STEP_SFX,
STEP_SFX_FAST,
STEP_STREAM,
STEP_STREAM_RAW,
STEP_STREAM_FAST,
STEP_MOD, STEP_MOD,
STEP_MOD_STOP,
STEP_TICK, STEP_TICK,
STEP_SILENCE STEP_SILENCE
} StepKindE; } StepKindE;
@ -83,6 +105,19 @@ typedef struct {
uint16_t ms; // how long it sounds (sfx: from its length) uint16_t ms; // how long it sounds (sfx: from its length)
} StepT; } StepT;
// A sine a sound step plays: count samples, period of them a cycle.
typedef struct {
const int8_t *samples;
uint32_t count;
uint16_t period;
} SineT;
// A stream step's sine and its progress through it.
typedef struct {
const SineT *sine;
uint32_t pos;
} StreamT;
// What the log will say of one step, kept in memory until the timeline is // What the log will say of one step, kept in memory until the timeline is
// over: a log line can take a disk write (a fifth of a second on the ST), // over: a log line can take a disk write (a fifth of a second on the ST),
// and written while it plays it would stall the program's sound. // and written while it plays it would stall the program's sound.
@ -95,7 +130,7 @@ typedef struct {
uint32_t count; // mod: its length, then playing; tick: calls uint32_t count; // mod: its length, then playing; tick: calls
uint32_t span; // tick: the time the calls had uint32_t span; // tick: the time the calls had
bool ok; // tick: registered; mod end: still playing bool ok; // tick: registered; mod end: still playing
bool modEnd; bool modEnd; // mod: its end; mod stop: the stop
} EventT; } EventT;
static EventT *eventAdd(uint32_t ms, const StepT *step); static EventT *eventAdd(uint32_t ms, const StepT *step);
@ -105,6 +140,8 @@ static bool pump(void);
static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength); static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength);
static uint32_t since(void); static uint32_t since(void);
static void silenceAll(void); static void silenceAll(void);
static const SineT *stepSine(StepKindE kind);
static uint32_t streamFill(void *ctx, int8_t *dst, uint32_t count);
static void tickCount(void); static void tickCount(void);
static bool waitMs(uint16_t ms); static bool waitMs(uint16_t ms);
@ -147,10 +184,17 @@ static const StepT kTimeline[] = {
{ STEP_SFX, JL_AUDIO_PAN_CENTER, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_SFX, JL_AUDIO_PAN_CENTER, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_SFX, JL_AUDIO_PAN_CENTER, 16000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_SFX, JL_AUDIO_PAN_CENTER, 16000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_SFX, JL_AUDIO_PAN_CENTER, 22050u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_SFX, JL_AUDIO_PAN_CENTER, 22050u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_SFX_FAST, JL_AUDIO_PAN_CENTER, SFX_FAST_RATE, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_SFX, JL_AUDIO_PAN_LEFT, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_SFX, JL_AUDIO_PAN_LEFT, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_SFX, JL_AUDIO_PAN_RIGHT, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }, { STEP_SFX, JL_AUDIO_PAN_RIGHT, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
// The same sound streamed, streamed raw, and streamed fast.
{ STEP_STREAM, JL_AUDIO_PAN_CENTER, 11025u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_STREAM_RAW, JL_AUDIO_PAN_CENTER, 12000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS },
{ STEP_STREAM_FAST, JL_AUDIO_PAN_CENTER, SFX_FAST_RATE, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
// A module that does not loop, to its end. // A module that does not loop, to its end.
{ STEP_MOD, 0u, 0u, 0u, MOD_WAIT_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }, { STEP_MOD, 0u, 0u, 0u, MOD_WAIT_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
// A module that loops, stopped while it plays.
{ STEP_MOD_STOP, 0u, 0u, 0u, MOD_STOP_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS },
// Tick callbacks, slow and at a frame's rate. // Tick callbacks, slow and at a frame's rate.
{ STEP_TICK, 0u, 10u, 0u, TICK_MS }, { STEP_TICK, 0u, 10u, 0u, TICK_MS },
{ STEP_TICK, 0u, 60u, 0u, TICK_MS }, { STEP_TICK, 0u, 60u, 0u, TICK_MS },
@ -165,6 +209,14 @@ static const int8_t kSineCycle[SFX_PERIOD] = { 0, 71, SFX_AMPLITUDE, 71, 0, -71,
static EventT gEvents[EVENTS_MAX]; static EventT gEvents[EVENTS_MAX];
static uint16_t gEventCount = 0u; static uint16_t gEventCount = 0u;
static int8_t gSine[SFX_SAMPLES]; static int8_t gSine[SFX_SAMPLES];
static int8_t gSineRaw[SFX_SAMPLES]; // gSine in the port's raw stream format
// The sines the steps play (stepSine); the fast one's samples are allocated
// at the start, as a static buffer that size would crowd the IIgs's bank.
static SineT gSineSlow = { gSine, SFX_SAMPLES, SFX_PERIOD };
static SineT gSineSlowRaw = { gSineRaw, SFX_SAMPLES, SFX_PERIOD };
static SineT gSineFast = { NULL, SFX_FAST_SAMPLES, SFX_FAST_PERIOD };
static StreamT gStream;
static uint8_t gRawFormat = JL_SFX_RAW_SIGNED8;
static uint32_t gStartMs = 0u; static uint32_t gStartMs = 0u;
static volatile uint32_t gTickCalls = 0u; static volatile uint32_t gTickCalls = 0u;
@ -193,6 +245,7 @@ static EventT *eventAdd(uint32_t ms, const StepT *step) {
// The log, once the timeline is over (the format in the header). // The log, once the timeline is over (the format in the header).
static void eventsLog(void) { static void eventsLog(void) {
const EventT *event; const EventT *event;
const SineT *sine;
uint16_t i; uint16_t i;
for (i = 0u; i < gEventCount; i++) { for (i = 0u; i < gEventCount; i++) {
@ -211,7 +264,15 @@ static void eventsLog(void) {
jlLogF("AUDPAR %lu noise %u %u", (unsigned long)event->ms, (unsigned)event->value, (unsigned)event->level); jlLogF("AUDPAR %lu noise %u %u", (unsigned long)event->ms, (unsigned)event->value, (unsigned)event->level);
break; break;
case STEP_SFX: case STEP_SFX:
jlLogF("AUDPAR %lu sfx %u %u %u %u", (unsigned long)event->ms, (unsigned)SFX_SLOT, (unsigned)event->value, (unsigned)(event->value / SFX_PERIOD), (unsigned)event->channel); case STEP_SFX_FAST:
sine = stepSine(event->kind);
jlLogF("AUDPAR %lu sfx %u %u %u %u %lu", (unsigned long)event->ms, (unsigned)SFX_SLOT, (unsigned)event->value, (unsigned)(event->value / sine->period), (unsigned)event->channel, (unsigned long)sine->count);
break;
case STEP_STREAM:
case STEP_STREAM_RAW:
case STEP_STREAM_FAST:
sine = stepSine(event->kind);
jlLogF("AUDPAR %lu stream %u %u %u %u %u %lu", (unsigned long)event->ms, (unsigned)SFX_SLOT, (unsigned)event->value, (unsigned)(event->value / sine->period), (unsigned)event->channel, (unsigned)(event->kind == STEP_STREAM_RAW), (unsigned long)sine->count);
break; break;
case STEP_MOD: case STEP_MOD:
if (event->modEnd) { if (event->modEnd) {
@ -220,6 +281,9 @@ static void eventsLog(void) {
jlLogF("AUDPAR %lu mod start %lu", (unsigned long)event->ms, (unsigned long)event->count); jlLogF("AUDPAR %lu mod start %lu", (unsigned long)event->ms, (unsigned long)event->count);
} }
break; break;
case STEP_MOD_STOP:
jlLogF("AUDPAR %lu mod %s", (unsigned long)event->ms, event->modEnd ? "stop" : "loop");
break;
case STEP_TICK: case STEP_TICK:
jlLogF("AUDPAR %lu tick %u %u %lu %lu", (unsigned long)event->ms, (unsigned)event->value, (unsigned)event->ok, (unsigned long)event->count, (unsigned long)event->span); jlLogF("AUDPAR %lu tick %u %u %lu %lu", (unsigned long)event->ms, (unsigned)event->value, (unsigned)event->ok, (unsigned long)event->count, (unsigned long)event->span);
break; break;
@ -268,6 +332,7 @@ static bool pump(void) {
// Each step's time is taken as it starts. // Each step's time is taken as it starts.
static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength) { static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength) {
const SineT *sine;
uint32_t start; uint32_t start;
EventT *event; EventT *event;
@ -288,9 +353,24 @@ static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength) {
(void)waitMs(step->ms); (void)waitMs(step->ms);
break; break;
case STEP_SFX: case STEP_SFX:
case STEP_SFX_FAST:
sine = stepSine(step->kind);
jlAudioSetPan(SFX_SLOT, (jlAudioPanE)step->channel); jlAudioSetPan(SFX_SLOT, (jlAudioPanE)step->channel);
jlAudioPlaySfx(SFX_SLOT, (const uint8_t *)gSine, SFX_SAMPLES, step->value); jlAudioPlaySfx(SFX_SLOT, (const uint8_t *)sine->samples, sine->count, step->value);
(void)waitMs((uint16_t)((uint32_t)SFX_SAMPLES * MS_PER_SECOND / step->value)); (void)waitMs((uint16_t)(sine->count * MS_PER_SECOND / step->value));
break;
case STEP_STREAM:
case STEP_STREAM_RAW:
case STEP_STREAM_FAST:
jlAudioSetPan(SFX_SLOT, (jlAudioPanE)step->channel);
gStream.sine = stepSine(step->kind);
gStream.pos = 0u;
if (step->kind == STEP_STREAM_RAW) {
jlAudioPlaySfxStreamRaw(SFX_SLOT, streamFill, &gStream, step->value);
} else {
jlAudioPlaySfxStream(SFX_SLOT, streamFill, &gStream, step->value);
}
(void)waitMs((uint16_t)(gStream.sine->count * MS_PER_SECOND / step->value));
break; break;
case STEP_MOD: case STEP_MOD:
if (mod != NULL) { if (mod != NULL) {
@ -311,6 +391,17 @@ static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength) {
} }
jlAudioStopMod(); jlAudioStopMod();
break; break;
case STEP_MOD_STOP:
if (mod != NULL) {
jlAudioPlayMod(mod, modLength, true);
}
(void)waitMs(step->ms);
event = eventAdd(since(), step);
if (event != NULL) {
event->modEnd = true;
}
jlAudioStopMod();
break;
case STEP_TICK: case STEP_TICK:
gTickCalls = 0u; gTickCalls = 0u;
if (event != NULL) { if (event != NULL) {
@ -347,6 +438,32 @@ static void silenceAll(void) {
} }
// The sine a sound step plays: the fast steps' (SFX_FAST_RATE), the raw
// stream's in the port's raw format, the rest the one-shots' signed bytes.
static const SineT *stepSine(StepKindE kind) {
if (kind == STEP_SFX_FAST || kind == STEP_STREAM_FAST) {
return &gSineFast;
}
return kind == STEP_STREAM_RAW ? &gSineSlowRaw : &gSineSlow;
}
// A stream step's fill: its sine, as stepSine chose it; 0 once it has all
// gone, which ends the stream. A copy, so the time the fill takes is the
// port's, not this program's.
static uint32_t streamFill(void *ctx, int8_t *dst, uint32_t count) {
StreamT *stream;
stream = (StreamT *)ctx;
if (count > stream->sine->count - stream->pos) {
count = stream->sine->count - stream->pos;
}
memcpy(dst, stream->sine->samples + stream->pos, count);
stream->pos += count;
return count;
}
static void tickCount(void) { static void tickCount(void) {
gTickCalls++; gTickCalls++;
} }
@ -368,6 +485,7 @@ static bool waitMs(uint16_t ms) {
int main(void) { int main(void) {
jlConfigT config; jlConfigT config;
uint8_t *mod; uint8_t *mod;
int8_t *fast;
uint32_t modLength; uint32_t modLength;
uint16_t i; uint16_t i;
@ -377,9 +495,22 @@ int main(void) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError()); fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return 1; return 1;
} }
gRawFormat = jlAudioSfxRawFormat();
for (i = 0u; i < SFX_SAMPLES; i++) { for (i = 0u; i < SFX_SAMPLES; i++) {
gSine[i] = kSineCycle[i % SFX_PERIOD]; gSine[i] = kSineCycle[i % SFX_PERIOD];
gSineRaw[i] = gRawFormat == JL_SFX_RAW_UNSIGNED8_NOZERO ? (int8_t)(uint8_t)(gSine[i] + RAW_BIAS) : gSine[i];
} }
// The fast sine: each cycle step of the slow one held for two samples.
fast = (int8_t *)malloc(SFX_FAST_SAMPLES);
if (fast == NULL) {
fprintf(stderr, "AUDPAR: no memory for the fast sine\n");
jlShutdown();
return 1;
}
for (i = 0u; i < SFX_FAST_SAMPLES; i++) {
fast[i] = kSineCycle[(i / (SFX_FAST_PERIOD / SFX_PERIOD)) % SFX_PERIOD];
}
gSineFast.samples = fast;
loadMod(&mod, &modLength); loadMod(&mod, &modLength);
jlSurfaceClear(jlStageGet(), 0u); jlSurfaceClear(jlStageGet(), 0u);
jlStagePresent(); jlStagePresent();
@ -396,6 +527,7 @@ int main(void) {
jlLogF("AUDPAR %lu done", (unsigned long)since()); jlLogF("AUDPAR %lu done", (unsigned long)since());
jlLogFlush(); jlLogFlush();
free(mod); free(mod);
free(fast);
jlShutdown(); jlShutdown();
return 0; return 0;
} }

View file

@ -4,8 +4,9 @@
// //
// Minimal headless serial self-test -- NO jlInit (no video/graphics), so it // Minimal headless serial self-test -- NO jlInit (no video/graphics), so it
// runs even where the graphics HAL can't (e.g. AROS on the Amiga). Opens the // runs even where the graphics HAL can't (e.g. AROS on the Amiga). Opens the
// DEFAULT port at 9600 8N1, sends a banner (TX heartbeat), then echoes every // DEFAULT port (or SERTEST_DEVICE) at 9600 8N1, sends a banner (TX
// received byte forever. A host serial peer verifies banner (TX) + echo (RX). // heartbeat), then echoes every received byte forever. A host serial peer
// verifies banner (TX) + echo (RX).
// //
// A peer that sees the modem lines (DOSBox's nullmodem reports them in the // A peer that sees the modem lines (DOSBox's nullmodem reports them in the
// byte stream as they change) can also send DTR_CHECK: the program then // byte stream as they change) can also send DTR_CHECK: the program then
@ -18,6 +19,15 @@
#define DTR_CHECK 0x04u // ASCII EOT: run the DTR check (not echoed) #define DTR_CHECK 0x04u // ASCII EOT: run the DTR check (not echoed)
#define DTR_ACK 0x06u // ASCII ACK: the peer has the whole marker line #define DTR_ACK 0x06u // ASCII ACK: the peer has the whole marker line
// The port, overridable at build time (-DSERTEST_DEVICE=JL_SERIAL_SLOT
// -DSERTEST_UNIT=2u) so a gate can open one other than the default.
#ifndef SERTEST_DEVICE
#define SERTEST_DEVICE JL_SERIAL_DEFAULT
#endif
#ifndef SERTEST_UNIT
#define SERTEST_UNIT 0u
#endif
static void dtrCheck(const jlSerialConfigT *cfg); static void dtrCheck(const jlSerialConfigT *cfg);
static void sendMark(const char *text); static void sendMark(const char *text);
@ -28,7 +38,7 @@ static void dtrCheck(const jlSerialConfigT *cfg) {
jlSerialSetDtr(true); jlSerialSetDtr(true);
sendMark("JLDTR-ON\r\n"); sendMark("JLDTR-ON\r\n");
jlSerialClose(); jlSerialClose();
if (!jlSerialOpen(JL_SERIAL_DEFAULT, cfg)) { if (!jlSerialOpen(SERTEST_DEVICE, cfg)) {
return; return;
} }
sendMark("JLDTR-REOPENED\r\n"); sendMark("JLDTR-REOPENED\r\n");
@ -74,8 +84,8 @@ int main(void) {
cfg.stopBits = 1u; cfg.stopBits = 1u;
cfg.parity = JL_SERIAL_PARITY_NONE; cfg.parity = JL_SERIAL_PARITY_NONE;
cfg.flow = JL_SERIAL_FLOW_NONE; cfg.flow = JL_SERIAL_FLOW_NONE;
cfg.unit = 0u; cfg.unit = SERTEST_UNIT;
if (!jlSerialOpen(JL_SERIAL_DEFAULT, &cfg)) { if (!jlSerialOpen(SERTEST_DEVICE, &cfg)) {
return 1; return 1;
} }
jlSerialWrite((const uint8_t *)banner, (uint16_t)(sizeof(banner) - 1u)); jlSerialWrite((const uint8_t *)banner, (uint16_t)(sizeof(banner) - 1u));

View file

@ -12,10 +12,12 @@
// 65816 replayer linked into the binary. // 65816 replayer linked into the binary.
// Amiga -- raw .MOD, played by Frank Wille's PTPlayer. // Amiga -- raw .MOD, played by Frank Wille's PTPlayer.
// DOS -- raw .MOD, mixed by JoeyLib's module player (PTPlayer's // DOS -- raw .MOD, mixed by JoeyLib's module player (PTPlayer's
// replay in C) into SB DMA (22 kHz mono 8-bit). // replay in C) into SB DMA (22 kHz 8-bit, stereo on an
// SB16).
// Atari ST -- raw .MOD, mixed by the module player at 12288 Hz and // Atari ST -- raw .MOD, mixed by the module player at 12288 Hz and
// played through the YM2149's channel A volume (PWM); the // played through the YM2149's channel A volume (PWM); the
// STe build plays it through DMA sound at 12517 Hz. // STe build plays it in stereo through DMA sound at
// 12517 Hz.
// X68000 -- raw .MOD, mixed by the module player at 7812 Hz into the // X68000 -- raw .MOD, mixed by the module player at 7812 Hz into the
// MSM6258 ADPCM channel. // MSM6258 ADPCM channel.
// DVX -- raw .MOD through DVX's music library. // DVX -- raw .MOD through DVX's music library.
@ -25,6 +27,18 @@
// the engine choice. jlInit starts audio (jlConfigT.noAudio leaves it // the engine choice. jlInit starts audio (jlConfigT.noAudio leaves it
// off) and jlShutdown stops it. Sound that does not start is non-fatal: // off) and jlShutdown stops it. Sound that does not start is non-fatal:
// jlInit still succeeds and audio calls become no-ops. // jlInit still succeeds and audio calls become no-ops.
//
// The mix is the same on every port, within 3 dB: a sound effect at full
// scale, and a module channel at full volume playing a full-scale sample,
// are each as loud as a tone voice at attenuation 0 (a square wave sampled
// at full scale peaks where the voice does), and the noise at attenuation 0
// is as loud as a voice. The ports that mix in software put that peak at a
// quarter of full scale, so the three voices and the noise together, or a
// module's four channels, fit; the plain ST puts it at half of its one
// sound chip channel, so a module whose three or four channels peak at once
// clips there. DOS keeps the mix on the Sound Blaster's mixer, which the
// cards before the SB Pro do not have: there effects and modules play about
// 5 dB over the voices.
#ifndef JOEYLIB_AUDIO_H #ifndef JOEYLIB_AUDIO_H
#define JOEYLIB_AUDIO_H #define JOEYLIB_AUDIO_H
@ -59,8 +73,9 @@
// ahead of time. // ahead of time.
void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop); void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
// Stop the current module (if any). The playhead is reset so the next // Stop the current module (if any), silent at once on every port (DOS and
// jlAudioPlayMod starts from the top. // the X68000 take it back out of what they have mixed ahead of the chip).
// The playhead is reset so the next jlAudioPlayMod starts from the top.
void jlAudioStopMod(void); void jlAudioStopMod(void);
// True if a module is currently producing output (false during silence // True if a module is currently producing output (false during silence
@ -71,7 +86,9 @@ bool jlAudioIsPlayingMod(void);
// Trigger a one-shot digital SFX on the given slot (0..JL_AUDIO_SFX_SLOTS-1). // Trigger a one-shot digital SFX on the given slot (0..JL_AUDIO_SFX_SLOTS-1).
// sample points at raw signed 8-bit PCM. rateHz is the playback rate // sample points at raw signed 8-bit PCM. rateHz is the playback rate
// the sample was recorded at; the engine pitches as needed for its // the sample was recorded at; the engine pitches as needed for its
// own output rate. If the slot is currently playing, the new sample // own output rate, any rate at its pitch (the IIgs above 26 kHz and the
// Amiga above 28.6 kHz skip samples, so a fast one loses its top there). If
// the slot is currently playing, the new sample
// replaces it. Most ports read the sample in place while it plays, so it // replaces it. Most ports read the sample in place while it plays, so it
// must stay valid and unchanged until it has finished or the slot is // must stay valid and unchanged until it has finished or the slot is
// stopped; the IIgs keeps a copy per slot keyed by address, length, and // stopped; the IIgs keeps a copy per slot keyed by address, length, and
@ -111,10 +128,11 @@ void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint
// Raw-format streaming: jlAudioPlaySfxStream, except `fill` delivers // Raw-format streaming: jlAudioPlaySfxStream, except `fill` delivers
// bytes already in the port's raw stream format (jlAudioSfxRawFormat), // bytes already in the port's raw stream format (jlAudioSfxRawFormat),
// so the port skips its per-byte conversion of every chunk. On the IIgs // so the port skips its per-byte conversion of every chunk. On the IIgs
// that conversion costs ~7 us a byte, ~50 ms per 7 KB chunk. Every other // that conversion costs ~7 us a byte, ~50 ms per 7 KB chunk. DVX takes the
// port's raw format is signed PCM, so there Raw is the plain call (DVX // IIgs's format too (its sound library's 8-bit PCM is unsigned) and skips
// still flips each byte for its sound library). A producer that cannot // its flip. Every other port's raw format is signed PCM, so there Raw is
// make the port's format uses jlAudioPlaySfxStream. // the plain call. A producer that cannot make the port's format uses
// jlAudioPlaySfxStream.
#define JL_SFX_RAW_SIGNED8 0u // int8_t PCM, the jlAudioPlaySfxStream contract #define JL_SFX_RAW_SIGNED8 0u // int8_t PCM, the jlAudioPlaySfxStream contract
#define JL_SFX_RAW_UNSIGNED8_NOZERO 1u // uint8_t PCM biased 0x80; 0x00 is forbidden (DOC stop byte) #define JL_SFX_RAW_UNSIGNED8_NOZERO 1u // uint8_t PCM biased 0x80; 0x00 is forbidden (DOC stop byte)
uint8_t jlAudioSfxRawFormat(void); uint8_t jlAudioSfxRawFormat(void);
@ -145,7 +163,10 @@ typedef enum {
// have one, and otherwise on the one channel PTPlayer picks. // have one, and otherwise on the one channel PTPlayer picks.
// X68000 ignored. Everything is mixed into ONE MSM6258 ADPCM stream, so // X68000 ignored. Everything is mixed into ONE MSM6258 ADPCM stream, so
// its pan bits would move all audio at once, not one slot. // its pan bits would move all audio at once, not one slot.
// DOS, ST ignored -- their output is mono (the STe build's too). // DOS all three on an SB16, the module's channels on Paula's sides;
// ignored on the Sound Blasters before it, which play mono.
// ST ignored -- its output is mono. The STe build plays all three
// (its DMA chip plays stereo), the module's as on DOS.
// DVX all three, through DVX's stereo mixer. // DVX all three, through DVX's stereo mixer.
// hosted all three (the mix is stereo), except Win16, which is mono. // hosted all three (the mix is stereo), except Win16, which is mono.
// Passing an out-of-range pan is ignored. // Passing an out-of-range pan is ignored.
@ -182,9 +203,9 @@ void jlAudioTone(uint16_t freqHz);
// correct). The Amiga uses a timer.device E-clock request and a // correct). The Amiga uses a timer.device E-clock request and a
// software interrupt; the ST and X68000 ride the system's Timer C // software interrupt; the ST and X68000 ride the system's Timer C
// interrupt (200 and 100 Hz), so calls due between its ticks come // interrupt (200 and 100 Hz), so calls due between its ticks come
// together. The IIgs and DVX call fn in program time from // together. The IIgs calls fn in program time from jlAudioFrameTick,
// jlAudioFrameTick, and the hosted ports from the mixer, between the // and DVX and the hosted ports from the mixer, between the samples where
// samples where each tick falls. // each tick falls.
typedef void (*jlAudioTickFnT)(void); typedef void (*jlAudioTickFnT)(void);
bool jlAudioTickRegister(jlAudioTickFnT fn, uint16_t hz); bool jlAudioTickRegister(jlAudioTickFnT fn, uint16_t hz);
@ -213,7 +234,8 @@ void jlAudioCriticalExit(void);
// IIgs -- Ensoniq DOC oscillators playing a resident square on SFX // IIgs -- Ensoniq DOC oscillators playing a resident square on SFX
// slots 0/1/2. // slots 0/1/2.
// X68000 -- YM2151 channels 0/1/2, one sine operator each. // X68000 -- YM2151 channels 0/1/2, one sine operator each.
// DVX -- square tones of DVX's sound library. // DVX -- square waves mixed in software into a stream of DVX's
// mixer (DVX's own square tones with only the PC speaker).
// hosted -- square waves mixed in software. // hosted -- square waves mixed in software.
// //
// A note sounds until the next call for that voice. The IIgs plays a // A note sounds until the next call for that voice. The IIgs plays a
@ -244,7 +266,9 @@ void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten);
// (channels 0-2 are the tone voices). // (channels 0-2 are the tone voices).
// IIgs -- a resident LFSR noise sample on SFX slot 3. // IIgs -- a resident LFSR noise sample on SFX slot 3.
// X68000 -- the YM2151's noise generator, at the rate exactly. // X68000 -- the YM2151's noise generator, at the rate exactly.
// DVX -- a noise tone of DVX's sound library. // DVX -- the YM2149's 17-bit generator, mixed in software (a
// noise tone of DVX's sound library with only the PC
// speaker).
// hosted -- the YM2149's 17-bit generator, mixed in software. // hosted -- the YM2149's 17-bit generator, mixed in software.
// //
// The same contract as jlAudioVoice: it sounds until the next call. // The same contract as jlAudioVoice: it sounds until the next call.

View file

@ -12,16 +12,17 @@
typedef struct { typedef struct {
// Runtime compiled-sprite cache size. 0 selects a per-platform // Runtime compiled-sprite cache size. 0 selects a per-platform
// default: 8 KB on the IIgs, 64 KB on the Amiga and ST, and 32 KB // default: 8 KB on the IIgs, 64 KB on the Amiga, ST, and X68000, and
// everywhere else (the 68k/x86 emitters are ~4-8x less code-dense // 32 KB on DOS, DVX, and Win16 (the 68k/x86 emitters are ~4-8x less
// than the 65816's, so a 32x32 sprite can need 8-16 KB there). The // code-dense than the 65816's, so a 32x32 sprite can need 8-16 KB
// ports that interpret every sprite still allocate it. Size against // there). The ports that interpret every sprite have no cache and
// jlSpriteCompiledSize() when tuning. Elsewhere the cache is one // allocate none. Size against jlSpriteCompiledSize() when tuning.
// block, and jlInit fails if it cannot be had. On the IIgs it is up // Every port takes as much of the request as the machine has, and
// to four Memory Manager blocks of at most 64 KB, each inside one // jlInit fails only if it can have none (jlSpriteCodegenBytesTotal
// bank (a compiled sprite cannot cross a bank), so at most 256 KB; // says what it got): on the IIgs up to four Memory Manager blocks of
// a bank that is refused is retried at 60 KB, and jlInit takes // at most 64 KB, each inside one bank (a compiled sprite cannot cross
// whatever part of the request it was granted. // a bank), so at most 256 KB; elsewhere one block, halved until the
// heap has room, down to 4 KB.
uint32_t codegenBytes; uint32_t codegenBytes;
// True: leave audio off. Every audio call is then silent, as on a // True: leave audio off. Every audio call is then silent, as on a
// machine without sound, and the program pays nothing for audio: // machine without sound, and the program pays nothing for audio:
@ -34,13 +35,13 @@ typedef struct {
// Initialize the library, audio too (unless config->noAudio). Returns true // Initialize the library, audio too (unless config->noAudio). Returns true
// on success; sound that does not start is not a failure (modules and // on success; sound that does not start is not a failure (modules and
// effects are then silent, and the log says so). On failure, jlLastError() // effects are then silent, and the log says so). On failure, jlLastError()
// returns a human-readable description; where the port's own start-up // returns a human-readable description of why, on every port.
// fails without saying why, that is just "jlpInit failed".
bool jlInit(const jlConfigT *config); bool jlInit(const jlConfigT *config);
// Shut down the library, audio first, releasing the library's own // Shut down the library, audio first, releasing the library's own
// resources. Surfaces and sprites the program created are not freed: // resources. Surfaces and sprites the program created are not freed:
// destroy them first. // destroy them first. A program that returns from main or calls exit
// without it has it run then (jlInit registers it with atexit).
void jlShutdown(void); void jlShutdown(void);
// Returns the most recent error message, or NULL if none. // Returns the most recent error message, or NULL if none.
@ -98,8 +99,9 @@ uint16_t jlFrameCount(void);
// by to convert iters-per-N-frames to ops/sec. // by to convert iters-per-N-frames to ops/sec.
uint16_t jlFrameHz(void); uint16_t jlFrameHz(void);
// Monotonic wall-clock millisecond counter since jlInit. It may // Monotonic wall-clock millisecond counter since jlInit, to the
// advance a frame at a time. Call it at least once every 15 minutes, or // millisecond (a frame at a time only where the machine's timer is
// taken: docs/ApiCore.adoc). Call it at least once every 15 minutes, or
// the time between two calls can be lost. Two consecutive calls // the time between two calls can be lost. Two consecutive calls
// separated by a long render report the true elapsed time, not the // separated by a long render report the true elapsed time, not the
// number of VBLs that fired. // number of VBLs that fired.

View file

@ -10,11 +10,13 @@
// DATA/ -- game content (levels, sprites, audio). jlDataOpen. // DATA/ -- game content (levels, sprites, audio). jlDataOpen.
// SAVES/ -- writable save / preference files. jlSave*. // SAVES/ -- writable save / preference files. jlSave*.
// Both are addressed by a bare name relative to their directory; the prefix is // Both are addressed by a bare name relative to their directory; the prefix is
// forced here so callers never hard-code "DATA/" or "SAVES/". The hosted // forced here so callers never hard-code "DATA/" or "SAVES/". Both are found
// ports make the program's folder the current directory in jlInit; DOS, the // in the program's own folder wherever it was started from, before jlInit as
// ST, the Amiga, and the X68000 use the current directory as they find it; // after: DOS from its start path, the Amiga as PROGDIR:, the X68000 from its
// the IIgs finds DATA/ under GS/OS prefix 0 and SAVES/ in the application's // process block, the IIgs as GS/OS prefix 1, the hosted ports from their
// folder (prefix 1); DVX roots both at run time (docs/ApiFile.adoc). // executable's path. The ST uses the current directory (TOS tells a program
// nothing of its folder); DVX keeps SAVES/ in the program's settings folder
// (docs/ApiFile.adoc).
#ifndef JOEYLIB_FILE_H #ifndef JOEYLIB_FILE_H
#define JOEYLIB_FILE_H #define JOEYLIB_FILE_H
@ -54,9 +56,10 @@ FILE *jlDataOpen(const char *name, const char *mode);
FILE *jlSaveOpen(const char *name, const char *mode); FILE *jlSaveOpen(const char *name, const char *mode);
// Write `len` bytes from `buf` to save file `name`, replacing any existing // Write `len` bytes from `buf` to save file `name`, replacing any existing
// contents. Returns true only if all `len` bytes were written. Not atomic: a // contents. Returns true only if all `len` bytes were written. The bytes go to
// write that fails part way leaves the save short. buf NULL with len > 0 // a temporary file that takes the save's place only once it is whole, so a
// returns false before the save is touched. // write that fails part way leaves the old save as it was. buf NULL with
// len > 0 returns false before the save is touched.
bool jlSaveWrite(const char *name, const void *buf, uint32_t len); bool jlSaveWrite(const char *name, const void *buf, uint32_t len);
// Read up to `max` bytes from save file `name` into `buf`. Returns the number // Read up to `max` bytes from save file `name` into `buf`. Returns the number

View file

@ -199,6 +199,7 @@
#define JL_HAS_STAGE_ALLOC_PIXELS // pinned $01:2000 display memory, function #define JL_HAS_STAGE_ALLOC_PIXELS // pinned $01:2000 display memory, function
#define JL_HAS_STAGE_FREE_PIXELS // no-op (pinned), function #define JL_HAS_STAGE_FREE_PIXELS // no-op (pinned), function
#define JL_HAS_FRAME_HZ // runtime 50/60 from battery RAM, function #define JL_HAS_FRAME_HZ // runtime 50/60 from battery RAM, function
#define JL_HAS_MILLIS_ELAPSED // heartbeat frames plus the beam's line
// (frameCount: port.h #defines jlpFrameCount straight to iigsGetTickWord, // (frameCount: port.h #defines jlpFrameCount straight to iigsGetTickWord,
// the VBL heartbeat tick read -- no function override to register here.) // the VBL heartbeat tick read -- no function override to register here.)
#define JL_HAS_WAIT_VBL // the VBL heartbeat count advancing, function #define JL_HAS_WAIT_VBL // the VBL heartbeat count advancing, function
@ -245,6 +246,9 @@
#define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE #define JL_HAS_SAVE_DELETE
#define JL_HAS_DISK_FREE #define JL_HAS_DISK_FREE
// DATA/, SAVES/ and joeylog.txt in the application's folder, GS/OS prefix
// 1 (fileRoot.h).
#define JL_HAS_FILE_ROOTS
// millisElapsed: generic frameCount*1000/frameHz (no override) // millisElapsed: generic frameCount*1000/frameHz (no override)
#elif defined(JOEYLIB_PLATFORM_AMIGA) #elif defined(JOEYLIB_PLATFORM_AMIGA)
#define JL_HAS_SURFACE_CLEAR // planar clear, function #define JL_HAS_SURFACE_CLEAR // planar clear, function
@ -314,10 +318,17 @@
#define JL_HAS_SERIAL_SET_DTR #define JL_HAS_SERIAL_SET_DTR
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts). // Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
#define JL_HAS_SERIAL_SET_RTS #define JL_HAS_SERIAL_SET_RTS
// save files + disk space (CreateDir/DeleteFile/Info via dos.library) // the millisecond clock: timer.device's E-clock (ReadEClock)
#define JL_HAS_MILLIS_ELAPSED
// save files + disk space (CreateDir/DeleteFile/Rename/Info via
// dos.library: libnix has no rename)
#define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE #define JL_HAS_SAVE_DELETE
#define JL_HAS_SAVE_RENAME
#define JL_HAS_DISK_FREE #define JL_HAS_DISK_FREE
// DATA/, SAVES/ and joeylog.txt in the program's folder, PROGDIR:
// (fileRoot.h).
#define JL_HAS_FILE_ROOTS
// compiled code written: clear the 68020+'s caches (exec CacheClearE) // compiled code written: clear the 68020+'s caches (exec CacheClearE)
#define JL_HAS_CODE_WRITTEN #define JL_HAS_CODE_WRITTEN
// millisElapsed: generic frameCount*1000/frameHz (no override) // millisElapsed: generic frameCount*1000/frameHz (no override)
@ -350,10 +361,10 @@
#define JL_HAS_SURFACE_ALLOC_PIXELS // st: NULL (planar storage), function #define JL_HAS_SURFACE_ALLOC_PIXELS // st: NULL (planar storage), function
#define JL_HAS_SURFACE_ALLOC_PORT_DATA // st StPlanarT + interleaved buffer, function #define JL_HAS_SURFACE_ALLOC_PORT_DATA // st StPlanarT + interleaved buffer, function
#define JL_HAS_SURFACE_FREE_PORT_DATA // st free buffer, function #define JL_HAS_SURFACE_FREE_PORT_DATA // st free buffer, function
#define JL_HAS_FRAME_HZ // the machine's 50 / 60 (71 mono), function #define JL_HAS_FRAME_HZ // the machine's 50 / 60, function
#define JL_HAS_FRAME_COUNT // VBL-ISR counter, function #define JL_HAS_FRAME_COUNT // VBL-ISR counter, function
#define JL_HAS_WAIT_VBL // frame-counter wait, function #define JL_HAS_WAIT_VBL // frame-counter wait, function
#define JL_HAS_MILLIS_ELAPSED // Supexec 200 Hz tick, function #define JL_HAS_MILLIS_ELAPSED // 200 Hz tick + Timer C's live count, function
// audio: every real port implements the full engine // audio: every real port implements the full engine
#define JL_HAS_AUDIO_INIT #define JL_HAS_AUDIO_INIT
#define JL_HAS_AUDIO_SHUTDOWN #define JL_HAS_AUDIO_SHUTDOWN
@ -362,6 +373,9 @@
#define JL_HAS_AUDIO_IS_PLAYING_MOD #define JL_HAS_AUDIO_IS_PLAYING_MOD
#define JL_HAS_AUDIO_PLAY_SFX #define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_PLAY_SFX_STREAM
#if defined(JOEYLIB_ATARI_STE)
#define JL_HAS_AUDIO_SET_PAN // the STe's DMA chip plays stereo
#endif
#define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_NOISE
@ -370,6 +384,7 @@
#define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK #define JL_HAS_AUDIO_FRAME_TICK
#define JL_HAS_MOD_MIX_RUN // the module player's 8-bit mix, src/m68k/modMix.s #define JL_HAS_MOD_MIX_RUN // the module player's 8-bit mix, src/m68k/modMix.s
#define JL_HAS_SFX_MIX_RUN // the SFX overlay's run, src/m68k/sfxMix.s
// lifecycle / present / input (every real port implements) // lifecycle / present / input (every real port implements)
#define JL_HAS_INIT #define JL_HAS_INIT
#define JL_HAS_SHUTDOWN #define JL_HAS_SHUTDOWN
@ -417,6 +432,7 @@
#define JL_HAS_AUDIO_IS_PLAYING_MOD #define JL_HAS_AUDIO_IS_PLAYING_MOD
#define JL_HAS_AUDIO_PLAY_SFX #define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_SET_PAN // the side, on an SB16 (stereo)
#define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_VOICE
#define JL_HAS_AUDIO_NOISE #define JL_HAS_AUDIO_NOISE
@ -444,10 +460,14 @@
#define JL_HAS_SERIAL_SET_DTR #define JL_HAS_SERIAL_SET_DTR
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts). // Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
#define JL_HAS_SERIAL_SET_RTS #define JL_HAS_SERIAL_SET_RTS
// save files + disk space (POSIX mkdir/remove + int21h/36h disk-free) // save files + disk space (POSIX mkdir/remove/statvfs: statvfs takes
// FAT32's free space past 2 GB)
#define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE #define JL_HAS_SAVE_DELETE
#define JL_HAS_DISK_FREE #define JL_HAS_DISK_FREE
// DATA/, SAVES/ and joeylog.txt in the program's folder, from the path
// DOS started it by (fileRoot.h).
#define JL_HAS_FILE_ROOTS
#elif defined(JOEYLIB_PLATFORM_X68000) #elif defined(JOEYLIB_PLATFORM_X68000)
// Platform services in src/x68000/hal.c, the serial add-on in // Platform services in src/x68000/hal.c, the serial add-on in
// src/x68000/serial.c, and planar graphics adapted from the Amiga // src/x68000/serial.c, and planar graphics adapted from the Amiga
@ -497,6 +517,9 @@
#define JL_HAS_DISK_FREE #define JL_HAS_DISK_FREE
#define JL_HAS_SAVE_DELETE #define JL_HAS_SAVE_DELETE
#define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DIR_ENSURE
// DATA/, SAVES/ and joeylog.txt in the program's folder, from its process
// block (fileRoot.h).
#define JL_HAS_FILE_ROOTS
// Serial add-on: RSDRV.SYS transmit and line setup, receive on the // Serial add-on: RSDRV.SYS transmit and line setup, receive on the
// port's own SCC interrupt (src/x68000/x68kSccRxIsr.s). // port's own SCC interrupt (src/x68000/x68kSccRxIsr.s).
#define JL_HAS_SERIAL_OPEN #define JL_HAS_SERIAL_OPEN
@ -521,6 +544,7 @@
#define JL_HAS_AUDIO_CRITICAL_EXIT #define JL_HAS_AUDIO_CRITICAL_EXIT
#define JL_HAS_AUDIO_FRAME_TICK #define JL_HAS_AUDIO_FRAME_TICK
#define JL_HAS_MOD_MIX_RUN // the module player's 8-bit mix, src/m68k/modMix.s #define JL_HAS_MOD_MIX_RUN // the module player's 8-bit mix, src/m68k/modMix.s
#define JL_HAS_SFX_MIX_RUN // the SFX overlay's run, src/m68k/sfxMix.s
#define JL_HAS_CODE_WRITTEN // the X68030's caches, after codegen #define JL_HAS_CODE_WRITTEN // the X68030's caches, after codegen
// SAMPLE side (src/x68000/audioPcm.c): the module player + the shared 5-slot SFX // SAMPLE side (src/x68000/audioPcm.c): the module player + the shared 5-slot SFX
// overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate // overlay, encoded to MSM6258 ADPCM and played via _iocs_adpcmlot. Separate
@ -565,6 +589,7 @@
#define JL_HAS_AUDIO_IS_PLAYING_MOD #define JL_HAS_AUDIO_IS_PLAYING_MOD
#define JL_HAS_AUDIO_PLAY_SFX #define JL_HAS_AUDIO_PLAY_SFX
#define JL_HAS_AUDIO_PLAY_SFX_STREAM #define JL_HAS_AUDIO_PLAY_SFX_STREAM
#define JL_HAS_AUDIO_PLAY_SFX_STREAM_RAW // unsigned bytes as they come (port.h)
#define JL_HAS_AUDIO_SET_PAN #define JL_HAS_AUDIO_SET_PAN
#define JL_HAS_AUDIO_STOP_SFX #define JL_HAS_AUDIO_STOP_SFX
#define JL_HAS_AUDIO_VOICE #define JL_HAS_AUDIO_VOICE
@ -603,6 +628,9 @@
#define JL_HAS_SAVE_DIR_ENSURE #define JL_HAS_SAVE_DIR_ENSURE
#define JL_HAS_SAVE_DELETE #define JL_HAS_SAVE_DELETE
#define JL_HAS_DISK_FREE #define JL_HAS_DISK_FREE
// DATA/, SAVES/ and joeylog.txt in the program's folder, from the
// executable's path (hostBackendProgramFolder, fileRoot.h).
#define JL_HAS_FILE_ROOTS
// audio: a software mixer (MOD through the module player, the SFX slots, three // audio: a software mixer (MOD through the module player, the SFX slots, three
// square-wave voices and YM-style noise) run on the program's thread, so // square-wave voices and YM-style noise) run on the program's thread, so
// the tick callback and the stream fills run there too and the critical // the tick callback and the stream fills run there too and the critical
@ -633,6 +661,19 @@
#define JL_HAS_SERIAL_FLUSH #define JL_HAS_SERIAL_FLUSH
#define JL_HAS_SERIAL_SET_DTR #define JL_HAS_SERIAL_SET_DTR
#define JL_HAS_SERIAL_SET_RTS #define JL_HAS_SERIAL_SET_RTS
#elif defined(JOEYLIB_HOSTED_POSIX_SERIAL)
// serial add-on (optional; a terminal device through termios,
// src/hosted/posix/posixSerial.c: SDL3 on Linux, Haiku, and BeOS; the
// poll moves what the driver holds into the add-on's ring)
#define JL_HAS_SERIAL_OPEN
#define JL_HAS_SERIAL_CLOSE
#define JL_HAS_SERIAL_POLL
#define JL_HAS_SERIAL_READ
#define JL_HAS_SERIAL_WRITE
#define JL_HAS_SERIAL_AVAILABLE
#define JL_HAS_SERIAL_FLUSH
#define JL_HAS_SERIAL_SET_DTR
#define JL_HAS_SERIAL_SET_RTS
#endif #endif
#elif defined(JOEYLIB_PLATFORM_BLANK) #elif defined(JOEYLIB_PLATFORM_BLANK)
// Copy-to-start template. It overrides ONLY the platform SERVICES (TODO // Copy-to-start template. It overrides ONLY the platform SERVICES (TODO

View file

@ -22,11 +22,14 @@
// opened in supervisor mode, as jlInit leaves every program). // opened in supervisor mode, as jlInit leaves every program).
// DVX -- a COM port through DVX's serial library. // DVX -- a COM port through DVX's serial library.
// Win32/16 -- a COM port through Windows' communications calls. // Win32/16 -- a COM port through Windows' communications calls.
// SDL3, the web, the PSP, Haiku, and BeOS have no serial port. // SDL3 (Linux), Haiku, BeOS -- a terminal device through termios
// (JOEYLIB_SERIAL in the environment names the default one).
// The web and the PSP have no serial port.
// //
// Model: non-blocking + buffered. Received bytes accumulate in a background // Model: non-blocking + buffered. Received bytes accumulate in a background
// ring (a hardware IRQ on DOS, the X68000 and the IIgs, the OS on Amiga/ST, // ring (a hardware IRQ on DOS, the X68000 and the IIgs, the OS on Amiga/ST,
// DVX, and Windows); jlSerialRead / jlSerialAvailable never block. Call // DVX, Windows, Linux, Haiku, and BeOS); jlSerialRead / jlSerialAvailable
// never block. Call
// jlSerialPoll() once per game frame. Transmit never waits long: a bounded // jlSerialPoll() once per game frame. Transmit never waits long: a bounded
// poll on most ports, a queue on the Amiga and Win16. // poll on most ports, a queue on the Amiga and Win16.
// //
@ -59,11 +62,15 @@
// JL_SERIAL_SLOT -- a card/port selected by jlSerialConfigT.unit: IIgs // JL_SERIAL_SLOT -- a card/port selected by jlSerialConfigT.unit: IIgs
// Super Serial Card slot (1..7); DOS or DVX COM number // Super Serial Card slot (1..7); DOS or DVX COM number
// (1..4); Win16 COM (1..9); Win32 COM (1..255); Amiga // (1..4); Win16 COM (1..9); Win32 COM (1..255); Amiga
// serial.device unit. The ST and X68000 have one port // serial.device unit; Linux /dev/ttyS<unit-1>; Haiku
// and open it for every device. // and BeOS the unit'th /dev/ports entry; ST family
// TOS Bconmap port (1 the Modem port; 2.. a Mega STe's,
// TT's, or Falcon's others). The X68000 has one port
// and opens it for every device.
// JL_SERIAL_DEFAULT -- the platform's natural RS-232 port (IIgs modem, DOS, // JL_SERIAL_DEFAULT -- the platform's natural RS-232 port (IIgs modem, DOS,
// DVX, and Windows COM1, ST Modem port, Amiga unit 0, // DVX, and Windows COM1, ST Modem port, Amiga unit 0,
// X68000 RS-232C). // X68000 RS-232C, Linux /dev/ttyS0, Haiku and BeOS
// the first /dev/ports entry).
typedef enum { typedef enum {
JL_SERIAL_DEFAULT = 0, JL_SERIAL_DEFAULT = 0,
JL_SERIAL_MODEM, JL_SERIAL_MODEM,
@ -93,7 +100,7 @@ typedef struct {
uint8_t stopBits; // 1 or 2 uint8_t stopBits; // 1 or 2
jlSerialParityE parity; jlSerialParityE parity;
jlSerialFlowE flow; jlSerialFlowE flow;
uint8_t unit; // JL_SERIAL_SLOT: IIgs slot 1..7 / DOS, DVX COM 1..4 / Win16 COM 1..9 / Win32 COM 1..255 / Amiga unit uint8_t unit; // JL_SERIAL_SLOT: IIgs slot 1..7 / DOS, DVX COM 1..4 / Win16 COM 1..9 / Win32 COM 1..255 / Amiga unit / ST Bconmap port 1..
} jlSerialConfigT; } jlSerialConfigT;
// Open + configure the port. Only one port is open at a time; a second // Open + configure the port. Only one port is open at a time; a second

View file

@ -157,7 +157,7 @@ void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
// UP): one extra 16-px group, (ceil(w/16) + 1) * 8 bytes per row. Give // UP): one extra 16-px group, (ceil(w/16) + 1) * 8 bytes per row. Give
// the buffer 2-byte alignment (a uint16_t array has it): on the Amiga, // the buffer 2-byte alignment (a uint16_t array has it): on the Amiga,
// ST, and X68000 an odd address sends save and restore down a much // ST, and X68000 an odd address sends save and restore down a much
// slower path, and on the Amiga and ST it crashes the trusted variants. // slower path, and it crashes the trusted variants.
#define jlSpriteBackupLength(_type, _wT, _hT) \ #define jlSpriteBackupLength(_type, _wT, _hT) \
((uint16_t)((((((((uint16_t)(_wT) * 8u) + 15u) >> 4) + 1u) * 8u * ((uint16_t)(_hT) * 8u)) + sizeof(_type) - 1u) / sizeof(_type))) ((uint16_t)((((((((uint16_t)(_wT) * 8u) + 15u) >> 4) + 1u) * 8u * ((uint16_t)(_hT) * 8u)) + sizeof(_type) - 1u) / sizeof(_type)))
@ -200,8 +200,8 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup);
// even x/width on chunky, backup->height == the rows the compiled // even x/width on chunky, backup->height == the rows the compiled
// save covers -- the sprite's pixel height, or on the IIgs the rows // save covers -- the sprite's pixel height, or on the IIgs the rows
// holding an opaque pixel), // holding an opaque pixel),
// * on the Amiga and ST, backup->bytes is at least 2-byte aligned (the // * on the Amiga, ST, and X68000, backup->bytes is at least 2-byte
// trusted save and save-and-draw need that too), // aligned (the trusted save and save-and-draw need that too),
// * the recorded region is fully on the destination surface. // * the recorded region is fully on the destination surface.
// It is the expert inner-loop entry for a game that pre-compiles its // It is the expert inner-loop entry for a game that pre-compiles its
// sprites (jlSpriteCompile) and already clamps positions on-screen -- // sprites (jlSpriteCompile) and already clamps positions on-screen --
@ -259,10 +259,11 @@ void jlSpriteCompact(void);
// Arena introspection. Used to gauge whether jlSpriteCompact is // Arena introspection. Used to gauge whether jlSpriteCompact is
// worth running, or whether the codegenBytes budget needs to grow. // worth running, or whether the codegenBytes budget needs to grow.
// Free space is (Total - Used), but it may be fragmented across // Free space is (Total - Used), but it may be fragmented across
// holes until jlSpriteCompact runs. On the IIgs Total is what jlInit was // holes until jlSpriteCompact runs. Total is what jlInit was granted,
// granted, in blocks of at most 64 KB that compacting never merges (one // which a machine short of memory can make less than codegenBytes; on the
// sprite's code must fit in one). The interpreting ports still allocate // IIgs in blocks of at most 64 KB that compacting never merges (one
// the arena: Total reports it and Used stays 0. // sprite's code must fit in one). The interpreting ports have no arena:
// both are 0.
uint32_t jlSpriteCodegenBytesUsed(void); uint32_t jlSpriteCodegenBytesUsed(void);
uint32_t jlSpriteCodegenBytesTotal(void); uint32_t jlSpriteCodegenBytesTotal(void);

View file

@ -311,7 +311,7 @@ portGroups() {
# wantedGroups -- the port groups the user chose: those holding an installed # wantedGroups -- the port groups the user chose: those holding an installed
# component that belongs to that group alone. Shared members (headless, # component that belongs to that group alone. Shared members (headless,
# libxmp, mame, cadius) do not count, or installing one port would make # mame, cadius) do not count, or installing one port would make
# every port that shares them look half-installed. # every port that shares them look half-installed.
wantedGroups() { wantedGroups() {
local id local id

View file

@ -6,10 +6,12 @@
# libxmp-lite 4.7.0: the MOD decoder the DOS, Atari ST, X68000, SDL3, PSP, # libxmp-lite 4.7.0: the MOD decoder the DOS, Atari ST, X68000, SDL3, PSP,
# web, Win16, Win32, Haiku and BeOS audio HALs compiled in until 2026-10-14, # web, Win16, Win32, Haiku and BeOS audio HALs compiled in until 2026-10-14,
# when JoeyLib's own module player (src/core/modPlayer.c) replaced it. No # when JoeyLib's own module player (src/core/modPlayer.c) replaced it. No
# build uses it now; it stays a reference to compare that player against. # build uses it now; it stays a reference to compare that player against,
# installed only when asked for by name (install libxmp), so no toolchain
# fetches it.
compDescription="audio: libxmp-lite 4.7.0 source (no build links it; a reference for the module player)" compDescription="audio: libxmp-lite 4.7.0 source (optional; no build links it; a reference for the module player)"
compGroups="toolchain:dos toolchain:atarist toolchain:x68000 toolchain:sdl3 toolchain:psp toolchain:web toolchain:win16 toolchain:win32 toolchain:haiku toolchain:haiku32 toolchain:beos" compGroups="optional"
compRequires="host" compRequires="host"
compHostDeps="tar" compHostDeps="tar"
compUrl="https://github.com/libxmp/libxmp/releases/download/libxmp-4.7.0/libxmp-lite-4.7.0.tar.gz" compUrl="https://github.com/libxmp/libxmp/releases/download/libxmp-4.7.0/libxmp-lite-4.7.0.tar.gz"

View file

@ -1,18 +0,0 @@
--- a/src/mame/apple/apple2gs.cpp
+++ b/src/mame/apple/apple2gs.cpp
@@ -3839,8 +3839,13 @@
ADDRESS_MAP_BANK(config, A2GS_C300_TAG).set_map(&apple2gs_state::c300bank_map).set_options(ENDIANNESS_LITTLE, 8, 32, 0x100);
/* serial */
- SCC85C30(config, m_scc, A2GS_14M / 2);
- m_scc->configure_channels(3'686'400, 3'686'400, 3'686'400, 3'686'400);
+ // RetroNet patch: a real IIgs feeds the SCC BRG a 3.6864 MHz PCLK (the classic 115200
+ // time-constant base); A2GS_14M/2 = 7.159 MHz made every firmware/native BRG rate ~1.94x
+ // fast. Zeroed rxc/txc: the m_rxc>0 path in z80scc update_serial() otherwise FORCES the
+ // receive rate to rxc/16 = 230400 regardless of the programmed BRG, which no rs232 peer
+ // can match.
+ SCC85C30(config, m_scc, 3'686'400);
+ m_scc->configure_channels(0, 0, 0, 0);
m_scc->out_int_callback().set(FUNC(apple2gs_state::scc_irq_w));
m_scc->out_txda_callback().set("printer", FUNC(rs232_port_device::write_txd));
m_scc->out_txdb_callback().set("modem", FUNC(rs232_port_device::write_txd));

View file

@ -6,11 +6,16 @@ drivers. The gates depend on every one of them.
| Patch | What it fixes | Needed by | | Patch | What it fixes | Needed by |
|---|---|---| |---|---|---|
| 0001-apple2gs-scc-clock | The IIgs SCC clock, so serial RX works at rates other than 230400 baud | `verify-iigs-serial.sh` |
| 0002-z80scc-rx-fifo | Makes the Z8530 receive FIFO depth selectable at run time (see below). By default it is 512 bytes, so a POLLED receiver (no RX interrupt) survives the emulator's deaf windows; `JOEY_Z80SCC_RX_FIFO=3` restores the real chip's depth. Originally written for RetroNet. | IIgs serial gates (512); `verify-iigs-serial-loss.sh` (3) | | 0002-z80scc-rx-fifo | Makes the Z8530 receive FIFO depth selectable at run time (see below). By default it is 512 bytes, so a POLLED receiver (no RX interrupt) survives the emulator's deaf windows; `JOEY_Z80SCC_RX_FIFO=3` restores the real chip's depth. Originally written for RetroNet. | IIgs serial gates (512); `verify-iigs-serial-loss.sh` (3) |
| 0003-x68k-crtc-timing | The X68000 CRTC: the graphic high-speed clear's timing (the main defect), and the text raster copy firing on both hsync edges instead of only the front porch | X68000 gates and timing | | 0003-x68k-crtc-timing | The X68000 CRTC: the graphic high-speed clear's timing (the main defect), and the text raster copy firing on both hsync edges instead of only the front porch | X68000 gates and timing |
| 0004-x68k-waitstates-rs232-mouse | Three X68000 changes that edit the same files: VRAM wait states (MAME modelled none), the RS-232C port wired to a slot so `-rs232c null_modem` works, and a backport of upstream commit ab6b6fc9e797 (Patrick Mackinlay, "x68k: add serial mouse"), which provides the `:mouse_port:x68k:X/Y/BTN` inputs | `verify-x68000-serial.sh`, `verify-x68000-mouse.sh`, X68000 timing | | 0004-x68k-waitstates-rs232-mouse | Three X68000 changes that edit the same files: VRAM wait states (MAME modelled none), the RS-232C port wired to a slot so `-rs232c null_modem` works, and a backport of upstream commit ab6b6fc9e797 (Patrick Mackinlay, "x68k: add serial mouse"), which provides the `:mouse_port:x68k:X/Y/BTN` inputs | `verify-x68000-serial.sh`, `verify-x68000-mouse.sh`, X68000 timing |
There is no 0001 any more. It gave the IIgs SCC a 3.6864 MHz PCLK and no
RTxC clock, to suit a HAL that ran the baud rate generator from PCLK. Stock
MAME already clocks the chip as a IIgs does (PCLK 7.16 MHz from the Mega II,
the 3.6864 MHz crystal on RTxC), and the HAL now runs the generator from
RTxC, as cc65's IIgs driver does, and takes 115200 straight from the crystal.
## Receive FIFO depth (0002) ## Receive FIFO depth (0002)
Real Z8530 silicon holds 3 received bytes (an ESCC, 8), so a real client Real Z8530 silicon holds 3 received bytes (an ESCC, 8), so a real client

View file

@ -18,7 +18,7 @@
# BE_CXX_RUNTIME (its C++ runtime library), then includes this file. # BE_CXX_RUNTIME (its C++ runtime library), then includes this file.
HOSTED_BACKEND := be HOSTED_BACKEND := be
HOSTED_DEFINES := -DJOEYLIB_PLATFORM_HOSTED -DJOEYLIB_HOSTED_BE HOSTED_DEFINES := -DJOEYLIB_PLATFORM_HOSTED -DJOEYLIB_HOSTED_BE -DJOEYLIB_HOSTED_POSIX_SERIAL
HOSTED_SDK_CFLAGS := HOSTED_SDK_CFLAGS :=
# The Interface and Application Kits (libbe), the Media Kit's BSoundPlayer # The Interface and Application Kits (libbe), the Media Kit's BSoundPlayer
# (libmedia), the Device Kit's BJoystick (libdevice); the backend's C++ is # (libmedia), the Device Kit's BJoystick (libdevice); the backend's C++ is

View file

@ -32,6 +32,10 @@
# PALPROBE Amiga: the palette gate's program: how fast palette and SCB # PALPROBE Amiga: the palette gate's program: how fast palette and SCB
# changes reach the screen through the copper, and a held screen # changes reach the screen through the copper, and a held screen
# of bands to check that they reach it right. # of bands to check that they reach it right.
# SERSLOT ST: SERTEST opening JL_SERIAL_SLOT unit 2 -- a Mega STe's SCC
# channel B through Bconmap -- for verify-atarist-serial-slot.sh.
# FASTRAM Amiga: the Fast RAM gate's program: a surface made once chip
# RAM has run out draws and copies as a chip one does.
# WBLAUNCH Amiga: the icon gate's program: a program's icon read and # WBLAUNCH Amiga: the icon gate's program: a program's icon read and
# drawn by icon.library, and the program started from it the way # drawn by icon.library, and the program started from it the way
# Workbench starts a tool. # Workbench starts a tool.
@ -40,7 +44,7 @@
# and tick rates, which every port must play alike. # and tick rates, which every port must play alike.
PROGRAMS := HELLO PATTERN DRAW KEYS SERIAL SERTEST SERMOUSE SAVE TILEBANK SCB JOY SPRITE AUDIO UBER ADV ADV2 PROGRAMS := HELLO PATTERN DRAW KEYS SERIAL SERTEST SERMOUSE SAVE TILEBANK SCB JOY SPRITE AUDIO UBER ADV ADV2
TEST_PROGRAMS := SCCPROBE SERLOSS SERLOSSS DIRTYMRK UBERPROB AUDIOPRB AUDPAR PALPROBE WBLAUNCH TEST_PROGRAMS := SCCPROBE SERLOSS SERLOSSS SERSLOT DIRTYMRK UBERPROB AUDIOPRB AUDPAR PALPROBE FASTRAM WBLAUNCH
# DVX's help and resource texts call these JoeyLib examples. # DVX's help and resource texts call these JoeyLib examples.
DVX_KIND := JoeyLib example DVX_KIND := JoeyLib example
@ -129,6 +133,13 @@ DIRTYMRK_BINDIR := $(PROGRAM_BUILD)/test
PALPROBE_SRCS := $(EXAMPLES)/palprobe/palprobe.c PALPROBE_SRCS := $(EXAMPLES)/palprobe/palprobe.c
PALPROBE_PORTS := amiga PALPROBE_PORTS := amiga
PALPROBE_BINDIR := $(PROGRAM_BUILD)/test PALPROBE_BINDIR := $(PROGRAM_BUILD)/test
SERSLOT_SRCS := $(EXAMPLES)/sertest/sertest.c
SERSLOT_PORTS := atarist
SERSLOT_BINDIR := $(PROGRAM_BUILD)/test
SERSLOT_CFLAGS := -DSERTEST_DEVICE=JL_SERIAL_SLOT -DSERTEST_UNIT=2u
FASTRAM_SRCS := $(REPO_DIR)/tests/amiga/fastRam.c
FASTRAM_PORTS := amiga
FASTRAM_BINDIR := $(PROGRAM_BUILD)/test
WBLAUNCH_SRCS := $(EXAMPLES)/wblaunch/wblaunch.c WBLAUNCH_SRCS := $(EXAMPLES)/wblaunch/wblaunch.c
WBLAUNCH_PORTS := amiga WBLAUNCH_PORTS := amiga
WBLAUNCH_BINDIR := $(PROGRAM_BUILD)/test WBLAUNCH_BINDIR := $(PROGRAM_BUILD)/test

View file

@ -56,6 +56,9 @@ endif
# system it runs on, built with the backend's flags; the backend includes # system it runs on, built with the backend's flags; the backend includes
# its headers. # its headers.
HOSTED_C_SRCS := $(wildcard $(HOSTED_DIR)/*.c) HOSTED_C_SRCS := $(wildcard $(HOSTED_DIR)/*.c)
# -DJOEYLIB_HOSTED_POSIX_SERIAL in HOSTED_DEFINES (make/sdl3.mk, make/be.mk):
# the serial add-on through termios, src/hosted/posix.
POSIX_C_SRCS := $(if $(filter -DJOEYLIB_HOSTED_POSIX_SERIAL,$(HOSTED_DEFINES)),$(wildcard $(HOSTED_DIR)/posix/*.c))
BACKEND_C_SRCS := $(wildcard $(BACKEND_DIR)/*.c) BACKEND_C_SRCS := $(wildcard $(BACKEND_DIR)/*.c)
BACKEND_CXX_SRCS := $(wildcard $(BACKEND_DIR)/*.cpp) BACKEND_CXX_SRCS := $(wildcard $(BACKEND_DIR)/*.cpp)
EXTRA_C_SRCS := $(if $(HOSTED_BACKEND_EXTRA),$(wildcard $(HOSTED_BACKEND_EXTRA)/*.c)) EXTRA_C_SRCS := $(if $(HOSTED_BACKEND_EXTRA),$(wildcard $(HOSTED_BACKEND_EXTRA)/*.c))
@ -67,6 +70,7 @@ LIB_OBJS := \
$(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \ $(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \
$(HOSTED_SPRITE_OBJS) \ $(HOSTED_SPRITE_OBJS) \
$(patsubst $(HOSTED_DIR)/%.c,$(BUILD)/obj/hosted/%.o,$(HOSTED_C_SRCS)) \ $(patsubst $(HOSTED_DIR)/%.c,$(BUILD)/obj/hosted/%.o,$(HOSTED_C_SRCS)) \
$(patsubst $(HOSTED_DIR)/posix/%.c,$(BUILD)/obj/posix/%.o,$(POSIX_C_SRCS)) \
$(patsubst $(BACKEND_DIR)/%.c,$(BUILD)/obj/backend/%.o,$(BACKEND_C_SRCS)) \ $(patsubst $(BACKEND_DIR)/%.c,$(BUILD)/obj/backend/%.o,$(BACKEND_C_SRCS)) \
$(patsubst $(BACKEND_DIR)/%.cpp,$(BUILD)/obj/backend/%.o,$(BACKEND_CXX_SRCS)) \ $(patsubst $(BACKEND_DIR)/%.cpp,$(BUILD)/obj/backend/%.o,$(BACKEND_CXX_SRCS)) \
$(patsubst $(HOSTED_BACKEND_EXTRA)/%.c,$(BUILD)/obj/backendExtra/%.o,$(EXTRA_C_SRCS)) $(patsubst $(HOSTED_BACKEND_EXTRA)/%.c,$(BUILD)/obj/backendExtra/%.o,$(EXTRA_C_SRCS))
@ -93,6 +97,10 @@ $(BUILD)/obj/hosted/%.o: $(HOSTED_DIR)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(HOSTED_CC) $(CFLAGS) -c $< -o $@ $(HOSTED_CC) $(CFLAGS) -c $< -o $@
$(BUILD)/obj/posix/%.o: $(HOSTED_DIR)/posix/%.c
@mkdir -p $(dir $@)
$(HOSTED_CC) $(CFLAGS) -c $< -o $@
# Only the backend sees its library's headers. # Only the backend sees its library's headers.
$(BUILD)/obj/backend/%.o: $(BACKEND_DIR)/%.c $(BUILD)/obj/backend/%.o: $(BACKEND_DIR)/%.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)

View file

@ -24,7 +24,7 @@ SDL3_PKG_CONFIG := PKG_CONFIG_PATH=$(SDL3_DIR)/lib/pkgconfig pkg-config
HOSTED_CC := $(HOST_CC) HOSTED_CC := $(HOST_CC)
HOSTED_AR := ar HOSTED_AR := ar
HOSTED_BACKEND := sdl3 HOSTED_BACKEND := sdl3
HOSTED_DEFINES := -DJOEYLIB_PLATFORM_HOSTED -DJOEYLIB_HOSTED_SDL3 HOSTED_DEFINES := -DJOEYLIB_PLATFORM_HOSTED -DJOEYLIB_HOSTED_SDL3 -DJOEYLIB_HOSTED_POSIX_SERIAL
HOSTED_SDK_CFLAGS := $(shell $(SDL3_PKG_CONFIG) --cflags sdl3) -DHOSTED_WINDOW_ICON HOSTED_SDK_CFLAGS := $(shell $(SDL3_PKG_CONFIG) --cflags sdl3) -DHOSTED_WINDOW_ICON
HOSTED_LINK := $(shell $(SDL3_PKG_CONFIG) --static --libs sdl3) HOSTED_LINK := $(shell $(SDL3_PKG_CONFIG) --static --libs sdl3)
PROGRAM_EXT := PROGRAM_EXT :=

View file

@ -3,10 +3,11 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// //
// Minimal math.h shim for the m68k-atari-mint toolchain (mintlib // Minimal math.h shim for the m68k-atari-mint toolchain (mintlib
// doesn't ship <math.h> or libm). Declares the few functions libxmp-lite // doesn't ship <math.h> or libm), so a program that includes <math.h>
// once reached for; the implementations live in math_shim.c next to this // builds on the ST: ceil, floor, log, pow and trunc (and their float
// header, built into the ST's libjoey.a. Accuracy is a few hundred parts // forms), nothing else (docs/Portability.adoc). The implementations live in
// per million, not for general-purpose use. // math_shim.c next to this header, built into the ST's libjoey.a. Accuracy
// is a few hundred parts per million, not for general-purpose use.
#ifndef JOEY_ATARIST_MATH_SHIM_H #ifndef JOEY_ATARIST_MATH_SHIM_H
#define JOEY_ATARIST_MATH_SHIM_H #define JOEY_ATARIST_MATH_SHIM_H

View file

@ -3,7 +3,8 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// //
// Tiny libm replacement on the m68k-atari-mint toolchain, built into the // Tiny libm replacement on the m68k-atari-mint toolchain, built into the
// ST's libjoey.a. Only the functions libxmp-lite once called are implemented. // ST's libjoey.a, for programs that include <math.h> (math.h here says
// which functions).
// Accuracy targets: a few hundred parts per million, plenty for // Accuracy targets: a few hundred parts per million, plenty for
// note-frequency math. // note-frequency math.

View file

@ -13,9 +13,10 @@
sets the window as the native SDL3 port's environment does: sets the window as the native SDL3 port's environment does:
NAME.html?scale=1&aspect=square&fullscreen=1 become JOEYLIB_SCALE, NAME.html?scale=1&aspect=square&fullscreen=1 become JOEYLIB_SCALE,
JOEYLIB_ASPECT and JOEYLIB_FULLSCREEN (the web gates open the stage at one JOEYLIB_ASPECT and JOEYLIB_FULLSCREEN (the web gates open the stage at one
canvas pixel per stage pixel that way). The program's joeylog.txt (the canvas pixel per stage pixel that way). The program's joeylog.txt (in its
in-memory file system's /joeylog.txt) is mirrored into the hidden #joeylog write folder in the in-memory file system, Module.joeyWriteFolder, once the
element once a second, for a look without a debugger. program has one) is mirrored into the hidden #joeylog element once a
second, for a look without a debugger.
--> -->
<html lang="en"> <html lang="en">
<head> <head>
@ -56,11 +57,11 @@
}); });
setInterval(function () { setInterval(function () {
var log; var log;
if (!Module.FS) { if (!Module.FS || !Module.joeyWriteFolder) {
return; return;
} }
try { try {
log = Module.FS.readFile("/joeylog.txt", { encoding: "utf8" }); log = Module.FS.readFile(Module.joeyWriteFolder + "joeylog.txt", { encoding: "utf8" });
} catch (e) { } catch (e) {
return; return;
} }

View file

@ -58,6 +58,7 @@ LIB_OBJS := \
$(BUILD)/obj/68k/planarPlot.o \ $(BUILD)/obj/68k/planarPlot.o \
$(BUILD)/obj/68k/stageBands.o \ $(BUILD)/obj/68k/stageBands.o \
$(BUILD)/obj/68k/modMix.o \ $(BUILD)/obj/68k/modMix.o \
$(BUILD)/obj/68k/sfxMix.o \
$(BUILD)/obj/68k/planarSpriteWalk.o \ $(BUILD)/obj/68k/planarSpriteWalk.o \
$(BUILD)/obj/68k/spriteEmitPlanar68k.o \ $(BUILD)/obj/68k/spriteEmitPlanar68k.o \
$(BUILD)/obj/68k/planarSprite.o \ $(BUILD)/obj/68k/planarSprite.o \

View file

@ -22,6 +22,9 @@
# 320x200x8, VGA mode 13h here), and fails unless DVX.LOG says the mode was # 320x200x8, VGA mode 13h here), and fails unless DVX.LOG says the mode was
# set: the same hashes, the full-screen present's timings. # set: the same hashes, the full-screen present's timings.
# #
# --square opens the window with square pixels (Game > Square Pixels, staged
# the same way) instead of 4:3.
#
# --probe runs UBER's probe build instead (make dvx builds it: its UBER_PROBE # --probe runs UBER's probe build instead (make dvx builds it: its UBER_PROBE
# rows, the display parity row among them, at one frame per timed op), its # rows, the display parity row among them, at one frame per timed op), its
# log copied to build/dvx/probe/joeylog.txt; scripts/verify-dvx-display.sh # log copied to build/dvx/probe/joeylog.txt; scripts/verify-dvx-display.sh
@ -30,7 +33,7 @@
# #
# scripts/bench-dvx.sh # default 600 s timeout # scripts/bench-dvx.sh # default 600 s timeout
# scripts/bench-dvx.sh --full-screen # on the whole screen # scripts/bench-dvx.sh --full-screen # on the whole screen
# scripts/bench-dvx.sh --probe # the probe build (with --full-screen too) # scripts/bench-dvx.sh --probe # the probe build (with --full-screen or --square too)
# TIMEOUT=900 scripts/bench-dvx.sh # override # TIMEOUT=900 scripts/bench-dvx.sh # override
set -euo pipefail set -euo pipefail
@ -42,6 +45,7 @@ joeyNeed DOSBOX dosbox
joeyNeed DVX_ROOT dvx joeyNeed DVX_ROOT dvx
fullScreen=0 fullScreen=0
square=0
program=uber program=uber
appDir=$(dvxAppDir uber) appDir=$(dvxAppDir uber)
out=$DVX_BIN/$DVX_LOG_NAME out=$DVX_BIN/$DVX_LOG_NAME
@ -50,13 +54,16 @@ for arg in "$@"; do
--full-screen) --full-screen)
fullScreen=1 fullScreen=1
;; ;;
--square)
square=1
;;
--probe) --probe)
program=uberprob program=uberprob
appDir=$DVX_PROBE_DIR/apps/$DVX_VENDOR/$program appDir=$DVX_PROBE_DIR/apps/$DVX_VENDOR/$program
out=$DVX_PROBE_DIR/$DVX_LOG_NAME out=$DVX_PROBE_DIR/$DVX_LOG_NAME
;; ;;
*) *)
echo "usage: scripts/bench-dvx.sh [--full-screen] [--probe]" >&2 echo "usage: scripts/bench-dvx.sh [--full-screen] [--square] [--probe]" >&2
exit 2 exit 2
;; ;;
esac esac
@ -84,8 +91,11 @@ cleanup() {
trap cleanup EXIT trap cleanup EXIT
dvxStage "$work/c" "$appDir" dvxStage "$work/c" "$appDir"
if [ "$fullScreen" = 1 ]; then switches=()
dvxFullScreen "$work/c" "$program" [ "$fullScreen" = 1 ] && switches+=("$DVX_SETTINGS_FULL_SCREEN")
[ "$square" = 1 ] && switches+=("$DVX_SETTINGS_SQUARE")
if [ ${#switches[@]} -gt 0 ]; then
dvxSettings "$work/c" "$program" "${switches[@]}"
fi fi
dvxDosboxConf "$work/dosbox.conf" "$work/c" dvxDosboxConf "$work/dosbox.conf" "$work/c"
mkdir -p "$work/xdg" mkdir -p "$work/xdg"

View file

@ -97,12 +97,19 @@ local EXIT_SETTLE = 900 -- keypress -> fclose -> back at the Human68k prompt
-- 16-bit word per pixel, 512 words per row. -- 16-bit word per pixel, 512 words per row.
-- The 320x200 stage is centred at origin (96, 156) either way. -- The 320x200 stage is centred at origin (96, 156) either way.
local presentPlane = "$PRESENT_PLANE" local presentPlane = "$PRESENT_PLANE"
-- The text plane holds two pages, and the port flips between them with the
-- text Y scroll (CRTC R11, which reads back): rows are counted from the page
-- on screen.
local function shownRow()
return mem:read_u16(0xE80016) & 0x3FF
end
local function stagePixel(px, py) local function stagePixel(px, py)
if presentPlane == "gfx" then if presentPlane == "gfx" then
return mem:read_u8(0xC00000 + ((156 + py) * 512 + 96 + px) * 2 + 1) return mem:read_u8(0xC00000 + ((156 + py) * 512 + 96 + px) * 2 + 1)
end end
local x = 96 + px local x = 96 + px
local byteOff = (156 + py) * 128 + (x >> 3) local byteOff = (156 + shownRow() + py) * 128 + (x >> 3)
local bit = 0x80 >> (x & 7) local bit = 0x80 >> (x & 7)
local v = 0 local v = 0
for plane = 0, 3 do for plane = 0, 3 do

View file

@ -19,7 +19,7 @@ work=$(mktemp -d -t joeylib-save.XXXXXX)
trap 'rm -rf "$work"' EXIT trap 'rm -rf "$work"' EXIT
"$cc" -DJOEYLIB_PLATFORM_BLANK \ "$cc" -DJOEYLIB_PLATFORM_BLANK \
-DJL_HAS_SAVE_DIR_ENSURE -DJL_HAS_SAVE_DELETE -DJL_HAS_DISK_FREE \ -DJL_HAS_SAVE_DIR_ENSURE -DJL_HAS_SAVE_DELETE -DJL_HAS_SAVE_RENAME -DJL_HAS_DISK_FREE \
-I"$repo/include" -I"$repo/include/joey" -I"$repo/src/core" -Wall -Wextra \ -I"$repo/include" -I"$repo/include/joey" -I"$repo/src/core" -Wall -Wextra \
"$repo/src/core/save.c" "$repo/tests/host/saveHost.c" \ "$repo/src/core/save.c" "$repo/tests/host/saveHost.c" \
-o "$work/saveHost" -o "$work/saveHost"

View file

@ -23,6 +23,7 @@ cd "$repo"
-DJL_HAS_SERIAL_OPEN -DJL_HAS_SERIAL_CLOSE -DJL_HAS_SERIAL_POLL \ -DJL_HAS_SERIAL_OPEN -DJL_HAS_SERIAL_CLOSE -DJL_HAS_SERIAL_POLL \
-DJL_HAS_SERIAL_READ -DJL_HAS_SERIAL_WRITE -DJL_HAS_SERIAL_AVAILABLE \ -DJL_HAS_SERIAL_READ -DJL_HAS_SERIAL_WRITE -DJL_HAS_SERIAL_AVAILABLE \
-DJL_HAS_SERIAL_FLUSH -DJL_HAS_SERIAL_SET_RTS -DJL_HAS_SERIAL_SET_DTR \ -DJL_HAS_SERIAL_FLUSH -DJL_HAS_SERIAL_SET_RTS -DJL_HAS_SERIAL_SET_DTR \
-DJL_HAS_MILLIS_ELAPSED \
-Iinclude -Iinclude/joey -Isrc/core -Wall -Wextra \ -Iinclude -Iinclude/joey -Isrc/core -Wall -Wextra \
src/core/serial.c tests/host/serialHost.c \ src/core/serial.c tests/host/serialHost.c \
-o "$work/serialHost" -o "$work/serialHost"

View file

@ -13,6 +13,9 @@
# Ctrl reaches the program. The mapper replaces every DOSBox binding: only # Ctrl reaches the program. The mapper replaces every DOSBox binding: only
# these keys and Escape reach the machine. Source after joyGate.sh. # these keys and Escape reach the machine. Source after joyGate.sh.
# #
# DOSBOX_JOY_SETTINGS [joystick] settings every run needs
# ("name=value"); DOSBOX_JOY_SET_ARGS has them
# as DOSBox --set arguments
# dosboxJoyMapper FILE write the mapper file # dosboxJoyMapper FILE write the mapper file
# dosboxJoyTap NAME press and let go of a key of the table # dosboxJoyTap NAME press and let go of a key of the table
# (escape) # (escape)
@ -29,6 +32,22 @@ DOSBOX_JOY_WINDOW=""
DOSBOX_JOY_SHOTS="" DOSBOX_JOY_SHOTS=""
DOSBOX_JOY_TAP_SECONDS=0.3 DOSBOX_JOY_TAP_SECONDS=0.3
# DOSBox Staging 0.83's 4axis sticks wrap buttons at two when no host
# joystick is open: CStickBindGroup sets the wrap to its two buttons, and
# C4AxisBindGroup raises the count to four but copies it to the wrap only
# with buttonwrap on, so stick B's buttons would press stick A's. buttonwrap
# on wraps at four. A host pad DOSBox opened set the wrap to its own button
# count and hid this, and would add its own axes and buttons, so every host
# pad is hidden from DOSBox's SDL (an id no device has is the only one let
# through) and the runs are the same whatever is plugged in.
DOSBOX_JOY_SETTINGS=("buttonwrap=on")
DOSBOX_JOY_SET_ARGS=()
for setting in "${DOSBOX_JOY_SETTINGS[@]}"; do
DOSBOX_JOY_SET_ARGS+=(--set "$setting")
done
unset setting
export SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT=0x0000/0x0000
# Control or name, its host key (xdotool's name), the key's SDL scancode and # Control or name, its host key (xdotool's name), the key's SDL scancode and
# the DOSBox mapper event it drives. # the DOSBox mapper event it drives.
DOSBOX_JOY_KEYS=( DOSBOX_JOY_KEYS=(

View file

@ -22,10 +22,11 @@ DVX_PROBE_DIR="${DVX_PROBE_DIR:-$JOEY_REPO/build/dvx/probe}"
DVX_VENDOR="${DVX_VENDOR:-joeylib}" DVX_VENDOR="${DVX_VENDOR:-joeylib}"
DVX_LOG_NAME=joeylog.txt DVX_LOG_NAME=joeylog.txt
# The port's own settings file in a program's settings folder, and its # The port's own settings file in a program's settings folder, and its
# full-screen switch (src/dvx/hal.c). # full-screen and square-pixels switches (src/dvx/hal.c).
DVX_SETTINGS_FILE=JOEYLIB.INI DVX_SETTINGS_FILE=JOEYLIB.INI
DVX_SETTINGS_SECTION=display DVX_SETTINGS_SECTION=display
DVX_SETTINGS_FULL_SCREEN=fullScreen DVX_SETTINGS_FULL_SCREEN=fullScreen
DVX_SETTINGS_SQUARE=squarePixels
# DVX.LOG's line when a program's full-screen mode is set (the port asks for # DVX.LOG's line when a program's full-screen mode is set (the port asks for
# the stage's size at 8 bits a pixel). # the stage's size at 8 bits a pixel).
DVX_FULL_SCREEN_LOG="Video: full screen 320x200 at 8 bpp" DVX_FULL_SCREEN_LOG="Video: full screen 320x200 at 8 bpp"
@ -90,18 +91,30 @@ dvxStage() {
} }
# dvxFullScreen DIR PROGRAM -- have PROGRAM start on the whole screen in the # dvxSettings DIR PROGRAM SWITCH... -- have PROGRAM start with the port's
# DVX staged in DIR: the port's own setting (JOEYLIB.INI, [display] # own display switches on (DVX_SETTINGS_FULL_SCREEN, DVX_SETTINGS_SQUARE) in
# fullScreen, in the program's settings folder), as Game > Full Screen leaves # the DVX staged in DIR: its JOEYLIB.INI, [display], in the program's
# it. DOS names are case-blind, so an existing folder of any case is used. # settings folder, as the Game menu leaves it. DOS names are case-blind, so
dvxFullScreen() { # an existing folder of any case is used.
dvxSettings() {
local conf local conf
local key
conf="$(find "$1/DVX" -ipath "*/config/apps/$DVX_VENDOR/$2" -type d -print -quit 2>/dev/null)" conf="$(find "$1/DVX" -ipath "*/config/apps/$DVX_VENDOR/$2" -type d -print -quit 2>/dev/null)"
if [ -z "$conf" ]; then if [ -z "$conf" ]; then
conf="$1/DVX/config/apps/$DVX_VENDOR/$2" conf="$1/DVX/config/apps/$DVX_VENDOR/$2"
mkdir -p "$conf" mkdir -p "$conf"
fi fi
printf '[%s]\r\n%s = true\r\n' "$DVX_SETTINGS_SECTION" "$DVX_SETTINGS_FULL_SCREEN" > "$conf/$DVX_SETTINGS_FILE" printf '[%s]\r\n' "$DVX_SETTINGS_SECTION" > "$conf/$DVX_SETTINGS_FILE"
for key in "${@:3}"; do
printf '%s = true\r\n' "$key" >> "$conf/$DVX_SETTINGS_FILE"
done
}
# dvxFullScreen DIR PROGRAM -- have PROGRAM start on the whole screen
# (dvxSettings).
dvxFullScreen() {
dvxSettings "$1" "$2" "$DVX_SETTINGS_FULL_SCREEN"
} }

View file

@ -18,6 +18,13 @@ works too).
WALL is the wall-clock budget in seconds. Prints one LOOP PASS / LOOP FAIL WALL is the wall-clock budget in seconds. Prints one LOOP PASS / LOOP FAIL
line; exit 0 on pass, 1 on fail. line; exit 0 on pass, 1 on fail.
--close-hold FILE (pty) then has SERTEST close the port: its DTR check, run
for the closes alone (a pty has no modem lines), every marker line
acknowledged, after which the port stays closed. Prints one CLOSE PASS /
CLOSE FAIL line more, and keeps the pty pair open until FILE exists or WALL
runs out, so the gate can read the guest end's settings after the close
(verify-sdl3-serial.sh).
--dtr-check (TCP, with DOSBox's nullmodem in transparent:0 mode, which puts --dtr-check (TCP, with DOSBox's nullmodem in transparent:0 mode, which puts
the guest's modem lines in the byte stream as they change: 0xFF 0xFF is a the guest's modem lines in the byte stream as they change: 0xFF 0xFF is a
data 0xFF, 0xFF <b> the lines, bit 0 RTS, bit 1 DTR) then runs SERTEST's data 0xFF, 0xFF <b> the lines, bit 0 RTS, bit 1 DTR) then runs SERTEST's
@ -132,6 +139,42 @@ def openPty(pathFile):
return master return master
def ackMarks(send, got, base, marks):
# Acknowledge each of SERTEST's marker lines (after got[base:]) as it
# completes, recording where it is in marks.
while len(marks) < len(MARKS):
name, mark = MARKS[len(marks)]
at = got.find(mark, base)
if at < 0:
return
marks[name] = at
send(DTR_ACK)
def closeHold(send, receive, wall, start, got, holdFile):
# After the loopback: SERTEST's DTR check for its closes alone, then the
# pty held open until holdFile exists.
base = len(got)
send(DTR_CHECK)
marks = {}
while len(marks) < len(MARKS) and time.time() - start < wall:
data = receive()
if data is None:
break
got += data
ackMarks(send, got, base, marks)
if len(marks) == len(MARKS):
print("CLOSE PASS: every marker acknowledged; SERTEST has closed the port")
status = 0
else:
print("CLOSE FAIL: no %r from SERTEST; stream=%r" % (MARKS[len(marks)][1], got[base:base + SHOWN_BYTES]))
status = 1
sys.stdout.flush()
while not os.path.exists(holdFile) and time.time() - start < wall:
receive()
return status
def dtrCheck(send, receive, wall, start, lines, got): def dtrCheck(send, receive, wall, start, lines, got):
# After the loopback: the stream so far is got; lines has decoded it. # After the loopback: the stream so far is got; lines has decoded it.
base = len(got) base = len(got)
@ -145,15 +188,8 @@ def dtrCheck(send, receive, wall, start, lines, got):
if data is None: if data is None:
break break
got += data got += data
# Acknowledge each marker line as it completes; the last starts the # The last marker starts the watch.
# watch. ackMarks(send, got, base, marks)
while len(marks) < len(MARKS):
name, mark = MARKS[len(marks)]
at = got.find(mark, base)
if at < 0:
break
marks[name] = at
send(DTR_ACK)
if hangupAt is None and len(marks) == len(MARKS): if hangupAt is None and len(marks) == len(MARKS):
hangupAt = time.time() hangupAt = time.time()
if len(marks) < len(MARKS): if len(marks) < len(MARKS):
@ -206,7 +242,12 @@ def loop(send, receive, wall, start, banner):
def main(argv): def main(argv):
withDtrCheck = "--dtr-check" in argv withDtrCheck = "--dtr-check" in argv
argv = [arg for arg in argv if arg != "--dtr-check"] argv = [arg for arg in argv if arg != "--dtr-check"]
if len(argv) < 4 or argv[1] not in ("tcp-server", "tcp-client", "pty") or (withDtrCheck and argv[1] == "pty"): holdFile = None
if "--close-hold" in argv and argv.index("--close-hold") + 1 < len(argv):
at = argv.index("--close-hold")
holdFile = argv[at + 1]
argv = argv[:at] + argv[at + 2:]
if len(argv) < 4 or argv[1] not in ("tcp-server", "tcp-client", "pty") or (withDtrCheck and argv[1] == "pty") or (holdFile is not None and argv[1] != "pty"):
sys.stderr.write(__doc__) sys.stderr.write(__doc__)
return 2 return 2
mode = argv[1] mode = argv[1]
@ -226,7 +267,11 @@ def main(argv):
except OSError: except OSError:
return b"" return b""
return loop(lambda data: os.write(master, data), receive, wall, start, banner)[0] send = lambda data: os.write(master, data)
status, got = loop(send, receive, wall, start, banner)
if status != 0 or holdFile is None:
return status
return closeHold(send, receive, wall, start, got, holdFile)
port = int(argv[2]) port = int(argv[2])
conn = acceptOne(port, wall) if mode == "tcp-server" else connectOne(port, wall, start) conn = acceptOne(port, wall) if mode == "tcp-server" else connectOne(port, wall, start)

View file

@ -19,6 +19,8 @@
// " ") for MS milliseconds // " ") for MS milliseconds
// reload load the page again (what a program // reload load the page again (what a program
// kept in IndexedDB comes back) // kept in IndexedDB comes back)
// open:PAGE load PAGE, a page beside --url's
// (same site, same browser storage)
// shot:FILE a PNG of the canvas alone, now // shot:FILE a PNG of the canvas alone, now
// pad:press:CONTROLS, pad:release:CONTROLS // pad:press:CONTROLS, pad:release:CONTROLS
// hold or let go of the --gamepads // hold or let go of the --gamepads
@ -34,13 +36,20 @@
// button and button 1 the east // button and button 1 the east
// --log FILE at the end, the program's joeylog.txt // --log FILE at the end, the program's joeylog.txt
// --file PATH:OUT at the end, the page's file PATH (its in-memory file // --file PATH:OUT at the end, the page's file PATH (its in-memory file
// system) into OUT; given once per file // system; a PATH without a leading / is in the
// program's write folder, Module.joeyWriteFolder) into
// OUT; given once per file
// --audio FILE record what the program plays, as a 16-bit stereo WAV: // --audio FILE record what the program plays, as a 16-bit stereo WAV:
// SDL3 plays through a ScriptProcessorNode, so a script // SDL3 plays through a ScriptProcessorNode, so a script
// put in the page before the program's own wraps the // put in the page before the program's own wraps the
// node's onaudioprocess and keeps a copy of every buffer // node's onaudioprocess and keeps a copy of every buffer
// it fills; the WAV, and the --step script, start with // it fills; the WAV, and the --step script, start with
// the first one // the first one
// --sound play through the system's sound output (PULSE_SINK
// in the environment picks the device) rather than
// muted: a muted page's audio runs on a fake clock of
// Chromium's, which can drift off real time, so a
// recording that must keep time plays to a sink
// Console lines and uncaught exceptions go to stderr as they happen. Exits 0 // Console lines and uncaught exceptions go to stderr as they happen. Exits 0
// when it ran to the end (or reached --until), 1 when --until never came, the // when it ran to the end (or reached --until), 1 when --until never came, the
// page threw, or Chromium could not be driven. // page threw, or Chromium could not be driven.
@ -62,8 +71,9 @@ const MS_PER_SECOND = 1000;
const DEBUG_PORT_MIN = 30000; const DEBUG_PORT_MIN = 30000;
const DEBUG_PORT_SPAN = 20000; const DEBUG_PORT_SPAN = 20000;
// The program's log, read from the in-memory file system ("" until there is one). // The program's log, read from its write folder in the in-memory file system
const READ_LOG = "(function () { try { return Module.FS.readFile('/joeylog.txt', { encoding: 'utf8' }); } catch (e) { return ''; } })()"; // ("" until there is one).
const READ_LOG = "(function () { try { return Module.FS.readFile(Module.joeyWriteFolder + 'joeylog.txt', { encoding: 'utf8' }); } catch (e) { return ''; } })()";
// The recorder --audio puts in the page: every buffer SDL's ScriptProcessorNode // The recorder --audio puts in the page: every buffer SDL's ScriptProcessorNode
// fills, kept as 16-bit stereo samples. // fills, kept as 16-bit stereo samples.
const AUDIO_RECORDER = `(function () { const AUDIO_RECORDER = `(function () {
@ -167,6 +177,7 @@ function parseArgs(argv) {
case "--log": opts.log = value; i++; break; case "--log": opts.log = value; i++; break;
case "--audio": opts.audio = value; i++; break; case "--audio": opts.audio = value; i++; break;
case "--file": opts.files.push(value); i++; break; case "--file": opts.files.push(value); i++; break;
case "--sound": opts.sound = true; break;
default: default:
throw new Error("unknown option " + name); throw new Error("unknown option " + name);
} }
@ -228,14 +239,16 @@ function connect(wsUrl, onEvent) {
} }
// One --step. // One --step; url is --url's, beside which open: finds its page.
async function runStep(cdp, step) { async function runStep(cdp, step, url) {
const at = step.indexOf(":"); const at = step.indexOf(":");
const verb = step.slice(0, at); const verb = step.slice(0, at);
const rest = step.slice(at + 1); const rest = step.slice(at + 1);
if (step === "reload") { if (step === "reload") {
await cdp.send("Page.reload"); await cdp.send("Page.reload");
} else if (verb === "open") {
await cdp.send("Page.navigate", { url: new URL(rest, url).href });
} else if (verb === "wait") { } else if (verb === "wait") {
await sleep(Number(rest) * MS_PER_SECOND); await sleep(Number(rest) * MS_PER_SECOND);
} else if (verb === "type") { } else if (verb === "type") {
@ -324,7 +337,8 @@ async function main() {
const profile = mkdtempSync(join(tmpdir(), "joeylib-web-")); const profile = mkdtempSync(join(tmpdir(), "joeylib-web-"));
const debugPort = DEBUG_PORT_MIN + Math.floor(Math.random() * DEBUG_PORT_SPAN); const debugPort = DEBUG_PORT_MIN + Math.floor(Math.random() * DEBUG_PORT_SPAN);
const chrome = spawn(opts.chrome, [ const chrome = spawn(opts.chrome, [
"--headless=new", "--no-sandbox", "--no-first-run", "--mute-audio", "--headless=new", "--no-sandbox", "--no-first-run",
...(opts.sound ? [] : ["--mute-audio"]),
"--autoplay-policy=no-user-gesture-required", "--autoplay-policy=no-user-gesture-required",
"--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--use-angle=swiftshader", "--enable-unsafe-swiftshader",
"--window-size=1024,800", "--window-size=1024,800",
@ -387,7 +401,7 @@ async function main() {
await sleep(POLL_MS); await sleep(POLL_MS);
} }
for (const step of opts.steps) { for (const step of opts.steps) {
await runStep(cdp, step); await runStep(cdp, step, opts.url);
} }
} }
if (opts.audio !== undefined) { if (opts.audio !== undefined) {
@ -404,7 +418,7 @@ async function main() {
for (const file of opts.files) { for (const file of opts.files) {
const at = file.lastIndexOf(":"); const at = file.lastIndexOf(":");
const path = file.slice(0, at); const path = file.slice(0, at);
const data = await evaluate(cdp, "(function () { try { var b = Module.FS.readFile(" + JSON.stringify(path) + "); var t = ''; var i; for (i = 0; i < b.length; i++) { t += String.fromCharCode(b[i]); } return btoa(t); } catch (e) { return null; } })()"); const data = await evaluate(cdp, "(function () { try { var p = " + JSON.stringify(path) + "; var b = Module.FS.readFile(p.charAt(0) === '/' ? p : Module.joeyWriteFolder + p); var t = ''; var i; for (i = 0; i < b.length; i++) { t += String.fromCharCode(b[i]); } return btoa(t); } catch (e) { return null; } })()");
if (data === null) { if (data === null) {
throw new Error("the page has no file " + path); throw new Error("the page has no file " + path);
} }

View file

@ -10,11 +10,19 @@
# #
# webNeed exit 2 unless Chromium and Node (emsdk's) are here # webNeed exit 2 unless Chromium and Node (emsdk's) are here
# webServe DIR serve DIR; sets WEB_PORT and WEB_SERVER_PID # webServe DIR serve DIR; sets WEB_PORT and WEB_SERVER_PID
# webSoundSink a PulseAudio null sink for Chromium to play to, so
# a gate that times the page's sound has a real clock
# pacing it (muted, Chromium's audio runs on a fake
# one that drifts off real time); false without a
# sound server
# webDrive PAGE ARGS... run webDriver.mjs on http://127.0.0.1:WEB_PORT/PAGE # webDrive PAGE ARGS... run webDriver.mjs on http://127.0.0.1:WEB_PORT/PAGE
# webStop stop the server # (playing to the sink, when there is one)
# webStop stop the server, and remove the sink
WEB_PORT="" WEB_PORT=""
WEB_SERVER_PID="" WEB_SERVER_PID=""
WEB_SINK_NAME=""
WEB_SINK_MODULE=""
WEB_PORT_MIN=20000 WEB_PORT_MIN=20000
WEB_PORT_SPAN=20000 WEB_PORT_SPAN=20000
@ -49,14 +57,32 @@ webServe() {
} }
webSoundSink() {
# shellcheck source=scripts/pulseRecordLib.sh
source "$JOEY_REPO/scripts/pulseRecordLib.sh"
pulseEnsureServer || return 1
WEB_SINK_NAME="joeyWeb$$"
WEB_SINK_MODULE=$(pulseCreateSink "$WEB_SINK_NAME") || WEB_SINK_MODULE=""
[ -n "$WEB_SINK_MODULE" ]
}
webDrive() { webDrive() {
local page="$1" local page="$1"
shift shift
if [ -n "$WEB_SINK_MODULE" ]; then
PULSE_SINK="$WEB_SINK_NAME" "$WEB_NODE" "$JOEY_REPO/scripts/lib/webDriver.mjs" --chrome "$WEB_CHROME" --url "http://127.0.0.1:$WEB_PORT/$page" --sound "$@"
else
"$WEB_NODE" "$JOEY_REPO/scripts/lib/webDriver.mjs" --chrome "$WEB_CHROME" --url "http://127.0.0.1:$WEB_PORT/$page" "$@" "$WEB_NODE" "$JOEY_REPO/scripts/lib/webDriver.mjs" --chrome "$WEB_CHROME" --url "http://127.0.0.1:$WEB_PORT/$page" "$@"
fi
} }
webStop() { webStop() {
joeyStop "$WEB_SERVER_PID" joeyStop "$WEB_SERVER_PID"
WEB_SERVER_PID="" WEB_SERVER_PID=""
if [ -n "$WEB_SINK_MODULE" ]; then
pulseUnloadSink "$WEB_SINK_MODULE"
WEB_SINK_MODULE=""
fi
} }

View file

@ -46,8 +46,10 @@
# atarist and ste: the palette -- every shifter colour the ST # atarist and ste: the palette -- every shifter colour the ST
# build writes is the 3-bit truncation, the STe build's (on an # build writes is the 3-bit truncation, the STe build's (on an
# STe and a Mega STe) the exact 4-bit $0RGB # STe and a Mega STe) the exact 4-bit $0RGB
# amiga: the palette -- palette changes reach the screen at copper # amiga: the palette -- palette and SCB changes present one a
# patch speed, and right, after a palette and an SCB change # frame (no copper rebuild), and right, after a palette and an
# SCB change; and a surface in Fast RAM (chip RAM full) draws and
# copies as a chip one
# tilebank each port: examples/tilebank loads the port's own .tbk bake # tilebank each port: examples/tilebank loads the port's own .tbk bake
# of the host fixture both ways, pixel-exact and with the host # of the host fixture both ways, pixel-exact and with the host
# gate's surface hash, and refuses every other target's bake (the # gate's surface hash, and refuses every other target's bake (the
@ -120,13 +122,18 @@
# host scripts/check-serial.sh (no emulator) # host scripts/check-serial.sh (no emulator)
# iigs SCC modem port -> MAME null_modem # iigs SCC modem port -> MAME null_modem
# x68000 SCC channel A -> MAME null_modem # x68000 SCC channel A -> MAME null_modem
# atarist Modem port -> Hatari pty # atarist Modem port -> Hatari pty; JL_SERIAL_SLOT unit 2 -> a
# Mega STe's SCC channel B (Bconmap), sending only
# amiga serial.device -> FS-UAE TCP # amiga serial.device -> FS-UAE TCP
# dos COM1 -> DOSBox nullmodem # dos COM1 -> DOSBox nullmodem
# dvx COM1 through DVX's rs232 library -> DOSBox nullmodem # dvx COM1 through DVX's rs232 library -> DOSBox nullmodem
# win32 COM1 through the Win32 comms calls -> Wine -> a pty # win32 COM1 through the Win32 comms calls -> Wine -> a pty
# win16 COM1 through Windows 3.11's OpenComm -> KPCOMM.DRV # win16 COM1 through Windows 3.11's OpenComm -> KPCOMM.DRV
# -> DOSBox nullmodem # -> DOSBox nullmodem
# sdl3 termios -> a pty; the close puts back the line's
# speed and leaves HUPCL off
# haiku, haiku32 termios -> COM2 in the VM -> QEMU TCP (BeOS
# R5's VM has no serial ports)
# mouse x68000: deltas, clamps and buttons through the IOCS mouse # mouse x68000: deltas, clamps and buttons through the IOCS mouse
# (and with serial streaming both ways on the same SCC) # (and with serial streaming both ways on the same SCC)
# save iigs: a SAVES/ blob written and read back through GS/OS # save iigs: a SAVES/ blob written and read back through GS/OS
@ -248,6 +255,7 @@ visual ste palette scripts/verify-atarist-palette.sh ste megaste
visual amiga palette scripts/verify-amiga-palette.sh visual amiga palette scripts/verify-amiga-palette.sh
visual amiga a1200pal JOEY_AMIGA_MODEL=A1200 scripts/verify-amiga-palette.sh visual amiga a1200pal JOEY_AMIGA_MODEL=A1200 scripts/verify-amiga-palette.sh
visual amiga a4000pal JOEY_AMIGA_MODEL=A4000 scripts/verify-amiga-palette.sh visual amiga a4000pal JOEY_AMIGA_MODEL=A4000 scripts/verify-amiga-palette.sh
visual amiga fastram scripts/verify-amiga-fastram.sh
tilebank iigs tilebank make -s -f make/iigs.mk iigs-verify-tilebank || exit 1 tilebank iigs tilebank make -s -f make/iigs.mk iigs-verify-tilebank || exit 1
tilebank x68000 tilebank scripts/verify-x68000-tilebank.sh tilebank x68000 tilebank scripts/verify-x68000-tilebank.sh
tilebank atarist tilebank scripts/verify-atarist-tilebank.sh tilebank atarist tilebank scripts/verify-atarist-tilebank.sh
@ -315,11 +323,15 @@ serial host serial scripts/check-serial.sh
serial iigs serial scripts/verify-iigs-serial.sh serial iigs serial scripts/verify-iigs-serial.sh
serial x68000 serial scripts/verify-x68000-serial.sh serial x68000 serial scripts/verify-x68000-serial.sh
serial atarist serial scripts/verify-atarist-serial.sh serial atarist serial scripts/verify-atarist-serial.sh
serial atarist slot scripts/verify-atarist-serial-slot.sh
serial amiga serial scripts/verify-amiga-serial.sh serial amiga serial scripts/verify-amiga-serial.sh
serial dos serial scripts/verify-dos-serial.sh serial dos serial scripts/verify-dos-serial.sh
serial dvx serial scripts/verify-dvx-serial.sh serial dvx serial scripts/verify-dvx-serial.sh
serial win32 serial scripts/verify-win32-serial.sh serial win32 serial scripts/verify-win32-serial.sh
serial win16 serial scripts/verify-win16-serial.sh serial win16 serial scripts/verify-win16-serial.sh
serial sdl3 serial scripts/verify-sdl3-serial.sh
serial haiku serial scripts/verify-be-serial.sh haiku
serial haiku32 serial scripts/verify-be-serial.sh haiku32
mouse x68000 mouse scripts/verify-x68000-mouse.sh mouse x68000 mouse scripts/verify-x68000-mouse.sh
mouse x68000 sermouse scripts/verify-x68000-serial-mouse.sh mouse x68000 sermouse scripts/verify-x68000-serial-mouse.sh
save iigs save make -s -f make/iigs.mk iigs-verify-save || exit 1 save iigs save make -s -f make/iigs.mk iigs-verify-save || exit 1

80
scripts/verify-amiga-fastram.sh Executable file
View file

@ -0,0 +1,80 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# verify-amiga-fastram.sh - a surface in Fast RAM draws and copies as one in
# chip RAM does.
#
# The Amiga puts a surface's planes in chip RAM, where the blitter that does
# its fills and copies can reach them; once chip RAM has run out, a surface
# other than the stage goes in Fast RAM, and its fills and copies run on the
# CPU (src/amiga/hal.c, jlpSurfaceAllocPortData). FASTRAM (tests/amiga/
# fastRam.c; make -f make/amiga.mk build/amiga/test/Fastram) runs under
# FS-UAE on an A500 with 2 MB of Fast RAM: it fills chip RAM with ballast,
# makes a surface, which must land in Fast RAM, and checks that the same
# drawing and copies on it and on a chip surface hash the same.
#
# Usage: scripts/verify-amiga-fastram.sh
# Env: TIMEOUT seconds to wait for the program (default 120)
# Exit: 0 PASS, 1 FAIL, 2 SKIP (FS-UAE or the Kickstart not installed)
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed FSUAE fsuae
source "$repo/scripts/lib/amigaGate.sh"
amigaMachine
timeout_s=${TIMEOUT:-120}
probe=$repo/build/amiga/test/Fastram
label=verify-amiga-fastram
make -s -f "$repo/make/amiga.mk" "$probe" || {
echo "$label: FAIL - building Fastram failed" >&2
exit 1
}
work=$(mktemp -d -t joeylib-amiga-fastram.XXXXXX)
mkdir -p "$work/s"
cp "$probe" "$work/"
printf 'stack 32768\n:Fastram\n' > "$work/s/startup-sequence"
display=:$((RANDOM % 400 + 1300))
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp > /dev/null 2>&1 &
xvfb_pid=$!
fs_pid=
cleanup() {
[[ -n $fs_pid ]] && joeyStop "$fs_pid"
kill "$xvfb_pid" 2>/dev/null || true
wait 2>/dev/null || true
rm -rf "$work"
}
trap cleanup EXIT
sleep 0.5
DISPLAY=$display "$FSUAE" "${AMIGA_MACHINE_ARGS[@]}" --fast_memory=2048 \
--hard_drive_0="$work" --hard_drive_0_label=JOEYLIB \
--automatic_input_grab=0 \
--fullscreen=0 --fade_out_duration=0 > "$work/fsuae.log" 2>&1 &
fs_pid=$!
log=$work/joeylog.txt
for ((waited = 0; waited < timeout_s; waited++)); do
if grep -q "FASTRAM: done" "$log" 2>/dev/null; then
break
fi
sleep 1
done
if ! grep -q "FASTRAM: done" "$log" 2>/dev/null; then
echo "$label: FAIL - the program did not finish within ${timeout_s}s" >&2
exit 1
fi
grep "FASTRAM:" "$log"
if ! grep -q "FASTRAM: fast surface chip=0" "$log"; then
echo "$label: FAIL - the surface made with chip RAM full did not land outside chip RAM"
exit 1
fi
if ! grep -q "FASTRAM: PASS" "$log"; then
echo "$label: FAIL - $(grep 'FASTRAM: ' "$log" | grep -v 'done\|surface chip' | head -3 | tr '\n' ';')"
exit 1
fi
echo "$label: PASS (a surface in Fast RAM, chip RAM full, draws and copies as a chip one, onto the stage too)"

View file

@ -6,19 +6,23 @@
# verify-amiga-palette.sh - the Amiga's palette and SCB changes through the # verify-amiga-palette.sh - the Amiga's palette and SCB changes through the
# copper: fast, and right on the screen. # copper: fast, and right on the screen.
# #
# The Amiga shows each SCB band's palette with a user copper list, built and # The Amiga shows each SCB band's palette from one of two band lists of its
# merged by graphics.library (MrgCop, LoadView, then a WaitTOF) -- about three # own, which the screen's user copper list (merged by graphics.library once)
# frames on an A500. A change that leaves every band where it was (a palette # jumps to: a present that changes palettes or the SCB writes the list not
# changing, or a band given another palette) patches the colour MOVEs of the # shown, and the VBL server switches to it with the planes (src/amiga/hal.c,
# installed list in place instead (src/amiga/hal.c, patchBandColors). The # bandListUpdate). Before, a band that moved rebuilt and merged the whole
# stage hashes cannot see any of this: they hash the surface, not the copper. # copper list (MrgCop, LoadView, then a WaitTOF) -- about three frames on an
# A500 -- and a palette change patched the shown list, a frame before its
# pixels. The stage hashes cannot see any of this: they hash the surface,
# not the copper.
# #
# PALPROBE (examples/palprobe; make -f make/amiga.mk build/amiga/test/Palprobe) # PALPROBE (examples/palprobe; make -f make/amiga.mk build/amiga/test/Palprobe)
# runs under FS-UAE on an A500. It logs how many palette changes, and SCB # runs under FS-UAE on an A500. It logs how many palette changes, and SCB
# changes, it presented a second; the palette rate must be at least # changes (bands moving), it presented a second, back to back: each waits
# MIN_PALETTE_RATE (one rebuild a change gave 16). Then it holds a screen of # for the VBL that shows the last, so both rates must be at least MIN_RATE,
# four bands after one palette change and one same-layout SCB change, and # nearly the frame rate (a rebuild a change gave 16). Then it holds a screen
# the screenshot's bands must be, top to bottom, red, green, yellow, red. # of four bands after one palette change and one same-layout SCB change,
# and the screenshot's bands must be, top to bottom, red, green, yellow, red.
# #
# Usage: scripts/verify-amiga-palette.sh # Usage: scripts/verify-amiga-palette.sh
# Env: TIMEOUT seconds to wait for the probe (default 120) # Env: TIMEOUT seconds to wait for the probe (default 120)
@ -33,9 +37,9 @@ amigaMachine
timeout_s=${TIMEOUT:-120} timeout_s=${TIMEOUT:-120}
probe=$repo/build/amiga/test/Palprobe probe=$repo/build/amiga/test/Palprobe
label=verify-amiga-palette label=verify-amiga-palette
# Palette changes presented a second: 16 when every change rebuilt the # Changes presented a second, back to back: one a frame is 50 on the PAL
# copper list, ~990 patching it in place (A500, 2026-10-06). # A500 (16 when every change rebuilt the copper list).
MIN_PALETTE_RATE=500 MIN_RATE=45
make -s -f "$repo/make/amiga.mk" "$probe" || { make -s -f "$repo/make/amiga.mk" "$probe" || {
echo "$label: FAIL - building Palprobe failed" >&2 echo "$label: FAIL - building Palprobe failed" >&2
@ -81,20 +85,20 @@ sleep 2
DISPLAY=$display import -window root "$work/screen.png" DISPLAY=$display import -window root "$work/screen.png"
grep "PALPROBE:" "$log" grep "PALPROBE:" "$log"
python3 - "$work/screen.png" "$log" "$MIN_PALETTE_RATE" "$label" <<'PY' python3 - "$work/screen.png" "$log" "$MIN_RATE" "$label" <<'PY'
import re import re
import sys import sys
from PIL import Image from PIL import Image
png, log, minRate, label = sys.argv[1], sys.argv[2], int(sys.argv[3]), sys.argv[4] png, log, minRate, label = sys.argv[1], sys.argv[2], int(sys.argv[3]), sys.argv[4]
rate = None rates = {}
for line in open(log, encoding="ascii", errors="replace"): for line in open(log, encoding="ascii", errors="replace"):
m = re.search(r"PALPROBE: palette\+present: \d+ in \d+ ms = (\d+)/s", line) m = re.search(r"PALPROBE: (palette|scb)\+present: \d+ in \d+ ms = (\d+)/s", line)
if m: if m:
rate = int(m.group(1)) rates[m.group(1)] = int(m.group(2))
if rate is None: if sorted(rates) != ["palette", "scb"]:
print(f"{label}: FAIL - no palette rate in the log") print(f"{label}: FAIL - the log does not have both rates ({rates})")
sys.exit(1) sys.exit(1)
# The bands down the middle column, top to bottom, black (the border) # The bands down the middle column, top to bottom, black (the border)
@ -112,12 +116,13 @@ for y in range(h):
bands.append(name) bands.append(name)
want = ["red", "green", "yellow", "red"] want = ["red", "green", "yellow", "red"]
fails = [] fails = []
if rate < minRate: for kind in ("palette", "scb"):
fails.append(f"palette changes presented {rate}/s, want at least {minRate}") if rates[kind] < minRate:
fails.append(f"{kind} changes presented {rates[kind]}/s, want at least {minRate}")
if bands != want: if bands != want:
fails.append(f"bands {bands}, want {want}") fails.append(f"bands {bands}, want {want}")
if fails: if fails:
print(f"{label}: FAIL - " + "; ".join(fails)) print(f"{label}: FAIL - " + "; ".join(fails))
sys.exit(1) sys.exit(1)
print(f"{label}: PASS (palette changes {rate}/s; bands {', '.join(want)} after a palette change and a same-layout SCB change)") print(f"{label}: PASS (palette changes {rates['palette']}/s, SCB changes {rates['scb']}/s; bands {', '.join(want)} after a palette change and a same-layout SCB change)")
PY PY

View file

@ -10,9 +10,12 @@
# build refuses a bank baked for any other target. # build refuses a bank baked for any other target.
# #
# Boots an A500 (real Kickstart) in FS-UAE under Xvfb from a scratch directory # Boots an A500 (real Kickstart) in FS-UAE under Xvfb from a scratch directory
# hard drive whose startup-sequence runs TileBank (examples/tilebank), with # hard drive whose startup-sequence runs GAME/TileBank (examples/tilebank)
# the DATA/tilebank make/amiga.mk staged from the host gate's fixture beside # from the drive's root, with the DATA/tilebank make/amiga.mk staged from the
# it. The program loads its own bake through jlTileBankLoadToSurface and # host gate's fixture beside it in GAME -- so it must find its DATA/ through
# PROGDIR:, not the current directory, and log there too
# (src/core/fileRoot.h). The program loads its own bake through
# jlTileBankLoadToSurface and
# through jlTileBankLoad + jlTilePaste, checks every pixel of both against the # through jlTileBankLoad + jlTilePaste, checks every pixel of both against the
# PNG, hashes both, and tries every other target's bake; its TILEBANK line in # PNG, hashes both, and tries every other target's bake; its TILEBANK line in
# joeylog.txt must say PASS with both hashes equal to the host gate's # joeylog.txt must say PASS with both hashes equal to the host gate's
@ -51,10 +54,10 @@ cleanup() {
} }
trap cleanup EXIT trap cleanup EXIT
mkdir -p "$work/hd/s" "$work/hd/DATA" mkdir -p "$work/hd/s" "$work/hd/GAME/DATA"
cp "$binDir/TileBank" "$work/hd/" cp "$binDir/TileBank" "$work/hd/GAME/"
cp -r "$dataDir" "$work/hd/DATA/tilebank" cp -r "$dataDir" "$work/hd/GAME/DATA/tilebank"
echo ":TileBank" > "$work/hd/s/startup-sequence" echo ":GAME/TileBank" > "$work/hd/s/startup-sequence"
display=":$((RANDOM % 400 + 1300))" display=":$((RANDOM % 400 + 1300))"
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp > /dev/null 2>&1 & Xvfb "$display" -screen 0 800x600x24 -nolisten tcp > /dev/null 2>&1 &
@ -73,7 +76,7 @@ DISPLAY="$display" timeout -s KILL "$wall" "$FSUAE" \
guard=$! guard=$!
fsuae=$(joeyChild "$guard") fsuae=$(joeyChild "$guard")
log=$(tileBankAwaitLine "$work/hd" "$lineSeconds" "$guard") log=$(tileBankAwaitLine "$work/hd/GAME" "$lineSeconds" "$guard")
joeyStop "$fsuae" "$guard" 2> /dev/null joeyStop "$fsuae" "$guard" 2> /dev/null
fsuae="" fsuae=""
guard="" guard=""

View file

@ -53,12 +53,13 @@
# being started and every sample it plays, so the STe checks are: # being started and every sample it plays, so the STe checks are:
# #
# 1. The tracker is alive (as above). # 1. The tracker is alive (as above).
# 2. The DMA chip ran, as the HAL programs it: mono at 12517 Hz, play # 2. The DMA chip ran, as the HAL programs it: stereo at 12517 Hz, play
# + repeat. # + repeat.
# 3. It played the SFX: each run pulls a stretch of non-silent samples # 3. It played the SFX: each run pulls a stretch of non-silent samples
# from the mix buffer, near full scale. A chip looping over silence # from the mix buffer, near half scale (the mix's unit for effects).
# (a mix that never reached the buffer, or unsigned samples left # A chip looping over silence (a mix that never reached the buffer,
# unconverted, which play as a DC step and not a wave) fails here. # or unsigned samples left unconverted, which play as a DC step and
# not a wave) fails here.
# 4. No PWM pump: reg 8 is never written at the pump's rate. The YM is # 4. No PWM pump: reg 8 is never written at the pump's rate. The YM is
# not a DAC on this machine. # not a DAC on this machine.
# 5. Channel A is the tracker's while the chip plays: voice 0's period # 5. Channel A is the tracker's while the chip plays: voice 0's period
@ -215,13 +216,13 @@ REG_MIXER = 7
TONE_B_REGS = (2, 3) # channel B period == the tracker's own voice TONE_B_REGS = (2, 3) # channel B period == the tracker's own voice
VOICE_A_REGS = (0, 1, 8) # channel A period + volume == the tracker's voice 0 VOICE_A_REGS = (0, 1, 8) # channel A period + volume == the tracker's voice 0
# STe DMA sound: "DMA snd mode write: 0x81 mode=mono freq=12517 ... pc=22eec" # STe DMA sound: "DMA snd mode write: 0x01 mode=stereo freq=12517 ... pc=22eec"
DMA_WRITE = re.compile(r'^DMA snd (?:mode|control) write: 0x([0-9a-f]+) .*pc=([0-9a-f]+)') DMA_WRITE = re.compile(r'^DMA snd (?:mode|control) write: 0x([0-9a-f]+) .*pc=([0-9a-f]+)')
DMA_MODE_MONO_12K = 0x81 # bit 7 mono, rate 1 = 12517 Hz DMA_MODE_STEREO_12K = 0x01 # bit 7 clear stereo, rate 1 = 12517 Hz
DMA_CTRL_PLAY_LOOP = 0x03 # play + repeat DMA_CTRL_PLAY_LOOP = 0x03 # play + repeat
DMA_UNCONVERTED_SILENCE = -128 # $80, the mixer's unsigned silence, read as signed DMA_UNCONVERTED_SILENCE = -128 # $80, the mixer's unsigned silence, read as signed
SFX_SAMPLES_MIN = 100 # test.sfx is ~390 non-silent samples at 12517 Hz SFX_SAMPLES_MIN = 100 # test.sfx is ~390 non-silent samples at 12517 Hz
SFX_PEAK_MIN = 32 # it peaks near full scale (122 measured) SFX_PEAK_MIN = 32 # it peaks near half scale, the mix's unit (61 measured)
UNSIGNED_PCT_MAX = 5.0 UNSIGNED_PCT_MAX = 5.0
@ -362,9 +363,9 @@ def gradeDma(summary):
if not runs: if not runs:
fails.append("the DMA sound chip never started: jlAudioPlaySfx did not reach it, so " fails.append("the DMA sound chip never started: jlAudioPlaySfx did not reach it, so "
"every digital sound on the STe is silent") "every digital sound on the STe is silent")
if not modes or any(m != DMA_MODE_MONO_12K for m in modes): if not modes or any(m != DMA_MODE_STEREO_12K for m in modes):
fails.append(f"DMA mode writes {[hex(m) for m in modes]}, want {hex(DMA_MODE_MONO_12K)} " fails.append(f"DMA mode writes {[hex(m) for m in modes]}, want {hex(DMA_MODE_STEREO_12K)} "
f"(mono, 12517 Hz) -- the mix is made for that rate") f"(stereo, 12517 Hz) -- the mix is made for that rate and layout")
if DMA_CTRL_PLAY_LOOP not in controls: if DMA_CTRL_PLAY_LOOP not in controls:
fails.append(f"DMA control writes {[hex(c) for c in controls]} never set play + repeat " fails.append(f"DMA control writes {[hex(c) for c in controls]} never set play + repeat "
f"({hex(DMA_CTRL_PLAY_LOOP)}), so the chip cannot loop over the mix buffer") f"({hex(DMA_CTRL_PLAY_LOOP)}), so the chip cannot loop over the mix buffer")

View file

@ -27,7 +27,11 @@
# #
# Then KEYS again on German EmuTOS, whose Alt layer types @ [ ] on Alt with # Then KEYS again on German EmuTOS, whose Alt layer types @ [ ] on Alt with
# the keys U-umlaut, O-umlaut, and A-umlaut, and \ { } with Shift too; Alt # the keys U-umlaut, O-umlaut, and A-umlaut, and \ { } with Shift too; Alt
# with a letter types nothing, and a plain key goes on typing. # with a letter types nothing, and a plain key goes on typing. And once more
# with the OS header made to say TOS 1.04 instead of EmuTOS (Hatari's
# debugger writes it before the boot): then the layer must come from
# src/atarist/stAltLayer.c's German table, found by the header's country,
# as on a German ST's own TOS, which keeps its layer out of Keytbl.
# #
# Usage: scripts/verify-atarist-input.sh # Usage: scripts/verify-atarist-input.sh
# Requires: hatari, the EmuTOS images (US and German), # Requires: hatari, the EmuTOS images (US and German),
@ -64,24 +68,28 @@ send() {
} }
# hatariBoot TOS -- KEYS in Hatari on that TOS image; sets hatari_pid and # hatariBoot TOS [HATARI-ARG...] -- KEYS in Hatari on that TOS image; sets
# rowAddr, KEYS's verification row in ST RAM, once its A5A5 sentinel is up. # hatari_pid and rowAddr, KEYS's verification row in ST RAM, once its A5A5
# sentinel is up.
# The video base is read from the shifter regs each attempt (planar # The video base is read from the shifter regs each attempt (planar
# sentinel = plane words 5000 A000 5000 A000). # sentinel = plane words 5000 A000 5000 A000).
hatariBoot() { hatariBoot() {
local tos="$1"
local i local i
local base local base
local addr local addr
local row local row
shift
joeyStop "${hatari_pid:-}" joeyStop "${hatari_pid:-}"
rm -f "$work/cmd.fifo" rm -f "$work/cmd.fifo"
(cd "$work" && exec env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy "$HATARI" \ (cd "$work" && exec env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy "$HATARI" \
--tos "$1" \ --tos "$tos" \
--harddrive "$bin_dir" \ --harddrive "$bin_dir" \
--gemdos-drive C \ --gemdos-drive C \
--auto 'C:\KEYS.PRG' \ --auto 'C:\KEYS.PRG' \
--cmd-fifo "$work/cmd.fifo" \ --cmd-fifo "$work/cmd.fifo" \
--fast-forward on \ --fast-forward on \
"$@" \
> "$work/hatari.log" 2>&1) & > "$work/hatari.log" 2>&1) &
hatari_pid=$! hatari_pid=$!
for i in $(seq 1 30); do for i in $(seq 1 30); do
@ -217,16 +225,36 @@ if [ -n "$KEYS_FAILS" ]; then
exit 1 exit 1
fi fi
# German EmuTOS's Alt layer. # altLayerType -- the Alt layer's keys, as deTarget says.
altLayerType() {
send "hatari-event keydown 56"
for c in 26 39 40; do send "hatari-event keypress $c"; done
send "hatari-event keydown 42"
for c in 26 39 40; do send "hatari-event keypress $c"; done
send "hatari-event keyup 42"
send "hatari-event keypress 30"
send "hatari-event keyup 56"
send "hatari-event keypress 2"
}
# German EmuTOS's Alt layer, from its Keytbl tables.
hatariBoot "$EMUTOS_DE_IMG" hatariBoot "$EMUTOS_DE_IMG"
send "hatari-event keydown 56" altLayerType
for c in 26 39 40; do send "hatari-event keypress $c"; done
send "hatari-event keydown 42"
for c in 26 39 40; do send "hatari-event keypress $c"; done
send "hatari-event keyup 42"
send "hatari-event keypress 30"
send "hatari-event keyup 56"
send "hatari-event keypress 2"
rowCheck "$deTarget" || exit 1 rowCheck "$deTarget" || exit 1
echo "$label: PASS (typed \"$target\", all ${#target} chars arrived via jlInputGetChar; $KEYS_STEPS named-key steps: the keypad's arrows and Enter, Up held with its second key; German EmuTOS's Alt layer typed \"$deTarget\")" # The same as TOS 1.04 would show it: "XXXX" over the header's "ETOS"
# (offset 0x2C) and version 0x0104 (offset 2), in the 512 KB ROM at
# 0xE00000.
printf 'w $e0002c $58 $58 $58 $58\nw $e00002 $01 $04\n' > "$work/tos104.dbg"
hatariBoot "$EMUTOS_DE_IMG" --parse "$work/tos104.dbg"
send 'hatari-debug m $e0002c 4'
sleep 1
if ! grep -qi "^00E0002C: 58 58 58 58" "$work/hatari.log"; then
echo "$label: FAIL - the OS header still says EmuTOS: $(grep -i '^00E0002C:' "$work/hatari.log" | tail -1)" >&2
exit 1
fi
altLayerType
rowCheck "$deTarget" || exit 1
echo "$label: PASS (typed \"$target\", all ${#target} chars arrived via jlInputGetChar; $KEYS_STEPS named-key steps: the keypad's arrows and Enter, Up held with its second key; German EmuTOS's Alt layer typed \"$deTarget\", from its tables and as TOS 1.04 from the built-in German one)"

View file

@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# verify-atarist-serial-slot.sh - JL_SERIAL_SLOT opens a Mega STe's other
# serial ports through Bconmap.
#
# A Mega STe, TT, or Falcon has serial ports besides the Modem port (its
# SCC's two, the TT its second MFP's), and TOS 2 and EmuTOS map the AUX
# device onto any of them with XBIOS Bconmap. JL_SERIAL_SLOT unit n maps
# AUX to Bconmap device 5 + n while the port is open (src/atarist/serial.c);
# unit 2 is SCC channel B (Modem 2). SERSLOT (examples/sertest built with
# -DSERTEST_DEVICE=JL_SERIAL_SLOT -DSERTEST_UNIT=2u) opens it and sends its
# banner. Hatari writes what SCC channel B sends to a file (--scc-b-out; it
# has no SCC input, so this is the sending side only) and what the Modem
# port sends to another (--rs232-out, which Hatari opens only along with an
# input, here an empty file):
#
# 1. Mega STe: the banner comes out of SCC channel B, and nothing out of
# the Modem port.
# 2. A plain ST, whose one port is the Modem port: unit 2 does not open,
# so nothing comes out at all.
#
# Usage: scripts/verify-atarist-serial-slot.sh
# Requires: hatari, EmuTOS (install.sh install emulator:atarist); SERSLOT.PRG
# (built here: make -f make/atarist.mk build/atarist/test/SERSLOT.PRG)
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed HATARI hatari
joeyNeed EMUTOS_IMG emutos
label=verify-atarist-serial-slot
prog=$repo/build/atarist/test/SERSLOT.PRG
banner="JLSERTEST-READY"
wall=90 # hard cap on Hatari, whatever happens to this script
runVbls=900 # EmuTOS boot + SERSLOT open and banner: ~600 frames
make -s -f "$repo/make/atarist.mk" "$prog" || {
echo "$label: FAIL - building SERSLOT failed" >&2
exit 1
}
work=$(mktemp -d -t joeylib-st-serslot.XXXXXX)
trap 'rm -rf "$work"' EXIT
# run MACHINE -- SERSLOT on MACHINE; SCC channel B's output in
# $work/MACHINE.scc, the Modem port's in $work/MACHINE.mfp.
run() {
mkdir -p "$work/$1"
cp "$prog" "$work/$1/"
: > "$work/$1.scc"
: > "$work/$1.mfp"
: > "$work/empty.in"
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy timeout -s KILL "$wall" "$HATARI" \
--machine "$1" --tos "$EMUTOS_IMG" --harddrive "$work/$1" --gemdos-drive C \
--drive-a off --drive-b off --sound off --fast-forward on --run-vbls "$runVbls" \
--scc-b-out "$work/$1.scc" --rs232-in "$work/empty.in" --rs232-out "$work/$1.mfp" \
--auto 'C:\SERSLOT.PRG' > "$work/$1.log" 2>&1
}
fails=""
run megaste
grep -q "$banner" "$work/megaste.scc" || fails="${fails}Mega STe: no banner out of SCC channel B; "
[ -s "$work/megaste.mfp" ] && fails="${fails}Mega STe: the Modem port sent $(wc -c < "$work/megaste.mfp") bytes; "
run st
[ -s "$work/st.scc" ] || [ -s "$work/st.mfp" ] && fails="${fails}plain ST: unit 2 opened something; "
if [ -n "$fails" ]; then
echo "$label: FAIL - ${fails%; }"
exit 1
fi
echo "$label: PASS (Mega STe: JL_SERIAL_SLOT unit 2 sends on SCC channel B, not the Modem port; plain ST: no unit 2)"

View file

@ -32,6 +32,10 @@
# hangs, AUDPAR with it # hangs, AUDPAR with it
# #
# PORT_STEREO lists the ports with stereo output, whose pans are graded. # PORT_STEREO lists the ports with stereo output, whose pans are graded.
# DOS is stereo on DOSBox's own Sound Blaster, an SB16; AUDPAR_DOS_SBTYPE
# runs it on another (sbpro2, sb2: the mono cards), graded in mono. Not
# sbpro1: DOSBox makes its two OPL2s the halves of one OPL3, whose rhythm
# mode is in the first half only, so its noise plays on the left alone.
# Win16 mixes in mono; beos is listed for the Be backend it shares with # Win16 mixes in mono; beos is listed for the Be backend it shares with
# Haiku, whose stereo is graded there. The IIgs is graded in mono, as a # Haiku, whose stereo is graded there. The IIgs is graded in mono, as a
# stock IIgs plays (its output sums every DOC channel): its stereo is a # stock IIgs plays (its output sums every DOC channel): its stereo is a
@ -45,11 +49,14 @@
# take a channel each side and are graded). # take a channel each side and are graded).
# A port whose chip cannot land every 2 dB step gets a level tolerance of # A port whose chip cannot land every 2 dB step gets a level tolerance of
# its own: the YM2149's sixteen levels are about 3.5 dB apart, so # its own: the YM2149's sixteen levels are about 3.5 dB apart, so
# the nearest one can be 1.75 dB off. # the nearest one can be 1.75 dB off, and on the STe, whose voices start at
# level 14 (the mix's level for them), 1.95: level 14 to 13 is 3.9 dB.
# #
# Usage: scripts/verify-audio-conformance.sh PORT # Usage: scripts/verify-audio-conformance.sh PORT
# Env: AUDPAR_KEEP set to keep the recording and the log in # Env: AUDPAR_KEEP set to keep the recording and the log in
# build/<port>/audpar/ # build/<port>/audpar/
# AUDPAR_DOS_SBTYPE DOSBox's sbtype for the DOS run (default its
# own, sb16)
# Exit: 0 PASS, 1 FAIL, 2 cannot run # Exit: 0 PASS, 1 FAIL, 2 cannot run
set -uo pipefail set -uo pipefail
@ -58,10 +65,15 @@ source "$repo/scripts/lib/joeyEnv.sh"
port=${1:-} port=${1:-}
label="verify-audio-conformance ($port)" label="verify-audio-conformance ($port)"
grader=$repo/tools/gradeAudioConformance.py grader=$repo/tools/gradeAudioConformance.py
PORT_STEREO="amiga dvx sdl3 web win32 psp haiku haiku32 beos" PORT_STEREO="amiga dos dvx ste sdl3 web win32 psp haiku haiku32 beos"
DOS_SBTYPE=${AUDPAR_DOS_SBTYPE:-}
if [ -n "$DOS_SBTYPE" ] && [ "$DOS_SBTYPE" != sb16 ]; then
PORT_STEREO=${PORT_STEREO/ dos / }
label="verify-audio-conformance ($port, $DOS_SBTYPE)"
fi
RUN_SECONDS=120 RUN_SECONDS=120
X68K_RUN_SECONDS=110 # emulated: Human68k's boot and AUDPAR's run X68K_RUN_SECONDS=110 # emulated: Human68k's boot and AUDPAR's run
IIGS_RUN_FRAMES=7200 # emulated, 60 a second: GS/OS, the Finder and AUDPAR IIGS_RUN_FRAMES=7800 # emulated, 60 a second: GS/OS, the Finder and AUDPAR
HAIKU_BOOT_SECONDS=120 # a Haiku VM booting until its job runner starts HAIKU_BOOT_SECONDS=120 # a Haiku VM booting until its job runner starts
RECORD_RATE=44100 RECORD_RATE=44100
QEMU_WAV_HEADER=44 # bytes: QEMU's wav audio driver's header, its sizes left 0 QEMU_WAV_HEADER=44 # bytes: QEMU's wav audio driver's header, its sizes left 0
@ -89,6 +101,10 @@ cleanup() {
if [ -n "$sinkModule" ]; then if [ -n "$sinkModule" ]; then
pulseUnloadSink "$sinkModule" pulseUnloadSink "$sinkModule"
fi fi
# The web run's own sink (webSoundSink), should it have failed early.
if [ -n "${WEB_SINK_MODULE:-}" ]; then
pulseUnloadSink "$WEB_SINK_MODULE"
fi
if [ -n "${AUDPAR_KEEP:-}" ] && [ -n "$port" ]; then if [ -n "${AUDPAR_KEEP:-}" ] && [ -n "$port" ]; then
mkdir -p "$repo/build/$port/audpar" mkdir -p "$repo/build/$port/audpar"
cp "$work"/out.wav "$work"/log.txt "$repo/build/$port/audpar/" 2>/dev/null cp "$work"/out.wav "$work"/log.txt "$repo/build/$port/audpar/" 2>/dev/null
@ -192,7 +208,7 @@ runDos() {
xvfbStart "$display" xvfbStart "$display"
pulseStart pulseStart
XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" PULSE_SINK="$sinkName" SDL_AUDIODRIVER=pulse \ XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" PULSE_SINK="$sinkName" SDL_AUDIODRIVER=pulse \
"$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" --conf "$repo/scripts/dosbox-386sx16.conf" \ "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" --conf "$repo/scripts/dosbox-386sx16.conf" ${DOS_SBTYPE:+--set "sbtype=$DOS_SBTYPE"} \
-c "MOUNT C $dir" -c "C:" -c "AUDPAR.EXE" -c "EXIT" > "$work/dosbox.log" 2>&1 & -c "MOUNT C $dir" -c "C:" -c "AUDPAR.EXE" -c "EXIT" > "$work/dosbox.log" 2>&1 &
dosPid=$! dosPid=$!
pids+=("$dosPid") pids+=("$dosPid")
@ -437,6 +453,11 @@ runWeb() {
make -s -C "$repo" -f make/web.mk program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/web.mk program-AUDPAR)" make -s -C "$repo" -f make/web.mk program-AUDPAR > "$work/build.log" 2>&1 || fail "the build failed (make -f make/web.mk program-AUDPAR)"
webServe "$repo/build/web/probe/AUDPAR" webServe "$repo/build/web/probe/AUDPAR"
pids+=("$WEB_SERVER_PID") pids+=("$WEB_SERVER_PID")
# Chromium plays to a null sink of its own, whose clock paces the page's
# sound: muted, its audio runs on a fake clock that drifts off real time
# (the timeline stretched by up to 15%). The page's own buffers are what
# is recorded.
webSoundSink || cannot "no PulseAudio/PipeWire null sink for Chromium to play to"
webDrive AUDPAR.html --seconds "$RUN_SECONDS" --until " done" --audio "$work/out.wav" --log "$work/log.txt" > "$work/web.log" 2>&1 || fail "AUDPAR did not finish in $RUN_SECONDS s ($(tail -1 "$work/web.log"))" webDrive AUDPAR.html --seconds "$RUN_SECONDS" --until " done" --audio "$work/out.wav" --log "$work/log.txt" > "$work/web.log" 2>&1 || fail "AUDPAR did not finish in $RUN_SECONDS s ($(tail -1 "$work/web.log"))"
webStop webStop
} }
@ -527,7 +548,8 @@ case " $PORT_STEREO " in
*" $port "*) flags+=(--stereo) ;; *" $port "*) flags+=(--stereo) ;;
esac esac
case "$port" in case "$port" in
atarist|ste) flags+=(--level-tolerance 1.75) ;; atarist) flags+=(--level-tolerance 1.75) ;;
ste) flags+=(--level-tolerance 1.95) ;;
amiga) flags+=(--skip "tone centred" "voice 0 centred" "voice 1 centred" "voice 2 centred") ;; amiga) flags+=(--skip "tone centred" "voice 0 centred" "voice 1 centred" "voice 2 centred") ;;
esac esac
python3 "$grader" "$port" "$work/log.txt" "$work/out.wav" "${flags[@]}" > "$work/grade.txt" python3 "$grader" "$port" "$work/log.txt" "$work/out.wav" "${flags[@]}" > "$work/grade.txt"

85
scripts/verify-be-serial.sh Executable file
View file

@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# verify-be-serial.sh - both-directions RS-232 gate for the Be API ports.
#
# Runs SERTEST (examples/sertest: banner, then echo) in the port's VM
# (scripts/lib/beGate.sh) with the machine's second serial port joined to
# the host peer (scripts/lib/serialLoopPeer.py) over TCP: the peer listens,
# QEMU's COM2 connects to it, and JOEYLIB_SERIAL names COM2's device to the
# program (Haiku's /dev/ports/pc_serial1; COM1 is the VM's console). The
# peer waits for the banner, sends a probe and expects it echoed, so the
# chain is JoeyLib's POSIX serial HAL (src/hosted/posix) -> the system's
# termios -> its serial driver -> QEMU -> the host. QEMU's TCP serial carries no modem lines: DTR and the close's
# settings are gated on SDL3 (verify-sdl3-serial.sh).
#
# BeOS R5 SKIPs: its serial driver finds the ports through the ISA plug and
# play bus, which the VM goes without (installer/lib/beos.sh: it crashes R5
# in the fail-safe video mode), so the machine has none.
#
# Usage: scripts/verify-be-serial.sh [haiku|haiku32|beos]
# Requires: the port's VM (component haikuVm, haikuVm32 or beosVm), python3;
# build/PORT/bin/SERTEST (make PORT).
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
source "$repo/scripts/lib/beGate.sh"
port=${1:-haiku}
label="verify-be-serial ($port)"
if [ "$port" = beos ]; then
echo "$label: SKIP (the BeOS R5 VM has no serial ports: R5 finds them through the ISA plug and play bus, which the VM goes without)"
exit 2
fi
beNeed "$port"
program=$(joeyExampleProgram sertest "$port")
[ -x "$program" ] || { echo "$label: build SERTEST first (make $port)" >&2; exit 2; }
bootSeconds=150
runSeconds=60 # SERTEST echoes until the job ends it
peerPortMin=40000
peerPortSpan=20000
device=/dev/ports/pc_serial1
work=$(mktemp -d -t joeylib-be-serial.XXXXXX)
peerPid=""
cleanup() {
beStop
joeyStop "$peerPid"
rm -rf "$work"
}
trap cleanup EXIT
mkdir -p "$work/job/SERTEST"
cp "$program" "$work/job/SERTEST/SERTEST"
{
printf 'ls /dev/ports > ports.txt\n'
printf '(\n'
beJobScript SERTEST SERTEST "" "$runSeconds" JOEYLIB_SERIAL="$device"
printf ')\n'
} > "$work/job/job.sh"
beJobDisk "$work/job.img" "$work/job"
peerPort=$((peerPortMin + RANDOM % peerPortSpan))
python3 "$repo/scripts/lib/serialLoopPeer.py" tcp-server "$peerPort" "$((bootSeconds + runSeconds))" > "$work/peer.log" 2>&1 &
peerPid=$!
sleep 1
beStart "$work" "$work/job.img" -serial "tcp:127.0.0.1:$peerPort"
if ! beWaitJob "$work" "$bootSeconds"; then
echo "$label: FAIL - the VM did not start the job within ${bootSeconds} s" >&2
exit 1
fi
wait "$peerPid"
peerStatus=$?
peerPid=""
beWaitExit "$work" "$((runSeconds + 60))" || true
beJobGet "$work/job.img" ports.txt "$work/ports.txt" || true
cat "$work/peer.log"
if [ "$peerStatus" -eq 0 ]; then
echo "$label: PASS (banner out, probe echoed through $device)"
else
echo "$label: FAIL - the probe did not come back through $device; the machine's ports: $(tr '\n' ' ' < "$work/ports.txt" 2> /dev/null)" >&2
exit 1
fi

View file

@ -58,7 +58,7 @@ boot() {
DOSBOX_JOY_SHOTS="$dir/shots" DOSBOX_JOY_SHOTS="$dir/shots"
XDG_CONFIG_HOME="$dir/xdg" DISPLAY="$DOSBOX_JOY_DISPLAY" SDL_AUDIODRIVER=dummy \ XDG_CONFIG_HOME="$dir/xdg" DISPLAY="$DOSBOX_JOY_DISPLAY" SDL_AUDIODRIVER=dummy \
timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
--conf "$repo/scripts/dosbox-386sx16.conf" --set "joysticktype=$2" \ --conf "$repo/scripts/dosbox-386sx16.conf" --set "joysticktype=$2" "${DOSBOX_JOY_SET_ARGS[@]}" \
--set "mapperfile=$dir/mapper.txt" --set "capture_dir=$dir/shots" \ --set "mapperfile=$dir/mapper.txt" --set "capture_dir=$dir/shots" \
-c "MOUNT C \"$(dirname "$program")\"" -c "C:" -c "$(basename "$program")" -c "EXIT" \ -c "MOUNT C \"$(dirname "$program")\"" -c "C:" -c "$(basename "$program")" -c "EXIT" \
> "$dir/dosbox.log" 2>&1 & > "$dir/dosbox.log" 2>&1 &

View file

@ -10,7 +10,9 @@
# #
# Runs TILEBANK.EXE (examples/tilebank) in DOSBox Staging on the 386SX-16 # Runs TILEBANK.EXE (examples/tilebank) in DOSBox Staging on the 386SX-16
# reference config, from a scratch C: holding it and the DATA/tilebank # reference config, from a scratch C: holding it and the DATA/tilebank
# make/dos.mk staged from the host gate's fixture. The program loads its own # make/dos.mk staged from the host gate's fixture, both in C:\GAME, started
# from C:\ -- so it must find its DATA/ beside itself, not in the current
# directory, and log there too (src/core/fileRoot.h). The program loads its own
# bake through jlTileBankLoadToSurface and through jlTileBankLoad + # bake through jlTileBankLoadToSurface and through jlTileBankLoad +
# jlTilePaste, checks every pixel of both against the PNG, hashes both, and # jlTilePaste, checks every pixel of both against the PNG, hashes both, and
# tries every other target's bake; its TILEBANK line in JOEYLOG.TXT must say # tries every other target's bake; its TILEBANK line in JOEYLOG.TXT must say
@ -49,9 +51,9 @@ cleanup() {
} }
trap cleanup EXIT trap cleanup EXIT
mkdir -p "$work/c/DATA" "$work/xdg" mkdir -p "$work/c/GAME/DATA" "$work/xdg"
cp "$binDir/TILEBANK.EXE" "$work/c/" cp "$binDir/TILEBANK.EXE" "$work/c/GAME/"
cp -r "$dataDir" "$work/c/DATA/tilebank" cp -r "$dataDir" "$work/c/GAME/DATA/tilebank"
display=":$((RANDOM % 400 + 1300))" display=":$((RANDOM % 400 + 1300))"
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp > /dev/null 2>&1 & Xvfb "$display" -screen 0 800x600x24 -nolisten tcp > /dev/null 2>&1 &
@ -63,11 +65,11 @@ sleep 1
XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \ XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \
timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
--conf "$conf" \ --conf "$conf" \
-c "MOUNT C \"$work/c\"" -c "C:" -c "TILEBANK.EXE" > "$work/dosbox.log" 2>&1 & -c "MOUNT C \"$work/c\"" -c "C:" -c "GAME\\TILEBANK.EXE" > "$work/dosbox.log" 2>&1 &
guard=$! guard=$!
dosbox=$(joeyChild "$guard") dosbox=$(joeyChild "$guard")
log=$(tileBankAwaitLine "$work/c" "$lineSeconds" "$guard") log=$(tileBankAwaitLine "$work/c/GAME" "$lineSeconds" "$guard")
joeyStop "$dosbox" "$guard" 2> /dev/null joeyStop "$dosbox" "$guard" 2> /dev/null
dosbox="" dosbox=""
guard="" guard=""

View file

@ -14,20 +14,24 @@
# and asks the port after every present how many pixels differ from the # and asks the port after every present how many pixels differ from the
# stage (dvxPortProbeDisplayMismatch in src/dvx/hal.c): the window's content # stage (dvxPortProbeDisplayMismatch in src/dvx/hal.c): the window's content
# buffer in the desktop's own pixel format, every screen pixel of a stage # buffer in the desktop's own pixel format, every screen pixel of a stage
# pixel's scale x scale block, or the full-screen backbuffer and its # pixel's block (scale across; down, the rows the window's shape gives it:
# palette; each pixel derived from the stage's palette entries, not through # 4:3 stretches every fifth row one more, square pixels take scale each),
# the present's tables. Each run is scripts/bench-dvx.sh --probe on one # or the full-screen backbuffer and its palette; each pixel derived from the
# desktop (DVX.INI's video mode), about 8 s. Every count must be 0, every # stage's palette entries, not through the present's tables. Each run is
# pixel must have been compared (64000 x scale^2, plus the 256 palette # scripts/bench-dvx.sh --probe on one desktop (DVX.INI's video mode), about
# entries on the full screen), and DVX.LOG must show the program on the # 8 s, in 4:3 or (":square") with square pixels. Every count must be 0, every
# desktop asked for, at the scale asked for. # pixel of the window must have been compared (320 x 240 x scale^2 in 4:3,
# 320 x 200 x scale^2 square, plus the 256 palette entries on the full
# screen), and DVX.LOG must show the program on the desktop asked for, at
# the scale and in the shape asked for.
# #
# The default desktops cover each pixel size the present writes (1, 2 and # The default desktops cover each pixel size the present writes (1, 2 and
# 4 bytes) at each window scale (1x, 2x, 3x), the 8-bit desktop's nearest # 4 bytes) at each window scale (1x, 2x, 3x: 3x is square pixels only, as 4:3
# at 3x does not fit 1024x768), in both shapes, the 8-bit desktop's nearest
# colours and both 16-bit packings (5-5-5 and 5-6-5), then the full screen # colours and both 16-bit packings (5-5-5 and 5-6-5), then the full screen
# (320x200 at 8 bits, its palette the stage's 16). # (320x200 at 8 bits, its palette the stage's 16).
# #
# Usage: scripts/verify-dvx-display.sh [WxHxBPP:SCALE | full]... # Usage: scripts/verify-dvx-display.sh [WxHxBPP:SCALE[:square] | full]...
# Requires: DOSBox Staging, Xvfb, the dvx component; the probe build # Requires: DOSBox Staging, Xvfb, the dvx component; the probe build
# (make dvx builds it, into build/dvx/probe). # (make dvx builds it, into build/dvx/probe).
set -uo pipefail set -uo pipefail
@ -46,10 +50,11 @@ paletteEntries=256
probeLine="UBER-PROBE: dvxDisplayMismatch" probeLine="UBER-PROBE: dvxDisplayMismatch"
fullSpec=full fullSpec=full
benchSeconds=120 # one probe run, boot to done line benchSeconds=120 # one probe run, boot to done line
defaults=(640x480x8:1 800x600x8:2 1024x768x8:3 defaults=(640x480x8:1 800x600x8:2 1024x768x8:3:square
640x480x16:1 800x600x15:2 1024x768x16:3 640x480x16:1:square 800x600x15:2 1024x768x16:3:square
640x480x32:1 800x600x32:2 1024x768x32:3 640x480x32:1 800x600x32:2:square 1024x768x32:2
"$fullSpec") "$fullSpec")
shape43Height=240
specs=("$@") specs=("$@")
[ ${#specs[@]} -gt 0 ] || specs=("${defaults[@]}") [ ${#specs[@]} -gt 0 ] || specs=("${defaults[@]}")
@ -78,18 +83,28 @@ for spec in "${specs[@]}"; do
h=$DVX_VIDEO_H h=$DVX_VIDEO_H
bpp=$DVX_VIDEO_BPP bpp=$DVX_VIDEO_BPP
scale=1 scale=1
shape=4:3
rows=$((shape43Height * scale))
want=$((stagePixels + paletteEntries)) want=$((stagePixels + paletteEntries))
benchArgs+=(--full-screen) benchArgs+=(--full-screen)
else else
if ! [[ "$spec" =~ ^([0-9]+)x([0-9]+)x([0-9]+):([0-9]+)$ ]]; then if ! [[ "$spec" =~ ^([0-9]+)x([0-9]+)x([0-9]+):([0-9]+)(:square)?$ ]]; then
echo "verify-dvx-display: bad desktop \"$spec\" (WxHxBPP:SCALE or $fullSpec)" >&2 echo "verify-dvx-display: bad desktop \"$spec\" (WxHxBPP:SCALE[:square] or $fullSpec)" >&2
exit 2 exit 2
fi fi
w=${BASH_REMATCH[1]} w=${BASH_REMATCH[1]}
h=${BASH_REMATCH[2]} h=${BASH_REMATCH[2]}
bpp=${BASH_REMATCH[3]} bpp=${BASH_REMATCH[3]}
scale=${BASH_REMATCH[4]} scale=${BASH_REMATCH[4]}
want=$((stagePixels * scale * scale)) if [ -n "${BASH_REMATCH[5]}" ]; then
shape=square
rows=$((stageHeight * scale))
benchArgs+=(--square)
else
shape=4:3
rows=$((shape43Height * scale))
fi
want=$((stageWidth * scale * rows))
fi fi
DVX_VIDEO_W=$w DVX_VIDEO_H=$h DVX_VIDEO_BPP=$bpp TIMEOUT=$benchSeconds \ DVX_VIDEO_W=$w DVX_VIDEO_H=$h DVX_VIDEO_BPP=$bpp TIMEOUT=$benchSeconds \
"$repo/scripts/bench-dvx.sh" "${benchArgs[@]}" > /dev/null 2>&1 "$repo/scripts/bench-dvx.sh" "${benchArgs[@]}" > /dev/null 2>&1
@ -101,8 +116,8 @@ for spec in "${specs[@]}"; do
fi fi
counts=${row#*"$probeLine" } counts=${row#*"$probeLine" }
echo "verify-dvx-display: $spec: $counts" echo "verify-dvx-display: $spec: $counts"
if ! grep -qF "JoeyLib: Uber Probe: $((stageWidth * scale))x$((stageHeight * scale)) at ${scale}x on a ${w}x${h} ${bpp}-bit desktop" "$dvxLog"; then if ! grep -qF "JoeyLib: Uber Probe: $((stageWidth * scale))x$rows at ${scale}x, $shape, on a ${w}x${h} ${bpp}-bit desktop" "$dvxLog"; then
fail "$spec" "DVX.LOG does not show the window at ${scale}x on a ${w}x${h} ${bpp}-bit desktop: $(grep -F 'JoeyLib: Uber Probe:' "$dvxLog")" fail "$spec" "DVX.LOG does not show the window at ${scale}x, $shape, on a ${w}x${h} ${bpp}-bit desktop: $(grep -F 'JoeyLib: Uber Probe:' "$dvxLog")"
continue continue
fi fi
if [ "$spec" = "$fullSpec" ] && ! grep -qF "$DVX_FULL_SCREEN_LOG" "$dvxLog"; then if [ "$spec" = "$fullSpec" ] && ! grep -qF "$DVX_FULL_SCREEN_LOG" "$dvxLog"; then

View file

@ -22,10 +22,11 @@
# as hex to its joeylog.txt (in its settings folder), which must equal # as hex to its joeylog.txt (in its settings folder), which must equal
# the typed ASCII. # the typed ASCII.
# Each desktop given (default 640x480 at 16 bits, where the window is 1x) is # Each desktop given (default 640x480 at 16 bits, where the window is 1x) is
# a run of its own; DVX.LOG must show KEYS's window on that desktop at the # a run of its own, the window 4:3 or (":square") with square pixels;
# scale given. # DVX.LOG must show KEYS's window on that desktop at the scale and in the
# shape given.
# #
# Usage: scripts/verify-dvx-input.sh [WxHxBPP:SCALE]... # Usage: scripts/verify-dvx-input.sh [WxHxBPP:SCALE[:square]]...
# e.g. scripts/verify-dvx-input.sh 800x600x8:2 1024x768x32:3 # e.g. scripts/verify-dvx-input.sh 800x600x8:2 1024x768x32:3
# Requires: DOSBox Staging, Xvfb, xdotool, python3 + PIL, the dvx component; # Requires: DOSBox Staging, Xvfb, xdotool, python3 + PIL, the dvx component;
# the keys program (make dvx). # the keys program (make dvx).
@ -41,6 +42,7 @@ program=keys
target="192.168.1.10:6510" target="192.168.1.10:6510"
stageWidth=320 stageWidth=320
stageHeight=200 stageHeight=200
shape43Height=240 # a 4:3 window's rows a scale for the stage's 200
wall=180 # hard cap on DOSBox, whatever happens to this script wall=180 # hard cap on DOSBox, whatever happens to this script
readySeconds=60 # DVX boot until its shell reports ready readySeconds=60 # DVX boot until its shell reports ready
settleSeconds=3 # then KEYS opening its window and taking the focus settleSeconds=3 # then KEYS opening its window and taking the focus
@ -98,13 +100,15 @@ run() {
local h local h
local bpp local bpp
local scale local scale
local shape
local rows
local ready="" local ready=""
local waited local waited
local log local log
local png local png
if ! [[ "$spec" =~ ^([0-9]+)x([0-9]+)x([0-9]+):([0-9]+)$ ]]; then if ! [[ "$spec" =~ ^([0-9]+)x([0-9]+)x([0-9]+):([0-9]+)(:square)?$ ]]; then
echo "verify-dvx-input: bad desktop \"$spec\" (WxHxBPP:SCALE)" >&2 echo "verify-dvx-input: bad desktop \"$spec\" (WxHxBPP:SCALE[:square])" >&2
exit 2 exit 2
fi fi
w=${BASH_REMATCH[1]} w=${BASH_REMATCH[1]}
@ -112,6 +116,14 @@ run() {
bpp=${BASH_REMATCH[3]} bpp=${BASH_REMATCH[3]}
scale=${BASH_REMATCH[4]} scale=${BASH_REMATCH[4]}
DVX_VIDEO_W=$w DVX_VIDEO_H=$h DVX_VIDEO_BPP=$bpp dvxStage "$dir/c" "$(dvxAppDir "$program")" DVX_VIDEO_W=$w DVX_VIDEO_H=$h DVX_VIDEO_BPP=$bpp dvxStage "$dir/c" "$(dvxAppDir "$program")"
if [ -n "${BASH_REMATCH[5]}" ]; then
shape=square
rows=$((stageHeight * scale))
dvxSettings "$dir/c" "$program" "$DVX_SETTINGS_SQUARE"
else
shape=4:3
rows=$((shape43Height * scale))
fi
dvxDosboxConf "$dir/dosbox.conf" "$dir/c" dvxDosboxConf "$dir/dosbox.conf" "$dir/c"
printf '\n[capture]\ncapture_dir = %s\ndefault_image_capture_formats = raw\n' "$dir/shots" >> "$dir/dosbox.conf" printf '\n[capture]\ncapture_dir = %s\ndefault_image_capture_formats = raw\n' "$dir/shots" >> "$dir/dosbox.conf"
mkdir -p "$dir/xdg" "$dir/shots" mkdir -p "$dir/xdg" "$dir/shots"
@ -165,8 +177,8 @@ run() {
dosbox="" dosbox=""
guard="" guard=""
if ! dvxDvxLog "$dir/c" 400 | grep -qF "JoeyLib: Keys: $((stageWidth * scale))x$((stageHeight * scale)) at ${scale}x on a ${w}x${h} ${bpp}-bit desktop"; then if ! dvxDvxLog "$dir/c" 400 | grep -qF "JoeyLib: Keys: $((stageWidth * scale))x$rows at ${scale}x, $shape, on a ${w}x${h} ${bpp}-bit desktop"; then
fail "$spec" "DVX.LOG does not show KEYS at ${scale}x on a ${w}x${h} ${bpp}-bit desktop: $(dvxDvxLog "$dir/c" 400 | grep -F 'JoeyLib: Keys:')" fail "$spec" "DVX.LOG does not show KEYS at ${scale}x, $shape, on a ${w}x${h} ${bpp}-bit desktop: $(dvxDvxLog "$dir/c" 400 | grep -F 'JoeyLib: Keys:')"
fi fi
png=$(ls "$dir"/shots/*.png 2> /dev/null | head -1) png=$(ls "$dir"/shots/*.png 2> /dev/null | head -1)
if [ -z "$png" ]; then if [ -z "$png" ]; then

View file

@ -22,7 +22,9 @@
# #
# JOY (examples/joy) draws, for each stick, a dot (in the middle at rest, # JOY (examples/joy) draws, for each stick, a dot (in the middle at rest,
# grey; green when moved), two button squares and a patch that says whether # grey; green when moved), two button squares and a patch that says whether
# the port reports a stick; scripts/lib/joyScreen.py reads the WHOLE state of # the port reports a stick. It runs with square pixels (dvxSettings): the
# 4:3 window repeats some rows, and the dots are read pixel for pixel.
# scripts/lib/joyScreen.py reads the WHOLE state of
# BOTH sticks off each screenshot (scripts/lib/joyGate.sh's steps: each # BOTH sticks off each screenshot (scripts/lib/joyGate.sh's steps: each
# direction of each stick alone, two diagonals each, each button alone and # direction of each stick alone, two diagonals each, each button alone and
# both together, with a release after every press so a latched line shows # both together, with a release after every press so a latched line shows
@ -90,6 +92,7 @@ boot() {
local dir="$JOY_WORK/$1" local dir="$JOY_WORK/$1"
local waited local waited
dvxStage "$dir/c" "$(dvxAppDir "$program")" dvxStage "$dir/c" "$(dvxAppDir "$program")"
dvxSettings "$dir/c" "$program" "$DVX_SETTINGS_SQUARE"
dvxDosboxConf "$dir/dosbox.conf" "$dir/c" dvxDosboxConf "$dir/dosbox.conf" "$dir/c"
mkdir -p "$dir/xdg" "$dir/shots" mkdir -p "$dir/xdg" "$dir/shots"
dosboxJoyMapper "$dir/mapper.txt" dosboxJoyMapper "$dir/mapper.txt"
@ -97,6 +100,7 @@ boot() {
DOSBOX_JOY_SHOTS="$dir/shots" DOSBOX_JOY_SHOTS="$dir/shots"
printf '\n[capture]\ncapture_dir = %s\n' "$dir/shots" >> "$dir/dosbox.conf" printf '\n[capture]\ncapture_dir = %s\n' "$dir/shots" >> "$dir/dosbox.conf"
printf '\n[joystick]\njoysticktype = %s\n' "$2" >> "$dir/dosbox.conf" printf '\n[joystick]\njoysticktype = %s\n' "$2" >> "$dir/dosbox.conf"
printf '%s\n' "${DOSBOX_JOY_SETTINGS[@]}" >> "$dir/dosbox.conf"
printf '\n[sdl]\nmapperfile = %s\n' "$dir/mapper.txt" >> "$dir/dosbox.conf" printf '\n[sdl]\nmapperfile = %s\n' "$dir/mapper.txt" >> "$dir/dosbox.conf"
XDG_CONFIG_HOME="$dir/xdg" DISPLAY="$DOSBOX_JOY_DISPLAY" SDL_AUDIODRIVER=dummy \ XDG_CONFIG_HOME="$dir/xdg" DISPLAY="$DOSBOX_JOY_DISPLAY" SDL_AUDIODRIVER=dummy \
timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \

View file

@ -167,6 +167,9 @@ case "${SERLOSS_BAUD:-9600}" in
4800) baudCode=6 ;; 4800) baudCode=6 ;;
9600) baudCode=7 ;; 9600) baudCode=7 ;;
19200) baudCode=9 ;; 19200) baudCode=9 ;;
38400) baudCode=11 ;;
57600) baudCode=12 ;;
115200) baudCode=13 ;;
*) echo "verify-iigs-serial-loss: unsupported SERLOSS_BAUD" >&2; exit 2 ;; *) echo "verify-iigs-serial-loss: unsupported SERLOSS_BAUD" >&2; exit 2 ;;
esac esac

View file

@ -21,9 +21,6 @@
# A1 missing -> the probe never ran / printer path is broken # A1 missing -> the probe never ran / printer path is broken
# #
# MAME CONNECTS OUT to the bitbanger socket, so the peer here listens first. # MAME CONNECTS OUT to the bitbanger socket, so the peer here listens first.
# Needs the PATCHED apple2gs (installer/patches/mame/0001-apple2gs-scc-clock.patch):
# stock 0.264 feeds the SCC BRG the wrong clock and pins RX at 230400, so no
# baud ever matches the null_modem.
# #
# scripts/verify-iigs-serial.sh # scripts/verify-iigs-serial.sh
set -uo pipefail set -uo pipefail

95
scripts/verify-sdl3-serial.sh Executable file
View file

@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
# See LICENSE.md for license information.
# SPDX-License-Identifier: MIT
#
# verify-sdl3-serial.sh - both-directions RS-232 gate for the SDL3 port, and
# the close's promise to put the line back.
#
# Runs SERTEST (examples/sertest: banner, then echo) with JOEYLIB_SERIAL
# naming the guest end of a pseudo-terminal pair the host peer
# (scripts/lib/serialLoopPeer.py) makes: the peer waits for the banner, sends
# a probe and expects it echoed, so the chain is JoeyLib's POSIX serial HAL
# (src/hosted/posix) -> termios -> the pty -> the host. Before the run the
# guest end is set to 4800 baud with HUPCL on; SERTEST opens it at 9600. The
# peer then has SERTEST close the port (its DTR check, which closes it twice
# and leaves it closed; a pseudo-terminal has no modem lines, so DTR itself
# is not judged here), and once SERTEST no longer holds the pty the line
# must read 4800 again, with HUPCL off: the close puts back the speed the
# port had and leaves DTR as it was.
#
# Usage: scripts/verify-sdl3-serial.sh
# Requires: python3, stty; build/sdl3/bin/SERTEST (make sdl3).
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
label=verify-sdl3-serial
program=$(joeyExampleProgram sertest sdl3)
[ -x "$program" ] || { echo "$label: build SERTEST first (make sdl3)" >&2; exit 2; }
wall=40
ptyWaitTries=40
closeWaitTries=40
speedBefore=4800
work=$(mktemp -d -t joeylib-sdl3-serial.XXXXXX)
peerPid=""
programPid=""
cleanup() {
touch "$work/release"
joeyStop "$programPid" "$peerPid"
rm -rf "$work"
}
trap cleanup EXIT
python3 "$repo/scripts/lib/serialLoopPeer.py" pty "$work/pty.path" "$wall" --close-hold "$work/release" > "$work/peer.log" 2>&1 &
peerPid=$!
for ((tries = 0; tries < ptyWaitTries; tries++)); do
[ -s "$work/pty.path" ] && break
sleep 0.25
done
[ -s "$work/pty.path" ] || { echo "$label: FAIL - the peer made no pty" >&2; exit 1; }
pty=$(cat "$work/pty.path")
stty -F "$pty" "$speedBefore" hupcl
JOEYLIB_SERIAL="$pty" "$program" > "$work/program.log" 2>&1 &
programPid=$!
for ((tries = 0; tries < wall * 4; tries++)); do
grep -q "^CLOSE \|^LOOP FAIL" "$work/peer.log" && break
joeyAlive "$peerPid" || break
sleep 0.25
done
# The close is done when SERTEST no longer has the pty open.
for ((tries = 0; tries < closeWaitTries; tries++)); do
stillOpen=no
for fd in /proc/"$programPid"/fd/*; do
[ "$(readlink "$fd" 2> /dev/null)" = "$pty" ] && stillOpen=yes
done
[ "$stillOpen" = no ] && break
sleep 0.25
done
settings=$(stty -F "$pty" -a 2>&1)
touch "$work/release"
wait "$peerPid"
peerPid=""
cat "$work/peer.log"
if ! grep -q "^LOOP PASS" "$work/peer.log"; then
echo "$label: FAIL - the probe was not echoed" >&2
exit 1
fi
if ! grep -q "^CLOSE PASS" "$work/peer.log" || [ "$stillOpen" = yes ]; then
echo "$label: FAIL - SERTEST did not close the port" >&2
exit 1
fi
speedAfter=$(printf '%s\n' "$settings" | sed -n 's/.*speed \([0-9]*\) baud.*/\1/p')
if [ "$speedAfter" != "$speedBefore" ]; then
echo "$label: FAIL - after the close the line runs at ${speedAfter:-?} baud, not the $speedBefore it had" >&2
exit 1
fi
if ! printf '%s\n' "$settings" | grep -qw -- "-hupcl"; then
echo "$label: FAIL - the close left HUPCL on, so the last close would drop DTR" >&2
exit 1
fi
echo "$label: PASS (banner out, probe echoed through the pty; the close put back $speedBefore baud and left HUPCL off)"

View file

@ -41,6 +41,9 @@ cleanup() {
trap cleanup EXIT trap cleanup EXIT
webServe "$dir" webServe "$dir"
# A null sink's clock paces the page's sound (webSoundSink), so the phases
# fall in real time.
webSoundSink || { echo "$label: no PulseAudio/PipeWire null sink for Chromium to play to" >&2; exit 2; }
# The same timeline as verify-sdl3-audio.sh (seconds since the first buffer). # The same timeline as verify-sdl3-audio.sh (seconds since the first buffer).
if ! webDrive "AUDIO.html" --seconds 120 --audio "$work/out.wav" \ if ! webDrive "AUDIO.html" --seconds 120 --audio "$work/out.wav" \
--step wait:5 --step hold:p:400 \ --step wait:5 --step hold:p:400 \

View file

@ -3,16 +3,21 @@
# See LICENSE.md for license information. # See LICENSE.md for license information.
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
# #
# verify-web-save.sh - Save gate for the web port: a save outlives the page. # verify-web-save.sh - Save gate for the web port: a save outlives the page,
# and each page keeps its own.
# #
# A web program's SAVES/ is kept in the browser's IndexedDB (the SDL3 # A web program's SAVES/ is kept in the browser's IndexedDB (the SDL3
# backend mounts IDBFS there with autoPersist, so each file is written back # backend mounts IDBFS there with autoPersist, so each file is written back
# as it is closed, and again when the page is left). SAVE (examples/save) # as it is closed, and again when the page is left), in a folder named after
# counts its launches in SAVES/runs.dat, so its page is opened in headless # the page: SAVE.html's is /SAVE/SAVES, a store of its own on a site every
# Chromium, given time to write, opened again (reloaded), and given time # page shares. SAVE (examples/save) counts its launches in SAVES/runs.dat.
# again: the count it then holds must be 2 -- the first launch's 1 came # Its folder is served with a second copy of its page, OTHER.html, and in
# back from IndexedDB and the second launch added one. Each wait covers a # one headless Chromium OTHER.html is opened first (its own count, 1), then
# cold browser loading the program (4 s once fell short). # SAVE.html, given time to write, opened again (reloaded), and given time
# again: the count SAVE.html then holds must be 2 -- the first launch's 1
# came back from IndexedDB and the second launch added one; a store shared
# with OTHER.html would say 3. Each wait covers a cold browser loading the
# program (4 s once fell short).
# #
# Usage: scripts/verify-web-save.sh # Usage: scripts/verify-web-save.sh
# Requires: Chromium, emsdk's node; build/web/bin/SAVE (make web). # Requires: Chromium, emsdk's node; build/web/bin/SAVE (make web).
@ -33,12 +38,14 @@ cleanup() {
} }
trap cleanup EXIT trap cleanup EXIT
webServe "$dir" cp -r "$dir" "$work/site"
webDrive "SAVE.html" --step wait:8 --step reload --step wait:8 --file "/SAVES/runs.dat:$work/runs.dat" cp "$work/site/SAVE.html" "$work/site/OTHER.html"
webServe "$work/site"
webDrive "OTHER.html" --step wait:8 --step open:SAVE.html --step wait:8 --step reload --step wait:8 --file "/SAVE/SAVES/runs.dat:$work/runs.dat"
count=$(python3 -c "import struct, sys; print(struct.unpack('<H', open(sys.argv[1], 'rb').read(2))[0])" "$work/runs.dat") count=$(python3 -c "import struct, sys; print(struct.unpack('<H', open(sys.argv[1], 'rb').read(2))[0])" "$work/runs.dat")
if [ "$count" = 2 ]; then if [ "$count" = 2 ]; then
echo "verify-web-save: PASS (the second launch counted 2: the first launch's save came back from IndexedDB)" echo "verify-web-save: PASS (SAVE.html's second launch counted 2 in /SAVE/SAVES: its first launch's save came back from IndexedDB, and OTHER.html's launch on the same site was not counted)"
else else
echo "verify-web-save: FAIL - the second launch counted $count, not 2" >&2 echo "verify-web-save: FAIL - SAVE.html's second launch counted $count, not 2 (3: its store is shared with OTHER.html)" >&2
exit 1 exit 1
fi fi

View file

@ -75,7 +75,7 @@ boot() {
dosboxJoyMapper "$dir/mapper.txt" dosboxJoyMapper "$dir/mapper.txt"
DOSBOX_JOY_SHOTS="$dir/shots" DOSBOX_JOY_SHOTS="$dir/shots"
WIN16_DOSBOX_ARGS=(--set "capture_dir=$dir/shots" --set "default_image_capture_formats=raw" WIN16_DOSBOX_ARGS=(--set "capture_dir=$dir/shots" --set "default_image_capture_formats=raw"
--set "joysticktype=$2" --set "mapperfile=$dir/mapper.txt" --set "joysticktype=$2" "${DOSBOX_JOY_SET_ARGS[@]}" --set "mapperfile=$dir/mapper.txt"
--set "cpu_cycles=$fixedCycles" --set "cpu_cycles_protected=$fixedCycles") --set "cpu_cycles=$fixedCycles" --set "cpu_cycles_protected=$fixedCycles")
win16Start "$dir" JOEYLIB_SCALE=1 JOEYLIB_ASPECT=square win16Start "$dir" JOEYLIB_SCALE=1 JOEYLIB_ASPECT=square
DOSBOX_JOY_DISPLAY="$WIN16_DISPLAY" DOSBOX_JOY_DISPLAY="$WIN16_DISPLAY"

View file

@ -13,6 +13,13 @@
# KPCOMM.DRV (the communications driver JoeyLib's Windows runs with in place # KPCOMM.DRV (the communications driver JoeyLib's Windows runs with in place
# of COMM.DRV) and Windows' virtual COM device -> DOSBox's UART -> host. # of COMM.DRV) and Windows' virtual COM device -> DOSBox's UART -> host.
# #
# Then the DTR check (--dtr-check), as verify-dvx-serial.sh does it: the
# nullmodem runs non-transparent, so DOSBox reports the guest's modem lines to
# the peer in the byte stream, and SERTEST drops and raises DTR, closes and
# reopens the port, then hangs up. jlSerialSetDtr must reach the line, and
# jlSerialClose must leave DTR as it is, up or down: KPCOMM.DRV keeps it at
# CloseComm when the HAL asks (KPESC_KEEP_DTR_ON_CLOSE).
#
# Usage: scripts/verify-win16-serial.sh # Usage: scripts/verify-win16-serial.sh
# Env: WIN16_SERIAL_PORT TCP port the peer listens on (default 6804) # Env: WIN16_SERIAL_PORT TCP port the peer listens on (default 6804)
# Requires: Windows on DOSBox (install.sh install emulator:win16), python3; # Requires: Windows on DOSBox (install.sh install emulator:win16), python3;
@ -29,8 +36,8 @@ win16Need
program=$(joeyExampleProgram sertest win16) program=$(joeyExampleProgram sertest win16)
[ -f "$program" ] || { echo "$label: build SERTEST.EXE first (make win16)" >&2; exit 2; } [ -f "$program" ] || { echo "$label: build SERTEST.EXE first (make win16)" >&2; exit 2; }
port="${WIN16_SERIAL_PORT:-6804}" port="${WIN16_SERIAL_PORT:-6804}"
# The peer's budget: Windows' boot, then the banner and the echo. # The peer's budget: Windows' boot, the banner and the echo, then the DTR check.
wall=60 wall=95
work=$(mktemp -d -t joeylib-win16-serial.XXXXXX) work=$(mktemp -d -t joeylib-win16-serial.XXXXXX)
peerPid="" peerPid=""
@ -41,22 +48,22 @@ cleanup() {
} }
trap cleanup EXIT trap cleanup EXIT
python3 "$repo/scripts/lib/serialLoopPeer.py" tcp-server "$port" "$wall" > "$work/peer.log" 2>&1 & python3 "$repo/scripts/lib/serialLoopPeer.py" tcp-server "$port" "$wall" --dtr-check > "$work/peer.log" 2>&1 &
peerPid=$! peerPid=$!
sleep 1 sleep 1
mkdir -p "$work/program" mkdir -p "$work/program"
cp "$program" "$work/program/SERTEST.EXE" cp "$program" "$work/program/SERTEST.EXE"
win16Stage "$work" "$work/program" SERTEST.EXE win16Stage "$work" "$work/program" SERTEST.EXE
# transparent:1 -- data bytes only: DOSBox's in-band escapes for the control # transparent:0 -- DOSBox sends the guest's modem lines in-band, which the
# lines would reach the plain TCP peer as data. # peer's --dtr-check reads.
WIN16_DOSBOX_ARGS=(--set "serial1=nullmodem server:127.0.0.1 port:$port transparent:1") WIN16_DOSBOX_ARGS=(--set "serial1=nullmodem server:127.0.0.1 port:$port transparent:0")
win16Start "$work" win16Start "$work"
wait "$peerPid" wait "$peerPid"
rc=$? rc=$?
peerPid="" peerPid=""
cat "$work/peer.log" cat "$work/peer.log"
if [ "$rc" -eq 0 ]; then if [ "$rc" -eq 0 ]; then
echo "$label: PASS (banner out, probe echoed through COM1)" echo "$label: PASS (banner out, probe echoed through COM1; DTR follows jlSerialSetDtr and survives jlSerialClose)"
else else
echo "$label: FAIL" >&2 echo "$label: FAIL" >&2
fi fi

View file

@ -9,18 +9,20 @@
# watches the two sound devices through memory taps, so the checks are on # watches the two sound devices through memory taps, so the checks are on
# what the hardware was actually told, not on a log line: # what the hardware was actually told, not on a log line:
# #
# 1. PPI port C ($E9A005) low nibble. That nibble IS the MSM6258's # 1. The MSM6258's rate and channels, as IOCS sets them from the mode
# sample rate (bits 3,2) and channel gating (bits 1,0): the gate # audioPcm.c asks for: PPI port C ($E9A005) bits 3,2 pick the divider
# wants rate index 2, divider 512, 7812 Hz -- the rate the mixer # (1024, 768, 512) and bits 1,0 gate the channels (0 both), and the
# renders at -- and pan 0, both channels. IOCS's own mode byte does # YM2151's CT1 pin (register $1B bit 7) the clock (0 8 MHz, the chip's
# not set these correctly (measured: mode 0x05 leaves 0x01, which is # reset state; 1 4 MHz). The gate wants 7812 Hz, the rate the mixer
# 3906 Hz out of the right speaker only), so audioPcm.c writes the # renders at, on both channels. (A mode byte where IOCS wants a word
# register itself and this checks that it stuck. # -- this port's old 0x05 -- is 3906 Hz out of one side.)
# 2. Every YM2151 noise write ($E90000 register 0x0F). The low five # 2. Every YM2151 noise write ($E90000 register 0x0F). The low five
# bits must be a value from kOpmNoiseFreq in src/x68000/audio.c, # bits must be OPM_NOISE_PERIODS - pitch for a pitch of 1 to
# which is read out of the source here rather than duplicated: a # NOISE_PITCH_MAX (0 plays as 1), both read out of
# pitch that escapes the table is the bug this gate exists for # src/x68000/audio.c rather than duplicated: that is the YM2149's
# (a plain `31 - pitch` ran the generator an octave bright). # 125000 / pitch Hz on the X68000's 4 MHz OPM, and a code outside it
# is the bug this gate exists for (a plain `31 - pitch` ran the
# generator an octave bright).
# 3. The ADPCM byte feed rate WHILE A SOUND IS PLAYING. Two samples per # 3. The ADPCM byte feed rate WHILE A SOUND IS PLAYING. Two samples per
# byte at 7812 Hz is 3906 bytes/s; half that means the divider is # byte at 7812 Hz is 3906 bytes/s; half that means the divider is
# wrong again and every sample the port plays is an octave low. It # wrong again and every sample the port plays is an octave low. It
@ -30,7 +32,7 @@
# #
# The default subject is JoeyLib's audio probe, examples/audio built to # The default subject is JoeyLib's audio probe, examples/audio built to
# press its own keys: it sweeps the noise generator through all 32 pitches, # press its own keys: it sweeps the noise generator through all 32 pitches,
# which reaches every distinct kOpmNoiseFreq value, plays its tracker song's # which reaches every noise code, plays its tracker song's
# noise drums, and keeps a phrase streaming through the ADPCM. # noise drums, and keeps a phrase streaming through the ADPCM.
# #
# Usage: scripts/verify-x68000-audio.sh [program] [seconds] # Usage: scripts/verify-x68000-audio.sh [program] [seconds]
@ -57,16 +59,20 @@ source=$repo/src/x68000/audio.c
[[ -f $bin ]] || { echo "verify-x68000-audio: $bin not built" >&2; exit 2; } [[ -f $bin ]] || { echo "verify-x68000-audio: $bin not built" >&2; exit 2; }
[[ -f $X68K_SYSTEM_DISK ]] || { echo "verify-x68000-audio: missing $X68K_SYSTEM_DISK" >&2; exit 2; } [[ -f $X68K_SYSTEM_DISK ]] || { echo "verify-x68000-audio: missing $X68K_SYSTEM_DISK" >&2; exit 2; }
# The noise table, straight out of the port so the two cannot drift apart. # The noise codes, from the port's own constants so the two cannot drift
# apart: OPM_NOISE_PERIODS - pitch, pitch 1 .. NOISE_PITCH_MAX.
table=$(python3 - "$source" <<'PY' table=$(python3 - "$source" <<'PY'
import re, sys import re, sys
src = open(sys.argv[1]).read() src = open(sys.argv[1]).read()
m = re.search(r'kOpmNoiseFreq\[32\]\s*=\s*\{(.*?)\}', src, re.S) def const(name):
if not m: m = re.search(r'#define\s+' + name + r'\s+(\d+)u?', src)
sys.exit("kOpmNoiseFreq not found") if not m:
print(",".join(v.strip() for v in m.group(1).split(",") if v.strip())) sys.exit(name + " not found")
return int(m.group(1))
periods = const("OPM_NOISE_PERIODS")
print(",".join(str(periods - p) for p in range(1, const("NOISE_PITCH_MAX") + 1)))
PY PY
) || { echo "verify-x68000-audio: cannot read kOpmNoiseFreq from $source" >&2; exit 2; } ) || { echo "verify-x68000-audio: cannot read the noise constants from $source" >&2; exit 2; }
work=$(mktemp -d -t joey-x68audio.XXXXXX) work=$(mktemp -d -t joey-x68audio.XXXXXX)
trap 'rm -rf "$work"' EXIT trap 'rm -rf "$work"' EXIT
@ -101,6 +107,8 @@ opmTap = mem:install_write_tap(0xE90000, 0xE90003, "opm", function(offset, data,
sel = v sel = v
elseif sel == 0x0F then elseif sel == 0x0F then
out:write(string.format("NOISE %02X\\n", v)) out:write(string.format("NOISE %02X\\n", v))
elseif sel == 0x1B then
out:write(string.format("CT %02X\\n", v))
end end
return data return data
end) end)
@ -157,7 +165,7 @@ python3 - "$work/audio.txt" "$table" "$PROG" <<'PY'
import sys import sys
path, table, prog = sys.argv[1], sys.argv[2], sys.argv[3] path, table, prog = sys.argv[1], sys.argv[2], sys.argv[3]
allowed = {int(v) for v in table.split(",")} allowed = {int(v) for v in table.split(",")}
noise, portc, rate, done = [], [], None, False noise, portc, ct, rate, done = [], [], [], None, False
for line in open(path): for line in open(path):
p = line.split() p = line.split()
if not p: if not p:
@ -166,6 +174,8 @@ for line in open(path):
noise.append(int(p[1], 16)) noise.append(int(p[1], 16))
elif p[0] == "PORTC": elif p[0] == "PORTC":
portc.append(int(p[1], 16)) portc.append(int(p[1], 16))
elif p[0] == "CT":
ct.append(int(p[1], 16))
elif p[0] == "PCMRATE": elif p[0] == "PCMRATE":
rate = float(p[1]) rate = float(p[1])
elif p[0] == "DONE": elif p[0] == "DONE":
@ -175,24 +185,27 @@ fails = []
if not done: if not done:
fails.append("the run never reached its time limit") fails.append("the run never reached its time limit")
# 1. The ADPCM rate/pan nibble. The last value written wins; the ROM's own # 1. The ADPCM rate and channels: the last values written win; the ROM's
# boot value (0x0B) does not count as ours. # own boot value of port C (0x0B) does not count as the port's.
ours = [v for v in portc if v != 0x0B] ours = [v for v in portc if v != 0x0B]
if not ours: if not ours:
fails.append("PPI port C was never programmed, so the ADPCM never played") fails.append("PPI port C was never programmed, so the ADPCM never played")
else: else:
got = ours[-1] & 0x0F got = ours[-1] & 0x0F
if got != 0x08: clock = 4000000 if ct and ct[-1] & 0x80 else 8000000
fails.append(f"port C low nibble {got:#04x}, want 0x08 " divider = (1024, 768, 512, 512)[(got >> 2) & 3]
f"(rate index {(got >> 2) & 3} pan {got & 3}; want rate 2 = 7812 Hz, pan 0 = both)") hz = clock // divider
if hz != 7812 or got & 3:
fails.append(f"port C low nibble {got:#04x} with a {clock // 1000000} MHz clock: "
f"{hz} Hz, channels {got & 3}; want 7812 Hz on both (0)")
# 2. Every enabled noise write must name a table entry. # 2. Every enabled noise write must be one of the port's codes.
on = {v & 0x1F for v in noise if v & 0x80} on = {v & 0x1F for v in noise if v & 0x80}
if not on: if not on:
fails.append("no noise writes seen at all") fails.append("no noise writes seen at all")
stray = sorted(on - allowed) stray = sorted(on - allowed)
if stray: if stray:
fails.append(f"noise codes outside kOpmNoiseFreq: {stray}") fails.append(f"noise codes no pitch makes: {stray}")
# 3. Byte feed rate: two samples per byte at 7812 Hz. # 3. Byte feed rate: two samples per byte at 7812 Hz.
if rate is None: if rate is None:
@ -204,8 +217,8 @@ elif not (3500.0 <= rate <= 4300.0):
portcText = "/".join(f"{v:02X}" for v in ours[-2:]) if ours else "never set" portcText = "/".join(f"{v:02X}" for v in ours[-2:]) if ours else "never set"
rateText = f"{rate:.0f} bytes/s in-burst" if rate is not None else "silent" rateText = f"{rate:.0f} bytes/s in-burst" if rate is not None else "silent"
print(f"verify-x68000-audio: {prog} noise writes {len(noise)} " print(f"verify-x68000-audio: {prog} noise writes {len(noise)} "
f"({len(on)} distinct codes, {len(allowed - on)} table entries unused), " f"({len(on)} distinct codes, {len(allowed - on)} of the port's unused), "
f"port C {portcText}, ADPCM {rateText}") f"port C {portcText}, CT writes {len(ct)}, ADPCM {rateText}")
if fails: if fails:
for f in fails: for f in fails:
print(f"verify-x68000-audio: FAIL ({f})", file=sys.stderr) print(f"verify-x68000-audio: FAIL ({f})", file=sys.stderr)

View file

@ -91,12 +91,19 @@ local EXIT_SETTLE = 900 -- keypress -> fclose -> back at the Human68k prompt
-- 16-bit word per pixel, 512 words per row. -- 16-bit word per pixel, 512 words per row.
-- The 320x200 stage is centred at origin (96, 156) either way. -- The 320x200 stage is centred at origin (96, 156) either way.
local presentPlane = "$PRESENT_PLANE" local presentPlane = "$PRESENT_PLANE"
-- The text plane holds two pages, and the port flips between them with the
-- text Y scroll (CRTC R11, which reads back): rows are counted from the page
-- on screen.
local function shownRow()
return mem:read_u16(0xE80016) & 0x3FF
end
local function stagePixel(px, py) local function stagePixel(px, py)
if presentPlane == "gfx" then if presentPlane == "gfx" then
return mem:read_u8(0xC00000 + ((156 + py) * 512 + 96 + px) * 2 + 1) return mem:read_u8(0xC00000 + ((156 + py) * 512 + 96 + px) * 2 + 1)
end end
local x = 96 + px local x = 96 + px
local byteOff = (156 + py) * 128 + (x >> 3) local byteOff = (156 + shownRow() + py) * 128 + (x >> 3)
local bit = 0x80 >> (x & 7) local bit = 0x80 >> (x & 7)
local v = 0 local v = 0
for plane = 0, 3 do for plane = 0, 3 do

View file

@ -83,9 +83,16 @@ local fields = {}
-- apart at 0xE00000, 128 bytes per row, leftmost pixel in bit 7, the stage -- apart at 0xE00000, 128 bytes per row, leftmost pixel in bit 7, the stage
-- centred at (96, 156). (Before 2026-09-24 it drew the graphics plane at -- centred at (96, 156). (Before 2026-09-24 it drew the graphics plane at
-- 0xC00000, which this gate used to read.) -- 0xC00000, which this gate used to read.)
-- The text plane holds two pages, and the port flips between them with the
-- text Y scroll (CRTC R11, which reads back): rows are counted from the page
-- on screen.
local function shownRow()
return mem:read_u16(0xE80016) & 0x3FF
end
local function textPix(px, y) local function textPix(px, y)
local sx = 96 + px local sx = 96 + px
local addr = 0xE00000 + (156 + y) * 128 + (sx >> 3) local addr = 0xE00000 + (156 + shownRow() + y) * 128 + (sx >> 3)
local bit = 7 - (sx & 7) local bit = 7 - (sx & 7)
local v = 0 local v = 0
for p = 0, 3 do for p = 0, 3 do

View file

@ -125,9 +125,16 @@ for stick = 1, 2 do
end end
-- Stage pixel (x, y) -> its colour index, one bit from each text plane. -- Stage pixel (x, y) -> its colour index, one bit from each text plane.
-- The text plane holds two pages, and the port flips between them with the
-- text Y scroll (CRTC R11, which reads back): rows are counted from the page
-- on screen.
local function shownRow()
return mem:read_u16(0xE80016) & 0x3FF
end
local function stagePixel(x, y) local function stagePixel(x, y)
local sx = ORIGIN_X + x local sx = ORIGIN_X + x
local addr = TVRAM + (ORIGIN_Y + y) * TVRAM_STRIDE + (sx >> 3) local addr = TVRAM + (ORIGIN_Y + shownRow() + y) * TVRAM_STRIDE + (sx >> 3)
local bit = 7 - (sx & 7) local bit = 7 - (sx & 7)
local v = 0 local v = 0
for p = 0, PLANES - 1 do for p = 0, PLANES - 1 do

View file

@ -65,9 +65,16 @@ local fails = {}
-- apart at 0xE00000, 128 bytes per row, leftmost pixel in bit 7, the stage -- apart at 0xE00000, 128 bytes per row, leftmost pixel in bit 7, the stage
-- centred at (96, 156). (Before 2026-09-24 it drew the graphics plane at -- centred at (96, 156). (Before 2026-09-24 it drew the graphics plane at
-- 0xC00000, which this gate used to read.) -- 0xC00000, which this gate used to read.)
-- The text plane holds two pages, and the port flips between them with the
-- text Y scroll (CRTC R11, which reads back): rows are counted from the page
-- on screen.
local function shownRow()
return mem:read_u16(0xE80016) & 0x3FF
end
local function textPix(px, y) local function textPix(px, y)
local sx = 96 + px local sx = 96 + px
local addr = 0xE00000 + (156 + y) * 128 + (sx >> 3) local addr = 0xE00000 + (156 + shownRow() + y) * 128 + (sx >> 3)
local bit = 7 - (sx & 7) local bit = 7 - (sx & 7)
local v = 0 local v = 0
for p = 0, 3 do for p = 0, 3 do

View file

@ -193,9 +193,16 @@ local function out(s)
io.flush() io.flush()
end end
-- The text plane holds two pages, and the port flips between them with the
-- text Y scroll (CRTC R11, which reads back): rows are counted from the page
-- on screen.
local function shownRow()
return mem:read_u16(0xE80016) & 0x3FF
end
local function stagePixel(px, py) local function stagePixel(px, py)
local x = ORIGIN_X + px local x = ORIGIN_X + px
local byteOff = (ORIGIN_Y + py) * TVRAM_STRIDE + (x >> 3) local byteOff = (ORIGIN_Y + shownRow() + py) * TVRAM_STRIDE + (x >> 3)
local bit = 0x80 >> (x & 7) local bit = 0x80 >> (x & 7)
local v = 0 local v = 0
for plane = 0, 3 do for plane = 0, 3 do

View file

@ -228,9 +228,16 @@ local wr9 = emu.item(scc.items["0/m_wr9"])
local wr2 = emu.item(sccA.items["0/m_wr2"]) local wr2 = emu.item(sccA.items["0/m_wr2"])
local iackFixed = 0 local iackFixed = 0
-- The text plane holds two pages, and the port flips between them with the
-- text Y scroll (CRTC R11, which reads back): rows are counted from the page
-- on screen.
local function shownRow()
return mem:read_u16(0xE80016) & 0x3FF
end
local function textPix(px, y) local function textPix(px, y)
local sx = ORIGIN_X + px local sx = ORIGIN_X + px
local addr = TEXT_BASE + (ORIGIN_Y + y) * ROW_BYTES + (sx >> 3) local addr = TEXT_BASE + (ORIGIN_Y + shownRow() + y) * ROW_BYTES + (sx >> 3)
local bit = 7 - (sx & 7) local bit = 7 - (sx & 7)
local v = 0 local v = 0
for p = 0, PLANES - 1 do for p = 0, PLANES - 1 do

View file

@ -55,11 +55,13 @@ work=$(mktemp -d -t joeylib-x68-tilebank.XXXXXX)
trap 'rm -rf "$work"' EXIT trap 'rm -rf "$work"' EXIT
# The boot disk carries only Human68k and AUTOEXEC; the program, its DATA and # The boot disk carries only Human68k and AUTOEXEC; the program, its DATA and
# its log live on the data disk in B:, which AUTOEXEC makes current. # its log live in GAME on the data disk in B:, and AUTOEXEC starts it from
x68kBootDisk "$work/boot.xdf" 'B:' 'B:\TILEBANK.X' || exit 1 # B:'s root -- so it must find its DATA/ from its process block, not the
# current directory, and log there too (src/core/fileRoot.h).
x68kBootDisk "$work/boot.xdf" 'B:' 'B:\GAME\TILEBANK.X' || exit 1
x68kDataDisk "$work/data.xdf" || exit 1 x68kDataDisk "$work/data.xdf" || exit 1
x68kXdf add "$work/data.xdf" "$binDir/TILEBANK.X" TILEBANK.X > /dev/null || exit 1 x68kXdf add "$work/data.xdf" "$binDir/TILEBANK.X" GAME/TILEBANK.X > /dev/null || exit 1
x68kAddTree "$work/data.xdf" "$dataDir" DATA/TILEBANK || exit 1 x68kAddTree "$work/data.xdf" "$dataDir" GAME/DATA/TILEBANK || exit 1
cat > "$work/tilebank.lua" <<LUA cat > "$work/tilebank.lua" <<LUA
local mem = manager.machine.devices[":maincpu"].spaces["program"] local mem = manager.machine.devices[":maincpu"].spaces["program"]
@ -82,9 +84,16 @@ local lamp = 0
local exitAt = 0 local exitAt = 0
local done = false local done = false
-- The text plane holds two pages, and the port flips between them with the
-- text Y scroll (CRTC R11, which reads back): rows are counted from the page
-- on screen.
local function shownRow()
return mem:read_u16(0xE80016) & 0x3FF
end
local function stagePixel(px, py) local function stagePixel(px, py)
local x = 96 + px local x = 96 + px
local byteOff = (156 + py) * 128 + (x >> 3) local byteOff = (156 + shownRow() + py) * 128 + (x >> 3)
local bit = 0x80 >> (x & 7) local bit = 0x80 >> (x & 7)
local v = 0 local v = 0
for plane = 0, 3 do for plane = 0, 3 do
@ -163,7 +172,7 @@ lampOk=1
if ! grep -q 'lamp=PASS' <<< "$out"; then if ! grep -q 'lamp=PASS' <<< "$out"; then
lampOk=0 lampOk=0
fi fi
if ! x68kXdf extract "$work/data.xdf" joeylog.txt "$work/joeylog.txt" > /dev/null 2>&1; then if ! x68kXdf extract "$work/data.xdf" GAME/joeylog.txt "$work/joeylog.txt" > /dev/null 2>&1; then
echo "verify-x68000-tilebank: FAIL - no joeylog.txt on the data disk (the program never logged, or never exited)" >&2 echo "verify-x68000-tilebank: FAIL - no joeylog.txt on the data disk (the program never logged, or never exited)" >&2
exit 1 exit 1
fi fi

View file

@ -28,7 +28,9 @@
// jlSurfaceT.portData points to this on Amiga: the 4 plane base // jlSurfaceT.portData points to this on Amiga: the 4 plane base
// pointers + stride. Every surface (including the stage, which gets // pointers + stride. Every surface (including the stage, which gets
// its OWN shadow planes, NOT aliased to the displayed gPlanes) owns // its OWN shadow planes, NOT aliased to the displayed gPlanes) owns
// AllocMem(MEMF_CHIP) planes so the blitter can reach them; present // AllocMem'd planes, in chip RAM where the blitter can reach them; a
// surface other than the stage made when chip RAM has run out gets them
// anywhere (chip false), and its fills and copies run on the CPU. present
// copies shadow -> display planes atomically. // copies shadow -> display planes atomically.
// //
// Direct fields rather than a union because the consumer is asm / // Direct fields rather than a union because the consumer is asm /
@ -39,6 +41,7 @@ typedef struct {
uint16_t bytesPerPlane; // = AMIGA_PLANE_SIZE (8000) uint16_t bytesPerPlane; // = AMIGA_PLANE_SIZE (8000)
bool ownsPlanes; // true = AllocMem'd (free at destroy); bool ownsPlanes; // true = AllocMem'd (free at destroy);
// false = aliased (don't free) // false = aliased (don't free)
bool chip; // the planes are chip RAM: the blitter reaches them
} AmigaPlanarT; } AmigaPlanarT;

View file

@ -51,6 +51,12 @@ extern UBYTE mt_E8Trigger;
#define PAULA_CLOCK_PAL 3546895UL #define PAULA_CLOCK_PAL 3546895UL
#define PAULA_PERIOD_MIN 124 // Paula HW limit #define PAULA_PERIOD_MIN 124 // Paula HW limit
#define PAULA_PERIOD_MAX 65535 #define PAULA_PERIOD_MAX 65535
// The fastest rate Paula plays: an effect faster than this plays decimated
// (audioDecimate), at its pitch.
#define PAULA_MAX_RATE ((uint16_t)(PAULA_CLOCK_PAL / PAULA_PERIOD_MIN))
// A one-shot's head, in Chip RAM before it starts: the rest is copied while
// Paula plays it (jlpAudioPlaySfx).
#define SFX_HEAD_BYTES 512u
#define PAULA_CHANNELS 4 // hardware audio channel count #define PAULA_CHANNELS 4 // hardware audio channel count
#define SFX_VOLUME_MAX 64 #define SFX_VOLUME_MAX 64
@ -116,7 +122,7 @@ extern UBYTE mt_E8Trigger;
typedef struct { typedef struct {
UBYTE *samples; // Chip-RAM copy of one SFX sample UBYTE *samples; // Chip-RAM copy of one SFX sample
UWORD lengthWords; // sample length in 16-bit words ULONG sampleBytes; // its allocation
// ---- streaming (jlpAudioPlaySfxStream) ---- // ---- streaming (jlpAudioPlaySfxStream) ----
UBYTE *chunk; // Chip-RAM chunk buffer, SFX_STREAM_CHUNK UBYTE *chunk; // Chip-RAM chunk buffer, SFX_STREAM_CHUNK
BYTE cha; // Paula channel, CHANNEL_PICK = centred BYTE cha; // Paula channel, CHANNEL_PICK = centred
@ -126,6 +132,8 @@ typedef struct {
void *ctx; void *ctx;
uint16_t rateHz; uint16_t rateHz;
uint16_t dueFrame; // the VBL count the next chunk goes out at uint16_t dueFrame; // the VBL count the next chunk goes out at
uint8_t decimate; // every decimate-th sample plays (audioDecimate), at rateHz
uint8_t phase; // audioDecimate's phase between chunks
bool active; bool active;
} SfxSlotT; } SfxSlotT;
@ -299,7 +307,7 @@ bool jlpAudioInit(void) {
gModuleLength = 0; gModuleLength = 0;
for (i = 0; i < JL_AUDIO_SFX_SLOTS; i++) { for (i = 0; i < JL_AUDIO_SFX_SLOTS; i++) {
gSfxSlots[i].samples = NULL; gSfxSlots[i].samples = NULL;
gSfxSlots[i].lengthWords = 0; gSfxSlots[i].sampleBytes = 0;
// Centred, not 0: zero is Paula channel 0, which would silently pin // Centred, not 0: zero is Paula channel 0, which would silently pin
// every slot hard left. // every slot hard left.
gSfxSlots[i].cha = CHANNEL_PICK; gSfxSlots[i].cha = CHANNEL_PICK;
@ -351,9 +359,9 @@ void jlpAudioShutdown(void) {
} }
for (i = 0; i < JL_AUDIO_SFX_SLOTS; i++) { for (i = 0; i < JL_AUDIO_SFX_SLOTS; i++) {
if (gSfxSlots[i].samples != NULL) { if (gSfxSlots[i].samples != NULL) {
FreeMem(gSfxSlots[i].samples, (ULONG)gSfxSlots[i].lengthWords * 2UL); FreeMem(gSfxSlots[i].samples, gSfxSlots[i].sampleBytes);
gSfxSlots[i].samples = NULL; gSfxSlots[i].samples = NULL;
gSfxSlots[i].lengthWords = 0; gSfxSlots[i].sampleBytes = 0;
} }
gSfxSlots[i].active = false; gSfxSlots[i].active = false;
if (gSfxSlots[i].chunk != NULL) { if (gSfxSlots[i].chunk != NULL) {
@ -466,11 +474,18 @@ static bool streamChunk(uint8_t slot) {
uint32_t got; uint32_t got;
// The fill contract is SIGNED 8-bit, which is exactly what Paula // The fill contract is SIGNED 8-bit, which is exactly what Paula
// wants, so the callback writes straight into Chip RAM. // wants, so the callback writes straight into Chip RAM. A stream faster
// than Paula keeps every decimate-th sample, packed to the front, and
// asks again if a short fill left too few.
do {
got = s->fill(s->ctx, (int8_t *)s->chunk, (uint32_t)SFX_STREAM_CHUNK); got = s->fill(s->ctx, (int8_t *)s->chunk, (uint32_t)SFX_STREAM_CHUNK);
if (got < 2u || got > (uint32_t)SFX_STREAM_CHUNK) { if (got == 0u || got > (uint32_t)SFX_STREAM_CHUNK) {
return false; return false;
} }
if (s->decimate > 1u) {
got = audioDecimate(s->chunk, s->chunk, got, s->decimate, &s->phase);
}
} while (got < 2u);
got &= ~1u; // Paula counts 16-bit words got &= ~1u; // Paula counts 16-bit words
sfx.sfx_ptr = s->chunk; sfx.sfx_ptr = s->chunk;
@ -577,11 +592,18 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
SfxSlotT *s; SfxSlotT *s;
UBYTE *chip; UBYTE *chip;
UWORD words; UWORD words;
uint32_t kept;
uint32_t head;
uint8_t factor;
uint8_t phase;
if (!gInstalled || slot >= JL_AUDIO_SFX_SLOTS) { if (!gInstalled || slot >= JL_AUDIO_SFX_SLOTS) {
return; return;
} }
if (length < 2 || length > 0x1FFFEUL) { // Faster than Paula plays: every factor-th sample, at its pitch.
factor = audioDecimationFactor(rateHz, PAULA_MAX_RATE);
kept = (length + factor - 1u) / factor;
if (kept < 2 || kept > 0x1FFFEUL) {
return; // Paula sample length is 16 bits of words = 128KB max return; // Paula sample length is 16 bits of words = 128KB max
} }
@ -590,27 +612,35 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
// about to be freed: stop it first. // about to be freed: stop it first.
slotSilence(slot); slotSilence(slot);
if (s->samples != NULL) { if (s->samples != NULL) {
FreeMem(s->samples, (ULONG)s->lengthWords * 2UL); FreeMem(s->samples, s->sampleBytes);
s->samples = NULL; s->samples = NULL;
s->lengthWords = 0; s->sampleBytes = 0;
} }
chip = (UBYTE *)AllocMem((ULONG)length, MEMF_CHIP); chip = (UBYTE *)AllocMem(kept, MEMF_CHIP);
if (chip == NULL) { if (chip == NULL) {
return; return;
} }
memcpy(chip, sample, length); // The head goes into Chip RAM first and the sound starts on it; the rest
words = (UWORD)(length >> 1); // follows while Paula plays, a copy far faster than Paula reads, so a
// long or fast sample starts as soon as a short one.
head = kept < SFX_HEAD_BYTES ? kept : SFX_HEAD_BYTES;
phase = 0u;
(void)audioDecimate(chip, sample, head * factor, factor, &phase);
words = (UWORD)(kept >> 1);
s->samples = chip; s->samples = chip;
s->lengthWords = words; s->sampleBytes = kept;
sfx.sfx_ptr = chip; sfx.sfx_ptr = chip;
sfx.sfx_len = words; sfx.sfx_len = words;
sfx.sfx_per = periodForRate(rateHz); sfx.sfx_per = periodForRate((uint16_t)(rateHz / factor));
sfx.sfx_vol = SFX_VOLUME_MAX; sfx.sfx_vol = SFX_VOLUME_MAX;
sfx.sfx_pri = SLOT_PRIORITY(slot); sfx.sfx_pri = SLOT_PRIORITY(slot);
sfxPlay(slot, &sfx); sfxPlay(slot, &sfx);
if (kept > head) {
(void)audioDecimate(chip + head, sample + head * factor, length - head * factor, factor, &phase);
}
} }
@ -637,9 +667,12 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
return; return;
} }
} }
// Faster than Paula plays: every decimate-th sample, at its pitch.
s->decimate = audioDecimationFactor(rateHz, PAULA_MAX_RATE);
s->phase = 0u;
s->fill = fill; s->fill = fill;
s->ctx = ctx; s->ctx = ctx;
s->rateHz = rateHz; s->rateHz = (uint16_t)(rateHz / s->decimate);
s->active = true; s->active = true;
if (!streamChunk(slot)) { if (!streamChunk(slot)) {
s->active = false; s->active = false;

File diff suppressed because it is too large Load diff

View file

@ -2,25 +2,32 @@
// See LICENSE.md for license information. // See LICENSE.md for license information.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// //
// Amiga save-file + disk-space HAL. // Amiga save-file + disk-space HAL, and the file roots.
// //
// SAVES/ is a real directory in the current directory (the program's own, // The program's folder (fileRoot.h) is PROGDIR:, the assign dos.library
// as Workbench starts it), made with dos.library CreateDir; delete is // gives every program for the folder it was loaded from (Kickstart 2.0 and
// DeleteFile; free space is Info() on a lock to the current directory, so it // later; on 1.3, and for a program started without a folder, the current
// measures the volume SAVES/ is written to however the program was started. // directory). SAVES/ is a real directory there,
// All ROM-backed, so no libc filesystem dependency. // made with dos.library CreateDir; delete is DeleteFile, rename Rename
// (libnix has no rename); free space is Info() on a lock to that folder,
// so it measures the volume SAVES/ is written to however the program was
// started. All ROM-backed, so no libc filesystem dependency.
#include <exec/types.h> #include <exec/types.h>
#include <dos/dos.h> #include <dos/dos.h>
#include <dos/dosextens.h>
#include <proto/dos.h> #include <proto/dos.h>
#include <proto/exec.h>
#include "joey/file.h" #include "joey/file.h"
#include "fileRoot.h"
#include "port.h" #include "port.h"
#define AMIGA_MAX_U32 0xFFFFFFFFu #define AMIGA_MAX_U32 0xFFFFFFFFu
#define CURRENT_DIR "" // Lock's name for the current directory #define PROGDIR_VERSION 36u // dos.library that knows PROGDIR:
#define PROGDIR_ROOT "PROGDIR:"
// Info() takes the block as a BPTR, so it must be longword aligned: a stack // Info() takes the block as a BPTR, so it must be longword aligned: a stack
@ -34,7 +41,7 @@ uint32_t jlpDiskFree(void) {
uint32_t bytesPerBlock; uint32_t bytesPerBlock;
bool ok; bool ok;
lock = Lock((CONST_STRPTR)CURRENT_DIR, SHARED_LOCK); lock = Lock((CONST_STRPTR)jlpWriteRoot(), SHARED_LOCK);
if (lock == 0) { if (lock == 0) {
return 0u; return 0u;
} }
@ -54,6 +61,18 @@ uint32_t jlpDiskFree(void) {
} }
// PROGDIR: where dos.library has it and the program has a folder, else ""
// (the current directory). A process started without one -- CreateProc, as
// Workbench 1.3 and some launchers start a tool -- has no PROGDIR:, and DOS
// would ask for a volume of that name and wait.
const char *jlpReadRoot(void) {
if (((struct Library *)DOSBase)->lib_Version < PROGDIR_VERSION) {
return "";
}
return ((struct Process *)FindTask(NULL))->pr_HomeDir != 0 ? PROGDIR_ROOT : "";
}
bool jlpSaveDelete(const char *path) { bool jlpSaveDelete(const char *path) {
return DeleteFile((CONST_STRPTR)path) != 0; return DeleteFile((CONST_STRPTR)path) != 0;
} }
@ -75,3 +94,13 @@ bool jlpSaveDirEnsure(const char *dir) {
} }
return false; return false;
} }
bool jlpSaveRename(const char *from, const char *to) {
return Rename((CONST_STRPTR)from, (CONST_STRPTR)to) != 0;
}
const char *jlpWriteRoot(void) {
return jlpReadRoot();
}

View file

@ -9,23 +9,27 @@
// * Timer A is NOT started at init: the ISR costs ~40% of the CPU at // * Timer A is NOT started at init: the ISR costs ~40% of the CPU at
// 12.3 kHz, so the pump is demand-driven -- installed on the first // 12.3 kHz, so the pump is demand-driven -- installed on the first
// PlayMod/PlaySfx (MFP prescaler 200, data 1) and uninstalled once // PlayMod/PlaySfx (MFP prescaler 200, data 1) and uninstalled once
// the mix buffer has drained back to pure silence with nothing // the mix buffer has held pure silence, with nothing playing, for
// playing. // PWM_IDLE_MS, after a ramp down to level 0 (and it starts with one
// up to silence), so neither end clicks.
// * jlpAudioPlayMod starts the module player at the same 12288 Hz mono // * jlpAudioPlayMod starts the module player at the same 12288 Hz mono
// unsigned-8-bit output that the ISR consumes one byte at a time. // unsigned-8-bit output that the ISR consumes one byte at a time.
// * Each Timer A tick the ISR pops one byte off the buffer, writes // * Each Timer A tick the ISR pops one byte off the buffer, writes its
// its high 4 bits to YM register 8 (channel A volume), advances // YM level (gYmLevel: the YM's sixteen levels are logarithmic, so each
// the play pointer, raises a refill flag at half/end. The refill // sample plays as the level nearest it on a straight line) to YM
// itself happens in jlpAudioFrameTick on the main thread: the mix is // register 8 (channel A volume), and advances the play pointer; the
// too long for an interrupt. // frame tick sees which half it has left. The refill itself happens in
// jlpAudioFrameTick on the main thread: the mix is too long for an
// interrupt.
// //
// Quality budget: 4-bit unsigned PWM at 12.3 kHz on the YM is grim by // Quality budget: sixteen logarithmic levels at 12.3 kHz on the YM is
// modern standards but period-correct for stock 520ST/1040ST without // grim by modern standards but period-correct for stock 520ST/1040ST
// the STe DMA chip. // without the STe DMA chip.
// //
// The STe build (make ste, JOEYLIB_ATARI_STE) plays the same mix buffer // The STe build (make ste, JOEYLIB_ATARI_STE) plays the mix buffer, in
// through the STe's 8-bit DMA sound chip instead, mono at 12517 Hz, // left and right byte pairs, through the STe's 8-bit DMA sound chip
// looping over the whole buffer with no interrupt at all. // instead, stereo at 12517 Hz, looping over the whole buffer with no
// interrupt at all.
// jlpAudioFrameTick reads the chip's frame counter to see which half it // jlpAudioFrameTick reads the chip's frame counter to see which half it
// has finished, converts each refilled half to the chip's signed // has finished, converts each refilled half to the chip's signed
// samples, and channel A stays the PSG's: the chiptune keeps all three // samples, and channel A stays the PSG's: the chiptune keeps all three
@ -76,23 +80,32 @@
#define YM_MIX_NOISE_A 0x08u #define YM_MIX_NOISE_A 0x08u
#define YM_MIX_NOISE_C 0x20u #define YM_MIX_NOISE_C 0x20u
#define MIX_BUFFER 2048 // The buffer is two halves of MIX_HALF_FRAMES frames: bytes on the plain
#define MIX_HALF (MIX_BUFFER / 2) // ST, left and right byte pairs on the STe, whose DMA chip plays stereo.
// Bytes ahead of the play position where a new sound's start-now overlay
// begins (sfxStartNow), so the write always lands before the read. On the
// plain ST the PWM interrupt takes about half the CPU, and that mix runs
// slower than the interrupt plays (~10.5K samples a second against 12288,
// measured under Hatari): it starts ~42 ms ahead, twice what the play
// position gains on it over the longest overlay. The STe's DMA chip takes
// no CPU: ~5 ms is enough.
#ifdef JOEYLIB_ATARI_STE #ifdef JOEYLIB_ATARI_STE
#define SFX_START_MARGIN 64u #define MIX_CHANNELS 2u
#else #else
#define SFX_START_MARGIN 512u #define MIX_CHANNELS 1u
#endif #endif
#define MIX_HALF_FRAMES 1024u
#define MIX_HALF (MIX_HALF_FRAMES * MIX_CHANNELS)
#define MIX_BUFFER (MIX_HALF * 2u)
// Frames ahead of the play position where a new sound's start-now overlay
// begins (sfxStartNow), so the write always lands before the read: ~5 ms.
// The overlay's mix (src/m68k/sfxMix.s) outruns the play position once
// started, at about twice the rate the plain ST's PWM interrupt plays even
// with the interrupt's share of the CPU gone, so the margin only covers the
// start.
#define SFX_START_MARGIN 64u
// The module player and the SFX overlay mix unsigned samples centred on $80. // The module player and the SFX overlay mix unsigned samples centred on $80.
#define MIX_SILENCE_UNSIGNED MOD_PLAYER_SILENCE_U8 #define MIX_SILENCE_UNSIGNED MOD_PLAYER_SILENCE_U8
// The 8-bit mix's unit (joey/audio.h's mix): a module channel at full volume
// and a full-scale sound effect each swing half a byte (modPlayerSetU8Shift,
// audioSfxSetShift). On the STe a side's two channels fill it; the plain
// ST's mono sums four, which clip when three or four peak together. The
// voices come down to the same level (kYmVolume).
#define MIX_UNIT_SHIFT 1u
#ifdef JOEYLIB_ATARI_STE #ifdef JOEYLIB_ATARI_STE
// STe build: the output stage is the DMA chip, which plays signed // STe build: the output stage is the DMA chip, which plays signed
@ -120,7 +133,7 @@
#define STE_DMA_MODE ((volatile uint8_t *)0xFFFF8921L) #define STE_DMA_MODE ((volatile uint8_t *)0xFFFF8921L)
#define STE_DMA_STOP 0x00u #define STE_DMA_STOP 0x00u
#define STE_DMA_PLAY_LOOP 0x03u // bit 0 play, bit 1 repeat #define STE_DMA_PLAY_LOOP 0x03u // bit 0 play, bit 1 repeat
#define STE_DMA_MONO_12K 0x81u // bit 7 mono, rate 1 = 12517 Hz #define STE_DMA_STEREO_12K 0x01u // bit 7 clear: stereo; rate 1 = 12517 Hz
#else #else
// ST build: the output stage is PWM through YM channel A's volume, one // ST build: the output stage is PWM through YM channel A's volume, one
// sample per MFP Timer A interrupt. // sample per MFP Timer A interrupt.
@ -141,6 +154,27 @@
#define MIX_RATE 12288 // Hz, matches MFP 2.4576 MHz / 200 #define MIX_RATE 12288 // Hz, matches MFP 2.4576 MHz / 200
#define MIX_SILENCE MIX_SILENCE_UNSIGNED #define MIX_SILENCE MIX_SILENCE_UNSIGNED
// The YM's channel-A volume is the DAC, and its sixteen levels are
// logarithmic, about 3 dB apart: each sample is played as the level nearest
// its value on a straight line from level 0 to level 15 (gYmLevel), so the
// waveform keeps its shape and silence sits near the middle, level 13. The
// levels' amplitudes, as the YM2149 puts them out (ST-Sound's measured
// table).
#define YM_LEVELS 16u
#define SAMPLE_VALUES 256u
#define YM_SAMPLE_MAX 255u
static const uint16_t kYmAmplitude[YM_LEVELS] = {
62u, 161u, 265u, 377u, 580u, 774u, 1155u, 1575u, 2260u, 3088u, 4570u, 6233u, 9330u, 12481u, 18663u, 25000u
};
// So the step from channel A's level 0 to silence's level is no click, the
// pump starts with a ramp up to silence and, once everything has played,
// ends with one down to 0 before it stops (the drain): PWM_RAMP_SAMPLES,
// ~10 ms, from PWM_DRAIN_MARGIN samples ahead of the ISR.
#define PWM_RAMP_SAMPLES 128u
#define PWM_DRAIN_MARGIN 64u
// The drain waits for this long of silence, so sounds close together keep
// the level at silence between them rather than ramp down and up again.
#define PWM_IDLE_MS 200u
#endif #endif
// jlAudioTickRegister rides TOS's 200 Hz Timer C (both builds: the plain // jlAudioTickRegister rides TOS's 200 Hz Timer C (both builds: the plain
@ -152,6 +186,7 @@
// the interrupt, as the contract allows (joey/audio.h). // the interrupt, as the contract allows (joey/audio.h).
#define VEC_MFP_TC (0x114 / 4) #define VEC_MFP_TC (0x114 / 4)
#define ST_TIMER_C_HZ 200u #define ST_TIMER_C_HZ 200u
#define SETEXC_READ (-1L) // Setexc's vector argument that only reads
// ----- Module state ----- // ----- Module state -----
@ -170,6 +205,16 @@ static uint8_t gPlayHalf = 0u;
// The PWM ISR's play pointer and the buffer's end (audio_isr.s). // The PWM ISR's play pointer and the buffer's end (audio_isr.s).
volatile uint8_t *gMixPos = NULL; volatile uint8_t *gMixPos = NULL;
volatile uint8_t *gMixEnd = NULL; volatile uint8_t *gMixEnd = NULL;
// Each sample's YM level (kYmAmplitude), built at jlpAudioInit: the ISR
// plays gYmLevel[sample], from beside its own code (audio_isr.s).
extern uint8_t gYmLevel[SAMPLE_VALUES];
// The drain (PWM_RAMP_SAMPLES): the ramp down is written, from
// PWM_DRAIN_MARGIN past gDrainFrom, where the ISR was; and the silence
// before it (PWM_IDLE_MS), since gIdleSinceMs.
static bool gDraining = false;
static uint16_t gDrainFrom = 0u;
static bool gIdle = false;
static uint32_t gIdleSinceMs = 0u;
extern void mfpTimerAIsr(void); extern void mfpTimerAIsr(void);
@ -211,6 +256,9 @@ static uint8_t gModTailHalf = 0;
// SFX overlay shared with the DOS HAL (see src/core/audioSfxMix.c). // SFX overlay shared with the DOS HAL (see src/core/audioSfxMix.c).
// Mixed in over the module's output during jlpAudioFrameTick. // Mixed in over the module's output during jlpAudioFrameTick.
static AudioSfxSlotT gSfxSlots[JL_AUDIO_SFX_SLOTS]; static AudioSfxSlotT gSfxSlots[JL_AUDIO_SFX_SLOTS];
#ifdef JOEYLIB_ATARI_STE
static uint8_t gSfxPan[JL_AUDIO_SFX_SLOTS]; // jlAudioSetPan's
#endif
// Single shadow of YM mixer reg 7's low 6 bits (tone+noise disables). // Single shadow of YM mixer reg 7's low 6 bits (tone+noise disables).
// Both the PWM (installTimerA) and PSG-tone (applyYmVoice) paths // Both the PWM (installTimerA) and PSG-tone (applyYmVoice) paths
@ -238,11 +286,20 @@ static long applyYmVoice(void);
static bool anySfxActive(void); static bool anySfxActive(void);
static uint16_t playOffset(void); static uint16_t playOffset(void);
static void pollRefill(void); static void pollRefill(void);
static bool pumpStart(void);
static void pumpStop(void);
static void refillHalf(uint8_t half); static void refillHalf(uint8_t half);
static void sfxOverlayAt(uint16_t at, uint16_t len, uint8_t slot); static void sfxOverlayAt(uint16_t at, uint16_t len, uint8_t slot);
static void sfxStartNow(uint8_t slot); static void sfxStartNow(uint8_t slot, bool fresh);
static void silenceMixBuffer(void); static void silenceMixBuffer(void);
static void tickUnhook(void); static void tickHookUpdate(void);
#ifndef JOEYLIB_ATARI_STE
static void drainCancel(void);
static bool drainDone(void);
static void drainStart(void);
static void rampWrite(uint16_t at, uint8_t from, uint8_t to);
static void ymLevelsBuild(void);
#endif
#ifdef JOEYLIB_ATARI_STE #ifdef JOEYLIB_ATARI_STE
static uint16_t dmaPlayOffset(void); static uint16_t dmaPlayOffset(void);
static void dmaSetFrame(volatile uint8_t *hi, volatile uint8_t *mid, volatile uint8_t *lo, uint32_t addr); static void dmaSetFrame(volatile uint8_t *hi, volatile uint8_t *mid, volatile uint8_t *lo, uint32_t addr);
@ -285,11 +342,14 @@ static long installTimerA(void) {
// Park the buffer in known-silent state (unsigned 8-bit middle // Park the buffer in known-silent state (unsigned 8-bit middle
// = 128) before opening the IRQ gate. // = 128) before opening the IRQ gate.
silenceMixBuffer(); silenceMixBuffer();
rampWrite(0u, 0u, MIX_SILENCE);
gMixPos = (volatile uint8_t *)gMixBuf; gMixPos = (volatile uint8_t *)gMixBuf;
gMixEnd = (volatile uint8_t *)(gMixBuf + MIX_BUFFER); gMixEnd = (volatile uint8_t *)(gMixBuf + MIX_BUFFER);
gNeedRefill[0] = 0; gNeedRefill[0] = 0;
gNeedRefill[1] = 0; gNeedRefill[1] = 0;
gPlayHalf = 0u; gPlayHalf = 0u;
gDraining = false;
gIdle = false;
// YM2149 setup for PWM-via-volume on channel A: // YM2149 setup for PWM-via-volume on channel A:
// reg 7 (mixer): set bits 0 (tone A off) and 3 (noise A off); // reg 7 (mixer): set bits 0 (tone A off) and 3 (noise A off);
@ -413,7 +473,7 @@ static long installDma(void) {
gPlayHalf = 0u; gPlayHalf = 0u;
dmaSetFrame(STE_DMA_START_HI, STE_DMA_START_MID, STE_DMA_START_LO, (uint32_t)gMixBuf); dmaSetFrame(STE_DMA_START_HI, STE_DMA_START_MID, STE_DMA_START_LO, (uint32_t)gMixBuf);
dmaSetFrame(STE_DMA_END_HI, STE_DMA_END_MID, STE_DMA_END_LO, (uint32_t)(gMixBuf + MIX_BUFFER)); dmaSetFrame(STE_DMA_END_HI, STE_DMA_END_MID, STE_DMA_END_LO, (uint32_t)(gMixBuf + MIX_BUFFER));
*STE_DMA_MODE = STE_DMA_MONO_12K; *STE_DMA_MODE = STE_DMA_STEREO_12K;
*STE_DMA_CTRL = STE_DMA_PLAY_LOOP; *STE_DMA_CTRL = STE_DMA_PLAY_LOOP;
return 0; return 0;
} }
@ -466,14 +526,18 @@ static void refillHalf(uint8_t half) {
dst = gMixBuf + (uint16_t)half * MIX_HALF; dst = gMixBuf + (uint16_t)half * MIX_HALF;
silentFill = !modPlayerActive() && !anySfxActive(); silentFill = !modPlayerActive() && !anySfxActive();
if (modPlayerActive()) { if (modPlayerActive()) {
if (!modPlayerRenderU8((uint8_t *)dst, MIX_HALF)) { if (!modPlayerRenderU8((uint8_t *)dst, MIX_HALF_FRAMES, MIX_CHANNELS)) {
gModTail = true; gModTail = true;
gModTailHalf = half; gModTailHalf = half;
} }
} else { } else {
memset((void *)dst, MIX_SILENCE_UNSIGNED, MIX_HALF); memset((void *)dst, MIX_SILENCE_UNSIGNED, MIX_HALF);
} }
#ifdef JOEYLIB_ATARI_STE
audioSfxOverlayMixStereo(dst, MIX_HALF_FRAMES, gSfxSlots, JL_AUDIO_SFX_SLOTS, gSfxPan, AUDIO_SFX_DST_UNSIGNED);
#else
audioSfxOverlayMix(dst, MIX_HALF, gSfxSlots, JL_AUDIO_SFX_SLOTS); audioSfxOverlayMix(dst, MIX_HALF, gSfxSlots, JL_AUDIO_SFX_SLOTS);
#endif
#ifdef JOEYLIB_ATARI_STE #ifdef JOEYLIB_ATARI_STE
dmaSignBytes(dst, MIX_HALF); dmaSignBytes(dst, MIX_HALF);
#endif #endif
@ -482,15 +546,15 @@ static void refillHalf(uint8_t half) {
} }
// Mix a slot over bytes of the buffer that are mixed already, in the // Mix a slot over len bytes of the buffer from at that are mixed already,
// output's own format: the STe's are signed, so flipped for the mix and back. // in the output's own format: the STe's are signed stereo pairs, the slot
// on its pan's side. The chip may reach them while this runs, so they are
// mixed as they stand, in order, never flipped to unsigned and back.
static void sfxOverlayAt(uint16_t at, uint16_t len, uint8_t slot) { static void sfxOverlayAt(uint16_t at, uint16_t len, uint8_t slot) {
#ifdef JOEYLIB_ATARI_STE #ifdef JOEYLIB_ATARI_STE
dmaSignBytes(gMixBuf + at, len); audioSfxOverlayMixStereo(gMixBuf + at, (uint16_t)(len / MIX_CHANNELS), &gSfxSlots[slot], 1u, &gSfxPan[slot], AUDIO_SFX_DST_SIGNED);
#endif #else
audioSfxOverlayMix(gMixBuf + at, len, &gSfxSlots[slot], 1u); audioSfxOverlayMix(gMixBuf + at, len, &gSfxSlots[slot], 1u);
#ifdef JOEYLIB_ATARI_STE
dmaSignBytes(gMixBuf + at, len);
#endif #endif
} }
@ -499,32 +563,43 @@ static void sfxOverlayAt(uint16_t at, uint16_t len, uint8_t slot) {
// the play position are mixed already, so on its own a sound waited for // the play position are mixed already, so on its own a sound waited for
// the next refill: up to two halves, ~80-170 ms. So its first samples go on // the next refill: up to two halves, ~80-170 ms. So its first samples go on
// top of what is mixed ahead -- the rest of the half playing and, if it has // top of what is mixed ahead -- the rest of the half playing and, if it has
// been refilled already, the half after it -- and the refills carry on from // been refilled already, the half after it -- from SFX_START_MARGIN past the
// where that leaves the slot. // play position, which is in the next half when the play position is that
static void sfxStartNow(uint8_t slot) { // near its half's end; and the refills carry on from where that leaves the
// slot.
static void sfxStartNow(uint8_t slot, bool fresh) {
uint16_t pos; uint16_t pos;
uint16_t start; uint16_t start;
uint16_t halfEnd; uint16_t halfEnd;
uint16_t aheadEnd;
uint8_t half; uint8_t half;
bool nextReady;
pos = playOffset(); // A pump just started plays from the buffer's start; the STe's frame
// counter may still say where the chip stopped last time until its new
// frame begins.
pos = fresh ? 0u : playOffset();
if (pos >= MIX_BUFFER) { if (pos >= MIX_BUFFER) {
return; return;
} }
half = (uint8_t)(pos >= MIX_HALF); half = (uint8_t)(pos >= MIX_HALF);
// The play position may have moved on since the frame tick last looked, // The play position may have moved on since the frame tick last looked,
// leaving a half behind that waits for its refill: that refill places // leaving a half behind that waits for its refill: that refill places
// the sound there. // the sound there. Offsets here run on past the buffer's end into the
nextReady = half == gPlayHalf; // other half, unwrapped.
start = (uint16_t)(pos + SFX_START_MARGIN);
halfEnd = (uint16_t)((half + 1u) * MIX_HALF); halfEnd = (uint16_t)((half + 1u) * MIX_HALF);
aheadEnd = (uint16_t)(halfEnd + (half == gPlayHalf ? MIX_HALF : 0u));
// A whole frame, so the sides keep their places.
start = (uint16_t)((pos / MIX_CHANNELS + SFX_START_MARGIN) * MIX_CHANNELS);
if (start >= aheadEnd) {
return;
}
if (start < halfEnd) { if (start < halfEnd) {
sfxOverlayAt(start, (uint16_t)(halfEnd - start), slot); sfxOverlayAt(start, (uint16_t)(halfEnd - start), slot);
gHalfSilent[half] = false; gHalfSilent[half] = false;
start = halfEnd;
} }
if (nextReady) { if (start < aheadEnd) {
sfxOverlayAt((uint16_t)((half ^ 1u) * MIX_HALF), MIX_HALF, slot); sfxOverlayAt((uint16_t)((half ^ 1u) * MIX_HALF + (start - halfEnd)), (uint16_t)(aheadEnd - start), slot);
gHalfSilent[half ^ 1u] = false; gHalfSilent[half ^ 1u] = false;
} }
} }
@ -541,10 +616,96 @@ static bool anySfxActive(void) {
} }
static void pumpStart(void) { #ifndef JOEYLIB_ATARI_STE
if (gPumpRunning) { // A sound starts while the drain is under way: the ramp down and the zeros
// after it, ahead of the ISR, become a ramp back up to silence and silence,
// the baseline the start-now overlay mixes onto.
static void drainCancel(void) {
uint16_t at;
uint16_t i;
if (!gDraining) {
return; return;
} }
gDraining = false;
at = (uint16_t)((playOffset() + PWM_DRAIN_MARGIN) % MIX_BUFFER);
for (i = 0u; i < MIX_BUFFER - 2u * PWM_DRAIN_MARGIN; i++) {
gMixBuf[(at + i) % MIX_BUFFER] = MIX_SILENCE;
}
rampWrite(at, 0u, MIX_SILENCE);
gHalfSilent[0] = false;
gHalfSilent[1] = false;
}
// The ISR has played the ramp down and is into the zeros after it: as far
// from where it was as the ramp's end, and not back round to it.
static bool drainDone(void) {
return (uint16_t)((playOffset() + MIX_BUFFER - gDrainFrom) % MIX_BUFFER) >= PWM_DRAIN_MARGIN + PWM_RAMP_SAMPLES;
}
// Everything has played: from just ahead of the ISR, a ramp down to level
// 0, then zeros round the buffer to just behind it; the pump stops once
// the ISR is past the ramp (drainDone).
static void drainStart(void) {
uint16_t at;
uint16_t i;
gDrainFrom = playOffset();
at = (uint16_t)((gDrainFrom + PWM_DRAIN_MARGIN) % MIX_BUFFER);
for (i = 0u; i < MIX_BUFFER - 2u * PWM_DRAIN_MARGIN; i++) {
gMixBuf[(at + i) % MIX_BUFFER] = 0u;
}
rampWrite(at, MIX_SILENCE, 0u);
gDraining = true;
}
// PWM_RAMP_SAMPLES of a straight line from sample value from to to, into
// the buffer from at on (round its end).
static void rampWrite(uint16_t at, uint8_t from, uint8_t to) {
uint16_t i;
for (i = 0u; i < PWM_RAMP_SAMPLES; i++) {
gMixBuf[(at + i) % MIX_BUFFER] = (uint8_t)((int16_t)from + ((int16_t)to - (int16_t)from) * (int16_t)i / (int16_t)PWM_RAMP_SAMPLES);
}
}
// gYmLevel: for each sample value, the level whose amplitude is nearest
// the value's place on the line from level 0's amplitude to level 15's.
static void ymLevelsBuild(void) {
uint32_t target;
uint32_t gap;
uint32_t best;
uint16_t s;
uint8_t v;
for (s = 0u; s < SAMPLE_VALUES; s++) {
target = kYmAmplitude[0] + ((uint32_t)(kYmAmplitude[YM_LEVELS - 1u] - kYmAmplitude[0]) * s) / YM_SAMPLE_MAX;
best = UINT32_MAX;
gYmLevel[s] = 0u;
for (v = 0u; v < YM_LEVELS; v++) {
gap = kYmAmplitude[v] > target ? kYmAmplitude[v] - target : target - kYmAmplitude[v];
if (gap < best) {
best = gap;
gYmLevel[s] = v;
}
}
}
}
#endif
// True when the pump was idle: it now plays from the buffer's start.
static bool pumpStart(void) {
if (gPumpRunning) {
#ifndef JOEYLIB_ATARI_STE
drainCancel();
#endif
return false;
}
#ifdef JOEYLIB_ATARI_STE #ifdef JOEYLIB_ATARI_STE
Supexec(installDma); Supexec(installDma);
#else #else
@ -553,6 +714,8 @@ static void pumpStart(void) {
gPumpRunning = true; gPumpRunning = true;
gHalfSilent[0] = false; gHalfSilent[0] = false;
gHalfSilent[1] = false; gHalfSilent[1] = false;
tickHookUpdate();
return true;
} }
@ -571,6 +734,7 @@ static void pumpStop(void) {
#else #else
Supexec(uninstallTimerA); Supexec(uninstallTimerA);
#endif #endif
tickHookUpdate();
} }
@ -585,19 +749,37 @@ bool jlpAudioInit(void) {
if (!steMachineCheck()) { if (!steMachineCheck()) {
return false; return false;
} }
#else
ymLevelsBuild();
#endif #endif
#ifdef JOEYLIB_ATARI_STE
memset(gSfxPan, JL_AUDIO_PAN_CENTER, sizeof(gSfxPan));
#endif
modPlayerSetU8Shift(MIX_UNIT_SHIFT);
audioSfxSetShift(MIX_UNIT_SHIFT);
gReady = true; gReady = true;
return true; return true;
} }
#ifdef JOEYLIB_ATARI_STE
// A slot's side, for the sound it plays and the next (the DMA chip plays
// stereo; the plain ST's one DAC cannot pan).
void jlpAudioSetPan(uint8_t slot, uint8_t pan) {
if (slot < JL_AUDIO_SFX_SLOTS && pan < JL_AUDIO_PAN_COUNT) {
gSfxPan[slot] = pan;
}
}
#endif
void jlpAudioShutdown(void) { void jlpAudioShutdown(void) {
// Pump first, then the PSG: stopping the pump hands channel A back and // Pump first, then the PSG: stopping the pump hands channel A back and
// replays voice 0, so the all-off has to come after it or a replayed note // replays voice 0, so the all-off has to come after it or a replayed note
// would outlive the program. (The all-off still runs independent of // would outlive the program. (The all-off still runs independent of
// gReady: the tone/noise voice layer is usable without jlpAudioInit, and // gReady: the tone/noise voice layer is usable without jlpAudioInit, and
// the YM latches -- a live voice keeps sounding after exit.) // the YM latches -- a live voice keeps sounding after exit.)
tickUnhook(); (void)jlpAudioTickRegister(NULL, 0u);
if (gReady) { if (gReady) {
pumpStop(); pumpStop();
} }
@ -625,7 +807,7 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
if (!modPlayerStart(data, length, MIX_RATE, loop)) { if (!modPlayerStart(data, length, MIX_RATE, loop)) {
return; return;
} }
pumpStart(); (void)pumpStart();
} }
@ -634,8 +816,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
return; return;
} }
audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE); audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE);
pumpStart(); sfxStartNow(slot, pumpStart());
sfxStartNow(slot);
} }
@ -644,8 +825,7 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
return; return;
} }
audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE); audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE);
pumpStart(); sfxStartNow(slot, pumpStart());
sfxStartNow(slot);
} }
@ -661,8 +841,12 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
// Volume mapping: attenuation is 2 dB a step on every port (joey/audio.h), // Volume mapping: attenuation is 2 dB a step on every port (joey/audio.h),
// and the YM's sixteen levels are about 3.5 dB apart (measured: 0, 3.5, // and the YM's sixteen levels are about 3.5 dB apart (measured: 0, 3.5,
// 7.4, 10.4, 13.8, 16.5, 19.9, 22.7, 26.0, 28.8, 32.3, 35.3, 39.1, 42.8 and // 7.4, 10.4, 13.8, 16.5, 19.9, 22.7, 26.0, 28.8, 32.3, 35.3, 39.1, 42.8 and
// 49.4 dB below level 15), so each attenuation takes the nearest level: // 49.4 dB below level 15), so each attenuation takes the level nearest 2 dB
// within 1.6 dB of the asked one, the closest the chip can come. YM also // a step below attenuation 0's. That top level is where the mix puts a
// voice, as loud as a full-scale effect at MIX_UNIT_SHIFT: the plain ST's
// level 13 (its PWM plays the digital sound through a channel like these),
// within 1.4 dB a step; the STe's level 14 (its DMA sound is louder than a
// YM channel), within 1.9 dB, half the 3.9 dB from level 14 to 13. YM also
// accepts bit 4 = envelope-controlled volume; we never set it. // accepts bit 4 = envelope-controlled volume; we never set it.
// //
// YM regs are supervisor-only on the ST, so this is a Supexec block. // YM regs are supervisor-only on the ST, so this is a Supexec block.
@ -673,9 +857,15 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
#define YM_TONE_HZ 125000u // 2 MHz / 16: Hz = this / period #define YM_TONE_HZ 125000u // 2 MHz / 16: Hz = this / period
#define YM_PERIOD_MAX 0x0FFFu #define YM_PERIOD_MAX 0x0FFFu
#ifdef JOEYLIB_ATARI_STE
static const uint8_t kYmVolume[15] = { static const uint8_t kYmVolume[15] = {
15u, 14u, 14u, 13u, 13u, 12u, 12u, 11u, 10u, 10u, 9u, 8u, 8u, 7u, 6u 14u, 13u, 13u, 12u, 12u, 11u, 10u, 10u, 9u, 8u, 8u, 7u, 6u, 6u, 5u
}; };
#else
static const uint8_t kYmVolume[15] = {
13u, 12u, 12u, 11u, 10u, 10u, 9u, 8u, 8u, 7u, 7u, 6u, 5u, 5u, 4u
};
#endif
static long applyYmVoice(void) { static long applyYmVoice(void) {
@ -776,29 +966,45 @@ static long ymPsgAllOff(void) {
// The tick rides TOS's 200 Hz Timer C (stTickIsr chains to TOS's handler), // The tick rides TOS's 200 Hz Timer C (stTickIsr chains to TOS's handler),
// counting the rate against it: see the Timer C notes at the top. // counting the rate against it: see the Timer C notes at the top. The hook
// may stay in place for the pump (tickHookUpdate), so the tick is switched
// off while its fields change -- at rate 0 stTickRun's carry never comes
// due -- and its rate goes in last. Each store is one instruction, so this
// needs no interrupts held off, and runs before jlInit's supervisor mode.
bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
tickUnhook(); gStTickHz = 0u;
if (fn == NULL) {
return true;
}
gStTickFn = fn;
gStTickHz = hz;
gStTickCarry = 0u; gStTickCarry = 0u;
gStTickOldVec = (void (*)(void))Setexc(VEC_MFP_TC, (long)stTickIsr); gStTickFn = fn;
gStTickHooked = true; if (fn != NULL) {
gStTickHz = hz;
}
tickHookUpdate();
return true; return true;
} }
// TOS's Timer C vector back, if it was taken. // The Timer C hook in place while something needs it: the program's tick,
static void tickUnhook(void) { // or the plain ST's PWM pump, whose Timer A the hook lets in during TOS's
if (!gStTickHooked) { // handler (tickIsr.s). TOS's vector is read before the hook goes in, so an
// interrupt between the two never chains to a vector not yet saved.
static void tickHookUpdate(void) {
bool want;
#ifdef JOEYLIB_ATARI_STE
want = gStTickFn != NULL;
#else
want = gStTickFn != NULL || gPumpRunning;
#endif
if (want == gStTickHooked) {
return; return;
} }
if (want) {
gStTickOldVec = (void (*)(void))Setexc(VEC_MFP_TC, SETEXC_READ);
(void)Setexc(VEC_MFP_TC, (long)stTickIsr);
} else {
(void)Setexc(VEC_MFP_TC, (long)gStTickOldVec); (void)Setexc(VEC_MFP_TC, (long)gStTickOldVec);
gStTickHooked = false; }
gStTickFn = NULL; gStTickHooked = want;
} }
@ -904,6 +1110,15 @@ void jlpAudioFrameTick(void) {
if (!gReady || !gPumpRunning) { if (!gReady || !gPumpRunning) {
return; return;
} }
#ifndef JOEYLIB_ATARI_STE
// The drain owns the buffer until the ISR is past its ramp.
if (gDraining) {
if (drainDone()) {
pumpStop();
}
return;
}
#endif
pollRefill(); pollRefill();
// An ended song's last half, flagged for refill, has played. // An ended song's last half, flagged for refill, has played.
if (gModTail && gNeedRefill[gModTailHalf]) { if (gModTail && gNeedRefill[gModTailHalf]) {
@ -916,8 +1131,21 @@ void jlpAudioFrameTick(void) {
refillHalf(1u); refillHalf(1u);
} }
// Drain-to-silence stop: both halves hold pure silence and nothing // Drain-to-silence stop: both halves hold pure silence and nothing
// is playing, so the ISR would stream silence forever -- park it. // is playing, so the ISR would stream silence forever -- park it (on
// the plain ST once PWM_IDLE_MS of it have gone by, after a ramp down
// to level 0: drainStart).
if (!modPlayerActive() && gHalfSilent[0] && gHalfSilent[1] && !anySfxActive()) { if (!modPlayerActive() && gHalfSilent[0] && gHalfSilent[1] && !anySfxActive()) {
#ifdef JOEYLIB_ATARI_STE
pumpStop(); pumpStop();
#else
if (!gIdle) {
gIdle = true;
gIdleSinceMs = jlpMillisElapsed();
} else if (jlpMillisElapsed() - gIdleSinceMs >= PWM_IDLE_MS) {
drainStart();
}
} else {
gIdle = false;
#endif
} }
} }

View file

@ -7,21 +7,23 @@
| |
| Each interrupt: | Each interrupt:
| 1. Reads one byte from the mix buffer (unsigned 8-bit). | 1. Reads one byte from the mix buffer (unsigned 8-bit).
| 2. Writes its high 4 bits to YM register 8 (channel A volume) so | 2. Writes its YM level (_gYmLevel[byte], the level nearest the
| the analog filter on the YM output emits a 4-bit-resolution | sample's value: the YM's levels are logarithmic) to YM register 8
| sample for that tick. | (channel A volume), so the analog filter on the YM output emits
| that sample for that tick.
| 3. Advances the play pointer, wrapping at the end of the buffer. | 3. Advances the play pointer, wrapping at the end of the buffer.
| 4. Acks the MFP Timer-A bit in ISRA and RTEs. | 4. Acks the MFP Timer-A bit in ISRA and RTEs.
| The frame tick reads the play pointer to see which half has played, as | The frame tick reads the play pointer to see which half has played, as
| the STe build reads its DMA chip's, so the ISR raises no refill flags. | the STe build reads its DMA chip's, so the ISR raises no refill flags.
| At 12.3 kHz every cycle here is ~1.2% of the 68000: the chip registers | At 12.3 kHz every cycle here is ~1.2% of the 68000: the chip registers
| are reached by short addresses, and the high nibble needs no mask (the | are reached by short addresses.
| YM takes the byte's low 4 bits).
| |
| Communication with the C HAL is via three globals defined in audio.c: | Communication with the C HAL is via three globals defined in audio.c:
| _gMixBuf -- start of the mix buffer (uint8_t array) | _gMixBuf -- start of the mix buffer (uint8_t array)
| _gMixPos -- volatile pointer to the next sample to play | _gMixPos -- volatile pointer to the next sample to play
| _gMixEnd -- end of buffer (used to detect wrap) | _gMixEnd -- end of buffer (used to detect wrap)
| and _gYmLevel, each sample value's YM level (256 bytes), defined here
| and filled by audio.c.
| |
| GAS-syntax (binutils m68k); assembled by m68k-atari-mint-as via the | GAS-syntax (binutils m68k); assembled by m68k-atari-mint-as via the
| gcc driver. | gcc driver.
@ -34,13 +36,13 @@
.equ YM_VOL_A, 8 .equ YM_VOL_A, 8
.equ MFP_ISRA, 0xFFFFFA0F .equ MFP_ISRA, 0xFFFFFA0F
.equ ISRA_ACK, 0xDF | clear Timer A's in-service bit .equ ISRA_ACK, 0xDF | clear Timer A's in-service bit
.equ NIBBLE, 4
_mfpTimerAIsr: _mfpTimerAIsr:
movem.l %d0/%a0,-(%sp) movem.l %d0/%a0,-(%sp)
movea.l _gMixPos,%a0 movea.l _gMixPos,%a0
moveq #0,%d0
move.b (%a0)+,%d0 move.b (%a0)+,%d0
lsr.b #NIBBLE,%d0 move.b (_gYmLevel,%pc,%d0.w),%d0
move.b #YM_VOL_A,(YM_SELECT).w move.b #YM_VOL_A,(YM_SELECT).w
move.b %d0,(YM_DATA).w move.b %d0,(YM_DATA).w
cmpa.l _gMixEnd,%a0 cmpa.l _gMixEnd,%a0
@ -51,3 +53,11 @@ _mfpTimerAIsr:
move.b #ISRA_ACK,(MFP_ISRA).w move.b #ISRA_ACK,(MFP_ISRA).w
movem.l (%sp)+,%d0/%a0 movem.l (%sp)+,%d0/%a0
rte rte
| Each sample value's YM level, which audio.c fills at jlpAudioInit. Here,
| right after the code that reads it, so the read is PC-relative: a table
| anywhere else would cost an address register (saved and restored) and
| its load every sample, and on the plain ST the mix already runs about as
| fast as the interrupt plays it. TOS does not protect a program's text.
.globl _gYmLevel
_gYmLevel: .space 256

View file

@ -14,15 +14,17 @@
// for a new palette region. For pattern.c's 8 uniform bands this // for a new palette region. For pattern.c's 8 uniform bands this
// is 8 entries; in the worst case it is 200 (one per scanline). // is 8 entries; in the worst case it is 200 (one per scanline).
// * VBL ISR pre-loads the first band's palette, then programs // * VBL ISR pre-loads the first band's palette, then programs
// MFP Timer B (event-count mode, TBDR = HBL delta to first // MFP Timer B (event-count mode) to fire at the end of the display
// transition) to fire at the END of the last scanline before // of the line TWO above the next band (fireDelta).
// the next band starts. // * Timer B's handler (rasterIsr.s) loads the band's palette into
// * Timer B ISR writes the current band's palette, advances the // registers, waits for the video address counter to reach the end
// transition index, and (stop/reload TBDR/restart) reprograms // of the next line's display, and stores all 16 colours in the
// Timer B to fire at the next transition. With 8 transitions per // horizontal blank before the band, so its first line shows them
// frame the ISR runs 8 times instead of 313 -- well under the // from its first pixel; rasterBandNext then aims it at the next
// ~147-HBL-fires-per-frame cap Hatari's MFP emulation imposes on // band. With 8 transitions per frame it runs 8 times instead of 313
// event-count mode, and ~0.2% CPU overhead vs ~60% for per-HBL. // -- well under the ~147-HBL-fires-per-frame cap Hatari's MFP
// emulation imposes on event-count mode -- and waits about a line
// each time, ~3% of the CPU.
// * A table set (ScreenTablesT) holds each of the 16 palettes already // * A table set (ScreenTablesT) holds each of the 16 palettes already
// in shifter format and the band table; jlpPresent requantizes only // in shifter format and the band table; jlpPresent requantizes only
// the palettes jlPaletteSet changed (gStagePaletteDirtyMask), and the // the palettes jlPaletteSet changed (gStagePaletteDirtyMask), and the
@ -137,12 +139,22 @@ typedef char stPresentSlamGeometryFits[(JL_SURFACE_HEIGHT == 200 && ST_BYTES_PER
// to zero). Both bytes are odd-addressed byte registers; byte access // to zero). Both bytes are odd-addressed byte registers; byte access
// to an odd address is legal on 68000 (only word/long faults). // to an odd address is legal on 68000 (only word/long faults).
#define ST_VID_BASE_HI ((volatile uint8_t *)0xFFFF8201L) // bits 23..16 #define ST_VID_BASE_HI ((volatile uint8_t *)0xFFFF8201L) // bits 23..16
#define ST_VID_COUNT_MID ((volatile uint8_t *)0xFFFF8207L) // video address counter, bits 15..8
#define ST_VID_COUNT_LOW ((volatile uint8_t *)0xFFFF8209L) // bits 7..0
#define ST_VID_BASE_MID ((volatile uint8_t *)0xFFFF8203L) // bits 15..8 #define ST_VID_BASE_MID ((volatile uint8_t *)0xFFFF8203L) // bits 15..8
// MFP hardware addresses. // MFP hardware addresses.
#define ST_MFP_TBCR ((volatile uint8_t *)0xFFFFFA1BL) // Timer B control #define ST_MFP_TBCR ((volatile uint8_t *)0xFFFFFA1BL) // Timer B control
#define ST_MFP_TBDR ((volatile uint8_t *)0xFFFFFA21L) // Timer B data #define ST_MFP_TBDR ((volatile uint8_t *)0xFFFFFA21L) // Timer B data
#define ST_MFP_ISRA ((volatile uint8_t *)0xFFFFFA0FL) // In-service A #define ST_MFP_ISRA ((volatile uint8_t *)0xFFFFFA0FL) // In-service A
#define ST_MFP_IPRB ((volatile uint8_t *)0xFFFFFA0DL) // Interrupt pending B
#define ST_MFP_TCDR ((volatile uint8_t *)0xFFFFFA23L) // Timer C data: the live count
#define ST_MFP_TIMER_C 0x20u // Timer C's bit in IPRB
// TOS's 200 Hz system clock: MFP Timer C at 2.4576 MHz / 64, reloaded with
// 192, its ticks counted at $4BA (_hz_200).
#define ST_HZ200 ((volatile uint32_t *)0x4BAUL)
#define ST_TICK_RELOAD 192u
#define ST_TICK_MS 5u
#define MFP_TBCR_STOP 0x00 #define MFP_TBCR_STOP 0x00
#define MFP_TBCR_EVENT 0x08 #define MFP_TBCR_EVENT 0x08
#define MFP_TB_CLEAR 0xFE // clear bit 0 of ISRA (Timer B) #define MFP_TB_CLEAR 0xFE // clear bit 0 of ISRA (Timer B)
@ -280,16 +292,24 @@ static inline __attribute__((always_inline)) uint16_t stFloodStopBits(const uint
// about 38 ms of 8 MHz 68000 per jlPaletteSet, which is what made Space // about 38 ms of 8 MHz 68000 per jlPaletteSet, which is what made Space
// Taxi's palette-cycled warp drop frames on the ST); and the band table, // Taxi's palette-cycled warp drop frames on the ST); and the band table,
// one entry per palette change down the screen: at display line // one entry per palette change down the screen: at display line
// bandStart[i], load palette bandPalIdx[i]. Built from the jlSurfaceT's // bandStart[i], load palette bandPalIdx[i]. Timer B fires for band i
// palettes and SCB by jlpPresent. // fireDelta[i] display lines after it fired for band i - 1 (for band 1,
// after the frame starts): at the end of the line two above it, or as
// soon after the last fire as it can (buildTransitions). Built from the
// jlSurfaceT's palettes and SCB by jlpPresent.
#define MAX_BANDS JL_SURFACE_HEIGHT #define MAX_BANDS JL_SURFACE_HEIGHT
// Timer B fires this many lines above a band: its handler then has a whole
// line to load the colours before the blank they go in (rasterIsr.s).
#define RASTER_LEAD_LINES 2
typedef struct { typedef struct {
uint16_t palettes [JL_SURFACE_PALETTE_COUNT][JL_SURFACE_COLORS_PER_PALETTE]; uint16_t palettes [JL_SURFACE_PALETTE_COUNT][JL_SURFACE_COLORS_PER_PALETTE];
uint16_t bandStart [MAX_BANDS]; uint16_t bandStart [MAX_BANDS];
uint8_t bandPalIdx[MAX_BANDS]; uint8_t bandPalIdx[MAX_BANDS];
uint8_t fireDelta [MAX_BANDS];
uint16_t bandCount; uint16_t bandCount;
} ScreenTablesT; } ScreenTablesT;
static void flipWait(void);
static void presentShow(const uint8_t *page); static void presentShow(const uint8_t *page);
static uint16_t quantizeColorToSt(uint16_t orgb); static uint16_t quantizeColorToSt(uint16_t orgb);
static void quantizeDirtyPalettes(ScreenTablesT *tables, const jlSurfaceT *src); static void quantizeDirtyPalettes(ScreenTablesT *tables, const jlSurfaceT *src);
@ -298,13 +318,15 @@ static ScreenTablesT *tablesToWrite(void);
extern void stLeaveSuper(void *savedSsp); extern void stLeaveSuper(void *savedSsp);
static long readFrameHz(void); static long readFrameHz(void);
static long readHz200(void); static long readClockMs(void);
static long writePrevPaletteRegs(void); static long writePrevPaletteRegs(void);
static __attribute__((interrupt_handler)) void timerBIsr(void);
static __attribute__((interrupt_handler)) void vblIsr(void); static __attribute__((interrupt_handler)) void vblIsr(void);
static void buildTransitions(ScreenTablesT *tables, const jlSurfaceT *src); static void buildTransitions(ScreenTablesT *tables, const jlSurfaceT *src);
static void rasterAim(const ScreenTablesT *tables, uint16_t band);
void rasterBandNext(void);
static void refreshPaletteStateIfNeeded(const jlSurfaceT *src); static void refreshPaletteStateIfNeeded(const jlSurfaceT *src);
extern void stRasterIsr(void);
// ----- Module state ----- // ----- Module state -----
@ -378,15 +400,27 @@ static volatile uint8_t gTablesShown = 0;
static volatile uint8_t gTablesPending = TABLES_NONE; static volatile uint8_t gTablesPending = TABLES_NONE;
static uint8_t gTablesWriting = TABLES_NONE; static uint8_t gTablesWriting = TABLES_NONE;
// Index of the band the Timer B ISR is currently scheduling TO. At // Index of the band whose palette is on the shifter. At VBL this is 0
// VBL this is 0 (band 0 palette pre-loaded, Timer B scheduled to // (band 0 palette pre-loaded, Timer B scheduled for band 1); each Timer B
// fire when it's time to transition to band 1). Each ISR fire writes // fire loads the next band's and advances it (rasterBandNext).
// the palette for gCurrentBand and advances to the next.
static volatile uint16_t gCurrentBand = 0; static volatile uint16_t gCurrentBand = 0;
// What the Timer B handler (rasterIsr.s) loads next: the band's 16 colours,
// and the video address counter's low 16 bits when the display of the line
// above the band ends (rasterAim), from gRasterBase, the counter at the
// frame's start (the shown page's address).
const uint16_t *gStRasterColors = gTables[0].palettes[0];
uint16_t gStRasterTarget = 0u;
static uint16_t gRasterBase = 0u;
// The VBL count (jlpFrameCount). // The VBL count (jlpFrameCount).
static volatile int16_t gFrameCount = 0; static volatile int16_t gFrameCount = 0;
// A flip handed over (presentShow) that no VBL has shown yet: until then
// the shifter still shows the page before it, the one the next present
// would write (flipWait). The VBL ISR clears it.
static volatile uint8_t gFlipPending = 0u;
// Saved exception vectors for restore on shutdown. // Saved exception vectors for restore on shutdown.
static void (*gOldVblVec)(void) = NULL; static void (*gOldVblVec)(void) = NULL;
static void (*gOldTimerBVec)(void) = NULL; static void (*gOldTimerBVec)(void) = NULL;
@ -407,6 +441,9 @@ static void (*gOldTimerBVec)(void) = NULL;
// bandPalIdx[i] is the palette index that band uses. // bandPalIdx[i] is the palette index that band uses.
static void buildTransitions(ScreenTablesT *tables, const jlSurfaceT *src) { static void buildTransitions(ScreenTablesT *tables, const jlSurfaceT *src) {
uint16_t line; uint16_t line;
uint16_t band;
int16_t fire;
int16_t lastFire;
uint8_t idx; uint8_t idx;
uint8_t prev; uint8_t prev;
@ -427,6 +464,28 @@ static void buildTransitions(ScreenTablesT *tables, const jlSurfaceT *src) {
prev = idx; prev = idx;
} }
} }
// Timer B fires for band i at the end of display line bandStart[i] - 2
// (RASTER_LEAD_LINES), the frame's line -1 being its start, or a line
// after the last fire when that is later: a band one line below the
// last has its colours by the line after it starts.
lastFire = -1;
for (band = 1; band < tables->bandCount; band++) {
fire = (int16_t)(tables->bandStart[band] - RASTER_LEAD_LINES);
if (fire <= lastFire) {
fire = (int16_t)(lastFire + 1);
}
tables->fireDelta[band] = (uint8_t)(fire - lastFire);
lastFire = fire;
}
}
// A present right after another, before the VBL that shows its flip, would
// write the page the shifter still shows: wait for that VBL first. A
// program that waits for the VBL between presents never waits here.
static void flipWait(void) {
while (gFlipPending != 0u) {
}
} }
@ -442,6 +501,7 @@ static void presentShow(const uint8_t *page) {
sr = m68kInterruptsOff(); sr = m68kInterruptsOff();
if (page != NULL) { if (page != NULL) {
setShifterVideoBase(page); setShifterVideoBase(page);
gFlipPending = 1u;
} }
if (gTablesWriting != TABLES_NONE) { if (gTablesWriting != TABLES_NONE) {
gTablesPending = gTablesWriting; gTablesPending = gTablesWriting;
@ -521,93 +581,57 @@ static ScreenTablesT *tablesToWrite(void) {
} }
// Timer B interrupt handler. Fires once at each band transition; // Aim the Timer B handler at band: its colours, and the counter's value when
// writes the band's palette to the shifter and lets Timer B's // the display of the line above it ends.
// auto-reload keep counting for the next fire. We deliberately do static void rasterAim(const ScreenTablesT *tables, uint16_t band) {
// NOT stop/reload/restart the timer here: that sequence would cost gStRasterColors = tables->palettes[tables->bandPalIdx[band]];
// 1-2 HBL edges each fire, and those losses compound across 7+ gStRasterTarget = (uint16_t)(gRasterBase + tables->bandStart[band] * ST_BYTES_PER_ROW);
// transitions into a visible "last band short" drift. }
// Timer B's work after its handler (rasterIsr.s) has loaded the next band's
// colours: advance to it, aim at the band after it, and end the interrupt.
// Timer B's auto-reload keeps counting for the next fire. We deliberately
// do NOT stop/reload/restart the timer here: that sequence would cost 1-2
// HBL edges each fire, and those losses compound across 7+ transitions into
// a visible "last band short" drift.
// //
// MFP event-count auto-reload latches the CURRENT TBDR at each // MFP event-count auto-reload latches the CURRENT TBDR at each underflow,
// underflow, so a value written here does not take effect on the // so a value written here does not take effect on the NEXT fire (that
// NEXT fire (that interval was already reloaded at the underflow // interval was already reloaded at the underflow that triggered this
// that triggered this ISR) but on the fire AFTER next. To time // interrupt) but on the fire AFTER next: the band + 2 delta.
// variable-height bands correctly we therefore program one fire void rasterBandNext(void) {
// ahead: in the ISR that transitions to `band` we load TBDR with
// the band+1 -> band+2 delta (the interval that the band+1
// underflow will reload and use to time the band+2 transition).
// Uniform bands (like pattern.c) have all deltas equal, so the
// one-ahead value is identical and they stay perfectly aligned.
static void timerBIsr(void) {
const ScreenTablesT *tables; const ScreenTablesT *tables;
uint16_t band; uint16_t band;
const uint16_t *src;
volatile uint16_t *dst;
uint16_t nextDelta;
tables = &gTables[gTablesShown]; tables = &gTables[gTablesShown];
band = gCurrentBand + 1; band = (uint16_t)(gCurrentBand + 1u);
gCurrentBand = band; gCurrentBand = band;
if (band + 1u < tables->bandCount) {
if (band < tables->bandCount) { rasterAim(tables, (uint16_t)(band + 1u));
// buildTransitions already folded out-of-range SCB values to 0. if (band + 2u < tables->bandCount) {
src = tables->palettes[tables->bandPalIdx[band]]; *ST_MFP_TBDR = tables->fireDelta[band + 2u];
dst = ST_PALETTE_REGS;
dst[ 0] = src[ 0];
dst[ 1] = src[ 1];
dst[ 2] = src[ 2];
dst[ 3] = src[ 3];
dst[ 4] = src[ 4];
dst[ 5] = src[ 5];
dst[ 6] = src[ 6];
dst[ 7] = src[ 7];
dst[ 8] = src[ 8];
dst[ 9] = src[ 9];
dst[10] = src[10];
dst[11] = src[11];
dst[12] = src[12];
dst[13] = src[13];
dst[14] = src[14];
dst[15] = src[15];
if (band + 1 < tables->bandCount) {
// A transition to band+1 is still pending; keep Timer B
// running. The band+1 underflow has already had its reload
// value latched (set one fire earlier), so program TBDR
// now with the band+1 -> band+2 delta, which the band+1
// underflow will reload and use to time band+2. Don't
// stop the timer.
if (band + 2 < tables->bandCount) {
nextDelta = tables->bandStart[band + 2] - tables->bandStart[band + 1];
if (nextDelta == 0 || nextDelta > 255) {
nextDelta = 1;
} }
*ST_MFP_TBDR = (uint8_t)nextDelta; } else {
} // No further transitions this frame.
*ST_MFP_ISRA = MFP_TB_CLEAR;
return;
}
}
// No further transitions this frame; stopping Timer B here only
// affects the (never-used) next fire, not timing of any band
// we've already scheduled.
*ST_MFP_TBCR = MFP_TBCR_STOP; *ST_MFP_TBCR = MFP_TBCR_STOP;
}
*ST_MFP_ISRA = MFP_TB_CLEAR; *ST_MFP_ISRA = MFP_TB_CLEAR;
} }
// Vertical blank handler. Pre-loads band 0's palette so the first // Vertical blank handler. Pre-loads band 0's palette so the first
// visible scanline is correct, then programs Timer B to fire at // visible scanline is correct, then programs Timer B to fire for the next
// the HBL delta to the next band transition (if any). // band transition (if any). The video address counter holds the shown
// page's address until the display starts: the band targets' base.
static void vblIsr(void) { static void vblIsr(void) {
const ScreenTablesT *tables; const ScreenTablesT *tables;
uint16_t delta;
const uint16_t *src; const uint16_t *src;
volatile uint16_t *dst; volatile uint16_t *dst;
gFrameCount = gFrameCount + 1; gFrameCount = gFrameCount + 1;
gCurrentBand = 0; gCurrentBand = 0;
gFlipPending = 0u;
// The table set the last present handed over shows from this frame, as // The table set the last present handed over shows from this frame, as
// does the page it flipped to (presentShow). // does the page it flipped to (presentShow).
if (gTablesPending != TABLES_NONE) { if (gTablesPending != TABLES_NONE) {
@ -642,26 +666,20 @@ static void vblIsr(void) {
// Program Timer B for the next band transition. // Program Timer B for the next band transition.
if (tables->bandCount > 1) { if (tables->bandCount > 1) {
delta = tables->bandStart[1] - tables->bandStart[0]; gRasterBase = (uint16_t)((*ST_VID_COUNT_MID << 8) | *ST_VID_COUNT_LOW);
if (delta == 0 || delta > 255) { rasterAim(tables, 1u);
delta = 1;
}
*ST_MFP_TBCR = MFP_TBCR_STOP; *ST_MFP_TBCR = MFP_TBCR_STOP;
*ST_MFP_TBDR = (uint8_t)delta; *ST_MFP_TBDR = tables->fireDelta[1];
*ST_MFP_ISRA = MFP_TB_CLEAR; *ST_MFP_ISRA = MFP_TB_CLEAR;
*ST_MFP_TBCR = MFP_TBCR_EVENT; *ST_MFP_TBCR = MFP_TBCR_EVENT;
// The restart above loads the counter from TBDR (= the band // The restart above loads the counter from TBDR (band 1's
// 0 -> 1 delta), so the first fire is correctly timed. Now // delta), so the first fire is correctly timed. Now pre-program
// pre-program TBDR with the band 1 -> 2 delta so the band-1 // TBDR with band 2's so the band-1 underflow auto-reloads with
// underflow auto-reloads with the right interval (one fire // the right interval (one fire ahead, matching rasterBandNext).
// ahead, matching timerBIsr). Writing TBDR while counting // Writing TBDR while counting only changes the next reload, not
// only changes the next reload, not the in-flight count. // the in-flight count.
if (tables->bandCount > 2) { if (tables->bandCount > 2) {
delta = tables->bandStart[2] - tables->bandStart[1]; *ST_MFP_TBDR = tables->fireDelta[2];
if (delta == 0 || delta > 255) {
delta = 1;
}
*ST_MFP_TBDR = (uint8_t)delta;
} }
} else { } else {
*ST_MFP_TBCR = MFP_TBCR_STOP; *ST_MFP_TBCR = MFP_TBCR_STOP;
@ -713,6 +731,14 @@ bool jlpInit(const jlConfigT *config) {
} }
#endif #endif
// A monochrome monitor (SM124) takes only the high resolution, which
// Getrez reports as the shifter's own mode number: the 16-colour
// 320x200 mode cannot be shown there.
if (Getrez() == ST_RES_MONO) {
coreSetError("a monochrome monitor cannot show the 16-colour 320x200 mode");
return false;
}
// Enter supervisor mode for the program's lifetime. The shifter // Enter supervisor mode for the program's lifetime. The shifter
// video-base, palette, and MFP registers live in the supervisor-only // video-base, palette, and MFP registers live in the supervisor-only
// $FF8000..$FFFFFF I/O region; jlpPresent pokes the video-base bytes // $FF8000..$FFFFFF I/O region; jlpPresent pokes the video-base bytes
@ -781,10 +807,11 @@ bool jlpInit(const jlConfigT *config) {
gOldTimerBVec = (void (*)(void))Setexc(VEC_MFP_TB, -1L); gOldTimerBVec = (void (*)(void))Setexc(VEC_MFP_TB, -1L);
(void)Setexc(VEC_VBL, (long)vblIsr); (void)Setexc(VEC_VBL, (long)vblIsr);
// Program MFP Timer B: event-count (HBL) mode, initial TBDR=1 // Install MFP Timer B's vector (stRasterIsr) stopped, TBDR=1 a
// (a placeholder -- VBL ISR reprograms it for the first real // placeholder, then enable it in IMRA: the VBL ISR starts it in
// transition per frame), vector=timerBIsr, then enable in IMRA. // event-count (HBL) mode each frame with bands, once it has aimed the
Xbtimer(1, MFP_TBCR_EVENT, 1, timerBIsr); // handler at the first (rasterAim).
Xbtimer(1, MFP_TBCR_STOP, 1, stRasterIsr);
Jenabint(INT_TIMER_B); Jenabint(INT_TIMER_B);
return true; return true;
@ -873,6 +900,7 @@ void jlpPresent(const jlSurfaceT *src) {
backIdx = (uint8_t)(gFrontIdx ^ 1u); backIdx = (uint8_t)(gFrontIdx ^ 1u);
backPage = gScreenPage[backIdx]; backPage = gScreenPage[backIdx];
flipWait();
slam = present68kStSlam(pd->base, backPage); slam = present68kStSlam(pd->base, backPage);
copied = (uint16_t)slam; copied = (uint16_t)slam;
@ -948,6 +976,32 @@ uint16_t jlpFrameCount(void) {
} }
// Milliseconds on TOS's 200 Hz clock: the ticks counted at $4BA, plus how
// far Timer C's live count has run down since its last reload, so the clock
// steps every 26 us, not every 5 ms. Read again if a tick is counted between
// the reads; a reload whose interrupt is still pending (IPRB) belongs to
// this reading only if the count read is the reloaded one, near the top.
// The MFP and $4BA need supervisor mode: the program holds it for its whole
// lifetime (jlpInit's Super(0L)), so the hot path reads them directly; the
// Supexec trap is only the pre-init / post-shutdown fallback. The core
// counts from jlInit (it subtracts the reading jlInit took).
static long readClockMs(void) {
uint32_t ticks;
uint8_t count;
bool pending;
do {
ticks = *ST_HZ200;
count = *ST_MFP_TCDR;
pending = (*ST_MFP_IPRB & ST_MFP_TIMER_C) != 0u;
} while (ticks != *ST_HZ200);
if (pending && count > ST_TICK_RELOAD / 2u) {
ticks++;
}
return (long)(ticks * ST_TICK_MS + (uint32_t)(ST_TICK_RELOAD - count) * ST_TICK_MS / ST_TICK_RELOAD);
}
uint16_t jlpFrameHz(void) { uint16_t jlpFrameHz(void) {
if (gSuperEntered) { if (gSuperEntered) {
return (uint16_t)readFrameHz(); return (uint16_t)readFrameHz();
@ -973,37 +1027,11 @@ static long readFrameHz(void) {
} }
// TOS maintains a 200 Hz long counter at $4BA, incremented by the
// Timer C VBL chain. The program holds supervisor mode for its whole
// lifetime (jlpInit Super(0L) -- same authority the $FFFF82xx reads in
// jlpFrameHz rely on), so the hot path reads $4BA directly; the
// Supexec trap (~100+ cyc GEMDOS round trip per call) is only the
// pre-init / post-shutdown fallback (rank 17). Resolution is 5 ms,
// plenty for sound scheduling at 60 Hz AGI ticks.
static volatile uint32_t gMillisBase200 = 0;
static volatile bool gMillisBaseSet = false;
static long readHz200(void) {
return (long)*((volatile uint32_t *)0x4BAUL);
}
uint32_t jlpMillisElapsed(void) { uint32_t jlpMillisElapsed(void) {
uint32_t ticks;
if (gSuperEntered) { if (gSuperEntered) {
ticks = (uint32_t)readHz200(); return (uint32_t)readClockMs();
} else {
ticks = (uint32_t)Supexec(readHz200);
} }
// 0 is a legal counter value (power-on tick 0 and every ~248-day return (uint32_t)Supexec(readClockMs);
// wrap), so use an explicit one-shot flag instead of a value-0
// sentinel to capture the base.
if (!gMillisBaseSet) {
gMillisBase200 = ticks;
gMillisBaseSet = true;
return 0u;
}
// 200 Hz counter -> ms = ticks * 5. Unsigned subtraction is wrap-safe.
return (uint32_t)((ticks - gMillisBase200) * 5u);
} }

View file

@ -32,9 +32,9 @@
// (caps-lock toggles on scan 0x3A); Ctrl chords are commands, not // (caps-lock toggles on scan 0x3A); Ctrl chords are commands, not
// text, and produce no character, and so are Alt ones but for the // text, and produce no character, and so are Alt ones but for the
// characters a national keyboard's Alt layer types (a German one's // characters a national keyboard's Alt layer types (a German one's
// @ [ ] { } \), where the OS keeps that layer in its Keytbl tables: // @ [ ] { } \): from the OS's Keytbl tables on TOS 4.00 and later and
// TOS from 4.00, and EmuTOS. Older TOS keeps it inside the keyboard // on EmuTOS, and from stAltLayer.c's copies of the layers older TOS
// code JoeyLib takes over. The IKBD sends one make per press // keeps inside the keyboard code JoeyLib takes over. The IKBD sends one make per press
// (TOS's software auto-repeat is gone with ikbdsys), so the poll repeats // (TOS's software auto-repeat is gone with ikbdsys), so the poll repeats
// a held key's character itself, at TOS's Kbrate settings. // a held key's character itself, at TOS's Kbrate settings.
@ -44,9 +44,9 @@
#include <mint/ostruct.h> #include <mint/ostruct.h>
#include <mint/sysvars.h> #include <mint/sysvars.h>
#include "m68kIrq.h"
#include "port.h" #include "port.h"
#include "inputInternal.h" #include "inputInternal.h"
#include "stAltLayer.h"
#include "joey/surface.h" #include "joey/surface.h"
#ifdef JOEYLIB_ATARI_STE #ifdef JOEYLIB_ATARI_STE
@ -86,6 +86,8 @@
#define TOS_ALT_LAYER_VERSION 0x0400u #define TOS_ALT_LAYER_VERSION 0x0400u
#define EMUTOS_MAGIC 0x45544F53uL #define EMUTOS_MAGIC 0x45544F53uL
#define EMUTOS_DUAL_KEYBOARD 0x0001u #define EMUTOS_DUAL_KEYBOARD 0x0001u
// os_conf: bit 0 PAL, the rest the ROM's country.
#define TOS_CONF_COUNTRY_SHIFT 1u
// Private ISR->poll ring for translated characters. Power of two so // Private ISR->poll ring for translated characters. Power of two so
// the wrap is a mask; holds ISR_CHAR_QUEUE_SIZE - 1 pending bytes. // the wrap is a mask; holds ISR_CHAR_QUEUE_SIZE - 1 pending bytes.
@ -154,9 +156,9 @@ typedef struct {
// ----- Prototypes ----- // ----- Prototypes -----
static uint8_t altChar(const uint8_t *pairs, uint8_t code); static uint8_t altChar(const uint8_t *pairs, uint8_t code);
static void altLayerFind(const KeyTabT *keyTab);
static void charRepeat(void); static void charRepeat(void);
static long ikbdHandler(void); static long ikbdHandler(void);
static bool keyTabHasAlt(const KeyTabT *keyTab);
#ifdef JOEYLIB_ATARI_STE #ifdef JOEYLIB_ATARI_STE
static void padToStick(const StePadT *pad, jlJoystickE js); static void padToStick(const StePadT *pad, jlJoystickE js);
#endif #endif
@ -248,9 +250,14 @@ static bool gHooked = false;
// uint8_t (not bool) so element size matches gKeyState's. See // uint8_t (not bool) so element size matches gKeyState's. See
// src/core/inputInternal.h for the full rationale. // src/core/inputInternal.h for the full rationale.
static volatile uint8_t gIsrState[JL_KEY_COUNT]; static volatile uint8_t gIsrState[JL_KEY_COUNT];
// Keys pressed since the last poll, so a tap between two polls still reads // Each key's presses, counted by the ISR (which alone writes them), and the
// down for one (inputLatch). // count the last poll saw: a key whose count moved was pressed since, so a
static volatile uint8_t gIsrTapped[JL_KEY_COUNT]; // tap between two polls still reads down for one (inputLatch's meaning).
// The poll writes nothing the ISR does, so it needs no interrupts held off:
// held off across the keys, the plain ST's 12288 Hz PWM pump lost five
// samples a poll, and digital sound played 1.4% flat.
static volatile uint8_t gIsrTaps[JL_KEY_COUNT];
static uint8_t gTapsSeen[JL_KEY_COUNT];
// Mouse delta accumulator. Each ACIA mouse packet adds dx/dy here; the // Mouse delta accumulator. Each ACIA mouse packet adds dx/dy here; the
// poll routine clamps the running absolute position into the surface // poll routine clamps the running absolute position into the surface
@ -274,7 +281,7 @@ static const uint8_t *gKeyTabUnshift = NULL;
static const uint8_t *gKeyTabShift = NULL; static const uint8_t *gKeyTabShift = NULL;
static const uint8_t *gKeyTabCaps = NULL; static const uint8_t *gKeyTabCaps = NULL;
// Its Alt layer: (scan code, character) pairs ended by a 0, or NULL where // Its Alt layer: (scan code, character) pairs ended by a 0, or NULL where
// the OS keeps none in the tables (keyTabHasAlt). // the keyboard has none (altLayerFind).
static const uint8_t *gKeyTabAltUnshift = NULL; static const uint8_t *gKeyTabAltUnshift = NULL;
static const uint8_t *gKeyTabAltShift = NULL; static const uint8_t *gKeyTabAltShift = NULL;
static const uint8_t *gKeyTabAltCaps = NULL; static const uint8_t *gKeyTabAltCaps = NULL;
@ -321,6 +328,32 @@ static uint8_t altChar(const uint8_t *pairs, uint8_t code) {
} }
// The Alt layer: Keytbl's own pair lists where its struct goes on with them
// (TOS_ALT_LAYER_VERSION), else the ones TOS before 4.00 keeps inside its
// keyboard code, by the ROM's country (stAltLayer.c). Read in supervisor
// mode (jlpInit's), as the OS header is.
static void altLayerFind(const KeyTabT *keyTab) {
const OSHEADER *os;
bool inTables;
os = ((const OSHEADER *)*_sysbase)->os_beg;
if ((uint32_t)os->p_rsv2 == EMUTOS_MAGIC) {
// Unless its Alt tables are a second whole layout: then no layer.
inTables = (keyTab->features & EMUTOS_DUAL_KEYBOARD) == 0u;
} else if (os->os_version >= TOS_ALT_LAYER_VERSION) {
inTables = true;
} else {
(void)stAltLayerFind((uint8_t)(os->os_conf >> TOS_CONF_COUNTRY_SHIFT), &gKeyTabAltUnshift, &gKeyTabAltShift, &gKeyTabAltCaps);
return;
}
if (inTables) {
gKeyTabAltUnshift = keyTab->altUnshift;
gKeyTabAltShift = keyTab->altShift;
gKeyTabAltCaps = keyTab->altCaps;
}
}
// The held key's character again, once TOS's delay has passed since it went // The held key's character again, once TOS's delay has passed since it went
// down and then at TOS's rate (the ISR names the key, gIsrRepeatCode). // down and then at TOS's rate (the ISR names the key, gIsrRepeatCode).
static void charRepeat(void) { static void charRepeat(void) {
@ -445,7 +478,7 @@ static long ikbdHandler(void) {
if (key != JL_KEY_NONE) { if (key != JL_KEY_NONE) {
gIsrState[key] = inputKeyHeld(gIsrState[key], entry, !isBreak); gIsrState[key] = inputKeyHeld(gIsrState[key], entry, !isBreak);
if (!isBreak) { if (!isBreak) {
gIsrTapped[key] = 1u; gIsrTaps[key]++;
} }
} }
@ -486,19 +519,6 @@ static long ikbdHandler(void) {
} }
// Whether keyTab goes on with the Alt layer (TOS_ALT_LAYER_VERSION). Read in
// supervisor mode (jlpInit's), as the OS header is.
static bool keyTabHasAlt(const KeyTabT *keyTab) {
const OSHEADER *os;
os = ((const OSHEADER *)*_sysbase)->os_beg;
if ((uint32_t)os->p_rsv2 == EMUTOS_MAGIC) {
return (keyTab->features & EMUTOS_DUAL_KEYBOARD) == 0u;
}
return os->os_version >= TOS_ALT_LAYER_VERSION;
}
#ifdef JOEYLIB_ATARI_STE #ifdef JOEYLIB_ATARI_STE
// A Jaguar pad as a stick: its directions, and its A and B as the two // A Jaguar pad as a stick: its directions, and its A and B as the two
// buttons. // buttons.
@ -572,7 +592,8 @@ void jlpInputInit(void) {
memset(gKeyState, 0, sizeof(gKeyState)); memset(gKeyState, 0, sizeof(gKeyState));
memset(gKeyPrev, 0, sizeof(gKeyPrev)); memset(gKeyPrev, 0, sizeof(gKeyPrev));
memset((void *)gIsrState, 0, sizeof(gIsrState)); memset((void *)gIsrState, 0, sizeof(gIsrState));
memset((void *)gIsrTapped, 0, sizeof(gIsrTapped)); memset((void *)gIsrTaps, 0, sizeof(gIsrTaps));
memset(gTapsSeen, 0, sizeof(gTapsSeen));
memset((void *)gIsrJoyByte, 0, sizeof(gIsrJoyByte)); memset((void *)gIsrJoyByte, 0, sizeof(gIsrJoyByte));
// Cache the TOS keyboard translation tables (the OS keymap) for // Cache the TOS keyboard translation tables (the OS keymap) for
@ -583,11 +604,7 @@ void jlpInputInit(void) {
gKeyTabUnshift = keyTab->unshift; gKeyTabUnshift = keyTab->unshift;
gKeyTabShift = keyTab->shift; gKeyTabShift = keyTab->shift;
gKeyTabCaps = keyTab->caps; gKeyTabCaps = keyTab->caps;
if (keyTabHasAlt(keyTab)) { altLayerFind(keyTab);
gKeyTabAltUnshift = keyTab->altUnshift;
gKeyTabAltShift = keyTab->altShift;
gKeyTabAltCaps = keyTab->altCaps;
}
} }
// Seed caps lock from TOS: taking over ikbdsys freezes the OS's // Seed caps lock from TOS: taking over ikbdsys freezes the OS's
// own tracking, so an engaged caps lock at launch would otherwise // own tracking, so an engaged caps lock at launch would otherwise
@ -638,14 +655,15 @@ void jlpInputPoll(void) {
uint8_t btn; uint8_t btn;
uint8_t joy; uint8_t joy;
uint8_t tail; uint8_t tail;
uint8_t taps;
uint16_t i; uint16_t i;
uint16_t sr;
// Interrupts held off across the latch, so a press between its read of a // Down when held, or pressed since the last poll (gIsrTaps).
// key's tap and its clearing of it is not lost. for (i = 0u; i < JL_KEY_COUNT; i++) {
sr = m68kInterruptsOff(); taps = gIsrTaps[i];
inputLatch(gKeyState, gIsrState, gIsrTapped, JL_KEY_COUNT); gKeyState[i] = (uint8_t)(gIsrState[i] | (taps != gTapsSeen[i]));
m68kInterruptsRestore(sr); gTapsSeen[i] = taps;
}
// Drain ISR-translated characters into the core queue. Single- // Drain ISR-translated characters into the core queue. Single-
// consumer side of the SPSC ring: only gIsrCharTail is written // consumer side of the SPSC ring: only gIsrCharTail is written

68
src/atarist/rasterIsr.s Normal file
View file

@ -0,0 +1,68 @@
| Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
| See LICENSE.md for license information.
| SPDX-License-Identifier: MIT
|
| The ST family's Timer B band handler (hal.c has the rest). A band's 16
| colours are 16 words, and the shifter shows a line's pixels from about
| cycle 56 to 376 of its 512 (508 at 60 Hz), so they must all land in the
| 190 or so cycles between two lines' displays. Taking the interrupt and
| copying 16 words takes longer than that, so Timer B fires a line early,
| at the end of the display of the line two above the band (hal.c,
| buildTransitions). This handler loads the colours (_gStRasterColors) into
| eight registers, waits for the video address counter to reach
| _gStRasterTarget -- its value at the end of the next line's display --
| and stores all 16 with one movem.l, about 72 cycles. rasterBandNext (C)
| then sets up the next band and ends the interrupt.
|
| It waits with the interrupt mask at 5, so the plain ST's PWM pump (Timer
| A, above Timer B in the MFP) is not held off past a sample, and raises it
| to 6 for the last RASTER_NEAR bytes and the store, so nothing lands in
| between. A counter more than RASTER_FAR bytes short of the target (a
| frame whose start the VBL handler did not see) is not waited for: the
| colours go at once.
|
| GAS-syntax (binutils m68k); assembled by m68k-atari-mint-as via the
| gcc driver.
.text
.globl _stRasterIsr
.equ VIDEO_COUNTER_MID, 0xffff8207 | the counter's middle byte; its low byte is 2 on
.equ PALETTE, 0xffff8240
.equ SR_ABOVE_TIMER_B, 0x2500 | supervisor, interrupt mask 5
.equ SR_MFP_HELD, 0x2600 | supervisor, interrupt mask 6
.equ RASTER_NEAR, 32 | bytes of display: about 64 cycles
.equ RASTER_FAR, 480 | bytes of display: three lines
_stRasterIsr:
move.w #SR_ABOVE_TIMER_B,%sr
movem.l %d0-%d7/%a0-%a2,-(%sp)
move.l _gStRasterColors,%a0
movem.l (%a0),%d1-%d7/%a2
move.w _gStRasterTarget,%a0
lea VIDEO_COUNTER_MID,%a1
movep.w 0(%a1),%d0
sub.w %a0,%d0
cmp.w #-RASTER_FAR,%d0
blt.s .Lstore
cmp.w #-RASTER_NEAR,%d0
bge.s .Lnear
.Lfar:
movep.w 0(%a1),%d0
sub.w %a0,%d0
cmp.w #-RASTER_NEAR,%d0
blt.s .Lfar
.Lnear:
move.w #SR_MFP_HELD,%sr
tst.w %d0
bpl.s .Lstore
.Lclose:
movep.w 0(%a1),%d0
sub.w %a0,%d0
bmi.s .Lclose
.Lstore:
movem.l %d1-%d7/%a2,PALETTE
move.w #SR_ABOVE_TIMER_B,%sr
jsr _rasterBandNext
movem.l (%sp)+,%d0-%d7/%a0-%a2
rte

View file

@ -6,7 +6,11 @@
// //
// The base ST's one RS-232 line is the Modem port, driven by the 68901 MFP // The base ST's one RS-232 line is the Modem port, driven by the 68901 MFP
// USART and wrapped by TOS as BIOS logical device 1 (AUX) with a receive- // USART and wrapped by TOS as BIOS logical device 1 (AUX) with a receive-
// buffered IOREC. This HAL rides that: XBIOS Rsconf sets baud/flow/format, // buffered IOREC. A Mega STe, TT, or Falcon has more (its SCC's two ports,
// the TT its second MFP's), and TOS 2 and EmuTOS map AUX onto any of them
// with XBIOS Bconmap: JL_SERIAL_SLOT unit n maps it to Bconmap device 5 + n
// while the port is open, and the close maps it back; every other device
// opens AUX as it is mapped. This HAL rides AUX: XBIOS Rsconf sets baud/flow/format,
// Bconin/Bconout move bytes, Bconstat/Bcostat gate them. All are GEMDOS/BIOS/ // Bconin/Bconout move bytes, Bconstat/Bcostat gate them. All are GEMDOS/BIOS/
// XBIOS traps that switch to supervisor internally, so -- unlike the direct // XBIOS traps that switch to supervisor internally, so -- unlike the direct
// $FFFFFAxx pokes elsewhere in the ST HAL -- serial needs no supervisor // $FFFFFAxx pokes elsewhere in the ST HAL -- serial needs no supervisor
@ -27,6 +31,13 @@
#define ST_AUX_DEV 1 #define ST_AUX_DEV 1
#define ST_IOREC_RS232 0 // Iorec's own numbering: 0 RS-232, 1 IKBD, 2 MIDI #define ST_IOREC_RS232 0 // Iorec's own numbering: 0 RS-232, 1 IKBD, 2 MIDI
// Bconmap's devices: the Modem port (the MFP's, whose DTR and RTS are the
// sound chip's port A lines) is the first. Bconmap(BMAP_CHECK) answers 0
// where Bconmap exists (TOS hands back the opcode where it does not).
#define ST_BCONMAP_MODEM 6
#define ST_BCONMAP_NONE 0
#define ST_NO_SUCH_PORT "serial: no such port here (JL_SERIAL_SLOT unit)"
// 68901 MFP USART control register (UCR) bit fields, built by serialUcr: // 68901 MFP USART control register (UCR) bit fields, built by serialUcr:
// bit 7 clock divide (1 = /16, standard async) // bit 7 clock divide (1 = /16, standard async)
// bits6-5 word length (00=8, 01=7, 10=6, 11=5) -> (8 - dataBits) // bits6-5 word length (00=8, 01=7, 10=6, 11=5) -> (8 - dataBits)
@ -58,6 +69,8 @@
#define ST_RX_BUFFER_BYTES 4096 #define ST_RX_BUFFER_BYTES 4096
#define ST_RX_LOW_WATER (ST_RX_BUFFER_BYTES / 4) #define ST_RX_LOW_WATER (ST_RX_BUFFER_BYTES / 4)
#define ST_RX_HIGH_WATER (ST_RX_BUFFER_BYTES / 4 * 3) #define ST_RX_HIGH_WATER (ST_RX_BUFFER_BYTES / 4 * 3)
// A write's backstop on a busy transmitter, in Bcostat polls (serialTxStalled):
// the clock bounds it first, once it runs.
#define ST_TX_WAIT_POLLS 200000ul #define ST_TX_WAIT_POLLS 200000ul
@ -69,6 +82,12 @@
static int16_t gSavedBaudCode = ST_RSCONF_NO_CHANGE; static int16_t gSavedBaudCode = ST_RSCONF_NO_CHANGE;
static int16_t gSavedUcr = ST_RSCONF_NO_CHANGE; static int16_t gSavedUcr = ST_RSCONF_NO_CHANGE;
// The Bconmap device AUX is mapped to while the port is open (the Modem port
// where TOS has no Bconmap), and the one a SLOT open mapped it away from
// (ST_BCONMAP_NONE: none), for the close to put back.
static int16_t gAuxDevice = ST_BCONMAP_MODEM;
static int16_t gSavedMap = ST_BCONMAP_NONE;
// TOS's receive record, and its own buffer settings while ours is in. // TOS's receive record, and its own buffer settings while ours is in.
static _IOREC *gRxIorec = NULL; static _IOREC *gRxIorec = NULL;
static _IOREC gSavedRxIorec; static _IOREC gSavedRxIorec;
@ -78,6 +97,7 @@ static char gRxBuffer[ST_RX_BUFFER_BYTES];
// ----- Prototypes ----- // ----- Prototypes -----
static bool serialAuxMap(jlSerialDeviceE device, uint8_t unit);
static int16_t serialBaudCode(uint32_t baud); static int16_t serialBaudCode(uint32_t baud);
static int32_t serialRsconf(int16_t baud, int16_t flow, int16_t ucr); static int32_t serialRsconf(int16_t baud, int16_t flow, int16_t ucr);
static long serialRxBufferInstall(void); static long serialRxBufferInstall(void);
@ -87,6 +107,32 @@ static int16_t serialUcr(const jlSerialConfigT *cfg);
// ----- Helpers ----- // ----- Helpers -----
// Point AUX at the port the open asked for: a SLOT unit maps it to Bconmap
// device 5 + unit, remembering the device it was on (gSavedMap); any other
// device leaves it where it is. Where TOS has no Bconmap, unit 1, the Modem
// port, is the only one. Sets gAuxDevice; false when there is no such port.
static bool serialAuxMap(jlSerialDeviceE device, uint8_t unit) {
const _BCONMAP *table;
bool mapped;
mapped = Bconmap(BMAP_CHECK) == 0;
gAuxDevice = mapped ? (int16_t)Bconmap(BMAP_INQUIRE) : ST_BCONMAP_MODEM;
if (device != JL_SERIAL_SLOT) {
return true;
}
if (!mapped) {
return unit == 1u;
}
table = (const _BCONMAP *)Bconmap(BMAP_MAPTAB);
if (unit == 0u || unit > (uint16_t)table->maptabsize) {
return false;
}
gSavedMap = (int16_t)Bconmap((int16_t)(ST_BCONMAP_MODEM + unit - 1u));
gAuxDevice = (int16_t)(ST_BCONMAP_MODEM + unit - 1u);
return true;
}
// Rsconf's speed codes are the index here, 0 fastest to 15 slowest. A rate // Rsconf's speed codes are the index here, 0 fastest to 15 slowest. A rate
// the MFP does not have plays at the fastest it has below it (the slowest, // the MFP does not have plays at the fastest it has below it (the slowest,
// 50, for anything under that), as every port rounds: 19200 is its top. // 50, for anything under that), as every port rounds: 19200 is its top.
@ -233,6 +279,13 @@ void jlpSerialClose(void) {
(void)serialRsconf(gSavedBaudCode, ST_RSCONF_NO_CHANGE, gSavedUcr); (void)serialRsconf(gSavedBaudCode, ST_RSCONF_NO_CHANGE, gSavedUcr);
gSavedBaudCode = ST_RSCONF_NO_CHANGE; gSavedBaudCode = ST_RSCONF_NO_CHANGE;
gSavedUcr = ST_RSCONF_NO_CHANGE; gSavedUcr = ST_RSCONF_NO_CHANGE;
// AUX back on the port it was on before a SLOT open, its settings above
// put back first.
if (gSavedMap != ST_BCONMAP_NONE) {
(void)Bconmap(gSavedMap);
gSavedMap = ST_BCONMAP_NONE;
}
gAuxDevice = ST_BCONMAP_MODEM;
} }
@ -250,7 +303,10 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
int32_t inquired; int32_t inquired;
uint32_t previous; uint32_t previous;
(void)device; // the base ST has one RS-232 line: the Modem port. if (!serialAuxMap(device, config->unit)) {
coreSetError(ST_NO_SUCH_PORT);
return false;
}
switch (config->flow) { switch (config->flow) {
case JL_SERIAL_FLOW_RTSCTS: case JL_SERIAL_FLOW_RTSCTS:
flow = 2; flow = 2;
@ -277,7 +333,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
// Raise DTR, as the open does on every port: TOS only sets it at boot, so a line an earlier // Raise DTR, as the open does on every port: TOS only sets it at boot, so a line an earlier
// jlSerialSetDtr(false) hung up would otherwise stay down. // jlSerialSetDtr(false) hung up would otherwise stay down.
Offgibit(ST_GI_DTR_ON_MASK); jlpSerialSetDtr(true);
// Snapshot the baud code BEFORE installing ours -- the inquire reports the // Snapshot the baud code BEFORE installing ours -- the inquire reports the
// rate currently in force. Range-gate the answer: a TOS that did not // rate currently in force. Range-gate the answer: a TOS that did not
@ -323,8 +379,12 @@ uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
// DTR on demand (jlSerialSetDtr, how a program hangs up). Neither Rsconf nor anything else in EmuTOS // DTR on demand (jlSerialSetDtr, how a program hangs up). Neither Rsconf nor anything else in EmuTOS
// touches port A bit 4 after boot (Hatari psg_write + xbios trace), and close does not either, so a // touches port A bit 4 after boot (Hatari psg_write + xbios trace), and close does not either, so a
// dropped DTR stays dropped. // dropped DTR stays dropped. Those lines are the Modem port's: TOS gives no way to the other ports'
// (an SCC's are its own registers, behind TOS's driver), so there it does nothing.
void jlpSerialSetDtr(bool on) { void jlpSerialSetDtr(bool on) {
if (gAuxDevice != ST_BCONMAP_MODEM) {
return;
}
if (on) { if (on) {
Offgibit(ST_GI_DTR_ON_MASK); Offgibit(ST_GI_DTR_ON_MASK);
} else { } else {
@ -334,8 +394,11 @@ void jlpSerialSetDtr(bool on) {
// RTS on demand (jlSerialSetRts). Under RTS/CTS TOS's receive interrupt drives RTS itself, at the // RTS on demand (jlSerialSetRts). Under RTS/CTS TOS's receive interrupt drives RTS itself, at the
// buffer's water marks, so it may change it again. // buffer's water marks, so it may change it again. The Modem port's alone, as DTR.
void jlpSerialSetRts(bool on) { void jlpSerialSetRts(bool on) {
if (gAuxDevice != ST_BCONMAP_MODEM) {
return;
}
if (on) { if (on) {
Offgibit(ST_GI_RTS_ON_MASK); Offgibit(ST_GI_RTS_ON_MASK);
} else { } else {
@ -345,17 +408,15 @@ void jlpSerialSetRts(bool on) {
uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) { uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
SerialTxWaitT wait;
uint16_t n; uint16_t n;
for (n = 0u; n < len; n++) { for (n = 0u; n < len; n++) {
uint32_t guard; // A bounded transmitter-ready wait (Bcostat -1 = ready), so a stalled
// Bounded transmitter-ready spin (Bcostat -1 = ready) so a stalled
// link (no CTS) reports a partial write instead of hanging the frame. // link (no CTS) reports a partial write instead of hanging the frame.
guard = 0u; serialTxWaitStart(&wait, ST_TX_WAIT_POLLS);
while (!Bcostat(ST_AUX_DEV)) { while (!Bcostat(ST_AUX_DEV)) {
guard++; if (serialTxStalled(&wait)) {
if (guard > ST_TX_WAIT_POLLS) {
return n; return n;
} }
} }

81
src/atarist/stAltLayer.c Normal file
View file

@ -0,0 +1,81 @@
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// The Alt layer of the national keyboards TOS before 4.00 keeps inside its
// keyboard code: the ASCII characters each country's keyboard types with
// Alt (and Alt with Shift), as Atari's ROMs for that country type them and
// EmuTOS's layout tables reproduce them (bios/keyb_*.h). Only ASCII is kept:
// jlInputGetChar delivers nothing else. A country whose TOS layer is not
// known here (French Switzerland, Denmark) types nothing with Alt, as do the
// countries whose keyboards have no such layer (the US, the UK, the
// Netherlands).
//
// Each list is (scan code, character) pairs ended by a 0, the form Keytbl's
// own Alt tables take on TOS 4.00 and later; the scan codes name key
// positions (0x1A is the key right of P, 0x27 and 0x28 the two right of L,
// 0x2B the one left of Return's lower half, 0x60 the one right of left
// Shift, 0x02-0x04 the 1-3 keys, 0x0D the one left of Backspace, 0x29 the
// one left of 1).
#include <stddef.h>
#include "stAltLayer.h"
#define TOS_COUNTRY_GERMANY 1u
#define TOS_COUNTRY_FRANCE 2u
#define TOS_COUNTRY_SPAIN 4u
#define TOS_COUNTRY_ITALY 5u
#define TOS_COUNTRY_SWEDEN 6u
#define TOS_COUNTRY_SWISS_GERMAN 8u
#define TOS_COUNTRY_FINLAND 10u
#define TOS_COUNTRY_NORWAY 11u
typedef struct {
uint8_t country;
const uint8_t *unshift;
const uint8_t *shift;
const uint8_t *caps;
} AltLayerT;
static const uint8_t kGermanPlain[] = { 0x1A, '@', 0x27, '[', 0x28, ']', 0 };
static const uint8_t kGermanShift[] = { 0x1A, '\\', 0x27, '{', 0x28, '}', 0 };
static const uint8_t kFrenchPlain[] = { 0x1A, '[', 0x1B, ']', 0x28, '\\', 0x2B, '@', 0 };
static const uint8_t kFrenchShift[] = { 0x1A, '{', 0x1B, '}', 0x2B, '~', 0 };
static const uint8_t kSpanishPlain[] = { 0x1A, '[', 0x1B, ']', 0x2B, '#', 0 };
static const uint8_t kSpanishShift[] = { 0x1A, '{', 0x1B, '}', 0x2B, '@', 0 };
static const uint8_t kItalianPlain[] = { 0x1A, '[', 0x1B, ']', 0x60, '`', 0 };
static const uint8_t kItalianShift[] = { 0x1A, '{', 0x1B, '}', 0x2B, '~', 0x60, '`', 0 };
static const uint8_t kNordicPlain[] = { 0x1A, '[', 0x1B, ']', 0x28, '`', 0x2B, '^', 0 };
static const uint8_t kNordicShift[] = { 0x1A, '{', 0x1B, '}', 0x28, '~', 0x2B, '@', 0 };
static const uint8_t kSwissGermanPlain[] = { 0x02, '|', 0x03, '@', 0x04, '#', 0x0D, '~', 0x1A, '[', 0x1B, ']', 0x28, '{', 0x29, '}', 0x2B, '\\', 0x60, '\\', 0 };
static const uint8_t kSwissGermanShift[] = { 0x1A, '{', 0x1B, '}', 0 };
static const uint8_t kNone[] = { 0 };
// Caps Lock types what no Shift does, but on the Swiss German keyboard,
// whose Caps Lock leaves the layer out.
static const AltLayerT kAltLayers[] = {
{ TOS_COUNTRY_GERMANY, kGermanPlain, kGermanShift, kGermanPlain },
{ TOS_COUNTRY_FRANCE, kFrenchPlain, kFrenchShift, kFrenchPlain },
{ TOS_COUNTRY_SPAIN, kSpanishPlain, kSpanishShift, kSpanishPlain },
{ TOS_COUNTRY_ITALY, kItalianPlain, kItalianShift, kItalianPlain },
{ TOS_COUNTRY_SWEDEN, kNordicPlain, kNordicShift, kNordicPlain },
{ TOS_COUNTRY_SWISS_GERMAN, kSwissGermanPlain, kSwissGermanShift, kNone },
{ TOS_COUNTRY_FINLAND, kNordicPlain, kNordicShift, kNordicPlain },
{ TOS_COUNTRY_NORWAY, kNordicPlain, kNordicShift, kNordicPlain },
};
bool stAltLayerFind(uint8_t country, const uint8_t **unshift, const uint8_t **shift, const uint8_t **caps) {
size_t i;
for (i = 0; i < sizeof(kAltLayers) / sizeof(kAltLayers[0]); i++) {
if (kAltLayers[i].country == country) {
*unshift = kAltLayers[i].unshift;
*shift = kAltLayers[i].shift;
*caps = kAltLayers[i].caps;
return true;
}
}
return false;
}

21
src/atarist/stAltLayer.h Normal file
View file

@ -0,0 +1,21 @@
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// The Alt layer of the national keyboards TOS before 4.00 keeps inside its
// keyboard code, where Keytbl cannot reach it (src/atarist/input.c reads the
// layer from Keytbl's tables on TOS 4.00 and later and on EmuTOS).
#ifndef JOEYLIB_ST_ALT_LAYER_H
#define JOEYLIB_ST_ALT_LAYER_H
#include <stdbool.h>
#include <stdint.h>
// The Alt layer of the TOS country `country` (the OS header's os_conf >> 1):
// three lists of (scan code, character) pairs ended by a 0, for no Shift or
// Caps Lock, with Shift, and with Caps Lock. False for a country with none
// (or none known), and the lists are then left alone.
bool stAltLayerFind(uint8_t country, const uint8_t **unshift, const uint8_t **shift, const uint8_t **caps);
#endif

View file

@ -12,13 +12,24 @@
| address (_gStTickOldVec) and returning into it with the exception frame | address (_gStTickOldVec) and returning into it with the exception frame
| untouched. The C call may use d0-d1/a0-a1, so they are kept here. | untouched. The C call may use d0-d1/a0-a1, so they are kept here.
| |
| It first lowers the interrupt mask from the 6 the MFP's interrupt raised it
| to, to 5. The MFP itself holds back Timer C and every channel below it
| until TOS's handler ends the interrupt (TOS runs it in software
| end-of-interrupt mode), so what this lets in is the channels above it: the
| plain ST's PWM pump's Timer A above all, which TOS's 50 Hz work in that
| handler (etv_timer, every fourth tick) held off past a sample, and the
| program's tick with it. The handler's rte puts back the interrupted mask.
|
| GAS-syntax (binutils m68k); assembled by m68k-atari-mint-as via the | GAS-syntax (binutils m68k); assembled by m68k-atari-mint-as via the
| gcc driver. | gcc driver.
.text .text
.globl _stTickIsr .globl _stTickIsr
.equ SR_ABOVE_TIMER_C, 0x2500 | supervisor, interrupt mask 5
_stTickIsr: _stTickIsr:
move.w #SR_ABOVE_TIMER_C,%sr
movem.l %d0-%d1/%a0-%a1,-(%sp) movem.l %d0-%d1/%a0-%a1,-(%sp)
jsr _stTickRun jsr _stTickRun
movem.l (%sp)+,%d0-%d1/%a0-%a1 movem.l (%sp)+,%d0-%d1/%a0-%a1

View file

@ -59,9 +59,9 @@ extern void stSpriteDrawShiftWalker(void);
#define ASSET_PALETTE_OFFSET 12 #define ASSET_PALETTE_OFFSET 12
#define ASSET_PALETTE_ENTRIES 16 #define ASSET_PALETTE_ENTRIES 16
// All game data is opened relative to this directory (see joey/file.h). // All game data is opened relative to this directory (see joey/file.h),
// under the port's read root (fileRoot.h).
#define DATA_DIR_PREFIX "DATA/" #define DATA_DIR_PREFIX "DATA/"
#define DATA_PATH_MAX 256
// Tile-bank payload reads stream the whole run in big freads instead of // Tile-bank payload reads stream the whole run in big freads instead of
// one 32-byte fread per tile: jlTileT is exactly JL_TILE_BYTES with no // one 32-byte fread per tile: jlTileT is exactly JL_TILE_BYTES with no
@ -208,37 +208,15 @@ static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette) {
// here is the single place the DATA/ convention is enforced -- every asset // here is the single place the DATA/ convention is enforced -- every asset
// loader and example routes file opens through this, so callers never // loader and example routes file opens through this, so callers never
// hard-code "DATA/" and the runtime tree layout stays consistent across // hard-code "DATA/" and the runtime tree layout stays consistent across
// ports. Returns NULL if the joined path overflows DATA_PATH_MAX or the // ports. Returns NULL if the joined path overflows FILE_PATH_MAX or the
// file cannot be opened. A port with run-time roots (fileRoot.h) puts // file cannot be opened.
// DATA/ under its read root.
FILE *jlDataOpen(const char *name, const char *mode) { FILE *jlDataOpen(const char *name, const char *mode) {
char path[DATA_PATH_MAX]; char path[FILE_PATH_MAX];
#if defined(JL_HAS_FILE_ROOTS)
if (name == NULL || mode == NULL || !fileRootJoin(path, sizeof(path), jlpReadRoot(), DATA_DIR_PREFIX, name)) { if (name == NULL || mode == NULL || !fileRootJoin(path, sizeof(path), jlpReadRoot(), DATA_DIR_PREFIX, name)) {
return NULL; return NULL;
} }
return fopen(path, mode); return fopen(path, mode);
#else
size_t prefixLen = sizeof(DATA_DIR_PREFIX) - 1u; // literal length, no NUL
size_t nameLen;
// A manual prefix concat instead of snprintf: the format is fixed
// ("DATA/" + name), and snprintf drags the whole C formatted-output
// engine (~20 KB of code plus a soft-float dependency) into every
// binary that opens a data file, which the IIgs bank-0 budget cannot
// spare. strlen/memcpy do the same join with none of that.
if (name == NULL || mode == NULL) {
return NULL;
}
nameLen = strlen(name);
if (prefixLen + nameLen + 1u > sizeof(path)) {
return NULL;
}
memcpy(path, DATA_DIR_PREFIX, prefixLen);
memcpy(path + prefixLen, name, nameLen + 1u); // copy the NUL too
return fopen(path, mode);
#endif
} }

View file

@ -11,6 +11,7 @@
// layer's included. // layer's included.
#include <stddef.h> #include <stddef.h>
#include <string.h>
#include "joey/audio.h" #include "joey/audio.h"
#include "joey/debug.h" #include "joey/debug.h"
@ -29,6 +30,38 @@ static bool gAudioOff = false; // the program wants no audio
static uint8_t gCriticalDepth = 0u; // jlAudioCriticalEnter calls not yet exited static uint8_t gCriticalDepth = 0u; // jlAudioCriticalEnter calls not yet exited
// ----- Port services (port.h) -----
// Every factor-th of count samples at src, from *phase on, to dst, which may
// be src itself (each sample lands at or before where it was). *phase
// becomes where the next block's first sample is. Returns how many it wrote.
// Factor 1 is a copy (the phase stays 0), at memmove's speed.
uint32_t audioDecimate(uint8_t *dst, const uint8_t *src, uint32_t count, uint8_t factor, uint8_t *phase) {
uint32_t i;
uint32_t n;
if (factor == 1u) {
if (dst != src) {
memmove(dst, src, count);
}
return count;
}
n = 0u;
for (i = *phase; i < count; i += factor) {
dst[n] = src[i];
n++;
}
*phase = (uint8_t)(i - count);
return n;
}
// The smallest whole factor that brings rateHz to maxHz or under.
uint8_t audioDecimationFactor(uint16_t rateHz, uint16_t maxHz) {
return (uint8_t)(((uint32_t)rateHz + maxHz - 1u) / maxHz);
}
// ----- Lifecycle (jlInit, jlShutdown) ----- // ----- Lifecycle (jlInit, jlShutdown) -----
void audioInit(bool wanted) { void audioInit(bool wanted) {

View file

@ -9,6 +9,20 @@
#include "audioSfxMixInternal.h" #include "audioSfxMixInternal.h"
// The unsigned 8-bit output's top, and a stereo frame's samples (left, then
// right).
#define AUDIO_SFX_SAMPLE_MAX 255 // an int, as the clamp's arithmetic is
#define AUDIO_SFX_STEREO 2u
// A slot's position: a 16.16 number, the sample index above the point.
#define FRAC_BITS 16
#define FRAC_MASK 0xFFFFu
// A run's index stays under this: the 68000 run (src/m68k/sfxMix.s) reads
// the samples at it as a signed word, and with a step's whole samples on top
// it never carries into the fraction above it.
#define RUN_END_MAX 0x7F00u
// interp()/mix arithmetic (delta * fracHalf up to ~8.4M, and the // interp()/mix arithmetic (delta * fracHalf up to ~8.4M, and the
// >> 15 on a possibly-negative value) requires a >= 32-bit int with // >> 15 on a possibly-negative value) requires a >= 32-bit int with
// arithmetic right shift. DOS (DJGPP) and ST (m68k gcc) both satisfy // arithmetic right shift. DOS (DJGPP) and ST (m68k gcc) both satisfy
@ -19,107 +33,78 @@
#error "audioSfxMix requires >= 32-bit int (DOS/ST only); IIgs must use NTPstreamsound" #error "audioSfxMix requires >= 32-bit int (DOS/ST only); IIgs must use NTPstreamsound"
#endif #endif
// Where a run's samples land: a byte each (mono), one side of each stereo
// pair (dst at that side's byte), or both sides of each pair.
typedef enum {
RUN_MONO = 0,
RUN_SIDE = 1,
RUN_BOTH = 2
} RunLayoutE;
// One slot's run into the output: count samples, each the one at src's
// 16.16 position pos (src its sample 0) interpolated toward the next by the
// fraction, shifted down shift bits, added into dst as layout says and
// clamped, the position stepping by step. The caller sets count so that no
// sample's index reaches the last sample it may read, so the run tests
// nothing else. The 68000 ports have it in assembly (src/m68k/sfxMix.s,
// JL_HAS_SFX_MIX_RUN), which reads these fields as longs in this order.
typedef struct {
const int8_t *src;
volatile uint8_t *dst;
uint32_t pos;
uint32_t step;
uint32_t count;
uint32_t format; // AUDIO_SFX_DST_*
uint32_t layout; // RunLayoutE
uint32_t shift; // gShift
} SfxRunT;
// How far under full scale a sound effect is mixed (audioSfxSetShift).
static uint8_t gShift = 0u;
// Prototypes (file-local, alphabetical). // Prototypes (file-local, alphabetical).
#if defined(JL_HAS_SFX_MIX_RUN)
void audioSfxMixRun(SfxRunT *run);
#define sfxRun audioSfxMixRun
#else
static int interp(int8_t s0, int8_t s1, uint16_t frac); static int interp(int8_t s0, int8_t s1, uint16_t frac);
static void sfxRun(SfxRunT *run);
#endif
static void slotMix(AudioSfxSlotT *slot, volatile uint8_t *dst, uint16_t frames, RunLayoutE layout, uint8_t format);
static bool streamRefill(AudioSfxSlotT *slot); static bool streamRefill(AudioSfxSlotT *slot);
void audioSfxOverlayMix(volatile uint8_t *dst, uint16_t len, AudioSfxSlotT *slots, uint8_t numSlots) { void audioSfxOverlayMix(volatile uint8_t *dst, uint16_t len, AudioSfxSlotT *slots, uint8_t numSlots) {
AudioSfxSlotT *slot;
const uint8_t *smp;
const int8_t *streamBuf;
uint32_t idx;
uint32_t stepIdx;
uint32_t length;
uint16_t frac;
uint16_t stepFrac;
uint16_t streamLen;
uint16_t i;
uint8_t s; uint8_t s;
int sample;
int mixed;
int8_t s0;
int8_t s1;
for (s = 0; s < numSlots; s++) { for (s = 0; s < numSlots; s++) {
slot = &slots[s]; if (slots[s].active) {
if (!slot->active) { slotMix(&slots[s], dst, len, RUN_MONO, AUDIO_SFX_DST_UNSIGNED);
}
}
}
void audioSfxOverlayMixStereo(volatile uint8_t *dst, uint16_t frames, AudioSfxSlotT *slots, uint8_t numSlots, const uint8_t *pans, uint8_t format) {
uint8_t s;
for (s = 0; s < numSlots; s++) {
if (!slots[s].active) {
continue; continue;
} }
// Split the 16.16 step once per slot: the inner loops advance if (pans[s] == JL_AUDIO_PAN_LEFT) {
// a uint32_t sample index + uint16_t fraction with a carry slotMix(&slots[s], dst, frames, RUN_SIDE, format);
// compare instead of 64-bit fixed-point math per sample. } else if (pans[s] == JL_AUDIO_PAN_RIGHT) {
stepIdx = slot->stepFp >> 16; slotMix(&slots[s], dst + 1, frames, RUN_SIDE, format);
stepFrac = (uint16_t)(slot->stepFp & 0xFFFFu);
idx = slot->posIdx;
frac = slot->posFrac;
// The streaming and fixed-buffer cases are two separate inner
// loops so the loop-invariant isStream test runs once per slot
// and the slot fields stay cached in locals across the
// volatile dst stores (which would otherwise force per-sample
// re-reads); the short mix body is intentionally duplicated
// between them.
if (slot->fill != NULL) {
// Streaming: refill the prefetch buffer when drained.
streamBuf = slot->streamBuf;
streamLen = slot->streamLen;
for (i = 0; i < len; i++) {
if (idx >= streamLen) {
if (!streamRefill(slot)) {
slot->active = false;
break;
}
// streamRefill rewound the slot position; re-read
// the fields it updated.
idx = slot->posIdx;
frac = slot->posFrac;
streamLen = slot->streamLen;
}
s0 = streamBuf[idx];
s1 = (idx + 1u < streamLen) ? streamBuf[idx + 1u] : s0;
sample = interp(s0, s1, frac);
mixed = (int)dst[i] - 128 + sample;
if (mixed > 127) {
mixed = 127;
} else if (mixed < -128) {
mixed = -128;
}
dst[i] = (uint8_t)(mixed + 128);
frac = (uint16_t)(frac + stepFrac);
if (frac < stepFrac) {
idx++;
}
idx += stepIdx;
}
} else { } else {
// Fixed buffer. slotMix(&slots[s], dst, frames, RUN_BOTH, format);
smp = slot->sample;
length = slot->length;
for (i = 0; i < len; i++) {
if (idx >= length) {
slot->active = false;
break;
}
s0 = (int8_t)smp[idx];
s1 = (idx + 1u < length) ? (int8_t)smp[idx + 1u] : s0;
sample = interp(s0, s1, frac);
mixed = (int)dst[i] - 128 + sample;
if (mixed > 127) {
mixed = 127;
} else if (mixed < -128) {
mixed = -128;
}
dst[i] = (uint8_t)(mixed + 128);
frac = (uint16_t)(frac + stepFrac);
if (frac < stepFrac) {
idx++;
}
idx += stepIdx;
} }
} }
slot->posIdx = idx; }
slot->posFrac = frac;
}
void audioSfxSetShift(uint8_t shift) {
gShift = shift;
} }
@ -160,19 +145,15 @@ void audioSfxSlotArmStream(AudioSfxSlotT *slot, jlAudioStreamFillT fill, void *c
} }
#if !defined(JL_HAS_SFX_MIX_RUN)
// Linear interpolation between two adjacent signed 8-bit samples. // Linear interpolation between two adjacent signed 8-bit samples.
// `frac` is the 16-bit fractional part of the read position; 0 = // `frac` is the 16-bit fractional part of the read position; 0 =
// exactly at s0, 0xFFFF = nearly s1. Result is a signed int in // exactly at s0, 0xFFFF = nearly s1. Result is a signed int in
// [-128, +127]. // [-128, +127]. frac's low bit is dropped, as the 68000 run's muls.w
// // needs (src/m68k/sfxMix.s), so the two give the same samples: at worst
// Formulated as a 16x16->32 signed multiply with explicit int16_t // 1 LSB of 8-bit audio from the exact delta * frac >> 16, inaudible. The
// locals so m68k gcc matches its mulhisi3 pattern and emits a single // >> 15 of a possibly-negative int32_t relies on arithmetic shift, which
// muls.w instead of a __mulsi3 library call (~250-350 cycles per // holds on every target this file builds for.
// sample on the 68000). Dropping frac's low bit means the result can
// differ from the exact delta * frac >> 16 by at most 1 LSB of 8-bit
// audio -- an accepted, inaudible deviation. The >> 15 of a possibly-
// negative int32_t relies on arithmetic shift, which holds on every
// target this file builds for (DOS/ST).
static int interp(int8_t s0, int8_t s1, uint16_t frac) { static int interp(int8_t s0, int8_t s1, uint16_t frac) {
int16_t delta; int16_t delta;
int16_t fracHalf; int16_t fracHalf;
@ -180,42 +161,147 @@ static int interp(int8_t s0, int8_t s1, uint16_t frac) {
delta = (int16_t)(s1 - s0); delta = (int16_t)(s1 - s0);
fracHalf = (int16_t)(uint16_t)(frac >> 1); fracHalf = (int16_t)(uint16_t)(frac >> 1);
#if defined(__m68k__)
// One muls.w: gcc for the 68000 calls __mulsi3 for this 16 x 16 bit
// product, which on the ST and the X68000 cost several times the rest of
// the mix.
product = delta;
__asm__ ("muls.w %1,%0" : "+d" (product) : "dm" (fracHalf) : "cc");
#else
product = (int32_t)delta * fracHalf; product = (int32_t)delta * fracHalf;
#endif
return (int)s0 + (int)(product >> 15); return (int)s0 + (int)(product >> 15);
} }
// SfxRunT's run, in C. The run's fields are cached in locals: dst's stores
// are bytes, which may alias anything, so the compiler would re-read them.
static void sfxRun(SfxRunT *run) {
volatile uint8_t *dst;
const int8_t *src;
uint32_t pos;
uint32_t step;
uint32_t count;
uint32_t idx;
uint8_t stride;
uint8_t format;
uint8_t shift;
bool both;
int sample;
int value;
src = run->src;
dst = run->dst;
pos = run->pos;
step = run->step;
format = (uint8_t)run->format;
shift = (uint8_t)run->shift;
both = run->layout == RUN_BOTH;
stride = run->layout == RUN_MONO ? 1u : AUDIO_SFX_STEREO;
for (count = run->count; count > 0u; count--) {
idx = pos >> FRAC_BITS;
sample = interp(src[idx], src[idx + 1u], (uint16_t)(pos & FRAC_MASK)) >> shift;
value = (int)(dst[0] ^ format) + sample;
dst[0] = (uint8_t)((value < 0 ? 0 : (value > AUDIO_SFX_SAMPLE_MAX ? AUDIO_SFX_SAMPLE_MAX : value)) ^ format);
if (both) {
value = (int)(dst[1] ^ format) + sample;
dst[1] = (uint8_t)((value < 0 ? 0 : (value > AUDIO_SFX_SAMPLE_MAX ? AUDIO_SFX_SAMPLE_MAX : value)) ^ format);
}
dst += stride;
pos += step;
}
run->dst = dst;
run->pos = pos;
run->count = 0u;
}
#endif
// One slot mixed over frames samples at dst (layout and format as SfxRunT's),
// in runs: each as far as the last sample whose next one it can read, then
// that last sample on its own, held (there is nothing after it to
// interpolate toward), then -- a stream's -- the next chunk. A slot whose
// position passes its end goes inactive.
static void slotMix(AudioSfxSlotT *slot, volatile uint8_t *dst, uint16_t frames, RunLayoutE layout, uint8_t format) {
SfxRunT run;
const int8_t *base;
int8_t held[2];
uint32_t length;
uint32_t idx;
uint32_t end;
uint32_t left;
uint32_t want;
run.dst = dst;
run.pos = slot->posFrac;
run.step = slot->stepFp;
run.format = format;
run.layout = (uint32_t)layout;
run.shift = gShift;
idx = slot->posIdx;
want = frames;
while (want > 0u) {
if (slot->fill != NULL) {
base = slot->streamBuf;
length = slot->streamLen;
} else {
base = (const int8_t *)slot->sample;
length = slot->length;
}
if (idx >= length) {
slot->posIdx = idx;
slot->posFrac = (uint16_t)run.pos;
if (slot->fill == NULL || !streamRefill(slot)) {
slot->active = false;
return;
}
// streamRefill moved the position into the new chunk.
idx = slot->posIdx;
run.pos = slot->posFrac;
continue;
}
if (idx + 1u < length) {
run.src = base + idx;
end = length - 1u - idx;
if (end > RUN_END_MAX) {
end = RUN_END_MAX;
}
} else {
held[0] = base[idx];
held[1] = held[0];
run.src = held;
end = 1u;
}
// The samples before the index reaches end.
left = ((end << FRAC_BITS) - run.pos + run.step - 1u) / run.step;
run.count = want < left ? want : left;
want -= run.count;
sfxRun(&run);
idx += run.pos >> FRAC_BITS;
run.pos &= FRAC_MASK;
}
slot->posIdx = idx;
slot->posFrac = (uint16_t)run.pos;
}
// Refill a streaming slot's prefetch buffer. Preserves the very last // Refill a streaming slot's prefetch buffer. Preserves the very last
// sample of the previous chunk at streamBuf[0] so linear interp can // sample of the previous chunk at streamBuf[0] so linear interp can
// step across the chunk boundary without a discontinuity click. The // step across the chunk boundary without a discontinuity click. The
// new chunk fills streamBuf[1..]. On a real refill (a previous chunk // new chunk fills streamBuf[1..]. On a real refill (a previous chunk
// existed) the position resets to 0 so the first read interpolates the // existed) the position carries on where it ran out: as far past the
// carried old-last sample at streamBuf[0] into the new chunk's first // old last sample, now streamBuf[0], as it had got, fraction and all,
// sample at streamBuf[1], bridging the boundary. On the very first // so the first read interpolates across the boundary and no time is
// prime there is no real previous sample, so the position resets to // lost or played twice. A step that carries past a chunk this short
// "after byte 0" and the first read starts at the new chunk's first // takes the next one too. On the very first prime there is no real
// sample (index 1). // previous sample, so the position starts "after byte 0" and the first
// read is the new chunk's first sample (index 1).
// //
// Returns true if more samples are available, false if the stream // Returns true if more samples are available, false if the stream
// has ended (caller should mark the slot inactive). // has ended (caller should mark the slot inactive).
static bool streamRefill(AudioSfxSlotT *slot) { static bool streamRefill(AudioSfxSlotT *slot) {
int8_t lastSample; int8_t lastSample;
uint32_t got; uint32_t got;
bool haveCarry; uint16_t oldLen;
do {
if (slot->streamEof) { if (slot->streamEof) {
return false; return false;
} }
haveCarry = (slot->streamLen > 0u); oldLen = slot->streamLen;
lastSample = haveCarry ? slot->streamBuf[slot->streamLen - 1u] : (int8_t)0; lastSample = oldLen > 0u ? slot->streamBuf[oldLen - 1u] : (int8_t)0;
// Clamp the callback's reported count to the request size so a // Clamp the callback's reported count to the request size so a
// non-conforming fill() that returns more than asked cannot make // non-conforming fill() that returns more than asked cannot make
// the mixer read past streamBuf. (Memory safety still requires // the mixer read past streamBuf. (Memory safety still requires
@ -232,18 +318,12 @@ static bool streamRefill(AudioSfxSlotT *slot) {
} }
slot->streamBuf[0] = lastSample; slot->streamBuf[0] = lastSample;
slot->streamLen = (uint16_t)(got + 1u); slot->streamLen = (uint16_t)(got + 1u);
if (haveCarry) { if (oldLen > 0u) {
// Bridge the chunk boundary: start at index 0 so the first slot->posIdx = slot->posIdx - (oldLen - 1u);
// read interpolates streamBuf[0] (old last sample) -> [1]
// (new first sample), replaying one sample with no audible
// discontinuity click.
slot->posIdx = 0u;
slot->posFrac = 0u;
} else { } else {
// First prime: streamBuf[0] is a synthetic zero, not a real
// sample. Skip it so it isn't played as a leading sample.
slot->posIdx = 1u; slot->posIdx = 1u;
slot->posFrac = 0u; slot->posFrac = 0u;
} }
} while (slot->posIdx >= slot->streamLen);
return true; return true;
} }

View file

@ -57,6 +57,12 @@ typedef struct {
bool streamEof; // last fill returned 0 bool streamEof; // last fill returned 0
} AudioSfxSlotT; } AudioSfxSlotT;
// Sound effects mixed shift bits under full scale from now on: a port whose
// 8-bit mix holds a module's four channels at a fraction of a byte each
// (modPlayerSetU8Shift) mixes its effects at the same level, as loud as a
// module channel (joey/audio.h's mix). 0 until a port sets it.
void audioSfxSetShift(uint8_t shift);
// Arm a slot for one-shot playback from a fixed PCM buffer. // Arm a slot for one-shot playback from a fixed PCM buffer.
// `outputRate` is the HAL's mixer output rate; the helper computes // `outputRate` is the HAL's mixer output rate; the helper computes
// stepFp from rateHz and outputRate. Sets `active = true`. No-op // stepFp from rateHz and outputRate. Sets `active = true`. No-op
@ -82,4 +88,15 @@ void audioSfxSlotArmStream(AudioSfxSlotT *slot, jlAudioStreamFillT fill, void *c
// memory but the function accepts both via the wider qualifier. // memory but the function accepts both via the wider qualifier.
void audioSfxOverlayMix(volatile uint8_t *dst, uint16_t len, AudioSfxSlotT *slots, uint8_t numSlots); void audioSfxOverlayMix(volatile uint8_t *dst, uint16_t len, AudioSfxSlotT *slots, uint8_t numSlots);
// The stereo form, for a port that mixes interleaved left and right 8-bit
// pairs (DOS on an SB16, the STe): each slot is added into the side its
// pans[] entry reaches (both, for JL_AUDIO_PAN_CENTER), each side clamped
// as audioSfxOverlayMix clamps. pans[i] is slots[i]'s. dst holds unsigned
// samples (AUDIO_SFX_DST_UNSIGNED) or signed ones (AUDIO_SFX_DST_SIGNED),
// each mixed in its own format and in order, a sample at a time, so a
// buffer that is playing can take a sound ahead of the play position.
#define AUDIO_SFX_DST_UNSIGNED 0x00u
#define AUDIO_SFX_DST_SIGNED 0x80u
void audioSfxOverlayMixStereo(volatile uint8_t *dst, uint16_t frames, AudioSfxSlotT *slots, uint8_t numSlots, const uint8_t *pans, uint8_t format);
#endif #endif

View file

@ -308,7 +308,7 @@ bool codegenArenaInit(uint32_t totalBytes) {
while (want > 0u && gBankCount < CODEGEN_ARENA_BANKS_MAX) { while (want > 0u && gBankCount < CODEGEN_ARENA_BANKS_MAX) {
uint32_t bytes = (want > CODEGEN_ARENA_IIGS_BANK) ? CODEGEN_ARENA_IIGS_BANK : want; uint32_t bytes = (want > CODEGEN_ARENA_IIGS_BANK) ? CODEGEN_ARENA_IIGS_BANK : want;
if (!bankAlloc(gBankCount, bytes)) { if (!bankAlloc(gBankCount, bytes)) {
if (bytes <= 60ul * 1024ul || !bankAlloc(gBankCount, 60ul * 1024ul)) { if (bytes <= CODEGEN_ARENA_IIGS_RETRY || !bankAlloc(gBankCount, CODEGEN_ARENA_IIGS_RETRY)) {
break; break;
} }
} }
@ -317,11 +317,17 @@ bool codegenArenaInit(uint32_t totalBytes) {
want = (want > gBankSize[gBankCount - 1u]) ? want - gBankSize[gBankCount - 1u] : 0u; want = (want > gBankSize[gBankCount - 1u]) ? want - gBankSize[gBankCount - 1u] : 0u;
} }
#else #else
// One block: the request, or as much of it as the heap gives, halving
// down to CODEGEN_ARENA_MIN_BYTES, as the IIgs takes what it is granted.
want = totalBytes; want = totalBytes;
if (bankAlloc(0u, want)) { while (!bankAlloc(0u, want)) {
if (want <= CODEGEN_ARENA_MIN_BYTES) {
return false;
}
want = want / 2u;
}
gTotalBytes = want; gTotalBytes = want;
gBankCount = 1u; gBankCount = 1u;
}
#endif #endif
return gBankCount != 0u; return gBankCount != 0u;
} }

View file

@ -44,16 +44,20 @@ typedef struct ArenaSlotT {
} ArenaSlotT; } ArenaSlotT;
// Banks: at most this many, and the IIgs's bank is one 64 KB block (the // Banks: at most this many, and the IIgs's bank is one 64 KB block (the
// Memory Manager refuses a no-cross block any larger). // Memory Manager refuses a no-cross block any larger); a whole free bank
// refused, it asks for any page-aligned 60 KB run inside one. Elsewhere one
// block, halved until the heap gives it, down to CODEGEN_ARENA_MIN_BYTES.
#define CODEGEN_ARENA_BANKS_MAX 4u #define CODEGEN_ARENA_BANKS_MAX 4u
#define CODEGEN_ARENA_BANK_SHIFT 24u #define CODEGEN_ARENA_BANK_SHIFT 24u
#define CODEGEN_ARENA_BANK_MASK 0x00FFFFFFul #define CODEGEN_ARENA_BANK_MASK 0x00FFFFFFul
#define CODEGEN_ARENA_IIGS_BANK 65536ul #define CODEGEN_ARENA_IIGS_BANK 65536ul
#define CODEGEN_ARENA_IIGS_RETRY (60ul * 1024ul)
#define CODEGEN_ARENA_MIN_BYTES 4096ul
// Lifecycle. Returns false if totalBytes is 0 or no bank at all could be // Lifecycle. Returns false if totalBytes is 0 or no memory at all could be
// allocated; a request the machine cannot fully satisfy yields as many // had; a request the machine cannot fully satisfy yields what it can
// banks as it can (codegenArenaBytesTotal tells). Idempotent: calling // (codegenArenaBytesTotal tells), on every port. Idempotent: calling Init
// Init twice without an intervening Shutdown is a no-op. // twice without an intervening Shutdown is a no-op.
bool codegenArenaInit(uint32_t totalBytes); bool codegenArenaInit(uint32_t totalBytes);
void codegenArenaShutdown(void); void codegenArenaShutdown(void);

View file

@ -26,6 +26,10 @@
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#if defined(__EMSCRIPTEN__)
#include <emscripten.h>
#endif
#include "joey/platform.h" #include "joey/platform.h"
#include "joey/debug.h" #include "joey/debug.h"
#include "fileRoot.h" #include "fileRoot.h"
@ -33,6 +37,10 @@
static const char *kLogPath = "joeylog.txt"; static const char *kLogPath = "joeylog.txt";
// One logged line, its newline left out: jlLogF formats into this much on
// every port, so a longer line is cut at 255 characters everywhere.
#define LOG_LINE_BYTES 256u
#ifdef JOEY_LOG_SERIAL_TEE #ifdef JOEY_LOG_SERIAL_TEE
// Supplied by the port. Declared ahead of the platform split because BOTH log // Supplied by the port. Declared ahead of the platform split because BOTH log
// implementations below tee through it. // implementations below tee through it.
@ -98,7 +106,7 @@ void jlLog(const char *msg) {
void jlLogF(const char *fmt, ...) { void jlLogF(const char *fmt, ...) {
char line[200]; char line[LOG_LINE_BYTES];
va_list args; va_list args;
if (fmt == NULL) { if (fmt == NULL) {
@ -121,6 +129,7 @@ void jlLogF(const char *fmt, ...) {
// below). // below).
void jlLogFlush(void) { void jlLogFlush(void) {
FILE *fp; FILE *fp;
char path[FILE_PATH_MAX];
uint16_t head; uint16_t head;
uint16_t start; uint16_t start;
uint16_t count; uint16_t count;
@ -131,7 +140,10 @@ void jlLogFlush(void) {
if (head == 0u && gJoeyLogRingFull == 0u) { if (head == 0u && gJoeyLogRingFull == 0u) {
return; return;
} }
fp = fopen(kLogPath, "w"); if (!fileRootJoin(path, sizeof(path), jlpWriteRoot(), kLogPath, "")) {
return;
}
fp = fopen(path, "w");
if (fp == NULL) { if (fp == NULL) {
return; return;
} }
@ -168,31 +180,21 @@ void jlLogReset(void) {
#else // not JOEYLIB_PLATFORM_IIGS #else // not JOEYLIB_PLATFORM_IIGS
static FILE *gLogFp = NULL; static FILE *gLogFp = NULL;
#if defined(JL_HAS_FILE_ROOTS) #if !defined(JOEYLIB_PLATFORM_DVX)
// The log FILE belongs to the process hosting the program and can outlive // A buffer that holds a whole line and its newline, so each line reaches
// it (a program ended by force never reaches jlLogClose), so it keeps the // the file in one write (logLine flushes per line). DVX's log FILE belongs
// host's own buffer rather than one inside the program. (DVX does not even // to the desktop's process and can outlive the program (one ended by force
// offer its programs setvbuf.) jlLog/jlLogF still flush once per line. // never reaches jlLogClose), so it keeps the host's own buffer -- DVX does
#define LOG_PATH_MAX 256 // not even offer its programs setvbuf.
#else static char gLogBuf[LOG_LINE_BYTES * 2u];
/* 16 KB full buffer so a single log line's many small vfprintf writes
* coalesce into one host write; jlLog/jlLogF then fflush once per
* line. Default stdio buffers are only ~512 bytes, which would split a
* long line across multiple writes. */
#define JOEY_LOG_BUF_BYTES 16384
static char gLogBuf[JOEY_LOG_BUF_BYTES];
#endif #endif
// ----- Prototypes ----- // ----- Prototypes -----
static void logClose(void); static void logClose(void);
static FILE *logFile(void); static FILE *logFile(void);
#if defined(JL_HAS_FILE_ROOTS) static void logLine(const char *line);
static FILE *logOpen(const char *mode); static FILE *logOpen(const char *mode);
#else
// joeylog.txt is relative to the current directory.
#define logOpen(_mode) fopen(kLogPath, (_mode))
#endif
static void logClose(void) { static void logClose(void) {
@ -207,7 +209,7 @@ static void logClose(void) {
static FILE *logFile(void) { static FILE *logFile(void) {
if (gLogFp == NULL) { if (gLogFp == NULL) {
gLogFp = logOpen("a"); gLogFp = logOpen("a");
#if !defined(JL_HAS_FILE_ROOTS) #if !defined(JOEYLIB_PLATFORM_DVX)
if (gLogFp != NULL) { if (gLogFp != NULL) {
(void)setvbuf(gLogFp, gLogBuf, _IOFBF, sizeof(gLogBuf)); (void)setvbuf(gLogFp, gLogBuf, _IOFBF, sizeof(gLogBuf));
} }
@ -217,35 +219,44 @@ static FILE *logFile(void) {
} }
#if defined(JL_HAS_FILE_ROOTS) // One line out: to the serial tee (when built in), the file, and on the
// web the browser's console as well (the page's file is lost with it).
static void logLine(const char *line) {
FILE *fp;
#ifdef JOEY_LOG_SERIAL_TEE
jlpLogTee(line);
jlpLogTee("\n");
#endif
#if defined(__EMSCRIPTEN__)
emscripten_log(EM_LOG_CONSOLE, "%s", line);
#endif
fp = logFile();
if (fp == NULL) {
return;
}
fputs(line, fp);
fputc('\n', fp);
fflush(fp);
}
// joeylog.txt under the port's write root (fileRoot.h). // joeylog.txt under the port's write root (fileRoot.h).
static FILE *logOpen(const char *mode) { static FILE *logOpen(const char *mode) {
char path[LOG_PATH_MAX]; char path[FILE_PATH_MAX];
if (!fileRootJoin(path, sizeof(path), jlpWriteRoot(), kLogPath, "")) { if (!fileRootJoin(path, sizeof(path), jlpWriteRoot(), kLogPath, "")) {
return NULL; return NULL;
} }
return fopen(path, mode); return fopen(path, mode);
} }
#endif
void jlLog(const char *msg) { void jlLog(const char *msg) {
FILE *fp;
if (msg == NULL) { if (msg == NULL) {
return; return;
} }
#ifdef JOEY_LOG_SERIAL_TEE logLine(msg);
jlpLogTee(msg);
jlpLogTee("\n");
#endif
fp = logFile();
if (fp == NULL) {
return;
}
fputs(msg, fp);
fputc('\n', fp);
fflush(fp);
} }
@ -257,42 +268,16 @@ void jlLogClose(void) {
void jlLogF(const char *fmt, ...) { void jlLogF(const char *fmt, ...) {
FILE *fp; char line[LOG_LINE_BYTES];
va_list args; va_list args;
if (fmt == NULL) { if (fmt == NULL) {
return; return;
} }
fp = logFile();
#ifdef JOEY_LOG_SERIAL_TEE
// Format once into a buffer so the same bytes reach the file and the wire.
// Only under the tee: default builds keep the straight vfprintf, which has
// no line-length ceiling.
{
char line[200];
va_start(args, fmt); va_start(args, fmt);
(void)vsnprintf(line, sizeof(line), fmt, args); (void)vsnprintf(line, sizeof(line), fmt, args);
va_end(args); va_end(args);
jlpLogTee(line); logLine(line);
jlpLogTee("\n");
if (fp == NULL) {
return;
}
fputs(line, fp);
fputc('\n', fp);
fflush(fp);
return;
}
#else
if (fp == NULL) {
return;
}
va_start(args, fmt);
vfprintf(fp, fmt, args);
va_end(args);
fputc('\n', fp);
fflush(fp);
#endif
} }
@ -304,15 +289,15 @@ void jlLogFlush(void) {
void jlLogReset(void) { void jlLogReset(void) {
FILE *fp;
logClose(); logClose();
/* Truncate by opening for write then closing; subsequent // Truncate by opening for write then closing; the next jlLog opens it
* jlLog* will reopen for append. */ // for append again.
{ fp = logOpen("w");
FILE *fp = logOpen("w");
if (fp != NULL) { if (fp != NULL) {
fclose(fp); fclose(fp);
} }
}
} }
#endif // JOEYLIB_PLATFORM_IIGS #endif // JOEYLIB_PLATFORM_IIGS

View file

@ -2,29 +2,31 @@
// See LICENSE.md for license information. // See LICENSE.md for license information.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// //
// Run-time file roots (JL_HAS_FILE_ROOTS). // File roots: the folders the core finds DATA/ in (the read root) and puts
// // SAVES/ and joeylog.txt in (the write root). A port that knows the
// On the machine ports the core opens DATA/, SAVES/ and joeylog.txt by those // program's own folder (JL_HAS_FILE_ROOTS) supplies it, so a program finds
// relative names (literals in assetLoad.c, save.c and debug.c), so they // its files wherever it was started from, before jlInit as after: DOS from
// resolve against the current directory: the program's own folder when its // the path DOS started it by, the Amiga as PROGDIR:, the X68000 from its
// desktop starts it (the IIgs roots SAVES/ at its application prefix // process block, the IIgs as GS/OS prefix 1, the hosted ports from their
// instead, JL_SAVE_DIR in port.h). A port // executable's path. DVX keeps SAVES/ and the log apart, in the program's
// whose programs share one current directory with other programs and end // settings folder. Without the flag both roots are "": the current
// without ending the process (DVX: each program is a module loaded into the // directory (the ST, whose GEMDOS tells a program nothing of where it
// desktop) defines JL_HAS_FILE_ROOTS in platform.h and supplies the folders // was started from).
// they live in. Without the flag this header declares nothing and the core
// keeps its literals, so no other port's code changes.
#ifndef JOEYLIB_FILE_ROOT_H #ifndef JOEYLIB_FILE_ROOT_H
#define JOEYLIB_FILE_ROOT_H #define JOEYLIB_FILE_ROOT_H
#include "joey/platform.h"
#if defined(JL_HAS_FILE_ROOTS)
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <string.h>
#include "joey/platform.h"
// The longest path the core builds: a root, DATA/ or SAVES/, and a name.
#define FILE_PATH_MAX 256
static inline bool fileRootJoin(char *out, size_t cap, const char *root, const char *rel, const char *name);
#if defined(JL_HAS_FILE_ROOTS)
// The roots are folder prefixes: "" or a path ending in a separator, valid // The roots are folder prefixes: "" or a path ending in a separator, valid
// from before the program's main runs until it ends. // from before the program's main runs until it ends.
// jlpReadRoot holds DATA/, the program's own read-only files // jlpReadRoot holds DATA/, the program's own read-only files
@ -32,21 +34,34 @@
// the first time it is asked for it // the first time it is asked for it
const char *jlpReadRoot(void); const char *jlpReadRoot(void);
const char *jlpWriteRoot(void); const char *jlpWriteRoot(void);
// Close joeylog.txt (debug.c). A port whose program ends without its process
// Close joeylog.txt (debug.c). The port calls it when its program ends: // ending calls it then (DVX: the file handle belongs to the desktop's
// the file handle belongs to the host process, which lives on. A later // process, which lives on). A later jlLog opens the file again, appending.
// jlLog opens the file again, appending.
void jlLogClose(void); void jlLogClose(void);
#else
#define jlpReadRoot() ""
#define jlpWriteRoot() ""
#endif
// out = root + rel + name. False when that does not fit in cap bytes. // out = root + rel + name. False when that does not fit in cap bytes. A
// plain copy: snprintf would pull the formatted-output engine into every
// program that opens a file, which the IIgs's bank 0 cannot spare.
static inline bool fileRootJoin(char *out, size_t cap, const char *root, const char *rel, const char *name) { static inline bool fileRootJoin(char *out, size_t cap, const char *root, const char *rel, const char *name) {
int written; size_t rootLen;
size_t relLen;
size_t nameLen;
written = snprintf(out, cap, "%s%s%s", root, rel, name); rootLen = strlen(root);
return written >= 0 && (size_t)written < cap; relLen = strlen(rel);
nameLen = strlen(name);
if (rootLen + relLen + nameLen + 1u > cap) {
return false;
}
memcpy(out, root, rootLen);
memcpy(out + rootLen, rel, relLen);
memcpy(out + rootLen + relLen, name, nameLen + 1u);
return true;
} }
#endif #endif
#endif

View file

@ -39,10 +39,16 @@
// sprite rather than failing when it still runs out. These ports // sprite rather than failing when it still runs out. These ports
// have real heaps. // have real heaps.
// //
// The X68000 runs the Amiga's planar 68000 emitters, so it takes the
// same 64 KB.
//
// DOS: two chunky shifts, ~1-6 KB per sprite; 32 KB is generous. // DOS: two chunky shifts, ~1-6 KB per sprite; 32 KB is generous.
//
// The ports that interpret every sprite (JL_SPRITE_INTERPRETED) have no
// arena at all.
#if defined(JOEYLIB_PLATFORM_IIGS) #if defined(JOEYLIB_PLATFORM_IIGS)
#define DEFAULT_CODEGEN_BYTES (8UL * 1024UL) #define DEFAULT_CODEGEN_BYTES (8UL * 1024UL)
#elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) #elif defined(SPRITE_68K)
#define DEFAULT_CODEGEN_BYTES (64UL * 1024UL) #define DEFAULT_CODEGEN_BYTES (64UL * 1024UL)
#else #else
#define DEFAULT_CODEGEN_BYTES (32UL * 1024UL) #define DEFAULT_CODEGEN_BYTES (32UL * 1024UL)
@ -62,6 +68,7 @@ static jlConfigT gConfig;
static bool gInitialized = false; static bool gInitialized = false;
static const char *gLastError = NULL; static const char *gLastError = NULL;
static uint32_t gMillisBase = 0u; // the port's millisecond count at jlInit static uint32_t gMillisBase = 0u; // the port's millisecond count at jlInit
static bool gShutdownAtExitOn = false; // jlShutdown registered to run at exit
// ----- Internal helpers (alphabetical) ----- // ----- Internal helpers (alphabetical) -----
@ -141,12 +148,14 @@ bool jlInit(const jlConfigT *config) {
return false; return false;
} }
#if !defined(JL_SPRITE_INTERPRETED)
if (!codegenArenaInit(gConfig.codegenBytes != 0 ? gConfig.codegenBytes : DEFAULT_CODEGEN_BYTES)) { if (!codegenArenaInit(gConfig.codegenBytes != 0 ? gConfig.codegenBytes : DEFAULT_CODEGEN_BYTES)) {
coreSetError("failed to allocate codegen arena"); coreSetError("no memory for any of the compiled-sprite cache");
stageFree(); stageFree();
jlpShutdown(); jlpShutdown();
return false; return false;
} }
#endif
// Empty the typed-character queue and centre the mouse before the // Empty the typed-character queue and centre the mouse before the
// port arms its input: a shutdown/re-init cycle must not hand the app // port arms its input: a shutdown/re-init cycle must not hand the app
@ -158,6 +167,14 @@ bool jlInit(const jlConfigT *config) {
// audio calls are then silent. // audio calls are then silent.
audioInit(!gConfig.noAudio); audioInit(!gConfig.noAudio);
// A program that ends without jlShutdown (exit, or main returning) must
// still leave the machine as jlInit found it: interrupts unhooked, the
// display and the sound put back. jlShutdown runs at exit, once
// registered; a program that shut down itself makes it a no-op.
if (!gShutdownAtExitOn) {
gShutdownAtExitOn = atexit(jlShutdown) == 0;
}
gInitialized = true; gInitialized = true;
return true; return true;
} }

View file

@ -94,13 +94,18 @@
#define SAMPLE_VALUES 256u #define SAMPLE_VALUES 256u
#define VOLUME_SHIFT 6u // a sample times a volume of 64 is the sample #define VOLUME_SHIFT 6u // a sample times a volume of 64 is the sample
#define MIX_CHUNK 256u // output samples mixed at a time #define MIX_CHUNK 256u // output samples mixed at a time
// 8-bit output: each channel's sample times its volume, over 64, and a // 8-bit output: each channel's sample times its volume, over 64, shifted down
// quarter of that, added into the output: a channel at full volume swings a // gU8Shift bits (modPlayerSetU8Shift), added into the output. At
// quarter of the range, as on the Amiga (two channels a side) and as // U8_SHIFT_QUARTER, the default, a channel at full volume swings a quarter of
// libxmp-lite mixed these ports' modules before. A quarter of each is -32 to // the range, -32 to 31, so the four together, -128 to 124 on the silence
// 31, so the four together, -128 to 124 on the silence byte, never wrap. // byte, never wrap. At U8_SHIFT_HALF a channel swings half: a side's two
#define U8_SHIFT 2u // still fit a byte, and a mono render sums its two sides through gU8Clamp,
// so four loud channels clip rather than wrap.
#define U8_SHIFT_QUARTER 2u
#define U8_SHIFT_HALF 1u
#define U8_BIAS 128 #define U8_BIAS 128
#define U8_MAX 255 // an int, as the clamp's arithmetic is
#define U8_SUMS (2u * SAMPLE_VALUES - 1u) // two bytes added: 0 .. 510
// A run's end, in samples from its start: within a 68000's signed word index, // A run's end, in samples from its start: within a 68000's signed word index,
// with room for a step past it. // with room for a step past it.
#define RUN_END_MAX 0x7F00u #define RUN_END_MAX 0x7F00u
@ -254,6 +259,7 @@ static void updateFunk(ChannelT *c);
static void vibrato(ChannelT *c, PaulaT *a, uint8_t param); static void vibrato(ChannelT *c, PaulaT *a, uint8_t param);
static void vibratoNc(ChannelT *c, PaulaT *a, uint8_t amp, uint8_t spd); static void vibratoNc(ChannelT *c, PaulaT *a, uint8_t amp, uint8_t spd);
static void voiceMixS16(PaulaT *a, int32_t *acc, uint32_t count); static void voiceMixS16(PaulaT *a, int32_t *acc, uint32_t count);
static void voiceMixSidesU8(uint32_t count);
static void voiceMixU8(PaulaT *a, uint8_t *dst, uint32_t count); static void voiceMixU8(PaulaT *a, uint8_t *dst, uint32_t count);
static void voiceSkip(PaulaT *a, uint32_t count); static void voiceSkip(PaulaT *a, uint32_t count);
static uint32_t voiceStep(PaulaT *a); static uint32_t voiceStep(PaulaT *a);
@ -357,10 +363,16 @@ static const uint8_t kArpeggio[ROW_TICKS_MAX] = {
// ----- Module state ----- // ----- Module state -----
static PlayerT gPlay; static PlayerT gPlay;
static int8_t gVolTab[VOLUME_MAX + 1u][SAMPLE_VALUES]; // a sample (as its byte) times a volume, over 256 static int8_t gVolTab[VOLUME_MAX + 1u][SAMPLE_VALUES]; // a sample (as its byte) times a volume, over 64 << gU8Shift
static bool gVolTabBuilt = false; static bool gVolTabBuilt = false;
static uint8_t gU8Shift = U8_SHIFT_QUARTER;
// The sum of two 8-bit sides (each centred on U8_BIAS), clamped to a byte.
static uint8_t gU8Clamp[U8_SUMS];
static int32_t gMixLeft[MIX_CHUNK]; static int32_t gMixLeft[MIX_CHUNK];
static int32_t gMixRight[MIX_CHUNK]; static int32_t gMixRight[MIX_CHUNK];
// The 8-bit stereo sides, before they are interleaved.
static uint8_t gU8Left[MIX_CHUNK];
static uint8_t gU8Right[MIX_CHUNK];
// mt_arpeggio: the note, then the note plus x semitones, then plus y, as // mt_arpeggio: the note, then the note plus x semitones, then plus y, as
@ -695,26 +707,48 @@ bool modPlayerRenderS16(int16_t *dst, uint32_t frames, uint8_t channels) {
} }
bool modPlayerRenderU8(uint8_t *dst, uint32_t count) { bool modPlayerRenderU8(uint8_t *dst, uint32_t frames, uint8_t channels) {
uint32_t run; uint32_t run;
uint32_t f;
uint8_t i; uint8_t i;
while (count > 0u) { while (frames > 0u) {
if (!renderAdvance(&run, count)) { if (!renderAdvance(&run, frames)) {
memset(dst, MOD_PLAYER_SILENCE_U8, count); memset(dst, MOD_PLAYER_SILENCE_U8, frames * channels);
return false; return false;
} }
if (channels == S16_STEREO) {
voiceMixSidesU8(run);
for (f = 0u; f < run; f++) {
*dst++ = gU8Left[f];
*dst++ = gU8Right[f];
}
} else if (gU8Shift < U8_SHIFT_QUARTER) {
// Four channels at more than a quarter can pass a byte: each
// side alone, then the two added through the clamp.
voiceMixSidesU8(run);
for (f = 0u; f < run; f++) {
*dst++ = gU8Clamp[(uint16_t)gU8Left[f] + gU8Right[f]];
}
} else {
memset(dst, MOD_PLAYER_SILENCE_U8, run); memset(dst, MOD_PLAYER_SILENCE_U8, run);
for (i = 0u; i < MOD_CHANNELS; i++) { for (i = 0u; i < MOD_CHANNELS; i++) {
voiceMixU8(&gPlay.paula[i], dst, run); voiceMixU8(&gPlay.paula[i], dst, run);
} }
dst += run; dst += run;
count -= run; }
frames -= run;
} }
return true; return true;
} }
void modPlayerSetU8Shift(uint8_t shift) {
gU8Shift = shift < U8_SHIFT_HALF ? U8_SHIFT_HALF : (shift > U8_SHIFT_QUARTER ? U8_SHIFT_QUARTER : shift);
gVolTabBuilt = false;
}
bool modPlayerStart(const uint8_t *data, uint32_t length, uint32_t rate, bool loop) { bool modPlayerStart(const uint8_t *data, uint32_t length, uint32_t rate, bool loop) {
const uint8_t *info; const uint8_t *info;
uint32_t samplesOffset; uint32_t samplesOffset;
@ -1394,6 +1428,18 @@ static void voiceMixS16(PaulaT *a, int32_t *acc, uint32_t count) {
} }
// The two sides into gU8Left and gU8Right: channels 0 and 3 left, 1 and 2
// right, as Paula plays them.
static void voiceMixSidesU8(uint32_t count) {
memset(gU8Left, MOD_PLAYER_SILENCE_U8, count);
memset(gU8Right, MOD_PLAYER_SILENCE_U8, count);
voiceMixU8(&gPlay.paula[0], gU8Left, count);
voiceMixU8(&gPlay.paula[1], gU8Right, count);
voiceMixU8(&gPlay.paula[2], gU8Right, count);
voiceMixU8(&gPlay.paula[3], gU8Left, count);
}
// One channel added into the 8-bit output: runs of its sample through the // One channel added into the 8-bit output: runs of its sample through the
// volume's table, each as far as its block's end, on to the next block there. // volume's table, each as far as its block's end, on to the next block there.
static void voiceMixU8(PaulaT *a, uint8_t *dst, uint32_t count) { static void voiceMixU8(PaulaT *a, uint8_t *dst, uint32_t count) {
@ -1479,22 +1525,28 @@ static bool voiceWrap(PaulaT *a, uint32_t *idx) {
} }
// gVolTab[v][s] is the signed sample s (as its byte) times volume v, over 256: // gVolTab[v][s] is the signed sample s (as its byte) times volume v, over 64
// a quarter of the sample at volume 64. Built by sums, so the 68000 needs no // << gU8Shift: the sample shifted down gU8Shift bits at volume 64. Built by
// multiplies for it. // sums, so the 68000 needs no multiplies for it. And gU8Clamp, the sides'
// sum.
static void volTabBuild(void) { static void volTabBuild(void) {
uint16_t volume; uint16_t volume;
uint16_t s; uint16_t s;
int16_t product; int16_t product;
int16_t sum;
for (volume = 0u; volume <= VOLUME_MAX; volume++) { for (volume = 0u; volume <= VOLUME_MAX; volume++) {
// From sample -128 up: product steps by volume. // From sample -128 up: product steps by volume.
product = (int16_t)(-(int16_t)U8_BIAS * (int16_t)volume); product = (int16_t)(-(int16_t)U8_BIAS * (int16_t)volume);
for (s = 0u; s < SAMPLE_VALUES; s++) { for (s = 0u; s < SAMPLE_VALUES; s++) {
gVolTab[volume][(uint8_t)(s + U8_BIAS)] = (int8_t)(product >> (VOLUME_SHIFT + U8_SHIFT)); gVolTab[volume][(uint8_t)(s + U8_BIAS)] = (int8_t)(product >> (VOLUME_SHIFT + gU8Shift));
product = (int16_t)(product + (int16_t)volume); product = (int16_t)(product + (int16_t)volume);
} }
} }
for (s = 0u; s < U8_SUMS; s++) {
sum = (int16_t)((int16_t)s - U8_BIAS);
gU8Clamp[s] = (uint8_t)(sum < 0 ? 0 : (sum > U8_MAX ? U8_MAX : sum));
}
gVolTabBuilt = true; gVolTabBuilt = true;
} }

View file

@ -20,16 +20,26 @@
// a song that does not loop, its end. // a song that does not loop, its end.
bool modPlayerActive(void); bool modPlayerActive(void);
// Mono unsigned 8-bit, centred on MOD_PLAYER_SILENCE_U8: the DOS, ST and // Unsigned 8-bit, centred on MOD_PLAYER_SILENCE_U8: the DOS, ST and X68000
// X68000 ports' format. Returns false once the song has ended (or nothing // ports' format, mono when channels is 1, and interleaved when it is 2
// plays): the rest of dst is silence and the player has stopped. // (Paula's left and right channels; each channel at the level
bool modPlayerRenderU8(uint8_t *dst, uint32_t count); // modPlayerSetU8Shift gives it, in mono or stereo alike). Returns
// false once the song has ended (or nothing plays): the rest of dst is
// silence and the player has stopped.
bool modPlayerRenderU8(uint8_t *dst, uint32_t frames, uint8_t channels);
// Signed 16-bit frames, interleaved when channels is 2 (Paula's left and // Signed 16-bit frames, interleaved when channels is 2 (Paula's left and
// right channels, as the Amiga plays them), mono when it is 1: the hosted // right channels, as the Amiga plays them), mono when it is 1: the hosted
// ports' format. Returns false as modPlayerRenderU8 does. // ports' format. Returns false as modPlayerRenderU8 does.
bool modPlayerRenderS16(int16_t *dst, uint32_t frames, uint8_t channels); bool modPlayerRenderS16(int16_t *dst, uint32_t frames, uint8_t channels);
// The 8-bit renders' level: a channel at full volume playing a full-scale
// sample swings full scale shifted down shift bits, 1 or 2 (2 until set:
// four channels fill a byte and never wrap). At 1 a side's two channels
// still fit, and a mono render adds its two sides through a clamp, so four
// loud channels clip. A port sets its own before modPlayerStart.
void modPlayerSetU8Shift(uint8_t shift);
// Plays the module data[0..length) at rate output samples a second. The // Plays the module data[0..length) at rate output samples a second. The
// player keeps its own copy, so data may go once this returns. loop false // player keeps its own copy, so data may go once this returns. loop false
// ends the song where it comes back to a row it has played (its end, or a // ends the song where it comes back to a row it has played (its end, or a

View file

@ -40,16 +40,15 @@
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
// Root the save tree at the APPLICATION directory via the GS/OS "1/" prefix // The file roots (fileRoot.h) are the APPLICATION directory, through the
// designator (the Loader sets prefix 1 to the app's directory at launch). // GS/OS "1/" prefix designator (the Loader sets prefix 1 to the app's
// Unrooted paths resolve against prefix 0, which a Finder launch leaves // directory at launch). Unrooted paths resolve against prefix 0, which a
// pointing away from (or nowhere near) the app's volume - measured: every // Finder launch leaves pointing away from (or nowhere near) the app's
// plain "SAVES/x" open failed under MAME GS/OS 6 while the same file opened // volume - measured: every plain "SAVES/x" open failed under MAME GS/OS 6
// fine through "1/SAVES/x". libc's fopen passes the string verbatim to // while the same file opened fine through "1/SAVES/x". libc's fopen passes
// GS/OS Open, which accepts prefix-designator syntax. // the string verbatim to GS/OS Open, which accepts prefix-designator syntax.
#define JL_SAVE_DIR "1/SAVES" #define IIGS_FILE_ROOT "1/"
#define JL_SAVE_DIR_PREFIX "1/SAVES/" #define JL_SAVE_PREFIX_NUM 1u // the GS/OS prefix that names: jlpDiskFree's volume
#define JL_SAVE_PREFIX_NUM 1u // the GS/OS prefix those name: jlpDiskFree's volume
// ===================================================================== // =====================================================================
// IIgs direct-dispatch macros. // IIgs direct-dispatch macros.
@ -823,6 +822,14 @@ static inline uint32_t audioNoiseHz(uint8_t pitch) {
return AUDIO_NOISE_CLOCK_HZ / (pitch == 0u ? 1u : (uint32_t)pitch); return AUDIO_NOISE_CLOCK_HZ / (pitch == 0u ? 1u : (uint32_t)pitch);
} }
// An effect faster than the port's chip plays (the Amiga's Paula, which
// reads every byte it is given) plays every audioDecimationFactor-th sample
// at rateHz over the factor, so at its own pitch (src/core/audio.c);
// audioDecimate picks them, a stream's chunks one after another with its
// phase carried between them. (The IIgs's DOC steps over samples itself.)
uint32_t audioDecimate(uint8_t *dst, const uint8_t *src, uint32_t count, uint8_t factor, uint8_t *phase);
uint8_t audioDecimationFactor(uint16_t rateHz, uint16_t maxHz);
bool jlpGenericAudioInit(void); bool jlpGenericAudioInit(void);
#if !defined(jlpAudioInit) #if !defined(jlpAudioInit)
#if defined(JL_HAS_AUDIO_INIT) #if defined(JL_HAS_AUDIO_INIT)
@ -889,6 +896,11 @@ void jlpGenericAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *c
#define jlpAudioPlaySfxStreamRaw(_a, _b, _c, _d) jlpAudioPlaySfxStream((_a), (_b), (_c), (_d)) #define jlpAudioPlaySfxStreamRaw(_a, _b, _c, _d) jlpAudioPlaySfxStream((_a), (_b), (_c), (_d))
#endif #endif
#endif #endif
// DVX's sound library takes 8-bit PCM unsigned, so the IIgs's raw format
// (unsigned, 0x00 never used) is DVX's own bytes too: no flip.
#if defined(JOEYLIB_PLATFORM_DVX)
#define jlpAudioSfxRawFormat() 1u
#endif
#if !defined(jlpAudioSfxRawFormat) #if !defined(jlpAudioSfxRawFormat)
#define jlpAudioSfxRawFormat() 0u #define jlpAudioSfxRawFormat() 0u
#endif #endif
@ -1096,6 +1108,18 @@ void jlpGenericSerialSetRts(bool on);
#define jlpSerialSetRts(_o) jlpGenericSerialSetRts((_o)) #define jlpSerialSetRts(_o) jlpGenericSerialSetRts((_o))
#endif #endif
#endif #endif
// A port that feeds its transmitter a byte at a time, polling until it takes
// each, bounds the wait with these (src/core/serial.c): serialTxStalled turns
// true once the transmitter has taken nothing for SERIAL_TX_STALL_MS (a peer
// holding transmit off, a line held), or after pollLimit polls, which is
// what stops a wait on a clock that does not run yet (before jlInit).
typedef struct {
uint32_t startMs;
uint32_t polls;
uint32_t pollLimit;
} SerialTxWaitT;
bool serialTxStalled(SerialTxWaitT *wait);
void serialTxWaitStart(SerialTxWaitT *wait, uint32_t pollLimit);
// --- Save files + disk space (platform-only; blank-port stub generic + JL_HAS override) --- // --- Save files + disk space (platform-only; blank-port stub generic + JL_HAS override) ---
@ -1125,6 +1149,14 @@ uint32_t jlpGenericDiskFree(void);
#define jlpDiskFree() jlpGenericDiskFree() #define jlpDiskFree() jlpGenericDiskFree()
#endif #endif
#endif #endif
bool jlpGenericSaveRename(const char *from, const char *to);
#if !defined(jlpSaveRename)
#if defined(JL_HAS_SAVE_RENAME)
bool jlpSaveRename(const char *from, const char *to);
#else
#define jlpSaveRename(_f, _t) jlpGenericSaveRename((_f), (_t))
#endif
#endif
// --- Compiled code written (the codegen arena) --- // --- Compiled code written (the codegen arena) ---

179
src/core/psgSynth.c Normal file
View file

@ -0,0 +1,179 @@
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// The software sound chip (psgSynth.h). Phase accumulators are 32-bit
// fractions of a cycle, the top bit a square's half; the noise's is 32.32,
// whole LFSR clocks above the point. Integer throughout, so it builds where
// there is no maths library.
#include <string.h>
#include "port.h"
#include "psgSynth.h"
// ----- Constants -----
// Attenuation 15 (and up) is silence (joey/audio.h).
#define ATTEN_SILENT 15u
#define ATTEN_STEPS 16u
#define PHASE_SHIFT 32
#define PHASE_HIGH_HALF 0x80000000u
// The YM2149 noise generator: a 17-bit LFSR fed by bit 0 XOR bit 3, clocked
// at AUDIO_NOISE_CLOCK_HZ / period, where period is the pitch (0 counts as 1).
#define NOISE_LFSR_SEED 1u
#define NOISE_LFSR_TOP_SHIFT 16
#define NOISE_LFSR_TAP_SHIFT 3
// ----- Types -----
typedef struct {
uint32_t phase;
uint32_t step; // phase per frame; 0 = silent
int16_t amp;
} SquareT;
// ----- Prototypes -----
static int16_t attenAmp(uint8_t atten);
static void mixEveryChannel(int32_t *frame, uint8_t channels, int16_t level);
// ----- Module state -----
// PSG_SYNTH_PEAK 2 dB less a step: round(PSG_SYNTH_PEAK * 10^(-step / 10)).
static const int16_t gAttenAmp[ATTEN_STEPS] = {
8191, 6506, 5168, 4105, 3261, 2590, 2057, 1634, 1298, 1031, 819, 651, 517, 411, 326, 0
};
typedef char psgSynthPeakFits[(4 * PSG_SYNTH_PEAK <= INT16_MAX) ? 1 : -1];
typedef char psgSynthTableTop[(PSG_SYNTH_PEAK == 8191) ? 1 : -1];
static uint32_t gRate = 0u;
static SquareT gVoices[JL_AUDIO_VOICES];
static uint64_t gNoisePhase = 0u;
static uint64_t gNoiseStep = 0u; // LFSR clocks per frame, 32.32
static uint32_t gNoiseLfsr = NOISE_LFSR_SEED;
static int16_t gNoiseAmp = 0;
static jlAudioTickFnT gTickFn = NULL;
static uint32_t gTickHz = 0u;
static uint32_t gTickAcc = 0u; // gTickHz a frame; a tick each time it passes gRate
// ----- Internal helpers (alphabetical) -----
static int16_t attenAmp(uint8_t atten) {
return gAttenAmp[atten < ATTEN_SILENT ? atten : ATTEN_SILENT];
}
static void mixEveryChannel(int32_t *frame, uint8_t channels, int16_t level) {
uint8_t c;
for (c = 0; c < channels; c++) {
frame[c] += level;
}
}
// ----- Synth API (alphabetical) -----
void psgSynthInit(uint32_t rate) {
gRate = rate;
memset(gVoices, 0, sizeof(gVoices));
gNoisePhase = 0u;
gNoiseStep = 0u;
gNoiseLfsr = NOISE_LFSR_SEED;
gNoiseAmp = 0;
gTickFn = NULL;
gTickHz = 0u;
gTickAcc = 0u;
}
void psgSynthMix(int32_t *mix, uint32_t frames, uint8_t channels) {
SquareT *sq;
uint32_t i;
uint32_t clocks;
uint8_t v;
int16_t level;
for (v = 0; v < JL_AUDIO_VOICES; v++) {
sq = &gVoices[v];
if (sq->step == 0u || sq->amp == 0) {
continue;
}
for (i = 0; i < frames; i++) {
sq->phase += sq->step;
level = (sq->phase & PHASE_HIGH_HALF) != 0u ? sq->amp : (int16_t)-sq->amp;
mixEveryChannel(&mix[i * channels], channels, level);
}
}
if (gNoiseAmp == 0) {
return;
}
for (i = 0; i < frames; i++) {
gNoisePhase += gNoiseStep;
for (clocks = (uint32_t)(gNoisePhase >> PHASE_SHIFT); clocks > 0u; clocks--) {
gNoiseLfsr = (gNoiseLfsr >> 1) | (((gNoiseLfsr ^ (gNoiseLfsr >> NOISE_LFSR_TAP_SHIFT)) & 1u) << NOISE_LFSR_TOP_SHIFT);
}
gNoisePhase &= (uint64_t)UINT32_MAX;
level = (gNoiseLfsr & 1u) != 0u ? gNoiseAmp : (int16_t)-gNoiseAmp;
mixEveryChannel(&mix[i * channels], channels, level);
}
}
void psgSynthNoise(uint8_t pitch, uint8_t atten) {
uint32_t period;
if (gRate == 0u) {
return;
}
period = pitch == 0u ? 1u : pitch;
gNoiseStep = ((uint64_t)AUDIO_NOISE_CLOCK_HZ << PHASE_SHIFT) / period / gRate;
gNoiseAmp = attenAmp(atten);
}
bool psgSynthTickRegister(jlAudioTickFnT fn, uint16_t hz) {
if (fn != NULL && (hz == 0u || hz > gRate)) {
return false;
}
gTickFn = fn;
gTickHz = hz;
gTickAcc = 0u;
return true;
}
uint32_t psgSynthTickRun(uint32_t frames) {
uint32_t run;
if (gTickFn == NULL) {
return frames;
}
// A callback may unregister itself, or register another.
while (gTickFn != NULL && gTickAcc >= gRate) {
gTickAcc -= gRate;
gTickFn();
}
if (gTickFn == NULL) {
return frames;
}
// Frames until the accumulator reaches the next tick.
run = (gRate - gTickAcc + gTickHz - 1u) / gTickHz;
if (run > frames) {
run = frames;
}
gTickAcc += run * gTickHz;
return run;
}
void psgSynthVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
if (voice >= JL_AUDIO_VOICES || gRate == 0u) {
return;
}
// hz cycles a second in 32-bit fractions of a cycle per frame.
gVoices[voice].step = (uint32_t)(((uint64_t)freqHz << PHASE_SHIFT) / gRate);
gVoices[voice].amp = attenAmp(atten);
}

48
src/core/psgSynth.h Normal file
View file

@ -0,0 +1,48 @@
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// The software sound chip: the three tone voices as square waves and the
// noise voice as the YM2149's 17-bit LFSR clocked at AUDIO_NOISE_CLOCK_HZ /
// pitch (the ST's chip, the reference for the noise curve), each attenuated
// 2 dB a step (joey/audio.h), mixed at one rate -- the sound of the hosted
// ports (src/hosted/audio.c) and of DVX (src/dvx/audio.c). And the tick
// placement those mixers share: a jlAudioTickRegister callback runs between
// exactly the samples it falls between, however the mixing is batched, so
// notes start where they should in the sound.
//
// One synth a program: module state, set up by psgSynthInit at the mix
// rate. Every call runs where the mixer runs (the program's thread, or
// DVX's sound pump), never in an interrupt.
#ifndef JOEYLIB_PSG_SYNTH_H
#define JOEYLIB_PSG_SYNTH_H
#include <stdbool.h>
#include <stdint.h>
#include "joey/audio.h"
// The loudest a voice (or the noise) gets in the mix: a quarter of a 16-bit
// sample, so four at full volume stay inside one. A mixer puts a full-scale
// sound effect's peak here too (joey/audio.h's mix).
#define PSG_SYNTH_PEAK 8191
// Silence, every voice and the noise, and no tick, at rate frames a second.
void psgSynthInit(uint32_t rate);
// Add frames of the voices and the noise to mix (channels interleaved
// samples a frame, each voice in every channel).
void psgSynthMix(int32_t *mix, uint32_t frames, uint8_t channels);
// jlAudioNoise and jlAudioVoice (voice 0 .. JL_AUDIO_VOICES - 1).
void psgSynthNoise(uint8_t pitch, uint8_t atten);
void psgSynthVoice(uint8_t voice, uint16_t freqHz, uint8_t atten);
// jlAudioTickRegister: fn hz times a second of the mix (NULL: none). False
// for a rate of 0 or above the mix rate.
bool psgSynthTickRegister(jlAudioTickFnT fn, uint16_t hz);
// The mixer is about to mix up to frames frames: every tick due before the
// first of them runs now, and the frames to mix before the next tick come
// back (frames when there is none in them). The mixer mixes exactly that
// many, then asks again.
uint32_t psgSynthTickRun(uint32_t frames);
#endif

View file

@ -5,11 +5,14 @@
// Writable save files (SAVES/) and a disk-space query. // Writable save files (SAVES/) and a disk-space query.
// //
// Mirrors jlDataOpen's forced-prefix approach (see assetLoad.c) but rooted at // Mirrors jlDataOpen's forced-prefix approach (see assetLoad.c) but rooted at
// the writable SAVES/ tree instead of read-only DATA/, and it creates SAVES/ // the writable SAVES/ tree, under the port's write root (fileRoot.h), instead
// on demand before a write open. Blob read/write/exists are plain stdio over // of read-only DATA/, and it creates SAVES/ on demand before a write open.
// jlSaveOpen; only the two operations stdio cannot do portably -- create the // Blob read/exists are plain stdio over jlSaveOpen; jlSaveWrite writes a
// directory and delete a file -- plus the disk-free query are per-port HAL // temporary file and renames it over the save, so a write that fails or is
// hooks (jlpSaveDirEnsure / jlpSaveDelete / jlpDiskFree). // cut short leaves the old save whole. The operations stdio cannot do
// portably -- create the directory, delete and rename a file -- plus the
// disk-free query are per-port HAL hooks (jlpSaveDirEnsure, jlpSaveDelete,
// jlpSaveRename, jlpDiskFree).
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
@ -19,21 +22,16 @@
#include "port.h" #include "port.h"
// A port that must root the save tree elsewhere overrides these in port.h #define SAVE_DIR "SAVES"
// (IIgs: GS/OS prefix-designator "1/" = the application directory - unrooted #define SAVE_DIR_PREFIX "SAVES/"
// paths resolve against prefix 0, which a Finder launch does not point at the
// app's volume, so plain "SAVES/x" opens fail there).
#ifndef JL_SAVE_DIR
#define JL_SAVE_DIR "SAVES"
#define JL_SAVE_DIR_PREFIX "SAVES/"
#endif
#define SAVE_DIR JL_SAVE_DIR
#define SAVE_DIR_PREFIX JL_SAVE_DIR_PREFIX
#define SAVE_PATH_MAX 256
// A save name every port's filesystem takes as it is: DOS's 8.3, which also // A save name every port's filesystem takes as it is: DOS's 8.3, which also
// fits ProDOS's 15 characters starting with a letter. // fits ProDOS's 15 characters starting with a letter.
#define SAVE_NAME_BASE_MAX 8u #define SAVE_NAME_BASE_MAX 8u
#define SAVE_NAME_EXT_MAX 3u #define SAVE_NAME_EXT_MAX 3u
// jlSaveWrite's temporary file: its 10-letter name is outside the rule, so
// no save can be called it, but every filesystem takes it (DOS keeps the
// first 8 letters of it, every time).
#define SAVE_TEMP_NAME "JOEYLIBTMP"
// ----- Helpers ----- // ----- Helpers -----
@ -41,33 +39,18 @@
static bool buildSavePath(char *out, size_t cap, const char *name); static bool buildSavePath(char *out, size_t cap, const char *name);
static bool charIsAlnum(char c); static bool charIsAlnum(char c);
static bool charIsLetter(char c); static bool charIsLetter(char c);
#if defined(JL_HAS_FILE_ROOTS)
static void ensureSaveDir(void); static void ensureSaveDir(void);
#else
// SAVES/ is relative to the current directory: ensure it by its literal name.
#define ensureSaveDir() ((void)jlpSaveDirEnsure(SAVE_DIR))
#endif
static bool saveNameValid(const char *name); static bool saveNameValid(const char *name);
// Join SAVE_DIR_PREFIX + name into `out` (under the write root on a port // Join SAVE_DIR_PREFIX + name under the write root into `out`. Returns
// with run-time roots, fileRoot.h). Returns false for a name outside the // false for a name outside the portable rule (saveNameValid), on every
// portable rule (saveNameValid), on every port alike, or if it would // port alike, or if it would overflow.
// overflow.
static bool buildSavePath(char *out, size_t cap, const char *name) { static bool buildSavePath(char *out, size_t cap, const char *name) {
#if !defined(JL_HAS_FILE_ROOTS)
int written;
#endif
if (!saveNameValid(name)) { if (!saveNameValid(name)) {
return false; return false;
} }
#if defined(JL_HAS_FILE_ROOTS)
return fileRootJoin(out, cap, jlpWriteRoot(), SAVE_DIR_PREFIX, name); return fileRootJoin(out, cap, jlpWriteRoot(), SAVE_DIR_PREFIX, name);
#else
written = snprintf(out, cap, SAVE_DIR_PREFIX "%s", name);
return written >= 0 && (size_t)written < cap;
#endif
} }
@ -81,16 +64,15 @@ static bool charIsLetter(char c) {
} }
#if defined(JL_HAS_FILE_ROOTS) // Create SAVES/ under the write root. Advisory: if the folder cannot be
// Create SAVES/ under the write root. Advisory, like the literal form above. // made, the open that follows fails and its caller sees that.
static void ensureSaveDir(void) { static void ensureSaveDir(void) {
char dir[SAVE_PATH_MAX]; char dir[FILE_PATH_MAX];
if (fileRootJoin(dir, sizeof(dir), jlpWriteRoot(), SAVE_DIR, "")) { if (fileRootJoin(dir, sizeof(dir), jlpWriteRoot(), SAVE_DIR, "")) {
(void)jlpSaveDirEnsure(dir); (void)jlpSaveDirEnsure(dir);
} }
} }
#endif
// Up to SAVE_NAME_BASE_MAX letters and digits starting with a letter, then // Up to SAVE_NAME_BASE_MAX letters and digits starting with a letter, then
@ -129,12 +111,9 @@ uint32_t jlDiskFree(void) {
bool jlSaveDelete(const char *name) { bool jlSaveDelete(const char *name) {
char path[SAVE_PATH_MAX]; char path[FILE_PATH_MAX];
if (name == NULL) { if (name == NULL || !buildSavePath(path, sizeof(path), name)) {
return false;
}
if (!buildSavePath(path, sizeof(path), name)) {
return false; return false;
} }
return jlpSaveDelete(path); return jlpSaveDelete(path);
@ -157,20 +136,14 @@ bool jlSaveExists(const char *name) {
FILE *jlSaveOpen(const char *name, const char *mode) { FILE *jlSaveOpen(const char *name, const char *mode) {
char path[SAVE_PATH_MAX]; char path[FILE_PATH_MAX];
if (name == NULL || mode == NULL) { if (name == NULL || mode == NULL || !buildSavePath(path, sizeof(path), name)) {
return NULL; return NULL;
} }
// Create SAVES/ before a write / append open. The result is advisory: if
// the directory cannot be made, fopen below simply fails and the caller
// sees NULL.
if (strchr(mode, 'w') != NULL || strchr(mode, 'a') != NULL) { if (strchr(mode, 'w') != NULL || strchr(mode, 'a') != NULL) {
ensureSaveDir(); ensureSaveDir();
} }
if (!buildSavePath(path, sizeof(path), name)) {
return NULL;
}
return fopen(path, mode); return fopen(path, mode);
} }
@ -192,16 +165,26 @@ uint32_t jlSaveRead(const char *name, void *buf, uint32_t max) {
} }
// The new bytes go to SAVE_TEMP_NAME first; only once they are all written
// and closed does the old save go and the temporary file take its name. A
// write that fails, or a machine switched off part way, leaves the old save
// as it was. The old save is deleted before the rename because not every
// system's rename replaces a file that exists.
bool jlSaveWrite(const char *name, const void *buf, uint32_t len) { bool jlSaveWrite(const char *name, const void *buf, uint32_t len) {
char path[FILE_PATH_MAX];
char temp[FILE_PATH_MAX];
FILE *fp; FILE *fp;
size_t written; size_t written;
bool ok;
// Refused before the save is opened: opening it for writing empties it,
// and a call with nothing to write must not cost the old one.
if (name == NULL || (buf == NULL && len > 0u)) { if (name == NULL || (buf == NULL && len > 0u)) {
return false; return false;
} }
fp = jlSaveOpen(name, "wb"); if (!buildSavePath(path, sizeof(path), name) || !fileRootJoin(temp, sizeof(temp), jlpWriteRoot(), SAVE_DIR_PREFIX, SAVE_TEMP_NAME)) {
return false;
}
ensureSaveDir();
fp = fopen(temp, "wb");
if (fp == NULL) { if (fp == NULL) {
return false; return false;
} }
@ -209,6 +192,11 @@ bool jlSaveWrite(const char *name, const void *buf, uint32_t len) {
if (buf != NULL && len > 0u) { if (buf != NULL && len > 0u) {
written = fwrite(buf, 1u, (size_t)len, fp); written = fwrite(buf, 1u, (size_t)len, fp);
} }
fclose(fp); ok = fclose(fp) == 0 && written == (size_t)len;
return written == (size_t)len; if (!ok) {
(void)jlpSaveDelete(temp);
return false;
}
(void)jlpSaveDelete(path);
return jlpSaveRename(temp, path);
} }

View file

@ -33,6 +33,12 @@
#define SERIAL_STOP_BITS_DEF 1u #define SERIAL_STOP_BITS_DEF 1u
#define SERIAL_BAUD_DEF 9600u #define SERIAL_BAUD_DEF 9600u
// A transmit wait gives up after this long (serialTxStalled). The clock is
// read every SERIAL_TX_CLOCK_POLLS polls, from the first such read on, so a
// byte the transmitter takes at once costs no clock read at all.
#define SERIAL_TX_STALL_MS 100u
#define SERIAL_TX_CLOCK_POLLS 64u
// ----- Module state ----- // ----- Module state -----
@ -167,3 +173,30 @@ bool jlSerialWriteByte(uint8_t b) {
static void serialAtExit(void) { static void serialAtExit(void) {
jlSerialClose(); jlSerialClose();
} }
// One more poll of a busy transmitter (port.h): true once the wait is over.
bool serialTxStalled(SerialTxWaitT *wait) {
uint32_t now;
wait->polls++;
if (wait->polls >= wait->pollLimit) {
return true;
}
if (wait->polls % SERIAL_TX_CLOCK_POLLS != 0u) {
return false;
}
now = jlpMillisElapsed();
if (wait->polls == SERIAL_TX_CLOCK_POLLS) {
wait->startMs = now;
return false;
}
return now - wait->startMs >= SERIAL_TX_STALL_MS;
}
// A transmit wait begins (port.h): pollLimit is the port's backstop.
void serialTxWaitStart(SerialTxWaitT *wait, uint32_t pollLimit) {
wait->polls = 0u;
wait->pollLimit = pollLimit;
}

View file

@ -49,7 +49,7 @@
* the sprite -- the backup captured the SPRITE instead of the background, so * the sprite -- the backup captured the SPRITE instead of the background, so
* the paired restore repainted it and left residue. Draw-only rows hid this * the paired restore repainted it and left residue. Draw-only rows hid this
* because a double draw is idempotent. */ * because a double draw is idempotent. */
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_X68000) #if defined(SPRITE_68K)
#define COMPILED_SPRITE_WRITES_PLANES 1 #define COMPILED_SPRITE_WRITES_PLANES 1
#else #else
#define COMPILED_SPRITE_WRITES_PLANES 0 #define COMPILED_SPRITE_WRITES_PLANES 0
@ -785,7 +785,7 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
if (!COMPILED_SPRITE_WRITES_PLANES) { if (!COMPILED_SPRITE_WRITES_PLANES) {
jlpSpriteDrawPlanes(s, sp, x, y); jlpSpriteDrawPlanes(s, sp, x, y);
} }
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JL_CPU_X86) #if defined(SPRITE_FAST_PATHS)
// P7-1 lever b: inline the dominant h==16 stage mark to drop // P7-1 lever b: inline the dominant h==16 stage mark to drop
// the spriteMarkDirty call boundary (see the restore arm in // the spriteMarkDirty call boundary (see the restore arm in
// jlSpriteRestoreUnder). Other heights / non-stage draws keep // jlSpriteRestoreUnder). Other heights / non-stage draws keep
@ -813,7 +813,7 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
// fully-validated jlSpriteDraw for uncompiled sprites and on the ports // fully-validated jlSpriteDraw for uncompiled sprites and on the ports
// without compiled sprites. // without compiled sprites.
void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) { void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JL_CPU_X86) #if defined(SPRITE_FAST_PATHS)
if (sp->slot != NULL) { if (sp->slot != NULL) {
uint8_t shift; uint8_t shift;
uint16_t drawOffset; uint16_t drawOffset;
@ -910,7 +910,7 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS
jlpSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes); jlpSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes);
jlpSpriteDrawPlanes(s, sp, x, y); jlpSpriteDrawPlanes(s, sp, x, y);
} }
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JL_CPU_X86) #if defined(SPRITE_FAST_PATHS)
// P7-1 lever b: inline the h==16 stage mark (see jlSpriteDraw). // P7-1 lever b: inline the h==16 stage mark (see jlSpriteDraw).
if (s == gStage && heightPx == 16) { if (s == gStage && heightPx == 16) {
SPRITE_MARK_STAGE_ROWS16(x, widthPx, y); SPRITE_MARK_STAGE_ROWS16(x, widthPx, y);
@ -944,7 +944,7 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS
// sprite entries). // sprite entries).
#if !defined(JOEYLIB_PLATFORM_IIGS) #if !defined(JOEYLIB_PLATFORM_IIGS)
void jlSpriteSaveAndDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) { void jlSpriteSaveAndDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) {
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JL_CPU_X86) #if defined(SPRITE_FAST_PATHS)
ArenaSlotT *slot; ArenaSlotT *slot;
slot = sp->slot; slot = sp->slot;
@ -1041,7 +1041,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
if (s == gStage && iigsSpriteRestoreStage(backup) == 0u) { if (s == gStage && iigsSpriteRestoreStage(backup) == 0u) {
return; return;
} }
#elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JL_CPU_X86) #elif defined(SPRITE_FAST_PATHS)
// Fused stage restore (P7-1 lever b): the compiled copy body // Fused stage restore (P7-1 lever b): the compiled copy body
// stays INLINED (spriteCompiledRestoreUnder is always-inline, // stays INLINED (spriteCompiledRestoreUnder is always-inline,
// sharing the backup-field loads with the validation above), // sharing the backup-field loads with the validation above),
@ -1118,7 +1118,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
// The IIgs body is asm (joeyDraw.s, beside the stage sprite entries). // The IIgs body is asm (joeyDraw.s, beside the stage sprite entries).
#if !defined(JOEYLIB_PLATFORM_IIGS) #if !defined(JOEYLIB_PLATFORM_IIGS)
void jlSpriteRestoreUnderTrusted(jlSurfaceT *s, const jlSpriteBackupT *backup) { void jlSpriteRestoreUnderTrusted(jlSurfaceT *s, const jlSpriteBackupT *backup) {
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JL_CPU_X86) #if defined(SPRITE_FAST_PATHS)
jlSpriteT *sp; jlSpriteT *sp;
int16_t bx; int16_t bx;
int16_t by; int16_t by;
@ -1243,7 +1243,7 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
// metadata so they can size their buffer for the next // metadata so they can size their buffer for the next
// SaveUnder call. RestoreUnder will refuse to operate on a // SaveUnder call. RestoreUnder will refuse to operate on a
// backup with bytes==NULL. // backup with bytes==NULL.
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_X68000) #if defined(SPRITE_68K)
// Planar metadata widening: a real (buffered) unclipped save on // Planar metadata widening: a real (buffered) unclipped save on
// these ports goes through the compiled 16-px-region path, so a // these ports goes through the compiled 16-px-region path, so a
// probe-then-allocate caller must be told the REGION size, not // probe-then-allocate caller must be told the REGION size, not
@ -1285,15 +1285,14 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
// TRUSTED save-under (P7-1). Skips the NULL / on-surface / backup gates // TRUSTED save-under (P7-1). Skips the NULL / on-surface / backup gates
// jlSpriteSaveUnder runs, going straight to the compiled save. Same // jlSpriteSaveUnder runs, going straight to the compiled save. Same
// trust contract as jlSpriteRestoreUnderTrusted (see joey/sprite.h). // trust contract as jlSpriteRestoreUnderTrusted (see joey/sprite.h).
// Optimized on Amiga/ST (the compiled save is a read, so there is no // Optimized on the 68000 ports and x86 (the compiled save is a read, so
// fused entry -- just skip validation, and set backup->sprite, which // there is no fused entry -- just skip validation, and set
// their compiled save does not and jlSpriteRestoreUnder* reads) and on // backup->sprite, which jlSpriteRestoreUnder* reads) and on the IIgs (the
// the IIgs (the stage save entry, which fills all the backup's // stage save entry, which fills all the backup's metadata); falls back to
// metadata); falls back to the fully-validated jlSpriteSaveUnder for // the fully-validated jlSpriteSaveUnder for uncompiled sprites, other
// uncompiled sprites, other destinations on the IIgs, and on DOS / // destinations on the IIgs, and on the interpreting ports.
// generic.
void jlSpriteSaveUnderTrusted(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) { void jlSpriteSaveUnderTrusted(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) {
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) #if defined(SPRITE_FAST_PATHS)
ArenaSlotT *slot; ArenaSlotT *slot;
slot = sp->slot; slot = sp->slot;

View file

@ -12,6 +12,17 @@
#include "codegenArenaInternal.h" #include "codegenArenaInternal.h"
#include "joey/sprite.h" #include "joey/sprite.h"
// SPRITE_68K: the 68000 ports, which share the planar sprite paths (the ST
// with an emitter of its own); their compiled routines write the planes
// directly. SPRITE_FAST_PATHS: the ports whose compiled draw, save and
// restore take the fused fast paths -- the 68000 ports and x86.
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_X68000)
#define SPRITE_68K
#endif
#if defined(SPRITE_68K) || defined(JL_CPU_X86)
#define SPRITE_FAST_PATHS
#endif
#define SPRITE_OP_DRAW 0 #define SPRITE_OP_DRAW 0
#define SPRITE_OP_SAVE 1 #define SPRITE_OP_SAVE 1
#define SPRITE_OP_RESTORE 2 #define SPRITE_OP_RESTORE 2
@ -83,7 +94,7 @@
// backup->bytes, so an odd caller buffer must route BOTH ops to the // backup->bytes, so an odd caller buffer must route BOTH ops to the
// byte-safe interpreted paths (pointer parity is stable between save // byte-safe interpreted paths (pointer parity is stable between save
// and restore, so provenance never splits). // and restore, so provenance never splits).
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST) || defined(JOEYLIB_PLATFORM_X68000) #if defined(SPRITE_68K)
#define SPRITE_GROUPS0(_wPx) ((uint16_t)(((_wPx) + 15u) >> 4)) #define SPRITE_GROUPS0(_wPx) ((uint16_t)(((_wPx) + 15u) >> 4))
// NOTE (P7-1, 2026-07-11): an algebraically-reduced form of these two // NOTE (P7-1, 2026-07-11): an algebraically-reduced form of these two
// macros (save class = (x&15)+((widthPx+15)&15) >= 16; restore class = // macros (save class = (x&15)+((widthPx+15)&15) >= 16; restore class =

View file

@ -2,23 +2,29 @@
// See LICENSE.md for license information. // See LICENSE.md for license information.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// //
// DOS audio HAL: Sound Blaster Pro / SB16 8-bit mono playback driven // DOS audio HAL: Sound Blaster 8-bit playback driven by 8237 auto-init
// by 8237 auto-init DMA, fed by the shared module player // DMA, fed by the shared module player (src/core/modPlayer.c) and SFX
// (src/core/modPlayer.c) and SFX overlay (src/core/audioSfxMix.c). // overlay (src/core/audioSfxMix.c): stereo on an SB16 (DSP 4.xx), at
// 22050 Hz exactly, the module's channels on Paula's sides and each sound
// effect where jlAudioSetPan put it; mono on the cards before it (an SB
// Pro's stereo needs the DSP's high-speed mode, which stops answering
// commands), at the 22222 Hz their time constant gives 22050.
// //
// Pipeline: // Pipeline:
// * jlpAudioInit detects the card via DSP reset + version, allocates // * jlpAudioInit detects the card via DSP reset + version, allocates
// a 4 KB conventional-memory DMA buffer (aligned to not cross a // a conventional-memory DMA buffer (aligned to not cross a 64 KB
// 64 KB boundary), installs an IRQ handler on the SB's IRQ line, // boundary), installs an IRQ handler on the SB's IRQ line, and arms
// and arms auto-init DMA at 22050 Hz mono 8-bit. The DMA buffer is // auto-init DMA. The DMA buffer is split into two halves of
// split into two halves; the SB IRQ fires at the end of each half // MIX_HALF_FRAMES frames; the SB IRQ fires at the end of each half
// and flags it, and jlpAudioFrameTick refills it on the main thread. // and flags it, and jlpAudioFrameTick refills it on the main thread.
// * jlpAudioPlayMod starts the module player; the refills render it. // * jlpAudioPlayMod starts the module player, mixed into what is ahead
// of the DSP at once; the refills render the rest.
// * jlpAudioPlaySfx arms an overlay slot, mixed over the module in the // * jlpAudioPlaySfx arms an overlay slot, mixed over the module in the
// same refills. // same way.
// //
// Defaults match DOSBox-Staging's emulated SB Pro: base $220, IRQ 7, // Defaults match DOSBox-Staging's emulated Sound Blaster (an SB16): base
// 8-bit DMA channel 1. BLASTER environment variable overrides each. // $220, IRQ 7, 8-bit DMA channel 1. BLASTER environment variable overrides
// each.
// //
// Because DJGPP runs in protected mode with paging, the IRQ handler // Because DJGPP runs in protected mode with paging, the IRQ handler
// code and any data it touches (the refill flags, the SB's port and IRQ) // code and any data it touches (the refill flags, the SB's port and IRQ)
@ -61,10 +67,43 @@
#define DSP_OUT_8_AUTO 0x1C #define DSP_OUT_8_AUTO 0x1C
#define DSP_HALT_DMA8 0xD0 #define DSP_HALT_DMA8 0xD0
#define DSP_EXIT_AUTO 0xDA #define DSP_EXIT_AUTO 0xDA
// The SB16's (DSP 4.xx): the output rate in Hz, then 8-bit auto-init output
// through the FIFO, its mode byte (stereo, unsigned) and its block length.
#define DSP_OUT_RATE 0x41
#define DSP_OUT_8_AUTO_FIFO 0xC6
#define DSP_MODE_STEREO_U8 0x20
#define DSP_MAJOR_SB16 4
#define DSP_RESET_RETRIES 1024 #define DSP_RESET_RETRIES 1024
#define DSP_READY_BYTE 0xAA #define DSP_READY_BYTE 0xAA
// ----- The SB Pro's and SB16's mixer -----
//
// The mix joey/audio.h asks for: a full-scale sample, and a module channel at
// full volume, as loud as a voice at attenuation 0. The mix puts both at a
// quarter of the DSP's full scale (MIX_UNIT_SHIFT), so the module's four
// channels fit a byte, and the OPL2's loudest voice is about 5 dB under
// that, so the mixer plays the DSP's output (its "voice" input) 4 dB under
// the FM, which it sets to full; jlShutdown puts both back. A card before
// the SB Pro has no mixer, and plays effects and modules that much over.
#define SB_MIXER_INDEX_OFFSET 0x04
#define SB_MIXER_DATA_OFFSET 0x05
#define DSP_MAJOR_SBPRO 3
// The SB Pro's: a nibble a side (left high), three bits of it, ~4 dB a step.
#define MIXPRO_VOICE 0x04
#define MIXPRO_FM 0x26
#define MIXPRO_FULL 0xFF
#define MIXPRO_VOICE_MIX 0xDD // one step down: about 4 dB
// The SB16's: a register a side, five bits (7-3), 2 dB a step.
#define MIX16_VOICE_L 0x32
#define MIX16_VOICE_R 0x33
#define MIX16_FM_L 0x34
#define MIX16_FM_R 0x35
#define MIX16_FULL 0xF8
#define MIX16_VOICE_MIX 0xE8 // 29 of 31: 4 dB down
#define MIXER_SETS_MAX 4u
#define MIXER_SET_COUNT(_t) ((uint8_t)(sizeof(_t) / sizeof((_t)[0])))
// ----- 8237 DMA controller (the 8-bit channel BLASTER's D names) ----- // ----- 8237 DMA controller (the 8-bit channel BLASTER's D names) -----
#define DMA_MASK 0x0A #define DMA_MASK 0x0A
@ -96,10 +135,20 @@ static const uint8_t kDmaPagePort[DMA8_CHANNELS] = { 0x87, 0x83, 0x81, 0x82 };
// the same time constant (see gEffRate, set in jlpAudioInit) is the // the same time constant (see gEffRate, set in jlpAudioInit) is the
// single source of truth handed to the module player and the SFX resampler. // single source of truth handed to the module player and the SFX resampler.
#define MIX_RATE 22050 #define MIX_RATE 22050
#define MIX_DMA_BUFFER 4096 // total; two 2048-byte halves // The buffer is two halves of MIX_HALF_FRAMES frames each: bytes in mono,
#define MIX_HALF (MIX_DMA_BUFFER / 2) // left and right byte pairs in stereo (an SB16 plays stereo; the cards
// before it play mono).
#define MIX_HALF_FRAMES 2048u
#define MIX_CHANNELS_MAX 2u
#define MIX_HALF_MAX (MIX_HALF_FRAMES * MIX_CHANNELS_MAX)
#define MIX_DMA_BUFFER_MAX (MIX_HALF_MAX * 2u)
#define MIX_SILENCE MOD_PLAYER_SILENCE_U8 #define MIX_SILENCE MOD_PLAYER_SILENCE_U8
// Bytes ahead of the DSP where a new sound's start-now overlay begins // The 8-bit mix's unit: a module channel at full volume and a full-scale
// sound effect each swing a quarter of a byte, so the module's four channels
// fit (modPlayerSetU8Shift, audioSfxSetShift); the card's mixer makes up
// the level (above).
#define MIX_UNIT_SHIFT 2u
// Frames ahead of the DSP where a new sound's start-now overlay begins
// (sfxStartNow): about 3 ms, so the write always lands before the read. // (sfxStartNow): about 3 ms, so the write always lands before the read.
#define SFX_START_MARGIN 64u #define SFX_START_MARGIN 64u
#define SB_DSP_CLOCK_HZ 1000000u // 8-bit DSP time-constant base #define SB_DSP_CLOCK_HZ 1000000u // 8-bit DSP time-constant base
@ -120,6 +169,9 @@ static bool gDspPresent = false;
// the module player and the SFX resampler, so they match what the DAC // the module player and the SFX resampler, so they match what the DAC
// actually clocks (~22222 Hz for a 22050 Hz target) instead of drifting. // actually clocks (~22222 Hz for a 22050 Hz target) instead of drifting.
static uint16_t gEffRate = MIX_RATE; static uint16_t gEffRate = MIX_RATE;
// Mono or stereo (DSP 4.xx), and so a half's bytes.
static uint8_t gChannels = 1u;
static uint16_t gHalfBytes = MIX_HALF_FRAMES;
// Conventional-memory DMA buffer. // Conventional-memory DMA buffer.
static int gDmaSelector = 0; static int gDmaSelector = 0;
@ -137,9 +189,14 @@ static bool gSbIrqWasMasked = false; // the line's mask before t
static bool gModTail = false; static bool gModTail = false;
static uint8_t gModTailHalf = 0; static uint8_t gModTailHalf = 0;
// What the module put in each half (silence where none played): a stopped
// module is taken back out of what is mixed ahead of the DSP (modRemove).
static uint8_t gModHalf[2][MIX_HALF_MAX];
// SFX overlay: the one-shot sample mixer shared with the ST and X68000 // SFX overlay: the one-shot sample mixer shared with the ST and X68000
// (src/core/audioSfxMix.c), run in refillHalf over the module's output. // (src/core/audioSfxMix.c), run in refillHalf over the module's output.
static AudioSfxSlotT gSfxSlots[JL_AUDIO_SFX_SLOTS]; static AudioSfxSlotT gSfxSlots[JL_AUDIO_SFX_SLOTS];
static uint8_t gSfxPan[JL_AUDIO_SFX_SLOTS]; // jlAudioSetPan's, stereo only
// ISR-side state. volatile so the main loop sees IRQ updates promptly. // ISR-side state. volatile so the main loop sees IRQ updates promptly.
// gNextHalf flips between 0 and 1 each IRQ; gNeedRefill[i] is raised // gNextHalf flips between 0 and 1 each IRQ; gNeedRefill[i] is raised
@ -151,6 +208,46 @@ static volatile bool gNeedRefill[2] = { false, false };
// Whether interrupts were on at the outermost jlAudioCriticalEnter. // Whether interrupts were on at the outermost jlAudioCriticalEnter.
static bool gCriticalIrqWasOn = false; static bool gCriticalIrqWasOn = false;
// What is mixed ahead of the DSP, from SFX_START_MARGIN frames past where
// it plays: from start to halfEnd, the end of the half it plays (both byte
// offsets in the buffer; none when start is past it), and when nextMixed
// (the other half's interrupt has come and its refill is done) the other
// half from otherFrom (bytes into it: past 0 when the margin reaches into
// it) on.
typedef struct {
uint16_t start;
uint16_t halfEnd;
uint16_t otherFrom;
uint8_t half;
bool nextMixed;
} AheadT;
// A mixer register the mix sets, and its value.
typedef struct {
uint8_t reg;
uint8_t value;
} MixerSetT;
static const MixerSetT kMixerPro[] = {
{ MIXPRO_VOICE, MIXPRO_VOICE_MIX },
{ MIXPRO_FM, MIXPRO_FULL }
};
static const MixerSetT kMixer16[] = {
{ MIX16_VOICE_L, MIX16_VOICE_MIX },
{ MIX16_VOICE_R, MIX16_VOICE_MIX },
{ MIX16_FM_L, MIX16_FULL },
{ MIX16_FM_R, MIX16_FULL }
};
typedef char mixer16Fits[(MIXER_SET_COUNT(kMixer16) <= MIXER_SETS_MAX) ? 1 : -1];
typedef char mixerProFits[(MIXER_SET_COUNT(kMixerPro) <= MIXER_SETS_MAX) ? 1 : -1];
// The card's mixer settings the mix made (none before the SB Pro), and what
// each register held before: put back at shutdown.
static const MixerSetT *gMixerSets = NULL;
static uint8_t gMixerSetCount = 0u;
static uint8_t gMixerSaved[MIXER_SETS_MAX];
// ----- Prototypes ----- // ----- Prototypes -----
static uint8_t adlibAttenTl(uint8_t atten); static uint8_t adlibAttenTl(uint8_t atten);
@ -162,6 +259,7 @@ static bool adlibReady(void);
static void adlibShutdown(void); static void adlibShutdown(void);
static void adlibSilence(void); static void adlibSilence(void);
static void adlibWrite(uint8_t reg, uint8_t val); static void adlibWrite(uint8_t reg, uint8_t val);
static bool aheadOfDsp(AheadT *ahead);
static bool allocDmaBuffer(void); static bool allocDmaBuffer(void);
static uint16_t dmaPlayOffset(void); static uint16_t dmaPlayOffset(void);
static int dspRead(void); static int dspRead(void);
@ -170,10 +268,19 @@ static uint8_t dspTimeConst(uint16_t rate);
static void dspWrite(uint8_t value); static void dspWrite(uint8_t value);
static void freeDmaBuffer(void); static void freeDmaBuffer(void);
static void haltDspDma(void); static void haltDspDma(void);
static void mixerBalance(void);
static uint8_t mixerRead(uint8_t reg);
static void mixerRestore(void);
static void mixerWrite(uint8_t reg, uint8_t value);
static void modAdd(uint8_t half, uint16_t from);
static void modRemove(uint8_t half, uint16_t from);
static void modRemoveAhead(void);
static void modStartNow(void);
static void parseBlasterEnv(void); static void parseBlasterEnv(void);
static void programDmaController(void); static void programDmaController(void);
static void programDspAutoInit(void); static void programDspAutoInit(void);
static void refillHalf(uint8_t halfIdx); static void refillHalf(uint8_t halfIdx);
static void sfxOverlay(uint8_t *dst, uint16_t frames, uint8_t first, uint8_t count);
static void sfxStartNow(uint8_t slot); static void sfxStartNow(uint8_t slot);
static void sbIsr(void); static void sbIsr(void);
static void speakerSet(uint16_t freqHz); static void speakerSet(uint16_t freqHz);
@ -303,6 +410,25 @@ static bool dspReset(void) {
// 64 KB physical-page boundary -- the 8237 cannot DMA across that // 64 KB physical-page boundary -- the 8237 cannot DMA across that
// boundary on 8-bit channels. We over-allocate to 2x size and use the // boundary on 8-bit channels. We over-allocate to 2x size and use the
// lower-aligned half so we always have room. // lower-aligned half so we always have room.
// Where the DSP plays and what is mixed ahead of it (AheadT); false when it
// is past the buffer (between the end and the auto-init restart).
static bool aheadOfDsp(AheadT *ahead) {
uint16_t pos;
pos = dmaPlayOffset();
ahead->half = (uint8_t)(pos / gHalfBytes);
if (ahead->half > 1u) {
return false;
}
// A whole frame, so the sides keep their places.
ahead->start = (uint16_t)((pos / gChannels + SFX_START_MARGIN) * gChannels);
ahead->halfEnd = (uint16_t)((ahead->half + 1u) * gHalfBytes);
ahead->otherFrom = ahead->start > ahead->halfEnd ? (uint16_t)(ahead->start - ahead->halfEnd) : 0u;
ahead->nextMixed = ahead->half == gNextHalf && !gNeedRefill[ahead->half ^ 1u] && ahead->otherFrom < gHalfBytes;
return true;
}
static bool allocDmaBuffer(void) { static bool allocDmaBuffer(void) {
int sel; int sel;
int seg; int seg;
@ -310,7 +436,7 @@ static bool allocDmaBuffer(void) {
uint32_t aligned; uint32_t aligned;
int paragraphs; int paragraphs;
paragraphs = ((MIX_DMA_BUFFER * 2) + 15) >> 4; paragraphs = ((MIX_DMA_BUFFER_MAX * 2) + 15) >> 4;
seg = __dpmi_allocate_dos_memory(paragraphs, &sel); seg = __dpmi_allocate_dos_memory(paragraphs, &sel);
if (seg < 0) { if (seg < 0) {
return false; return false;
@ -318,8 +444,8 @@ static bool allocDmaBuffer(void) {
linear = (uint32_t)seg << 4; linear = (uint32_t)seg << 4;
// Round linear up to a starting offset that gives us // Round linear up to a starting offset that gives us
// MIX_DMA_BUFFER without crossing the next 64 KB boundary. // MIX_DMA_BUFFER_MAX without crossing the next 64 KB boundary.
if (((linear & 0xFFFF) + MIX_DMA_BUFFER) > 0x10000) { if (((linear & 0xFFFF) + MIX_DMA_BUFFER_MAX) > 0x10000) {
aligned = (linear + 0xFFFFu) & ~0xFFFFu; aligned = (linear + 0xFFFFu) & ~0xFFFFu;
} else { } else {
aligned = linear; aligned = linear;
@ -328,7 +454,7 @@ static bool allocDmaBuffer(void) {
gDmaSelector = sel; gDmaSelector = sel;
gDmaLinear = aligned; gDmaLinear = aligned;
gDmaBuf = (uint8_t *)(__djgpp_conventional_base + aligned); gDmaBuf = (uint8_t *)(__djgpp_conventional_base + aligned);
memset(gDmaBuf, MIX_SILENCE, MIX_DMA_BUFFER); memset(gDmaBuf, MIX_SILENCE, MIX_DMA_BUFFER_MAX);
return true; return true;
} }
@ -350,7 +476,7 @@ static void programDmaController(void) {
page = (uint8_t)((gDmaLinear >> 16) & 0xFF); page = (uint8_t)((gDmaLinear >> 16) & 0xFF);
offset = (uint16_t)(gDmaLinear & 0xFFFF); offset = (uint16_t)(gDmaLinear & 0xFFFF);
count = (uint16_t)(MIX_DMA_BUFFER - 1); count = (uint16_t)(gHalfBytes * 2u - 1u);
outportb(DMA_MASK, (uint8_t)(DMA_MASK_DISABLE | gSbDma8)); outportb(DMA_MASK, (uint8_t)(DMA_MASK_DISABLE | gSbDma8));
outportb(DMA_FLIPFLOP, 0); outportb(DMA_FLIPFLOP, 0);
@ -381,18 +507,28 @@ static uint8_t dspTimeConst(uint16_t rate) {
} }
// An interrupt at the end of each half. The SB16 takes the rate in Hz and
// plays stereo; the cards before it take a time constant and play mono.
static void programDspAutoInit(void) { static void programDspAutoInit(void) {
uint16_t blockMinusOne; uint16_t blockMinusOne;
blockMinusOne = (uint16_t)(gHalfBytes - 1u);
dspWrite(DSP_SPEAKER_ON); dspWrite(DSP_SPEAKER_ON);
if (gChannels == MIX_CHANNELS_MAX) {
dspWrite(DSP_OUT_RATE);
dspWrite((uint8_t)((MIX_RATE >> 8) & 0xFF));
dspWrite((uint8_t)(MIX_RATE & 0xFF));
dspWrite(DSP_OUT_8_AUTO_FIFO);
dspWrite(DSP_MODE_STEREO_U8);
dspWrite((uint8_t)(blockMinusOne & 0xFF));
dspWrite((uint8_t)((blockMinusOne >> 8) & 0xFF));
return;
}
dspWrite(DSP_TIME_CONST); dspWrite(DSP_TIME_CONST);
dspWrite(dspTimeConst(MIX_RATE)); dspWrite(dspTimeConst(MIX_RATE));
blockMinusOne = (uint16_t)(MIX_HALF - 1);
dspWrite(DSP_BLOCK_SIZE); dspWrite(DSP_BLOCK_SIZE);
dspWrite((uint8_t)(blockMinusOne & 0xFF)); dspWrite((uint8_t)(blockMinusOne & 0xFF));
dspWrite((uint8_t)((blockMinusOne >> 8) & 0xFF)); dspWrite((uint8_t)((blockMinusOne >> 8) & 0xFF));
dspWrite(DSP_OUT_8_AUTO); dspWrite(DSP_OUT_8_AUTO);
} }
@ -404,6 +540,127 @@ static void haltDspDma(void) {
} }
// The card's mixer to the mix (the mixer constants above), what it held
// noted first. Nothing on a card before the SB Pro.
static void mixerBalance(void) {
uint8_t i;
if (gDspMajor >= DSP_MAJOR_SB16) {
gMixerSets = kMixer16;
gMixerSetCount = MIXER_SET_COUNT(kMixer16);
} else if (gDspMajor >= DSP_MAJOR_SBPRO) {
gMixerSets = kMixerPro;
gMixerSetCount = MIXER_SET_COUNT(kMixerPro);
} else {
gMixerSetCount = 0u;
}
for (i = 0; i < gMixerSetCount; i++) {
gMixerSaved[i] = mixerRead(gMixerSets[i].reg);
mixerWrite(gMixerSets[i].reg, gMixerSets[i].value);
}
}
static uint8_t mixerRead(uint8_t reg) {
outportb(gSbBase + SB_MIXER_INDEX_OFFSET, reg);
return inportb(gSbBase + SB_MIXER_DATA_OFFSET);
}
// The mixer as mixerBalance found it.
static void mixerRestore(void) {
uint8_t i;
for (i = 0; i < gMixerSetCount; i++) {
mixerWrite(gMixerSets[i].reg, gMixerSaved[i]);
}
gMixerSetCount = 0u;
}
static void mixerWrite(uint8_t reg, uint8_t value) {
outportb(gSbBase + SB_MIXER_INDEX_OFFSET, reg);
outportb(gSbBase + SB_MIXER_DATA_OFFSET, value);
}
// Mix the module into half from byte from on, over what is there (the
// effects): what it renders is kept in gModHalf, as a refill keeps it. A
// song that ends in it sounds until the DSP has played it.
static void modAdd(uint8_t half, uint16_t from) {
uint8_t *dst;
uint8_t *mod;
uint16_t i;
int value;
dst = gDmaBuf + (uint32_t)half * gHalfBytes;
mod = gModHalf[half];
if (!modPlayerRenderU8(mod + from, (uint32_t)(gHalfBytes - from) / gChannels, gChannels)) {
gModTail = true;
gModTailHalf = half;
}
for (i = from; i < gHalfBytes; i++) {
value = (int)dst[i] + (int)mod[i] - MIX_SILENCE;
dst[i] = (uint8_t)(value < 0 ? 0 : (value > UINT8_MAX ? UINT8_MAX : value));
}
}
// Take the module back out of half from byte from on: each byte less its
// module part (gModHalf) and back on the silence line, so the effects mixed
// over it go on alone. A byte the effects pushed to a limit comes out a
// little off, as clipping left it.
static void modRemove(uint8_t half, uint16_t from) {
uint8_t *dst;
uint8_t *mod;
uint16_t i;
int value;
dst = gDmaBuf + (uint32_t)half * gHalfBytes;
mod = gModHalf[half];
for (i = from; i < gHalfBytes; i++) {
value = (int)dst[i] - (int)mod[i] + MIX_SILENCE;
dst[i] = (uint8_t)(value < 0 ? 0 : (value > UINT8_MAX ? UINT8_MAX : value));
mod[i] = MIX_SILENCE;
}
}
// A stopped (or replaced) module out of everything mixed ahead of the DSP,
// so it stops now rather than up to two halves (~185 ms) later; modStartNow
// is the other way round.
static void modRemoveAhead(void) {
AheadT ahead;
if (!aheadOfDsp(&ahead)) {
return;
}
if (ahead.start < ahead.halfEnd) {
modRemove(ahead.half, (uint16_t)(ahead.start - (uint16_t)ahead.half * gHalfBytes));
}
if (ahead.nextMixed) {
modRemove((uint8_t)(ahead.half ^ 1u), ahead.otherFrom);
}
}
// A module just started, mixed into everything mixed ahead of the DSP, as
// sfxStartNow does a sound, so it starts now rather than at the next refill.
static void modStartNow(void) {
AheadT ahead;
if (!aheadOfDsp(&ahead)) {
return;
}
if (ahead.start < ahead.halfEnd) {
modAdd(ahead.half, (uint16_t)(ahead.start - (uint16_t)ahead.half * gHalfBytes));
}
if (ahead.nextMixed && modPlayerActive()) {
modAdd((uint8_t)(ahead.half ^ 1u), ahead.otherFrom);
}
}
// Refill one half of the DMA buffer from the module player. // Refill one half of the DMA buffer from the module player.
// Called from jlpAudioFrameTick on the main thread (NOT in IRQ // Called from jlpAudioFrameTick on the main thread (NOT in IRQ
// context); the IRQ only flags which halves need refilling. // context); the IRQ only flags which halves need refilling.
@ -413,16 +670,17 @@ static void haltDspDma(void) {
static void refillHalf(uint8_t halfIdx) { static void refillHalf(uint8_t halfIdx) {
uint8_t *dst; uint8_t *dst;
dst = gDmaBuf + ((uint32_t)halfIdx * MIX_HALF); dst = gDmaBuf + ((uint32_t)halfIdx * gHalfBytes);
if (modPlayerActive()) { if (modPlayerActive()) {
if (!modPlayerRenderU8(dst, MIX_HALF)) { if (!modPlayerRenderU8(gModHalf[halfIdx], MIX_HALF_FRAMES, gChannels)) {
gModTail = true; gModTail = true;
gModTailHalf = halfIdx; gModTailHalf = halfIdx;
} }
} else { } else {
memset(dst, MIX_SILENCE, MIX_HALF); memset(gModHalf[halfIdx], MIX_SILENCE, gHalfBytes);
} }
audioSfxOverlayMix(dst, MIX_HALF, gSfxSlots, JL_AUDIO_SFX_SLOTS); memcpy(dst, gModHalf[halfIdx], gHalfBytes);
sfxOverlay(dst, MIX_HALF_FRAMES, 0u, JL_AUDIO_SFX_SLOTS);
} }
@ -445,6 +703,17 @@ static void sbIsr(void) {
} }
// Slots first .. first + count - 1 mixed over frames frames at dst: in
// stereo each on its pan's side (audioSfxOverlayMixStereo).
static void sfxOverlay(uint8_t *dst, uint16_t frames, uint8_t first, uint8_t count) {
if (gChannels == 1u) {
audioSfxOverlayMix(dst, frames, &gSfxSlots[first], count);
return;
}
audioSfxOverlayMixStereo(dst, frames, &gSfxSlots[first], count, &gSfxPan[first], AUDIO_SFX_DST_UNSIGNED);
}
// Start a just-armed slot now. A refill mixes it, but the halves ahead of // Start a just-armed slot now. A refill mixes it, but the halves ahead of
// the DSP are already mixed, so on its own a sound waited for the next // the DSP are already mixed, so on its own a sound waited for the next
// refill: up to two halves, ~90-190 ms. So its first samples go on top of // refill: up to two halves, ~90-190 ms. So its first samples go on top of
@ -452,25 +721,16 @@ static void sbIsr(void) {
// has been refilled already, the half after it -- and the refills carry on // has been refilled already, the half after it -- and the refills carry on
// from where that leaves the slot. // from where that leaves the slot.
static void sfxStartNow(uint8_t slot) { static void sfxStartNow(uint8_t slot) {
uint16_t pos; AheadT ahead;
uint16_t start;
uint16_t halfEnd;
uint8_t half;
pos = dmaPlayOffset(); if (!aheadOfDsp(&ahead)) {
half = (uint8_t)(pos / MIX_HALF);
if (half > 1u) {
return; return;
} }
start = (uint16_t)(pos + SFX_START_MARGIN); if (ahead.start < ahead.halfEnd) {
halfEnd = (uint16_t)((half + 1u) * MIX_HALF); sfxOverlay(gDmaBuf + ahead.start, (uint16_t)((ahead.halfEnd - ahead.start) / gChannels), slot, 1u);
if (start < halfEnd) {
audioSfxOverlayMix(gDmaBuf + start, (uint16_t)(halfEnd - start), &gSfxSlots[slot], 1u);
} }
// The half after it is ahead of the DSP only if the interrupt for this if (ahead.nextMixed) {
// one has come (gNextHalf) and its refill is done. sfxOverlay(gDmaBuf + (uint32_t)(ahead.half ^ 1u) * gHalfBytes + ahead.otherFrom, (uint16_t)((gHalfBytes - ahead.otherFrom) / gChannels), slot, 1u);
if (half == gNextHalf && !gNeedRefill[half ^ 1u]) {
audioSfxOverlayMix(gDmaBuf + (uint32_t)(half ^ 1u) * MIX_HALF, MIX_HALF, &gSfxSlots[slot], 1u);
} }
} }
@ -498,10 +758,16 @@ bool jlpAudioInit(void) {
gDspMajor = (uint8_t)major; gDspMajor = (uint8_t)major;
gDspMinor = (uint8_t)minor; gDspMinor = (uint8_t)minor;
// Lock in the effective playback rate from the same time constant // An SB16 plays stereo at MIX_RATE itself. Before it, lock in the
// the SB DAC will receive, so the module player and the SFX resampler // effective playback rate from the same time constant the SB DAC will
// agree with the hardware instead of using the nominal MIX_RATE. // receive, so the module player and the SFX resampler agree with the
gEffRate = (uint16_t)(SB_DSP_CLOCK_HZ / (256u - dspTimeConst(MIX_RATE))); // hardware instead of using the nominal MIX_RATE.
gChannels = gDspMajor >= DSP_MAJOR_SB16 ? MIX_CHANNELS_MAX : 1u;
gHalfBytes = (uint16_t)(MIX_HALF_FRAMES * gChannels);
gEffRate = gChannels == MIX_CHANNELS_MAX ? (uint16_t)MIX_RATE : (uint16_t)(SB_DSP_CLOCK_HZ / (256u - dspTimeConst(MIX_RATE)));
memset(gSfxPan, JL_AUDIO_PAN_CENTER, sizeof(gSfxPan));
modPlayerSetU8Shift(MIX_UNIT_SHIFT);
audioSfxSetShift(MIX_UNIT_SHIFT);
if (!allocDmaBuffer()) { if (!allocDmaBuffer()) {
return false; return false;
@ -535,12 +801,22 @@ bool jlpAudioInit(void) {
programDspAutoInit(); programDspAutoInit();
gSbIrqWasMasked = picIrqMasked(gSbIrq); gSbIrqWasMasked = picIrqMasked(gSbIrq);
picUnmaskIrq(gSbIrq); picUnmaskIrq(gSbIrq);
mixerBalance();
gDspPresent = true; gDspPresent = true;
return true; return true;
} }
// A slot's side, for the sound it plays and the next: stereo (an SB16)
// only; a mono card plays every sound in the middle.
void jlpAudioSetPan(uint8_t slot, uint8_t pan) {
if (slot < JL_AUDIO_SFX_SLOTS && pan < JL_AUDIO_PAN_COUNT) {
gSfxPan[slot] = pan;
}
}
void jlpAudioShutdown(void) { void jlpAudioShutdown(void) {
int vector; int vector;
@ -566,6 +842,7 @@ void jlpAudioShutdown(void) {
gModTail = false; gModTail = false;
freeDmaBuffer(); freeDmaBuffer();
(void)dspReset(); (void)dspReset();
mixerRestore();
gDspPresent = false; gDspPresent = false;
} }
@ -579,8 +856,14 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
if (!gDspPresent) { if (!gDspPresent) {
return; return;
} }
// A song still playing goes now, not over the new one's start.
if (modPlayerActive()) {
modRemoveAhead();
}
gModTail = false; gModTail = false;
(void)modPlayerStart(data, length, gEffRate, loop); if (modPlayerStart(data, length, gEffRate, loop)) {
modStartNow();
}
} }
@ -709,6 +992,9 @@ static const uint8_t kAdlibSlotMap[ADLIB_VOICE_COUNT * 2] = {
#define ADLIB_BD_KEY_SD 0x08u // reg 0xBD bit 3: snare key #define ADLIB_BD_KEY_SD 0x08u // reg 0xBD bit 3: snare key
#define ADLIB_NOISE_MAX_HZ 6200u // pitch 0 tops out the bright end #define ADLIB_NOISE_MAX_HZ 6200u // pitch 0 tops out the bright end
#define ADLIB_NOISE_MIN_HZ 194u // pitch 31, the deepest the snare goes #define ADLIB_NOISE_MIN_HZ 194u // pitch 31, the deepest the snare goes
// The snare sounds 4-5 dB louder than a tone voice at the same total level:
// this many 0.75 dB steps down, it is as loud as one.
#define ADLIB_NOISE_TL_TRIM 7u
// The standard AdLib probe: timer 1 started at its fastest must have both // The standard AdLib probe: timer 1 started at its fastest must have both
// status flags set within 80 us, and none before. A PC with no OPL2 reads // status flags set within 80 us, and none before. A PC with no OPL2 reads
@ -747,9 +1033,12 @@ static void adlibWrite(uint8_t reg, uint8_t val) {
// Attenuation is 2 dB a step on every port (joey/audio.h); the OPL2's total // Attenuation is 2 dB a step on every port (joey/audio.h); the OPL2's total
// level is 0.75 dB a step, so a step is 8/3 of it, rounded. // level is 0.75 dB a step, so a step is 8/3 of it, rounded up from a third:
// the chip's deepest levels sound a little louder than their total level
// (measured under DOSBox: TL 37 is 27.0 dB down, not 27.75), and this keeps
// every step within half a dB of 2.
static uint8_t adlibAttenTl(uint8_t atten) { static uint8_t adlibAttenTl(uint8_t atten) {
return (uint8_t)(((uint16_t)atten * 8u + 1u) / 3u); return (uint8_t)(((uint16_t)atten * 8u + 2u) / 3u);
} }
@ -1099,19 +1388,17 @@ void jlpAudioNoise(uint8_t pitch, uint8_t atten) {
return; return;
} }
adlibWrite((uint8_t)(0x40u + ADLIB_SLOT_SD), adlibAttenTl(atten)); adlibWrite((uint8_t)(0x40u + ADLIB_SLOT_SD), (uint8_t)(adlibAttenTl(atten) + ADLIB_NOISE_TL_TRIM));
// pitch 0..31 spread evenly from ADLIB_NOISE_MAX_HZ down to // pitch 0..31 spread evenly from ADLIB_NOISE_MAX_HZ down to
// ADLIB_NOISE_MIN_HZ. (Core clamps pitch to <= 31 before the HAL // ADLIB_NOISE_MIN_HZ. (Core clamps pitch to <= 31 before the HAL
// sees it.) // sees it.)
// //
// Evenly, because the shared curve every other port follows -- // The rate every other port plays (AUDIO_NOISE_CLOCK_HZ / pitch) cannot
// rate = 26000 - pitch * 750, from src/iigs/audio_full.c -- is // be matched: the OPL2's snare is a tuned drum voice rather than a
// linear in pitch, so a linear spread here puts a given pitch at the // noise source, and its band tops out near 6 kHz where that rate runs
// same place in the range as it sits on those machines. The absolute // to 125 kHz. An even spread keeps every pitch distinct and in order,
// rates cannot be matched: the OPL2's snare is a tuned drum voice // bright to dark.
// rather than a sampled noise source, and its band tops out near
// 6 kHz where the curve asks for 26 kHz.
// //
// It replaces MAX / (pitch + 1), a hyperbola that spent nearly all // It replaces MAX / (pitch + 1), a hyperbola that spent nearly all
// its resolution on the first few pitches and squashed everything // its resolution on the first few pitches and squashed everything
@ -1174,6 +1461,7 @@ void jlpAudioStopMod(void) {
return; return;
} }
modPlayerStop(); modPlayerStop();
modRemoveAhead();
gModTail = false; gModTail = false;
} }

Some files were not shown because too many files have changed in this diff Show more