joeylib2/examples/sprite/sprite.c

218 lines
7.7 KiB
C

// 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 <stdio.h>
#include <joey/joey.h>
#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(uint16_t, 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;
}