765 lines
35 KiB
Text
765 lines
35 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.* It
|
|
lives in bank 0, and a single `malloc` is capped at roughly 32 KB -- often far
|
|
less in practice, because the clang libc heap on that port can be a few hundred
|
|
bytes. A program that `malloc`s a 32000-byte buffer works on four targets and
|
|
fails 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.
|
|
|
|
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, 60 and 70 Hz. Scale
|
|
by `jlFrameHz()` or work from `jlMillisElapsed()`.
|
|
|
|
`jlFrameCount()` is a 16-bit counter, so you must poll it faster than twice the
|
|
frame rate or you will miss edges, and it wraps roughly every 18 minutes at
|
|
60 Hz.
|
|
|
|
=== 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 at roughly 1/40th the speed, 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. An odd
|
|
buffer address routes save and restore onto slow interpreted paths on the
|
|
planar ports, because the 68000 cannot perform word accesses at odd addresses.
|
|
|
|
[source,c]
|
|
----
|
|
// Right: 2-byte aligned by construction.
|
|
static uint16_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4) / 2];
|
|
|
|
// Risky: may land on an odd address.
|
|
static uint8_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4)];
|
|
----
|
|
|
|
Note also that on the planar ports the region a save-under actually captures is
|
|
the *16-pixel-aligned window* containing the sprite, not the sprite rectangle.
|
|
`jlSpriteSaveUnder` reports the true geometry in the backup structure's
|
|
`x`, `width`, `height` and `sizeBytes` fields. It is that recorded window you
|
|
must not overwrite before restoring.
|
|
|
|
=== 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.
|
|
|
|
Keep names portable across the retro filesystems this library targets: *at most
|
|
8 characters plus a short extension, letters and digits only*, so they satisfy
|
|
both ProDOS and DOS 8.3. Do not rely on case being preserved.
|
|
|
|
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 two platforms they share physical
|
|
hardware:
|
|
|
|
[cols="2,5", options="header"]
|
|
|===
|
|
|Layer |What it is
|
|
|
|
|Module playback
|
|
|`jlAudioPlayMod` -- four-channel Protracker `.MOD`, mixed in software on
|
|
DOS and ST, hardware-replayed on Amiga and IIgs.
|
|
|
|
|Chip tracker
|
|
|`jlMusicPlay` -- a portable event stream driving three tone voices plus
|
|
noise, at register-write cost with no CPU mixing.
|
|
|
|
|Voices and effects
|
|
|`jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx` and the streaming forms.
|
|
|===
|
|
|
|
[WARNING]
|
|
====
|
|
On the Atari ST and Amiga, the tone voices *are* the same hardware the module
|
|
player drives. While a `.MOD` is playing, `jlAudioVoice` writes fight the
|
|
player's register writes and lose. Choose the module layer or the voice and
|
|
tracker layer *per scene*, not both at once. (The STe build plays modules and
|
|
sound effects through the STe's DMA sound chip instead, so there the voices
|
|
keep the YM2149 to themselves -- but a program that also ships the ST build
|
|
still has to follow the rule.)
|
|
====
|
|
|
|
The chip tracker is the portable choice for music. It costs almost nothing on
|
|
every port -- unlike `.MOD` playback, which needs real CPU mixing and is
|
|
marginal on the ST.
|
|
|
|
On the IIgs, `jlAudioVoice` and `jlAudioNoise` require `jlAudioInit` (they ride
|
|
the tracker engine's DOC path) and behave as retriggered one-shots of about
|
|
80 ms, so a note held longer must be re-asserted roughly once a frame. On DOS,
|
|
ST and Amiga those two calls are direct hardware programming and work without
|
|
`jlAudioInit`.
|
|
|
|
A failed `jlAudioInit` is *not* fatal. `jlInit` still succeeds and every audio
|
|
call becomes a silent no-op, 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, Win32, PSP, Haiku, BeOS |Win16
|
|
|
|
|Frame rate
|
|
|60 Hz
|
|
|50 Hz (PAL)
|
|
|60 Hz (NTSC)
|
|
|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
|
|
|text-plane planar
|
|
|chunky 4bpp
|
|
|chunky 4bpp
|
|
|chunky 4bpp
|
|
|
|
|Sprites
|
|
|compiled
|
|
|compiled
|
|
|compiled
|
|
|compiled
|
|
|compiled
|
|
|compiled
|
|
|interpreted
|
|
|interpreted
|
|
|
|
|Stereo pan
|
|
|all three
|
|
|left and right
|
|
|ignored (mono)
|
|
|ignored (mono)
|
|
|ignored
|
|
|all three
|
|
|all three
|
|
|ignored (mono)
|
|
|
|
|`jlAudioTone`
|
|
|DOC oscillator
|
|
|Paula
|
|
|YM2149
|
|
|PIT counter 2
|
|
|-
|
|
|a DVX tone (FM chip or mixer)
|
|
|voice 0 of the mixer
|
|
|voice 0 of the mixer
|
|
|
|
|Joystick
|
|
|analog, auto-disables
|
|
|digital
|
|
|digital
|
|
|digital
|
|
|digital
|
|
|analog (DVX calibrates)
|
|
|the OS's gamepads or joysticks (OS calibrates); 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; the PSP has no keyboard
|
|
|Windows'
|
|
|
|
|Serial ports
|
|
|modem, printer, slot card
|
|
|`serial.device`
|
|
|Modem port
|
|
|COM1..COM4
|
|
|-
|
|
|COM1..COM4 (DVX's library)
|
|
|Win32: COM1 and up; the others -
|
|
|COM1..COM9
|
|
|===
|
|
|
|
The joystick row deserves a note: the IIgs port *auto-disables* joystick
|
|
polling after a short window with no stick detected, because polling costs
|
|
about 3 ms per frame there. It does not re-probe on its own. If a user plugs a
|
|
stick in while the program is running, you must call `jlJoystickReset` to
|
|
resume polling.
|
|
|
|
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. `JOY_BUTTON_0` is a stick's
|
|
trigger A and `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, `JOYSTICK_0` is port 1, the joystick port, and `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 `JOYSTICK_1` stays at rest whatever
|
|
is plugged in. Port 1's fire button is the same line as the mouse's right
|
|
button, so a press reads as both `JOY_BUTTON_0` and `MOUSE_BUTTON_RIGHT`.
|
|
|
|
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 `lowlevel.library`'s own detection.
|
|
|
|
On DOS the game port's counts depend on the stick and on the CPU's speed, so
|
|
the first reading after `jlInit` or `jlJoystickReset` is taken as the stick's
|
|
rest, and a push past half that value either way reads as a full push. Call
|
|
`jlJoystickReset` with the stick centred if it was not at the start.
|
|
|
|
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 something on the X68000, and only there. Its
|
|
display has one 16-colour palette, so the port reloads it 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 and arms no
|
|
interrupt. 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). Palette and SCB changes take effect 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 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; 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
|
|
|
|
|Fills and copies
|
|
|The CPU
|
|
|The BLiTTER, for fills of 8 or more rows (24 for one 16-pixel column) and
|
|
copies of 4 or more rows; 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: 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
|
|
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, 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\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.
|
|
* *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.
|
|
`joey/platform.h` defines `JOEYLIB_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 are joysticks 0 and 1: the left stick or the d-pad, the south
|
|
button (A on an Xbox pad) as `JOY_BUTTON_0` and the east button (B) as
|
|
`JOY_BUTTON_1`; they come and go as they are plugged in.
|
|
* *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 libxmp-lite (as on DOS), 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. `jlAudioInit` is needed for every sound, the tone
|
|
voices included.
|
|
* *No serial port.*
|
|
|
|
`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.* `SAVES/` is kept in the browser's IndexedDB for the page's
|
|
address, read back when the program starts and written back every two
|
|
seconds, so a save outlives the page.
|
|
* *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.
|
|
|
|
=== 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`.
|
|
* *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, no keyboard.* The PSP's controls are joystick 0,
|
|
through SDL's gamepad mapping: the d-pad or the analog nub, cross as
|
|
`JOY_BUTTON_0` and circle as `JOY_BUTTON_1`; joystick 1 is never there.
|
|
Having no keyboard, the PSP also gives Start as the Return key and Select
|
|
as the Escape key, held (`KEY_RETURN`, `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. No
|
|
other key is ever down, nothing else is typed (the PSP's on-screen
|
|
keyboard is never opened), and 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 drawn with `StretchDIBits` at 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.
|
|
* *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.
|
|
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, a DIB whose 256-entry colour table
|
|
holds all 16 palettes and is realized as a logical palette whenever a frame
|
|
is shown, so on a 256-colour display every colour is exact, SCB palettes
|
|
included. `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.
|
|
* *Sound* is 8-bit mono at 22.05 kHz through waveOut, what a Sound Blaster
|
|
and a 386 handle well; 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. None of it has run
|
|
on a real 386 here.
|
|
|
|
=== 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 from a bitmap the app_server scales 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). 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 `JOYSTICK_0` and `JOYSTICK_1`, their first
|
|
two axes and buttons.
|
|
* *Files.* The program's folder is the current directory from the moment
|
|
the window opens.
|
|
* *No serial port.*
|
|
* *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/bench-be.sh`). None of it has run on a real BeOS or Haiku machine
|
|
here.
|