Compare commits

..
7 Commits
Author SHA1 Message Date
anth64 55fc4958f7 feat: wire fps diagnostics into video present/init
Also fixes frame_budget_ns being left unset when vsync paces
presentation without an explicit sleep (present_fn == present), and
exposes it as skele_frame_budget_ms for reporting alongside the
sleep-based capped path.
2026-08-23 17:44:04 +02:00
anth64 db05df4227 feat: add tick processing/idle time diagnostics
Tracks actual tick work duration and idle/sleep time separately from
tick rate, via zero-arg begin/end pairs (skele_clock_tick_begin/end,
skele_clock_idle_begin/end) rather than passed timestamps, since the
prior interval-between-calls approach just measured the fixed tick
period, not real processing cost.
2026-08-23 17:43:57 +02:00
anth64 5ad0ddfbc0 feat: add fps module for rolling frame/fps diagnostics
Real-time 1-second rolling window, independent of tick rate or
display refresh.
2026-08-23 17:43:51 +02:00
anth64 55055a042d feat: add vsync-priority frame pacing to video backends
Present now dispatches through a function pointer chosen at init and
re-chosen by two new runtime setters: real vsync if requested and the
driver grants it, else a manual sleep-based cap (auto-detects display
refresh rate, falls back to 60fps if that can't be queried), else
fully uncapped. skele_video_using_vsync() reports what was actually
granted; skele_video_set_vsync()/skele_video_set_max_fps() let a game
change either live without tearing down the renderer/GL context.
2026-08-23 17:06:33 +02:00
anth64 ddaeafc14f docs: write README 2026-08-19 22:05:32 +02:00
anth64 c4bed2f596 refactor(video/sdl): dedupe scale computation and use calloc for framebuffer 2026-08-19 21:53:18 +02:00
anth64 4e7049f49a refactor(input/sdl): store key/pad/mouse button state as bitmasks 2026-08-19 21:49:59 +02:00
13 changed files with 536 additions and 48 deletions
+198 -1
View File
@@ -1,4 +1,201 @@
# skele
Generic game framework written in C99.
[![License: MPL 2.0](https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0)
`skele` is a lightweight, generic game framework written in C99. It provides a **platform-agnostic core** (clock, video, input) split cleanly from OS/backend-specific implementations, so games and engines built on it stay portable without fighting the framework's opinions.
It pairs naturally with [`stk`](https://git.forlornoutpost.ca/anth64/stk) for hot-reloadable game modules, but has no hard dependency on it beyond the optional `skele_stk_setup`/`skele_stk_teardown` glue.
---
## Key Features
- **Platform-agnostic core** (`include/`) with OS/backend-specific implementations swapped in at build time (`src/platform/`)
- **Two video backends** behind one API: software/SDL3 blit (`libskele`) and OpenGL (`libglskele`)
- **Headless server builds**, linking only the engine core, no client/video/input code pulled in
- **Fixed-timestep clock** with monotonic time and signal-aware sleep (POSIX and Win32)
- **SDL3-backed input**: keyboard, gamepad (up to 4 pads), mouse, with held/pressed-this-frame state and analog axes
- **256-color palette + software blit path** for indexed-color renderers (Doom-era style pipelines)
- **Optional `stk` hot-reload integration** via `skele_stk_setup`/`skele_stk_teardown`
---
## Quick Start
### Building
```bash
# Unix (Linux/BSD/macOS)
./build.sh debug release
# Windows
build.bat debug release
```
Builds three static libraries into `bin/{debug,release}`:
- `libskele.a` - software/SDL3 client backend
- `libglskele.a` - OpenGL client backend
- `libskeleserver.a` - headless engine core only
Raspberry Pi 5 is auto-detected and builds against GL 3.1 instead of the default 3.3.
### Installation
#### Unix (Linux/BSD/macOS)
```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
```
#### Windows
```
build.bat release
```
* Once finished building, copy the headers from `include/` to `your_project/include/skele/` and copy the built `.lib`/`.dll` files to your project's `lib` directory.
---
## Usage
### Basic Example (software client)
```c
#include "client/blit.h"
#include "client/input.h"
#include "client/video.h"
#include "clock.h"
#include "skele.h"
#include <stdlib.h>
#include <stk/stk.h>
static uint8_t running = 1;
static void on_signal(void) { running = 0; }
int main(void)
{
skele_video_config_t video_cfg;
uint64_t last, now, elapsed, accum = 0;
uint8_t *pixels;
skele_clock_init(on_signal);
if (skele_stk_setup() != SKELE_INIT_SUCCESS)
return 1;
if (skele_init() != SKELE_INIT_SUCCESS) {
skele_stk_teardown();
return 1;
}
video_cfg.render_width = SKELE_DEFAULT_RENDER_WIDTH;
video_cfg.render_height = SKELE_DEFAULT_RENDER_HEIGHT;
video_cfg.window_width = 0;
video_cfg.window_height = 0;
video_cfg.flags = 0;
if (skele_video_init(video_cfg) != SKELE_INIT_SUCCESS) {
skele_stk_teardown();
skele_shutdown();
return 1;
}
pixels = calloc(video_cfg.render_width * video_cfg.render_height,
sizeof(uint8_t));
last = skele_time_ns();
while (running) {
stk_poll();
if (!skele_input_poll())
break;
now = skele_time_ns();
elapsed = now - last;
accum += elapsed;
last = now;
while (accum >= skele_tick_ns) {
skele_tick();
accum -= skele_tick_ns;
}
skele_video_blit(pixels);
skele_video_present();
}
free(pixels);
skele_video_shutdown();
skele_stk_teardown();
skele_shutdown();
return 0;
}
```
See `example/client`, `example/gl_client`, and `example/server` for the full reference loops, including a fixed-timestep accumulator suitable for interpolated rendering.
### Headless / Server Builds
Link `libskeleserver.a` instead of `libskele.a`/`libglskele.a` to get the engine core (clock, tick, `stk` glue) without pulling in any video/input/SDL3 dependency:
```bash
cc -o my_server main.c -lskeleserver -lstk
```
### API Reference
#### Core (`skele.h`)
- `uint8_t skele_init(void)` / `void skele_shutdown(void)` - engine lifecycle
- `void skele_tick(void)` - advance one simulation tick
- `void skele_set_tick_rate(uint8_t rate)` - configure ticks/sec (default `SKELE_DEFAULT_TICK_RATE`)
- `uint64_t skele_tick_ns` - nanoseconds per tick, for accumulator loops
- `uint8_t skele_stk_setup(void)` / `void skele_stk_teardown(void)` - wire up `stk` hot-reload
#### Clock (`clock.h`)
- `void skele_clock_init(void (*on_signal)(void))` - monotonic clock + signal handling
- `uint64_t skele_time_ns(void)` - current monotonic time
- `void skele_sleep_ns(uint64_t ns)` - sleep for a duration
#### Video (`client/video.h`)
- `uint8_t skele_video_init(skele_video_config_t cfg)` / `void skele_video_shutdown(void)`
- `void skele_video_present(void)` - present the current frame
- `void skele_video_set_title(const char *title)`
- `void skele_video_toggle_fullscreen(void)` / `skele_video_set_fullscreen_kind(...)`
- `void skele_video_cycle_scale(void)` - cycle integer window scale
- `void skele_video_set_mouse_grab(uint8_t grab)`
#### Blit / Palette (`client/blit.h`, `client/palette.h`)
- `void skele_video_blit(uint8_t *pixels)` - blit an indexed-color framebuffer through the active palette
- `void skele_palette_set(skele_palette_t pal)` / `skele_palette_set_index(uint8_t index, uint32_t color)`
#### Input (`client/input.h`)
- `uint8_t skele_input_poll(void)` - pump platform events, returns `0` on quit
- `uint8_t skele_key_down(skele_key_t key)` / `skele_key_held(skele_key_t key)` - pressed-this-frame vs. currently-held
- `uint8_t skele_pad_connected(uint8_t pad)`, `skele_pad_button_down/held(...)`, `float skele_pad_axis(...)` - up to `SKELE_MAX_PADS` gamepads
- `void skele_mouse_delta(int32_t *dx, int32_t *dy)`, `skele_mouse_button_down/held(...)`
---
## Project Status
**Current Version:** 0.0.0
Early/active development. Platform backends (clock, video, input) are functional and exercised by real client applications, but the engine core (`skele_init`/`skele_tick`) is still minimal. API is not yet stable; expect breaking changes until the core lifecycle is fleshed out.
No test suite yet.
---
## License
Mozilla Public License 2.0 (MPL-2.0)
---
## Contributing
Contributions welcome! Please ensure code follows C99 standard and works across all supported platforms.
+1 -1
View File
@@ -3,7 +3,7 @@ INPUT_SRC = src/platform/client/input/sdl.c
.include "config.mk"
ENGINE_SRCS = src/skele.c ${CLOCK_SRC}
ENGINE_SRCS = src/skele.c src/fps.c ${CLOCK_SRC}
SOFT_PLAT_SRCS = src/platform/client/video/sdl.c ${INPUT_SRC}
GL_PLAT_SRCS = src/platform/client/video/gl.c ${INPUT_SRC}
SOFT_CLIENT_SRCS = example/client/main.c
+2 -1
View File
@@ -30,7 +30,8 @@ int main(int argc, char *argv[])
video_cfg.render_height = SKELE_DEFAULT_RENDER_HEIGHT;
video_cfg.window_width = 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) {
skele_stk_teardown();
+2 -1
View File
@@ -29,7 +29,8 @@ int main(int argc, char *argv[])
video_cfg.render_height = SKELE_DEFAULT_RENDER_HEIGHT;
video_cfg.window_width = 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) {
skele_stk_teardown();
+1 -1
View File
@@ -8,7 +8,7 @@ endif
include config.mk
ENGINE_SRCS := src/skele.c $(CLOCK_SRC)
ENGINE_SRCS := src/skele.c src/fps.c $(CLOCK_SRC)
SOFT_PLAT_SRCS := src/platform/client/video/sdl.c $(INPUT_SRC)
GL_PLAT_SRCS := src/platform/client/video/gl.c $(INPUT_SRC)
SOFT_CLIENT_SRCS := example/client/main.c
+8
View File
@@ -12,10 +12,14 @@ extern "C" {
#define SKELE_VIDEO_RESIZABLE 0x04
#define SKELE_VIDEO_HIGHDPI 0x08
#define SKELE_VIDEO_FULLSCREEN_EXCLUSIVE 0x10
#define SKELE_VIDEO_VSYNC 0x20
#define SKELE_DEFAULT_RENDER_WIDTH 320
#define SKELE_DEFAULT_RENDER_HEIGHT 200
#define SKELE_VIDEO_FPS_UNCAPPED UINT16_MAX
#define SKELE_VIDEO_FPS_FALLBACK 60
typedef enum {
SKELE_FULLSCREEN_BORDERLESS,
SKELE_FULLSCREEN_EXCLUSIVE
@@ -26,12 +30,16 @@ typedef struct {
uint16_t render_height;
uint16_t window_width;
uint16_t window_height;
uint16_t max_fps;
uint8_t flags;
} skele_video_config_t;
uint8_t skele_video_init(skele_video_config_t cfg);
void skele_video_shutdown(void);
void skele_video_present(void);
uint8_t skele_video_using_vsync(void);
void skele_video_set_vsync(uint8_t enabled);
void skele_video_set_max_fps(uint16_t max_fps);
void skele_video_set_title(const char *title);
void skele_video_toggle_fullscreen(void);
void skele_video_set_fullscreen_kind(skele_fullscreen_kind_t kind);
+8
View File
@@ -7,9 +7,17 @@
extern "C" {
#endif
extern float skele_tps;
extern uint64_t skele_tick_time_ns;
extern uint64_t skele_tick_idle_ns;
void skele_clock_init(void (*on_signal)(void));
uint64_t skele_time_ns(void);
void skele_sleep_ns(uint64_t ns);
void skele_clock_tick_begin(void);
void skele_clock_tick_end(void);
void skele_clock_idle_begin(void);
void skele_clock_idle_end(void);
#ifdef __cplusplus
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef SKELE_FPS_H
#define SKELE_FPS_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
extern float skele_fps;
extern float skele_frame_time_ms;
extern float skele_frame_budget_ms;
void skele_fps_init(void);
void skele_fps_frame_mark(void);
#ifdef __cplusplus
}
#endif
#endif /* SKELE_FPS_H */
+41
View File
@@ -0,0 +1,41 @@
#include "fps.h"
#include "clock.h"
float skele_fps = 0.0f;
float skele_frame_time_ms = 0.0f;
float skele_frame_budget_ms = 0.0f;
static uint64_t win_start = 0;
static uint64_t last_frame_ns = 0;
static uint64_t frame_ns_accum = 0;
static uint32_t frame_count = 0;
void skele_fps_init(void)
{
win_start = skele_time_ns();
last_frame_ns = win_start;
frame_ns_accum = 0;
frame_count = 0;
skele_fps = 0.0f;
skele_frame_time_ms = 0.0f;
}
void skele_fps_frame_mark(void)
{
uint64_t now = skele_time_ns();
frame_ns_accum += now - last_frame_ns;
last_frame_ns = now;
frame_count++;
if (now - win_start >= 1000000000ULL) {
double elapsed_s = (double)(now - win_start) / 1e9;
skele_fps = (float)(frame_count / elapsed_s);
skele_frame_time_ms =
(float)((frame_ns_accum / (double)frame_count) / 1e6);
win_start = now;
frame_ns_accum = 0;
frame_count = 0;
}
}
+29 -27
View File
@@ -3,16 +3,16 @@
#include <SDL3/SDL.h>
#include <string.h>
static uint8_t key_held[SKELE_KEY_COUNT];
static uint8_t key_down[SKELE_KEY_COUNT];
static uint64_t key_held = 0;
static uint64_t key_down = 0;
static SDL_Gamepad *pads[SKELE_MAX_PADS];
static uint8_t pad_btn_held[SKELE_MAX_PADS][SKELE_PAD_BUTTON_COUNT];
static uint8_t pad_btn_down[SKELE_MAX_PADS][SKELE_PAD_BUTTON_COUNT];
static uint16_t pad_btn_held[SKELE_MAX_PADS];
static uint16_t pad_btn_down[SKELE_MAX_PADS];
static float pad_axes[SKELE_MAX_PADS][SKELE_PAD_AXIS_COUNT];
static uint8_t mouse_btn_held[SKELE_MOUSE_BUTTON_COUNT];
static uint8_t mouse_btn_down[SKELE_MOUSE_BUTTON_COUNT];
static uint8_t mouse_btn_held = 0;
static uint8_t mouse_btn_down = 0;
static int32_t mouse_dx = 0;
static int32_t mouse_dy = 0;
@@ -249,9 +249,9 @@ uint8_t skele_input_poll(void)
skele_pad_button_t btn;
skele_mouse_button_t mbtn;
memset(key_down, 0, sizeof(key_down));
key_down = 0;
memset(pad_btn_down, 0, sizeof(pad_btn_down));
memset(mouse_btn_down, 0, sizeof(mouse_btn_down));
mouse_btn_down = 0;
mouse_dx = 0;
mouse_dy = 0;
@@ -268,15 +268,15 @@ uint8_t skele_input_poll(void)
case SDL_EVENT_MOUSE_BUTTON_DOWN:
mbtn = sdl_btn_to_mouse(e.button.button);
if (mbtn != SKELE_MOUSE_BUTTON_COUNT) {
mouse_btn_down[mbtn] = 1;
mouse_btn_held[mbtn] = 1;
mouse_btn_down |= (uint8_t)(1u << mbtn);
mouse_btn_held |= (uint8_t)(1u << mbtn);
}
break;
case SDL_EVENT_MOUSE_BUTTON_UP:
mbtn = sdl_btn_to_mouse(e.button.button);
if (mbtn != SKELE_MOUSE_BUTTON_COUNT)
mouse_btn_held[mbtn] = 0;
mouse_btn_held &= (uint8_t)~(1u << mbtn);
break;
case SDL_EVENT_KEY_DOWN:
@@ -292,8 +292,8 @@ uint8_t skele_input_poll(void)
default:
key = scancode_to_key(e.key.scancode);
if (key != SKELE_KEY_UNKNOWN) {
key_down[key] = 1;
key_held[key] = 1;
key_down |= (uint64_t)1 << key;
key_held |= (uint64_t)1 << key;
}
break;
}
@@ -302,7 +302,7 @@ uint8_t skele_input_poll(void)
case SDL_EVENT_KEY_UP:
key = scancode_to_key(e.key.scancode);
if (key != SKELE_KEY_UNKNOWN)
key_held[key] = 0;
key_held &= ~((uint64_t)1 << key);
break;
case SDL_EVENT_GAMEPAD_ADDED:
@@ -310,8 +310,7 @@ uint8_t skele_input_poll(void)
if (slot >= 0) {
pads[slot] = SDL_OpenGamepad(e.gdevice.which);
if (pads[slot])
memset(pad_btn_held[slot], 0,
sizeof(pad_btn_held[slot]));
pad_btn_held[slot] = 0;
}
break;
@@ -320,8 +319,7 @@ uint8_t skele_input_poll(void)
if (slot >= 0) {
SDL_CloseGamepad(pads[slot]);
pads[slot] = NULL;
memset(pad_btn_held[slot], 0,
sizeof(pad_btn_held[slot]));
pad_btn_held[slot] = 0;
memset(pad_axes[slot], 0,
sizeof(pad_axes[slot]));
}
@@ -332,8 +330,8 @@ uint8_t skele_input_poll(void)
btn =
sdl_btn_to_pad((SDL_GamepadButton)e.gbutton.button);
if (slot >= 0 && btn != SKELE_PAD_BUTTON_COUNT) {
pad_btn_down[slot][btn] = 1;
pad_btn_held[slot][btn] = 1;
pad_btn_down[slot] |= (uint16_t)(1u << btn);
pad_btn_held[slot] |= (uint16_t)(1u << btn);
}
break;
@@ -342,7 +340,7 @@ uint8_t skele_input_poll(void)
btn =
sdl_btn_to_pad((SDL_GamepadButton)e.gbutton.button);
if (slot >= 0 && btn != SKELE_PAD_BUTTON_COUNT)
pad_btn_held[slot][btn] = 0;
pad_btn_held[slot] &= (uint16_t)~(1u << btn);
break;
default:
@@ -359,12 +357,12 @@ uint8_t skele_input_poll(void)
uint8_t skele_key_down(skele_key_t key)
{
return key < SKELE_KEY_COUNT ? key_down[key] : 0;
return key < SKELE_KEY_COUNT ? (uint8_t)((key_down >> key) & 1) : 0;
}
uint8_t skele_key_held(skele_key_t key)
{
return key < SKELE_KEY_COUNT ? key_held[key] : 0;
return key < SKELE_KEY_COUNT ? (uint8_t)((key_held >> key) & 1) : 0;
}
uint8_t skele_pad_connected(uint8_t pad)
@@ -375,14 +373,14 @@ uint8_t skele_pad_connected(uint8_t pad)
uint8_t skele_pad_button_down(uint8_t pad, skele_pad_button_t btn)
{
return (pad < SKELE_MAX_PADS && btn < SKELE_PAD_BUTTON_COUNT)
? pad_btn_down[pad][btn]
? (uint8_t)((pad_btn_down[pad] >> btn) & 1)
: 0;
}
uint8_t skele_pad_button_held(uint8_t pad, skele_pad_button_t btn)
{
return (pad < SKELE_MAX_PADS && btn < SKELE_PAD_BUTTON_COUNT)
? pad_btn_held[pad][btn]
? (uint8_t)((pad_btn_held[pad] >> btn) & 1)
: 0;
}
@@ -403,10 +401,14 @@ void skele_mouse_delta(int32_t *dx, int32_t *dy)
uint8_t skele_mouse_button_down(skele_mouse_button_t btn)
{
return btn < SKELE_MOUSE_BUTTON_COUNT ? mouse_btn_down[btn] : 0;
return btn < SKELE_MOUSE_BUTTON_COUNT
? (uint8_t)((mouse_btn_down >> btn) & 1)
: 0;
}
uint8_t skele_mouse_button_held(skele_mouse_button_t btn)
{
return btn < SKELE_MOUSE_BUTTON_COUNT ? mouse_btn_held[btn] : 0;
return btn < SKELE_MOUSE_BUTTON_COUNT
? (uint8_t)((mouse_btn_held >> btn) & 1)
: 0;
}
+82 -2
View File
@@ -1,4 +1,6 @@
#include "client/video.h"
#include "clock.h"
#include "fps.h"
#include "skele.h"
#include <SDL3/SDL.h>
#include <stk/stk_log.h>
@@ -12,6 +14,63 @@ static uint8_t is_fullscreen = 0;
static skele_fullscreen_kind_t last_fullscreen_kind =
SKELE_FULLSCREEN_EXCLUSIVE;
static void (*present_fn)(void);
static uint64_t frame_budget_ns = 0;
static uint64_t last_present_ns = 0;
static uint8_t vsync_active = 0;
static uint8_t want_vsync = 0;
static uint16_t configured_max_fps = 0;
static void present(void) { SDL_GL_SwapWindow(window); }
static void present_capped(void)
{
uint64_t now, elapsed;
SDL_GL_SwapWindow(window);
now = skele_time_ns();
elapsed = now - last_present_ns;
if (elapsed < frame_budget_ns)
skele_sleep_ns(frame_budget_ns - elapsed);
last_present_ns = skele_time_ns();
}
static void apply_present_mode(void)
{
const SDL_DisplayMode *refresh_mode;
uint16_t cap;
if (want_vsync && SDL_GL_SetSwapInterval(1))
vsync_active = 1;
else {
SDL_GL_SetSwapInterval(0);
vsync_active = 0;
}
cap = configured_max_fps;
if (cap == SKELE_VIDEO_FPS_UNCAPPED) {
frame_budget_ns = 0;
skele_frame_budget_ms = 0.0f;
present_fn = present;
return;
}
if (!cap) {
refresh_mode = SDL_GetCurrentDisplayMode(
SDL_GetDisplayForWindow(window));
cap = (refresh_mode && refresh_mode->refresh_rate > 0)
? (uint16_t)refresh_mode->refresh_rate
: SKELE_VIDEO_FPS_FALLBACK;
}
frame_budget_ns = 1000000000ULL / cap;
skele_frame_budget_ms = frame_budget_ns / 1e6f;
last_present_ns = skele_time_ns();
present_fn = vsync_active ? present : present_capped;
}
static uint8_t max_scale(uint16_t rw, uint16_t rh)
{
SDL_DisplayID display;
@@ -159,8 +218,12 @@ uint8_t skele_video_init(skele_video_config_t cfg)
return SKELE_INIT_FAILURE;
}
SDL_GL_SetSwapInterval(1);
want_vsync = (cfg.flags & SKELE_VIDEO_VSYNC) ? 1 : 0;
configured_max_fps = cfg.max_fps;
apply_present_mode();
SDL_ShowWindow(window);
skele_fps_init();
return SKELE_INIT_SUCCESS;
}
@@ -179,7 +242,24 @@ void skele_video_shutdown(void)
is_fullscreen = 0;
}
void skele_video_present(void) { SDL_GL_SwapWindow(window); }
void skele_video_present(void)
{
present_fn();
skele_fps_frame_mark();
}
uint8_t skele_video_using_vsync(void) { return vsync_active; }
void skele_video_set_vsync(uint8_t enabled)
{
want_vsync = enabled;
apply_present_mode();
}
void skele_video_set_max_fps(uint16_t max_fps)
{
configured_max_fps = max_fps;
apply_present_mode();
}
void skele_video_cycle_scale(void) { cycle_scale(); }
void skele_video_toggle_fullscreen(void)
+86 -12
View File
@@ -1,6 +1,8 @@
#include "client/blit.h"
#include "client/palette.h"
#include "client/video.h"
#include "clock.h"
#include "fps.h"
#include "skele.h"
#include <SDL3/SDL.h>
#include <stdint.h>
@@ -21,6 +23,63 @@ static uint8_t is_fullscreen = 0;
static skele_fullscreen_kind_t last_fullscreen_kind =
SKELE_FULLSCREEN_EXCLUSIVE;
static void (*present_fn)(void);
static uint64_t frame_budget_ns = 0;
static uint64_t last_present_ns = 0;
static uint8_t vsync_active = 0;
static uint8_t want_vsync = 0;
static uint16_t configured_max_fps = 0;
static void present(void) { SDL_RenderPresent(renderer); }
static void present_capped(void)
{
uint64_t now, elapsed;
SDL_RenderPresent(renderer);
now = skele_time_ns();
elapsed = now - last_present_ns;
if (elapsed < frame_budget_ns)
skele_sleep_ns(frame_budget_ns - elapsed);
last_present_ns = skele_time_ns();
}
static void apply_present_mode(void)
{
const SDL_DisplayMode *refresh_mode;
uint16_t cap;
if (want_vsync && SDL_SetRenderVSync(renderer, 1))
vsync_active = 1;
else {
SDL_SetRenderVSync(renderer, 0);
vsync_active = 0;
}
cap = configured_max_fps;
if (cap == SKELE_VIDEO_FPS_UNCAPPED) {
frame_budget_ns = 0;
skele_frame_budget_ms = 0.0f;
present_fn = present;
return;
}
if (!cap) {
refresh_mode = SDL_GetCurrentDisplayMode(
SDL_GetDisplayForWindow(window));
cap = (refresh_mode && refresh_mode->refresh_rate > 0)
? (uint16_t)refresh_mode->refresh_rate
: SKELE_VIDEO_FPS_FALLBACK;
}
frame_budget_ns = 1000000000ULL / cap;
skele_frame_budget_ms = frame_budget_ns / 1e6f;
last_present_ns = skele_time_ns();
present_fn = vsync_active ? present : present_capped;
}
static uint8_t max_scale(uint16_t rw, uint16_t rh)
{
SDL_DisplayID display;
@@ -82,7 +141,7 @@ uint8_t skele_video_init(skele_video_config_t cfg)
SDL_DisplayID display;
SDL_WindowFlags flags = 0;
const SDL_DisplayMode *mode;
uint16_t window_w, window_h, aw, ah;
uint16_t window_w, window_h;
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD)) {
stk_log(STK_LOG_ERROR, "video: SDL_Init failed: %s",
@@ -125,24 +184,17 @@ uint8_t skele_video_init(skele_video_config_t cfg)
return SKELE_INIT_FAILURE;
}
aw = (uint16_t)((mode->w * 3) / 4);
ah = (uint16_t)((mode->h * 3) / 4);
cur_scale = 1;
while ((vid_w * (cur_scale + 1)) <= aw &&
(vid_h * (cur_scale + 1)) <= ah)
cur_scale++;
cur_scale = max_scale(vid_w, vid_h);
window_w = vid_w * cur_scale;
window_h = vid_h * cur_scale;
}
rgba_buf = malloc(vid_total * sizeof(uint32_t));
rgba_buf = calloc(vid_total, sizeof(uint32_t));
if (!rgba_buf) {
stk_log(STK_LOG_ERROR, "video: out of memory");
SDL_Quit();
return SKELE_INIT_FAILURE;
}
memset(rgba_buf, 0, vid_total * sizeof(uint32_t));
window = SDL_CreateWindow("skele", window_w, window_h,
flags | SDL_WINDOW_HIDDEN);
@@ -188,9 +240,14 @@ uint8_t skele_video_init(skele_video_config_t cfg)
}
SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST);
SDL_SetRenderVSync(renderer, 1);
want_vsync = (cfg.flags & SKELE_VIDEO_VSYNC) ? 1 : 0;
configured_max_fps = cfg.max_fps;
apply_present_mode();
memset(palette, 0, sizeof(palette));
SDL_ShowWindow(window);
skele_fps_init();
return SKELE_INIT_SUCCESS;
}
@@ -230,7 +287,24 @@ void skele_video_blit(uint8_t *pixels)
SDL_RenderTexture(renderer, texture, NULL, NULL);
}
void skele_video_present(void) { SDL_RenderPresent(renderer); }
void skele_video_present(void)
{
present_fn();
skele_fps_frame_mark();
}
uint8_t skele_video_using_vsync(void) { return vsync_active; }
void skele_video_set_vsync(uint8_t enabled)
{
want_vsync = enabled;
apply_present_mode();
}
void skele_video_set_max_fps(uint16_t max_fps)
{
configured_max_fps = max_fps;
apply_present_mode();
}
void skele_video_cycle_scale(void) { cycle_scale(); }
void skele_video_toggle_fullscreen(void)
+57 -2
View File
@@ -1,21 +1,76 @@
#include "skele.h"
#include "clock.h"
#include <stk/stk.h>
#include <stk/stk_log.h>
uint64_t skele_tick_ns = 1000000000ULL / SKELE_DEFAULT_TICK_RATE;
float skele_tps = 0.0f;
uint64_t skele_tick_time_ns = 0;
uint64_t skele_tick_idle_ns = 0;
static uint8_t stk_initialized = 0;
static uint64_t tick_win_start = 0;
static uint64_t tick_ns_accum = 0;
static uint64_t tick_idle_accum = 0;
static uint32_t tick_count = 0;
static uint64_t tick_start_ns = 0;
static uint64_t idle_start_ns = 0;
void skele_set_tick_rate(uint8_t rate)
{
skele_tick_ns = 1000000000ULL / (rate ? rate : SKELE_DEFAULT_TICK_RATE);
}
uint8_t skele_init(void) { return SKELE_INIT_SUCCESS; }
uint8_t skele_init(void)
{
tick_win_start = skele_time_ns();
tick_ns_accum = 0;
tick_idle_accum = 0;
tick_count = 0;
skele_tps = 0.0f;
skele_tick_time_ns = 0;
skele_tick_idle_ns = 0;
return SKELE_INIT_SUCCESS;
}
void skele_shutdown(void) {}
void skele_tick(void) {}
void skele_tick(void)
{
skele_clock_tick_begin();
skele_clock_tick_end();
}
void skele_clock_tick_begin(void) { tick_start_ns = skele_time_ns(); }
void skele_clock_tick_end(void)
{
uint64_t now = skele_time_ns();
tick_ns_accum += now - tick_start_ns;
tick_count++;
if (now - tick_win_start >= 1000000000ULL) {
double elapsed_s = (now - tick_win_start) / 1e9;
skele_tps = tick_count / elapsed_s;
skele_tick_time_ns = tick_ns_accum / tick_count;
skele_tick_idle_ns = tick_idle_accum / tick_count;
tick_win_start = now;
tick_ns_accum = 0;
tick_idle_accum = 0;
tick_count = 0;
}
}
void skele_clock_idle_begin(void) { idle_start_ns = skele_time_ns(); }
void skele_clock_idle_end(void)
{
tick_idle_accum += skele_time_ns() - idle_start_ns;
}
uint8_t skele_stk_setup(void)
{