// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // Sprite demo: bounces a 16x16 ball using the jlSpriteT API. The ball // art is authored as a 16x16 4bpp packed image in this file (one row // per source line) and converted at startup into the 2x2 tile layout // jlSpriteT expects. jlSpriteDraw handles transparent-color-0; we use // jlSpriteSaveUnder/RestoreUnder to undraw the previous frame's ball // without redrawing the whole screen. The ball's speed is given per // second and each frame moves it by the time since the last one // (jlMillisElapsed), so it crosses the screen as fast on every port. #include #include #define BALL_W 16 #define BALL_H 16 #define BALL_TILES_X (BALL_W / 8) #define BALL_TILES_Y (BALL_H / 8) #define BALL_TILE_BYTES (BALL_TILES_X * BALL_TILES_Y * JL_TILE_BYTES) // The save-under buffer is sized by jlSpriteBackupLength (joey/sprite.h): // the worst case at any x, on every port. #define BALL_PALETTE_IDX 0 #define BALL_SPEED_X 120 // pixels per second, the same on every port #define BALL_SPEED_Y 60 #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 #define MILLIS_PER_SECOND 1000 #define BALL_X_MAX ((int32_t)(JL_SURFACE_WIDTH - BALL_W) << SUBPIXEL_SHIFT) #define BALL_Y_MAX ((int32_t)(JL_SURFACE_HEIGHT - BALL_H) << SUBPIXEL_SHIFT) #define COLOR_BG 0 // Authored layout: 16 pixels wide x 16 rows = 8 bytes/row x 16 rows. // 0 = transparent // 2 = ball body (yellow) // 3 = highlight (white) // High nibble of each byte is the LEFT pixel. static const uint8_t gBallAuthored[BALL_H * (BALL_W / 2)] = { 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, // row 0 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, // row 1 0x02, 0x22, 0x32, 0x22, 0x22, 0x22, 0x22, 0x20, // row 2 0x02, 0x23, 0x32, 0x22, 0x22, 0x22, 0x22, 0x20, // row 3 0x22, 0x33, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 4 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 5 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 6 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 7 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 8 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 9 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // row 10 0x02, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x20, // row 11 0x02, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x20, // row 12 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, // row 13 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, // row 14 0x00, 0x00, 0x00, 0x22, 0x22, 0x00, 0x00, 0x00 // row 15 }; static uint8_t gBallTiles[BALL_TILE_BYTES]; static uint16_t gBallBackup[jlSpriteBackupLength(BALL_W / 8, BALL_H / 8)]; // Repack from authored "wide row" layout to tile-major jlSpriteT // layout: tile (0,0), tile (1,0), tile (0,1), tile (1,1), each tile // internally stored row-by-row at 4 bytes per row. static void repackBallTiles(void) { uint16_t tx; uint16_t ty; uint16_t row; uint16_t b; uint8_t *dst; for (ty = 0; ty < BALL_TILES_Y; ty++) { for (tx = 0; tx < BALL_TILES_X; tx++) { dst = &gBallTiles[(ty * BALL_TILES_X + tx) * JL_TILE_BYTES]; for (row = 0; row < 8; row++) { for (b = 0; b < 4; b++) { dst[row * 4 + b] = gBallAuthored[((ty * 8) + row) * (BALL_W / 2) + (tx * 4) + b]; } } } } } static void buildPalette(jlSurfaceT *screen) { uint16_t colors[16]; uint16_t i; for (i = 0; i < 16; i++) { colors[i] = 0x0000; } colors[2] = 0x0FF0; // yellow body colors[3] = 0x0FFF; // white highlight jlPaletteSet(screen, BALL_PALETTE_IDX, colors); } int main(void) { jlConfigT config; jlSurfaceT *screen; jlSpriteT *ball; jlSpriteBackupT backup; int32_t x; // 1/256 of a pixel int32_t y; int32_t vx; // pixels per second int32_t vy; uint32_t lastMillis; uint32_t now; uint32_t elapsed; /* Amiga planar emits 8 pre-shifted DRAW variants per sprite (one * per x % 8 alignment) so the codegen arena needs roughly 8x what * the chunky two-shift case asks for. 32 KB fits a 16x16 ball * with all variants. */ config.codegenBytes = 32UL * 1024; config.noAudio = true; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); return 1; } screen = jlStageGet(); if (screen == NULL) { fprintf(stderr, "jlStageGet returned NULL\n"); jlShutdown(); return 1; } repackBallTiles(); ball = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y); if (ball == NULL) { fprintf(stderr, "jlSpriteCreate failed\n"); jlShutdown(); return 1; } // Compile draw routines into the codegen arena. Returns false on // platforms that don't have a real emitter or if the arena is // full -- either way the demo still runs via the interpreter // path in jlSpriteDraw. (void)jlSpriteCompile(ball); buildPalette(screen); jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, BALL_PALETTE_IDX); jlSurfaceClear(screen, COLOR_BG); jlStagePresent(); backup.bytes = (uint8_t *)gBallBackup; x = (int32_t)40 << SUBPIXEL_SHIFT; y = (int32_t)30 << SUBPIXEL_SHIFT; vx = BALL_SPEED_X; vy = BALL_SPEED_Y; jlSpriteSaveAndDraw(screen, ball, (int16_t)(x >> SUBPIXEL_SHIFT), (int16_t)(y >> SUBPIXEL_SHIFT), &backup); jlStagePresent(); lastMillis = jlMillisElapsed(); for (;;) { jlInputPoll(); if (jlKeyPressed(JL_KEY_ESCAPE)) { break; } // Do all off-screen work first (restore + move + draw), then // ONE jlStagePresent flushes the union of dirty bands set by // restoreUnder + draw. Add a jlWaitVBL() before the present // to land it inside the VBL window so the CRT never sees a // half-updated framebuffer (matters most on single-buffered // chunky targets like IIgs SHR; on planar c2p platforms it // also avoids c2p racing the raster). VBL wait is omitted // here so the demo runs at the sprite pipeline's native // throughput -- expect tearing on the ball. The ball's speed // does not depend on it: it moves by the time each frame took. jlSpriteRestoreUnder(screen, &backup); now = jlMillisElapsed(); elapsed = now - lastMillis; lastMillis = now; if (elapsed > MAX_STEP_MILLIS) { elapsed = MAX_STEP_MILLIS; } x += (vx * (int32_t)elapsed * ((int32_t)1 << SUBPIXEL_SHIFT)) / MILLIS_PER_SECOND; y += (vy * (int32_t)elapsed * ((int32_t)1 << SUBPIXEL_SHIFT)) / MILLIS_PER_SECOND; // A wall sets the direction rather than flipping it, so a frame // that moved nothing cannot turn the ball back twice. if (x <= 0) { x = 0; vx = BALL_SPEED_X; } if (x >= BALL_X_MAX) { x = BALL_X_MAX; vx = -BALL_SPEED_X; } if (y <= 0) { y = 0; vy = BALL_SPEED_Y; } if (y >= BALL_Y_MAX) { y = BALL_Y_MAX; vy = -BALL_SPEED_Y; } jlSpriteSaveAndDraw(screen, ball, (int16_t)(x >> SUBPIXEL_SHIFT), (int16_t)(y >> SUBPIXEL_SHIFT), &backup); jlStagePresent(); } jlSpriteDestroy(ball); jlShutdown(); return 0; }