Implement hot reloading

This commit is contained in:
Camden Dixie O'Brien 2025-01-05 19:41:44 +00:00
parent 1531817dc4
commit 2077ecde63
6 changed files with 100 additions and 13 deletions

View File

@ -5,6 +5,7 @@ project(epec-mcu-emulator LANGUAGES C)
option(WERROR "Treat warnings as errors" OFF)
option(UBSAN "Enable undefined behaviour sanitizer" OFF)
option(PERFMON "Monitor performance of game code" ON)
option(HOTRELOAD "Enable hot reloading of game code" OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
@ -19,6 +20,9 @@ macro(set_default_target_options target)
target_compile_options(${target} PRIVATE -fsanitize=undefined)
target_link_options(${target} PRIVATE -fsanitize=undefined)
endif()
if(${HOTRELOAD})
target_compile_definitions(engine PRIVATE HOTRELOAD)
endif()
endmacro()
add_custom_target(format
@ -40,3 +44,9 @@ find_package(SDL2_image REQUIRED)
add_subdirectory(engine)
add_subdirectory(game)
# Really this should be in engine/CMakeLists.txt, but it has to go
# after the declaration of the game target.
if(${HOTRELOAD})
target_compile_definitions(engine PRIVATE GAMELIB=\"$<TARGET_FILE:game>\")
endif()

14
README
View File

@ -13,4 +13,16 @@ The build is handled with CMake (version 3.10 or later):
The path to a directory containing the assets must be passed on the
command line to run the game:
build/app/game ASSETS-DIR
build/game/game ASSETS-DIR
Hot Reloading
The engine supports hot reloading of the game code via the F5 key; set
the HOTRELOAD option on in CMake to enable it. You also have to run an
executable built from the engine rather than the game if using this
feature.
cmake -B build -D HOTRELOAD=ON
cmake --build build
build/engine/engine assets/

View File

@ -1,6 +1,11 @@
add_library(engine engine.c)
if(${HOTRELOAD})
add_executable(engine engine.c)
else()
add_library(engine SHARED engine.c)
endif()
set_default_target_options(engine)
target_include_directories(engine PUBLIC include)
if(${PERFMON})
target_compile_definitions(engine PRIVATE PERFMON)
endif()
target_link_libraries(engine PRIVATE SDL2::SDL2)

View File

@ -6,6 +6,7 @@
#include "engine_hooks.h"
#include <assert.h>
#include <dlfcn.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdlib.h>
@ -25,8 +26,44 @@ typedef struct {
double max_start, max_evt, max_update, max_render, max_total;
} perf_t;
#ifdef HOTRELOAD
static void *game_lib;
static engineconf_t game_conf;
#define X(hook) static hook##_t *hook;
HOOKS_XLIST
#undef X
static void load_game_lib(void)
{
bool firstload = game_lib == NULL;
if (!firstload)
dlclose(game_lib);
game_lib = dlopen(GAMELIB, RTLD_NOW);
assert(game_lib);
// We're copying this rather than using it as a pointer so that
// the syntax to use it with/without hot reloading is the same.
engineconf_t *conf = (engineconf_t *)dlsym(game_lib, "game_conf");
if (!firstload)
assert(conf->memsize == game_conf.memsize);
memcpy(&game_conf, conf, sizeof(engineconf_t));
#define X(hook) \
hook = (hook##_t *)dlsym(game_lib, #hook); \
assert(hook);
HOOKS_XLIST
#undef X
}
#endif
int main(int argc, char *argv[])
{
#ifdef HOTRELOAD
load_game_lib();
#endif
int err = SDL_Init(SDL_INIT_VIDEO);
assert(0 == err);
@ -56,6 +93,10 @@ int main(int argc, char *argv[])
// Handle all events currently in queue
SDL_Event evt;
while (SDL_PollEvent(&evt)) {
#ifdef HOTRELOAD
if (evt.type == SDL_KEYDOWN && evt.key.keysym.sym == SDLK_F5)
load_game_lib();
#endif
if (game_evt(gamemem, &evt) != GAMESTATUS_OK)
goto quit;
}

View File

@ -21,14 +21,28 @@ typedef enum {
GAMESTATUS_QUIT,
} gamestatus_t;
typedef void
game_init_t(int argc, char *argv[], void *mem, SDL_Renderer *renderer);
typedef void game_teardown_t(void *mem);
typedef gamestatus_t game_evt_t(void *mem, const SDL_Event *evt);
typedef gamestatus_t game_update_t(void *mem, double dt);
typedef void
game_render_t(const void *mem, SDL_Renderer *renderer, long unsigned t);
#define HOOKS_XLIST \
X(game_init) \
X(game_teardown) \
X(game_evt) \
X(game_update) \
X(game_render)
#ifndef HOTRELOAD
extern const engineconf_t game_conf;
void game_init(int argc, char *argv[], void *mem, SDL_Renderer *renderer);
void game_teardown(void *mem);
gamestatus_t game_evt(void *mem, const SDL_Event *evt);
gamestatus_t game_update(void *mem, double dt);
void game_render(const void *mem, SDL_Renderer *renderer, long unsigned t);
#define X(hook) hook##_t hook;
HOOKS_XLIST
#undef X
#endif
#endif

View File

@ -1,12 +1,17 @@
if(${HOTRELOAD})
add_library(game SHARED main.c)
get_target_property(ENGINE_INCLUDES engine INCLUDE_DIRECTORIES)
target_include_directories(game PRIVATE ${ENGINE_INCLUDES})
else()
add_executable(game main.c)
target_link_libraries(game PRIVATE engine)
endif()
set_default_target_options(game)
target_include_directories(game PUBLIC include)
target_link_libraries(game PRIVATE
engine
m
LibXml2::LibXml2
SDL2::SDL2
SDL2::SDL2main
SDL2_image::SDL2_image
)