Compare commits

..
5 Commits
Author SHA1 Message Date
anth64 26d08228a7 feat: update diagnostic logs for skele work/total/budget split 2026-08-24 17:23:44 +02:00
anth64 605d34777b feat: log tick/idle diagnostics in example server
Brackets game_tick() with skele_clock_tick_begin/end and the
sleep-to-hit-tick-rate call with skele_clock_idle_begin/end.
2026-08-23 17:44:35 +02:00
anth64 9ba3494eac feat: log fps/frame/tick diagnostics in example clients
Once/sec via stk_log, using skele's new fps.h/clock.h counters.
2026-08-23 17:44:33 +02:00
anth64 829ba66782 feat: opt in to vsync-priority frame pacing
skele's video_cfg gained a vsync flag and frame cap; without it these
clients now run fully uncapped instead of the old hardcoded vsync-on.
2026-08-23 17:08:18 +02:00
anth64 2c4ff25c04 docs: write README 2026-08-19 22:11:22 +02:00
4 changed files with 153 additions and 6 deletions
+102 -1
View File
@@ -1,4 +1,105 @@
# scaffold # scaffold
A game engine template built on skele and stk. [![License: BSD 3-Clause](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
`scaffold` is a minimal starter template for building a game on top of [`skele`](https://git.forlornoutpost.ca/anth64/skele) and [`stk`](https://git.forlornoutpost.ca/anth64/stk). It is not an engine of its own, it is the smallest working skeleton that wires the two together: a fixed-timestep game loop, hot-reload polling, and three build targets (software client, OpenGL client, headless server) sharing one game module.
Copy this repository to bootstrap a new game and start filling in `src/game.c`.
---
## Key Features
- **Three binaries from one game module**: software client, OpenGL client, and headless server, all calling the same `game_init`/`game_tick`/`game_shutdown` contract
- **Fixed-timestep accumulator loop** wired to `skele_tick_ns`, correct across variable frame times
- **`stk` hot-reload wired in from the start**, polled every loop iteration
- **Static or dynamic linking** against skele/stk via `LINK_TYPE`
- **Cross-platform build** (Linux, BSD, macOS, Windows, Raspberry Pi 5 GL auto-detect)
---
## Quick Start
### Building
```bash
# Unix (Linux/BSD/macOS)
./build.sh debug release
# Windows
build.bat debug release
```
Builds three binaries into `bin/{debug,release}`:
- `<game>_client` - software rendering client
- `<game>_glclient` - OpenGL client
- `<game>_server` - headless server, no video/input linked
Set `LINK_TYPE=dynamic` (default `static`) to link against shared `libskele`/`libstk` instead:
```bash
./build.sh LINK_TYPE=dynamic debug release
```
### Running
```bash
./build.sh run # software client
./build.sh run_gl # OpenGL client
```
### Installation
```bash
./build.sh install
```
Installs to `/usr` on Linux, `/usr/local` on BSD/macOS by default. Use `PREFIX` to customize:
```bash
./build.sh PREFIX=$HOME/.local install
```
---
## Usage
### Starting a New Game
1. Rename `GAME_NAME` in `config.mk` (used to name the built binaries).
2. Implement `game_init`, `game_tick`, and `game_shutdown` in `src/game.c`.
3. Add any additional source files to `GAME_SRCS` in `config.mk`, they are compiled into all three binaries.
### The Game Contract (`include/game.h`)
```c
uint8_t game_init(void); /* return 0 to abort startup */
void game_tick(void); /* called once per simulation tick */
void game_shutdown(void); /* called once on exit */
```
This is the entire surface a game needs to implement. Rendering, input, windowing, and hot-reload are all handled by the client `main.c` files calling into `skele`/`stk`, not by the game module itself.
### Client Loop (software and GL)
Both `src/client/main.c` and `src/gl_client/main.c` follow the same pattern: init clock and signal handling, set up `stk`, init `skele`, init the game, then run a fixed-timestep accumulator loop polling input and `stk` each frame before presenting. They are kept as separate files so each can diverge independently as you add renderer-specific setup (window flags, GL context options, and so on) without one client's changes leaking into the other.
### Server Loop
`src/server/main.c` runs the same accumulator pattern without any video/input calls, calling only `stk_poll()` and `game_tick()` each iteration, then sleeping out the remainder of the tick.
---
## Project Status
Very early. This is a starter template, not a finished project. `game.c` ships as an empty stub (`game_init` returns success, `game_tick`/`game_shutdown` do nothing). No automated tests.
---
## License
BSD 3-Clause License
---
## Contributing
Contributions welcome! Please ensure code follows C99 standard and works across all supported platforms.
+17 -2
View File
@@ -2,8 +2,10 @@
#include <skele/client/input.h> #include <skele/client/input.h>
#include <skele/client/video.h> #include <skele/client/video.h>
#include <skele/clock.h> #include <skele/clock.h>
#include <skele/fps.h>
#include <skele/skele.h> #include <skele/skele.h>
#include <stk/stk.h> #include <stk/stk.h>
#include <stk/stk_log.h>
static uint8_t running = 1; static uint8_t running = 1;
@@ -12,7 +14,7 @@ static void on_signal(void) { running = 0; }
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
skele_video_config_t video_cfg; skele_video_config_t video_cfg;
uint64_t last, now, elapsed, accum = 0; uint64_t last, now, elapsed, accum = 0, last_print = 0;
skele_clock_init(on_signal); skele_clock_init(on_signal);
@@ -28,7 +30,8 @@ int main(int argc, char *argv[])
video_cfg.render_height = SKELE_DEFAULT_RENDER_HEIGHT; video_cfg.render_height = SKELE_DEFAULT_RENDER_HEIGHT;
video_cfg.window_width = 0; video_cfg.window_width = 0;
video_cfg.window_height = 0; video_cfg.window_height = 0;
video_cfg.flags = 0; video_cfg.max_fps = 0;
video_cfg.flags = SKELE_VIDEO_VSYNC;
if (skele_video_init(video_cfg) != SKELE_INIT_SUCCESS) { if (skele_video_init(video_cfg) != SKELE_INIT_SUCCESS) {
skele_stk_teardown(); skele_stk_teardown();
@@ -64,6 +67,18 @@ int main(int argc, char *argv[])
} }
skele_video_present(); skele_video_present();
if (now - last_print >= 1000000000ULL) {
stk_log(STK_LOG_INFO,
"fps=%.1f frame=%.2fms/%.2fms/%.2fms tps=%.1f tick=%lluns/%lluns/%lluns",
skele_fps, skele_frame_work_time_ms,
skele_frame_render_time_ms, skele_frame_budget_ms,
skele_tps,
(unsigned long long)skele_tick_work_time_ns,
(unsigned long long)skele_tick_total_time_ns,
(unsigned long long)skele_tick_ns);
last_print = now;
}
} }
game_shutdown(); game_shutdown();
+17 -2
View File
@@ -2,8 +2,10 @@
#include <skele/client/input.h> #include <skele/client/input.h>
#include <skele/client/video.h> #include <skele/client/video.h>
#include <skele/clock.h> #include <skele/clock.h>
#include <skele/fps.h>
#include <skele/skele.h> #include <skele/skele.h>
#include <stk/stk.h> #include <stk/stk.h>
#include <stk/stk_log.h>
static uint8_t running = 1; static uint8_t running = 1;
@@ -12,7 +14,7 @@ static void on_signal(void) { running = 0; }
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
skele_video_config_t video_cfg; skele_video_config_t video_cfg;
uint64_t last, now, elapsed, accum = 0; uint64_t last, now, elapsed, accum = 0, last_print = 0;
skele_clock_init(on_signal); skele_clock_init(on_signal);
@@ -28,7 +30,8 @@ int main(int argc, char *argv[])
video_cfg.render_height = SKELE_DEFAULT_RENDER_HEIGHT; video_cfg.render_height = SKELE_DEFAULT_RENDER_HEIGHT;
video_cfg.window_width = 0; video_cfg.window_width = 0;
video_cfg.window_height = 0; video_cfg.window_height = 0;
video_cfg.flags = 0; video_cfg.max_fps = 0;
video_cfg.flags = SKELE_VIDEO_VSYNC;
if (skele_video_init(video_cfg) != SKELE_INIT_SUCCESS) { if (skele_video_init(video_cfg) != SKELE_INIT_SUCCESS) {
skele_stk_teardown(); skele_stk_teardown();
@@ -64,6 +67,18 @@ int main(int argc, char *argv[])
} }
skele_video_present(); skele_video_present();
if (now - last_print >= 1000000000ULL) {
stk_log(STK_LOG_INFO,
"fps=%.1f frame=%.2fms/%.2fms/%.2fms tps=%.1f tick=%lluns/%lluns/%lluns",
skele_fps, skele_frame_work_time_ms,
skele_frame_render_time_ms, skele_frame_budget_ms,
skele_tps,
(unsigned long long)skele_tick_work_time_ns,
(unsigned long long)skele_tick_total_time_ns,
(unsigned long long)skele_tick_ns);
last_print = now;
}
} }
game_shutdown(); game_shutdown();
+16
View File
@@ -2,6 +2,7 @@
#include <skele/clock.h> #include <skele/clock.h>
#include <skele/skele.h> #include <skele/skele.h>
#include <stk/stk.h> #include <stk/stk.h>
#include <stk/stk_log.h>
static uint8_t running = 1; static uint8_t running = 1;
@@ -11,6 +12,7 @@ int main(int argc, char *argv[])
{ {
uint64_t tick_start; uint64_t tick_start;
uint64_t elapsed; uint64_t elapsed;
uint64_t last_print = 0;
(void)argc; (void)argc;
(void)argv; (void)argv;
@@ -33,13 +35,27 @@ int main(int argc, char *argv[])
while (running) { while (running) {
tick_start = skele_time_ns(); tick_start = skele_time_ns();
skele_clock_tick_begin();
stk_poll(); stk_poll();
game_tick(); game_tick();
skele_clock_tick_end();
if (tick_start - last_print >= 1000000000ULL) {
stk_log(STK_LOG_INFO,
"tps=%.1f tick=%lluns/%lluns/%lluns",
skele_tps,
(unsigned long long)skele_tick_work_time_ns,
(unsigned long long)skele_tick_total_time_ns,
(unsigned long long)skele_tick_ns);
last_print = tick_start;
}
elapsed = skele_time_ns() - tick_start; elapsed = skele_time_ns() - tick_start;
skele_clock_idle_begin();
if (elapsed < skele_tick_ns) if (elapsed < skele_tick_ns)
skele_sleep_ns(skele_tick_ns - elapsed); skele_sleep_ns(skele_tick_ns - elapsed);
skele_clock_idle_end();
} }
game_shutdown(); game_shutdown();