381 lines
15 KiB
Text
381 lines
15 KiB
Text
== Writing Portable Code
|
|
|
|
A JoeyLib program that follows the rules in this chapter compiles and behaves
|
|
the same on all five machines and on DVX. 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, prefer `jlMemCopy` over `memcpy`. It has the same contract
|
|
(no overlap) but the IIgs implements it as `MVN` block moves at 7 cycles per
|
|
byte instead of a libc byte loop, 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 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", options="header"]
|
|
|===
|
|
|Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000 |DVX
|
|
|
|
|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)
|
|
|
|
|Pixel storage
|
|
|chunky 4bpp
|
|
|planar
|
|
|planar, interleaved
|
|
|chunky 4bpp
|
|
|text-plane planar
|
|
|chunky 4bpp
|
|
|
|
|Stereo pan
|
|
|all three
|
|
|left and right
|
|
|ignored (mono)
|
|
|ignored (mono)
|
|
|ignored
|
|
|all three
|
|
|
|
|`jlAudioTone`
|
|
|DOC oscillator
|
|
|Paula
|
|
|YM2149
|
|
|PIT counter 2
|
|
|-
|
|
|a DVX tone (FM chip or mixer)
|
|
|
|
|Joystick
|
|
|analog, auto-disables
|
|
|digital
|
|
|digital
|
|
|digital
|
|
|digital
|
|
|analog (DVX calibrates)
|
|
|
|
|Keyboard layout
|
|
|machine's own
|
|
|machine's own
|
|
|machine's own
|
|
|fixed US
|
|
|machine's own
|
|
|DVX's
|
|
|
|
|Serial ports
|
|
|modem, printer, slot card
|
|
|`serial.device`
|
|
|Modem port
|
|
|COM1..COM4
|
|
|-
|
|
|COM1..COM4 (DVX's library)
|
|
|===
|
|
|
|
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.
|
|
|
|
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. 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.
|
|
|
|
=== 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.
|
|
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.
|