5 Commits
Author SHA1 Message Date
odweta f5be3992a6 add scaffold for 2048 2026-10-05 03:54:40 +02:00
odweta 904b8c3a29 update false claim 2026-10-05 03:47:51 +02:00
odweta 78ae90391a add much-needed optimizations and enhance the snake example game 2026-10-05 03:45:46 +02:00
odweta e72588f792 update README with text 2026-10-05 02:27:32 +02:00
odweta 760d2abc6c add basic text rendering 2026-10-05 02:24:13 +02:00
10 changed files with 543 additions and 61 deletions
+15 -3
View File
@@ -3,19 +3,22 @@ LIBS := `pkg-config --libs sdl3`
INC := `pkg-config --cflags sdl3` -I./include
SRC := src/engine/engine.c \
src/engine/engine_init.c \
src/engine/render.c
src/engine/render.c \
src/engine/text.c
OUT := build/main
SNAKE_SRC := src/examples/snake.c $(SRC)
SNAKE := build/snake
2048_SRC := src/examples/2048.c $(SRC)
2048 := build/2048
build:
$(CC) -o $(OUT) $(SRC) $(INC) $(LIBS)
$(CC) -o $(OUT) -O3 -DNDEBUG src/main.c $(SRC) $(INC) $(LIBS)
run:
$(OUT)
build-snake:
$(CC) -o $(SNAKE) $(SNAKE_SRC) $(INC) $(LIBS)
$(CC) -o $(SNAKE) -O3 -DNDEBUG $(SNAKE_SRC) $(INC) $(LIBS)
run-snake:
$(SNAKE)
@@ -23,6 +26,15 @@ run-snake:
clean-snake:
rm $(SNAKE)
build-2048:
$(CC) -o $(2048) -O3 -DNDEBUG $(2048_SRC) $(INC) $(LIBS)
run-2048:
$(2048)
clean-2048:
rm $(2048)
clean:
rm $(OUT)
+17 -1
View File
@@ -7,7 +7,7 @@
1. Put Pixel - Lets you color one given pixel in the framebuffer.
```c
void put_pixel(unsigned int x, unsigned int y, uint32_t color);
void put_pixel(uint32_t x, uint32_t y, uint32_t color);
```
2. Clear Framebuffer - Lets you fill the entire framebuffer with one color.
@@ -16,6 +16,22 @@ void put_pixel(unsigned int x, unsigned int y, uint32_t color);
void clear_framebuffer(uint32_t color);
```
3. Put String - Lets you display multiline text with a size, foreground and background color on a given (x,y). You can specify the spacing between letters and lines. Supported characters are in `text.c` - feel free to add some if you want to.
```c
void
put_string(
const char *text,
uint32_t x,
uint32_t y,
uint32_t size,
uint32_t spacing,
uint32_t line_spacing,
uint32_t fg_color,
uint32_t bg_color
);
```
#### Usage
The main function of a program that uses this engine should look something like this:
+25 -1
View File
@@ -11,6 +11,8 @@
#define DEFAULT_WINDOW_NAME "Dangerzone"
#define WINDOW_NAME_LENGTH 128
#define TARGET_FRAME 1.0 / 60.0
#define FONT_ROWS 7
#define FONT_COLS 5
typedef struct {
char window_name[WINDOW_NAME_LENGTH];
@@ -39,6 +41,8 @@ typedef struct {
uint32_t framebuffer[WINDOW_WIDTH * WINDOW_HEIGHT];
SDL_Event event;
bool is_running;
bool framebuffer_dirty;
SDL_Rect dirty_rect;
uint16_t width, height;
} game_state_t;
@@ -86,10 +90,30 @@ clear_framebuffer(
uint32_t color
);
void
clear_rect(int x, int y, int w, int h, uint32_t color);
void
mark_dirty_rect(int x, int y, int w, int h);
void
put_char(
char c,
uint32_t x,
uint32_t y,
uint8_t size
uint32_t size,
uint32_t fg_color,
uint32_t bg_color
);
void
put_string(
const char *text,
uint32_t x,
uint32_t y,
uint32_t size,
uint32_t spacing,
uint32_t line_spacing,
uint32_t fg_color,
uint32_t bg_color
);
+135 -18
View File
@@ -3,12 +3,144 @@
engine_internal_state_t *internal_state = NULL;
game_state_t *game_state = NULL;
void
mark_dirty_rect(int x, int y, int w, int h)
{
if (w <= 0 || h <= 0) {
return;
}
// clip rectangle to the framebuffer
if (x < 0) {
w += x;
x = 0;
}
if (y < 0) {
h += y;
y = 0;
}
if (x + w > game_state->width) {
w = game_state->width - x;
}
if (y + h > game_state->height) {
h = game_state->height - y;
}
if (w <= 0 || h <= 0) {
return;
}
SDL_Rect rect = {
.x = x,
.y = y,
.w = w,
.h = h
};
if (!game_state->framebuffer_dirty) {
game_state->dirty_rect = rect;
game_state->framebuffer_dirty = true;
return;
}
int left = game_state->dirty_rect.x < rect.x
? game_state->dirty_rect.x
: rect.x;
int top = game_state->dirty_rect.y < rect.y
? game_state->dirty_rect.y
: rect.y;
int old_right =
game_state->dirty_rect.x + game_state->dirty_rect.w;
int new_right = rect.x + rect.w;
int old_bottom =
game_state->dirty_rect.y + game_state->dirty_rect.h;
int new_bottom = rect.y + rect.h;
int right = old_right > new_right
? old_right
: new_right;
int bottom = old_bottom > new_bottom
? old_bottom
: new_bottom;
game_state->dirty_rect = (SDL_Rect) {
.x = left,
.y = top,
.w = right - left,
.h = bottom - top
};
}
bool
upload_dirty_texture(void)
{
if (!game_state->framebuffer_dirty) {
return true;
}
SDL_Texture *texture =
internal_state->sdl_state->texture;
SDL_Rect rect = game_state->dirty_rect;
void *locked_pixels = NULL;
int locked_pitch = 0;
if (!SDL_LockTexture(
texture,
&rect,
&locked_pixels,
&locked_pitch
)) {
SDL_Log(
"SDL_LockTexture failed: %s",
SDL_GetError()
);
return false;
}
const size_t source_pitch =
(size_t)game_state->width * sizeof(uint32_t);
const size_t row_bytes =
(size_t)rect.w * sizeof(uint32_t);
const uint8_t *source =
(const uint8_t *)game_state->framebuffer +
(size_t)rect.y * source_pitch +
(size_t)rect.x * sizeof(uint32_t);
uint8_t *destination =
(uint8_t *)locked_pixels;
for (int y = 0; y < rect.h; ++y) {
memcpy(
destination + (size_t)y * locked_pitch,
source + (size_t)y * source_pitch,
row_bytes
);
}
SDL_UnlockTexture(texture);
game_state->framebuffer_dirty = false;
return true;
}
void
engine_start() {
// main game loop
while(game_state->is_running) {
uint64_t start = SDL_GetPerformanceCounter();
// poll events
while (SDL_PollEvent(&game_state->event)) {
internal_state->engine_poll_events();
@@ -18,11 +150,7 @@ engine_start() {
internal_state->engine_update_framebuffer();
// update the texture with the modified framebuffer
SDL_UpdateTexture(
internal_state->sdl_state->texture,
NULL, game_state->framebuffer,
internal_state->sdl_state->width * sizeof(uint32_t)
);
upload_dirty_texture();
SDL_RenderClear(
internal_state->sdl_state->renderer
@@ -37,17 +165,6 @@ engine_start() {
SDL_RenderPresent(
internal_state->sdl_state->renderer
);
uint64_t end = SDL_GetPerformanceCounter();
double elapsed =
(double)(end - start) /
(double)SDL_GetPerformanceFrequency();
// cap the framerate at '1.0/TARGET_FRAME' fps
if (elapsed < TARGET_FRAME) {
SDL_Delay((TARGET_FRAME - elapsed) * 1000);
}
}
}
+25 -2
View File
@@ -65,6 +65,11 @@ engine_sdl_init_renderer(
return RCODE_FAILURE;
}
// enable VSync
if (!SDL_SetRenderVSync(state->renderer, 1)) {
eprintf("SDL_SetRenderVSync failed: %s\n", SDL_GetError());
}
return RCODE_SUCCESS;
}
@@ -127,9 +132,16 @@ engine_init(
) {
internal_state = (engine_internal_state_t *)
malloc(sizeof(engine_internal_state_t));
if (internal_state == NULL) {
return RCODE_FAILURE;
}
game_state = (game_state_t *)
malloc(sizeof(game_state_t));
if (game_state == NULL) {
free(internal_state);
return RCODE_FAILURE;
}
internal_state->engine_poll_events
= engine_poll_events;
@@ -138,11 +150,22 @@ engine_init(
= engine_update_framebuffer;
internal_state->sdl_state = engine_sdl_init();
if (internal_state->sdl_state == NULL) {
free(internal_state);
free(game_state);
return RCODE_FAILURE;
}
game_state->is_running = true;
game_state->framebuffer_dirty = true;
game_state->dirty_rect = (SDL_Rect) {
.x = 0,
.y = 0,
.w = game_state->width,
.h = game_state->height
};
game_state->width = internal_state->sdl_state->width;
game_state->height = internal_state->sdl_state->height;
return (game_state == NULL || internal_state == NULL)
? RCODE_FAILURE : RCODE_SUCCESS;
return RCODE_SUCCESS;
}
+34 -6
View File
@@ -1,5 +1,6 @@
#include "engine.h"
#include "error.h"
#include <stdint.h>
// alter a pixel at an imaginary x & y coordinates
void
@@ -19,6 +20,8 @@ put_pixel(
} else {
game_state->framebuffer[game_state->width * y + x]
= color;
mark_dirty_rect(x, y, 1, 1);
}
}
@@ -39,18 +42,43 @@ put_rect(
put_pixel(i, j, color);
}
}
mark_dirty_rect(x, y, w, h);
}
// fill the entire framebuffer with a given color
void
void
clear_framebuffer(
uint32_t color
) {
uint16_t w = game_state->width;
uint16_t h = game_state->height;
for (size_t i = 0;
i < (size_t)game_state->width * game_state->height;
++i) {
game_state->framebuffer[i] = color;
}
for (int i = 0; i < w * h; i++) {
game_state->framebuffer[i] = color;
}
mark_dirty_rect(
0,
0,
game_state->width,
game_state->height
);
}
void
clear_rect(int x, int y, int w, int h, uint32_t color)
{
for (int py = y; py < y + h; ++py) {
for (int px = x; px < x + w; ++px) {
if (px >= 0 &&
px < game_state->width &&
py >= 0 &&
py < game_state->height) {
game_state->framebuffer[
py * game_state->width + px
] = color;
}
}
}
mark_dirty_rect(x, y, w, h);
}
+143
View File
@@ -0,0 +1,143 @@
// rendering characters in the framebuffer
#include "engine.h"
#include <stdint.h>
/*
* each character has 8 rows
* only the lower 4 bits of each byte are used
*/
static const uint8_t font[128][FONT_ROWS] = {
[' '] = { 0, 0, 0, 0, 0, 0, 0 },
['A'] = { 0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11 },
['B'] = { 0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E },
['C'] = { 0x0F, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0F },
['D'] = { 0x1E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1E },
['E'] = { 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F },
['F'] = { 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10 },
['G'] = { 0x0F, 0x10, 0x10, 0x17, 0x11, 0x11, 0x0F },
['H'] = { 0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11 },
['I'] = { 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x1F },
['J'] = { 0x01, 0x01, 0x01, 0x01, 0x11, 0x11, 0x0E },
['K'] = { 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11 },
['L'] = { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F },
['M'] = { 0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11 },
['N'] = { 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11 },
['O'] = { 0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E },
['P'] = { 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10 },
['Q'] = { 0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D },
['R'] = { 0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11 },
['S'] = { 0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E },
['T'] = { 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 },
['U'] = { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E },
['V'] = { 0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04 },
['W'] = { 0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0A },
['X'] = { 0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11 },
['Y'] = { 0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04 },
['Z'] = { 0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F },
['0'] = { 0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E },
['1'] = { 0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E },
['2'] = { 0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F },
['3'] = { 0x1E, 0x01, 0x01, 0x0E, 0x01, 0x01, 0x1E },
['4'] = { 0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02 },
['5'] = { 0x1F, 0x10, 0x10, 0x1E, 0x01, 0x01, 0x1E },
['6'] = { 0x0E, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x0E },
['7'] = { 0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08 },
['8'] = { 0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E },
['9'] = { 0x0E, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x0E },
['.'] = { 0, 0, 0, 0, 0, 0x0C, 0x0C },
[','] = { 0, 0, 0, 0, 0, 0x0C, 0x08 },
[':'] = { 0, 0x0C, 0x0C, 0, 0, 0x0C, 0x0C },
[';'] = { 0, 0x0C, 0x0C, 0, 0, 0x0C, 0x08 },
['!'] = { 0x04, 0x04, 0x04, 0x04, 0x04, 0, 0x04 },
['?'] = { 0x0E, 0x11, 0x01, 0x02, 0x04, 0, 0x04 },
['-'] = { 0, 0, 0, 0x1F, 0, 0, 0 },
['_'] = { 0, 0, 0, 0, 0, 0, 0x1F },
['+'] = { 0, 0x04, 0x04, 0x1F, 0x04, 0x04, 0 },
['='] = { 0, 0x1F, 0, 0, 0x1F, 0, 0 },
['/'] = { 0x01, 0x01, 0x02, 0x04, 0x08, 0x10, 0x10 },
['\\'] = { 0x10, 0x10, 0x08, 0x04, 0x02, 0x01, 0x01 },
['('] = { 0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02 },
[')'] = { 0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08 },
['['] = { 0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0E },
[']'] = { 0x0E, 0x02, 0x02, 0x02, 0x02, 0x02, 0x0E },
['<'] = { 0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02 },
['>'] = { 0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08 },
['*'] = { 0, 0x15, 0x0E, 0x1F, 0x0E, 0x15, 0 },
['#'] = { 0x0A, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x0A },
['%'] = { 0x19, 0x19, 0x02, 0x04, 0x08, 0x13, 0x13 },
['@'] = { 0x0E, 0x11, 0x17, 0x15, 0x17, 0x10, 0x0F }
};
/**
* put a char 'c' on 'x' and 'y' in the framebuffer
* with a foreground color of 'color'
*/
void
put_char(
char c,
uint32_t x,
uint32_t y,
uint32_t size,
uint32_t fg_color,
uint32_t bg_color
) {
// convert lowercase letters to uppercase
if (c >= 'a' && c <= 'z') {
c -= 'a' - 'A';
}
// avoid indexing outside font[128]
if ((unsigned char)c >= 128) {
c = '?';
}
for (uint32_t row = 0; row < FONT_ROWS; row++) {
uint8_t bits = font[(unsigned char)c][row];
for (uint32_t col = 0; col < FONT_COLS; col++) {
uint32_t color =
bits & (1u << (FONT_COLS - 1 - col))
? fg_color : bg_color;
put_rect(
x + col * size,
y + row * size,
size,
size,
color
);
}
}
mark_dirty_rect(x, y, size, size);
}
void
put_string(
const char *text,
uint32_t x,
uint32_t y,
uint32_t size,
uint32_t spacing,
uint32_t line_spacing,
uint32_t fg_color,
uint32_t bg_color
) {
uint32_t start_x = x;
while (*text) {
if (*text == '\n') {
x = start_x;
y += (FONT_ROWS + line_spacing) * size;
} else {
put_char(*text, x, y, size, fg_color, bg_color);
x += (FONT_COLS + spacing) * size;
}
text++;
}
}
+51
View File
@@ -0,0 +1,51 @@
#include "engine.h"
// polling events every frame
void
client_poll_event()
{
SDL_Event ev = game_state->event;
if (ev.type == SDL_EVENT_QUIT) {
game_state->is_running = false;
}
if (ev.type == SDL_EVENT_KEY_DOWN) {
switch (ev.key.key) {
case SDLK_Q:
game_state->is_running = false;
break;
default:
break;
}
}
}
// performed every frame
void
client_update_framebuffer()
{
// single green demo pixel
game_state->framebuffer[0] = 0x00FF00;
put_rect(0, 0, 1100, 200, 0xFFFF00);
put_string("Hello, traveller. You have come\nto the right place!",
20, 20, 2, 2, 3, 0x000000, 0xFFFF00
);
}
int
main()
{
// initialize the engine
if (engine_init(
client_poll_event,
client_update_framebuffer
)) {
engine_start();
engine_stop();
return 0;
} else {
return 1;
}
}
+91 -24
View File
@@ -11,7 +11,7 @@ typedef struct {
int32_t y;
} v2;
#define SNAKE_MAX_LENGTH 64
#define SNAKE_MAX_LENGTH 10
typedef struct {
// position of the head of the snake in the framebuffer
@@ -32,6 +32,9 @@ typedef struct {
} snake_state_t;
snake_state_t snake;
bool put_string_game_over;
bool put_string_win;
bool redraw = false;
bool
is_opposite_direction(v2 a, v2 b)
@@ -88,25 +91,6 @@ client_poll_event()
}
}
/**
* draws a rectangle with its left-top corner at (x,y) with
* a width of 'w' and height of 'h' filled with color 'color'
*/
void
put_rect(
unsigned int x,
unsigned int y,
unsigned int w,
unsigned int h,
uint32_t color
) {
for (int i = x; i < x + w; i++) {
for (int j = y; j < y + h; j++) {
put_pixel(i, j, color);
}
}
}
// a wrapper function for put_rect using a v2 struct
void
put_rect_v2(
@@ -175,11 +159,17 @@ calculate_snake_death_collisions()
{
for (int i = 1; i < snake.length; i++) {
if (v2_eq(snake.body[0], snake.body[i])) {
game_state->is_running = false;
put_string_game_over = true;
}
}
}
void
evaluate_win()
{
put_string_win = (snake.length >= SNAKE_MAX_LENGTH);
}
/**
* performed every frame
* runs on a separate worker thread - you can take advantage
@@ -190,7 +180,7 @@ client_update_framebuffer()
{
uint64_t now = SDL_GetTicks();
if (now >= snake.next_move_time) {
if (now >= snake.next_move_time && !put_string_game_over) {
// ------- business logic --------
@@ -202,7 +192,62 @@ client_update_framebuffer()
calculate_snake_death_collisions();
evaluate_win();
v2 old_head = snake.body[0];
v2 old_tail = snake.body[snake.length - 1];
advance_snake_pos();
v2 new_head = snake.body[0];
clear_rect(
old_tail.x,
old_tail.y,
snake.size,
snake.size,
0x000000
);
if (snake.length > 1) {
put_rect(
old_head.x,
old_head.y,
snake.size,
snake.size,
0x00AA00
);
}
put_rect(
new_head.x,
new_head.y,
snake.size,
snake.size,
0x00FF00
);
mark_dirty_rect(
old_tail.x,
old_tail.y,
snake.size,
snake.size
);
mark_dirty_rect(
old_head.x,
old_head.y,
snake.size,
snake.size
);
mark_dirty_rect(
new_head.x,
new_head.y,
snake.size,
snake.size
);
// -------------------------------
@@ -212,8 +257,11 @@ client_update_framebuffer()
snake.next_move_time = now + snake.move_interval;
}
clear_framebuffer(0x000000);
redraw = true;
game_state->framebuffer_dirty = true;
}
if (redraw) {
// draw the apple
if (snake.is_apple_alive) {
put_rect_v2(
@@ -229,6 +277,7 @@ client_update_framebuffer()
snake.body[0].x, snake.body[0].y,
snake.size, snake.size, 0x00FF00
);
// draw the tail
for (int i = 1; i < snake.length; i++) {
put_rect(
@@ -236,6 +285,21 @@ client_update_framebuffer()
snake.size, snake.size, 0x00AA00
);
}
// draw the splash texts
if (put_string_game_over) {
put_string("GAME OVER!\n<q> to quit",
0, 0, 5, 2, 3, 0xFF0000, 0x000000
);
}
if (put_string_win) {
put_string("YOU WIN!\n<q> to quit",
0, 100, 5, 2, 3, 0x00FF00, 0x000000
);
}
redraw = false;
}
}
@@ -249,11 +313,14 @@ snake_init()
snake.dir = (v2){1, 0};
snake.length = 1;
snake.size = 20;
snake.move_interval = 300;
snake.move_interval = 50;
snake.next_move_time = snake.move_interval;
snake.is_apple_alive = false;
snake.pending_dir = snake.dir;
snake.direction_pending = false;
put_string_game_over = false;
put_string_win = false;
}
int
+7 -6
View File
@@ -18,19 +18,20 @@ client_poll_event()
default:
break;
}
}
}
}
/**
* performed every frame
* runs on a separate worker thread - you can take advantage
* of sleep or usleep from <unistd.h> to create delays
*/
// performed every frame
void
client_update_framebuffer()
{
// single green demo pixel
game_state->framebuffer[0] = 0x00FF00;
put_rect(0, 0, 1100, 200, 0xFFFF00);
put_string("Hello, traveller. You have come\nto the right place!",
20, 20, 2, 2, 3, 0x000000, 0xFFFF00
);
}
int