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(WERROR "Treat warnings as errors" OFF)
option(UBSAN "Enable undefined behaviour sanitizer" OFF) option(UBSAN "Enable undefined behaviour sanitizer" OFF)
option(PERFMON "Monitor performance of game code" ON) option(PERFMON "Monitor performance of game code" ON)
option(HOTRELOAD "Enable hot reloading of game code" OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
@ -19,6 +20,9 @@ macro(set_default_target_options target)
target_compile_options(${target} PRIVATE -fsanitize=undefined) target_compile_options(${target} PRIVATE -fsanitize=undefined)
target_link_options(${target} PRIVATE -fsanitize=undefined) target_link_options(${target} PRIVATE -fsanitize=undefined)
endif() endif()
if(${HOTRELOAD})
target_compile_definitions(engine PRIVATE HOTRELOAD)
endif()
endmacro() endmacro()
add_custom_target(format add_custom_target(format
@ -40,3 +44,9 @@ find_package(SDL2_image REQUIRED)
add_subdirectory(engine) add_subdirectory(engine)
add_subdirectory(game) 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 The path to a directory containing the assets must be passed on the
command line to run the game: 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) set_default_target_options(engine)
target_include_directories(engine PUBLIC include) target_include_directories(engine PUBLIC include)
if(${PERFMON}) if(${PERFMON})
target_compile_definitions(engine PRIVATE PERFMON) target_compile_definitions(engine PRIVATE PERFMON)
endif() endif()
target_link_libraries(engine PRIVATE SDL2::SDL2)

View File

@ -6,6 +6,7 @@
#include "engine_hooks.h" #include "engine_hooks.h"
#include <assert.h> #include <assert.h>
#include <dlfcn.h>
#include <stdatomic.h> #include <stdatomic.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdlib.h> #include <stdlib.h>
@ -25,8 +26,44 @@ typedef struct {
double max_start, max_evt, max_update, max_render, max_total; double max_start, max_evt, max_update, max_render, max_total;
} perf_t; } 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[]) int main(int argc, char *argv[])
{ {
#ifdef HOTRELOAD
load_game_lib();
#endif
int err = SDL_Init(SDL_INIT_VIDEO); int err = SDL_Init(SDL_INIT_VIDEO);
assert(0 == err); assert(0 == err);
@ -56,6 +93,10 @@ int main(int argc, char *argv[])
// Handle all events currently in queue // Handle all events currently in queue
SDL_Event evt; SDL_Event evt;
while (SDL_PollEvent(&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) if (game_evt(gamemem, &evt) != GAMESTATUS_OK)
goto quit; goto quit;
} }

View File

@ -21,14 +21,28 @@ typedef enum {
GAMESTATUS_QUIT, GAMESTATUS_QUIT,
} gamestatus_t; } 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; extern const engineconf_t game_conf;
#define X(hook) hook##_t hook;
void game_init(int argc, char *argv[], void *mem, SDL_Renderer *renderer); HOOKS_XLIST
void game_teardown(void *mem); #undef X
#endif
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);
#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) add_executable(game main.c)
target_link_libraries(game PRIVATE engine)
endif()
set_default_target_options(game) set_default_target_options(game)
target_include_directories(game PUBLIC include) target_include_directories(game PUBLIC include)
target_link_libraries(game PRIVATE target_link_libraries(game PRIVATE
engine
m m
LibXml2::LibXml2 LibXml2::LibXml2
SDL2::SDL2 SDL2::SDL2
SDL2::SDL2main
SDL2_image::SDL2_image SDL2_image::SDL2_image
) )