== 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 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, 32 KB on Amiga, ST and DOS. * 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. * On the IIgs the arena is one Memory Manager block that may not cross a 64 KB bank boundary, so requests above 65536 are clamped to 65536 -- and even a clamped request can fail if no free bank exists. If the arena is too small, sprites still draw: they fall back to an interpreter that is roughly *forty times slower*. 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[SURFACE_COLORS_PER_PALETTE]; // ... fill in 16 entries of $0RGB ... jlPaletteSet(stage, 0, colors); jlScbSetRange(stage, 0, 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[SURFACE_COLORS_PER_PALETTE]; uint16_t count = jlSpriteBankLoad("player.spr", cels, 16, palette); uint16_t i; for (i = 0; i < count; i++) { jlSpritePrewarm(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` or `jlSpritePrewarm` 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(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. It is cheap on every port because the underlying mechanism is always a hardware wait, never a software timer. `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()` is a no-op on ports whose audio runs from a hardware interrupt, but it is required on ports that mix in user context, and it is what advances tracker music and keeps streaming sound effects fed. Call it unconditionally, 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. [NOTE] ==== If you write pixels by some route other than the library's primitives, the dirty tracking does not know about it, and `jlStagePresent` will skip those rows. That is the one case where you have to mark the region yourself. ==== ==== Frame-Rate Independence The five targets do not share a refresh rate: 50 Hz (Amiga PAL), 60 Hz (IIgs, ST NTSC, X68000) and 70 Hz (VGA mode 13h). A loop that moves a sprite "two pixels per frame" runs 40 percent faster on DOS than on a PAL Amiga. Two ways to handle it. Ask the frame rate and scale your per-frame step: [source,c] ---- uint16_t hz = jlFrameHz(); // 50, 60 or 70 ---- Or drive from the millisecond clock, which is the more robust choice for anything that must feel the same everywhere: [source,c] ---- uint32_t now = jlMillisElapsed(); uint32_t step = now - previous; previous = now; ---- `jlMillisElapsed()` reports true elapsed wall-clock time, so a frame that took a long time to render is accounted for correctly rather than counted as one tick. === Part 4: Shut Down [source,c] ---- jlAudioShutdown(); // if you called jlAudioInit jlShutdown(); return 0; ---- `jlShutdown` restores the display mode, frees the library's allocations, and un-hooks interrupt vectors. Sprites and surfaces you created are released with it, but 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 all five targets as-is. [source,c] ---- // A minimal but complete JoeyLib program. #include #define PLAYER_SPEED 2 static jlSurfaceT *gStage; static bool gRunning = true; static int16_t gPlayerX = 160; static int16_t gPlayerY = 100; static void buildPalette(void); static void handleInput(void); static void renderFrame(void); static void buildPalette(void) { uint16_t colors[SURFACE_COLORS_PER_PALETTE]; uint8_t i; for (i = 0; i < 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, SURFACE_HEIGHT - 1, 0); } static void handleInput(void) { if (jlKeyPressed(KEY_ESCAPE)) { gRunning = false; } if (jlKeyDown(KEY_LEFT)) { gPlayerX -= PLAYER_SPEED; } if (jlKeyDown(KEY_RIGHT)) { gPlayerX += PLAYER_SPEED; } if (jlKeyDown(KEY_UP)) { gPlayerY -= PLAYER_SPEED; } if (jlKeyDown(KEY_DOWN)) { gPlayerY += PLAYER_SPEED; } // A joystick, if one is attached, drives the same movement. if (jlJoystickConnected(JOYSTICK_0)) { if (jlJoystickX(JOYSTICK_0) < -64) { gPlayerX -= PLAYER_SPEED; } if (jlJoystickX(JOYSTICK_0) > 64) { gPlayerX += PLAYER_SPEED; } } // Keep the player on screen: the primitives would clip safely, but // clamping keeps the object whole. if (gPlayerX < 0) { gPlayerX = 0; } if (gPlayerX > SURFACE_WIDTH - 16) { gPlayerX = SURFACE_WIDTH - 16; } if (gPlayerY < 0) { gPlayerY = 0; } if (gPlayerY > SURFACE_HEIGHT - 16) { gPlayerY = SURFACE_HEIGHT - 16; } } static void renderFrame(void) { jlSurfaceClear(gStage, 0); jlDrawRect(gStage, 0, 0, SURFACE_WIDTH, SURFACE_HEIGHT, 1); jlFillRect(gStage, gPlayerX, gPlayerY, 16, 16, 2); } int main(void) { jlConfigT config; config.codegenBytes = 32UL * 1024; if (!jlInit(&config)) { return 1; } gStage = jlStageGet(); if (gStage == NULL) { jlShutdown(); return 1; } buildPalette(); jlRandomSeed(12345); // deterministic on every port while (gRunning) { jlWaitVBL(); jlInputPoll(); jlAudioFrameTick(); handleInput(); 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[JOEY_SPRITE_BACKUP_BYTES(2, 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 the `JOEY_SPRITE_BACKUP_BYTES` macro, and declare it as `uint16_t` (or otherwise 2-byte aligned) -- an odd address pushes save and restore onto slow interpreted paths on the planar ports, because the 68000 cannot do word accesses at odd addresses. ==== 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 window first // ... then draw ALL sprites ... ---- This scales better with many sprites, and it is the right choice when sprites overlap. Erase every window 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 window it touches. This is exactly why the two patterns must not be mixed in one region. ====