joeylib2/docs/Portability.adoc

1214 lines
63 KiB
Text

== Writing Portable Code
A JoeyLib program that follows the rules in this chapter compiles and behaves
the same on all five machines, on DVX, under SDL3, in a browser, and on Windows. Most of the rules exist because one
particular target cannot do something the others can, and the Apple IIgs is
usually that target.
=== The Rules, in Short
. Use `jlAlloc` for anything large, never `malloc`.
. Never assume a frame rate. Ask `jlFrameHz()` or use `jlMillisElapsed()`.
. Never assume colour 0 is anything but black.
. Compile your sprites before you draw them.
. Keep large objects out of function-local variables on the IIgs.
. Use `jlRandom`, not `rand`, when identical behaviour across ports matters.
. Keep file names short, uppercase-safe, and 8.3-compatible.
. Do not mix save-under with clean-buffer erasing in the same screen region.
. Pick one of the audio layers per scene, not two.
The rest of this chapter explains each one.
=== Memory
This is the rule that catches everyone. *The C heap on the IIgs is tiny.* The
whole heap is one Memory Manager block of 32 KB to 48 KB, and if that block
cannot be had at start-up it is only what the program's link left in its
bank, which can be a few hundred bytes. A program that `malloc`s a
32000-byte buffer works on every other target and can fail on the reference
one.
Use the library allocator, which is backed by each platform's native allocator
(the Memory Manager on the IIgs):
[source,c]
----
uint8_t *buffer = (uint8_t *)jlAlloc(32000);
if (buffer == NULL) {
// handle it -- this genuinely can fail on a stock machine
}
// ...
jlFree(buffer);
----
`jlAlloc` does *not* zero the memory it returns. Never free a `jlAlloc` block
with `free`, or a `malloc` block with `jlFree`: on most ports they are the
same, but on the IIgs a `jlAlloc` block is a Memory Manager handle, and mixing
them corrupts memory.
For bulk copies, `jlMemCopy` and `memcpy` are the same thing: `jlMemCopy` is
`memcpy` with a 32-bit count. On the IIgs both are `MVN` block moves at 7
cycles per byte, split correctly at bank boundaries.
==== Large Local Variables
On the IIgs, a large function-local variable in a deep call chain can silently
overflow the soft stack and crash -- roughly 2 KB is enough to do it, and the
trigger is call depth rather than anything the compiler warns about. Make big
buffers `static`, or allocate them with `jlAlloc`:
[source,c]
----
// DANGEROUS on the IIgs, especially deep in a call chain:
void renderScene(void) {
uint8_t scratch[2048];
// ...
}
// Safe everywhere:
void renderScene(void) {
static uint8_t scratch[2048];
// ...
}
----
=== Timing
Covered in <<_frame_rate_independence>>, and worth repeating because it is the
second most common portability bug: the targets run at 50, 55, 60, and 70 Hz.
Scale by `jlFrameHz()` or work from `jlMillisElapsed()`.
`jlFrameCount()` is a 16-bit counter that wraps roughly every 18 minutes at
60 Hz, so compare two readings by subtracting them.
=== Colour and the Palette
Colour 0 is black in every palette on every platform, and `jlPaletteSet`
silently rewrites entry 0 to `$000` no matter what you pass. Do not design
artwork that needs colour 0 to be something else -- you have 15 free colours per
palette, and 16 palettes to switch between per scanline.
Colour 0 is also the transparent colour for sprites. A sprite pixel of colour 0
is never drawn. If you need an opaque rectangle, use the tile operations, which
have both opaque and masked forms.
=== Sprites
Two rules.
*Compile before drawing.* `jlSpriteDraw` never compiles on demand. An
uncompiled sprite runs through the interpreter, two to six times slower
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 it crashes the trusted variants.
[source,c]
----
// Right: 2-byte aligned by construction.
static uint16_t gBacking[jlSpriteBackupLength(uint16_t, 4, 4)];
// Risky: may land on an odd address.
static uint8_t gBacking[jlSpriteBackupLength(uint8_t, 4, 4)];
----
Note also that the region a save-under actually captures is often not the
sprite rectangle: it can be the *16-pixel-aligned region* containing the
sprite (the Amiga, ST, and X68000), only the rows holding an opaque pixel (the
IIgs), or only the visible part of a sprite partly off the surface (every
port). `jlSpriteSaveUnder` reports the true geometry in the backup structure's
`x`, `y`, `width`, `height`, and `sizeBytes` fields. It is that recorded region
you must not overwrite before restoring. On the IIgs the buffer must not cross
a 64 KB bank boundary, which a `jlAlloc` block and the C heap never do.
=== Randomness
`rand()` differs wildly between C libraries, so a procedurally generated level
would not match across ports. `jlRandom` is xorshift32 and produces
*bit-identical* output on every platform for a given seed:
[source,c]
----
jlRandomSeed(12345);
uint16_t roll = jlRandomRange(6); // 0..5, same everywhere
----
A seed of 0 is remapped to 1, because xorshift cannot escape the all-zero
state. This generator is fine for games and useless for cryptography.
=== Files
Two directories sit beside the executable, and the library prefixes them for
you:
* `DATA/` -- read-only game content, opened with `jlDataOpen`
* `SAVES/` -- writable saves and preferences, via the `jlSave*` calls
Pass bare names: `jlDataOpen("levels/title.dat", "rb")`, never
`"DATA/levels/title.dat"`. On DVX the two are not in the current directory
at all (<<_dvx_programs>>), so a program that opens `+"DATA/..."+` itself finds
nothing there. DOS, the ST, the Amiga, and the X68000 look in the current
directory, so start a program there from its own folder; the IIgs looks for
`DATA/` under GS/OS prefix 0 but keeps `SAVES/` in the application's folder
(<<_files_and_saves>>).
Keep names portable across the retro filesystems this library targets: *at most
8 characters plus a short extension, letters and digits only, starting with a
letter*, so they satisfy both ProDOS and DOS 8.3. Nothing checks them for you.
Do not rely on case being preserved, and spell a name the same way every
time: on Linux, Haiku, BeOS, and the web, case matters.
A failed save is not fatal -- the calls report failure and your program should
degrade gracefully, because on some volumes `SAVES/` simply is not writable.
=== Audio
JoeyLib gives you three sound layers, and on some platforms they share physical
hardware:
[cols="2,5", options="header"]
|===
|Layer |What it is
|Module playback
|`jlAudioPlayMod` -- four-channel Protracker `.MOD`, mixed in software by
JoeyLib's module player on DOS, the ST, the X68000, and the hosted ports,
played by DVX's music library on DVX, and replayed by the sound hardware on
the Amiga and IIgs. The module player is the Amiga's replayer, PTPlayer,
translated to C, so those ports play a module as the Amiga does, at a PAL
Amiga's pitch, Protracker 2.3's quirks and all.
|Chip tracker
|`jlMusicPlay` -- a portable event stream driving three tone voices plus
noise, at register-write cost on the machines' sound chips (mixed in
software on DVX and the hosted ports).
|Voices and effects
|`jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx`, and the streaming forms.
|===
[WARNING]
====
On the Amiga the tone voices are the hardware the module player drives, and
while a `.MOD` is playing, `jlAudioVoice` writes fight the player's register
writes and lose. On the plain ST, digital sound takes the chip channel tone
voice 0 plays on, so voice 0 is silent while a module or effect plays. On the
IIgs the voices share sound effect slots 0 to 2 and the noise slot 3. Choose
the module layer or the voice and tracker layer *per scene*, not both at once.
====
The chip tracker is the portable choice for music. It costs almost nothing on
every port -- unlike `.MOD` playback, which needs real CPU mixing where the
sound hardware cannot replay a module. On a plain 8 MHz ST a four-channel
module takes about three quarters of the machine (measured under Hatari: a
third for the interrupt that plays each sample through the YM2149, the rest
the mix), leaving a quarter for the game; the STe's DMA sound needs no such
interrupt.
A `jlAudioVoice` or `jlAudioNoise` note sounds until the next call for it, as
long as `jlAudioFrameTick` runs every frame: the IIgs plays each note as a
short sample and restarts the held ones from there.
`jlInit` starts audio (`jlConfigT.noAudio` leaves it off) and `jlShutdown`
stops it. Sound that does not start is *not* fatal: `jlInit` still succeeds,
modules and sound effects become silent no-ops (the tone and noise voices
still play on the Amiga, the ST, DOS, and the X68000), so a program need not
treat missing sound as an error.
=== Per-Platform Behaviour Summary
The API is uniform; these are the places where what you get differs.
[cols="2,2,2,2,2,2,2,2,2", options="header"]
|===
|Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000 |DVX |SDL3, Web, macOS, Win32, PSP, Haiku, BeOS |Win16
|Frame rate
|60 Hz (50 on a PAL machine)
|50 Hz (PAL; 60 on an NTSC machine without PAL)
|50 or 60 Hz, as the machine is set
|70 Hz
|55 Hz (56 under MAME)
|70 Hz (virtual; the real retrace on the whole screen)
|60 Hz (virtual)
|60 Hz (virtual)
|Pixel storage
|chunky 4bpp
|planar
|planar, interleaved
|chunky 4bpp
|planar (shown through the text plane)
|chunky 4bpp
|chunky 4bpp
|chunky 4bpp
|Sprites
|compiled
|compiled
|compiled
|compiled
|compiled
|compiled
|interpreted
|compiled
|Stereo pan
|all three
|all three (centred on a free channel each side)
|ignored (mono); all three in the STe build
|all three on an SB16; ignored (mono) before it
|ignored
|all three
|all three
|ignored (mono)
|Program icon (<<_icons>>)
|the Finder's (`Icons` folder)
|Workbench's (`Name.info`)
|none
|none
|none
|Program Manager's
|the window's (SDL3), the page's (web), Finder's and the Dock's (macOS),
Explorer's (Win32), the menu's (PSP), Tracker's (Haiku, BeOS)
|Program Manager's
|Tone voices (`jlAudioTone` is voice 0)
|DOC oscillators on sound effect slots 0 to 2
|Paula channels 0 to 2
|YM2149 (voice 0 silent while digital sound plays)
|OPL2 (with none, voice 0 on the PC speaker)
|YM2151 (sine waves)
|DVX tones (FM chip or mixer)
|square waves in the mixer
|square waves in the mixer
|Joystick
|one, analog, auto-disables
|digital
|digital
|analog
|digital
|analog (DVX calibrates)
|SDL's gamepads and joysticks (SDL3, web, macOS); Windows' joysticks (Win32); the system's joysticks
(Haiku, BeOS); the PSP's own controls
|Windows' joysticks (Control Panel calibrates)
|Keyboard layout
|machine's own
|machine's own
|machine's own
|fixed US
|machine's own
|DVX's
|the OS's; on the PSP, a US one on the screen
|Windows'
|Serial ports
|modem, printer, slot card
|`serial.device`
|Modem port
|COM1..COM4
|RS-232C (needs RSDRV.SYS)
|COM1..COM4 (DVX's library)
|Win32: COM1 and up; SDL3 (Linux), macOS, Haiku, BeOS: terminal devices; web, PSP: none
|COM1..COM9
|===
The joystick row deserves a note: the IIgs port stops polling for a stick
after about a second (60 polls) with none detected, because polling for an
absent stick costs about 1 ms a frame there; `jlJoystickReset` starts it again
(<<_jljoystickreset>>).
On the X68000 both joystick ports always report a stick, as on the ST: an
empty port cannot be told from a stick at rest. `JL_JOY_BUTTON_0` is a stick's
trigger A and `JL_JOY_BUTTON_1` its trigger B. Holding both directions of an axis
reads as the positive one (right, down), as on the Amiga and ST.
On the ST, `JL_JOYSTICK_0` is port 1, the joystick port, and `JL_JOYSTICK_1` port 0.
JoeyLib keeps TOS's keyboard-processor mode, in which port 0 is the mouse's:
the ST reports no stick movement there, so `JL_JOYSTICK_1` stays at rest whatever
is plugged in. A stick there has one button, so a quick press is
`JL_JOY_BUTTON_0` and a held one `JL_JOY_BUTTON_1` (<<_jljoydown_jljoypressed_jljoyreleased>>).
Port 1's fire button is the same line as the mouse's right button, so a press
reads as both the stick's button and `JL_MOUSE_BUTTON_RIGHT`.
The 1040 STe also has two enhanced joystick ports, and the STe build reads an
Atari Jaguar pad in each. A pad in enhanced port A is `JL_JOYSTICK_0` whenever
it was used more recently than port 1's stick, so either one plays, and a pad
in enhanced port B is `JL_JOYSTICK_1`. A pad's A and B buttons are
`JL_JOY_BUTTON_0` and `JL_JOY_BUTTON_1`. The Mega STe has no enhanced ports.
On the Amiga the sticks are read through `lowlevel.library`, which Kickstart
3.1 does not have in ROM: AmigaOS loads it from `LIBS:`, so the boot drive must
carry Workbench's `Libs/lowlevel.library`, or no stick is seen. `joey-run` puts
the installer's copy there (component `amigaLowlevel`); a game shipped to boot
on its own must do the same. `lowlevel.library` reports a stick only once it
has moved, and never a mouse. It also checks each port again now and then, and
a two-button stick with both buttons held can read as nothing at that moment,
so once a port has shown a stick JoeyLib keeps it one; `jlJoystickReset` hands
the port back to the library's own detection. `lowlevel.library` reports
a one-button stick and a two-button one alike, so a stick is taken for a
one-button one, a held button being `JL_JOY_BUTTON_1`, until its second button
is first pressed; a CD32 pad has both from the start.
On DOS the game port's counts depend on the stick and on the CPU's speed, so a
stick's first reading -- after `jlInit`, after `jlJoystickReset`, or when it is
plugged in -- is taken as its centre, and its axes are read in proportion to
that: a count of 0 or twice the centre is a full push.
The X68000's display refreshes at 55.46 Hz. `jlFrameHz` times it when the
program starts and rounds to the nearest whole number, so it reports 55 --
and 56 under MAME, whose X68000 display runs slightly fast (56.07 Hz).
`jlMillisElapsed` does not depend on it: it reads the machine's 100 Hz system
timer to the millisecond.
Per-scanline palettes cost CPU time on the ST and the X68000, whose displays
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). 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
The API is the same on every port, and where a port behaved differently from
the rest because of the library, the library has been changed. What is left is
set by the machines and by the systems the ports run on, and stays. Each entry
names the ports, says how they differ, and gives the reason.
[[portsDifferDisplay]]
==== Display
* *IIgs: a big present can tear.* The Super Hi-Res display reads one fixed
buffer, with no second page to flip to, and a full-screen present takes
several frames, so the screen can show part of the old picture and part of
the new.
* *Win16, and DVX in a window: no vertical blank.* Windows 3.1 has no call that
waits for the display's retrace, and DVX syncs to it only on its full screen,
so `jlWaitVBL` keeps a virtual frame clock there and nothing stops a present
from tearing.
* *X68000: a small, wide picture.* The machine has no 320x200 mode, so the stage
sits in the middle of a 512x512 screen, a picture about 2.1 times as wide as
it is tall. That screen is a 31 kHz mode, so the monitor must take 31 kHz.
* *ST: eight levels a channel.* The plain ST's video chip has 3 bits per colour
channel, so `$0RGB` values one apart can look the same. The STe build shows
all 4 bits.
* *X68000: one intensity bit for three channels.* Its colours have 5 bits per
channel and an intensity bit the three share. JoeyLib sets that bit for every
colour but black, so white is full white, and a channel that is 0 in any
other colour shows at 1/63 rather than 0: `$F00` carries a trace of green and
blue.
* *Windows and DVX on a 256-colour display.* The display has one palette for
everything on it. Windows keeps 20 of its entries for itself, and DVX shares
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>>).
* *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 (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
* *IIgs keyboard: one key at a time, by character.* The keyboard reports only
the last character typed and whether any key is down. So JoeyLib sees one key
besides the modifiers at a time, knows a key by the character it types rather
than by its place on the keyboard, and reads it released only when every key
is up. With Ctrl held, Ctrl+H, I, J, K, M, and U read as those letters, not as
the left arrow, Tab, the down and up arrows, Return, and the right arrow,
whose codes they share; a shifted digit key reads as its digit only on
the US layout; and the keypad's keys read as the characters they type, so
its 8, 2, 4, and 6 are the digits, not the arrows as on the other ports.
* *IIgs keyboard: the keys it lacks.* It has no F1 to F10, no forward Delete
(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: 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`.
* *IIgs joystick: one stick, sharing two keys.* The game port takes one stick,
and its buttons are wired to the same lines as the Command and Option keys,
so holding either key reads as a button.
* *Mouse buttons.* The IIgs mouse has one button, and the ST and X68000 mice
have two; the other ports report three where the mouse has them.
* *ST and X68000: a stick is always connected.* An empty joystick port reads
exactly as a stick at rest, so `jlJoystickConnected` is true for both.
* *ST: the mouse port's stick.* The keyboard processor reports port 0 as the
mouse in the mode JoeyLib keeps it in, so `JL_JOYSTICK_1` never moves, and
port 1's fire button is the mouse's right button too.
* *Amiga: one button or two.* `lowlevel.library` reports a one-button and a
two-button stick alike, so a stick counts as a one-button one until its
second button is first pressed.
* *PSP: no mouse, one key at a time.* The PSP has no mouse, and its on-screen
keyboard has one cursor, so it presses one key at a time (Shift, Ctrl, and
Alt stay down until the next key is let go).
[[portsDifferAudio]]
==== Audio
* *Plain ST: voice 0 is silent while digital sound plays.* The ST has no
digital sound chip, so JoeyLib plays modules and effects by changing the
sound chip's channel A volume 12288 times a second, and channel A is tone
voice 0's. Voice 0 sounds again when the digital sound stops, and voices 1
and 2 play on. The STe build uses the STe's DMA sound and keeps all three.
* *ST: noise shares voice 2's volume.* The YM2149 has one volume per channel,
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.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
voice was using, and a module plays on all four. A channel plays on one side
only, so a centred effect needs a free channel on each side, and without
them it plays on one channel, from one side; and the tone voices, one
channel each, are heard from one side: voice 0 (and `jlAudioTone`) on the
left, voices 1 and 2 on the right.
* *IIgs: voices on the effect slots.* By design the IIgs plays tone voices 0 to
2 on sound effect slots 0 to 2 and the noise on slot 3, so an effect on one
of those slots and a voice cut each other off. Use slot 4 for effects while
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. 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
delivers them at `jlAudioFrameTick`, because its sound library may not be
called from an interrupt, and so does the IIgs, counting them against the
display's beats.
* *DOS, the ST, and the X68000 mix in program time.* The module player and
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, and
sound effects and modules, mixed at a quarter of full scale like every
software mix, keep 6 of their 8 bits.
[[portsDifferSerial]]
==== Serial
* *Top speeds.* The ST runs at 19200 baud at most (its MFP's rate table), a
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. 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
port's channel is also AppleTalk's.
* *IIgs Super Serial Card: lines tied together.* The 6551's DTR is also its
receiver's enable, and dropping its RTS stops its transmitter.
* *X68000: DTR rises at every line setup.* The ROM's and RSDRV.SYS's line setup
raise DTR whenever it runs.
* *Windows: DTR drops at close.* Win32's driver, and Win16 with Windows' own
COMM.DRV, drop DTR when the port is closed, and neither has a call that
prevents it.
* *IIgs: disk access stops interrupts.* GS/OS holds interrupts off for 65 to
220 ms at a time while it reads or writes a 3.5-inch floppy, which can lose
received bytes (<<_what_can_lose_received_bytes>>).
[[portsDifferFiles]]
==== Files and Saves
* *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. 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.
* *Web saves are the browser's.* A browser may clear a site's stored data to
make room, and a private window keeps nothing once it closes.
* *macOS: saves in Application Support.* An app's own folder may not be
writable, so `SAVES/` and the log go in the user's
`~/Library/Application Support/NAME/` (<<_macos_programs>>).
[[portsDifferMemory]]
==== Sprites and Memory
* *IIgs: code and copies stay inside 64 KB banks.* The 65816 runs code within
one 64 KB bank and its block copy stays within one, so each sprite's code
must fit in one bank of the codegen arena, the arena is at most four banks,
compacting never merges free space across banks, and a save-under buffer
must not cross a bank.
* *Code size differs by processor.* The 68000 and x86 emitters make code four
to eight times larger than the 65816 one, so with the same `codegenBytes` a
large sprite can compile on the IIgs and fall back to the interpreter
elsewhere.
* *IIgs: a small C heap, and `jlFree` apart from `free`.* The C library's heap
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
* *Frame rates.* The displays run at 50, 55, 60, and 70 Hz, as the table above
shows; the hosted ports and DVX keep a virtual 60 and 70 Hz.
* *IIgs: the clock stops while interrupts are off.* The IIgs has no free-running
timer JoeyLib can read, so its clock counts the display's vertical blanks,
and blanks that come while interrupts are held off are lost: a long stretch
with interrupts off, a floppy access for one, leaves the clock behind.
* *DOS: frames counted by the clock.* VGA has no dependable retrace interrupt,
so `jlFrameCount` counts 70ths of a second of the millisecond clock while
`jlWaitVBL` waits for the real retrace (70.086 Hz), and the two slowly drift
apart.
=== A Portability Checklist
Before you call a program portable, confirm:
* [ ] No `malloc` of anything over a few hundred bytes -- use `jlAlloc`.
* [ ] No large function-local arrays; they are `static` or heap.
* [ ] Movement and animation scale with `jlFrameHz()` or `jlMillisElapsed()`.
* [ ] `jlInputPoll()` is called exactly once per frame.
* [ ] `jlAudioFrameTick()` is called every frame.
* [ ] Every sprite is compiled during loading.
* [ ] Sprite backup buffers are 2-byte aligned and sized with the macro.
* [ ] Save file names are 8.3-safe.
* [ ] Only one audio layer is active per scene.
* [ ] `jlShutdown()` runs on every exit path.
* [ ] The program has actually been run on every target, not just built.
That last item is the one that matters. Every port has emulator run scripts
under `scripts/`, and building successfully proves considerably less than
running.
=== The Atari ST Family
The ST port builds two ways, and a program ships the one that suits its
audience (or both):
* *The ST build* (`make atarist`, programs in `build/atarist/bin`) is the
baseline. It is pure 68000 and *never uses STe hardware*: it runs on every
ST -- STF, STFM, Mega ST, STe, and Mega STe alike -- exactly as on a plain
ST, at no cost for the STe's existence.
* *The STe build* (`make ste`, programs in `build/ste/bin`) is the same port
compiled with `JOEYLIB_ATARI_STE`. It uses the STe's hardware
unconditionally, so it *needs an STe or Mega STe*: on any other machine
`jlInit` fails and `jlLastError` says why (a Mega ST has a BLiTTER but no
DMA sound, so it is refused too). `jlPlatformName` reports `Atari STe`.
Nothing in the API differs between them:
[cols="2,3,3", options="header"]
|===
|Feature |ST build (any ST) |STe build (STe, Mega STe)
|Palette
|3 bits per channel: each channel's low bit is dropped, so `$0RGB` values one
apart can look the same
|4 bits per channel: every `$0RGB` value exactly
|Digital sound (`.MOD`, SFX, streams)
|PWM through the YM2149's channel A volume, driven by a 12288 Hz timer
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
|The BLiTTER, for fills of 8 or more rows (24 for one 16-pixel column),
copies of 4 or more rows, and a tile layer's band; smaller work stays on
the CPU, which is faster there
|CPU clock
|As the machine is (8 MHz; TOS's setting on a Mega STe)
|8 MHz on an STe; a Mega STe is switched to 16 MHz with its cache on while
the program runs, and back to what TOS had when it ends (checked once, at
`jlInit`)
|===
The pixels are identical: UBER's 51 surface hashes match the ST's golden for
the STe build on an STe and a Mega STe, and the gates check that under Hatari.
`./joey-run` gives an ST-build program a plain ST and an STe-build program an
STe; `--machine st`, `megast`, `ste`, or `megaste` picks another. A project
builds the STe variant with `make ste`, like any other port; it takes the ST's
baked assets.
What the STe build buys, measured with UBER under Hatari against the ST build
on a plain ST: a large `jlFillRect` runs up to 1.76 times as fast (80x80), a
vertical line 1.4 times and `jlSurfaceClear` 1.1 times; everything else runs
as on an ST. On a Mega STe at 16 MHz every operation runs about twice as fast.
Hatari models that 16 MHz without the RAM wait states a real Mega STe has, so
expect less on memory-bound operations there.
=== DVX Programs
DVX is a DOS desktop: a VESA graphics GUI with windows, cooperative
multitasking, and its own libraries for sound, music, joysticks, and serial
ports. On DVX a JoeyLib program is one of its programs, a `.app` module
running in a window of its own beside the others. The graphics are the
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, 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
13h, or a VBE mode of that size), and back in its window. There it looks
exactly as on MS-DOS: the screen's 256 colours hold all 16 palettes, so
SCB palettes are exact at any desktop depth and palette changes are free.
The other programs run on meanwhile. Alt+Tab, Ctrl+Esc or another window
coming to the front brings the desktop back, with the program still
running and drawing in its window; giving its window the focus again puts
it back on the whole screen. The choice is kept in the program's settings
folder (`JOEYLIB.INI`), so it starts the way it was left. It is the user's
choice: JoeyLib's API has no full-screen call. (Under DOSBox, Alt+Enter is
DOSBox's own full-screen key; use Alt with the keypad's Enter, or the
menu.)
* *A virtual 70 Hz.* A window has no vertical blank. `jlFrameHz()` is 70, as
on DOS, and `jlWaitVBL()` waits for the next seventieth of a second of
DVX's clock while the other programs run; nothing prevents tearing. On the
whole screen `jlWaitVBL()` gives the other programs a turn, then waits for
the screen's real vertical retrace, as on DOS, so a present right after it
does not tear. The frame count is the same clock in both, so switching
never makes it jump.
* *Keys DVX keeps.* While the window is active, or the program has the whole
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, 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
`jlSerialPoll`, as `exit()` does anywhere: its `atexit` handlers run, then
`jlShutdown`, and its log is closed.
* *Sharing the machine.* DVX multitasks cooperatively: the other programs,
and this one's own window events, run only while it yields. `jlWaitVBL`
yields, and so do `jlInputPoll`, `jlStagePresent`, and `jlSerialPoll`, at
most once a frame. A loop that calls none of them freezes the desktop until
it ends.
* *Files.* DVX's programs share one current directory, so JoeyLib roots its
files at run time: `DATA/` is found beside the program (its folder under
`APPS`), while `SAVES/` and `joeylog.txt` go to the program's own settings
folder, `CONFIG\APPS\VENDOR\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 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
calibrated sets its range as it moves, so until it is calibrated a full
push can read a little short of 127. DVX's keyboard joystick is not
reported as one.
* *Serial ports* belong to DVX's serial library, one program each. As on
every port, closing one leaves DTR as it is.
* A program's `main` is compiled as `jlGameMain`, which the port calls with
`argv[0]` the `.app`'s path.
`make dvx` builds every example as a DVX program (the `dvx` installer
component supplies DVX and its SDK, <<_setting_up_the_toolchains>>), and
`./joey-run --port dvx NAME` starts one in DVX under DOSBox Staging.
=== SDL3 Programs
The SDL3 port builds a JoeyLib program as a native program for the machine
you work on: Linux today, through SDL 3, which the installer builds into
JoeyLib and every program links in (the `sdl3` component). It is the hosted
core in `src/hosted` -- everything that is the same under any desktop
operating system -- with an SDL3 backend in `src/hosted/sdl3`. Its stage is
the generic chunky one, so UBER's hashes are DOS's, and the rest behaves like
this:
* *A window, or the whole screen.* The stage is scaled to fill the window at
4:3, the shape 320x200 has on the old machines' monitors (each pixel 1.2
times as tall as it is wide), with black bars as needed; SDL's pixel-art
scaling keeps the pixels sharp at any size. Each scanline goes through its
own palette, so SCB palettes are exact. Three environment variables set it
up: `JOEYLIB_SCALE=N` opens the window N times 320 pixels wide (default 3),
`JOEYLIB_ASPECT=square` keeps the pixels square (16:10) instead, and
`JOEYLIB_FULLSCREEN=1` starts on the whole screen. Alt+Enter switches
between the window and the whole screen. JoeyLib's API has no full-screen
call; it is the user's choice.
* *A virtual 60 Hz.* A window has no vertical blank. `jlFrameHz()` is 60 and
`jlWaitVBL()` sleeps to the next sixtieth of a second of the machine's
clock. The window is redrawn at most once a frame, however often the
program presents.
* *Sprites are interpreted.* There is no code emitter for the host's CPU, so
`jlSpriteCompile` returns false and every sprite is drawn by the
interpreter -- far faster than any of the old machines all the same. (The
Win16 port, below, compiles them as DOS does.)
`joey/platform.h` defines `JL_SPRITE_INTERPRETED`, for a program that
checks how many of its sprites compiled. A pre-compiled bank (`.spc`)
loads its cels' pixels and leaves their code: the DOS bake is used.
* *Input.* Keys by where they are on the keyboard, so another layout does not
move the arrow keys; typed characters through the OS's keyboard layout. The
mouse is the pointer over the stage, in stage pixels (the OS pointer is
hidden over the window: JoeyLib programs draw their own). The first two
gamepads or joysticks are joysticks 0 and 1, coming and going as they are
plugged in. A gamepad SDL knows how to map gives its left stick or its
d-pad, its south button (A on an Xbox pad) as `JL_JOY_BUTTON_0`, and its
east button (B) as `JL_JOY_BUTTON_1`; any other joystick its first two axes
or its first hat, and its first two buttons.
* *Closing ends the program.* Closing the window ends the program at its next
`jlWaitVBL`, `jlInputPoll`, or `jlStagePresent`, as `exit()` does anywhere:
its `atexit` handlers run and the window and the sound are closed.
* *Files.* The program's own folder is the current directory from `jlInit`
on, as on the machine ports: `DATA/`, `SAVES/`, and `joeylog.txt` are beside
the program.
* *Sound* is mixed in software into 16-bit stereo at 44.1 kHz: `.MOD` music
through the module player (as on DOS), Paula's channels hard left and
right as on the Amiga, the SFX slots in stereo, the three tone
voices as square waves, and the noise voice as the ST's YM2149 makes it. It
is mixed in program time by `jlAudioFrameTick`, `jlWaitVBL`,
`jlInputPoll`, and `jlStagePresent`, a few frames ahead, so a
`jlAudioTickRegister` callback runs in program time too -- but between
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.
* *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
with its `DATA/`), and `./joey-run --port sdl3 NAME` runs one; so does the
program itself, from anywhere.
=== Web Programs
The web port is the SDL3 port compiled to WebAssembly with Emscripten (the
`web` installer components: Emscripten through emsdk, and SDL 3 built with
it): the same hosted core, the same backend, the same stage, so UBER's hashes
are DOS's here too. Everything in <<_sdl3_programs>> holds, except:
* *A page.* A program is a folder, `build/web/bin/NAME/`: `NAME.html` (the
page), `NAME.js`, `NAME.wasm`, and `NAME.data`, the program's `DATA/`
packed in when it is built. Put the folder on any web server and open
`NAME.html`; a browser will not run it from a `file:` address. The page's
address takes the window's settings: `NAME.html?scale=2&aspect=square`.
* *Your loop still works.* A browser does not let a page's program block,
and a JoeyLib program owns its loop and blocks in `jlWaitVBL`. Programs are
built with Emscripten's ASYNCIFY, and the port's waits hand the browser its
turn and carry on where they were, so an unmodified program runs. It gets
its turn back at least once a frame even when it never waits (a loop on
`jlInputPoll` alone, as `jlWaitForAnyKey` is).
* *Sound starts with the first click or key press*, as browsers require; the
page says so.
* *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.
`make web` builds every example as a page, and `./joey-run --port web NAME`
serves its folder on this machine and opens it in your browser.
=== macOS Programs
The macOS port is the SDL3 port built as Mac apps, on Linux: the same hosted
core, the same backend, the same stage (UBER's hashes are DOS's). It is
compiled with zig against Apple's macOS SDK, once for Apple silicon and once
for Intel, the two joined into one universal program that runs on either,
on macOS 13 (Ventura) or newer, with SDL 3 built the same way and linked in
(the `macos` installer components: zig, the SDK, and SDL 3). Everything in
<<_sdl3_programs>> holds, except:
* *An app.* A program is `build/macos/bin/NAME.app`: the program in
`Contents/MacOS/`, its `DATA/` and its icon in `Contents/Resources/`, which
is its folder and the current directory, and the `Info.plist` that names
them. Copy the app to a Mac, or zip it to send it; it opens from anywhere.
* *Saves in Application Support.* `SAVES/` and `joeylog.txt` go in
`~/Library/Application Support/NAME/`, not beside the program: an app in
`/Applications` may not be writable by its user, and macOS runs a
downloaded app from a read-only copy until it is moved. `jlDiskFree` is
the free space there.
* *Retina screens.* The window draws at the screen's own resolution, so the
stage is scaled up once, sharply, and not blurred again by the system.
* *Serial* through termios, as on Linux: a Mac has no serial port of its
own, so the ports are USB adapters' call-out devices (`/dev/cu.*`, the
Mac's own Bluetooth and debug devices left out), in name order, or the
device `JOEYLIB_SERIAL` names.
* *Untested on a Mac.* Nothing here runs Mac programs: `make verify` builds
every example and checks every app is one macOS opens -- both processors'
programs, the Apple silicon one signed, for macOS 13 on, linking only the
system's own libraries, with its `Info.plist` and icon
(`scripts/check-macos-apps.sh`) -- but what an app does on a Mac has not
been seen yet.
`make macos` builds every example as an app; `./joey-run` recognises one
and says it runs on a Mac.
=== PSP Programs
The PSP port is the SDL3 port built as PSP homebrew: the same hosted core
and backend, cross-compiled with pspdev's `psp-gcc` against SDL 3's PSP port
(the `psp` installer components: pspdev, and SDL 3 built with it). Its stage
is the same chunky one, so UBER's hashes are DOS's. Everything in
<<_sdl3_programs>> holds, except:
* *A folder for `PSP/GAME`.* A program is a folder, `build/psp/bin/NAME/`:
`EBOOT.PBP`, titled with the program's name (or its description's `TITLE`)
in the PSP's menu, and its `DATA/`. Copy the folder into `PSP/GAME/` on the
memory stick. Its folder is its current directory, so `DATA/`, `SAVES/`,
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.
* *Its own controls, and a keyboard on the screen.* The PSP's controls are
joystick 0, through SDL's gamepad mapping: the d-pad or the analog nub,
cross as `JL_JOY_BUTTON_0` and circle as `JL_JOY_BUTTON_1`; joystick 1 is
never there. Start is also the Return key and Select the Escape key, held
(`JL_KEY_RETURN`, `JL_KEY_ESCAPE`) and typed (`JL_CHAR_RETURN`,
`JL_CHAR_ESCAPE` from `jlInputGetChar`) as those keys are, so
`jlWaitForAnyKey`, Return to go on, and Escape to quit work at a press.
Square opens a PC keyboard drawn over the bottom of the screen, US layout,
for every other key: the d-pad moves its cursor, cross presses the key
under it (held down as long as cross is), L and R move the keyboard to
the top of the screen and back, out of the way of what the program shows
there, and Square closes it. Shift, Ctrl, and Alt stay down until the
next other key is let go (or until pressed again), and Caps turns capital
letters on and off. Its keys go down and up and type as a desktop's do,
so `jlKeyDown`, `jlKeyPressed`, and `jlInputGetChar` see the same as they
would there, F1 to F10, the arrows, and Ctrl and Alt chords included.
While it is open, joystick 0 rests: the d-pad and cross are the
keyboard's. There is no pointer to move.
* *HOME quits.* The HOME menu's Exit ends the program as closing a window
does.
* *Free space* (`jlDiskFree`) is that of the memory stick (or a PSP Go's
internal storage) the program is on.
* *Memory.* A PSP-1000 has 32 MB, about 24 MB of it for programs; later
models have more.
* *Alignment.* The PSP's MIPS processor stops a program that reads or writes
a 16- or 32-bit value at an address that is not a multiple of its size,
where an x86 does not notice (a 68000 stops too, but only on odd
addresses). Read a multi-byte value from a file buffer a byte at a time.
The gates run every PSP program with PPSSPP checking every access the same
way.
`make psp` builds every example, and `./joey-run --port psp --headless NAME`
runs one under PPSSPP's headless build (the `ppsspp` installer component,
patched for the gates); `--snapshot` saves the PSP's screen. There is no
window to play in here: to play, copy the folder to a PSP, or open its
`EBOOT.PBP` in PPSSPP. None of it has run on a real PSP yet.
=== Windows Programs
The Win32 port is the hosted core with a backend of its own, written to the
Win32 API (`src/hosted/win32`), for the Windows SDL3 does not reach --
Windows 95, 98, ME, NT 4, and 2000 -- and every Windows since. Its stage is
the same chunky one, so UBER's hashes are DOS's. Everything in
<<_sdl3_programs>> holds, except:
* *Old Windows on purpose.* A program is a 32-bit `NAME.EXE` built by
llvm-mingw against `MSVCRT.DLL` (the `win32` installer component), marked
for Windows 4.0 so 95 and NT 4 will load it, linked statically, and
calling only what USER, GDI, WINMM, and KERNEL32 had in 1995. `MSVCRT.DLL`
came with Windows 95 OSR2 and Internet Explorer; a first-release Windows
95 needs it installed.
* *One thread.* The window is made on the program's thread, so its messages
arrive there and the port dispatches them at `jlInputPoll`, `jlWaitVBL`,
and `jlStagePresent`; the sound goes out through waveOut buffers handed
over at the same calls. There are no callbacks and no locks.
* *The window.* The stage is stretched with `StretchBlt` to the stage's
shape, 4:3 or square (the same `JOEYLIB_SCALE`, `JOEYLIB_ASPECT`, and
`JOEYLIB_FULLSCREEN` settings), black around it; full screen is a
borderless window over the whole monitor, which leaves the display mode
alone. On a 256-colour desktop Windows maps every colour to the nearest
one it has. Each frame draws only what changed since the last: a game
that moves a few sprites pays for their rectangles, not for the whole
window, which on a Windows 95 machine is most of a frame's time.
* *Keys* by the PC scan code every key message carries (the same table as
DOS), typed characters through the user's keyboard layout, and the first
two joysticks through `joyGetPosEx`, calibrated in the Control Panel. Keys
held when the window loses the keyboard are let go.
* *Serial* through the Win32 COM calls (COM1, or COM<unit> for a slot).
Closing the port lets Windows drop DTR; Win32 has no call to prevent it.
`make win32` builds every example as a Windows program in `build/win32/bin/`,
and `./joey-run --port win32 NAME` runs one under Wine (the `wine` installer
component, a portable Wine kept in `toolchains/`); the gates run there too,
`scripts/verify-win32-display.sh` among them, though that checks Wine's GDI
rather than Windows'. None of it has run on a real Windows 9x or NT 4 here.
The Win16 port is the hosted core with a backend written to the Windows 3.1
API (`src/hosted/win16`), for Windows 3.1 and Windows for Workgroups 3.11 on
a 386 or better. Its stage is the same chunky one, so UBER's hashes are DOS's
too. Everything said of Win32 above holds -- one thread, the window, keys by
scan code, DTR dropped at close -- except:
* *32-bit code on Windows 3.1.* A program is a Windows 3.x `NAME.EXE` built
with Open Watcom's Win386 target (the `win16` installer component): flat
32-bit code bound to Watcom's supervisor, which turns each call into the
16-bit one Windows takes, so it needs Windows in 386 enhanced mode. It
calls only what Windows 3.1 has: USER, GDI, KERNEL, and MMSYSTEM.
* *Exact colours.* The frame is 8-bit, each pixel one entry of a 256-entry
colour table holding all 16 palettes, which on a 256-colour display is a
logical palette too, so every colour is exact, SCB palettes included, as
far as the display has room: Windows keeps 20 of a 256-colour display's
entries for itself, so with more than 14 palettes in use and more than 236
different colours on screen, some show as the nearest it has.
`JOEYLIB_SCALE` defaults to 2, and the settings are read from the DOS
environment Windows started from (`joey-run` puts its own there). Under
DOSBox, Alt+Enter is DOSBox's own full-screen key; use Alt with the
keypad's Enter.
* *Speed on a 386.* Sprites are compiled to x86 code, as on DOS. The port
expands the stage into the window's pixels itself, a presented row at a
time and scaled as it goes, and hands Windows only the rectangles that
changed, those close enough together as one. With Microsoft's WinG 1.0 on
the machine (its runtime in Windows' `SYSTEM` folder) the rows go straight
into a WinG bitmap the size of the window, and drawing a rectangle is a
plain copy to the screen; without it, each rectangle goes through a device
bitmap on a 256-colour display, and only another kind of display pays for
Windows matching every pixel's colour. `JOEYLIB_WING=0` leaves WinG alone.
JoeyLib does not ship WinG: a game that wants it on its players' machines
ships the runtime under Microsoft's terms (see `LICENSES.md`). While the
SCBs use 14 palettes or fewer, the frame's colours are the screen's own (an
identity palette, as WinG recommends), so a palette change, colour cycling
included, reaches the screen at once with nothing redrawn; more palettes
than that, or a display without a palette, redraw the window when colours
change. On a 386SX-16 (DOSBox's model of one), a game loop presenting
every frame runs at, in frames a second, with an Apple IIgs (under MAME)
for scale:
+
[cols="3,1,1,1,1", options="header"]
|===
| |2x with WinG |2x without |1:1 |IIgs
|Nothing moving, or colour cycling |60 |60 |60 |60
|One 16x16 sprite moving |60 |60 |60 |60
|Eight sprites moving |26 |20 |30 |20
|Eight sprites in a column |30 |20 |30 |20
|The whole screen changing |6 |4 |10 |8.5
|===
+
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.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
their buttons 1 and 2. Windows 3.x reads sticks only through a joystick
driver, which Windows 3.1 itself does not have; without one no stick is
seen. JoeyLib's Windows runs KPJOY.DRV (the `kpjoy` installer component,
built from the kpjoy project): the PC game port's two sticks, each axis's
rest taken from its first readings, so leave the stick centred as the
program starts. A game shipped for Windows 3.x can ship KPJOY.DRV with it
(`[drivers] joystick=kpjoy.drv` in SYSTEM.INI).
* *Files.* The program's folder, drive included, is the current directory
from the moment the window opens.
* *Serial* through Windows 3.1's communications calls (`OpenComm` and the
rest): COM1, or COM<unit> for a slot. JoeyLib's Windows runs KPCOMM.DRV
in place of COMM.DRV (the `kpcomm` installer component, built from the
mscomm project): both 16550A FIFOs and 115200 baud. Windows 3.1's own
driver goes up to 57600, and a faster rate gets that there.
* *Shipping.* Every Win16 program carries Open Watcom's C library and its
Win386 supervisor, and KPJOY.DRV and KPCOMM.DRV carry Open Watcom code
too. Open Watcom's license (the Sybase Open Watcom Public License,
section 2.2(d)) asks for a notice, in the program and in its
documentation, that the source of that code is available and where. The
port builds the notice into every program and the drivers carry it, never
shown; the documentation's part is this line, in the game's readme or
manual:
+
....
This software contains code from Open Watcom. Its source code is available
under the terms of the Sybase Open Watcom Public License version 1.0 from
https://github.com/open-watcom/open-watcom-v2
....
+
KPJOY.DRV and KPCOMM.DRV are MIT licensed: ship each with its `LICENSE.md`,
which the installer puts beside the driver in `toolchains/opt/`.
`make win16` builds every example as a Windows 3.x program in
`build/win16/bin/`, and `./joey-run --port win16 NAME` runs one on Windows
for Workgroups 3.11 in DOSBox Staging (the `win311` installer component, set
up from your own install disc); the gates run there too, among them
`scripts/verify-win16-display.sh`, which checks every pixel the window shows
against the stage after every present, with WinG and without (the optional
`wing` installer component: `./install.sh import WING10.EXE`, Microsoft's
WinG 1.0 SDK, then `./install.sh install wing`; the gates then install its
runtime in their Windows). None of it has run on a real 386
here; the speeds above are DOSBox's 386SX-16 model's, and the IIgs's are
MAME's.
=== Haiku and BeOS Programs
The Be API ports are the hosted core with a backend written to the Be API
(`src/hosted/be`, C++), which Haiku reimplements, kept to the calls BeOS R5
already had. One source builds three ports: `haiku` (Haiku x86_64, gcc 13),
`haiku32` (Haiku x86, gcc 2.95, the BeOS C++ ABI), and `beos` (BeOS R5 x86,
built with the same gcc 2.95 against R5's own headers and libraries, which
the `beosR5` installer component copies off your BeOS 5 Professional CD).
Their stage is the same chunky one, so UBER's hashes are DOS's. Everything in
<<_sdl3_programs>> holds, except:
* *Threads.* The program's `main` keeps its own thread; the application and
the window run on threads of their own, as every Be program's do. Key and
mouse messages reach the program as events taken at `jlInputPoll`,
`jlWaitVBL`, and `jlStagePresent`, and the sound comes from a
`BSoundPlayer`.
* *The window* shows the stage scaled without filtering, at the stage's
shape, 4:3 or square, black around it (the same `JOEYLIB_SCALE`,
`JOEYLIB_ASPECT`, and `JOEYLIB_FULLSCREEN` settings; `JOEYLIB_SCALE`
defaults to 3). The port scales the frame itself and draws only what
changed since the last frame. Alt+Enter switches between the window and
the whole screen. The pointer is hidden over the window.
* *Keys* by raw key code, the key's place on the keyboard whatever the
keymap; typed characters are the keymap's, 7-bit ASCII only. The first
two joysticks (`BJoystick`) are `JL_JOYSTICK_0` and `JL_JOYSTICK_1`, their first
two axes and buttons.
* *Files.* The program's folder is the current directory from the moment
the window opens.
* *Its icon* is in its resources (<<_icons>>), but Tracker shows a file by
the icons in its attributes, which a program built on another machine does
not have. When it opens its window without them, the program copies them
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.
* *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`.
`make haiku`, `make haiku32`, and `make beos` build every example into
`build/<port>/bin/`, and `./joey-run --port <port> NAME` runs one in that
port's VM under QEMU: Haiku R1/beta6 (the `haikuVm` and `haikuVm32` installer
components) or BeOS R5 (`beosVm`, made from your CD), booted from a disk it
never writes to, with the program's folder on a second disk. R5 drives none
of QEMU's video cards, so its VM is taken through the boot loader's menu to
the fail-safe (VESA) mode, 1024x768 in 16-bit colour; it has no sound card.
The gates run in the same VMs (`scripts/verify-be-input.sh`,
`scripts/verify-be-display.sh`, `scripts/verify-be-icon.sh`, and
`scripts/bench-be.sh`). None of it has
run on a real BeOS or Haiku machine here.