From 78ae90391adf2243d19568fd28b1f35fee2ba2ff Mon Sep 17 00:00:00 2001 From: odweta Date: Mon, 5 Oct 2026 03:45:46 +0200 Subject: [PATCH] add much-needed optimizations and enhance the snake example game --- Makefile | 2 +- include/engine.h | 8 ++ src/engine/engine.c | 153 ++++++++++++++++++++++++++++++++++----- src/engine/engine_init.c | 27 ++++++- src/engine/render.c | 39 ++++++++-- src/engine/text.c | 2 + src/examples/snake.c | 96 ++++++++++++++++++++++-- 7 files changed, 295 insertions(+), 32 deletions(-) diff --git a/Makefile b/Makefile index cd627a9..9b6e44d 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ run: $(OUT) build-snake: - $(CC) -o $(SNAKE) $(SNAKE_SRC) $(INC) $(LIBS) + $(CC) -o $(SNAKE) -O3 -DNDEBUG $(SNAKE_SRC) $(INC) $(LIBS) run-snake: $(SNAKE) diff --git a/include/engine.h b/include/engine.h index ff323cc..964178e 100644 --- a/include/engine.h +++ b/include/engine.h @@ -41,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; @@ -88,6 +90,12 @@ 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, diff --git a/src/engine/engine.c b/src/engine/engine.c index 7e12fc0..c850365 100644 --- a/src/engine/engine.c +++ b/src/engine/engine.c @@ -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); - } } } diff --git a/src/engine/engine_init.c b/src/engine/engine_init.c index 28e9444..556bc64 100644 --- a/src/engine/engine_init.c +++ b/src/engine/engine_init.c @@ -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; } \ No newline at end of file diff --git a/src/engine/render.c b/src/engine/render.c index 4711e49..610b399 100644 --- a/src/engine/render.c +++ b/src/engine/render.c @@ -20,6 +20,8 @@ put_pixel( } else { game_state->framebuffer[game_state->width * y + x] = color; + + mark_dirty_rect(x, y, 1, 1); } } @@ -40,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); +} diff --git a/src/engine/text.c b/src/engine/text.c index b17df86..bb58e4b 100644 --- a/src/engine/text.c +++ b/src/engine/text.c @@ -112,6 +112,8 @@ put_char( ); } } + + mark_dirty_rect(x, y, size, size); } void diff --git a/src/examples/snake.c b/src/examples/snake.c index 3b88146..1bef957 100644 --- a/src/examples/snake.c +++ b/src/examples/snake.c @@ -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) @@ -156,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 @@ -171,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 -------- @@ -183,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 + ); + // ------------------------------- @@ -193,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( @@ -210,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( @@ -217,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 to quit", + 0, 0, 5, 2, 3, 0xFF0000, 0x000000 + ); + } + + if (put_string_win) { + put_string("YOU WIN!\n to quit", + 0, 100, 5, 2, 3, 0x00FF00, 0x000000 + ); + } + + redraw = false; } } @@ -230,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