commit 81dd4840837c968fa43530d4890c5183765692c1
parent 66a5e8ef34618f8b4bf1e30a89947a281cbd0af1
Author: amin <dev@aminmesbah.com>
Date: Fri, 3 Nov 2017 07:43:16 +0000
Add dynamic reloading of game code on linux
Inspired and informed by Casey Muratori's method in the wonderful
Handmade Hero [1]. The the game is compiled into a shared object that is
dynamically loaded by the platform layer.
There is a way to do this with dlls on windows as well.
I had to temporarily remove the `-Wpedantic` compiler flag to stop
getting the "ISO C forbids conversion of object pointer to function
pointer" error. I'm technically relying on undefined behavior, though in
practice it's fine on most modern platforms to cast from a void* to a
function pointer.
[1] https://hero.handmade.network/episode/code/day021
[2] http://nullprogram.com/blog/2014/12/23/
[3] http://chrismdp.com/2015/08/how-to-add-live-code-reload-to-your-game/
FossilOrigin-Name: a602a2c74669b4a0ed8fd76fba6efd0132461668ca90739035db5fd7b84883d6
Diffstat:
4 files changed, 103 insertions(+), 11 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,5 +1,6 @@
CC = gcc
-CFLAGS = -std=c99 -Wall -Wextra -Wpedantic -Wshadow -Wno-unused-parameter
+#TODO: add -Wpedantic back
+CFLAGS = -std=c99 -Wall -Wextra -Wshadow -Wno-unused-parameter
LDFLAGS = $(SDL_LDFLAGS) -lm
SDL_CFLAGS := $(shell sdl2-config --cflags)
@@ -9,32 +10,44 @@ override CFLAGS += $(SDL_CFLAGS)
SRC_FILES = platform_sdl.c game.c entity.c
SRC = $(addprefix src/, $(SRC_FILES))
-EXE = ohsp
+EXE_FILE = ohsp
+
+LIB_FILES = game.c entity.c
+LIB = $(addprefix src/, $(LIB_FILES))
+LIB_NAME = game.so
DBGDIR = debug
-DBGEXE = $(DBGDIR)/$(EXE)
+DBGEXE = $(DBGDIR)/$(EXE_FILE)
+DBGLIB = $(DBGDIR)/$(LIB_NAME)
DBGCFLAGS = -g -Og -Werror
RELDIR = release
-RELEXE = $(RELDIR)/$(EXE)
+RELEXE = $(RELDIR)/$(EXE_FILE)
+RELLIB = $(RELDIR)/$(LIB_NAME)
RELCFLAGS = -O2 -Os
-.PHONY: all clean compile_debug compile_release debug memcheck prep run todo
+.PHONY: all clean compile_debug compile_release debug lib_debug lib_release memcheck prep run todo
all: compile_debug compile_release
clean:
rm -f $(RELDIR)/* $(DBGDIR)/*
-compile_debug: prep
+compile_debug: prep lib_debug
$(CC) $(CFLAGS) $(DBGCFLAGS) $(SRC) -o $(DBGEXE) $(LDFLAGS)
-compile_release: prep
+compile_release: prep lib_release
$(CC) $(CFLAGS) $(RELCFLAGS) $(SRC) -o $(RELEXE) $(LDFLAGS)
debug: compile_debug
gdb $(DBGEXE)
+lib_debug:
+ $(CC) $(CFLAGS) -fpic -shared $(DBGCFLAGS) $(LIB) -o $(DBGLIB) $(LDFLAGS)
+
+lib_release:
+ $(CC) $(CFLAGS) -fpic -shared $(RELCFLAGS) $(LIB) -o $(RELLIB) $(LDFLAGS)
+
memcheck: compile_debug
valgrind --track-origins=yes ./$(DBGEXE)
diff --git a/src/game.h b/src/game.h
@@ -63,6 +63,10 @@ struct OffscreenBuffer
unsigned int pitch;
};
+// TODO: Consider using the #define trick from HMH
+typedef void (game_update_t)(struct GameState*, struct GameControllerInput, int, int);
+typedef void (game_render_t)(struct OffscreenBuffer*, float, struct GameState*);
+
void game_init(struct GameState *game_state, int field_width, int field_height);
void game_update(struct GameState *game_state, struct GameControllerInput game_input, int field_width, int field_height);
void game_render(struct OffscreenBuffer *buffer, float dt, struct GameState *game_state);
diff --git a/src/platform_sdl.c b/src/platform_sdl.c
@@ -1,7 +1,5 @@
#include "platform_sdl.h"
-#include "game.h"
-
#include <stdbool.h>
#include <stdio.h>
#include <time.h>
@@ -15,6 +13,56 @@ extern bool PAUSED;
static struct SDLOffscreenBuffer global_back_buffer;
+void sdl_unload_game_code(struct SDLGameCode *game_code)
+{
+ if (!game_code)
+ {
+ printf("Invalid pointer *game_code\n");
+ return;
+ }
+
+ if (game_code->game_code_library)
+ {
+ SDL_UnloadObject(game_code->game_code_library);
+ game_code->game_code_library = 0;
+ }
+ game_code->is_valid = false;
+ // TODO: see if we need to set these function pointers to stubs (hmh021)
+ game_code->game_update = 0;
+ game_code->game_render = 0;
+}
+
+
+// TODO: Add temp dll in case loading fails
+struct SDLGameCode sdl_load_game_code(char *source_lib_path)
+{
+ struct SDLGameCode game_code;
+ game_code.is_valid = false;
+ game_code.game_code_library = 0;
+ game_code.game_update = 0;
+ game_code.game_render = 0;
+
+ game_code.game_code_library = SDL_LoadObject(source_lib_path);
+ if (game_code.game_code_library)
+ {
+ // NOTE: The C standard (as of C99) distinguishes function pointers
+ // from object pointers (`void *` is an object pointer). Technically it
+ // is undefined behavior to cast between these two pointer types. In
+ // practice, it works on most modern platforms.
+ game_code.game_update = (game_update_t *) SDL_LoadFunction(game_code.game_code_library, "game_update");
+ game_code.game_render = (game_render_t *) SDL_LoadFunction(game_code.game_code_library, "game_render");
+ game_code.is_valid = (game_code.game_update && game_code.game_render);
+ }
+
+ if (!game_code.is_valid)
+ {
+ printf("Game code is not valid: %s\n", SDL_GetError());
+ }
+
+ return game_code;
+}
+
+
struct SDLWindowDimension sdl_get_window_dimension(SDL_Window *window)
{
struct SDLWindowDimension result;
@@ -248,6 +296,10 @@ int main(int argc, char *argv[])
controller_input.right_stick_x = 0;
controller_input.right_stick_y = 0;
+ // TODO: Make this not hard-coded
+ struct SDLGameCode game_code = sdl_load_game_code("./release/game.so");
+ int counter = 0;
+
while (running)
{
uint64_t current_ms = (SDL_GetPerformanceCounter() * SECOND) / SDL_GetPerformanceFrequency();
@@ -256,6 +308,17 @@ int main(int argc, char *argv[])
lag += elapsed_ms;
//printf("%" PRIu64 ", %" PRIu64 ", %f\n", elapsed_ms, lag, MS_PER_UPDATE);
+ if (counter < 500)
+ {
+ ++counter;
+ }
+ else
+ {
+ sdl_unload_game_code(&game_code);
+ game_code = sdl_load_game_code("./release/game.so");
+ counter = 0;
+ }
+
SDL_Event event;
while (SDL_PollEvent(&event))
{
@@ -281,14 +344,14 @@ int main(int argc, char *argv[])
{
while (lag >= MS_PER_UPDATE)
{
- game_update(&game_state, controller_input, buffer.width, buffer.height);
+ game_code.game_update(&game_state, controller_input, buffer.width, buffer.height);
//printf("\t%" PRIu64 ", %f\n", lag, MS_PER_UPDATE);
lag -= MS_PER_UPDATE;
}
}
clear_screen(&global_back_buffer, COLOR_BACKGROUND);
- game_render(&buffer, lag/SECOND, &game_state);
+ game_code.game_render(&buffer, lag/SECOND, &game_state);
sdl_update_window(renderer, &global_back_buffer);
if (elapsed_ms <= MS_PER_FRAME)
{
diff --git a/src/platform_sdl.h b/src/platform_sdl.h
@@ -1,6 +1,9 @@
#ifndef PLATFORM_SDL_H
#include "SDL.h"
+#include "game.h"
+
+#include <stdbool.h>
#define MAX_CONTROLLERS 4
#define DEADZONE_THRESHOLD 9000
@@ -21,5 +24,14 @@ struct SDLWindowDimension
int height;
};
+struct SDLGameCode
+{
+ bool is_valid;
+ void *game_code_library;
+ time_t last_write_time;
+ game_update_t *game_update;
+ game_render_t *game_render;
+};
+
#define PLATFORM_SDL_H
#endif