Lots more fixes to make each target behave more like the others.
This commit is contained in:
parent
d33d9c9c25
commit
484d22691b
136 changed files with 6803 additions and 2454 deletions
|
|
@ -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
|
||||
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
|
||||
|
||||
[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`
|
||||
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.
|
||||
|
||||
|
|
@ -148,7 +162,9 @@ new sample replaces it.
|
|||
|`sample` |`const uint8_t *` |Raw signed 8-bit PCM.
|
||||
|`length` |`uint32_t` |Sample count in bytes.
|
||||
|`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.
|
||||
|
|
@ -203,7 +219,7 @@ with no caller-side allocation.
|
|||
|`fill` |`jlAudioStreamFillT` |Refill callback.
|
||||
|`ctx` |`void *` |Opaque pointer handed back to the callback.
|
||||
|`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.
|
||||
|
|
@ -232,9 +248,10 @@ void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill,
|
|||
`jlAudioPlaySfxStreamRaw` is `jlAudioPlaySfxStream` except your callback
|
||||
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
|
||||
microseconds a byte, some 50 ms for each 7 KB chunk. Every other port's raw
|
||||
format is signed PCM, so there this is simply the plain call (DVX still flips
|
||||
each byte for its sound library).
|
||||
microseconds a byte, some 50 ms for each 7 KB chunk. DVX's raw format is the
|
||||
IIgs's too, since its sound library's 8-bit PCM is unsigned, and it skips its
|
||||
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`.
|
||||
|
||||
|
|
@ -279,7 +296,12 @@ What each port can actually do:
|
|||
PTPlayer picks.
|
||||
|X68000 |Ignored. Everything mixes into one ADPCM stream, so panning would move
|
||||
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).
|
||||
|SDL3, web, Win32, PSP, Haiku, BeOS |All three: the port mixes in stereo.
|
||||
|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;
|
||||
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,
|
||||
so its tones are sine waves; DVX plays square tones of its sound library,
|
||||
which puts them on the FM chip when there is a free voice and mixes them
|
||||
otherwise; the hosted ports mix square waves in software. The OPL2's waves
|
||||
are not square either.
|
||||
so its tones are sine waves; DVX and the hosted ports mix square waves in
|
||||
software (DVX through a stream of its sound library's mixer; on a machine
|
||||
whose only sound is the PC speaker, DVX's own square tones instead). The
|
||||
OPL2's waves are not square either.
|
||||
|
||||
[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
|
||||
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
|
||||
sample on slot 3, up to 26 kHz, likewise for pitches 0 to 4; DVX a noise tone
|
||||
of its sound library; and the hosted ports mix the YM2149's own noise in
|
||||
software (its 17-bit generator), so it sounds as on the ST. DOS uses the OPL2
|
||||
sample on slot 3, up to 26 kHz, likewise for pitches 0 to 4; and DVX and the
|
||||
hosted ports mix the YM2149's own noise in software (its 17-bit generator),
|
||||
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,
|
||||
not free-running noise, whose pitch spreads over the snare's range rather than
|
||||
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
|
||||
did not start, or when the port cannot take the rate (above 1000 Hz on DVX, or
|
||||
above the mixer's rate on the hosted ports). Uninstalling always returns
|
||||
`true`.
|
||||
did not start, or when the port cannot take the rate (above the mixer's rate
|
||||
on DVX and the hosted ports). Uninstalling always returns `true`.
|
||||
|
||||
[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
|
||||
differs. DOS, the Amiga, and the hosted ports place each one at its own time.
|
||||
The ST and X68000 deliver them from the system's own timer, 200 and 100 times a
|
||||
second, so the calls due between two of its ticks come together. The IIgs and
|
||||
DVX deliver them in program time from `jlAudioFrameTick`, so a frame's calls
|
||||
differs. DOS, the Amiga, DVX, and the hosted ports place each one at its own
|
||||
time. The ST and X68000 deliver them from the system's own timer, 200 and 100
|
||||
times a second, so the calls due between two of its ticks come together. The
|
||||
IIgs delivers them in program time from `jlAudioFrameTick`, so a frame's calls
|
||||
come together at it.
|
||||
|
||||
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
|
||||
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
|
||||
program time, once per counted tick, at the next `jlAudioFrameTick`: DVX's
|
||||
sound library, which `jlAudioVoice` and the rest go through there, may not be
|
||||
called from an interrupt. `fn` may therefore do anything, but what it does
|
||||
lands on the frame, not between frames.
|
||||
|
||||
On the hosted ports the sound is mixed in program time, a few frames ahead,
|
||||
and `fn` is called by the mixer between exactly the two samples where its tick
|
||||
falls: in program time, so it may do anything, yet a note it starts begins at
|
||||
the right place in the sound. `hz` may be anything up to the mixer's rate,
|
||||
44100 (22050 on Win16).
|
||||
On DVX and the hosted ports the sound is mixed in program time, a few frames
|
||||
ahead (on DVX by its sound library's pump, which DVX also runs while the
|
||||
desktop has the processor), and `fn` is called by the mixer between exactly
|
||||
the two samples where its tick falls: in program time, so it may do anything,
|
||||
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
|
||||
where the card refuses that, on Win16), and on DVX the sound card's mixing
|
||||
rate. DVX's ticks keep time on a machine with no sound card too: its null
|
||||
driver runs the mixer as a card would.
|
||||
|
||||
=== jlAudioCriticalEnter / jlAudioCriticalExit
|
||||
|
||||
|
|
|
|||
|
|
@ -19,14 +19,15 @@ typedef struct {
|
|||
|`codegenBytes`
|
||||
|`uint32_t`
|
||||
|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
|
||||
X68000, DOS, DVX, and Win16. The other hosted ports interpret every sprite
|
||||
and never use the cache, but still allocate it (32 KB by default). The
|
||||
cache is one block, and `jlInit` fails if it cannot be had -- except on the
|
||||
IIgs, where it is up to four Memory Manager blocks of at most 64 KB, each
|
||||
inside one 64 KB bank, so at most 256 KB; a bank that is refused is tried
|
||||
again at 60 KB, and `jlInit` takes whatever part of the request it was
|
||||
granted. Size it against `jlSpriteCompiledSize()`.
|
||||
default: 8 KB on the IIgs, 64 KB on the Amiga, ST, and X68000, and 32 KB on
|
||||
DOS, DVX, and Win16. The other hosted ports interpret every sprite and have
|
||||
no cache: they allocate none, whatever is asked. Every port takes as much of
|
||||
the request as the machine has, and `jlInit` fails only if it can have none
|
||||
of it; `jlSpriteCodegenBytesTotal()` says what it got. On the IIgs the cache
|
||||
is up to four Memory Manager blocks of at most 64 KB, each inside one 64 KB
|
||||
bank, so at most 256 KB (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.
|
||||
Size it against `jlSpriteCompiledSize()`.
|
||||
|
||||
|`noAudio`
|
||||
|`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()`
|
||||
describes the reason. Where the port's own start-up fails without saying
|
||||
why, as the IIgs, Amiga, ST, X68000, and DVX ports' can, the reason is just
|
||||
`jlpInit failed`.
|
||||
describes the reason, on every port: for example, the plain ST refuses a
|
||||
monochrome monitor, and the Amiga says when chip RAM cannot hold the two
|
||||
screen pages.
|
||||
|
||||
=== jlShutdown
|
||||
|
||||
|
|
@ -74,8 +75,10 @@ void jlShutdown(void);
|
|||
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
|
||||
port installed. Surfaces and sprites the program created are not released:
|
||||
destroy them with `jlSurfaceDestroy` and `jlSpriteDestroy` first. Call it on
|
||||
every exit path; skipping it can leave the machine unusable.
|
||||
destroy them with `jlSurfaceDestroy` and `jlSpriteDestroy` first. A program
|
||||
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.
|
||||
|
||||
|
|
@ -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 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
|
||||
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.
|
||||
|
||||
*Parameters:* none. *Returns:* nothing.
|
||||
|
|
@ -243,9 +250,15 @@ seconds.
|
|||
uint32_t jlMillisElapsed(void);
|
||||
----
|
||||
|
||||
Monotonic millisecond counter since `jlInit`. It may advance 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.
|
||||
Monotonic millisecond counter since `jlInit`, read from a hardware timer to
|
||||
the millisecond on every port: the IIgs from the frame count and where the
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
Declared in `<joey/debug.h>`.
|
||||
|
||||
A crash-tracing logger that writes `joeylog.txt` in the current directory
|
||||
(on DVX, in the program's settings folder,
|
||||
`CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`). Debugging a program on an emulated
|
||||
1988 machine is awkward, and this is usually the fastest way to find out what
|
||||
it did before it stopped doing it. The headless run scripts retrieve the log
|
||||
automatically. The library itself writes to it only when something fails, such
|
||||
as sound that does not start.
|
||||
A crash-tracing logger that writes `joeylog.txt` in the program's folder,
|
||||
beside `DATA/` and `SAVES/` (<<_files>>: on the ST, the current directory; on
|
||||
DVX, the program's settings folder, `CONFIG\APPS\JOEYLIB\NAME\JOEYLOG.TXT`).
|
||||
Debugging a program on an emulated 1988 machine is awkward, and this is usually
|
||||
the fastest way to find out what it did before it stopped doing it. The
|
||||
headless run scripts retrieve the log automatically. The library itself writes
|
||||
to it only when something fails, such as sound that does not start.
|
||||
|
||||
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
|
||||
that never calls it leaves no log.
|
||||
* *The web* writes the file into the page's memory, where it is lost when the
|
||||
page closes.
|
||||
* *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.
|
||||
page closes, so every line also goes to the browser's console.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
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
|
||||
out and observe hardware state through the emulator instead.
|
||||
====
|
||||
|
||||
On DOS the library also creates `CRASH.LOG` in the current directory at
|
||||
`jlInit`, and writes the processor's registers there if the program faults.
|
||||
On DOS the library also opens `CRASH.LOG` beside the program at `jlInit`, and
|
||||
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
|
||||
|
||||
|
|
@ -61,8 +59,8 @@ void jlLogF(const char *fmt, ...);
|
|||
----
|
||||
|
||||
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
|
||||
line is cut at 199 characters.
|
||||
library ends the line, so the format needs no trailing `\n`. A formatted line
|
||||
is cut at 255 characters, on every port.
|
||||
|
||||
[cols="1,1,4", options="header"]
|
||||
|===
|
||||
|
|
|
|||
|
|
@ -8,24 +8,29 @@ packager:
|
|||
* `DATA/` -- game content: levels, sprites, audio.
|
||||
* `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
|
||||
`jlInit`, so both are found beside the program wherever it was started
|
||||
from.
|
||||
* *DOS, the ST, the Amiga, and the X68000* look in the current directory. A
|
||||
program started from its own folder (as a desktop starts it) finds them;
|
||||
one started from a command line elsewhere finds no `DATA/` and makes its
|
||||
`SAVES/` there.
|
||||
* *The IIgs* looks for `DATA/` under GS/OS prefix 0 and puts `SAVES/` in the
|
||||
application's folder (prefix 1). When the two differ, data and saves come
|
||||
from different folders.
|
||||
* *DVX* is the exception, because its programs share one current directory:
|
||||
there `DATA/` is beside the program, in its folder under DVX's `APPS`, and
|
||||
`SAVES/` is in the program's own settings folder,
|
||||
* *DOS* takes the folder from the path it started the program by, *the Amiga*
|
||||
uses `PROGDIR:` (Kickstart 2.0 and later; under 1.3, the current
|
||||
directory), *the X68000* reads it from the program's process block, *the
|
||||
IIgs* uses GS/OS prefix 1 (the application's folder), and *the hosted ports*
|
||||
take it from their executable's path. The hosted ports also make it the
|
||||
current directory in `jlInit`, for the program's own relative paths.
|
||||
* *The ST* looks in the current directory: TOS tells a program nothing of
|
||||
where it was started from. A program the desktop starts runs in its own
|
||||
folder; one started from a command line elsewhere finds no `DATA/` and makes
|
||||
its `SAVES/` there.
|
||||
* *DVX* keeps the two apart, because its programs share one current
|
||||
directory: there `DATA/` is beside the program, in its folder under DVX's
|
||||
`APPS`, and `SAVES/` is in the program's own settings folder,
|
||||
`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
|
||||
themselves.
|
||||
a reinstall never overwrites it.
|
||||
* *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
|
||||
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);
|
||||
----
|
||||
|
||||
Writes a whole save file, replacing any existing contents. The write is not
|
||||
atomic: if it fails part way (a full disk, say) the save is left short. A
|
||||
`NULL` `buf` with a non-zero `len` returns `false` and leaves the save alone.
|
||||
Writes a whole save file, replacing any existing contents. The bytes go to a
|
||||
temporary file first, and only once all of them are written does it take the
|
||||
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"]
|
||||
|===
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
characters, and so does the ST's Alt where TOS keeps its keyboard's Alt
|
||||
layer in its tables (TOS 4.00 and later, and EmuTOS: a German keyboard's
|
||||
@ [ ] { } \), and otherwise nothing (<<portsDifferInput>>). A held key
|
||||
characters, and so does the ST's Alt, the characters its national
|
||||
keyboard's Alt layer gives (a German keyboard's @ [ ] { } \;
|
||||
<<portsDifferInput>>). A held key
|
||||
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
|
||||
methods.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
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
|
||||
communications calls, whose driver owns the ports the same way. SDL3, the web,
|
||||
the PSP, Haiku, and BeOS have no serial port: `jlSerialOpen` fails there.
|
||||
communications calls, whose driver owns the ports the same way; SDL3 on Linux,
|
||||
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
|
||||
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`
|
||||
once per frame. (The X68000 takes its interrupt only when the port is opened in
|
||||
supervisor mode, which `jlInit` puts every program in; opened without it, the
|
||||
port is drained from RSDRV.SYS's own 64-byte buffer by the serial calls.)
|
||||
OS on Amiga, ST, DVX, Windows, Linux, Haiku, and BeOS -- and reads never
|
||||
block. Call `jlSerialPoll`
|
||||
once per frame.
|
||||
|
||||
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
|
||||
|
|
@ -70,9 +70,10 @@ at 9600 baud, so:
|
|||
|
||||
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
|
||||
X68000 and delivered as it came on the IIgs and DOS; on the other ports the
|
||||
system's driver decides.
|
||||
A byte that arrives with a framing, parity, or overrun error is delivered as
|
||||
it came on the IIgs, DOS, the X68000, Linux, Haiku, and BeOS (the port does
|
||||
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
|
||||
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
|
||||
stay loss-free at 9600 through full-screen presents, clears, joystick reads,
|
||||
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
|
||||
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.
|
||||
|
|
@ -209,8 +218,9 @@ A field left 0 takes a default: `baud` 9600, `dataBits` 8, and `stopBits` 1.
|
|||
|Port |Rates
|
||||
|
||||
|IIgs modem and printer ports
|
||||
|115200 divided by a whole number, up to 57600; a rate between two of those
|
||||
runs at the one above it, and anything faster at 57600.
|
||||
|115200 divided by a whole number: 115200 itself straight from the ports'
|
||||
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
|
||||
|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
|
||||
|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`
|
||||
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;
|
||||
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
|
||||
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
|
||||
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
|
||||
X68000 have one port, and open it for every device, `JL_SERIAL_SLOT` included.
|
||||
1..9, a Win32 COM number 1..255, an Amiga `serial.device` unit, or on Linux
|
||||
`/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
|
||||
|
||||
|
|
@ -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
|
||||
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
|
||||
write a farewell and close straight away. DOS, the ST, and the X68000 put back
|
||||
the speed and framing the port had before the open; the IIgs (whose serial
|
||||
chip's settings cannot be read back) and DVX leave it as JoeyLib set it.
|
||||
write a farewell and close straight away. DOS, the ST, the X68000, Linux,
|
||||
Haiku, and BeOS put back the speed and framing the port had before the open
|
||||
(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
|
||||
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
|
||||
whatever RTS was before the open.
|
||||
|
||||
Windows cannot make the promise. On Win32 and Win16 the close gives the port
|
||||
back to Windows' driver, and neither has a call that keeps DTR up past it. A
|
||||
Windows program that must not hang up the line keeps the port open.
|
||||
Win32 cannot make the promise: the close gives the port back to Windows'
|
||||
driver, which has no call that keeps DTR up past it, so a Win32 program that
|
||||
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
|
||||
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
|
||||
takes them and give up after a bounded wait when it stalls (a peer holding
|
||||
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
|
||||
all.
|
||||
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
|
||||
the background, DVX in its serial library's 2 KB ring, and Win16 in Windows'
|
||||
driver's queue, so those never wait at all.
|
||||
|
||||
[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
|
||||
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
|
||||
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
|
||||
|
|
@ -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
|
||||
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
|
||||
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"]
|
||||
|===
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
alignment.
|
||||
====
|
||||
|
|
@ -361,8 +361,8 @@ following hold:
|
|||
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,
|
||||
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
|
||||
trusted save and save-and-draw as well as the restore;
|
||||
* on the Amiga, ST, and X68000, `+backup->bytes+` is at least 2-byte aligned,
|
||||
for the trusted save and save-and-draw as well as the restore;
|
||||
* the recorded region is fully on the destination surface.
|
||||
|
||||
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.
|
||||
|
||||
*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
|
||||
IIgs the total is what `jlInit` was granted, which can be less than
|
||||
`codegenBytes` asked for, and it is split into blocks of at most 64 KB that
|
||||
compacting never merges: one sprite's code must fit in one block. On the
|
||||
ports that interpret every sprite the total is the arena they allocated
|
||||
anyway, and the bytes in use stay 0.
|
||||
though it may be fragmented across holes until `jlSpriteCompact` runs. The
|
||||
total is what `jlInit` was granted, which can be less than `codegenBytes`
|
||||
asked for on a machine short of memory. On the IIgs it is split into blocks
|
||||
of at most 64 KB that compacting never merges: one sprite's code must fit in
|
||||
one block. The ports that interpret every sprite have no arena: both are 0.
|
||||
|
|
|
|||
|
|
@ -40,8 +40,9 @@ jlSurfaceT *jlSurfaceCreate(void);
|
|||
----
|
||||
|
||||
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
|
||||
a 512 KB machine, beside the display's own pages, only a few fit.
|
||||
that comes from chip RAM, where the blitter can work on its planes; once chip
|
||||
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.
|
||||
|
||||
|
|
@ -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
|
||||
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.
|
||||
|
||||
[NOTE]
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ depending on the machine, and nothing warns you.
|
|||
*Align your backup buffers.* Give any `jlSpriteBackupT` backing array 2-byte
|
||||
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
|
||||
slower path, and on the Amiga and ST it crashes the trusted variants.
|
||||
slower path, and it crashes the trusted variants.
|
||||
|
||||
[source,c]
|
||||
----
|
||||
|
|
@ -257,8 +257,8 @@ The API is uniform; these are the places where what you get differs.
|
|||
|Stereo pan
|
||||
|all three
|
||||
|all three (centred on a free channel each side)
|
||||
|ignored (mono)
|
||||
|ignored (mono)
|
||||
|ignored (mono); all three in the STe build
|
||||
|all three on an SB16; ignored (mono) before it
|
||||
|ignored
|
||||
|all three
|
||||
|all three
|
||||
|
|
@ -313,7 +313,7 @@ The API is uniform; these are the places where what you get differs.
|
|||
|COM1..COM4
|
||||
|RS-232C (needs RSDRV.SYS)
|
||||
|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
|
||||
|===
|
||||
|
||||
|
|
@ -366,27 +366,29 @@ and 56 under MAME, whose X68000 display runs slightly fast (56.07 Hz).
|
|||
timer to the millisecond.
|
||||
|
||||
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
|
||||
ST takes a timer interrupt at every change of palette, every frame, and copies
|
||||
the next band's 16 colours there. The X68000 reloads its palette as the beam
|
||||
scans: an interrupt stores the next band's colours in the horizontal blanking
|
||||
before the band's first line. A screen with one palette costs nothing on
|
||||
either and arms no interrupt. On the X68000 each change of palette between one
|
||||
have one 16-colour palette. Both reload it as the beam scans, and store each
|
||||
band's 16 colours in the horizontal blanking before the band's first line, so
|
||||
the line shows them from its first pixel. The ST takes a timer interrupt a
|
||||
line above every change of palette, every frame, and waits in it for that
|
||||
line's display to end: about one line of CPU time (64 microseconds) a change.
|
||||
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
|
||||
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
|
||||
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
|
||||
Amiga the copper changes the palettes at no CPU cost, but an SCB change that
|
||||
moves where a band starts or ends rebuilds the copper list, which holds up
|
||||
that `jlStagePresent` for about three frames on an A500; a change of colours,
|
||||
or of which palette an unmoved band uses, costs nothing. On the ST a
|
||||
present's colours and pixels show from the same frame: it hands the
|
||||
interrupts their new colours with the page it flips to. On the X68000, a
|
||||
screen of two or more bands takes palette and SCB changes at the start of the
|
||||
next frame rather than mid-frame, so a fade can lag its pixels by one frame
|
||||
but never tears; a screen of one palette has its colours written at the
|
||||
present.
|
||||
chapter for how long a run can be before received bytes are lost). The ST
|
||||
does the same for palettes one line apart, and spends those lines in the
|
||||
interrupt. On the Amiga the copper changes the palettes at no CPU cost: a
|
||||
present that changes palettes or the SCB writes a list of the bands' copper
|
||||
instructions, a few hundred bytes. On the Amiga, the ST, DOS, and the X68000
|
||||
a present's colours and pixels show from the same frame: the Amiga's VBL
|
||||
switches the copper to the new list with the page it flips to, the ST and
|
||||
the X68000 hand their interrupts the new colours with the page they flip to,
|
||||
and DOS loads changed colours in the vertical blank that shows the new page.
|
||||
A present that changes colours right after another, before the vertical
|
||||
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
|
||||
|
||||
|
|
@ -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
|
||||
nearest one the display has, and Win16 shows them exactly only while the 236
|
||||
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
|
||||
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
|
||||
frame, and the X68000 in a raster interrupt. A screen with one palette costs
|
||||
nothing (<<_per_platform_behaviour_summary>>).
|
||||
frame (about a line of CPU time each), and the X68000 in a raster interrupt.
|
||||
A screen with one palette costs nothing (<<_per_platform_behaviour_summary>>).
|
||||
|
||||
[[portsDifferInput]]
|
||||
==== 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:
|
||||
`JL_KEY_LSHIFT` and `JL_KEY_RSHIFT` both read either. Option is
|
||||
`JL_KEY_LALT`.
|
||||
* *ST keyboard: the Alt layer on TOS before 4.00.* A national keyboard types
|
||||
some ASCII with Alt (a German one's @ [ ] { } \). TOS 4.00 and later, and
|
||||
EmuTOS, keep that layer in the keyboard tables JoeyLib reads, so Alt types
|
||||
it; older TOS keeps it in the keyboard code JoeyLib replaces, so there Alt
|
||||
with a key types nothing, as on the ports where Alt is a command key.
|
||||
* *ST keyboard: no Alt layer on two countries' older TOS.* A national keyboard
|
||||
types some ASCII with Alt (a German one's @ [ ] { } \). TOS 4.00 and later,
|
||||
and EmuTOS, keep that layer in the keyboard tables JoeyLib reads; older TOS
|
||||
keeps it in the keyboard code JoeyLib replaces, so JoeyLib carries its own
|
||||
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
|
||||
`JL_KEY_LSHIFT` and `JL_KEY_RSHIFT` both read either. OPT.1 and OPT.2 are
|
||||
`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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
|
@ -506,18 +507,47 @@ names the ports, says how they differ, and gives the reason.
|
|||
voices or `jlMusicPlay` sound.
|
||||
* *IIgs: mono.* A stock IIgs mixes its stereo channels into one speaker, so
|
||||
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:
|
||||
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
|
||||
rhythm-mode snare is the nearest, a tuned drum whose pitch cannot follow the
|
||||
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
|
||||
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.
|
||||
* *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
|
||||
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
|
||||
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
|
||||
|
|
@ -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
|
||||
refilled at `jlAudioFrameTick`: a program that stops calling it for longer
|
||||
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
|
||||
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]]
|
||||
==== 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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
|
@ -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
|
||||
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
|
||||
(<<_files>>).
|
||||
and the web tell upper and lower case apart. The save calls refuse a name
|
||||
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
|
||||
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.
|
||||
|
|
@ -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`
|
||||
block is a Memory Manager handle, so `jlFree` and `free` must never be given
|
||||
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]]
|
||||
==== Timing
|
||||
|
|
@ -646,9 +697,11 @@ Nothing in the API differs between them:
|
|||
|
||||
|Digital sound (`.MOD`, SFX, streams)
|
||||
|PWM through the YM2149's channel A volume, driven by a 12288 Hz timer
|
||||
interrupt; tone voice 0 is silent while it plays
|
||||
|The 8-bit DMA sound chip, mono at 12517 Hz, with no interrupt; all three tone
|
||||
voices keep playing
|
||||
interrupt, each sample the nearest of the YM's sixteen logarithmic levels
|
||||
(so about three bits near full scale); tone voice 0 is silent while it
|
||||
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
|
||||
|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:
|
||||
|
||||
* *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
|
||||
640x480, 2x at 800x600, 3x at 1024x768. 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
|
||||
at the largest whole scale, up to 3x, that fits the desktop, in 4:3 as a
|
||||
CRT shows it and as the SDL3 window does (each stage row a fifth taller:
|
||||
1x is 320x240), or with Game > Square Pixels, square pixels (16:10, as
|
||||
`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.
|
||||
* *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
|
||||
|
|
@ -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
|
||||
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
|
||||
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.
|
||||
* *Closing ends the program.* Alt+F4, the close box, and Game > Exit end the
|
||||
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
|
||||
files so a reinstall never overwrites them.
|
||||
* *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
|
||||
chip when there is one. A `jlAudioTickRegister` callback runs in program
|
||||
time, at the next `jlAudioFrameTick`, because DVX's sound library may not
|
||||
be called from an interrupt.
|
||||
other program's: `.MOD` music, SFX in stereo, and the tone voices and the
|
||||
noise as the hosted ports make them, square waves and the YM2149's noise
|
||||
mixed in software into a stream of DVX's mixer, so their levels and
|
||||
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.
|
||||
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
|
||||
|
|
@ -808,7 +869,11 @@ this:
|
|||
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
|
||||
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
|
||||
`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).
|
||||
* *Sound starts with the first click or key press*, as browsers require; the
|
||||
page says so.
|
||||
* *Saves persist.* `SAVES/` is kept in the browser's IndexedDB, read back
|
||||
when the program starts and written back as each save changes, so a save
|
||||
outlives the page however it ends. The store is the site's, not the page's:
|
||||
every JoeyLib program served from the same site shares one `SAVES/`, so give
|
||||
each game's saves names of their own. `jlDiskFree` there is the browser's
|
||||
own estimate of what the site may still store.
|
||||
* *Saves persist, page by page.* `SAVES/` is kept in the browser's
|
||||
IndexedDB, read back the first time the program asks for it and written
|
||||
back as each save changes, so a save outlives the page however it ends.
|
||||
Each page keeps its own, as each program keeps its own `SAVES/` folder on
|
||||
a machine: `games/hero.html` and `games/maze.html` on one site never see
|
||||
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
|
||||
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
|
||||
not signed) and in PPSSPP. The ELF it is packed from stays in
|
||||
`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
|
||||
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.
|
||||
|
|
@ -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
|
||||
screen every frame, so a game that redraws everything every frame wants
|
||||
`JOEYLIB_SCALE=1` there.
|
||||
* *Sound* is 16-bit mono at 22.05 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
|
||||
* *Sound* is 16-bit mono at 22.222 kHz through waveOut, what a Sound Blaster
|
||||
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
|
||||
attenuation steps coarser); stereo pan is ignored.
|
||||
* *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
|
||||
its icon once it has run; that is the one time a program writes to its own
|
||||
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
|
||||
know, so `make beos` links without combined relocations (`-z
|
||||
nocombreloc`). Build for R5 with `make beos`, not `make haiku32`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
* `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
|
||||
hosted ports interpret every sprite and never use it, though they still
|
||||
allocate it.
|
||||
Amiga, ST, and X68000, and 32 KB on DOS, DVX, and Win16. The other hosted
|
||||
ports interpret every sprite and allocate no arena at all.
|
||||
* 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
|
||||
those ports. A 32x32 sprite can need 8 to 16 KB there.
|
||||
* Measure, do not guess: after compiling a sprite, `jlSpriteCompiledSize()`
|
||||
reports its exact cost on the current platform.
|
||||
* On the IIgs a compiled sprite cannot cross a 64 KB bank boundary, so the
|
||||
arena is up to four Memory Manager blocks of at most 64 KB, each inside one
|
||||
bank: 256 KB at most. A bank that is refused is tried again at 60 KB, and
|
||||
`jlInit` takes whatever part of the request it was granted. Elsewhere the
|
||||
arena is one block, and `jlInit` fails if it cannot be had.
|
||||
* Every port takes as much of the request as the machine has, and `jlInit`
|
||||
fails only if it can have none of it; `jlSpriteCodegenBytesTotal()` says
|
||||
what it got. On the IIgs a compiled sprite cannot cross a 64 KB bank
|
||||
boundary, so the arena is up to four Memory Manager blocks of at most
|
||||
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
|
||||
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
|
||||
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
|
||||
the trusted variants.
|
||||
save and restore down a much slower path, and it crashes the trusted
|
||||
variants.
|
||||
|
||||
==== The Clean-Buffer Pattern
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@
|
|||
// port played and tools/gradeAudioConformance.py can measure it against the
|
||||
// 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
|
||||
// pans, a module that does not loop ending (and jlAudioIsPlayingMod saying
|
||||
// so), and tick callbacks at their rates.
|
||||
// pans, streamed ones (signed, and in the port's raw format), a module that
|
||||
// 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
|
||||
// from the first sync tone's start:
|
||||
|
|
@ -18,9 +19,17 @@
|
|||
// tone <hz> jlAudioTone
|
||||
// voice <v> <hz> <atten> jlAudioVoice
|
||||
// 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
|
||||
// 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
|
||||
// that came, and the time they had
|
||||
// silence everything stopped
|
||||
|
|
@ -31,6 +40,7 @@
|
|||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <joey/joey.h>
|
||||
#include <joey/file.h>
|
||||
|
|
@ -57,10 +67,17 @@
|
|||
#define VOICE_HZ 660u
|
||||
#define TICK_MS 2000u
|
||||
#define MOD_WAIT_MS 12000u
|
||||
#define MOD_STOP_MS 1500u // a looping module plays this long, then stops
|
||||
#define SFX_SLOT 0u
|
||||
#define SFX_SAMPLES 4000u
|
||||
#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 RAW_BIAS 0x80 // JL_SFX_RAW_UNSIGNED8_NOZERO's zero
|
||||
#define MS_PER_SECOND 1000u
|
||||
#define EVENTS_MAX 128u
|
||||
|
||||
|
|
@ -70,7 +87,12 @@ typedef enum {
|
|||
STEP_VOICE,
|
||||
STEP_NOISE,
|
||||
STEP_SFX,
|
||||
STEP_SFX_FAST,
|
||||
STEP_STREAM,
|
||||
STEP_STREAM_RAW,
|
||||
STEP_STREAM_FAST,
|
||||
STEP_MOD,
|
||||
STEP_MOD_STOP,
|
||||
STEP_TICK,
|
||||
STEP_SILENCE
|
||||
} StepKindE;
|
||||
|
|
@ -83,6 +105,19 @@ typedef struct {
|
|||
uint16_t ms; // how long it sounds (sfx: from its length)
|
||||
} 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
|
||||
// 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.
|
||||
|
|
@ -95,7 +130,7 @@ typedef struct {
|
|||
uint32_t count; // mod: its length, then playing; tick: calls
|
||||
uint32_t span; // tick: the time the calls had
|
||||
bool ok; // tick: registered; mod end: still playing
|
||||
bool modEnd;
|
||||
bool modEnd; // mod: its end; mod stop: the stop
|
||||
} EventT;
|
||||
|
||||
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 uint32_t since(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 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, 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_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_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.
|
||||
{ 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.
|
||||
{ STEP_TICK, 0u, 10u, 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 uint16_t gEventCount = 0u;
|
||||
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 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).
|
||||
static void eventsLog(void) {
|
||||
const EventT *event;
|
||||
const SineT *sine;
|
||||
uint16_t 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);
|
||||
break;
|
||||
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;
|
||||
case STEP_MOD:
|
||||
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);
|
||||
}
|
||||
break;
|
||||
case STEP_MOD_STOP:
|
||||
jlLogF("AUDPAR %lu mod %s", (unsigned long)event->ms, event->modEnd ? "stop" : "loop");
|
||||
break;
|
||||
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);
|
||||
break;
|
||||
|
|
@ -268,8 +332,9 @@ static bool pump(void) {
|
|||
|
||||
// Each step's time is taken as it starts.
|
||||
static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength) {
|
||||
uint32_t start;
|
||||
EventT *event;
|
||||
const SineT *sine;
|
||||
uint32_t start;
|
||||
EventT *event;
|
||||
|
||||
start = since();
|
||||
event = eventAdd(start, step);
|
||||
|
|
@ -288,9 +353,24 @@ static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength) {
|
|||
(void)waitMs(step->ms);
|
||||
break;
|
||||
case STEP_SFX:
|
||||
case STEP_SFX_FAST:
|
||||
sine = stepSine(step->kind);
|
||||
jlAudioSetPan(SFX_SLOT, (jlAudioPanE)step->channel);
|
||||
jlAudioPlaySfx(SFX_SLOT, (const uint8_t *)gSine, SFX_SAMPLES, step->value);
|
||||
(void)waitMs((uint16_t)((uint32_t)SFX_SAMPLES * MS_PER_SECOND / step->value));
|
||||
jlAudioPlaySfx(SFX_SLOT, (const uint8_t *)sine->samples, sine->count, 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;
|
||||
case STEP_MOD:
|
||||
if (mod != NULL) {
|
||||
|
|
@ -311,6 +391,17 @@ static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength) {
|
|||
}
|
||||
jlAudioStopMod();
|
||||
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:
|
||||
gTickCalls = 0u;
|
||||
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) {
|
||||
gTickCalls++;
|
||||
}
|
||||
|
|
@ -368,6 +485,7 @@ static bool waitMs(uint16_t ms) {
|
|||
int main(void) {
|
||||
jlConfigT config;
|
||||
uint8_t *mod;
|
||||
int8_t *fast;
|
||||
uint32_t modLength;
|
||||
uint16_t i;
|
||||
|
||||
|
|
@ -377,9 +495,22 @@ int main(void) {
|
|||
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||
return 1;
|
||||
}
|
||||
gRawFormat = jlAudioSfxRawFormat();
|
||||
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);
|
||||
jlSurfaceClear(jlStageGet(), 0u);
|
||||
jlStagePresent();
|
||||
|
|
@ -396,6 +527,7 @@ int main(void) {
|
|||
jlLogF("AUDPAR %lu done", (unsigned long)since());
|
||||
jlLogFlush();
|
||||
free(mod);
|
||||
free(fast);
|
||||
jlShutdown();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
//
|
||||
// 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
|
||||
// DEFAULT port at 9600 8N1, sends a banner (TX heartbeat), then echoes every
|
||||
// received byte forever. A host serial peer verifies banner (TX) + echo (RX).
|
||||
// DEFAULT port (or SERTEST_DEVICE) at 9600 8N1, sends a banner (TX
|
||||
// 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
|
||||
// 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_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 sendMark(const char *text);
|
||||
|
||||
|
|
@ -28,7 +38,7 @@ static void dtrCheck(const jlSerialConfigT *cfg) {
|
|||
jlSerialSetDtr(true);
|
||||
sendMark("JLDTR-ON\r\n");
|
||||
jlSerialClose();
|
||||
if (!jlSerialOpen(JL_SERIAL_DEFAULT, cfg)) {
|
||||
if (!jlSerialOpen(SERTEST_DEVICE, cfg)) {
|
||||
return;
|
||||
}
|
||||
sendMark("JLDTR-REOPENED\r\n");
|
||||
|
|
@ -74,8 +84,8 @@ int main(void) {
|
|||
cfg.stopBits = 1u;
|
||||
cfg.parity = JL_SERIAL_PARITY_NONE;
|
||||
cfg.flow = JL_SERIAL_FLOW_NONE;
|
||||
cfg.unit = 0u;
|
||||
if (!jlSerialOpen(JL_SERIAL_DEFAULT, &cfg)) {
|
||||
cfg.unit = SERTEST_UNIT;
|
||||
if (!jlSerialOpen(SERTEST_DEVICE, &cfg)) {
|
||||
return 1;
|
||||
}
|
||||
jlSerialWrite((const uint8_t *)banner, (uint16_t)(sizeof(banner) - 1u));
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@
|
|||
// 65816 replayer linked into the binary.
|
||||
// Amiga -- raw .MOD, played by Frank Wille's PTPlayer.
|
||||
// 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
|
||||
// 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
|
||||
// MSM6258 ADPCM channel.
|
||||
// DVX -- raw .MOD through DVX's music library.
|
||||
|
|
@ -25,6 +27,18 @@
|
|||
// the engine choice. jlInit starts audio (jlConfigT.noAudio leaves it
|
||||
// off) and jlShutdown stops it. Sound that does not start is non-fatal:
|
||||
// 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
|
||||
#define JOEYLIB_AUDIO_H
|
||||
|
|
@ -59,8 +73,9 @@
|
|||
// ahead of time.
|
||||
void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
|
||||
|
||||
// Stop the current module (if any). The playhead is reset so the next
|
||||
// jlAudioPlayMod starts from the top.
|
||||
// Stop the current module (if any), silent at once on every port (DOS and
|
||||
// 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);
|
||||
|
||||
// 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).
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
|
|
@ -111,10 +128,11 @@ void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint
|
|||
// Raw-format streaming: jlAudioPlaySfxStream, except `fill` delivers
|
||||
// bytes already in the port's raw stream format (jlAudioSfxRawFormat),
|
||||
// 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
|
||||
// port's raw format is signed PCM, so there Raw is the plain call (DVX
|
||||
// still flips each byte for its sound library). A producer that cannot
|
||||
// make the port's format uses jlAudioPlaySfxStream.
|
||||
// that conversion costs ~7 us a byte, ~50 ms per 7 KB chunk. DVX takes the
|
||||
// IIgs's format too (its sound library's 8-bit PCM is unsigned) and skips
|
||||
// its flip. Every other port's raw format is signed PCM, so there Raw is
|
||||
// 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_UNSIGNED8_NOZERO 1u // uint8_t PCM biased 0x80; 0x00 is forbidden (DOC stop byte)
|
||||
uint8_t jlAudioSfxRawFormat(void);
|
||||
|
|
@ -145,7 +163,10 @@ typedef enum {
|
|||
// have one, and otherwise on the one channel PTPlayer picks.
|
||||
// X68000 ignored. Everything is mixed into ONE MSM6258 ADPCM stream, so
|
||||
// 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.
|
||||
// hosted all three (the mix is stereo), except Win16, which is mono.
|
||||
// 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
|
||||
// software interrupt; the ST and X68000 ride the system's Timer C
|
||||
// interrupt (200 and 100 Hz), so calls due between its ticks come
|
||||
// together. The IIgs and DVX call fn in program time from
|
||||
// jlAudioFrameTick, and the hosted ports from the mixer, between the
|
||||
// samples where each tick falls.
|
||||
// together. The IIgs calls fn in program time from jlAudioFrameTick,
|
||||
// and DVX and the hosted ports from the mixer, between the samples where
|
||||
// each tick falls.
|
||||
typedef void (*jlAudioTickFnT)(void);
|
||||
bool jlAudioTickRegister(jlAudioTickFnT fn, uint16_t hz);
|
||||
|
||||
|
|
@ -213,7 +234,8 @@ void jlAudioCriticalExit(void);
|
|||
// IIgs -- Ensoniq DOC oscillators playing a resident square on SFX
|
||||
// slots 0/1/2.
|
||||
// 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.
|
||||
//
|
||||
// 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).
|
||||
// IIgs -- a resident LFSR noise sample on SFX slot 3.
|
||||
// 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.
|
||||
//
|
||||
// The same contract as jlAudioVoice: it sounds until the next call.
|
||||
|
|
|
|||
|
|
@ -12,16 +12,17 @@
|
|||
|
||||
typedef struct {
|
||||
// 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
|
||||
// everywhere else (the 68k/x86 emitters are ~4-8x less code-dense
|
||||
// than the 65816's, so a 32x32 sprite can need 8-16 KB there). The
|
||||
// ports that interpret every sprite still allocate it. Size against
|
||||
// jlSpriteCompiledSize() when tuning. Elsewhere the cache is one
|
||||
// block, and jlInit fails if it cannot be had. On the IIgs it is up
|
||||
// to four Memory Manager blocks of at most 64 KB, each inside one
|
||||
// bank (a compiled sprite cannot cross a bank), so at most 256 KB;
|
||||
// a bank that is refused is retried at 60 KB, and jlInit takes
|
||||
// whatever part of the request it was granted.
|
||||
// default: 8 KB on the IIgs, 64 KB on the Amiga, ST, and X68000, and
|
||||
// 32 KB on DOS, DVX, and Win16 (the 68k/x86 emitters are ~4-8x less
|
||||
// code-dense than the 65816's, so a 32x32 sprite can need 8-16 KB
|
||||
// there). The ports that interpret every sprite have no cache and
|
||||
// allocate none. Size against jlSpriteCompiledSize() when tuning.
|
||||
// Every port takes as much of the request as the machine has, and
|
||||
// jlInit fails only if it can have none (jlSpriteCodegenBytesTotal
|
||||
// says what it got): on the IIgs up to four Memory Manager blocks of
|
||||
// at most 64 KB, each inside one bank (a compiled sprite cannot cross
|
||||
// a bank), so at most 256 KB; elsewhere one block, halved until the
|
||||
// heap has room, down to 4 KB.
|
||||
uint32_t codegenBytes;
|
||||
// True: leave audio off. Every audio call is then silent, as on a
|
||||
// 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
|
||||
// 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()
|
||||
// returns a human-readable description; where the port's own start-up
|
||||
// fails without saying why, that is just "jlpInit failed".
|
||||
// returns a human-readable description of why, on every port.
|
||||
bool jlInit(const jlConfigT *config);
|
||||
|
||||
// Shut down the library, audio first, releasing the library's own
|
||||
// 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);
|
||||
|
||||
// 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.
|
||||
uint16_t jlFrameHz(void);
|
||||
|
||||
// Monotonic wall-clock millisecond counter since jlInit. It may
|
||||
// advance a frame at a time. Call it at least once every 15 minutes, or
|
||||
// Monotonic wall-clock millisecond counter since jlInit, to the
|
||||
// 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
|
||||
// separated by a long render report the true elapsed time, not the
|
||||
// number of VBLs that fired.
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@
|
|||
// DATA/ -- game content (levels, sprites, audio). jlDataOpen.
|
||||
// SAVES/ -- writable save / preference files. jlSave*.
|
||||
// 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
|
||||
// ports make the program's folder the current directory in jlInit; DOS, the
|
||||
// ST, the Amiga, and the X68000 use the current directory as they find it;
|
||||
// the IIgs finds DATA/ under GS/OS prefix 0 and SAVES/ in the application's
|
||||
// folder (prefix 1); DVX roots both at run time (docs/ApiFile.adoc).
|
||||
// forced here so callers never hard-code "DATA/" or "SAVES/". Both are found
|
||||
// in the program's own folder wherever it was started from, before jlInit as
|
||||
// after: DOS from its start path, the Amiga as PROGDIR:, the X68000 from its
|
||||
// process block, the IIgs as GS/OS prefix 1, the hosted ports from their
|
||||
// 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
|
||||
#define JOEYLIB_FILE_H
|
||||
|
|
@ -54,9 +56,10 @@ FILE *jlDataOpen(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
|
||||
// contents. Returns true only if all `len` bytes were written. Not atomic: a
|
||||
// write that fails part way leaves the save short. buf NULL with len > 0
|
||||
// returns false before the save is touched.
|
||||
// contents. Returns true only if all `len` bytes were written. The bytes go to
|
||||
// a temporary file that takes the save's place only once it is whole, so a
|
||||
// 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);
|
||||
|
||||
// Read up to `max` bytes from save file `name` into `buf`. Returns the number
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@
|
|||
#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_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,
|
||||
// the VBL heartbeat tick read -- no function override to register here.)
|
||||
#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_DELETE
|
||||
#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)
|
||||
#elif defined(JOEYLIB_PLATFORM_AMIGA)
|
||||
#define JL_HAS_SURFACE_CLEAR // planar clear, function
|
||||
|
|
@ -314,10 +318,17 @@
|
|||
#define JL_HAS_SERIAL_SET_DTR
|
||||
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
|
||||
#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_DELETE
|
||||
#define JL_HAS_SAVE_RENAME
|
||||
#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)
|
||||
#define JL_HAS_CODE_WRITTEN
|
||||
// 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_PORT_DATA // st StPlanarT + interleaved 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_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
|
||||
#define JL_HAS_AUDIO_INIT
|
||||
#define JL_HAS_AUDIO_SHUTDOWN
|
||||
|
|
@ -362,6 +373,9 @@
|
|||
#define JL_HAS_AUDIO_IS_PLAYING_MOD
|
||||
#define JL_HAS_AUDIO_PLAY_SFX
|
||||
#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_VOICE
|
||||
#define JL_HAS_AUDIO_NOISE
|
||||
|
|
@ -370,6 +384,7 @@
|
|||
#define JL_HAS_AUDIO_CRITICAL_EXIT
|
||||
#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_SFX_MIX_RUN // the SFX overlay's run, src/m68k/sfxMix.s
|
||||
// lifecycle / present / input (every real port implements)
|
||||
#define JL_HAS_INIT
|
||||
#define JL_HAS_SHUTDOWN
|
||||
|
|
@ -417,6 +432,7 @@
|
|||
#define JL_HAS_AUDIO_IS_PLAYING_MOD
|
||||
#define JL_HAS_AUDIO_PLAY_SFX
|
||||
#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_VOICE
|
||||
#define JL_HAS_AUDIO_NOISE
|
||||
|
|
@ -444,10 +460,14 @@
|
|||
#define JL_HAS_SERIAL_SET_DTR
|
||||
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
|
||||
#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_DELETE
|
||||
#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)
|
||||
// Platform services in src/x68000/hal.c, the serial add-on in
|
||||
// src/x68000/serial.c, and planar graphics adapted from the Amiga
|
||||
|
|
@ -497,6 +517,9 @@
|
|||
#define JL_HAS_DISK_FREE
|
||||
#define JL_HAS_SAVE_DELETE
|
||||
#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
|
||||
// port's own SCC interrupt (src/x68000/x68kSccRxIsr.s).
|
||||
#define JL_HAS_SERIAL_OPEN
|
||||
|
|
@ -521,6 +544,7 @@
|
|||
#define JL_HAS_AUDIO_CRITICAL_EXIT
|
||||
#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_SFX_MIX_RUN // the SFX overlay's run, src/m68k/sfxMix.s
|
||||
#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
|
||||
// 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_PLAY_SFX
|
||||
#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_STOP_SFX
|
||||
#define JL_HAS_AUDIO_VOICE
|
||||
|
|
@ -603,6 +628,9 @@
|
|||
#define JL_HAS_SAVE_DIR_ENSURE
|
||||
#define JL_HAS_SAVE_DELETE
|
||||
#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
|
||||
// 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
|
||||
|
|
@ -633,6 +661,19 @@
|
|||
#define JL_HAS_SERIAL_FLUSH
|
||||
#define JL_HAS_SERIAL_SET_DTR
|
||||
#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
|
||||
#elif defined(JOEYLIB_PLATFORM_BLANK)
|
||||
// Copy-to-start template. It overrides ONLY the platform SERVICES (TODO
|
||||
|
|
|
|||
|
|
@ -22,11 +22,14 @@
|
|||
// opened in supervisor mode, as jlInit leaves every program).
|
||||
// DVX -- a COM port through DVX's serial library.
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
// Super Serial Card slot (1..7); DOS or DVX COM number
|
||||
// (1..4); Win16 COM (1..9); Win32 COM (1..255); Amiga
|
||||
// serial.device unit. The ST and X68000 have one port
|
||||
// and open it for every device.
|
||||
// serial.device unit; Linux /dev/ttyS<unit-1>; Haiku
|
||||
// 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,
|
||||
// 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 {
|
||||
JL_SERIAL_DEFAULT = 0,
|
||||
JL_SERIAL_MODEM,
|
||||
|
|
@ -93,7 +100,7 @@ typedef struct {
|
|||
uint8_t stopBits; // 1 or 2
|
||||
jlSerialParityE parity;
|
||||
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;
|
||||
|
||||
// Open + configure the port. Only one port is open at a time; a second
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// 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
|
||||
// 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) \
|
||||
((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
|
||||
// save covers -- the sprite's pixel height, or on the IIgs the rows
|
||||
// holding an opaque pixel),
|
||||
// * on the Amiga and ST, backup->bytes is at least 2-byte aligned (the
|
||||
// trusted save and save-and-draw need that too),
|
||||
// * on the Amiga, ST, and X68000, backup->bytes is at least 2-byte
|
||||
// aligned (the trusted save and save-and-draw need that too),
|
||||
// * the recorded region is fully on the destination surface.
|
||||
// It is the expert inner-loop entry for a game that pre-compiles its
|
||||
// sprites (jlSpriteCompile) and already clamps positions on-screen --
|
||||
|
|
@ -259,10 +259,11 @@ void jlSpriteCompact(void);
|
|||
// Arena introspection. Used to gauge whether jlSpriteCompact is
|
||||
// worth running, or whether the codegenBytes budget needs to grow.
|
||||
// Free space is (Total - Used), but it may be fragmented across
|
||||
// holes until jlSpriteCompact runs. On the IIgs Total is what jlInit was
|
||||
// granted, in blocks of at most 64 KB that compacting never merges (one
|
||||
// sprite's code must fit in one). The interpreting ports still allocate
|
||||
// the arena: Total reports it and Used stays 0.
|
||||
// holes until jlSpriteCompact runs. Total is what jlInit was granted,
|
||||
// which a machine short of memory can make less than codegenBytes; on the
|
||||
// IIgs in blocks of at most 64 KB that compacting never merges (one
|
||||
// sprite's code must fit in one). The interpreting ports have no arena:
|
||||
// both are 0.
|
||||
uint32_t jlSpriteCodegenBytesUsed(void);
|
||||
uint32_t jlSpriteCodegenBytesTotal(void);
|
||||
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ portGroups() {
|
|||
|
||||
# wantedGroups -- the port groups the user chose: those holding an installed
|
||||
# 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.
|
||||
wantedGroups() {
|
||||
local id
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@
|
|||
# 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,
|
||||
# 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)"
|
||||
compGroups="toolchain:dos toolchain:atarist toolchain:x68000 toolchain:sdl3 toolchain:psp toolchain:web toolchain:win16 toolchain:win32 toolchain:haiku toolchain:haiku32 toolchain:beos"
|
||||
compDescription="audio: libxmp-lite 4.7.0 source (optional; no build links it; a reference for the module player)"
|
||||
compGroups="optional"
|
||||
compRequires="host"
|
||||
compHostDeps="tar"
|
||||
compUrl="https://github.com/libxmp/libxmp/releases/download/libxmp-4.7.0/libxmp-lite-4.7.0.tar.gz"
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -6,11 +6,16 @@ drivers. The gates depend on every one of them.
|
|||
|
||||
| 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) |
|
||||
| 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 |
|
||||
|
||||
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)
|
||||
|
||||
Real Z8530 silicon holds 3 received bytes (an ESCC, 8), so a real client
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
# BE_CXX_RUNTIME (its C++ runtime library), then includes this file.
|
||||
|
||||
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 :=
|
||||
# The Interface and Application Kits (libbe), the Media Kit's BSoundPlayer
|
||||
# (libmedia), the Device Kit's BJoystick (libdevice); the backend's C++ is
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@
|
|||
# PALPROBE Amiga: the palette gate's program: how fast palette and SCB
|
||||
# changes reach the screen through the copper, and a held screen
|
||||
# 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
|
||||
# drawn by icon.library, and the program started from it the way
|
||||
# Workbench starts a tool.
|
||||
|
|
@ -40,7 +44,7 @@
|
|||
# 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
|
||||
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_KIND := JoeyLib example
|
||||
|
|
@ -129,6 +133,13 @@ DIRTYMRK_BINDIR := $(PROGRAM_BUILD)/test
|
|||
PALPROBE_SRCS := $(EXAMPLES)/palprobe/palprobe.c
|
||||
PALPROBE_PORTS := amiga
|
||||
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_PORTS := amiga
|
||||
WBLAUNCH_BINDIR := $(PROGRAM_BUILD)/test
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@ endif
|
|||
# system it runs on, built with the backend's flags; the backend includes
|
||||
# its headers.
|
||||
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_CXX_SRCS := $(wildcard $(BACKEND_DIR)/*.cpp)
|
||||
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)) \
|
||||
$(HOSTED_SPRITE_OBJS) \
|
||||
$(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)/%.cpp,$(BUILD)/obj/backend/%.o,$(BACKEND_CXX_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 $@)
|
||||
$(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.
|
||||
$(BUILD)/obj/backend/%.o: $(BACKEND_DIR)/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ SDL3_PKG_CONFIG := PKG_CONFIG_PATH=$(SDL3_DIR)/lib/pkgconfig pkg-config
|
|||
HOSTED_CC := $(HOST_CC)
|
||||
HOSTED_AR := ar
|
||||
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_LINK := $(shell $(SDL3_PKG_CONFIG) --static --libs sdl3)
|
||||
PROGRAM_EXT :=
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Minimal math.h shim for the m68k-atari-mint toolchain (mintlib
|
||||
// doesn't ship <math.h> or libm). Declares the few functions libxmp-lite
|
||||
// once reached for; the implementations live in 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.
|
||||
// doesn't ship <math.h> or libm), so a program that includes <math.h>
|
||||
// builds on the ST: ceil, floor, log, pow and trunc (and their float
|
||||
// forms), nothing else (docs/Portability.adoc). The implementations live in
|
||||
// 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
|
||||
#define JOEY_ATARIST_MATH_SHIM_H
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// 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
|
||||
// note-frequency math.
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@
|
|||
sets the window as the native SDL3 port's environment does:
|
||||
NAME.html?scale=1&aspect=square&fullscreen=1 become JOEYLIB_SCALE,
|
||||
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
|
||||
in-memory file system's /joeylog.txt) is mirrored into the hidden #joeylog
|
||||
element once a second, for a look without a debugger.
|
||||
canvas pixel per stage pixel that way). The program's joeylog.txt (in its
|
||||
write folder in the in-memory file system, Module.joeyWriteFolder, once the
|
||||
program has one) is mirrored into the hidden #joeylog element once a
|
||||
second, for a look without a debugger.
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
|
@ -56,11 +57,11 @@
|
|||
});
|
||||
setInterval(function () {
|
||||
var log;
|
||||
if (!Module.FS) {
|
||||
if (!Module.FS || !Module.joeyWriteFolder) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
log = Module.FS.readFile("/joeylog.txt", { encoding: "utf8" });
|
||||
log = Module.FS.readFile(Module.joeyWriteFolder + "joeylog.txt", { encoding: "utf8" });
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ LIB_OBJS := \
|
|||
$(BUILD)/obj/68k/planarPlot.o \
|
||||
$(BUILD)/obj/68k/stageBands.o \
|
||||
$(BUILD)/obj/68k/modMix.o \
|
||||
$(BUILD)/obj/68k/sfxMix.o \
|
||||
$(BUILD)/obj/68k/planarSpriteWalk.o \
|
||||
$(BUILD)/obj/68k/spriteEmitPlanar68k.o \
|
||||
$(BUILD)/obj/68k/planarSprite.o \
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@
|
|||
# 320x200x8, VGA mode 13h here), and fails unless DVX.LOG says the mode was
|
||||
# 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
|
||||
# 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
|
||||
|
|
@ -30,7 +33,7 @@
|
|||
#
|
||||
# scripts/bench-dvx.sh # default 600 s timeout
|
||||
# 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
|
||||
|
||||
set -euo pipefail
|
||||
|
|
@ -42,6 +45,7 @@ joeyNeed DOSBOX dosbox
|
|||
joeyNeed DVX_ROOT dvx
|
||||
|
||||
fullScreen=0
|
||||
square=0
|
||||
program=uber
|
||||
appDir=$(dvxAppDir uber)
|
||||
out=$DVX_BIN/$DVX_LOG_NAME
|
||||
|
|
@ -50,13 +54,16 @@ for arg in "$@"; do
|
|||
--full-screen)
|
||||
fullScreen=1
|
||||
;;
|
||||
--square)
|
||||
square=1
|
||||
;;
|
||||
--probe)
|
||||
program=uberprob
|
||||
appDir=$DVX_PROBE_DIR/apps/$DVX_VENDOR/$program
|
||||
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
|
||||
;;
|
||||
esac
|
||||
|
|
@ -84,8 +91,11 @@ cleanup() {
|
|||
trap cleanup EXIT
|
||||
|
||||
dvxStage "$work/c" "$appDir"
|
||||
if [ "$fullScreen" = 1 ]; then
|
||||
dvxFullScreen "$work/c" "$program"
|
||||
switches=()
|
||||
[ "$fullScreen" = 1 ] && switches+=("$DVX_SETTINGS_FULL_SCREEN")
|
||||
[ "$square" = 1 ] && switches+=("$DVX_SETTINGS_SQUARE")
|
||||
if [ ${#switches[@]} -gt 0 ]; then
|
||||
dvxSettings "$work/c" "$program" "${switches[@]}"
|
||||
fi
|
||||
dvxDosboxConf "$work/dosbox.conf" "$work/c"
|
||||
mkdir -p "$work/xdg"
|
||||
|
|
|
|||
|
|
@ -97,12 +97,19 @@ local EXIT_SETTLE = 900 -- keypress -> fclose -> back at the Human68k prompt
|
|||
-- 16-bit word per pixel, 512 words per row.
|
||||
-- The 320x200 stage is centred at origin (96, 156) either way.
|
||||
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)
|
||||
if presentPlane == "gfx" then
|
||||
return mem:read_u8(0xC00000 + ((156 + py) * 512 + 96 + px) * 2 + 1)
|
||||
end
|
||||
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 v = 0
|
||||
for plane = 0, 3 do
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ work=$(mktemp -d -t joeylib-save.XXXXXX)
|
|||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
"$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 \
|
||||
"$repo/src/core/save.c" "$repo/tests/host/saveHost.c" \
|
||||
-o "$work/saveHost"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ cd "$repo"
|
|||
-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_FLUSH -DJL_HAS_SERIAL_SET_RTS -DJL_HAS_SERIAL_SET_DTR \
|
||||
-DJL_HAS_MILLIS_ELAPSED \
|
||||
-Iinclude -Iinclude/joey -Isrc/core -Wall -Wextra \
|
||||
src/core/serial.c tests/host/serialHost.c \
|
||||
-o "$work/serialHost"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
# Ctrl reaches the program. The mapper replaces every DOSBox binding: only
|
||||
# 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
|
||||
# dosboxJoyTap NAME press and let go of a key of the table
|
||||
# (escape)
|
||||
|
|
@ -29,6 +32,22 @@ DOSBOX_JOY_WINDOW=""
|
|||
DOSBOX_JOY_SHOTS=""
|
||||
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
|
||||
# the DOSBox mapper event it drives.
|
||||
DOSBOX_JOY_KEYS=(
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@ DVX_PROBE_DIR="${DVX_PROBE_DIR:-$JOEY_REPO/build/dvx/probe}"
|
|||
DVX_VENDOR="${DVX_VENDOR:-joeylib}"
|
||||
DVX_LOG_NAME=joeylog.txt
|
||||
# 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_SECTION=display
|
||||
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
|
||||
# the stage's size at 8 bits a pixel).
|
||||
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
|
||||
# DVX staged in DIR: the port's own setting (JOEYLIB.INI, [display]
|
||||
# fullScreen, in the program's settings folder), as Game > Full Screen leaves
|
||||
# it. DOS names are case-blind, so an existing folder of any case is used.
|
||||
dvxFullScreen() {
|
||||
# dvxSettings DIR PROGRAM SWITCH... -- have PROGRAM start with the port's
|
||||
# own display switches on (DVX_SETTINGS_FULL_SCREEN, DVX_SETTINGS_SQUARE) in
|
||||
# the DVX staged in DIR: its JOEYLIB.INI, [display], in the program's
|
||||
# settings folder, as the Game menu leaves it. DOS names are case-blind, so
|
||||
# an existing folder of any case is used.
|
||||
dvxSettings() {
|
||||
local conf
|
||||
local key
|
||||
conf="$(find "$1/DVX" -ipath "*/config/apps/$DVX_VENDOR/$2" -type d -print -quit 2>/dev/null)"
|
||||
if [ -z "$conf" ]; then
|
||||
conf="$1/DVX/config/apps/$DVX_VENDOR/$2"
|
||||
mkdir -p "$conf"
|
||||
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"
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,13 @@ works too).
|
|||
WALL is the wall-clock budget in seconds. Prints one LOOP PASS / LOOP 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
|
||||
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
|
||||
|
|
@ -132,6 +139,42 @@ def openPty(pathFile):
|
|||
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):
|
||||
# After the loopback: the stream so far is got; lines has decoded it.
|
||||
base = len(got)
|
||||
|
|
@ -145,15 +188,8 @@ def dtrCheck(send, receive, wall, start, lines, got):
|
|||
if data is None:
|
||||
break
|
||||
got += data
|
||||
# Acknowledge each marker line as it completes; the last starts the
|
||||
# watch.
|
||||
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)
|
||||
# The last marker starts the watch.
|
||||
ackMarks(send, got, base, marks)
|
||||
if hangupAt is None and len(marks) == len(MARKS):
|
||||
hangupAt = time.time()
|
||||
if len(marks) < len(MARKS):
|
||||
|
|
@ -206,7 +242,12 @@ def loop(send, receive, wall, start, banner):
|
|||
def main(argv):
|
||||
withDtrCheck = "--dtr-check" in argv
|
||||
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__)
|
||||
return 2
|
||||
mode = argv[1]
|
||||
|
|
@ -226,7 +267,11 @@ def main(argv):
|
|||
except OSError:
|
||||
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])
|
||||
conn = acceptOne(port, wall) if mode == "tcp-server" else connectOne(port, wall, start)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
// " ") for MS milliseconds
|
||||
// reload load the page again (what a program
|
||||
// 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
|
||||
// pad:press:CONTROLS, pad:release:CONTROLS
|
||||
// hold or let go of the --gamepads
|
||||
|
|
@ -34,13 +36,20 @@
|
|||
// button and button 1 the east
|
||||
// --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
|
||||
// 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:
|
||||
// SDL3 plays through a ScriptProcessorNode, so a script
|
||||
// put in the page before the program's own wraps the
|
||||
// node's onaudioprocess and keeps a copy of every buffer
|
||||
// it fills; the WAV, and the --step script, start with
|
||||
// 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
|
||||
// when it ran to the end (or reached --until), 1 when --until never came, the
|
||||
// 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_SPAN = 20000;
|
||||
|
||||
// The program's log, read from the in-memory file system ("" until there is one).
|
||||
const READ_LOG = "(function () { try { return Module.FS.readFile('/joeylog.txt', { encoding: 'utf8' }); } catch (e) { return ''; } })()";
|
||||
// The program's log, read from its write folder in the in-memory file system
|
||||
// ("" 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
|
||||
// fills, kept as 16-bit stereo samples.
|
||||
const AUDIO_RECORDER = `(function () {
|
||||
|
|
@ -167,6 +177,7 @@ function parseArgs(argv) {
|
|||
case "--log": opts.log = value; i++; break;
|
||||
case "--audio": opts.audio = value; i++; break;
|
||||
case "--file": opts.files.push(value); i++; break;
|
||||
case "--sound": opts.sound = true; break;
|
||||
default:
|
||||
throw new Error("unknown option " + name);
|
||||
}
|
||||
|
|
@ -228,14 +239,16 @@ function connect(wsUrl, onEvent) {
|
|||
}
|
||||
|
||||
|
||||
// One --step.
|
||||
async function runStep(cdp, step) {
|
||||
// One --step; url is --url's, beside which open: finds its page.
|
||||
async function runStep(cdp, step, url) {
|
||||
const at = step.indexOf(":");
|
||||
const verb = step.slice(0, at);
|
||||
const rest = step.slice(at + 1);
|
||||
|
||||
if (step === "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") {
|
||||
await sleep(Number(rest) * MS_PER_SECOND);
|
||||
} else if (verb === "type") {
|
||||
|
|
@ -324,7 +337,8 @@ async function main() {
|
|||
const profile = mkdtempSync(join(tmpdir(), "joeylib-web-"));
|
||||
const debugPort = DEBUG_PORT_MIN + Math.floor(Math.random() * DEBUG_PORT_SPAN);
|
||||
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",
|
||||
"--use-angle=swiftshader", "--enable-unsafe-swiftshader",
|
||||
"--window-size=1024,800",
|
||||
|
|
@ -387,7 +401,7 @@ async function main() {
|
|||
await sleep(POLL_MS);
|
||||
}
|
||||
for (const step of opts.steps) {
|
||||
await runStep(cdp, step);
|
||||
await runStep(cdp, step, opts.url);
|
||||
}
|
||||
}
|
||||
if (opts.audio !== undefined) {
|
||||
|
|
@ -404,7 +418,7 @@ async function main() {
|
|||
for (const file of opts.files) {
|
||||
const at = file.lastIndexOf(":");
|
||||
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) {
|
||||
throw new Error("the page has no file " + path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,19 @@
|
|||
#
|
||||
# webNeed exit 2 unless Chromium and Node (emsdk's) are here
|
||||
# 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
|
||||
# webStop stop the server
|
||||
# (playing to the sink, when there is one)
|
||||
# webStop stop the server, and remove the sink
|
||||
|
||||
WEB_PORT=""
|
||||
WEB_SERVER_PID=""
|
||||
WEB_SINK_NAME=""
|
||||
WEB_SINK_MODULE=""
|
||||
WEB_PORT_MIN=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() {
|
||||
local page="$1"
|
||||
shift
|
||||
"$WEB_NODE" "$JOEY_REPO/scripts/lib/webDriver.mjs" --chrome "$WEB_CHROME" --url "http://127.0.0.1:$WEB_PORT/$page" "$@"
|
||||
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" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
webStop() {
|
||||
joeyStop "$WEB_SERVER_PID"
|
||||
WEB_SERVER_PID=""
|
||||
if [ -n "$WEB_SINK_MODULE" ]; then
|
||||
pulseUnloadSink "$WEB_SINK_MODULE"
|
||||
WEB_SINK_MODULE=""
|
||||
fi
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,8 +46,10 @@
|
|||
# atarist and ste: the palette -- every shifter colour the ST
|
||||
# build writes is the 3-bit truncation, the STe build's (on an
|
||||
# STe and a Mega STe) the exact 4-bit $0RGB
|
||||
# amiga: the palette -- palette changes reach the screen at copper
|
||||
# patch speed, and right, after a palette and an SCB change
|
||||
# amiga: the palette -- palette and SCB changes present one a
|
||||
# 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
|
||||
# of the host fixture both ways, pixel-exact and with the host
|
||||
# gate's surface hash, and refuses every other target's bake (the
|
||||
|
|
@ -120,13 +122,18 @@
|
|||
# host scripts/check-serial.sh (no emulator)
|
||||
# iigs SCC modem port -> 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
|
||||
# dos COM1 -> DOSBox nullmodem
|
||||
# dvx COM1 through DVX's rs232 library -> DOSBox nullmodem
|
||||
# win32 COM1 through the Win32 comms calls -> Wine -> a pty
|
||||
# win16 COM1 through Windows 3.11's OpenComm -> KPCOMM.DRV
|
||||
# -> 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
|
||||
# (and with serial streaming both ways on the same SCC)
|
||||
# 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 a1200pal JOEY_AMIGA_MODEL=A1200 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 x68000 tilebank scripts/verify-x68000-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 x68000 serial scripts/verify-x68000-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 dos serial scripts/verify-dos-serial.sh
|
||||
serial dvx serial scripts/verify-dvx-serial.sh
|
||||
serial win32 serial scripts/verify-win32-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 sermouse scripts/verify-x68000-serial-mouse.sh
|
||||
save iigs save make -s -f make/iigs.mk iigs-verify-save || exit 1
|
||||
|
|
|
|||
80
scripts/verify-amiga-fastram.sh
Executable file
80
scripts/verify-amiga-fastram.sh
Executable 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)"
|
||||
|
|
@ -6,19 +6,23 @@
|
|||
# verify-amiga-palette.sh - the Amiga's palette and SCB changes through the
|
||||
# copper: fast, and right on the screen.
|
||||
#
|
||||
# The Amiga shows each SCB band's palette with a user copper list, built and
|
||||
# merged by graphics.library (MrgCop, LoadView, then a WaitTOF) -- about three
|
||||
# frames on an A500. A change that leaves every band where it was (a palette
|
||||
# changing, or a band given another palette) patches the colour MOVEs of the
|
||||
# installed list in place instead (src/amiga/hal.c, patchBandColors). The
|
||||
# stage hashes cannot see any of this: they hash the surface, not the copper.
|
||||
# The Amiga shows each SCB band's palette from one of two band lists of its
|
||||
# own, which the screen's user copper list (merged by graphics.library once)
|
||||
# jumps to: a present that changes palettes or the SCB writes the list not
|
||||
# shown, and the VBL server switches to it with the planes (src/amiga/hal.c,
|
||||
# bandListUpdate). Before, a band that moved rebuilt and merged the whole
|
||||
# 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)
|
||||
# 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
|
||||
# MIN_PALETTE_RATE (one rebuild a change gave 16). Then it holds a screen 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.
|
||||
# changes (bands moving), it presented a second, back to back: each waits
|
||||
# for the VBL that shows the last, so both rates must be at least MIN_RATE,
|
||||
# nearly the frame rate (a rebuild a change gave 16). Then it holds a screen
|
||||
# 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
|
||||
# Env: TIMEOUT seconds to wait for the probe (default 120)
|
||||
|
|
@ -33,9 +37,9 @@ amigaMachine
|
|||
timeout_s=${TIMEOUT:-120}
|
||||
probe=$repo/build/amiga/test/Palprobe
|
||||
label=verify-amiga-palette
|
||||
# Palette changes presented a second: 16 when every change rebuilt the
|
||||
# copper list, ~990 patching it in place (A500, 2026-10-06).
|
||||
MIN_PALETTE_RATE=500
|
||||
# Changes presented a second, back to back: one a frame is 50 on the PAL
|
||||
# A500 (16 when every change rebuilt the copper list).
|
||||
MIN_RATE=45
|
||||
|
||||
make -s -f "$repo/make/amiga.mk" "$probe" || {
|
||||
echo "$label: FAIL - building Palprobe failed" >&2
|
||||
|
|
@ -81,20 +85,20 @@ sleep 2
|
|||
DISPLAY=$display import -window root "$work/screen.png"
|
||||
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 sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
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"):
|
||||
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:
|
||||
rate = int(m.group(1))
|
||||
if rate is None:
|
||||
print(f"{label}: FAIL - no palette rate in the log")
|
||||
rates[m.group(1)] = int(m.group(2))
|
||||
if sorted(rates) != ["palette", "scb"]:
|
||||
print(f"{label}: FAIL - the log does not have both rates ({rates})")
|
||||
sys.exit(1)
|
||||
|
||||
# The bands down the middle column, top to bottom, black (the border)
|
||||
|
|
@ -112,12 +116,13 @@ for y in range(h):
|
|||
bands.append(name)
|
||||
want = ["red", "green", "yellow", "red"]
|
||||
fails = []
|
||||
if rate < minRate:
|
||||
fails.append(f"palette changes presented {rate}/s, want at least {minRate}")
|
||||
for kind in ("palette", "scb"):
|
||||
if rates[kind] < minRate:
|
||||
fails.append(f"{kind} changes presented {rates[kind]}/s, want at least {minRate}")
|
||||
if bands != want:
|
||||
fails.append(f"bands {bands}, want {want}")
|
||||
if fails:
|
||||
print(f"{label}: FAIL - " + "; ".join(fails))
|
||||
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
|
||||
|
|
|
|||
|
|
@ -10,9 +10,12 @@
|
|||
# build refuses a bank baked for any other target.
|
||||
#
|
||||
# Boots an A500 (real Kickstart) in FS-UAE under Xvfb from a scratch directory
|
||||
# hard drive whose startup-sequence runs TileBank (examples/tilebank), with
|
||||
# the DATA/tilebank make/amiga.mk staged from the host gate's fixture beside
|
||||
# it. The program loads its own bake through jlTileBankLoadToSurface and
|
||||
# hard drive whose startup-sequence runs GAME/TileBank (examples/tilebank)
|
||||
# from the drive's root, with the DATA/tilebank make/amiga.mk staged from the
|
||||
# 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
|
||||
# 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
|
||||
|
|
@ -51,10 +54,10 @@ cleanup() {
|
|||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$work/hd/s" "$work/hd/DATA"
|
||||
cp "$binDir/TileBank" "$work/hd/"
|
||||
cp -r "$dataDir" "$work/hd/DATA/tilebank"
|
||||
echo ":TileBank" > "$work/hd/s/startup-sequence"
|
||||
mkdir -p "$work/hd/s" "$work/hd/GAME/DATA"
|
||||
cp "$binDir/TileBank" "$work/hd/GAME/"
|
||||
cp -r "$dataDir" "$work/hd/GAME/DATA/tilebank"
|
||||
echo ":GAME/TileBank" > "$work/hd/s/startup-sequence"
|
||||
|
||||
display=":$((RANDOM % 400 + 1300))"
|
||||
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp > /dev/null 2>&1 &
|
||||
|
|
@ -73,7 +76,7 @@ DISPLAY="$display" timeout -s KILL "$wall" "$FSUAE" \
|
|||
guard=$!
|
||||
fsuae=$(joeyChild "$guard")
|
||||
|
||||
log=$(tileBankAwaitLine "$work/hd" "$lineSeconds" "$guard")
|
||||
log=$(tileBankAwaitLine "$work/hd/GAME" "$lineSeconds" "$guard")
|
||||
joeyStop "$fsuae" "$guard" 2> /dev/null
|
||||
fsuae=""
|
||||
guard=""
|
||||
|
|
|
|||
|
|
@ -53,12 +53,13 @@
|
|||
# being started and every sample it plays, so the STe checks are:
|
||||
#
|
||||
# 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.
|
||||
# 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
|
||||
# (a mix that never reached the buffer, or unsigned samples left
|
||||
# unconverted, which play as a DC step and not a wave) fails here.
|
||||
# from the mix buffer, near half scale (the mix's unit for effects).
|
||||
# A chip looping over silence (a mix that never reached the buffer,
|
||||
# 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
|
||||
# not a DAC on this machine.
|
||||
# 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
|
||||
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_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_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_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
|
||||
|
||||
|
||||
|
|
@ -362,9 +363,9 @@ def gradeDma(summary):
|
|||
if not runs:
|
||||
fails.append("the DMA sound chip never started: jlAudioPlaySfx did not reach it, so "
|
||||
"every digital sound on the STe is silent")
|
||||
if not modes or any(m != DMA_MODE_MONO_12K for m in modes):
|
||||
fails.append(f"DMA mode writes {[hex(m) for m in modes]}, want {hex(DMA_MODE_MONO_12K)} "
|
||||
f"(mono, 12517 Hz) -- the mix is made for that rate")
|
||||
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_STEREO_12K)} "
|
||||
f"(stereo, 12517 Hz) -- the mix is made for that rate and layout")
|
||||
if DMA_CTRL_PLAY_LOOP not in controls:
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -27,7 +27,11 @@
|
|||
#
|
||||
# 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
|
||||
# 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
|
||||
# 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
|
||||
# rowAddr, KEYS's verification row in ST RAM, once its A5A5 sentinel is up.
|
||||
# hatariBoot TOS [HATARI-ARG...] -- KEYS in Hatari on that TOS image; sets
|
||||
# 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
|
||||
# sentinel = plane words 5000 A000 5000 A000).
|
||||
hatariBoot() {
|
||||
local tos="$1"
|
||||
local i
|
||||
local base
|
||||
local addr
|
||||
local row
|
||||
shift
|
||||
joeyStop "${hatari_pid:-}"
|
||||
rm -f "$work/cmd.fifo"
|
||||
(cd "$work" && exec env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy "$HATARI" \
|
||||
--tos "$1" \
|
||||
--tos "$tos" \
|
||||
--harddrive "$bin_dir" \
|
||||
--gemdos-drive C \
|
||||
--auto 'C:\KEYS.PRG' \
|
||||
--cmd-fifo "$work/cmd.fifo" \
|
||||
--fast-forward on \
|
||||
"$@" \
|
||||
> "$work/hatari.log" 2>&1) &
|
||||
hatari_pid=$!
|
||||
for i in $(seq 1 30); do
|
||||
|
|
@ -217,16 +225,36 @@ if [ -n "$KEYS_FAILS" ]; then
|
|||
exit 1
|
||||
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"
|
||||
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"
|
||||
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\")"
|
||||
# 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)"
|
||||
|
|
|
|||
71
scripts/verify-atarist-serial-slot.sh
Executable file
71
scripts/verify-atarist-serial-slot.sh
Executable 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)"
|
||||
|
|
@ -32,6 +32,10 @@
|
|||
# hangs, AUDPAR with it
|
||||
#
|
||||
# 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
|
||||
# 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
|
||||
|
|
@ -45,11 +49,14 @@
|
|||
# take a channel each side and are graded).
|
||||
# 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
|
||||
# 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
|
||||
# Env: AUDPAR_KEEP set to keep the recording and the log in
|
||||
# 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
|
||||
set -uo pipefail
|
||||
|
||||
|
|
@ -58,10 +65,15 @@ source "$repo/scripts/lib/joeyEnv.sh"
|
|||
port=${1:-}
|
||||
label="verify-audio-conformance ($port)"
|
||||
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
|
||||
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
|
||||
RECORD_RATE=44100
|
||||
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
|
||||
pulseUnloadSink "$sinkModule"
|
||||
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
|
||||
mkdir -p "$repo/build/$port/audpar"
|
||||
cp "$work"/out.wav "$work"/log.txt "$repo/build/$port/audpar/" 2>/dev/null
|
||||
|
|
@ -192,7 +208,7 @@ runDos() {
|
|||
xvfbStart "$display"
|
||||
pulseStart
|
||||
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 &
|
||||
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)"
|
||||
webServe "$repo/build/web/probe/AUDPAR"
|
||||
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"))"
|
||||
webStop
|
||||
}
|
||||
|
|
@ -527,7 +548,8 @@ case " $PORT_STEREO " in
|
|||
*" $port "*) flags+=(--stereo) ;;
|
||||
esac
|
||||
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") ;;
|
||||
esac
|
||||
python3 "$grader" "$port" "$work/log.txt" "$work/out.wav" "${flags[@]}" > "$work/grade.txt"
|
||||
|
|
|
|||
85
scripts/verify-be-serial.sh
Executable file
85
scripts/verify-be-serial.sh
Executable 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
|
||||
|
|
@ -58,7 +58,7 @@ boot() {
|
|||
DOSBOX_JOY_SHOTS="$dir/shots"
|
||||
XDG_CONFIG_HOME="$dir/xdg" DISPLAY="$DOSBOX_JOY_DISPLAY" SDL_AUDIODRIVER=dummy \
|
||||
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" \
|
||||
-c "MOUNT C \"$(dirname "$program")\"" -c "C:" -c "$(basename "$program")" -c "EXIT" \
|
||||
> "$dir/dosbox.log" 2>&1 &
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
#
|
||||
# Runs TILEBANK.EXE (examples/tilebank) in DOSBox Staging on the 386SX-16
|
||||
# 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 +
|
||||
# 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
|
||||
|
|
@ -49,9 +51,9 @@ cleanup() {
|
|||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$work/c/DATA" "$work/xdg"
|
||||
cp "$binDir/TILEBANK.EXE" "$work/c/"
|
||||
cp -r "$dataDir" "$work/c/DATA/tilebank"
|
||||
mkdir -p "$work/c/GAME/DATA" "$work/xdg"
|
||||
cp "$binDir/TILEBANK.EXE" "$work/c/GAME/"
|
||||
cp -r "$dataDir" "$work/c/GAME/DATA/tilebank"
|
||||
|
||||
display=":$((RANDOM % 400 + 1300))"
|
||||
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 \
|
||||
timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
|
||||
--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=$!
|
||||
dosbox=$(joeyChild "$guard")
|
||||
|
||||
log=$(tileBankAwaitLine "$work/c" "$lineSeconds" "$guard")
|
||||
log=$(tileBankAwaitLine "$work/c/GAME" "$lineSeconds" "$guard")
|
||||
joeyStop "$dosbox" "$guard" 2> /dev/null
|
||||
dosbox=""
|
||||
guard=""
|
||||
|
|
|
|||
|
|
@ -14,20 +14,24 @@
|
|||
# and asks the port after every present how many pixels differ from the
|
||||
# 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
|
||||
# pixel's scale x scale block, or the full-screen backbuffer and its
|
||||
# palette; each pixel derived from the stage's palette entries, not through
|
||||
# the present's tables. Each run is scripts/bench-dvx.sh --probe on one
|
||||
# desktop (DVX.INI's video mode), about 8 s. Every count must be 0, every
|
||||
# pixel must have been compared (64000 x scale^2, plus the 256 palette
|
||||
# entries on the full screen), and DVX.LOG must show the program on the
|
||||
# desktop asked for, at the scale asked for.
|
||||
# pixel's block (scale across; down, the rows the window's shape gives it:
|
||||
# 4:3 stretches every fifth row one more, square pixels take scale each),
|
||||
# or the full-screen backbuffer and its palette; each pixel derived from the
|
||||
# stage's palette entries, not through the present's tables. Each run is
|
||||
# scripts/bench-dvx.sh --probe on one desktop (DVX.INI's video mode), about
|
||||
# 8 s, in 4:3 or (":square") with square pixels. Every count must be 0, every
|
||||
# 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
|
||||
# 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
|
||||
# (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
|
||||
# (make dvx builds it, into build/dvx/probe).
|
||||
set -uo pipefail
|
||||
|
|
@ -46,10 +50,11 @@ paletteEntries=256
|
|||
probeLine="UBER-PROBE: dvxDisplayMismatch"
|
||||
fullSpec=full
|
||||
benchSeconds=120 # one probe run, boot to done line
|
||||
defaults=(640x480x8:1 800x600x8:2 1024x768x8:3
|
||||
640x480x16:1 800x600x15:2 1024x768x16:3
|
||||
640x480x32:1 800x600x32:2 1024x768x32:3
|
||||
defaults=(640x480x8:1 800x600x8:2 1024x768x8:3:square
|
||||
640x480x16:1:square 800x600x15:2 1024x768x16:3:square
|
||||
640x480x32:1 800x600x32:2:square 1024x768x32:2
|
||||
"$fullSpec")
|
||||
shape43Height=240
|
||||
specs=("$@")
|
||||
[ ${#specs[@]} -gt 0 ] || specs=("${defaults[@]}")
|
||||
|
||||
|
|
@ -78,18 +83,28 @@ for spec in "${specs[@]}"; do
|
|||
h=$DVX_VIDEO_H
|
||||
bpp=$DVX_VIDEO_BPP
|
||||
scale=1
|
||||
shape=4:3
|
||||
rows=$((shape43Height * scale))
|
||||
want=$((stagePixels + paletteEntries))
|
||||
benchArgs+=(--full-screen)
|
||||
else
|
||||
if ! [[ "$spec" =~ ^([0-9]+)x([0-9]+)x([0-9]+):([0-9]+)$ ]]; then
|
||||
echo "verify-dvx-display: bad desktop \"$spec\" (WxHxBPP:SCALE or $fullSpec)" >&2
|
||||
if ! [[ "$spec" =~ ^([0-9]+)x([0-9]+)x([0-9]+):([0-9]+)(:square)?$ ]]; then
|
||||
echo "verify-dvx-display: bad desktop \"$spec\" (WxHxBPP:SCALE[:square] or $fullSpec)" >&2
|
||||
exit 2
|
||||
fi
|
||||
w=${BASH_REMATCH[1]}
|
||||
h=${BASH_REMATCH[2]}
|
||||
bpp=${BASH_REMATCH[3]}
|
||||
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
|
||||
DVX_VIDEO_W=$w DVX_VIDEO_H=$h DVX_VIDEO_BPP=$bpp TIMEOUT=$benchSeconds \
|
||||
"$repo/scripts/bench-dvx.sh" "${benchArgs[@]}" > /dev/null 2>&1
|
||||
|
|
@ -101,8 +116,8 @@ for spec in "${specs[@]}"; do
|
|||
fi
|
||||
counts=${row#*"$probeLine" }
|
||||
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
|
||||
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")"
|
||||
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, $shape, on a ${w}x${h} ${bpp}-bit desktop: $(grep -F 'JoeyLib: Uber Probe:' "$dvxLog")"
|
||||
continue
|
||||
fi
|
||||
if [ "$spec" = "$fullSpec" ] && ! grep -qF "$DVX_FULL_SCREEN_LOG" "$dvxLog"; then
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@
|
|||
# as hex to its joeylog.txt (in its settings folder), which must equal
|
||||
# the typed ASCII.
|
||||
# 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
|
||||
# scale given.
|
||||
# a run of its own, the window 4:3 or (":square") with square pixels;
|
||||
# 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
|
||||
# Requires: DOSBox Staging, Xvfb, xdotool, python3 + PIL, the dvx component;
|
||||
# the keys program (make dvx).
|
||||
|
|
@ -41,6 +42,7 @@ program=keys
|
|||
target="192.168.1.10:6510"
|
||||
stageWidth=320
|
||||
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
|
||||
readySeconds=60 # DVX boot until its shell reports ready
|
||||
settleSeconds=3 # then KEYS opening its window and taking the focus
|
||||
|
|
@ -98,13 +100,15 @@ run() {
|
|||
local h
|
||||
local bpp
|
||||
local scale
|
||||
local shape
|
||||
local rows
|
||||
local ready=""
|
||||
local waited
|
||||
local log
|
||||
local png
|
||||
|
||||
if ! [[ "$spec" =~ ^([0-9]+)x([0-9]+)x([0-9]+):([0-9]+)$ ]]; then
|
||||
echo "verify-dvx-input: bad desktop \"$spec\" (WxHxBPP:SCALE)" >&2
|
||||
if ! [[ "$spec" =~ ^([0-9]+)x([0-9]+)x([0-9]+):([0-9]+)(:square)?$ ]]; then
|
||||
echo "verify-dvx-input: bad desktop \"$spec\" (WxHxBPP:SCALE[:square])" >&2
|
||||
exit 2
|
||||
fi
|
||||
w=${BASH_REMATCH[1]}
|
||||
|
|
@ -112,6 +116,14 @@ run() {
|
|||
bpp=${BASH_REMATCH[3]}
|
||||
scale=${BASH_REMATCH[4]}
|
||||
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"
|
||||
printf '\n[capture]\ncapture_dir = %s\ndefault_image_capture_formats = raw\n' "$dir/shots" >> "$dir/dosbox.conf"
|
||||
mkdir -p "$dir/xdg" "$dir/shots"
|
||||
|
|
@ -165,8 +177,8 @@ run() {
|
|||
dosbox=""
|
||||
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
|
||||
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:')"
|
||||
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, $shape, on a ${w}x${h} ${bpp}-bit desktop: $(dvxDvxLog "$dir/c" 400 | grep -F 'JoeyLib: Keys:')"
|
||||
fi
|
||||
png=$(ls "$dir"/shots/*.png 2> /dev/null | head -1)
|
||||
if [ -z "$png" ]; then
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@
|
|||
#
|
||||
# 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
|
||||
# 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
|
||||
# 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
|
||||
|
|
@ -90,6 +92,7 @@ boot() {
|
|||
local dir="$JOY_WORK/$1"
|
||||
local waited
|
||||
dvxStage "$dir/c" "$(dvxAppDir "$program")"
|
||||
dvxSettings "$dir/c" "$program" "$DVX_SETTINGS_SQUARE"
|
||||
dvxDosboxConf "$dir/dosbox.conf" "$dir/c"
|
||||
mkdir -p "$dir/xdg" "$dir/shots"
|
||||
dosboxJoyMapper "$dir/mapper.txt"
|
||||
|
|
@ -97,6 +100,7 @@ boot() {
|
|||
DOSBOX_JOY_SHOTS="$dir/shots"
|
||||
printf '\n[capture]\ncapture_dir = %s\n' "$dir/shots" >> "$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"
|
||||
XDG_CONFIG_HOME="$dir/xdg" DISPLAY="$DOSBOX_JOY_DISPLAY" SDL_AUDIODRIVER=dummy \
|
||||
timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
|
||||
|
|
|
|||
|
|
@ -167,6 +167,9 @@ case "${SERLOSS_BAUD:-9600}" in
|
|||
4800) baudCode=6 ;;
|
||||
9600) baudCode=7 ;;
|
||||
19200) baudCode=9 ;;
|
||||
38400) baudCode=11 ;;
|
||||
57600) baudCode=12 ;;
|
||||
115200) baudCode=13 ;;
|
||||
*) echo "verify-iigs-serial-loss: unsupported SERLOSS_BAUD" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
|
|
|
|||
|
|
@ -21,9 +21,6 @@
|
|||
# A1 missing -> the probe never ran / printer path is broken
|
||||
#
|
||||
# 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
|
||||
set -uo pipefail
|
||||
|
|
|
|||
95
scripts/verify-sdl3-serial.sh
Executable file
95
scripts/verify-sdl3-serial.sh
Executable 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)"
|
||||
|
|
@ -41,6 +41,9 @@ cleanup() {
|
|||
trap cleanup EXIT
|
||||
|
||||
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).
|
||||
if ! webDrive "AUDIO.html" --seconds 120 --audio "$work/out.wav" \
|
||||
--step wait:5 --step hold:p:400 \
|
||||
|
|
|
|||
|
|
@ -3,16 +3,21 @@
|
|||
# See LICENSE.md for license information.
|
||||
# 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
|
||||
# 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)
|
||||
# counts its launches in SAVES/runs.dat, so its page is opened in headless
|
||||
# Chromium, given time to write, opened again (reloaded), and given time
|
||||
# again: the count it then holds must be 2 -- the first launch's 1 came
|
||||
# back from IndexedDB and the second launch added one. Each wait covers a
|
||||
# cold browser loading the program (4 s once fell short).
|
||||
# as it is closed, and again when the page is left), in a folder named after
|
||||
# the page: SAVE.html's is /SAVE/SAVES, a store of its own on a site every
|
||||
# page shares. SAVE (examples/save) counts its launches in SAVES/runs.dat.
|
||||
# Its folder is served with a second copy of its page, OTHER.html, and in
|
||||
# one headless Chromium OTHER.html is opened first (its own count, 1), then
|
||||
# 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
|
||||
# Requires: Chromium, emsdk's node; build/web/bin/SAVE (make web).
|
||||
|
|
@ -33,12 +38,14 @@ cleanup() {
|
|||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
webServe "$dir"
|
||||
webDrive "SAVE.html" --step wait:8 --step reload --step wait:8 --file "/SAVES/runs.dat:$work/runs.dat"
|
||||
cp -r "$dir" "$work/site"
|
||||
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")
|
||||
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
|
||||
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
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ boot() {
|
|||
dosboxJoyMapper "$dir/mapper.txt"
|
||||
DOSBOX_JOY_SHOTS="$dir/shots"
|
||||
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")
|
||||
win16Start "$dir" JOEYLIB_SCALE=1 JOEYLIB_ASPECT=square
|
||||
DOSBOX_JOY_DISPLAY="$WIN16_DISPLAY"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@
|
|||
# KPCOMM.DRV (the communications driver JoeyLib's Windows runs with in place
|
||||
# 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
|
||||
# Env: WIN16_SERIAL_PORT TCP port the peer listens on (default 6804)
|
||||
# Requires: Windows on DOSBox (install.sh install emulator:win16), python3;
|
||||
|
|
@ -29,8 +36,8 @@ win16Need
|
|||
program=$(joeyExampleProgram sertest win16)
|
||||
[ -f "$program" ] || { echo "$label: build SERTEST.EXE first (make win16)" >&2; exit 2; }
|
||||
port="${WIN16_SERIAL_PORT:-6804}"
|
||||
# The peer's budget: Windows' boot, then the banner and the echo.
|
||||
wall=60
|
||||
# The peer's budget: Windows' boot, the banner and the echo, then the DTR check.
|
||||
wall=95
|
||||
|
||||
work=$(mktemp -d -t joeylib-win16-serial.XXXXXX)
|
||||
peerPid=""
|
||||
|
|
@ -41,22 +48,22 @@ cleanup() {
|
|||
}
|
||||
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=$!
|
||||
sleep 1
|
||||
mkdir -p "$work/program"
|
||||
cp "$program" "$work/program/SERTEST.EXE"
|
||||
win16Stage "$work" "$work/program" SERTEST.EXE
|
||||
# transparent:1 -- data bytes only: DOSBox's in-band escapes for the control
|
||||
# lines would reach the plain TCP peer as data.
|
||||
WIN16_DOSBOX_ARGS=(--set "serial1=nullmodem server:127.0.0.1 port:$port transparent:1")
|
||||
# transparent:0 -- DOSBox sends the guest's modem lines in-band, which the
|
||||
# peer's --dtr-check reads.
|
||||
WIN16_DOSBOX_ARGS=(--set "serial1=nullmodem server:127.0.0.1 port:$port transparent:0")
|
||||
win16Start "$work"
|
||||
wait "$peerPid"
|
||||
rc=$?
|
||||
peerPid=""
|
||||
cat "$work/peer.log"
|
||||
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
|
||||
echo "$label: FAIL" >&2
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -9,18 +9,20 @@
|
|||
# watches the two sound devices through memory taps, so the checks are on
|
||||
# what the hardware was actually told, not on a log line:
|
||||
#
|
||||
# 1. PPI port C ($E9A005) low nibble. That nibble IS the MSM6258's
|
||||
# sample rate (bits 3,2) and channel gating (bits 1,0): the gate
|
||||
# wants rate index 2, divider 512, 7812 Hz -- the rate the mixer
|
||||
# renders at -- and pan 0, both channels. IOCS's own mode byte does
|
||||
# not set these correctly (measured: mode 0x05 leaves 0x01, which is
|
||||
# 3906 Hz out of the right speaker only), so audioPcm.c writes the
|
||||
# register itself and this checks that it stuck.
|
||||
# 1. The MSM6258's rate and channels, as IOCS sets them from the mode
|
||||
# audioPcm.c asks for: PPI port C ($E9A005) bits 3,2 pick the divider
|
||||
# (1024, 768, 512) and bits 1,0 gate the channels (0 both), and the
|
||||
# YM2151's CT1 pin (register $1B bit 7) the clock (0 8 MHz, the chip's
|
||||
# reset state; 1 4 MHz). The gate wants 7812 Hz, the rate the mixer
|
||||
# renders at, on both channels. (A mode byte where IOCS wants a word
|
||||
# -- this port's old 0x05 -- is 3906 Hz out of one side.)
|
||||
# 2. Every YM2151 noise write ($E90000 register 0x0F). The low five
|
||||
# bits must be a value from kOpmNoiseFreq in src/x68000/audio.c,
|
||||
# which is read out of the source here rather than duplicated: a
|
||||
# pitch that escapes the table is the bug this gate exists for
|
||||
# (a plain `31 - pitch` ran the generator an octave bright).
|
||||
# bits must be OPM_NOISE_PERIODS - pitch for a pitch of 1 to
|
||||
# NOISE_PITCH_MAX (0 plays as 1), both read out of
|
||||
# src/x68000/audio.c rather than duplicated: that is the YM2149's
|
||||
# 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
|
||||
# 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
|
||||
|
|
@ -30,7 +32,7 @@
|
|||
#
|
||||
# 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,
|
||||
# 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.
|
||||
#
|
||||
# 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 $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'
|
||||
import re, sys
|
||||
src = open(sys.argv[1]).read()
|
||||
m = re.search(r'kOpmNoiseFreq\[32\]\s*=\s*\{(.*?)\}', src, re.S)
|
||||
if not m:
|
||||
sys.exit("kOpmNoiseFreq not found")
|
||||
print(",".join(v.strip() for v in m.group(1).split(",") if v.strip()))
|
||||
def const(name):
|
||||
m = re.search(r'#define\s+' + name + r'\s+(\d+)u?', src)
|
||||
if not m:
|
||||
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
|
||||
) || { 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)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
|
@ -101,6 +107,8 @@ opmTap = mem:install_write_tap(0xE90000, 0xE90003, "opm", function(offset, data,
|
|||
sel = v
|
||||
elseif sel == 0x0F then
|
||||
out:write(string.format("NOISE %02X\\n", v))
|
||||
elseif sel == 0x1B then
|
||||
out:write(string.format("CT %02X\\n", v))
|
||||
end
|
||||
return data
|
||||
end)
|
||||
|
|
@ -157,7 +165,7 @@ python3 - "$work/audio.txt" "$table" "$PROG" <<'PY'
|
|||
import sys
|
||||
path, table, prog = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
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):
|
||||
p = line.split()
|
||||
if not p:
|
||||
|
|
@ -166,6 +174,8 @@ for line in open(path):
|
|||
noise.append(int(p[1], 16))
|
||||
elif p[0] == "PORTC":
|
||||
portc.append(int(p[1], 16))
|
||||
elif p[0] == "CT":
|
||||
ct.append(int(p[1], 16))
|
||||
elif p[0] == "PCMRATE":
|
||||
rate = float(p[1])
|
||||
elif p[0] == "DONE":
|
||||
|
|
@ -175,24 +185,27 @@ fails = []
|
|||
if not done:
|
||||
fails.append("the run never reached its time limit")
|
||||
|
||||
# 1. The ADPCM rate/pan nibble. The last value written wins; the ROM's own
|
||||
# boot value (0x0B) does not count as ours.
|
||||
# 1. The ADPCM rate and channels: the last values written win; the ROM's
|
||||
# own boot value of port C (0x0B) does not count as the port's.
|
||||
ours = [v for v in portc if v != 0x0B]
|
||||
if not ours:
|
||||
fails.append("PPI port C was never programmed, so the ADPCM never played")
|
||||
else:
|
||||
got = ours[-1] & 0x0F
|
||||
if got != 0x08:
|
||||
fails.append(f"port C low nibble {got:#04x}, want 0x08 "
|
||||
f"(rate index {(got >> 2) & 3} pan {got & 3}; want rate 2 = 7812 Hz, pan 0 = both)")
|
||||
clock = 4000000 if ct and ct[-1] & 0x80 else 8000000
|
||||
divider = (1024, 768, 512, 512)[(got >> 2) & 3]
|
||||
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}
|
||||
if not on:
|
||||
fails.append("no noise writes seen at all")
|
||||
stray = sorted(on - allowed)
|
||||
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.
|
||||
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"
|
||||
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)} "
|
||||
f"({len(on)} distinct codes, {len(allowed - on)} table entries unused), "
|
||||
f"port C {portcText}, ADPCM {rateText}")
|
||||
f"({len(on)} distinct codes, {len(allowed - on)} of the port's unused), "
|
||||
f"port C {portcText}, CT writes {len(ct)}, ADPCM {rateText}")
|
||||
if fails:
|
||||
for f in fails:
|
||||
print(f"verify-x68000-audio: FAIL ({f})", file=sys.stderr)
|
||||
|
|
|
|||
|
|
@ -91,12 +91,19 @@ local EXIT_SETTLE = 900 -- keypress -> fclose -> back at the Human68k prompt
|
|||
-- 16-bit word per pixel, 512 words per row.
|
||||
-- The 320x200 stage is centred at origin (96, 156) either way.
|
||||
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)
|
||||
if presentPlane == "gfx" then
|
||||
return mem:read_u8(0xC00000 + ((156 + py) * 512 + 96 + px) * 2 + 1)
|
||||
end
|
||||
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 v = 0
|
||||
for plane = 0, 3 do
|
||||
|
|
|
|||
|
|
@ -83,9 +83,16 @@ local fields = {}
|
|||
-- 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
|
||||
-- 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 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 v = 0
|
||||
for p = 0, 3 do
|
||||
|
|
|
|||
|
|
@ -125,9 +125,16 @@ for stick = 1, 2 do
|
|||
end
|
||||
|
||||
-- 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 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 v = 0
|
||||
for p = 0, PLANES - 1 do
|
||||
|
|
|
|||
|
|
@ -65,9 +65,16 @@ local fails = {}
|
|||
-- 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
|
||||
-- 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 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 v = 0
|
||||
for p = 0, 3 do
|
||||
|
|
|
|||
|
|
@ -193,9 +193,16 @@ local function out(s)
|
|||
io.flush()
|
||||
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 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 v = 0
|
||||
for plane = 0, 3 do
|
||||
|
|
|
|||
|
|
@ -228,9 +228,16 @@ local wr9 = emu.item(scc.items["0/m_wr9"])
|
|||
local wr2 = emu.item(sccA.items["0/m_wr2"])
|
||||
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 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 v = 0
|
||||
for p = 0, PLANES - 1 do
|
||||
|
|
|
|||
|
|
@ -55,11 +55,13 @@ work=$(mktemp -d -t joeylib-x68-tilebank.XXXXXX)
|
|||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
# 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.
|
||||
x68kBootDisk "$work/boot.xdf" 'B:' 'B:\TILEBANK.X' || exit 1
|
||||
# its log live in GAME on the data disk in B:, and AUTOEXEC starts it from
|
||||
# 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
|
||||
x68kXdf add "$work/data.xdf" "$binDir/TILEBANK.X" TILEBANK.X > /dev/null || exit 1
|
||||
x68kAddTree "$work/data.xdf" "$dataDir" DATA/TILEBANK || exit 1
|
||||
x68kXdf add "$work/data.xdf" "$binDir/TILEBANK.X" GAME/TILEBANK.X > /dev/null || exit 1
|
||||
x68kAddTree "$work/data.xdf" "$dataDir" GAME/DATA/TILEBANK || exit 1
|
||||
|
||||
cat > "$work/tilebank.lua" <<LUA
|
||||
local mem = manager.machine.devices[":maincpu"].spaces["program"]
|
||||
|
|
@ -82,9 +84,16 @@ local lamp = 0
|
|||
local exitAt = 0
|
||||
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 x = 96 + px
|
||||
local byteOff = (156 + py) * 128 + (x >> 3)
|
||||
local byteOff = (156 + shownRow() + py) * 128 + (x >> 3)
|
||||
local bit = 0x80 >> (x & 7)
|
||||
local v = 0
|
||||
for plane = 0, 3 do
|
||||
|
|
@ -163,7 +172,7 @@ lampOk=1
|
|||
if ! grep -q 'lamp=PASS' <<< "$out"; then
|
||||
lampOk=0
|
||||
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
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@
|
|||
// jlSurfaceT.portData points to this on Amiga: the 4 plane base
|
||||
// pointers + stride. Every surface (including the stage, which gets
|
||||
// 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.
|
||||
//
|
||||
// Direct fields rather than a union because the consumer is asm /
|
||||
|
|
@ -39,6 +41,7 @@ typedef struct {
|
|||
uint16_t bytesPerPlane; // = AMIGA_PLANE_SIZE (8000)
|
||||
bool ownsPlanes; // true = AllocMem'd (free at destroy);
|
||||
// false = aliased (don't free)
|
||||
bool chip; // the planes are chip RAM: the blitter reaches them
|
||||
} AmigaPlanarT;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,12 @@ extern UBYTE mt_E8Trigger;
|
|||
#define PAULA_CLOCK_PAL 3546895UL
|
||||
#define PAULA_PERIOD_MIN 124 // Paula HW limit
|
||||
#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 SFX_VOLUME_MAX 64
|
||||
|
|
@ -116,7 +122,7 @@ extern UBYTE mt_E8Trigger;
|
|||
|
||||
typedef struct {
|
||||
UBYTE *samples; // Chip-RAM copy of one SFX sample
|
||||
UWORD lengthWords; // sample length in 16-bit words
|
||||
ULONG sampleBytes; // its allocation
|
||||
// ---- streaming (jlpAudioPlaySfxStream) ----
|
||||
UBYTE *chunk; // Chip-RAM chunk buffer, SFX_STREAM_CHUNK
|
||||
BYTE cha; // Paula channel, CHANNEL_PICK = centred
|
||||
|
|
@ -126,6 +132,8 @@ typedef struct {
|
|||
void *ctx;
|
||||
uint16_t rateHz;
|
||||
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;
|
||||
} SfxSlotT;
|
||||
|
||||
|
|
@ -299,7 +307,7 @@ bool jlpAudioInit(void) {
|
|||
gModuleLength = 0;
|
||||
for (i = 0; i < JL_AUDIO_SFX_SLOTS; i++) {
|
||||
gSfxSlots[i].samples = NULL;
|
||||
gSfxSlots[i].lengthWords = 0;
|
||||
gSfxSlots[i].sampleBytes = 0;
|
||||
// Centred, not 0: zero is Paula channel 0, which would silently pin
|
||||
// every slot hard left.
|
||||
gSfxSlots[i].cha = CHANNEL_PICK;
|
||||
|
|
@ -351,9 +359,9 @@ void jlpAudioShutdown(void) {
|
|||
}
|
||||
for (i = 0; i < JL_AUDIO_SFX_SLOTS; i++) {
|
||||
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].lengthWords = 0;
|
||||
gSfxSlots[i].sampleBytes = 0;
|
||||
}
|
||||
gSfxSlots[i].active = false;
|
||||
if (gSfxSlots[i].chunk != NULL) {
|
||||
|
|
@ -466,11 +474,18 @@ static bool streamChunk(uint8_t slot) {
|
|||
uint32_t got;
|
||||
|
||||
// The fill contract is SIGNED 8-bit, which is exactly what Paula
|
||||
// wants, so the callback writes straight into Chip RAM.
|
||||
got = s->fill(s->ctx, (int8_t *)s->chunk, (uint32_t)SFX_STREAM_CHUNK);
|
||||
if (got < 2u || got > (uint32_t)SFX_STREAM_CHUNK) {
|
||||
return false;
|
||||
}
|
||||
// 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);
|
||||
if (got == 0u || got > (uint32_t)SFX_STREAM_CHUNK) {
|
||||
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
|
||||
|
||||
sfx.sfx_ptr = s->chunk;
|
||||
|
|
@ -577,11 +592,18 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
|
|||
SfxSlotT *s;
|
||||
UBYTE *chip;
|
||||
UWORD words;
|
||||
uint32_t kept;
|
||||
uint32_t head;
|
||||
uint8_t factor;
|
||||
uint8_t phase;
|
||||
|
||||
if (!gInstalled || slot >= JL_AUDIO_SFX_SLOTS) {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -590,27 +612,35 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
|
|||
// about to be freed: stop it first.
|
||||
slotSilence(slot);
|
||||
if (s->samples != NULL) {
|
||||
FreeMem(s->samples, (ULONG)s->lengthWords * 2UL);
|
||||
FreeMem(s->samples, s->sampleBytes);
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
memcpy(chip, sample, length);
|
||||
words = (UWORD)(length >> 1);
|
||||
// The head goes into Chip RAM first and the sound starts on it; the rest
|
||||
// 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->lengthWords = words;
|
||||
s->sampleBytes = kept;
|
||||
|
||||
sfx.sfx_ptr = chip;
|
||||
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_pri = SLOT_PRIORITY(slot);
|
||||
sfxPlay(slot, &sfx);
|
||||
if (kept > head) {
|
||||
(void)audioDecimate(chip + head, sample + head * factor, length - head * factor, factor, &phase);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -637,10 +667,13 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
|
|||
return;
|
||||
}
|
||||
}
|
||||
s->fill = fill;
|
||||
s->ctx = ctx;
|
||||
s->rateHz = rateHz;
|
||||
s->active = true;
|
||||
// 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->ctx = ctx;
|
||||
s->rateHz = (uint16_t)(rateHz / s->decimate);
|
||||
s->active = true;
|
||||
if (!streamChunk(slot)) {
|
||||
s->active = false;
|
||||
}
|
||||
|
|
|
|||
1007
src/amiga/hal.c
1007
src/amiga/hal.c
File diff suppressed because it is too large
Load diff
|
|
@ -2,25 +2,32 @@
|
|||
// See LICENSE.md for license information.
|
||||
// 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,
|
||||
// as Workbench starts it), made with dos.library CreateDir; delete is
|
||||
// DeleteFile; free space is Info() on a lock to the current directory, so it
|
||||
// measures the volume SAVES/ is written to however the program was started.
|
||||
// All ROM-backed, so no libc filesystem dependency.
|
||||
// The program's folder (fileRoot.h) is PROGDIR:, the assign dos.library
|
||||
// gives every program for the folder it was loaded from (Kickstart 2.0 and
|
||||
// later; on 1.3, and for a program started without a folder, the current
|
||||
// directory). SAVES/ is a real directory there,
|
||||
// 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 <dos/dos.h>
|
||||
#include <dos/dosextens.h>
|
||||
|
||||
#include <proto/dos.h>
|
||||
#include <proto/exec.h>
|
||||
|
||||
#include "joey/file.h"
|
||||
#include "fileRoot.h"
|
||||
#include "port.h"
|
||||
|
||||
|
||||
#define AMIGA_MAX_U32 0xFFFFFFFFu
|
||||
#define CURRENT_DIR "" // Lock's name for the current directory
|
||||
#define AMIGA_MAX_U32 0xFFFFFFFFu
|
||||
#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
|
||||
|
|
@ -34,7 +41,7 @@ uint32_t jlpDiskFree(void) {
|
|||
uint32_t bytesPerBlock;
|
||||
bool ok;
|
||||
|
||||
lock = Lock((CONST_STRPTR)CURRENT_DIR, SHARED_LOCK);
|
||||
lock = Lock((CONST_STRPTR)jlpWriteRoot(), SHARED_LOCK);
|
||||
if (lock == 0) {
|
||||
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) {
|
||||
return DeleteFile((CONST_STRPTR)path) != 0;
|
||||
}
|
||||
|
|
@ -75,3 +94,13 @@ bool jlpSaveDirEnsure(const char *dir) {
|
|||
}
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,23 +9,27 @@
|
|||
// * 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
|
||||
// PlayMod/PlaySfx (MFP prescaler 200, data 1) and uninstalled once
|
||||
// the mix buffer has drained back to pure silence with nothing
|
||||
// playing.
|
||||
// the mix buffer has held pure silence, with nothing playing, for
|
||||
// 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
|
||||
// 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
|
||||
// its high 4 bits to YM register 8 (channel A volume), advances
|
||||
// the play pointer, raises a refill flag at half/end. The refill
|
||||
// itself happens in jlpAudioFrameTick on the main thread: the mix is
|
||||
// too long for an interrupt.
|
||||
// * Each Timer A tick the ISR pops one byte off the buffer, writes its
|
||||
// YM level (gYmLevel: the YM's sixteen levels are logarithmic, so each
|
||||
// sample plays as the level nearest it on a straight line) to YM
|
||||
// register 8 (channel A volume), and advances the play pointer; the
|
||||
// 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
|
||||
// modern standards but period-correct for stock 520ST/1040ST without
|
||||
// the STe DMA chip.
|
||||
// Quality budget: sixteen logarithmic levels at 12.3 kHz on the YM is
|
||||
// grim by modern standards but period-correct for stock 520ST/1040ST
|
||||
// without the STe DMA chip.
|
||||
//
|
||||
// The STe build (make ste, JOEYLIB_ATARI_STE) plays the same mix buffer
|
||||
// through the STe's 8-bit DMA sound chip instead, mono at 12517 Hz,
|
||||
// looping over the whole buffer with no interrupt at all.
|
||||
// The STe build (make ste, JOEYLIB_ATARI_STE) plays the mix buffer, in
|
||||
// left and right byte pairs, through the STe's 8-bit DMA sound chip
|
||||
// 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
|
||||
// has finished, converts each refilled half to the chip's signed
|
||||
// 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_C 0x20u
|
||||
|
||||
#define MIX_BUFFER 2048
|
||||
#define MIX_HALF (MIX_BUFFER / 2)
|
||||
// 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.
|
||||
// The buffer is two halves of MIX_HALF_FRAMES frames: bytes on the plain
|
||||
// ST, left and right byte pairs on the STe, whose DMA chip plays stereo.
|
||||
#ifdef JOEYLIB_ATARI_STE
|
||||
#define SFX_START_MARGIN 64u
|
||||
#define MIX_CHANNELS 2u
|
||||
#else
|
||||
#define SFX_START_MARGIN 512u
|
||||
#define MIX_CHANNELS 1u
|
||||
#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.
|
||||
#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
|
||||
// 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_STOP 0x00u
|
||||
#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
|
||||
// ST build: the output stage is PWM through YM channel A's volume, one
|
||||
// sample per MFP Timer A interrupt.
|
||||
|
|
@ -141,6 +154,27 @@
|
|||
|
||||
#define MIX_RATE 12288 // Hz, matches MFP 2.4576 MHz / 200
|
||||
#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
|
||||
|
||||
// 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).
|
||||
#define VEC_MFP_TC (0x114 / 4)
|
||||
#define ST_TIMER_C_HZ 200u
|
||||
#define SETEXC_READ (-1L) // Setexc's vector argument that only reads
|
||||
|
||||
// ----- 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).
|
||||
volatile uint8_t *gMixPos = 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);
|
||||
|
||||
|
|
@ -211,6 +256,9 @@ static uint8_t gModTailHalf = 0;
|
|||
// SFX overlay shared with the DOS HAL (see src/core/audioSfxMix.c).
|
||||
// Mixed in over the module's output during jlpAudioFrameTick.
|
||||
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).
|
||||
// Both the PWM (installTimerA) and PSG-tone (applyYmVoice) paths
|
||||
|
|
@ -238,11 +286,20 @@ static long applyYmVoice(void);
|
|||
static bool anySfxActive(void);
|
||||
static uint16_t playOffset(void);
|
||||
static void pollRefill(void);
|
||||
static bool pumpStart(void);
|
||||
static void pumpStop(void);
|
||||
static void refillHalf(uint8_t half);
|
||||
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 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
|
||||
static uint16_t dmaPlayOffset(void);
|
||||
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
|
||||
// = 128) before opening the IRQ gate.
|
||||
silenceMixBuffer();
|
||||
rampWrite(0u, 0u, MIX_SILENCE);
|
||||
gMixPos = (volatile uint8_t *)gMixBuf;
|
||||
gMixEnd = (volatile uint8_t *)(gMixBuf + MIX_BUFFER);
|
||||
gNeedRefill[0] = 0;
|
||||
gNeedRefill[1] = 0;
|
||||
gPlayHalf = 0u;
|
||||
gDraining = false;
|
||||
gIdle = false;
|
||||
|
||||
// YM2149 setup for PWM-via-volume on channel A:
|
||||
// reg 7 (mixer): set bits 0 (tone A off) and 3 (noise A off);
|
||||
|
|
@ -413,7 +473,7 @@ static long installDma(void) {
|
|||
gPlayHalf = 0u;
|
||||
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));
|
||||
*STE_DMA_MODE = STE_DMA_MONO_12K;
|
||||
*STE_DMA_MODE = STE_DMA_STEREO_12K;
|
||||
*STE_DMA_CTRL = STE_DMA_PLAY_LOOP;
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -466,14 +526,18 @@ static void refillHalf(uint8_t half) {
|
|||
dst = gMixBuf + (uint16_t)half * MIX_HALF;
|
||||
silentFill = !modPlayerActive() && !anySfxActive();
|
||||
if (modPlayerActive()) {
|
||||
if (!modPlayerRenderU8((uint8_t *)dst, MIX_HALF)) {
|
||||
if (!modPlayerRenderU8((uint8_t *)dst, MIX_HALF_FRAMES, MIX_CHANNELS)) {
|
||||
gModTail = true;
|
||||
gModTailHalf = half;
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
#endif
|
||||
#ifdef JOEYLIB_ATARI_STE
|
||||
dmaSignBytes(dst, MIX_HALF);
|
||||
#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
|
||||
// output's own format: the STe's are signed, so flipped for the mix and back.
|
||||
// Mix a slot over len bytes of the buffer from at that are mixed already,
|
||||
// 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) {
|
||||
#ifdef JOEYLIB_ATARI_STE
|
||||
dmaSignBytes(gMixBuf + at, len);
|
||||
#endif
|
||||
audioSfxOverlayMixStereo(gMixBuf + at, (uint16_t)(len / MIX_CHANNELS), &gSfxSlots[slot], 1u, &gSfxPan[slot], AUDIO_SFX_DST_SIGNED);
|
||||
#else
|
||||
audioSfxOverlayMix(gMixBuf + at, len, &gSfxSlots[slot], 1u);
|
||||
#ifdef JOEYLIB_ATARI_STE
|
||||
dmaSignBytes(gMixBuf + at, len);
|
||||
#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 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
|
||||
// been refilled already, the half after it -- and the refills carry on from
|
||||
// where that leaves the slot.
|
||||
static void sfxStartNow(uint8_t slot) {
|
||||
// been refilled already, the half after it -- from SFX_START_MARGIN past the
|
||||
// play position, which is in the next half when the play position is that
|
||||
// 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 start;
|
||||
uint16_t halfEnd;
|
||||
uint16_t aheadEnd;
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
half = (uint8_t)(pos >= MIX_HALF);
|
||||
// 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
|
||||
// the sound there.
|
||||
nextReady = half == gPlayHalf;
|
||||
start = (uint16_t)(pos + SFX_START_MARGIN);
|
||||
halfEnd = (uint16_t)((half + 1u) * MIX_HALF);
|
||||
// the sound there. Offsets here run on past the buffer's end into the
|
||||
// other half, unwrapped.
|
||||
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) {
|
||||
sfxOverlayAt(start, (uint16_t)(halfEnd - start), slot);
|
||||
gHalfSilent[half] = false;
|
||||
start = halfEnd;
|
||||
}
|
||||
if (nextReady) {
|
||||
sfxOverlayAt((uint16_t)((half ^ 1u) * MIX_HALF), MIX_HALF, slot);
|
||||
if (start < aheadEnd) {
|
||||
sfxOverlayAt((uint16_t)((half ^ 1u) * MIX_HALF + (start - halfEnd)), (uint16_t)(aheadEnd - start), slot);
|
||||
gHalfSilent[half ^ 1u] = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -541,10 +616,96 @@ static bool anySfxActive(void) {
|
|||
}
|
||||
|
||||
|
||||
static void pumpStart(void) {
|
||||
if (gPumpRunning) {
|
||||
#ifndef JOEYLIB_ATARI_STE
|
||||
// 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;
|
||||
}
|
||||
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
|
||||
Supexec(installDma);
|
||||
#else
|
||||
|
|
@ -553,6 +714,8 @@ static void pumpStart(void) {
|
|||
gPumpRunning = true;
|
||||
gHalfSilent[0] = false;
|
||||
gHalfSilent[1] = false;
|
||||
tickHookUpdate();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -571,6 +734,7 @@ static void pumpStop(void) {
|
|||
#else
|
||||
Supexec(uninstallTimerA);
|
||||
#endif
|
||||
tickHookUpdate();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -585,19 +749,37 @@ bool jlpAudioInit(void) {
|
|||
if (!steMachineCheck()) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
ymLevelsBuild();
|
||||
#endif
|
||||
#ifdef JOEYLIB_ATARI_STE
|
||||
memset(gSfxPan, JL_AUDIO_PAN_CENTER, sizeof(gSfxPan));
|
||||
#endif
|
||||
modPlayerSetU8Shift(MIX_UNIT_SHIFT);
|
||||
audioSfxSetShift(MIX_UNIT_SHIFT);
|
||||
gReady = 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) {
|
||||
// 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
|
||||
// would outlive the program. (The all-off still runs independent of
|
||||
// gReady: the tone/noise voice layer is usable without jlpAudioInit, and
|
||||
// the YM latches -- a live voice keeps sounding after exit.)
|
||||
tickUnhook();
|
||||
(void)jlpAudioTickRegister(NULL, 0u);
|
||||
if (gReady) {
|
||||
pumpStop();
|
||||
}
|
||||
|
|
@ -625,7 +807,7 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
|
|||
if (!modPlayerStart(data, length, MIX_RATE, loop)) {
|
||||
return;
|
||||
}
|
||||
pumpStart();
|
||||
(void)pumpStart();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -634,8 +816,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
|
|||
return;
|
||||
}
|
||||
audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE);
|
||||
pumpStart();
|
||||
sfxStartNow(slot);
|
||||
sfxStartNow(slot, pumpStart());
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -644,8 +825,7 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
|
|||
return;
|
||||
}
|
||||
audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE);
|
||||
pumpStart();
|
||||
sfxStartNow(slot);
|
||||
sfxStartNow(slot, pumpStart());
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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),
|
||||
// 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
|
||||
// 49.4 dB below level 15), so each attenuation takes the nearest level:
|
||||
// within 1.6 dB of the asked one, the closest the chip can come. YM also
|
||||
// 49.4 dB below level 15), so each attenuation takes the level nearest 2 dB
|
||||
// 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.
|
||||
//
|
||||
// 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_PERIOD_MAX 0x0FFFu
|
||||
|
||||
#ifdef JOEYLIB_ATARI_STE
|
||||
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) {
|
||||
|
|
@ -776,29 +966,45 @@ static long ymPsgAllOff(void) {
|
|||
|
||||
|
||||
// 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) {
|
||||
tickUnhook();
|
||||
if (fn == NULL) {
|
||||
return true;
|
||||
gStTickHz = 0u;
|
||||
gStTickCarry = 0u;
|
||||
gStTickFn = fn;
|
||||
if (fn != NULL) {
|
||||
gStTickHz = hz;
|
||||
}
|
||||
gStTickFn = fn;
|
||||
gStTickHz = hz;
|
||||
gStTickCarry = 0u;
|
||||
gStTickOldVec = (void (*)(void))Setexc(VEC_MFP_TC, (long)stTickIsr);
|
||||
gStTickHooked = true;
|
||||
tickHookUpdate();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// TOS's Timer C vector back, if it was taken.
|
||||
static void tickUnhook(void) {
|
||||
if (!gStTickHooked) {
|
||||
// The Timer C hook in place while something needs it: the program's tick,
|
||||
// or the plain ST's PWM pump, whose Timer A the hook lets in during TOS's
|
||||
// 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;
|
||||
}
|
||||
(void)Setexc(VEC_MFP_TC, (long)gStTickOldVec);
|
||||
gStTickHooked = false;
|
||||
gStTickFn = NULL;
|
||||
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);
|
||||
}
|
||||
gStTickHooked = want;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -904,6 +1110,15 @@ void jlpAudioFrameTick(void) {
|
|||
if (!gReady || !gPumpRunning) {
|
||||
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();
|
||||
// An ended song's last half, flagged for refill, has played.
|
||||
if (gModTail && gNeedRefill[gModTailHalf]) {
|
||||
|
|
@ -916,8 +1131,21 @@ void jlpAudioFrameTick(void) {
|
|||
refillHalf(1u);
|
||||
}
|
||||
// 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()) {
|
||||
#ifdef JOEYLIB_ATARI_STE
|
||||
pumpStop();
|
||||
#else
|
||||
if (!gIdle) {
|
||||
gIdle = true;
|
||||
gIdleSinceMs = jlpMillisElapsed();
|
||||
} else if (jlpMillisElapsed() - gIdleSinceMs >= PWM_IDLE_MS) {
|
||||
drainStart();
|
||||
}
|
||||
} else {
|
||||
gIdle = false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,21 +7,23 @@
|
|||
|
|
||||
| Each interrupt:
|
||||
| 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
|
||||
| the analog filter on the YM output emits a 4-bit-resolution
|
||||
| sample for that tick.
|
||||
| 2. Writes its YM level (_gYmLevel[byte], the level nearest the
|
||||
| sample's value: the YM's levels are logarithmic) to YM register 8
|
||||
| (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.
|
||||
| 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 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
|
||||
| are reached by short addresses, and the high nibble needs no mask (the
|
||||
| YM takes the byte's low 4 bits).
|
||||
| are reached by short addresses.
|
||||
|
|
||||
| Communication with the C HAL is via three globals defined in audio.c:
|
||||
| _gMixBuf -- start of the mix buffer (uint8_t array)
|
||||
| _gMixPos -- volatile pointer to the next sample to play
|
||||
| _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
|
||||
| gcc driver.
|
||||
|
|
@ -34,13 +36,13 @@
|
|||
.equ YM_VOL_A, 8
|
||||
.equ MFP_ISRA, 0xFFFFFA0F
|
||||
.equ ISRA_ACK, 0xDF | clear Timer A's in-service bit
|
||||
.equ NIBBLE, 4
|
||||
|
||||
_mfpTimerAIsr:
|
||||
movem.l %d0/%a0,-(%sp)
|
||||
movea.l _gMixPos,%a0
|
||||
moveq #0,%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 %d0,(YM_DATA).w
|
||||
cmpa.l _gMixEnd,%a0
|
||||
|
|
@ -51,3 +53,11 @@ _mfpTimerAIsr:
|
|||
move.b #ISRA_ACK,(MFP_ISRA).w
|
||||
movem.l (%sp)+,%d0/%a0
|
||||
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
|
||||
|
|
|
|||
|
|
@ -14,15 +14,17 @@
|
|||
// 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).
|
||||
// * VBL ISR pre-loads the first band's palette, then programs
|
||||
// MFP Timer B (event-count mode, TBDR = HBL delta to first
|
||||
// transition) to fire at the END of the last scanline before
|
||||
// the next band starts.
|
||||
// * Timer B ISR writes the current band's palette, advances the
|
||||
// transition index, and (stop/reload TBDR/restart) reprograms
|
||||
// Timer B to fire at the next transition. With 8 transitions per
|
||||
// frame the ISR runs 8 times instead of 313 -- well under the
|
||||
// ~147-HBL-fires-per-frame cap Hatari's MFP emulation imposes on
|
||||
// event-count mode, and ~0.2% CPU overhead vs ~60% for per-HBL.
|
||||
// MFP Timer B (event-count mode) to fire at the end of the display
|
||||
// of the line TWO above the next band (fireDelta).
|
||||
// * Timer B's handler (rasterIsr.s) loads the band's palette into
|
||||
// registers, waits for the video address counter to reach the end
|
||||
// of the next line's display, and stores all 16 colours in the
|
||||
// horizontal blank before the band, so its first line shows them
|
||||
// from its first pixel; rasterBandNext then aims it at the next
|
||||
// band. With 8 transitions per frame it runs 8 times instead of 313
|
||||
// -- 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
|
||||
// in shifter format and the band table; jlpPresent requantizes only
|
||||
// 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 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_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
|
||||
|
||||
// MFP hardware addresses.
|
||||
#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_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_EVENT 0x08
|
||||
#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
|
||||
// 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
|
||||
// bandStart[i], load palette bandPalIdx[i]. Built from the jlSurfaceT's
|
||||
// palettes and SCB by jlpPresent.
|
||||
// bandStart[i], load palette bandPalIdx[i]. Timer B fires for band i
|
||||
// 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
|
||||
// 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 {
|
||||
uint16_t palettes [JL_SURFACE_PALETTE_COUNT][JL_SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t bandStart [MAX_BANDS];
|
||||
uint8_t bandPalIdx[MAX_BANDS];
|
||||
uint8_t fireDelta [MAX_BANDS];
|
||||
uint16_t bandCount;
|
||||
} ScreenTablesT;
|
||||
|
||||
static void flipWait(void);
|
||||
static void presentShow(const uint8_t *page);
|
||||
static uint16_t quantizeColorToSt(uint16_t orgb);
|
||||
static void quantizeDirtyPalettes(ScreenTablesT *tables, const jlSurfaceT *src);
|
||||
|
|
@ -298,13 +318,15 @@ static ScreenTablesT *tablesToWrite(void);
|
|||
extern void stLeaveSuper(void *savedSsp);
|
||||
|
||||
static long readFrameHz(void);
|
||||
static long readHz200(void);
|
||||
static long readClockMs(void);
|
||||
static long writePrevPaletteRegs(void);
|
||||
|
||||
static __attribute__((interrupt_handler)) void timerBIsr(void);
|
||||
static __attribute__((interrupt_handler)) void vblIsr(void);
|
||||
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);
|
||||
extern void stRasterIsr(void);
|
||||
|
||||
// ----- Module state -----
|
||||
|
||||
|
|
@ -378,15 +400,27 @@ static volatile uint8_t gTablesShown = 0;
|
|||
static volatile uint8_t gTablesPending = TABLES_NONE;
|
||||
static uint8_t gTablesWriting = TABLES_NONE;
|
||||
|
||||
// Index of the band the Timer B ISR is currently scheduling TO. At
|
||||
// VBL this is 0 (band 0 palette pre-loaded, Timer B scheduled to
|
||||
// fire when it's time to transition to band 1). Each ISR fire writes
|
||||
// the palette for gCurrentBand and advances to the next.
|
||||
// Index of the band whose palette is on the shifter. At VBL this is 0
|
||||
// (band 0 palette pre-loaded, Timer B scheduled for band 1); each Timer B
|
||||
// fire loads the next band's and advances it (rasterBandNext).
|
||||
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).
|
||||
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.
|
||||
static void (*gOldVblVec)(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.
|
||||
static void buildTransitions(ScreenTablesT *tables, const jlSurfaceT *src) {
|
||||
uint16_t line;
|
||||
uint16_t band;
|
||||
int16_t fire;
|
||||
int16_t lastFire;
|
||||
uint8_t idx;
|
||||
uint8_t prev;
|
||||
|
||||
|
|
@ -427,6 +464,28 @@ static void buildTransitions(ScreenTablesT *tables, const jlSurfaceT *src) {
|
|||
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();
|
||||
if (page != NULL) {
|
||||
setShifterVideoBase(page);
|
||||
gFlipPending = 1u;
|
||||
}
|
||||
if (gTablesWriting != TABLES_NONE) {
|
||||
gTablesPending = gTablesWriting;
|
||||
|
|
@ -521,93 +581,57 @@ static ScreenTablesT *tablesToWrite(void) {
|
|||
}
|
||||
|
||||
|
||||
// Timer B interrupt handler. Fires once at each band transition;
|
||||
// writes the band's palette to the shifter and lets Timer B's
|
||||
// auto-reload keep 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.
|
||||
// Aim the Timer B handler at band: its colours, and the counter's value when
|
||||
// the display of the line above it ends.
|
||||
static void rasterAim(const ScreenTablesT *tables, uint16_t band) {
|
||||
gStRasterColors = tables->palettes[tables->bandPalIdx[band]];
|
||||
gStRasterTarget = (uint16_t)(gRasterBase + tables->bandStart[band] * ST_BYTES_PER_ROW);
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
// underflow, so a value written here does not take effect on the
|
||||
// NEXT fire (that interval was already reloaded at the underflow
|
||||
// that triggered this ISR) but on the fire AFTER next. To time
|
||||
// variable-height bands correctly we therefore program one fire
|
||||
// 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) {
|
||||
// MFP event-count auto-reload latches the CURRENT TBDR at each underflow,
|
||||
// so a value written here does not take effect on the NEXT fire (that
|
||||
// interval was already reloaded at the underflow that triggered this
|
||||
// interrupt) but on the fire AFTER next: the band + 2 delta.
|
||||
void rasterBandNext(void) {
|
||||
const ScreenTablesT *tables;
|
||||
uint16_t band;
|
||||
const uint16_t *src;
|
||||
volatile uint16_t *dst;
|
||||
uint16_t nextDelta;
|
||||
|
||||
tables = &gTables[gTablesShown];
|
||||
band = gCurrentBand + 1;
|
||||
band = (uint16_t)(gCurrentBand + 1u);
|
||||
gCurrentBand = band;
|
||||
|
||||
if (band < tables->bandCount) {
|
||||
// buildTransitions already folded out-of-range SCB values to 0.
|
||||
src = tables->palettes[tables->bandPalIdx[band]];
|
||||
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;
|
||||
}
|
||||
*ST_MFP_ISRA = MFP_TB_CLEAR;
|
||||
return;
|
||||
if (band + 1u < tables->bandCount) {
|
||||
rasterAim(tables, (uint16_t)(band + 1u));
|
||||
if (band + 2u < tables->bandCount) {
|
||||
*ST_MFP_TBDR = tables->fireDelta[band + 2u];
|
||||
}
|
||||
} else {
|
||||
// No further transitions this frame.
|
||||
*ST_MFP_TBCR = MFP_TBCR_STOP;
|
||||
}
|
||||
|
||||
// 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_ISRA = MFP_TB_CLEAR;
|
||||
}
|
||||
|
||||
|
||||
// Vertical blank handler. Pre-loads band 0's palette so the first
|
||||
// visible scanline is correct, then programs Timer B to fire at
|
||||
// the HBL delta to the next band transition (if any).
|
||||
// visible scanline is correct, then programs Timer B to fire for the next
|
||||
// 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) {
|
||||
const ScreenTablesT *tables;
|
||||
uint16_t delta;
|
||||
const uint16_t *src;
|
||||
volatile uint16_t *dst;
|
||||
|
||||
gFrameCount = gFrameCount + 1;
|
||||
gCurrentBand = 0;
|
||||
gFlipPending = 0u;
|
||||
// The table set the last present handed over shows from this frame, as
|
||||
// does the page it flipped to (presentShow).
|
||||
if (gTablesPending != TABLES_NONE) {
|
||||
|
|
@ -642,26 +666,20 @@ static void vblIsr(void) {
|
|||
|
||||
// Program Timer B for the next band transition.
|
||||
if (tables->bandCount > 1) {
|
||||
delta = tables->bandStart[1] - tables->bandStart[0];
|
||||
if (delta == 0 || delta > 255) {
|
||||
delta = 1;
|
||||
}
|
||||
gRasterBase = (uint16_t)((*ST_VID_COUNT_MID << 8) | *ST_VID_COUNT_LOW);
|
||||
rasterAim(tables, 1u);
|
||||
*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_TBCR = MFP_TBCR_EVENT;
|
||||
// The restart above loads the counter from TBDR (= the band
|
||||
// 0 -> 1 delta), so the first fire is correctly timed. Now
|
||||
// pre-program TBDR with the band 1 -> 2 delta so the band-1
|
||||
// underflow auto-reloads with the right interval (one fire
|
||||
// ahead, matching timerBIsr). Writing TBDR while counting
|
||||
// only changes the next reload, not the in-flight count.
|
||||
// The restart above loads the counter from TBDR (band 1's
|
||||
// delta), so the first fire is correctly timed. Now pre-program
|
||||
// TBDR with band 2's so the band-1 underflow auto-reloads with
|
||||
// the right interval (one fire ahead, matching rasterBandNext).
|
||||
// Writing TBDR while counting only changes the next reload, not
|
||||
// the in-flight count.
|
||||
if (tables->bandCount > 2) {
|
||||
delta = tables->bandStart[2] - tables->bandStart[1];
|
||||
if (delta == 0 || delta > 255) {
|
||||
delta = 1;
|
||||
}
|
||||
*ST_MFP_TBDR = (uint8_t)delta;
|
||||
*ST_MFP_TBDR = tables->fireDelta[2];
|
||||
}
|
||||
} else {
|
||||
*ST_MFP_TBCR = MFP_TBCR_STOP;
|
||||
|
|
@ -713,6 +731,14 @@ bool jlpInit(const jlConfigT *config) {
|
|||
}
|
||||
#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
|
||||
// video-base, palette, and MFP registers live in the supervisor-only
|
||||
// $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);
|
||||
(void)Setexc(VEC_VBL, (long)vblIsr);
|
||||
|
||||
// Program MFP Timer B: event-count (HBL) mode, initial TBDR=1
|
||||
// (a placeholder -- VBL ISR reprograms it for the first real
|
||||
// transition per frame), vector=timerBIsr, then enable in IMRA.
|
||||
Xbtimer(1, MFP_TBCR_EVENT, 1, timerBIsr);
|
||||
// Install MFP Timer B's vector (stRasterIsr) stopped, TBDR=1 a
|
||||
// placeholder, then enable it in IMRA: the VBL ISR starts it in
|
||||
// event-count (HBL) mode each frame with bands, once it has aimed the
|
||||
// handler at the first (rasterAim).
|
||||
Xbtimer(1, MFP_TBCR_STOP, 1, stRasterIsr);
|
||||
Jenabint(INT_TIMER_B);
|
||||
|
||||
return true;
|
||||
|
|
@ -873,6 +900,7 @@ void jlpPresent(const jlSurfaceT *src) {
|
|||
backIdx = (uint8_t)(gFrontIdx ^ 1u);
|
||||
backPage = gScreenPage[backIdx];
|
||||
|
||||
flipWait();
|
||||
slam = present68kStSlam(pd->base, backPage);
|
||||
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) {
|
||||
if (gSuperEntered) {
|
||||
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 ticks;
|
||||
if (gSuperEntered) {
|
||||
ticks = (uint32_t)readHz200();
|
||||
} else {
|
||||
ticks = (uint32_t)Supexec(readHz200);
|
||||
return (uint32_t)readClockMs();
|
||||
}
|
||||
// 0 is a legal counter value (power-on tick 0 and every ~248-day
|
||||
// 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);
|
||||
return (uint32_t)Supexec(readClockMs);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@
|
|||
// (caps-lock toggles on scan 0x3A); Ctrl chords are commands, not
|
||||
// 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
|
||||
// @ [ ] { } \), where the OS keeps that layer in its Keytbl tables:
|
||||
// TOS from 4.00, and EmuTOS. Older TOS keeps it inside the keyboard
|
||||
// code JoeyLib takes over. The IKBD sends one make per press
|
||||
// @ [ ] { } \): from the OS's Keytbl tables on TOS 4.00 and later and
|
||||
// on EmuTOS, and from stAltLayer.c's copies of the layers older TOS
|
||||
// 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
|
||||
// a held key's character itself, at TOS's Kbrate settings.
|
||||
|
||||
|
|
@ -44,9 +44,9 @@
|
|||
#include <mint/ostruct.h>
|
||||
#include <mint/sysvars.h>
|
||||
|
||||
#include "m68kIrq.h"
|
||||
#include "port.h"
|
||||
#include "inputInternal.h"
|
||||
#include "stAltLayer.h"
|
||||
#include "joey/surface.h"
|
||||
|
||||
#ifdef JOEYLIB_ATARI_STE
|
||||
|
|
@ -86,6 +86,8 @@
|
|||
#define TOS_ALT_LAYER_VERSION 0x0400u
|
||||
#define EMUTOS_MAGIC 0x45544F53uL
|
||||
#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
|
||||
// the wrap is a mask; holds ISR_CHAR_QUEUE_SIZE - 1 pending bytes.
|
||||
|
|
@ -154,9 +156,9 @@ typedef struct {
|
|||
// ----- Prototypes -----
|
||||
|
||||
static uint8_t altChar(const uint8_t *pairs, uint8_t code);
|
||||
static void altLayerFind(const KeyTabT *keyTab);
|
||||
static void charRepeat(void);
|
||||
static long ikbdHandler(void);
|
||||
static bool keyTabHasAlt(const KeyTabT *keyTab);
|
||||
#ifdef JOEYLIB_ATARI_STE
|
||||
static void padToStick(const StePadT *pad, jlJoystickE js);
|
||||
#endif
|
||||
|
|
@ -248,9 +250,14 @@ static bool gHooked = false;
|
|||
// uint8_t (not bool) so element size matches gKeyState's. See
|
||||
// src/core/inputInternal.h for the full rationale.
|
||||
static volatile uint8_t gIsrState[JL_KEY_COUNT];
|
||||
// Keys pressed since the last poll, so a tap between two polls still reads
|
||||
// down for one (inputLatch).
|
||||
static volatile uint8_t gIsrTapped[JL_KEY_COUNT];
|
||||
// Each key's presses, counted by the ISR (which alone writes them), and the
|
||||
// count the last poll saw: a key whose count moved was pressed since, so a
|
||||
// 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
|
||||
// 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 *gKeyTabCaps = NULL;
|
||||
// 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 *gKeyTabAltShift = 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
|
||||
// down and then at TOS's rate (the ISR names the key, gIsrRepeatCode).
|
||||
static void charRepeat(void) {
|
||||
|
|
@ -445,7 +478,7 @@ static long ikbdHandler(void) {
|
|||
if (key != JL_KEY_NONE) {
|
||||
gIsrState[key] = inputKeyHeld(gIsrState[key], entry, !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
|
||||
// A Jaguar pad as a stick: its directions, and its A and B as the two
|
||||
// buttons.
|
||||
|
|
@ -572,7 +592,8 @@ void jlpInputInit(void) {
|
|||
memset(gKeyState, 0, sizeof(gKeyState));
|
||||
memset(gKeyPrev, 0, sizeof(gKeyPrev));
|
||||
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));
|
||||
|
||||
// Cache the TOS keyboard translation tables (the OS keymap) for
|
||||
|
|
@ -583,11 +604,7 @@ void jlpInputInit(void) {
|
|||
gKeyTabUnshift = keyTab->unshift;
|
||||
gKeyTabShift = keyTab->shift;
|
||||
gKeyTabCaps = keyTab->caps;
|
||||
if (keyTabHasAlt(keyTab)) {
|
||||
gKeyTabAltUnshift = keyTab->altUnshift;
|
||||
gKeyTabAltShift = keyTab->altShift;
|
||||
gKeyTabAltCaps = keyTab->altCaps;
|
||||
}
|
||||
altLayerFind(keyTab);
|
||||
}
|
||||
// Seed caps lock from TOS: taking over ikbdsys freezes the OS's
|
||||
// own tracking, so an engaged caps lock at launch would otherwise
|
||||
|
|
@ -638,14 +655,15 @@ void jlpInputPoll(void) {
|
|||
uint8_t btn;
|
||||
uint8_t joy;
|
||||
uint8_t tail;
|
||||
uint8_t taps;
|
||||
uint16_t i;
|
||||
uint16_t sr;
|
||||
|
||||
// Interrupts held off across the latch, so a press between its read of a
|
||||
// key's tap and its clearing of it is not lost.
|
||||
sr = m68kInterruptsOff();
|
||||
inputLatch(gKeyState, gIsrState, gIsrTapped, JL_KEY_COUNT);
|
||||
m68kInterruptsRestore(sr);
|
||||
// Down when held, or pressed since the last poll (gIsrTaps).
|
||||
for (i = 0u; i < JL_KEY_COUNT; i++) {
|
||||
taps = gIsrTaps[i];
|
||||
gKeyState[i] = (uint8_t)(gIsrState[i] | (taps != gTapsSeen[i]));
|
||||
gTapsSeen[i] = taps;
|
||||
}
|
||||
|
||||
// Drain ISR-translated characters into the core queue. Single-
|
||||
// consumer side of the SPSC ring: only gIsrCharTail is written
|
||||
|
|
|
|||
68
src/atarist/rasterIsr.s
Normal file
68
src/atarist/rasterIsr.s
Normal 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
|
||||
|
|
@ -6,7 +6,11 @@
|
|||
//
|
||||
// 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-
|
||||
// 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/
|
||||
// XBIOS traps that switch to supervisor internally, so -- unlike the direct
|
||||
// $FFFFFAxx pokes elsewhere in the ST HAL -- serial needs no supervisor
|
||||
|
|
@ -27,6 +31,13 @@
|
|||
#define ST_AUX_DEV 1
|
||||
#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:
|
||||
// bit 7 clock divide (1 = /16, standard async)
|
||||
// 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_LOW_WATER (ST_RX_BUFFER_BYTES / 4)
|
||||
#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
|
||||
|
||||
|
||||
|
|
@ -69,6 +82,12 @@
|
|||
static int16_t gSavedBaudCode = 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.
|
||||
static _IOREC *gRxIorec = NULL;
|
||||
static _IOREC gSavedRxIorec;
|
||||
|
|
@ -78,6 +97,7 @@ static char gRxBuffer[ST_RX_BUFFER_BYTES];
|
|||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static bool serialAuxMap(jlSerialDeviceE device, uint8_t unit);
|
||||
static int16_t serialBaudCode(uint32_t baud);
|
||||
static int32_t serialRsconf(int16_t baud, int16_t flow, int16_t ucr);
|
||||
static long serialRxBufferInstall(void);
|
||||
|
|
@ -87,6 +107,32 @@ static int16_t serialUcr(const jlSerialConfigT *cfg);
|
|||
|
||||
// ----- 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
|
||||
// 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.
|
||||
|
|
@ -233,6 +279,13 @@ void jlpSerialClose(void) {
|
|||
(void)serialRsconf(gSavedBaudCode, ST_RSCONF_NO_CHANGE, gSavedUcr);
|
||||
gSavedBaudCode = 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;
|
||||
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) {
|
||||
case JL_SERIAL_FLOW_RTSCTS:
|
||||
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
|
||||
// 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
|
||||
// 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
|
||||
// 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) {
|
||||
if (gAuxDevice != ST_BCONMAP_MODEM) {
|
||||
return;
|
||||
}
|
||||
if (on) {
|
||||
Offgibit(ST_GI_DTR_ON_MASK);
|
||||
} 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
|
||||
// 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) {
|
||||
if (gAuxDevice != ST_BCONMAP_MODEM) {
|
||||
return;
|
||||
}
|
||||
if (on) {
|
||||
Offgibit(ST_GI_RTS_ON_MASK);
|
||||
} else {
|
||||
|
|
@ -345,17 +408,15 @@ void jlpSerialSetRts(bool on) {
|
|||
|
||||
|
||||
uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
|
||||
uint16_t n;
|
||||
SerialTxWaitT wait;
|
||||
uint16_t n;
|
||||
|
||||
for (n = 0u; n < len; n++) {
|
||||
uint32_t guard;
|
||||
|
||||
// Bounded transmitter-ready spin (Bcostat -1 = ready) so a stalled
|
||||
// A bounded transmitter-ready wait (Bcostat -1 = ready), so a stalled
|
||||
// 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)) {
|
||||
guard++;
|
||||
if (guard > ST_TX_WAIT_POLLS) {
|
||||
if (serialTxStalled(&wait)) {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
81
src/atarist/stAltLayer.c
Normal file
81
src/atarist/stAltLayer.c
Normal 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
21
src/atarist/stAltLayer.h
Normal 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
|
||||
|
|
@ -12,13 +12,24 @@
|
|||
| 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.
|
||||
|
|
||||
| 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
|
||||
| gcc driver.
|
||||
|
||||
.text
|
||||
.globl _stTickIsr
|
||||
|
||||
.equ SR_ABOVE_TIMER_C, 0x2500 | supervisor, interrupt mask 5
|
||||
|
||||
_stTickIsr:
|
||||
move.w #SR_ABOVE_TIMER_C,%sr
|
||||
movem.l %d0-%d1/%a0-%a1,-(%sp)
|
||||
jsr _stTickRun
|
||||
movem.l (%sp)+,%d0-%d1/%a0-%a1
|
||||
|
|
|
|||
|
|
@ -59,9 +59,9 @@ extern void stSpriteDrawShiftWalker(void);
|
|||
#define ASSET_PALETTE_OFFSET 12
|
||||
#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_PATH_MAX 256
|
||||
|
||||
// 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
|
||||
|
|
@ -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
|
||||
// loader and example routes file opens through this, so callers never
|
||||
// hard-code "DATA/" and the runtime tree layout stays consistent across
|
||||
// ports. Returns NULL if the joined path overflows DATA_PATH_MAX or the
|
||||
// file cannot be opened. A port with run-time roots (fileRoot.h) puts
|
||||
// DATA/ under its read root.
|
||||
// ports. Returns NULL if the joined path overflows FILE_PATH_MAX or the
|
||||
// file cannot be opened.
|
||||
FILE *jlDataOpen(const char *name, const char *mode) {
|
||||
char path[DATA_PATH_MAX];
|
||||
#if defined(JL_HAS_FILE_ROOTS)
|
||||
char path[FILE_PATH_MAX];
|
||||
|
||||
if (name == NULL || mode == NULL || !fileRootJoin(path, sizeof(path), jlpReadRoot(), DATA_DIR_PREFIX, name)) {
|
||||
return NULL;
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
// layer's included.
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "joey/audio.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
|
||||
|
||||
|
||||
// ----- 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) -----
|
||||
|
||||
void audioInit(bool wanted) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,20 @@
|
|||
|
||||
#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
|
||||
// >> 15 on a possibly-negative value) requires a >= 32-bit int with
|
||||
// arithmetic right shift. DOS (DJGPP) and ST (m68k gcc) both satisfy
|
||||
|
|
@ -19,110 +33,81 @@
|
|||
#error "audioSfxMix requires >= 32-bit int (DOS/ST only); IIgs must use NTPstreamsound"
|
||||
#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).
|
||||
static int interp(int8_t s0, int8_t s1, uint16_t frac);
|
||||
#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 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);
|
||||
|
||||
|
||||
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;
|
||||
int sample;
|
||||
int mixed;
|
||||
int8_t s0;
|
||||
int8_t s1;
|
||||
uint8_t s;
|
||||
|
||||
for (s = 0; s < numSlots; s++) {
|
||||
slot = &slots[s];
|
||||
if (!slot->active) {
|
||||
if (slots[s].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;
|
||||
}
|
||||
// Split the 16.16 step once per slot: the inner loops advance
|
||||
// a uint32_t sample index + uint16_t fraction with a carry
|
||||
// compare instead of 64-bit fixed-point math per sample.
|
||||
stepIdx = slot->stepFp >> 16;
|
||||
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;
|
||||
}
|
||||
if (pans[s] == JL_AUDIO_PAN_LEFT) {
|
||||
slotMix(&slots[s], dst, frames, RUN_SIDE, format);
|
||||
} else if (pans[s] == JL_AUDIO_PAN_RIGHT) {
|
||||
slotMix(&slots[s], dst + 1, frames, RUN_SIDE, format);
|
||||
} else {
|
||||
// Fixed buffer.
|
||||
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;
|
||||
}
|
||||
slotMix(&slots[s], dst, frames, RUN_BOTH, format);
|
||||
}
|
||||
slot->posIdx = idx;
|
||||
slot->posFrac = frac;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void audioSfxSetShift(uint8_t shift) {
|
||||
gShift = shift;
|
||||
}
|
||||
|
||||
|
||||
void audioSfxSlotArm(AudioSfxSlotT *slot, const uint8_t *sample, uint32_t length, uint16_t rateHz, uint32_t outputRate) {
|
||||
if (rateHz == 0 || outputRate == 0) {
|
||||
return;
|
||||
|
|
@ -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.
|
||||
// `frac` is the 16-bit fractional part of the read position; 0 =
|
||||
// exactly at s0, 0xFFFF = nearly s1. Result is a signed int in
|
||||
// [-128, +127].
|
||||
//
|
||||
// Formulated as a 16x16->32 signed multiply with explicit int16_t
|
||||
// locals so m68k gcc matches its mulhisi3 pattern and emits a single
|
||||
// muls.w instead of a __mulsi3 library call (~250-350 cycles per
|
||||
// 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).
|
||||
// [-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
|
||||
// 1 LSB of 8-bit audio from the exact delta * frac >> 16, inaudible. The
|
||||
// >> 15 of a possibly-negative int32_t relies on arithmetic shift, which
|
||||
// holds on every target this file builds for.
|
||||
static int interp(int8_t s0, int8_t s1, uint16_t frac) {
|
||||
int16_t delta;
|
||||
int16_t fracHalf;
|
||||
|
|
@ -180,70 +161,169 @@ static int interp(int8_t s0, int8_t s1, uint16_t frac) {
|
|||
|
||||
delta = (int16_t)(s1 - s0);
|
||||
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;
|
||||
#endif
|
||||
product = (int32_t)delta * fracHalf;
|
||||
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
|
||||
// sample of the previous chunk at streamBuf[0] so linear interp can
|
||||
// step across the chunk boundary without a discontinuity click. The
|
||||
// new chunk fills streamBuf[1..]. On a real refill (a previous chunk
|
||||
// existed) the position resets to 0 so the first read interpolates the
|
||||
// carried old-last sample at streamBuf[0] into the new chunk's first
|
||||
// sample at streamBuf[1], bridging the boundary. On the very first
|
||||
// prime there is no real previous sample, so the position resets to
|
||||
// "after byte 0" and the first read starts at the new chunk's first
|
||||
// sample (index 1).
|
||||
// existed) the position carries on where it ran out: as far past the
|
||||
// old last sample, now streamBuf[0], as it had got, fraction and all,
|
||||
// so the first read interpolates across the boundary and no time is
|
||||
// lost or played twice. A step that carries past a chunk this short
|
||||
// takes the next one too. On the very first prime there is no real
|
||||
// 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
|
||||
// has ended (caller should mark the slot inactive).
|
||||
static bool streamRefill(AudioSfxSlotT *slot) {
|
||||
int8_t lastSample;
|
||||
uint32_t got;
|
||||
bool haveCarry;
|
||||
uint16_t oldLen;
|
||||
|
||||
if (slot->streamEof) {
|
||||
return false;
|
||||
}
|
||||
haveCarry = (slot->streamLen > 0u);
|
||||
lastSample = haveCarry ? slot->streamBuf[slot->streamLen - 1u] : (int8_t)0;
|
||||
// Clamp the callback's reported count to the request size so a
|
||||
// non-conforming fill() that returns more than asked cannot make
|
||||
// the mixer read past streamBuf. (Memory safety still requires
|
||||
// fill() to honor count -- an over-returning callback has already
|
||||
// over-written streamBuf+1; this caps only the subsequent reads.)
|
||||
got = slot->fill(slot->fillCtx, slot->streamBuf + 1, AUDIO_SFX_STREAM_BUF - 1u);
|
||||
if (got > (uint32_t)(AUDIO_SFX_STREAM_BUF - 1u)) {
|
||||
got = AUDIO_SFX_STREAM_BUF - 1u;
|
||||
}
|
||||
if (got == 0u) {
|
||||
slot->streamEof = true;
|
||||
slot->streamLen = 0u;
|
||||
return false;
|
||||
}
|
||||
slot->streamBuf[0] = lastSample;
|
||||
slot->streamLen = (uint16_t)(got + 1u);
|
||||
if (haveCarry) {
|
||||
// Bridge the chunk boundary: start at index 0 so the first
|
||||
// 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 {
|
||||
// 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->posFrac = 0u;
|
||||
}
|
||||
do {
|
||||
if (slot->streamEof) {
|
||||
return false;
|
||||
}
|
||||
oldLen = slot->streamLen;
|
||||
lastSample = oldLen > 0u ? slot->streamBuf[oldLen - 1u] : (int8_t)0;
|
||||
// Clamp the callback's reported count to the request size so a
|
||||
// non-conforming fill() that returns more than asked cannot make
|
||||
// the mixer read past streamBuf. (Memory safety still requires
|
||||
// fill() to honor count -- an over-returning callback has already
|
||||
// over-written streamBuf+1; this caps only the subsequent reads.)
|
||||
got = slot->fill(slot->fillCtx, slot->streamBuf + 1, AUDIO_SFX_STREAM_BUF - 1u);
|
||||
if (got > (uint32_t)(AUDIO_SFX_STREAM_BUF - 1u)) {
|
||||
got = AUDIO_SFX_STREAM_BUF - 1u;
|
||||
}
|
||||
if (got == 0u) {
|
||||
slot->streamEof = true;
|
||||
slot->streamLen = 0u;
|
||||
return false;
|
||||
}
|
||||
slot->streamBuf[0] = lastSample;
|
||||
slot->streamLen = (uint16_t)(got + 1u);
|
||||
if (oldLen > 0u) {
|
||||
slot->posIdx = slot->posIdx - (oldLen - 1u);
|
||||
} else {
|
||||
slot->posIdx = 1u;
|
||||
slot->posFrac = 0u;
|
||||
}
|
||||
} while (slot->posIdx >= slot->streamLen);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,12 @@ typedef struct {
|
|||
bool streamEof; // last fill returned 0
|
||||
} 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.
|
||||
// `outputRate` is the HAL's mixer output rate; the helper computes
|
||||
// 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.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ bool codegenArenaInit(uint32_t totalBytes) {
|
|||
while (want > 0u && gBankCount < CODEGEN_ARENA_BANKS_MAX) {
|
||||
uint32_t bytes = (want > CODEGEN_ARENA_IIGS_BANK) ? CODEGEN_ARENA_IIGS_BANK : want;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -317,11 +317,17 @@ bool codegenArenaInit(uint32_t totalBytes) {
|
|||
want = (want > gBankSize[gBankCount - 1u]) ? want - gBankSize[gBankCount - 1u] : 0u;
|
||||
}
|
||||
#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;
|
||||
if (bankAlloc(0u, want)) {
|
||||
gTotalBytes = want;
|
||||
gBankCount = 1u;
|
||||
while (!bankAlloc(0u, want)) {
|
||||
if (want <= CODEGEN_ARENA_MIN_BYTES) {
|
||||
return false;
|
||||
}
|
||||
want = want / 2u;
|
||||
}
|
||||
gTotalBytes = want;
|
||||
gBankCount = 1u;
|
||||
#endif
|
||||
return gBankCount != 0u;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,16 +44,20 @@ typedef struct ArenaSlotT {
|
|||
} ArenaSlotT;
|
||||
|
||||
// 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_BANK_SHIFT 24u
|
||||
#define CODEGEN_ARENA_BANK_MASK 0x00FFFFFFul
|
||||
#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
|
||||
// allocated; a request the machine cannot fully satisfy yields as many
|
||||
// banks as it can (codegenArenaBytesTotal tells). Idempotent: calling
|
||||
// Init twice without an intervening Shutdown is a no-op.
|
||||
// Lifecycle. Returns false if totalBytes is 0 or no memory at all could be
|
||||
// had; a request the machine cannot fully satisfy yields what it can
|
||||
// (codegenArenaBytesTotal tells), on every port. Idempotent: calling Init
|
||||
// twice without an intervening Shutdown is a no-op.
|
||||
bool codegenArenaInit(uint32_t totalBytes);
|
||||
void codegenArenaShutdown(void);
|
||||
|
||||
|
|
|
|||
131
src/core/debug.c
131
src/core/debug.c
|
|
@ -26,6 +26,10 @@
|
|||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
|
||||
#include "joey/platform.h"
|
||||
#include "joey/debug.h"
|
||||
#include "fileRoot.h"
|
||||
|
|
@ -33,6 +37,10 @@
|
|||
|
||||
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
|
||||
// Supplied by the port. Declared ahead of the platform split because BOTH log
|
||||
// implementations below tee through it.
|
||||
|
|
@ -98,7 +106,7 @@ void jlLog(const char *msg) {
|
|||
|
||||
|
||||
void jlLogF(const char *fmt, ...) {
|
||||
char line[200];
|
||||
char line[LOG_LINE_BYTES];
|
||||
va_list args;
|
||||
|
||||
if (fmt == NULL) {
|
||||
|
|
@ -121,6 +129,7 @@ void jlLogF(const char *fmt, ...) {
|
|||
// below).
|
||||
void jlLogFlush(void) {
|
||||
FILE *fp;
|
||||
char path[FILE_PATH_MAX];
|
||||
uint16_t head;
|
||||
uint16_t start;
|
||||
uint16_t count;
|
||||
|
|
@ -131,7 +140,10 @@ void jlLogFlush(void) {
|
|||
if (head == 0u && gJoeyLogRingFull == 0u) {
|
||||
return;
|
||||
}
|
||||
fp = fopen(kLogPath, "w");
|
||||
if (!fileRootJoin(path, sizeof(path), jlpWriteRoot(), kLogPath, "")) {
|
||||
return;
|
||||
}
|
||||
fp = fopen(path, "w");
|
||||
if (fp == NULL) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -168,31 +180,21 @@ void jlLogReset(void) {
|
|||
#else // not JOEYLIB_PLATFORM_IIGS
|
||||
|
||||
static FILE *gLogFp = NULL;
|
||||
#if defined(JL_HAS_FILE_ROOTS)
|
||||
// The log FILE belongs to the process hosting the program and can outlive
|
||||
// it (a program ended by force never reaches jlLogClose), so it keeps the
|
||||
// host's own buffer rather than one inside the program. (DVX does not even
|
||||
// offer its programs setvbuf.) jlLog/jlLogF still flush once per line.
|
||||
#define LOG_PATH_MAX 256
|
||||
#else
|
||||
/* 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];
|
||||
#if !defined(JOEYLIB_PLATFORM_DVX)
|
||||
// A buffer that holds a whole line and its newline, so each line reaches
|
||||
// the file in one write (logLine flushes per line). DVX's log FILE belongs
|
||||
// to the desktop's process and can outlive the program (one ended by force
|
||||
// never reaches jlLogClose), so it keeps the host's own buffer -- DVX does
|
||||
// not even offer its programs setvbuf.
|
||||
static char gLogBuf[LOG_LINE_BYTES * 2u];
|
||||
#endif
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static void logClose(void);
|
||||
static FILE *logFile(void);
|
||||
#if defined(JL_HAS_FILE_ROOTS)
|
||||
static void logLine(const char *line);
|
||||
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) {
|
||||
|
|
@ -207,7 +209,7 @@ static void logClose(void) {
|
|||
static FILE *logFile(void) {
|
||||
if (gLogFp == NULL) {
|
||||
gLogFp = logOpen("a");
|
||||
#if !defined(JL_HAS_FILE_ROOTS)
|
||||
#if !defined(JOEYLIB_PLATFORM_DVX)
|
||||
if (gLogFp != NULL) {
|
||||
(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).
|
||||
static FILE *logOpen(const char *mode) {
|
||||
char path[LOG_PATH_MAX];
|
||||
char path[FILE_PATH_MAX];
|
||||
|
||||
if (!fileRootJoin(path, sizeof(path), jlpWriteRoot(), kLogPath, "")) {
|
||||
return NULL;
|
||||
}
|
||||
return fopen(path, mode);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
void jlLog(const char *msg) {
|
||||
FILE *fp;
|
||||
if (msg == NULL) {
|
||||
return;
|
||||
}
|
||||
#ifdef JOEY_LOG_SERIAL_TEE
|
||||
jlpLogTee(msg);
|
||||
jlpLogTee("\n");
|
||||
#endif
|
||||
fp = logFile();
|
||||
if (fp == NULL) {
|
||||
return;
|
||||
}
|
||||
fputs(msg, fp);
|
||||
fputc('\n', fp);
|
||||
fflush(fp);
|
||||
logLine(msg);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -257,42 +268,16 @@ void jlLogClose(void) {
|
|||
|
||||
|
||||
void jlLogF(const char *fmt, ...) {
|
||||
FILE *fp;
|
||||
char line[LOG_LINE_BYTES];
|
||||
va_list args;
|
||||
|
||||
if (fmt == NULL) {
|
||||
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);
|
||||
(void)vsnprintf(line, sizeof(line), fmt, args);
|
||||
va_end(args);
|
||||
jlpLogTee(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);
|
||||
(void)vsnprintf(line, sizeof(line), fmt, args);
|
||||
va_end(args);
|
||||
fputc('\n', fp);
|
||||
fflush(fp);
|
||||
#endif
|
||||
logLine(line);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -304,14 +289,14 @@ void jlLogFlush(void) {
|
|||
|
||||
|
||||
void jlLogReset(void) {
|
||||
FILE *fp;
|
||||
|
||||
logClose();
|
||||
/* Truncate by opening for write then closing; subsequent
|
||||
* jlLog* will reopen for append. */
|
||||
{
|
||||
FILE *fp = logOpen("w");
|
||||
if (fp != NULL) {
|
||||
fclose(fp);
|
||||
}
|
||||
// Truncate by opening for write then closing; the next jlLog opens it
|
||||
// for append again.
|
||||
fp = logOpen("w");
|
||||
if (fp != NULL) {
|
||||
fclose(fp);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,29 +2,31 @@
|
|||
// See LICENSE.md for license information.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Run-time file roots (JL_HAS_FILE_ROOTS).
|
||||
//
|
||||
// On the machine ports the core opens DATA/, SAVES/ and joeylog.txt by those
|
||||
// relative names (literals in assetLoad.c, save.c and debug.c), so they
|
||||
// resolve against the current directory: the program's own folder when its
|
||||
// desktop starts it (the IIgs roots SAVES/ at its application prefix
|
||||
// instead, JL_SAVE_DIR in port.h). A port
|
||||
// whose programs share one current directory with other programs and end
|
||||
// without ending the process (DVX: each program is a module loaded into the
|
||||
// desktop) defines JL_HAS_FILE_ROOTS in platform.h and supplies the folders
|
||||
// they live in. Without the flag this header declares nothing and the core
|
||||
// keeps its literals, so no other port's code changes.
|
||||
// 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
|
||||
// program's own folder (JL_HAS_FILE_ROOTS) supplies it, so a program finds
|
||||
// its files wherever it was started from, before jlInit as after: DOS from
|
||||
// the path DOS started it by, the Amiga as PROGDIR:, the X68000 from its
|
||||
// process block, the IIgs as GS/OS prefix 1, the hosted ports from their
|
||||
// executable's path. DVX keeps SAVES/ and the log apart, in the program's
|
||||
// settings folder. Without the flag both roots are "": the current
|
||||
// directory (the ST, whose GEMDOS tells a program nothing of where it
|
||||
// was started from).
|
||||
#ifndef JOEYLIB_FILE_ROOT_H
|
||||
#define JOEYLIB_FILE_ROOT_H
|
||||
|
||||
#include "joey/platform.h"
|
||||
|
||||
#if defined(JL_HAS_FILE_ROOTS)
|
||||
|
||||
#include <stdbool.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
|
||||
// from before the program's main runs until it ends.
|
||||
// jlpReadRoot holds DATA/, the program's own read-only files
|
||||
|
|
@ -32,21 +34,34 @@
|
|||
// the first time it is asked for it
|
||||
const char *jlpReadRoot(void);
|
||||
const char *jlpWriteRoot(void);
|
||||
|
||||
// Close joeylog.txt (debug.c). The port calls it when its program ends:
|
||||
// the file handle belongs to the host process, which lives on. A later
|
||||
// jlLog opens the file again, appending.
|
||||
// Close joeylog.txt (debug.c). A port whose program ends without its process
|
||||
// ending calls it then (DVX: the file handle belongs to the desktop's
|
||||
// process, which lives on). A later jlLog opens the file again, appending.
|
||||
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) {
|
||||
int written;
|
||||
size_t rootLen;
|
||||
size_t relLen;
|
||||
size_t nameLen;
|
||||
|
||||
written = snprintf(out, cap, "%s%s%s", root, rel, name);
|
||||
return written >= 0 && (size_t)written < cap;
|
||||
rootLen = strlen(root);
|
||||
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
|
||||
|
|
|
|||
|
|
@ -39,10 +39,16 @@
|
|||
// sprite rather than failing when it still runs out. These ports
|
||||
// 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.
|
||||
//
|
||||
// The ports that interpret every sprite (JL_SPRITE_INTERPRETED) have no
|
||||
// arena at all.
|
||||
#if defined(JOEYLIB_PLATFORM_IIGS)
|
||||
#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)
|
||||
#else
|
||||
#define DEFAULT_CODEGEN_BYTES (32UL * 1024UL)
|
||||
|
|
@ -59,9 +65,10 @@ static void clearError(void);
|
|||
// ----- Module state -----
|
||||
|
||||
static jlConfigT gConfig;
|
||||
static bool gInitialized = false;
|
||||
static const char *gLastError = NULL;
|
||||
static uint32_t gMillisBase = 0u; // the port's millisecond count at jlInit
|
||||
static bool gInitialized = false;
|
||||
static const char *gLastError = NULL;
|
||||
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) -----
|
||||
|
||||
|
|
@ -141,12 +148,14 @@ bool jlInit(const jlConfigT *config) {
|
|||
return false;
|
||||
}
|
||||
|
||||
#if !defined(JL_SPRITE_INTERPRETED)
|
||||
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();
|
||||
jlpShutdown();
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// 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
|
||||
|
|
@ -158,6 +167,14 @@ bool jlInit(const jlConfigT *config) {
|
|||
// audio calls are then silent.
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,13 +94,18 @@
|
|||
#define SAMPLE_VALUES 256u
|
||||
#define VOLUME_SHIFT 6u // a sample times a volume of 64 is the sample
|
||||
#define MIX_CHUNK 256u // output samples mixed at a time
|
||||
// 8-bit output: each channel's sample times its volume, over 64, and a
|
||||
// quarter of that, added into the output: a channel at full volume swings a
|
||||
// quarter of the range, as on the Amiga (two channels a side) and as
|
||||
// libxmp-lite mixed these ports' modules before. A quarter of each is -32 to
|
||||
// 31, so the four together, -128 to 124 on the silence byte, never wrap.
|
||||
#define U8_SHIFT 2u
|
||||
// 8-bit output: each channel's sample times its volume, over 64, shifted down
|
||||
// gU8Shift bits (modPlayerSetU8Shift), added into the output. At
|
||||
// U8_SHIFT_QUARTER, the default, a channel at full volume swings a quarter of
|
||||
// the range, -32 to 31, so the four together, -128 to 124 on the silence
|
||||
// byte, never wrap. At U8_SHIFT_HALF a channel swings half: a side's two
|
||||
// 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_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,
|
||||
// with room for a step past it.
|
||||
#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 vibratoNc(ChannelT *c, PaulaT *a, uint8_t amp, uint8_t spd);
|
||||
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 voiceSkip(PaulaT *a, uint32_t count);
|
||||
static uint32_t voiceStep(PaulaT *a);
|
||||
|
|
@ -357,10 +363,16 @@ static const uint8_t kArpeggio[ROW_TICKS_MAX] = {
|
|||
// ----- Module state -----
|
||||
|
||||
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 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 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
|
||||
|
|
@ -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 f;
|
||||
uint8_t i;
|
||||
|
||||
while (count > 0u) {
|
||||
if (!renderAdvance(&run, count)) {
|
||||
memset(dst, MOD_PLAYER_SILENCE_U8, count);
|
||||
while (frames > 0u) {
|
||||
if (!renderAdvance(&run, frames)) {
|
||||
memset(dst, MOD_PLAYER_SILENCE_U8, frames * channels);
|
||||
return false;
|
||||
}
|
||||
memset(dst, MOD_PLAYER_SILENCE_U8, run);
|
||||
for (i = 0u; i < MOD_CHANNELS; i++) {
|
||||
voiceMixU8(&gPlay.paula[i], dst, run);
|
||||
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);
|
||||
for (i = 0u; i < MOD_CHANNELS; i++) {
|
||||
voiceMixU8(&gPlay.paula[i], dst, run);
|
||||
}
|
||||
dst += run;
|
||||
}
|
||||
dst += run;
|
||||
count -= run;
|
||||
frames -= run;
|
||||
}
|
||||
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) {
|
||||
const uint8_t *info;
|
||||
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
|
||||
// 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) {
|
||||
|
|
@ -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:
|
||||
// a quarter of the sample at volume 64. Built by sums, so the 68000 needs no
|
||||
// multiplies for it.
|
||||
// gVolTab[v][s] is the signed sample s (as its byte) times volume v, over 64
|
||||
// << gU8Shift: the sample shifted down gU8Shift bits at volume 64. Built by
|
||||
// sums, so the 68000 needs no multiplies for it. And gU8Clamp, the sides'
|
||||
// sum.
|
||||
static void volTabBuild(void) {
|
||||
uint16_t volume;
|
||||
uint16_t s;
|
||||
int16_t product;
|
||||
int16_t sum;
|
||||
|
||||
for (volume = 0u; volume <= VOLUME_MAX; volume++) {
|
||||
// From sample -128 up: product steps by volume.
|
||||
product = (int16_t)(-(int16_t)U8_BIAS * (int16_t)volume);
|
||||
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);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,16 +20,26 @@
|
|||
// a song that does not loop, its end.
|
||||
bool modPlayerActive(void);
|
||||
|
||||
// Mono unsigned 8-bit, centred on MOD_PLAYER_SILENCE_U8: the DOS, ST and
|
||||
// X68000 ports' format. 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 count);
|
||||
// Unsigned 8-bit, centred on MOD_PLAYER_SILENCE_U8: the DOS, ST and X68000
|
||||
// ports' format, mono when channels is 1, and interleaved when it is 2
|
||||
// (Paula's left and right channels; each channel at the level
|
||||
// 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
|
||||
// right channels, as the Amiga plays them), mono when it is 1: the hosted
|
||||
// ports' format. Returns false as modPlayerRenderU8 does.
|
||||
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
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -40,16 +40,15 @@
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
// Root the save tree at the APPLICATION directory via the GS/OS "1/" prefix
|
||||
// designator (the Loader sets prefix 1 to the app's directory at launch).
|
||||
// Unrooted paths resolve against prefix 0, which a Finder launch leaves
|
||||
// pointing away from (or nowhere near) the app's volume - measured: every
|
||||
// plain "SAVES/x" open failed under MAME GS/OS 6 while the same file opened
|
||||
// fine through "1/SAVES/x". libc's fopen passes the string verbatim to
|
||||
// GS/OS Open, which accepts prefix-designator syntax.
|
||||
#define JL_SAVE_DIR "1/SAVES"
|
||||
#define JL_SAVE_DIR_PREFIX "1/SAVES/"
|
||||
#define JL_SAVE_PREFIX_NUM 1u // the GS/OS prefix those name: jlpDiskFree's volume
|
||||
// The file roots (fileRoot.h) are the APPLICATION directory, through the
|
||||
// GS/OS "1/" prefix designator (the Loader sets prefix 1 to the app's
|
||||
// directory at launch). Unrooted paths resolve against prefix 0, which a
|
||||
// Finder launch leaves pointing away from (or nowhere near) the app's
|
||||
// volume - measured: every plain "SAVES/x" open failed under MAME GS/OS 6
|
||||
// while the same file opened fine through "1/SAVES/x". libc's fopen passes
|
||||
// the string verbatim to GS/OS Open, which accepts prefix-designator syntax.
|
||||
#define IIGS_FILE_ROOT "1/"
|
||||
#define JL_SAVE_PREFIX_NUM 1u // the GS/OS prefix that names: jlpDiskFree's volume
|
||||
|
||||
// =====================================================================
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
#if !defined(jlpAudioInit)
|
||||
#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))
|
||||
#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)
|
||||
#define jlpAudioSfxRawFormat() 0u
|
||||
#endif
|
||||
|
|
@ -1096,6 +1108,18 @@ void jlpGenericSerialSetRts(bool on);
|
|||
#define jlpSerialSetRts(_o) jlpGenericSerialSetRts((_o))
|
||||
#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) ---
|
||||
|
|
@ -1125,6 +1149,14 @@ uint32_t jlpGenericDiskFree(void);
|
|||
#define jlpDiskFree() jlpGenericDiskFree()
|
||||
#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) ---
|
||||
|
|
|
|||
179
src/core/psgSynth.c
Normal file
179
src/core/psgSynth.c
Normal 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
48
src/core/psgSynth.h
Normal 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
|
||||
110
src/core/save.c
110
src/core/save.c
|
|
@ -5,11 +5,14 @@
|
|||
// Writable save files (SAVES/) and a disk-space query.
|
||||
//
|
||||
// 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/
|
||||
// on demand before a write open. Blob read/write/exists are plain stdio over
|
||||
// jlSaveOpen; only the two operations stdio cannot do portably -- create the
|
||||
// directory and delete a file -- plus the disk-free query are per-port HAL
|
||||
// hooks (jlpSaveDirEnsure / jlpSaveDelete / jlpDiskFree).
|
||||
// the writable SAVES/ tree, under the port's write root (fileRoot.h), instead
|
||||
// of read-only DATA/, and it creates SAVES/ on demand before a write open.
|
||||
// Blob read/exists are plain stdio over jlSaveOpen; jlSaveWrite writes a
|
||||
// temporary file and renames it over the save, so a write that fails or is
|
||||
// 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 <string.h>
|
||||
|
|
@ -19,21 +22,16 @@
|
|||
#include "port.h"
|
||||
|
||||
|
||||
// A port that must root the save tree elsewhere overrides these in port.h
|
||||
// (IIgs: GS/OS prefix-designator "1/" = the application directory - unrooted
|
||||
// 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
|
||||
#define SAVE_DIR "SAVES"
|
||||
#define SAVE_DIR_PREFIX "SAVES/"
|
||||
// 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.
|
||||
#define SAVE_NAME_BASE_MAX 8u
|
||||
#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 -----
|
||||
|
|
@ -41,33 +39,18 @@
|
|||
static bool buildSavePath(char *out, size_t cap, const char *name);
|
||||
static bool charIsAlnum(char c);
|
||||
static bool charIsLetter(char c);
|
||||
#if defined(JL_HAS_FILE_ROOTS)
|
||||
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);
|
||||
|
||||
|
||||
// Join SAVE_DIR_PREFIX + name into `out` (under the write root on a port
|
||||
// with run-time roots, fileRoot.h). Returns false for a name outside the
|
||||
// portable rule (saveNameValid), on every port alike, or if it would
|
||||
// overflow.
|
||||
// Join SAVE_DIR_PREFIX + name under the write root into `out`. Returns
|
||||
// false for a name outside the portable rule (saveNameValid), on every
|
||||
// port alike, or if it would overflow.
|
||||
static bool buildSavePath(char *out, size_t cap, const char *name) {
|
||||
#if !defined(JL_HAS_FILE_ROOTS)
|
||||
int written;
|
||||
|
||||
#endif
|
||||
if (!saveNameValid(name)) {
|
||||
return false;
|
||||
}
|
||||
#if defined(JL_HAS_FILE_ROOTS)
|
||||
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, like the literal form above.
|
||||
// Create SAVES/ under the write root. Advisory: if the folder cannot be
|
||||
// made, the open that follows fails and its caller sees that.
|
||||
static void ensureSaveDir(void) {
|
||||
char dir[SAVE_PATH_MAX];
|
||||
char dir[FILE_PATH_MAX];
|
||||
|
||||
if (fileRootJoin(dir, sizeof(dir), jlpWriteRoot(), SAVE_DIR, "")) {
|
||||
(void)jlpSaveDirEnsure(dir);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// 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) {
|
||||
char path[SAVE_PATH_MAX];
|
||||
char path[FILE_PATH_MAX];
|
||||
|
||||
if (name == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (!buildSavePath(path, sizeof(path), name)) {
|
||||
if (name == NULL || !buildSavePath(path, sizeof(path), name)) {
|
||||
return false;
|
||||
}
|
||||
return jlpSaveDelete(path);
|
||||
|
|
@ -142,7 +121,7 @@ bool jlSaveDelete(const char *name) {
|
|||
|
||||
|
||||
bool jlSaveExists(const char *name) {
|
||||
FILE *fp;
|
||||
FILE *fp;
|
||||
|
||||
if (name == NULL) {
|
||||
return false;
|
||||
|
|
@ -157,27 +136,21 @@ bool jlSaveExists(const char *name) {
|
|||
|
||||
|
||||
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;
|
||||
}
|
||||
// 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) {
|
||||
ensureSaveDir();
|
||||
}
|
||||
if (!buildSavePath(path, sizeof(path), name)) {
|
||||
return NULL;
|
||||
}
|
||||
return fopen(path, mode);
|
||||
}
|
||||
|
||||
|
||||
uint32_t jlSaveRead(const char *name, void *buf, uint32_t max) {
|
||||
FILE *fp;
|
||||
size_t got;
|
||||
FILE *fp;
|
||||
size_t got;
|
||||
|
||||
if (name == NULL || buf == NULL || max == 0u) {
|
||||
return 0u;
|
||||
|
|
@ -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) {
|
||||
FILE *fp;
|
||||
size_t written;
|
||||
char path[FILE_PATH_MAX];
|
||||
char temp[FILE_PATH_MAX];
|
||||
FILE *fp;
|
||||
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)) {
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -209,6 +192,11 @@ bool jlSaveWrite(const char *name, const void *buf, uint32_t len) {
|
|||
if (buf != NULL && len > 0u) {
|
||||
written = fwrite(buf, 1u, (size_t)len, fp);
|
||||
}
|
||||
fclose(fp);
|
||||
return written == (size_t)len;
|
||||
ok = fclose(fp) == 0 && written == (size_t)len;
|
||||
if (!ok) {
|
||||
(void)jlpSaveDelete(temp);
|
||||
return false;
|
||||
}
|
||||
(void)jlpSaveDelete(path);
|
||||
return jlpSaveRename(temp, path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@
|
|||
#define SERIAL_STOP_BITS_DEF 1u
|
||||
#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 -----
|
||||
|
||||
|
|
@ -167,3 +173,30 @@ bool jlSerialWriteByte(uint8_t b) {
|
|||
static void serialAtExit(void) {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
* 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
|
||||
* 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
|
||||
#else
|
||||
#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) {
|
||||
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
|
||||
// the spriteMarkDirty call boundary (see the restore arm in
|
||||
// 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
|
||||
// without compiled sprites.
|
||||
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) {
|
||||
uint8_t shift;
|
||||
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);
|
||||
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).
|
||||
if (s == gStage && heightPx == 16) {
|
||||
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).
|
||||
#if !defined(JOEYLIB_PLATFORM_IIGS)
|
||||
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;
|
||||
|
||||
slot = sp->slot;
|
||||
|
|
@ -1041,7 +1041,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
|
|||
if (s == gStage && iigsSpriteRestoreStage(backup) == 0u) {
|
||||
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
|
||||
// stays INLINED (spriteCompiledRestoreUnder is always-inline,
|
||||
// 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).
|
||||
#if !defined(JOEYLIB_PLATFORM_IIGS)
|
||||
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;
|
||||
int16_t bx;
|
||||
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
|
||||
// SaveUnder call. RestoreUnder will refuse to operate on a
|
||||
// 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
|
||||
// these ports goes through the compiled 16-px-region path, so a
|
||||
// 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
|
||||
// jlSpriteSaveUnder runs, going straight to the compiled save. Same
|
||||
// trust contract as jlSpriteRestoreUnderTrusted (see joey/sprite.h).
|
||||
// Optimized on Amiga/ST (the compiled save is a read, so there is no
|
||||
// fused entry -- just skip validation, and set backup->sprite, which
|
||||
// their compiled save does not and jlSpriteRestoreUnder* reads) and on
|
||||
// the IIgs (the stage save entry, which fills all the backup's
|
||||
// metadata); falls back to the fully-validated jlSpriteSaveUnder for
|
||||
// uncompiled sprites, other destinations on the IIgs, and on DOS /
|
||||
// generic.
|
||||
// Optimized on the 68000 ports and x86 (the compiled save is a read, so
|
||||
// there is no fused entry -- just skip validation, and set
|
||||
// backup->sprite, which jlSpriteRestoreUnder* reads) and on the IIgs (the
|
||||
// stage save entry, which fills all the backup's metadata); falls back to
|
||||
// the fully-validated jlSpriteSaveUnder for uncompiled sprites, other
|
||||
// destinations on the IIgs, and on the interpreting ports.
|
||||
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;
|
||||
|
||||
slot = sp->slot;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@
|
|||
#include "codegenArenaInternal.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_SAVE 1
|
||||
#define SPRITE_OP_RESTORE 2
|
||||
|
|
@ -83,7 +94,7 @@
|
|||
// backup->bytes, so an odd caller buffer must route BOTH ops to the
|
||||
// byte-safe interpreted paths (pointer parity is stable between save
|
||||
// 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))
|
||||
// NOTE (P7-1, 2026-07-11): an algebraically-reduced form of these two
|
||||
// macros (save class = (x&15)+((widthPx+15)&15) >= 16; restore class =
|
||||
|
|
|
|||
400
src/dos/audio.c
400
src/dos/audio.c
|
|
@ -2,23 +2,29 @@
|
|||
// See LICENSE.md for license information.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// DOS audio HAL: Sound Blaster Pro / SB16 8-bit mono playback driven
|
||||
// by 8237 auto-init DMA, fed by the shared module player
|
||||
// (src/core/modPlayer.c) and SFX overlay (src/core/audioSfxMix.c).
|
||||
// DOS audio HAL: Sound Blaster 8-bit playback driven by 8237 auto-init
|
||||
// DMA, fed by the shared module player (src/core/modPlayer.c) and SFX
|
||||
// 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:
|
||||
// * jlpAudioInit detects the card via DSP reset + version, allocates
|
||||
// a 4 KB conventional-memory DMA buffer (aligned to not cross a
|
||||
// 64 KB boundary), installs an IRQ handler on the SB's IRQ line,
|
||||
// and arms auto-init DMA at 22050 Hz mono 8-bit. The DMA buffer is
|
||||
// split into two halves; the SB IRQ fires at the end of each half
|
||||
// a conventional-memory DMA buffer (aligned to not cross a 64 KB
|
||||
// boundary), installs an IRQ handler on the SB's IRQ line, and arms
|
||||
// auto-init DMA. The DMA buffer is split into two halves of
|
||||
// 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.
|
||||
// * 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
|
||||
// same refills.
|
||||
// same way.
|
||||
//
|
||||
// Defaults match DOSBox-Staging's emulated SB Pro: base $220, IRQ 7,
|
||||
// 8-bit DMA channel 1. BLASTER environment variable overrides each.
|
||||
// Defaults match DOSBox-Staging's emulated Sound Blaster (an SB16): base
|
||||
// $220, IRQ 7, 8-bit DMA channel 1. BLASTER environment variable overrides
|
||||
// each.
|
||||
//
|
||||
// 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)
|
||||
|
|
@ -61,10 +67,43 @@
|
|||
#define DSP_OUT_8_AUTO 0x1C
|
||||
#define DSP_HALT_DMA8 0xD0
|
||||
#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_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) -----
|
||||
|
||||
#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
|
||||
// single source of truth handed to the module player and the SFX resampler.
|
||||
#define MIX_RATE 22050
|
||||
#define MIX_DMA_BUFFER 4096 // total; two 2048-byte halves
|
||||
#define MIX_HALF (MIX_DMA_BUFFER / 2)
|
||||
// The buffer is two halves of MIX_HALF_FRAMES frames each: bytes in mono,
|
||||
// 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
|
||||
// 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.
|
||||
#define SFX_START_MARGIN 64u
|
||||
#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
|
||||
// actually clocks (~22222 Hz for a 22050 Hz target) instead of drifting.
|
||||
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.
|
||||
static int gDmaSelector = 0;
|
||||
|
|
@ -137,9 +189,14 @@ static bool gSbIrqWasMasked = false; // the line's mask before t
|
|||
static bool gModTail = false;
|
||||
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
|
||||
// (src/core/audioSfxMix.c), run in refillHalf over the module's output.
|
||||
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.
|
||||
// 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.
|
||||
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 -----
|
||||
|
||||
static uint8_t adlibAttenTl(uint8_t atten);
|
||||
|
|
@ -162,6 +259,7 @@ static bool adlibReady(void);
|
|||
static void adlibShutdown(void);
|
||||
static void adlibSilence(void);
|
||||
static void adlibWrite(uint8_t reg, uint8_t val);
|
||||
static bool aheadOfDsp(AheadT *ahead);
|
||||
static bool allocDmaBuffer(void);
|
||||
static uint16_t dmaPlayOffset(void);
|
||||
static int dspRead(void);
|
||||
|
|
@ -170,10 +268,19 @@ static uint8_t dspTimeConst(uint16_t rate);
|
|||
static void dspWrite(uint8_t value);
|
||||
static void freeDmaBuffer(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 programDmaController(void);
|
||||
static void programDspAutoInit(void);
|
||||
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 sbIsr(void);
|
||||
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
|
||||
// boundary on 8-bit channels. We over-allocate to 2x size and use the
|
||||
// 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) {
|
||||
int sel;
|
||||
int seg;
|
||||
|
|
@ -310,7 +436,7 @@ static bool allocDmaBuffer(void) {
|
|||
uint32_t aligned;
|
||||
int paragraphs;
|
||||
|
||||
paragraphs = ((MIX_DMA_BUFFER * 2) + 15) >> 4;
|
||||
paragraphs = ((MIX_DMA_BUFFER_MAX * 2) + 15) >> 4;
|
||||
seg = __dpmi_allocate_dos_memory(paragraphs, &sel);
|
||||
if (seg < 0) {
|
||||
return false;
|
||||
|
|
@ -318,8 +444,8 @@ static bool allocDmaBuffer(void) {
|
|||
linear = (uint32_t)seg << 4;
|
||||
|
||||
// Round linear up to a starting offset that gives us
|
||||
// MIX_DMA_BUFFER without crossing the next 64 KB boundary.
|
||||
if (((linear & 0xFFFF) + MIX_DMA_BUFFER) > 0x10000) {
|
||||
// MIX_DMA_BUFFER_MAX without crossing the next 64 KB boundary.
|
||||
if (((linear & 0xFFFF) + MIX_DMA_BUFFER_MAX) > 0x10000) {
|
||||
aligned = (linear + 0xFFFFu) & ~0xFFFFu;
|
||||
} else {
|
||||
aligned = linear;
|
||||
|
|
@ -328,7 +454,7 @@ static bool allocDmaBuffer(void) {
|
|||
gDmaSelector = sel;
|
||||
gDmaLinear = 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;
|
||||
}
|
||||
|
||||
|
|
@ -350,7 +476,7 @@ static void programDmaController(void) {
|
|||
|
||||
page = (uint8_t)((gDmaLinear >> 16) & 0xFF);
|
||||
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_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) {
|
||||
uint16_t blockMinusOne;
|
||||
|
||||
blockMinusOne = (uint16_t)(gHalfBytes - 1u);
|
||||
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(dspTimeConst(MIX_RATE));
|
||||
|
||||
blockMinusOne = (uint16_t)(MIX_HALF - 1);
|
||||
dspWrite(DSP_BLOCK_SIZE);
|
||||
dspWrite((uint8_t)(blockMinusOne & 0xFF));
|
||||
dspWrite((uint8_t)((blockMinusOne >> 8) & 0xFF));
|
||||
|
||||
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.
|
||||
// Called from jlpAudioFrameTick on the main thread (NOT in IRQ
|
||||
// context); the IRQ only flags which halves need refilling.
|
||||
|
|
@ -413,16 +670,17 @@ static void haltDspDma(void) {
|
|||
static void refillHalf(uint8_t halfIdx) {
|
||||
uint8_t *dst;
|
||||
|
||||
dst = gDmaBuf + ((uint32_t)halfIdx * MIX_HALF);
|
||||
dst = gDmaBuf + ((uint32_t)halfIdx * gHalfBytes);
|
||||
if (modPlayerActive()) {
|
||||
if (!modPlayerRenderU8(dst, MIX_HALF)) {
|
||||
if (!modPlayerRenderU8(gModHalf[halfIdx], MIX_HALF_FRAMES, gChannels)) {
|
||||
gModTail = true;
|
||||
gModTailHalf = halfIdx;
|
||||
}
|
||||
} 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
|
||||
// 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
|
||||
|
|
@ -452,25 +721,16 @@ static void sbIsr(void) {
|
|||
// has been refilled already, the half after it -- and the refills carry on
|
||||
// from where that leaves the slot.
|
||||
static void sfxStartNow(uint8_t slot) {
|
||||
uint16_t pos;
|
||||
uint16_t start;
|
||||
uint16_t halfEnd;
|
||||
uint8_t half;
|
||||
AheadT ahead;
|
||||
|
||||
pos = dmaPlayOffset();
|
||||
half = (uint8_t)(pos / MIX_HALF);
|
||||
if (half > 1u) {
|
||||
if (!aheadOfDsp(&ahead)) {
|
||||
return;
|
||||
}
|
||||
start = (uint16_t)(pos + SFX_START_MARGIN);
|
||||
halfEnd = (uint16_t)((half + 1u) * MIX_HALF);
|
||||
if (start < halfEnd) {
|
||||
audioSfxOverlayMix(gDmaBuf + start, (uint16_t)(halfEnd - start), &gSfxSlots[slot], 1u);
|
||||
if (ahead.start < ahead.halfEnd) {
|
||||
sfxOverlay(gDmaBuf + ahead.start, (uint16_t)((ahead.halfEnd - ahead.start) / gChannels), slot, 1u);
|
||||
}
|
||||
// The half after it is ahead of the DSP only if the interrupt for this
|
||||
// one has come (gNextHalf) and its refill is done.
|
||||
if (half == gNextHalf && !gNeedRefill[half ^ 1u]) {
|
||||
audioSfxOverlayMix(gDmaBuf + (uint32_t)(half ^ 1u) * MIX_HALF, MIX_HALF, &gSfxSlots[slot], 1u);
|
||||
if (ahead.nextMixed) {
|
||||
sfxOverlay(gDmaBuf + (uint32_t)(ahead.half ^ 1u) * gHalfBytes + ahead.otherFrom, (uint16_t)((gHalfBytes - ahead.otherFrom) / gChannels), slot, 1u);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -498,10 +758,16 @@ bool jlpAudioInit(void) {
|
|||
gDspMajor = (uint8_t)major;
|
||||
gDspMinor = (uint8_t)minor;
|
||||
|
||||
// Lock in the effective playback rate from the same time constant
|
||||
// the SB DAC will receive, so the module player and the SFX resampler
|
||||
// agree with the hardware instead of using the nominal MIX_RATE.
|
||||
gEffRate = (uint16_t)(SB_DSP_CLOCK_HZ / (256u - dspTimeConst(MIX_RATE)));
|
||||
// An SB16 plays stereo at MIX_RATE itself. Before it, lock in the
|
||||
// effective playback rate from the same time constant the SB DAC will
|
||||
// receive, so the module player and the SFX resampler agree with the
|
||||
// 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()) {
|
||||
return false;
|
||||
|
|
@ -535,12 +801,22 @@ bool jlpAudioInit(void) {
|
|||
programDspAutoInit();
|
||||
gSbIrqWasMasked = picIrqMasked(gSbIrq);
|
||||
picUnmaskIrq(gSbIrq);
|
||||
mixerBalance();
|
||||
|
||||
gDspPresent = 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) {
|
||||
int vector;
|
||||
|
||||
|
|
@ -566,6 +842,7 @@ void jlpAudioShutdown(void) {
|
|||
gModTail = false;
|
||||
freeDmaBuffer();
|
||||
(void)dspReset();
|
||||
mixerRestore();
|
||||
gDspPresent = false;
|
||||
}
|
||||
|
||||
|
|
@ -579,8 +856,14 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
|
|||
if (!gDspPresent) {
|
||||
return;
|
||||
}
|
||||
// A song still playing goes now, not over the new one's start.
|
||||
if (modPlayerActive()) {
|
||||
modRemoveAhead();
|
||||
}
|
||||
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_NOISE_MAX_HZ 6200u // pitch 0 tops out the bright end
|
||||
#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
|
||||
// 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
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
// ADLIB_NOISE_MIN_HZ. (Core clamps pitch to <= 31 before the HAL
|
||||
// sees it.)
|
||||
//
|
||||
// Evenly, because the shared curve every other port follows --
|
||||
// rate = 26000 - pitch * 750, from src/iigs/audio_full.c -- is
|
||||
// linear in pitch, so a linear spread here puts a given pitch at the
|
||||
// same place in the range as it sits on those machines. The absolute
|
||||
// rates cannot be matched: the OPL2's snare is a tuned drum voice
|
||||
// rather than a sampled noise source, and its band tops out near
|
||||
// 6 kHz where the curve asks for 26 kHz.
|
||||
// The rate every other port plays (AUDIO_NOISE_CLOCK_HZ / pitch) cannot
|
||||
// be matched: the OPL2's snare is a tuned drum voice rather than a
|
||||
// noise source, and its band tops out near 6 kHz where that rate runs
|
||||
// to 125 kHz. An even spread keeps every pitch distinct and in order,
|
||||
// bright to dark.
|
||||
//
|
||||
// It replaces MAX / (pitch + 1), a hyperbola that spent nearly all
|
||||
// its resolution on the first few pitches and squashed everything
|
||||
|
|
@ -1174,6 +1461,7 @@ void jlpAudioStopMod(void) {
|
|||
return;
|
||||
}
|
||||
modPlayerStop();
|
||||
modRemoveAhead();
|
||||
gModTail = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue