359 lines
10 KiB
Text
359 lines
10 KiB
Text
== 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, DOS and DVX.
|
|
* 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 targets do not share a refresh rate: 50 Hz (Amiga PAL), 55 Hz
|
|
(X68000), 60 Hz (IIgs, ST NTSC) 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.
|
|
|
|
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 every target as-is.
|
|
|
|
[source,c]
|
|
----
|
|
// A minimal but complete JoeyLib program.
|
|
|
|
#include <joey/joey.h>
|
|
|
|
#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.
|
|
====
|