commit be373a7d2000d67d16196cd02b27e577828c334a
parent 39b705d57e67d22b6cb1c30e777803f0020b2e55
Author: amin <dev@aminmesbah.com>
Date: Fri, 1 Jun 2018 05:12:47 +0000
Hotload game code
FossilOrigin-Name: d41312085f5ec71e9749997d5c485106ae9060750798a1f49d118ec241c57eff
Diffstat:
4 files changed, 167 insertions(+), 22 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,45 +1,57 @@
CC = clang
CFLAGS = -std=c99 -Ilib -Wall -Wextra -Wshadow -Wswitch-enum -Wno-unused-parameter -Wno-missing-braces
-LDFLAGS = -ldl -lglfw -lGL -lGLEW -lGLU -lm -lpthread -lX11 -lXi -lXrandr
+LDFLAGS = -ldl -lglfw -lGL -lGLEW -lm
SRC_FILES = game.c glmth.c platform_linux.c shader.c
SRC = $(addprefix src/, $(SRC_FILES))
EXE_FILE = a_game
+LIB_FILES = game.c glmth.c shader.c
+LIB = $(addprefix src/, $(LIB_FILES))
+LIB_NAME = game.so
+
DBGDIR = build/debug
DBGEXE = $(DBGDIR)/$(EXE_FILE)
DBGCFLAGS = -g -Og -Werror
RELDIR = build/release
RELEXE = $(RELDIR)/$(EXE_FILE)
-RELCFLAGS = -O2 -Os
+RELLIB = $(RELDIR)/$(LIB_NAME)
+RELLIBTMP = $(RELLIB).tmp
+RELCFLAGS = -DPLATFORM_HOTLOAD_GAME_CODE -O2 -Os
-.PHONY: all clean debug debug_dir gdb memcheck memcheck release release_dir run todo
+.PHONY: default all build_debug build_lib build_release clean dir_debug dir_release gdb memcheck run todo
-all: debug release
+default: build_release
-clean:
- rm -f $(RELDIR)/* $(DBGDIR)/*
+all: build_debug build_release
-debug: debug_dir
+build_debug: dir_debug
$(CC) $(CFLAGS) $(DBGCFLAGS) $(SRC) -o $(DBGEXE) $(LDFLAGS)
-debug_dir:
- @mkdir -p $(DBGDIR)
+build_lib:
+ $(CC) $(CFLAGS) -fpic -shared $(RELCFLAGS) $(LIB) -o $(RELLIBTMP) $(LDFLAGS)
+ mv $(RELLIBTMP) $(RELLIB)
-gdb: debug
- gdb $(DBGEXE)
+build_release: dir_release build_lib
+ $(CC) $(CFLAGS) $(RELCFLAGS) $(SRC) -o $(RELEXE) $(LDFLAGS)
-memcheck: debug
- valgrind --track-origins=yes ./$(DBGEXE)
+clean:
+ rm -f $(RELDIR)/* $(DBGDIR)/*
-release: release_dir
- $(CC) $(CFLAGS) $(RELCFLAGS) $(SRC) -o $(RELEXE) $(LDFLAGS)
+dir_debug:
+ @mkdir -p $(DBGDIR)
-release_dir:
+dir_release:
@mkdir -p $(RELDIR)
-run: release
+gdb: build_debug
+ gdb $(DBGEXE)
+
+memcheck: build_debug
+ valgrind --track-origins=yes ./$(DBGEXE)
+
+run: build_release
./$(RELEXE)
todo:
diff --git a/src/game.h b/src/game.h
@@ -11,6 +11,8 @@ struct GameState
struct Shader triangle_shader;
};
+typedef void (game_update_and_render_func)(struct GameState *game_state);
+
void game_init(struct GameState *game_state);
void game_update_and_render(struct GameState *game_state);
void game_deinit(struct GameState *game_state);
diff --git a/src/platform_linux.c b/src/platform_linux.c
@@ -1,16 +1,23 @@
+#include "platform_linux.h"
+
+#include <inttypes.h>
+#include <stdint.h>
#include <stdio.h>
+#include <time.h>
+
+#include <dlfcn.h>
+#include <sys/stat.h>
#define GLEW_STATC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
-#include "game.h"
-
-#define SCR_WIDTH 800
-#define SCR_HEIGHT 600
void error_callback(int error, const char* description);
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
+time_t file_get_modified_time(char *file_path);
+void unload_game_code(struct GameCode *game_code);
+struct GameCode load_game_code(char *source_lib_path);
int main(void)
@@ -28,7 +35,7 @@ int main(void)
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
- GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "A Game", NULL, NULL);
+ GLFWwindow* window = glfwCreateWindow(PLATFORM_SCR_WIDTH, PLATFORM_SCR_HEIGHT, "A Game", NULL, NULL);
if (!window)
{
fprintf(stderr, "GLFW window creation failed\n");
@@ -51,9 +58,38 @@ int main(void)
struct GameState game_state = {0};
game_init(&game_state);
+#ifdef PLATFORM_HOTLOAD_GAME_CODE
+ struct GameCode game_code = load_game_code(PLATFORM_GAME_LIB_PATH);
+#endif
+
+ uint64_t lag = 0;
+ uint64_t previous_ms = (glfwGetTimerValue() * PLATFORM_SECOND) / glfwGetTimerFrequency();
+
while (!glfwWindowShouldClose(window))
{
+ uint64_t current_ms = (glfwGetTimerValue() * PLATFORM_SECOND) / glfwGetTimerFrequency();
+ uint64_t elapsed_ms = current_ms - previous_ms;
+ previous_ms = current_ms;
+ lag += elapsed_ms;
+ //printf("%" PRIu64 ", %" PRIu64 ", %f\n", elapsed_ms, lag, PLATFORM_MS_PER_UPDATE);
+
+#ifdef PLATFORM_HOTLOAD_GAME_CODE
+ time_t last_write_time = file_get_modified_time(PLATFORM_GAME_LIB_PATH);
+ bool game_code_has_changed = last_write_time && (last_write_time != game_code.last_write_time);
+ if (game_code_has_changed)
+ {
+ unload_game_code(&game_code);
+ game_code = load_game_code(PLATFORM_GAME_LIB_PATH);
+ if (!game_code.is_valid)
+ {
+ // TODO: fall back to backup?
+ }
+ }
+ game_code.game_update_and_render(&game_state);
+#else
game_update_and_render(&game_state);
+#endif // PLATFORM_HOTLOAD_GAME_CODE
+
glfwSwapBuffers(window);
glfwPollEvents();
}
@@ -76,3 +112,71 @@ void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
+
+
+time_t file_get_modified_time(char *file_path)
+{
+ time_t mtime = 0;
+ struct stat file_status = {0};
+ if (stat(file_path, &file_status) == 0)
+ {
+ //printf("File: %s last modified: %s\n", file_path, ctime(&file_status.st_mtime));
+ mtime = file_status.st_mtime;
+ }
+ else
+ {
+ fprintf(stderr, "ERROR: Failed to stat file: %s\n", file_path);
+ }
+ return mtime;
+}
+
+
+void unload_game_code(struct GameCode *game_code)
+{
+ if (!game_code)
+ {
+ fprintf(stderr, "ERROR: Invalid pointer *game_code\n");
+ return;
+ }
+
+ if (game_code->game_code_library)
+ {
+ dlclose(game_code->game_code_library);
+ game_code->game_code_library = 0;
+ }
+ game_code->is_valid = false;
+ game_code->game_update_and_render = 0;
+}
+
+
+// TODO: Add backup dll in case loading fails
+struct GameCode load_game_code(char *source_lib_path)
+{
+ struct GameCode game_code = {0};
+
+ game_code.last_write_time = file_get_modified_time(source_lib_path);
+ if (game_code.last_write_time)
+ {
+ game_code.game_code_library = dlopen(source_lib_path, RTLD_LAZY);
+ 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.
+ //
+ // In this case, we are protected by POSIX, which specifies that
+ // function and data pointers must be the same size. We will only ever
+ // be using dlsym on POSIX-compliant platforms.
+ game_code.game_update_and_render = (game_update_and_render_func *) dlsym(game_code.game_code_library, "game_update_and_render");
+ game_code.is_valid = (game_code.game_update_and_render);
+ }
+ }
+
+ if (!game_code.is_valid)
+ {
+ fprintf(stderr, "ERROR: Game code is not valid: %s\n", dlerror());
+ }
+
+ return game_code;
+}
diff --git a/src/platform_linux.h b/src/platform_linux.h
@@ -0,0 +1,27 @@
+#ifndef PLATFORM_LINUX_H
+#define PLATFORM_LINUX_H
+
+#include <stdbool.h>
+#include <time.h>
+
+#include "game.h"
+
+#define PLATFORM_GAME_LIB_PATH "./build/release/game.so"
+#define PLATFORM_SCR_WIDTH 800
+#define PLATFORM_SCR_HEIGHT 600
+
+#define PLATFORM_SECOND 1000.0f
+#define PLATFORM_FPS 60
+#define PLATFORM_MS_PER_FRAME (PLATFORM_SECOND / PLATFORM_FPS)
+#define PLATFORM_UPDATES_PER_SECOND 120
+#define PLATFORM_MS_PER_UPDATE (PLATFORM_SECOND / PLATFORM_UPDATES_PER_SECOND)
+
+struct GameCode
+{
+ bool is_valid;
+ void *game_code_library;
+ time_t last_write_time;
+ game_update_and_render_func *game_update_and_render;
+};
+
+#endif