== 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 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 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//bin/`, and `./joey-run --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.