== Anatomy of a JoeyLib Program Every JoeyLib program has the same four-part shape: configure and initialise, set up the screen and load assets, run a frame loop, shut down. This chapter walks through each part and ends with a complete annotated skeleton you can copy. === Part 1: Configure and Initialise [source,c] ---- jlConfigT config; config.codegenBytes = 32UL * 1024; // sprite blitter cache config.noAudio = false; // jlInit starts audio too if (!jlInit(&config)) { // jlLastError() explains why. There is no display yet, so on a // real machine the only place to report this is stderr or a log. return 1; } ---- `codegenBytes` sizes the *codegen arena*: the scratch memory the library emits native sprite blitter code into. Getting it wrong is the most common cause of sprites drawing slowly, so it is worth understanding. * `0` selects a per-platform default: 8 KB on the IIgs, 64 KB on the Amiga, ST, and X68000, and 32 KB on DOS, DVX, and Win16. The other hosted ports interpret every sprite and allocate no arena at all. * The 68000 and x86 emitters produce code roughly four to eight times less dense than the 65816 one, so the same sprite needs far more arena space on those ports. A 32x32 sprite can need 8 to 16 KB there. * Measure, do not guess: after compiling a sprite, `jlSpriteCompiledSize()` reports its exact cost on the current platform. * Every port takes as much of the request as the machine has, and `jlInit` fails only if it can have none of it; `jlSpriteCodegenBytesTotal()` says what it got. On the IIgs a compiled sprite cannot cross a 64 KB bank boundary, so the arena is up to four Memory Manager blocks of at most 64 KB, each inside one bank: 256 KB at most (a bank that is refused is tried again at 60 KB). Elsewhere it is one block, halved until the heap has room, down to 4 KB. If the arena is too small, sprites still draw: they fall back to an interpreter that is *two to six times slower*, depending on the machine. Nothing reports this as an error, so a program that never checks can be silently crawling. === Part 2: The Stage, the Palette, and Assets [source,c] ---- jlSurfaceT *stage = jlStageGet(); ---- The *stage* is the library-owned back buffer. You never create or destroy it; it is valid from `jlInit` to `jlShutdown`. All drawing goes here, and `jlStagePresent()` copies it to the real display. Set up colour before drawing: [source,c] ---- uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; // ... fill in 16 entries of $0RGB ... jlPaletteSet(stage, 0, colors); jlScbSetRange(stage, 0, JL_SURFACE_HEIGHT - 1, 0); ---- Each surface carries *16 palettes of 16 colours* plus a 200-entry table (one per scanline) selecting which palette that line uses. That table is the *SCB*, or scanline control byte table. Using one palette everywhere is the simple case; assigning different palettes to different bands is how a JoeyLib program shows more than 16 colours at once. Load assets after init, because sprite loading needs the codegen arena: [source,c] ---- jlSpriteT *cels[16]; uint16_t palette[JL_SURFACE_COLORS_PER_PALETTE]; uint16_t count = jlSpriteBankLoad("player.spr", cels, 16, palette); uint16_t i; for (i = 0; i < count; i++) { (void)jlSpriteCompile(cels[i]); // compile NOW, not mid-frame } ---- [IMPORTANT] ==== `jlSpriteDraw` does *not* compile a sprite on demand. An uncompiled sprite draws through the interpreter every single time. Call `jlSpriteCompile` during loading -- otherwise the first draw of each sprite either stalls a frame (if you compile lazily yourself) or never gets fast at all. ==== === Part 3: The Frame Loop The library does not schedule anything. You own the loop. The canonical shape: [source,c] ---- while (running) { jlWaitVBL(); // pace to the display's refresh jlInputPoll(); // refresh keyboard, mouse, joystick jlAudioFrameTick(); // pump the audio engine if (jlKeyPressed(JL_KEY_ESCAPE)) { running = false; } updateGame(); // your logic renderFrame(stage); // your drawing jlStagePresent(); // make it visible } ---- Four library calls, in that order, and everything else is yours. `jlWaitVBL()` blocks until the next vertical blank. On the machine ports that is the display's own; the hosted ports, and DVX in a window, have none, and wait for the next tick of a virtual frame clock instead. `jlInputPoll()` must be called exactly once per frame. The edge predicates (`jlKeyPressed`, `jlKeyReleased`, and the mouse and joystick equivalents) are defined relative to the *previous poll*, so calling it twice per frame makes edges disappear, and not calling it means nothing ever changes. `jlAudioFrameTick()` advances tracker music and keeps streaming sound effects fed. Call it once per frame. `jlStagePresent()` flips the dirty parts of the stage to the display. The drawing primitives record which rows they touched, so presenting only copies what actually changed. You do not need to tell it what you drew: every call that changes the stage's pixels marks them, and the API has no other way to write them. Palette and SCB changes reach the display at the next present too. ==== Frame-Rate Independence The targets do not share a refresh rate: 50 Hz (the Amiga, and a PAL IIgs or ST), 55 Hz (X68000), 60 Hz (the IIgs, an NTSC ST, and every hosted port's virtual rate), and 70 Hz (VGA mode 13h, and DVX's virtual rate). A loop that moves a sprite "two pixels per frame" runs 40 percent faster on DOS than on a PAL Amiga. So give every speed per second, never per frame, and work out each frame how far that is. The millisecond clock does it best. `jlMillisElapsed()` reports true elapsed wall-clock time, so a frame that took a long time to render moves things as far as the time it took, rather than counting as one tick: [source,c] ---- uint32_t now = jlMillisElapsed(); uint32_t elapsed = now - gLastMillis; // milliseconds since the last frame gLastMillis = now; ---- A thing then moves its speed times `elapsed` / 1000. At 70 frames a second that is often a fraction of a pixel, which only adds up if positions keep the fraction, so keep them in fixed point (the skeleton below uses 1/256 of a pixel) and draw at the whole-pixel part. `jlFrameHz()` (50 to 70) is the other way: a per-frame step of speed / `jlFrameHz()` is right while every frame takes one refresh, and falls behind whenever one takes longer. === Part 4: Shut Down [source,c] ---- jlShutdown(); return 0; ---- `jlShutdown` stops audio, restores the display mode, frees the library's allocations, and un-hooks interrupt vectors. Sprites and surfaces you created are *not* released with it: destroy them with `jlSpriteDestroy` and `jlSurfaceDestroy` before you shut down. The buffers *you* allocated (including tile data you handed to `jlSpriteCreate`, which is never copied) remain yours to free. === The Complete Skeleton This compiles and runs on every target as-is. [source,c] ---- // A minimal but complete JoeyLib program. #include #include #define PLAYER_SIZE 16 #define PLAYER_SPEED 120 // pixels per second, the same on every port #define SUBPIXEL_SHIFT 8 // positions are kept in 1/256 of a pixel #define MAX_STEP_MILLIS 100 // after a stall, move no further than this static jlSurfaceT *gStage; static bool gRunning = true; static int32_t gPlayerX = 160L << SUBPIXEL_SHIFT; static int32_t gPlayerY = 100L << SUBPIXEL_SHIFT; static uint32_t gLastMillis = 0; static void buildPalette(void); static int32_t clampPosition(int32_t position, int16_t limit); static void handleInput(int32_t step); static void renderFrame(void); static int32_t stepForFrame(void); static void buildPalette(void) { uint16_t colors[JL_SURFACE_COLORS_PER_PALETTE]; uint8_t i; for (i = 0; i < JL_SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = 0x000; } colors[1] = 0xFFF; // white colors[2] = 0x0C0; // green colors[3] = 0x00C; // blue jlPaletteSet(gStage, 0, colors); jlScbSetRange(gStage, 0, JL_SURFACE_HEIGHT - 1, 0); } // A position (in 1/256 of a pixel) kept between 0 and limit pixels. static int32_t clampPosition(int32_t position, int16_t limit) { int32_t max = (int32_t)limit << SUBPIXEL_SHIFT; if (position < 0) { return 0; } if (position > max) { return max; } return position; } // Moves the player by step (1/256 of a pixel) in each direction held. static void handleInput(int32_t step) { if (jlKeyPressed(JL_KEY_ESCAPE)) { gRunning = false; } if (jlKeyDown(JL_KEY_LEFT)) { gPlayerX -= step; } if (jlKeyDown(JL_KEY_RIGHT)) { gPlayerX += step; } if (jlKeyDown(JL_KEY_UP)) { gPlayerY -= step; } if (jlKeyDown(JL_KEY_DOWN)) { gPlayerY += step; } // A joystick, if one is attached, drives the same movement. if (jlJoystickConnected(JL_JOYSTICK_0)) { if (jlJoystickX(JL_JOYSTICK_0) < -64) { gPlayerX -= step; } if (jlJoystickX(JL_JOYSTICK_0) > 64) { gPlayerX += step; } } // Keep the player on screen: the primitives would clip safely, but // clamping keeps the object whole. gPlayerX = clampPosition(gPlayerX, JL_SURFACE_WIDTH - PLAYER_SIZE); gPlayerY = clampPosition(gPlayerY, JL_SURFACE_HEIGHT - PLAYER_SIZE); } static void renderFrame(void) { jlSurfaceClear(gStage, 0); jlDrawRect(gStage, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, 1); jlFillRect(gStage, (int16_t)(gPlayerX >> SUBPIXEL_SHIFT), (int16_t)(gPlayerY >> SUBPIXEL_SHIFT), PLAYER_SIZE, PLAYER_SIZE, 2); } // How far the player moves this frame, in 1/256 of a pixel: PLAYER_SPEED // times the time since the last frame, whatever the port's frame rate. static int32_t stepForFrame(void) { uint32_t now = jlMillisElapsed(); uint32_t elapsed = now - gLastMillis; gLastMillis = now; if (elapsed > MAX_STEP_MILLIS) { elapsed = MAX_STEP_MILLIS; } return (int32_t)((((uint32_t)PLAYER_SPEED * elapsed) << SUBPIXEL_SHIFT) / 1000u); } int main(void) { jlConfigT config; config.codegenBytes = 32UL * 1024; config.noAudio = false; if (!jlInit(&config)) { return 1; } gStage = jlStageGet(); if (gStage == NULL) { jlShutdown(); return 1; } buildPalette(); jlRandomSeed(12345); // deterministic on every port gLastMillis = jlMillisElapsed(); while (gRunning) { jlWaitVBL(); jlInputPoll(); jlAudioFrameTick(); handleInput(stepForFrame()); renderFrame(); jlStagePresent(); } jlShutdown(); return 0; } ---- === Two Drawing Strategies Once you have moving objects over a background, you need to erase them. There are two patterns, and *mixing them in the same region corrupts the screen*. ==== Save-Under and Restore Before drawing a sprite, capture what is underneath; next frame, put it back. [source,c] ---- uint16_t backing[jlSpriteBackupLength(uint16_t, 2, 2)]; jlSpriteBackupT backup; backup.bytes = (uint8_t *)backing; backup.sizeBytes = sizeof(backing); // each frame: jlSpriteRestoreUnder(stage, &backup); // erase last frame's sprite jlSpriteSaveAndDraw(stage, sprite, x, y, &backup); ---- Size the buffer with `jlSpriteBackupLength`, and declare it as `uint16_t` (or otherwise 2-byte aligned): on the Amiga, ST, and X68000 an odd address sends save and restore down a much slower path, and it crashes the trusted variants. ==== The Clean-Buffer Pattern Keep a pristine copy of the background, and restore rectangles from it. [source,c] ---- jlSurfaceT *clean = jlSurfaceCreate(); // ... draw the static scene into clean, once ... // each frame: jlSurfaceCopyRect(stage, clean, x, y, w, h); // erase EVERY region first // ... then draw ALL sprites ... ---- This scales better with many sprites, and it is the right choice when sprites overlap. Erase every region first, then draw every sprite -- never interleave erase and draw. [WARNING] ==== `jlSurfaceCopyRect` widens the copied region outward to the 16-pixel group grid on *every* port, so it can touch up to 15 pixels either side of the rectangle you asked for. That means it can overwrite content just outside your rectangle, and it silently invalidates any live `jlSpriteBackupT` whose recorded region it touches. This is exactly why the two patterns must not be mixed in one region. ====