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