== Writing Portable Code A JoeyLib program that follows the rules in this chapter compiles and behaves the same on all five machines. 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"`. 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", options="header"] |=== |Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000 |Frame rate |60 Hz |50 Hz (PAL) |60 Hz (NTSC) |70 Hz |60 Hz |Pixel storage |chunky 4bpp |planar |planar, interleaved |chunky 4bpp |text-plane planar |Stereo pan |all three |left and right |ignored (mono) |ignored (mono) |ignored |`jlAudioTone` |DOC oscillator |Paula |YM2149 |PIT counter 2 |- |Joystick |analog, auto-disables |digital |digital |digital |digital |Keyboard layout |machine's own |machine's own |machine's own |fixed US |machine's own |Serial ports |modem, printer, slot card |`serial.device` |Modem port |COM1..COM4 |- |=== 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. === 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 all five targets, not just built. That last item is the one that matters. All five ports have emulator run scripts under `scripts/`, and building successfully proves considerably less than running.