a-game

2D platformer written from scratch.
git clone git://git.amin.space/a-game.git
Log | Files | Refs | README | LICENSE

platform_linux.c (9952B)


      1 #include <time.h>
      2 
      3 #include <dlfcn.h>
      4 #include <sys/stat.h>
      5 
      6 #include "platform_info.h"
      7 #include "am_gl.h"
      8 #include <GLFW/glfw3.h>
      9 
     10 #include "game.c"
     11 #include "platform.h"
     12 #include "platform_linux.h"
     13 
     14 internal void framebuffer_size_callback(GLFWwindow* window, int width, int height);
     15 internal void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods);
     16 
     17 // TODO: map this in a nicer way
     18 global_variable int platform_input_map[NUM_GAME_BUTTONS] = {
     19     [BTN_LEFT] = GLFW_KEY_LEFT,
     20     [BTN_RIGHT] = GLFW_KEY_RIGHT,
     21     [BTN_UP] = GLFW_KEY_UP,
     22     [BTN_DOWN] = GLFW_KEY_DOWN,
     23     [BTN_JUMP] = GLFW_KEY_S,
     24     [BTN_DEBUG_FLOAT] = GLFW_KEY_F11,
     25 };
     26 
     27 int main(void)
     28 {
     29     glfwSetErrorCallback(error_callback);
     30 
     31     if (!glfwInit())
     32     {
     33         fprintf(stderr, "GLFW initialization failed\n");
     34         return -1;
     35     }
     36 
     37     glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
     38     glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
     39     glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
     40     glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
     41 
     42     GLFWwindow* window = glfwCreateWindow(PLATFORM_SCR_WIDTH, PLATFORM_SCR_HEIGHT, "A Game", NULL, NULL);
     43     if (!window)
     44     {
     45         fprintf(stderr, "GLFW window creation failed\n");
     46         glfwTerminate();
     47         return -1;
     48     }
     49     glfwMakeContextCurrent(window);
     50     glfwSetKeyCallback(window, key_callback);
     51 
     52 #ifdef USE_TEST_SEED
     53     srand((uint32_t)0);
     54 #else
     55     srand((uint32_t)time(NULL));
     56 #endif
     57 
     58     if (!am_gl_init())
     59     {
     60         fprintf(stderr, "OpenGL function loading failed\n.");
     61         glfwTerminate();
     62         return -1;
     63     }
     64 
     65     struct GameMemory game_memory = {
     66         .buffer_size = MEBIBYTES(64),
     67         .platform = {
     68             &linux_read_entire_file,
     69             &linux_memory_free,
     70         },
     71     };
     72     // TODO: replace with mmap
     73     game_memory.buffer = malloc(game_memory.buffer_size);
     74     if (!game_memory.buffer)
     75     {
     76         fprintf(stderr, "Game memory allocation failed\n");
     77         glfwTerminate();
     78         return -1;
     79     }
     80 
     81     struct GameInput game_input = {0};
     82     glfwSetWindowUserPointer(window, &game_input);
     83     game_init(&game_memory, (v2u) {PLATFORM_SCR_WIDTH, PLATFORM_SCR_HEIGHT});
     84 
     85 #ifdef PLATFORM_HOTLOAD_GAME_CODE
     86     struct GameCode game_code = load_game_code(PLATFORM_GAME_LIB_PATH);
     87     game_code.game_load_opengl_symbols();
     88 #endif
     89 
     90     // NOTE(amin): lag should always be very small, so it's fine to use a float
     91     // to store it. We will not run out of precision.
     92     uint64_t previous_wall_clock_time = glfwGetTimerValue();
     93     uint64_t timer_frequency = glfwGetTimerFrequency();
     94 
     95     while (!glfwWindowShouldClose(window))
     96     {
     97         uint64_t current_wall_clock_time = glfwGetTimerValue();
     98 
     99         {
    100             uint64_t previous_frame_duration = current_wall_clock_time - previous_wall_clock_time;
    101             previous_wall_clock_time = current_wall_clock_time;
    102             f32 dt = (float)previous_frame_duration / (float)timer_frequency;
    103             // TODO: Debug periodic dropped frame on linux when fullscreen
    104             //printf("ms per frame: %f\n", dt * 1000.0f);
    105             game_input.dt = dt;
    106         }
    107 
    108         int32_t framebuffer_width = PLATFORM_SCR_WIDTH;
    109         int32_t framebuffer_height = PLATFORM_SCR_HEIGHT;
    110         glfwGetFramebufferSize(window, &framebuffer_width, &framebuffer_height);
    111         assert(framebuffer_width >= 0);
    112         assert(framebuffer_height >= 0);
    113         v2u framebuffer = {framebuffer_width, framebuffer_height};
    114 
    115 #ifdef PLATFORM_HOTLOAD_GAME_CODE
    116         time_t last_write_time = file_get_modified_time(PLATFORM_GAME_LIB_PATH);
    117         bool game_code_has_changed = last_write_time && (last_write_time != game_code.last_write_time);
    118         if (game_code_has_changed)
    119         {
    120             unload_game_code(&game_code);
    121             game_code = load_game_code(PLATFORM_GAME_LIB_PATH);
    122             game_code.game_load_opengl_symbols();
    123             if (!game_code.is_valid)
    124             {
    125                 // TODO: fall back to backup?
    126             }
    127         }
    128         game_code.game_update_and_render(&game_memory, &game_input, framebuffer);
    129 #else
    130         game_update_and_render(&game_memory, &game_input, framebuffer);
    131 #endif // PLATFORM_HOTLOAD_GAME_CODE
    132 
    133         // TODO: Do something sane when vsync is disabled
    134         glfwSwapBuffers(window);
    135 
    136         // ಠ_ಠ: We are affected here by a GLFW bug [1]. On my Thinkpad X220, if
    137         // I use the built-in keyboard, simultaneous key events sometimes get
    138         // dropped. This causes, for example, a press of the jump key to often
    139         // not be registered when the left or right arrow key is pressed
    140         // simultaneously.
    141         //
    142         // If I plug in my beloved Leopold FC750R, the problem does not
    143         // manifest.
    144         //
    145         // I have reproduced this behavior using glfw/tests/events.c
    146         //
    147         // TODO: Ditch GLFW on Linux, hope this gets fixed soon, or build it
    148         // from source and apply the workaround discussed in [1].
    149         //
    150         // [1] https://github.com/glfw/glfw/issues/1112
    151         glfwPollEvents();
    152     }
    153 
    154     game_cleanup(&game_memory);
    155 
    156     // TODO: replace with munmap
    157     free(game_memory.buffer);
    158 
    159     glfwDestroyWindow(window);
    160     glfwTerminate();
    161     return 0;
    162 }
    163 
    164 internal void error_callback(int error, const char *description)
    165 {
    166     fprintf(stderr, "Error: %s\n", description);
    167 }
    168 
    169 internal void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods)
    170 {
    171     struct GameInput *game_input = (struct GameInput *)glfwGetWindowUserPointer(window);
    172 
    173     enum GameButton game_button = NULL_GAME_BUTTON;
    174     // TODO: Determine the button in a nicer way
    175     for (enum GameButton b = 0; b < NUM_GAME_BUTTONS; b++)
    176     {
    177         if (platform_input_map[b] == key)
    178         {
    179             game_button = b;
    180         }
    181     }
    182     if (game_button != NULL_GAME_BUTTON)
    183     {
    184         switch (action)
    185         {
    186             case GLFW_PRESS:
    187                 game_input->button_states |= (1 << game_button);
    188                 break;
    189             case GLFW_RELEASE:
    190                 game_input->button_states &= ~(1 << game_button);
    191                 break;
    192             case GLFW_REPEAT:
    193                 // Do nothing
    194                 break;
    195             default:
    196                 exit(1);
    197                 break;
    198         }
    199     }
    200 }
    201 
    202 internal time_t file_get_modified_time(char *file_path)
    203 {
    204     time_t mtime = 0;
    205     struct stat file_status = {0};
    206     if (stat(file_path, &file_status) == 0)
    207     {
    208         //printf("File: %s last modified: %s\n", file_path, ctime(&file_status.st_mtime));
    209         mtime = file_status.st_mtime;
    210     }
    211     else
    212     {
    213         fprintf(stderr, "ERROR: Failed to stat file: %s\n", file_path);
    214     }
    215     return mtime;
    216 }
    217 
    218 PLATFORM_MEMORY_FREE(linux_memory_free)
    219 {
    220     free(ptr);
    221 }
    222 
    223 internal PLATFORM_READ_ENTIRE_FILE(linux_read_entire_file)
    224 {
    225     assert(a);
    226     FILE *handle = fopen(file_path, "r");
    227     u8 *buffer = NULL;
    228 
    229     if (handle)
    230     {
    231         // get file size
    232         fseek(handle, 0, SEEK_END);
    233         u32 num_bytes_in_file = ftell(handle);
    234         rewind(handle);
    235 
    236         if (out_num_bytes)
    237         {
    238             *out_num_bytes = num_bytes_in_file;
    239         }
    240 
    241         buffer = mem_st_alloc_buffer(a, u8, num_bytes_in_file + 1);
    242 
    243         u32 bytes_read = fread(buffer, sizeof(u8), num_bytes_in_file, handle);
    244         // IMPORTANT! fread() doesn't add the '\0'
    245         buffer[num_bytes_in_file] = '\0';
    246 
    247         // TODO: handle this case more loudly
    248         if (num_bytes_in_file != bytes_read)
    249         {
    250             buffer = NULL;
    251         }
    252 
    253         fclose(handle);
    254     }
    255     else
    256     {
    257         printf("Error: Couldn't open file at path: %s", file_path);
    258         // TODO: handle errors here in a better way
    259         exit(1);
    260     }
    261 
    262     return buffer;
    263 }
    264 
    265 #ifdef PLATFORM_HOTLOAD_GAME_CODE
    266 internal void unload_game_code(struct GameCode *game_code)
    267 {
    268     if (!game_code)
    269     {
    270         fprintf(stderr, "ERROR: Invalid pointer *game_code\n");
    271         return;
    272     }
    273 
    274     if (game_code->game_code_library)
    275     {
    276         dlclose(game_code->game_code_library);
    277         game_code->game_code_library = NULL;
    278     }
    279     game_code->is_valid = false;
    280     game_code->game_load_opengl_symbols = NULL;
    281     game_code->game_update_and_render = NULL;
    282 }
    283 
    284 
    285 // TODO: Add backup dll in case loading fails
    286 internal struct GameCode load_game_code(char *source_lib_path)
    287 {
    288     struct GameCode game_code = {0};
    289 
    290     game_code.last_write_time = file_get_modified_time(source_lib_path);
    291     if (game_code.last_write_time)
    292     {
    293         game_code.game_code_library = dlopen(source_lib_path, RTLD_LAZY);
    294         if (game_code.game_code_library)
    295         {
    296             // NOTE: The C standard (as of C99) distinguishes function pointers
    297             // from object pointers (`void *` is an object pointer). Technically it
    298             // is undefined behavior to cast between these two pointer types. In
    299             // practice, it works on most modern platforms.
    300             //
    301             // In this case, we are protected by POSIX, which specifies that
    302             // function and data pointers must be the same size. We will only ever
    303             // be using dlsym on POSIX-compliant platforms.
    304             static_assert(
    305                 sizeof(void *) == sizeof(void(*)()),
    306                 "Object pointer must be the same size as function pointer"
    307             );
    308             game_code.game_load_opengl_symbols = (game_load_opengl_symbols_func *) dlsym(game_code.game_code_library, "game_load_opengl_symbols");
    309             game_code.game_update_and_render = (game_update_and_render_func *) dlsym(game_code.game_code_library, "game_update_and_render");
    310             game_code.is_valid = (game_code.game_load_opengl_symbols && game_code.game_update_and_render);
    311         }
    312     }
    313 
    314     if (!game_code.is_valid)
    315     {
    316         fprintf(stderr, "ERROR: Game code is not valid: %s\n", dlerror());
    317     }
    318 
    319     return game_code;
    320 }
    321 #endif // PLATFORM_HOTLOAD_GAME_CODE