389 lines
12 KiB
Text
389 lines
12 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
|
|
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 <stddef.h>
|
|
|
|
#include <joey/joey.h>
|
|
|
|
#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 *jlgStage;
|
|
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(jlgStage, 0, colors);
|
|
jlScbSetRange(jlgStage, 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(jlgStage, 0);
|
|
jlDrawRect(jlgStage, 0, 0, JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, 1);
|
|
jlFillRect(jlgStage, (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;
|
|
}
|
|
|
|
jlgStage = jlStageGet();
|
|
if (jlgStage == 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.
|
|
====
|