289 lines
12 KiB
Markdown
289 lines
12 KiB
Markdown
|
|
# 03. Getting started
|
|||
|
|
|
|||
|
|
This chapter gets libakgl into your build and a window on your screen. It assumes you have
|
|||
|
|
read [Chapter 4](04-errors.md), because the first program is written in the error protocol
|
|||
|
|
and will not make sense otherwise.
|
|||
|
|
|
|||
|
|
## Dependencies
|
|||
|
|
|
|||
|
|
| Dependency | Floor | Why libakgl needs it |
|
|||
|
|
|---|---|---|
|
|||
|
|
| SDL3 | 3.4.x (3.4.8 is vendored) | Window, renderer, events, properties, timing, mutexes |
|
|||
|
|
| SDL3_image | vendored | Decoding spritesheet and tileset images |
|
|||
|
|
| SDL3_mixer | vendored | `akgl_load_start_bgm`, the mixer device and track table |
|
|||
|
|
| SDL3_ttf | vendored | Font loading, rasterizing and metrics |
|
|||
|
|
| jansson | vendored | Parsing every JSON asset format |
|
|||
|
|
| libakerror | **>= 2.0.1, hard floor** | The error protocol. Part of libakgl's *public* interface |
|
|||
|
|
| libakstdlib | >= 0.2 | Checked wrappers over libc — `aksl_strncpy`, `aksl_fclose`, `aksl_snprintf` |
|
|||
|
|
| semver | vendored, in-tree | Savegame and version comparison |
|
|||
|
|
|
|||
|
|
All eight are git submodules under `deps/`, so a recursive clone gives you a build with no
|
|||
|
|
system packages at all.
|
|||
|
|
|
|||
|
|
**The libakerror floor is enforced, not advisory.** Two `#error` feature tests in
|
|||
|
|
`include/akgl/error.h` fire on a stale header:
|
|||
|
|
|
|||
|
|
```c excerpt=include/akgl/error.h
|
|||
|
|
#ifndef AKERR_EXIT_STATUS_UNREPRESENTABLE
|
|||
|
|
#error "libakgl requires libakerror >= 2.0.1: the akerror.h on the include path predates akerr_exit(). Rebuild and reinstall libakerror."
|
|||
|
|
#endif
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
It is a hard floor because libakerror 2.0.0 made `akerr_next_error()` return a context that
|
|||
|
|
already holds its reference and moved `__akerr_last_ignored` into thread-local storage —
|
|||
|
|
and both of those expand at **your** call sites, through `IGNORE` and the `FAIL_*` macros,
|
|||
|
|
because `akerror.h` is part of libakgl's public interface. A tree that mixes headers and
|
|||
|
|
libraries across that line double-counts every reference and never returns a pool slot. The
|
|||
|
|
soname moved to `libakerror.so.2` to stop the library half happening by accident; the
|
|||
|
|
`#error` catches the header half in an install tree.
|
|||
|
|
|
|||
|
|
## Building libakgl itself
|
|||
|
|
|
|||
|
|
```sh norun
|
|||
|
|
git clone --recursive https://source.starfort.tech/andrew/libakgl.git
|
|||
|
|
cd libakgl
|
|||
|
|
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
|||
|
|
cmake --build build --parallel
|
|||
|
|
ctest --test-dir build --output-on-failure
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
If a dependency is already installed system-wide, libakgl finds it and skips the vendored
|
|||
|
|
copy. The rule is `if(NOT TARGET ...)` per dependency, so mixing is fine: a system SDL3 with
|
|||
|
|
a vendored jansson works.
|
|||
|
|
|
|||
|
|
## Route 1 — `add_subdirectory()`
|
|||
|
|
|
|||
|
|
This is what `akbasic` does, and it is the route with the fewest ways to go wrong: one set
|
|||
|
|
of compiler flags, one set of headers, nothing to install.
|
|||
|
|
|
|||
|
|
```cmake
|
|||
|
|
add_subdirectory(third_party/libakgl)
|
|||
|
|
|
|||
|
|
add_executable(mygame src/main.c)
|
|||
|
|
target_link_libraries(mygame PRIVATE akgl::akgl)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`akgl::akgl` carries its own usage requirements, so the SDL3, libakerror and jansson include
|
|||
|
|
paths reach your target without you naming them.
|
|||
|
|
|
|||
|
|
**`AKGL_WERROR` is `OFF` by default, and leaving it off is the right thing for a consumer.**
|
|||
|
|
It is turned on only by two CI jobs in this repository. The reason is exactly your build: a
|
|||
|
|
target-level `-Werror` turns every diagnostic a newer compiler invents into a broken build
|
|||
|
|
for somebody else's project, and the warning set is not even stable across optimization
|
|||
|
|
levels — an `-O2` build reports ten `-Wstringop-truncation` warnings that `-O0` and
|
|||
|
|
`-fsyntax-only` do not. `-Wall` is on for every target this project owns, and the tree builds
|
|||
|
|
clean under it at every optimization level.
|
|||
|
|
|
|||
|
|
If you have already vendored SDL3 yourself, declare it before the `add_subdirectory` and
|
|||
|
|
libakgl will use yours rather than adding a second copy:
|
|||
|
|
|
|||
|
|
```cmake
|
|||
|
|
add_subdirectory(third_party/SDL) # declares SDL3::SDL3
|
|||
|
|
add_subdirectory(third_party/libakgl) # sees it and does not add its own
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Route 2 — pkg-config
|
|||
|
|
|
|||
|
|
`make install` puts `akgl.pc` in `lib/pkgconfig/`. Here is what it contains, in full:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
prefix=/usr/local
|
|||
|
|
exec_prefix=${prefix}
|
|||
|
|
libdir=${exec_prefix}/lib
|
|||
|
|
includedir=${exec_prefix}/include
|
|||
|
|
|
|||
|
|
Name: akgl
|
|||
|
|
Description: AKLabs Game library
|
|||
|
|
Version: 0.7.0
|
|||
|
|
Cflags: -I${includedir}/
|
|||
|
|
Libs: -L${libdir} -lakgl
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**`akgl.pc` names no dependencies at all.** There is no `Requires:` and no
|
|||
|
|
`Requires.private:` line, so `pkg-config --cflags --libs akgl` tells you nothing about
|
|||
|
|
libakerror, libakstdlib, SDL3, SDL3_image, SDL3_mixer, SDL3_ttf or jansson. That has always
|
|||
|
|
been wrong, and it is sharper than it looks: `akerror.h` is part of libakgl's *public*
|
|||
|
|
interface, so a consumer that builds against whatever `akerror.h` is on its include path and
|
|||
|
|
links whatever `libakerror.so` the loader finds can get exactly the mismatch pkg-config had
|
|||
|
|
every opportunity to prevent.
|
|||
|
|
|
|||
|
|
Until that is fixed, name them yourself. These are the modules whose headers libakgl's own
|
|||
|
|
public headers include:
|
|||
|
|
|
|||
|
|
```sh norun
|
|||
|
|
pkg-config --cflags --libs akgl akerror akstdlib sdl3 sdl3-ttf sdl3-mixer jansson
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`sdl3-image` is needed at link time but appears in no public header, so it belongs in the
|
|||
|
|
private half.
|
|||
|
|
|
|||
|
|
Recorded in `TODO.md` under "akgl.pc names no dependencies", along with why the fix wants
|
|||
|
|
its own commit: adding a `Requires:` line changes what `pkg-config --libs akgl` emits for
|
|||
|
|
every existing consumer.
|
|||
|
|
|
|||
|
|
**There is no CMake package config.** `install()` ships the library, the headers and
|
|||
|
|
`akgl.pc`, and nothing generates an `akglConfig.cmake`, so `find_package(akgl)` does not
|
|||
|
|
work against an install tree. Use `add_subdirectory()` or pkg-config.
|
|||
|
|
|
|||
|
|
## The startup order that actually works
|
|||
|
|
|
|||
|
|
Verified against `src/game.c`, not against the header comment:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
1. fill in akgl_game.name, .version and .uri
|
|||
|
|
| akgl_game_init refuses to run without all three, and there are no
|
|||
|
|
| defaults -- the window title, SDL's app metadata and the savegame
|
|||
|
|
| compatibility check are all built from them
|
|||
|
|
v
|
|||
|
|
2. akgl_game_init()
|
|||
|
|
| error codes, version stamp, frame clock, state mutex, pools, the eight
|
|||
|
|
| registries, SDL app metadata, control maps, SDL_Init(VIDEO|GAMEPAD|
|
|||
|
|
| AUDIO), controller DB, gamepads, SDL_mixer, SDL_ttf, and finally the
|
|||
|
|
| four globals pointed at their default storage.
|
|||
|
|
|
|
|||
|
|
| What it does NOT do: create the window, choose a physics backend, or
|
|||
|
|
| load any configuration. Those read properties, which you have not set
|
|||
|
|
| yet.
|
|||
|
|
v
|
|||
|
|
3. akgl_registry_load_properties() or akgl_set_property()
|
|||
|
|
| screen size, physics constants, anything else
|
|||
|
|
v
|
|||
|
|
4. akgl_render_2d_init(akgl_renderer) <- creates the window; reads
|
|||
|
|
| game.screenwidth/screenheight
|
|||
|
|
| akgl_physics_factory(akgl_physics, name) <- "null" or "arcade"
|
|||
|
|
v
|
|||
|
|
5. load assets, then loop on akgl_game_update()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**`akgl_game_init` does not choose a physics backend for you.** `physics.h` says the
|
|||
|
|
`physics.engine` property drives `akgl_physics_factory`; it does not. That string appears
|
|||
|
|
nowhere in `src/`, and `akgl_game_init` never calls the factory. You call it yourself, with
|
|||
|
|
an `akgl_String` holding `"null"` or `"arcade"`. The header comment is wrong and is left
|
|||
|
|
alone here deliberately — this manual documents what the code does, and correcting header
|
|||
|
|
prose is a separate commit.
|
|||
|
|
|
|||
|
|
## The first window
|
|||
|
|
|
|||
|
|
Sixty frames of an empty world, then out. It compiles under
|
|||
|
|
`-std=gnu99 -Wall -Werror` and runs headless.
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
#include <akerror.h>
|
|||
|
|
#include <akstdlib.h>
|
|||
|
|
#include <akgl/game.h>
|
|||
|
|
#include <akgl/heap.h>
|
|||
|
|
#include <akgl/physics.h>
|
|||
|
|
#include <akgl/registry.h>
|
|||
|
|
#include <akgl/renderer.h>
|
|||
|
|
#include <akgl/staticstring.h>
|
|||
|
|
#include <akgl/text.h>
|
|||
|
|
|
|||
|
|
int main(void)
|
|||
|
|
{
|
|||
|
|
akgl_String *engine = NULL;
|
|||
|
|
int frame = 0;
|
|||
|
|
|
|||
|
|
PREPARE_ERROR(errctx);
|
|||
|
|
|
|||
|
|
ATTEMPT {
|
|||
|
|
/* 1. akgl_game_init refuses to run without all three of these. */
|
|||
|
|
CATCH(errctx, aksl_strncpy(akgl_game.name, sizeof(akgl_game.name),
|
|||
|
|
"firstwindow", sizeof(akgl_game.name) - 1));
|
|||
|
|
CATCH(errctx, aksl_strncpy(akgl_game.version, sizeof(akgl_game.version),
|
|||
|
|
"0.1.0", sizeof(akgl_game.version) - 1));
|
|||
|
|
CATCH(errctx, aksl_strncpy(akgl_game.uri, sizeof(akgl_game.uri),
|
|||
|
|
"net.example.firstwindow", sizeof(akgl_game.uri) - 1));
|
|||
|
|
|
|||
|
|
/* 2. SDL, the pools, the registries, audio, fonts, gamepads. */
|
|||
|
|
CATCH(errctx, akgl_game_init());
|
|||
|
|
|
|||
|
|
/* 3. Configuration. akgl_render_2d_init reads these two. */
|
|||
|
|
CATCH(errctx, akgl_set_property("game.screenwidth", "640"));
|
|||
|
|
CATCH(errctx, akgl_set_property("game.screenheight", "480"));
|
|||
|
|
|
|||
|
|
/* 4. The window and the renderer, then a physics backend. */
|
|||
|
|
CATCH(errctx, akgl_render_2d_init(akgl_renderer));
|
|||
|
|
CATCH(errctx, akgl_heap_next_string(&engine));
|
|||
|
|
CATCH(errctx, akgl_string_initialize(engine, "null"));
|
|||
|
|
CATCH(errctx, akgl_physics_factory(akgl_physics, engine));
|
|||
|
|
|
|||
|
|
/* 5. Load assets, then loop. */
|
|||
|
|
for ( frame = 0; frame < 60; frame++ ) {
|
|||
|
|
CATCH(errctx, akgl_game_update(NULL));
|
|||
|
|
}
|
|||
|
|
} CLEANUP {
|
|||
|
|
IGNORE(akgl_heap_release_string(engine));
|
|||
|
|
IGNORE(akgl_text_unloadallfonts());
|
|||
|
|
} PROCESS(errctx) {
|
|||
|
|
} FINISH_NORETURN(errctx);
|
|||
|
|
|
|||
|
|
SDL_Quit();
|
|||
|
|
return 0;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Five things in there are worth pointing at.
|
|||
|
|
|
|||
|
|
**`aksl_strncpy`, not `strncpy`.** Every name in this library is a registry key, handed to
|
|||
|
|
`strcmp`, `realpath` and SDL property calls, none of which stop at the end of the field.
|
|||
|
|
`strncpy(dest, src, sizeof(dest))` leaves `dest` unterminated whenever the source fills it.
|
|||
|
|
Bound `n` at `sizeof(dest) - 1` and let `aksl_strncpy` raise `AKERR_OUTOFBOUNDS` if the bytes
|
|||
|
|
do not fit.
|
|||
|
|
|
|||
|
|
**`FINISH_NORETURN`, not `FINISH`, in `main`.** `FINISH` ends by returning the error context
|
|||
|
|
from the enclosing function, and `main` returns `int`. With `FINISH_NORETURN` and no
|
|||
|
|
`HANDLE` block, an unhandled error goes to libakerror's default handler, which logs it and
|
|||
|
|
calls `akerr_exit()` — so `SDL_Quit()` and `return 0` are reached only on the success path.
|
|||
|
|
|
|||
|
|
**Never `exit(3)` an akerr status.** An exit status is one byte wide and libakgl's band
|
|||
|
|
starts at 256, so `exit(AKGL_ERR_SDL)` is a wait status of 0 and the shell sees a clean run.
|
|||
|
|
`akerr_exit()` maps 0 to 0, 1–255 to themselves, and anything else to
|
|||
|
|
`AKERR_EXIT_STATUS_UNREPRESENTABLE` (125). See [Chapter 4](04-errors.md), Table 3.
|
|||
|
|
|
|||
|
|
**`akgl_text_unloadallfonts()` before `SDL_Quit()`.** `SDL_Quit` destroys the property
|
|||
|
|
registry the fonts live in and takes the last reference to every one of them with it. This
|
|||
|
|
program loads no fonts, and the call is in the shutdown anyway because that is where it
|
|||
|
|
belongs the moment one is loaded — [Chapter 16](16-text-and-fonts.md).
|
|||
|
|
|
|||
|
|
**Sixty `akgl_game_update` calls log `Low FPS! 0` sixty times.** `akgl_game.fps` is a
|
|||
|
|
completed-second average, so it reads 0 for the first second of the process, which is under
|
|||
|
|
the low-FPS threshold. `akgl_game.lowfpsfunc` is a hook for exactly this: point it at
|
|||
|
|
something that sheds work, or at nothing.
|
|||
|
|
|
|||
|
|
## Running headless
|
|||
|
|
|
|||
|
|
On a server, in CI, or under valgrind, force the dummy drivers:
|
|||
|
|
|
|||
|
|
```sh norun
|
|||
|
|
SDL_VIDEO_DRIVER=dummy SDL_RENDER_DRIVER=software SDL_AUDIO_DRIVER=dummy ./mygame
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
These are the SDL3 spellings, and the ones `scripts/memcheck.sh` exports. SDL3 also still
|
|||
|
|
honours SDL2's `SDL_VIDEODRIVER` and `SDL_AUDIODRIVER` as aliases — `SDL_RENDER_DRIVER` was
|
|||
|
|
never spelled any other way. Without them, `akgl_game_init` fails at `SDL_Init` with
|
|||
|
|
`AKGL_ERR_SDL` and the message `Couldn't initialize SDL: No available video device`.
|
|||
|
|
|
|||
|
|
A program can set the same thing from inside itself with `SDL_SetHint` before
|
|||
|
|
`akgl_game_init`, which is what most of the test suites in `tests/` do:
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
#include <SDL3/SDL.h>
|
|||
|
|
|
|||
|
|
void go_headless(void)
|
|||
|
|
{
|
|||
|
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
|||
|
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
|||
|
|
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The software renderer is a real rasterizer, not a stub — everything draws, nothing is
|
|||
|
|
displayed, and `SDL_RenderReadPixels` gives you the frame back. That is how the image
|
|||
|
|
assertions in `tests/` work.
|
|||
|
|
|
|||
|
|
## Where to go next
|
|||
|
|
|
|||
|
|
[Chapter 5](05-the-heap.md) for the pools you just claimed a string from,
|
|||
|
|
[Chapter 7](07-the-game-and-the-frame.md) for what `akgl_game_update` actually does, or
|
|||
|
|
[Chapter 19](19-tutorial-sidescroller.md) to start building a game.
|