diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a48c44..08147d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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=\"$\") +endif() diff --git a/README b/README index 6673c04..17d6ac2 100644 --- a/README +++ b/README @@ -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/ diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 7248276..9d60c23 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -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) diff --git a/engine/engine.c b/engine/engine.c index 5a2aec7..650ad8c 100644 --- a/engine/engine.c +++ b/engine/engine.c @@ -6,6 +6,7 @@ #include "engine_hooks.h" #include +#include #include #include #include @@ -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; } diff --git a/engine/include/engine_hooks.h b/engine/include/engine_hooks.h index c0de503..0490a51 100644 --- a/engine/include/engine_hooks.h +++ b/engine/include/engine_hooks.h @@ -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 diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt index 247a9fb..8b88df6 100644 --- a/game/CMakeLists.txt +++ b/game/CMakeLists.txt @@ -1,12 +1,17 @@ -add_executable(game main.c) +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 )