ohsp

Prototype for a game with dual thruster controls.
git clone git://git.amin.space/ohsp.git
Log | Files | Refs | LICENSE

platform_sdl.c (17976B)


      1 #include "platform_sdl.h"
      2 
      3 #if !defined(OHSP_NO_HOT_RELOAD)
      4 #include <dlfcn.h>
      5 #include <sys/stat.h>
      6 #include <stdio.h>
      7 #include <time.h>
      8 
      9 time_t file_get_modified_time(char *file_path)
     10 {
     11     time_t mtime = 0;
     12     struct stat file_status = {0};
     13     if (stat(file_path, &file_status) == 0)
     14     {
     15         //printf("File: %s last modified: %s\n", file_path, ctime(&file_status.st_mtime));
     16         mtime = file_status.st_mtime;
     17     }
     18     else
     19     {
     20         fprintf(stderr, "ERROR: Failed to stat file: %s\n", file_path);
     21     }
     22     return mtime;
     23 }
     24 
     25 
     26 void unload_game_code(struct SDLGameCode *game_code)
     27 {
     28     if (!game_code)
     29     {
     30         printf("Invalid pointer *game_code\n");
     31         return;
     32     }
     33 
     34     if (game_code->game_code_library)
     35     {
     36         dlclose(game_code->game_code_library);
     37         game_code->game_code_library = 0;
     38     }
     39     game_code->is_valid = false;
     40     game_code->game_update = 0;
     41     game_code->game_render = 0;
     42 }
     43 
     44 
     45 // TODO: Add backup dll in case loading fails
     46 struct SDLGameCode load_game_code(char *source_lib_path)
     47 {
     48     struct SDLGameCode game_code = {0};
     49 
     50     game_code.last_write_time = file_get_modified_time(source_lib_path);
     51     if (game_code.last_write_time)
     52     {
     53         game_code.game_code_library = dlopen(source_lib_path, RTLD_LAZY);
     54         if (game_code.game_code_library)
     55         {
     56             // NOTE: The C standard (as of C99) distinguishes function pointers
     57             // from object pointers (`void *` is an object pointer). Technically it
     58             // is undefined behavior to cast between these two pointer types. In
     59             // practice, it works on most modern platforms.
     60             //
     61             // In this case, we are protected by POSIX, which specifies that
     62             // function and data pointers must be the same size. We will only ever
     63             // be using dlsym on POSIX-compliant platforms.
     64             game_code.game_update = (game_update_t *) dlsym(game_code.game_code_library, "game_update");
     65             game_code.game_render = (game_render_t *) dlsym(game_code.game_code_library, "game_render");
     66             game_code.is_valid = (game_code.game_update && game_code.game_render);
     67         }
     68     }
     69 
     70     if (!game_code.is_valid)
     71     {
     72         fprintf(stderr, "ERROR: Game code is not valid: %s\n", dlerror());
     73     }
     74 
     75     return game_code;
     76 }
     77 #endif
     78 
     79 #include <stdbool.h>
     80 #include <stdio.h>
     81 #include <string.h>
     82 #include <time.h>
     83 
     84 #ifndef MAP_ANONYMOUS
     85 #define MAP_ANONYMOUS MAP_ANON
     86 #endif
     87 
     88 extern bool PAUSED;
     89 
     90 static struct SDLOffscreenBuffer global_back_buffer = {0};
     91 static struct SDLInputRecord input_record = {0};
     92 static bool TRACE = false;
     93 
     94 
     95 struct SDLWindowDimension sdl_get_window_dimension(SDL_Window *window)
     96 {
     97     struct SDLWindowDimension result = {0};
     98     SDL_GetWindowSize(window, &result.width, &result.height);
     99     return(result);
    100 }
    101 
    102 
    103 void sdl_resize_texture(struct SDLOffscreenBuffer *buffer, SDL_Renderer *renderer, int width, int height)
    104 {
    105     if (buffer->memory)
    106     {
    107         free(buffer->memory);
    108     }
    109 
    110     if (buffer->texture)
    111     {
    112         SDL_DestroyTexture(buffer->texture);
    113     }
    114 
    115     buffer->texture = SDL_CreateTexture(
    116             renderer,
    117             SDL_PIXELFORMAT_ARGB8888,
    118             SDL_TEXTUREACCESS_STREAMING,
    119             width, height);
    120 
    121     buffer->width = width;
    122     buffer->height = height;
    123     buffer->pitch = width * BYTES_PER_PIXEL;
    124 
    125     buffer->memory = malloc(width * height * BYTES_PER_PIXEL);
    126 }
    127 
    128 
    129 void sdl_update_window(SDL_Renderer *renderer, struct SDLOffscreenBuffer *buffer)
    130 {
    131     if (SDL_UpdateTexture(buffer->texture, 0, buffer->memory, buffer->pitch))
    132     {
    133         // TODO(amin): Handle this error
    134     }
    135 
    136     SDL_RenderCopy(renderer, buffer->texture, 0, 0);
    137     SDL_RenderPresent(renderer);
    138 }
    139 
    140 
    141 void clear_screen(struct SDLOffscreenBuffer *buffer, uint32_t pixel_value)
    142 {
    143     // NOTE(amin): Memset is faster than nested for loops, but can only set
    144     // pixels to single byte values
    145     memset(buffer->memory, pixel_value, buffer->height * buffer->width * BYTES_PER_PIXEL);
    146 }
    147 
    148 
    149 // TODO: make the deadzone circular
    150 float sdl_process_controller_axis_value(int16_t value, int16_t deadzone_threshold)
    151 {
    152     float result = 0.0f;
    153     if (value < -deadzone_threshold)
    154     {
    155         result = (float)(value + deadzone_threshold) / (32768.0f - deadzone_threshold);
    156     }
    157     else if (value > deadzone_threshold)
    158     {
    159         result = (float)(value - deadzone_threshold) / (32767.0f - deadzone_threshold);
    160     }
    161     return result;
    162 }
    163 
    164 
    165 bool sdl_handle_event(SDL_Event *event, struct GameControllerInput *controller_input)
    166 {
    167     if (!event || !controller_input)
    168     {
    169         return true;
    170     }
    171 
    172     bool should_quit = false;
    173 
    174     switch(event->type)
    175     {
    176         case SDL_QUIT:
    177         {
    178             printf("SDL_QUIT\n");
    179             should_quit = true;
    180         } break;
    181 
    182         case SDL_KEYDOWN:
    183         case SDL_KEYUP:
    184         {
    185             SDL_Keycode key_code = event->key.keysym.sym;
    186             bool is_down = (event->key.state == SDL_PRESSED);
    187             if (is_down)
    188             {
    189                 if (key_code == SDLK_p)
    190                 {
    191                     PAUSED = !PAUSED;
    192                 }
    193                 else if (key_code == SDLK_t)
    194                 {
    195                     TRACE = !TRACE;
    196                 }
    197                 else if (key_code == SDLK_r)
    198                 {
    199                     if (input_record.state == INACTIVE)
    200                     {
    201                         printf("input_record: RECORDING\n");
    202                         input_record.initial_state_recorded = false;
    203                         input_record.record_head = 0;
    204                         input_record.state = RECORDING;
    205                     }
    206                     else if (input_record.state == RECORDING)
    207                     {
    208                         printf("input_record: INACTIVE\n");
    209                         input_record.state = INACTIVE;
    210                     }
    211                     else if (input_record.state == PLAYING_BACK)
    212                     {
    213                         printf("Can't record while playing back\n");
    214                     }
    215                     else
    216                     {
    217                         printf("ERROR: input_record invalid state\n");
    218                     }
    219                 }
    220                 else if (key_code == SDLK_l)
    221                 {
    222                     if (input_record.state == INACTIVE)
    223                     {
    224                         printf("input_record: PLAYING_BACK\n");
    225                         input_record.playback_head = 0;
    226                         input_record.state = PLAYING_BACK;
    227                     }
    228                     else if (input_record.state == PLAYING_BACK)
    229                     {
    230                         printf("input_record: INACTIVE\n");
    231                         input_record.state = INACTIVE;
    232                     }
    233                     else if (input_record.state == RECORDING)
    234                     {
    235                         printf("Can't play back while recording\n");
    236                     }
    237                     else
    238                     {
    239                         printf("ERROR: input_record invalid state\n");
    240                     }
    241                 }
    242             }
    243         } break;
    244 
    245         case SDL_CONTROLLERAXISMOTION:
    246         {
    247             // down and right are positive
    248             float normalized_axis_value = sdl_process_controller_axis_value(event->caxis.value, DEADZONE_THRESHOLD);
    249 
    250             //printf("SDL_CONTROLLERAXISMOTION (%d, %s, %d, %f)\n",
    251             //        event->caxis.which,
    252             //        SDL_GameControllerGetStringForAxis(event->caxis.axis),
    253             //        event->caxis.value,
    254             //        normalized_axis_value);
    255 
    256             switch(event->caxis.axis)
    257             {
    258                 case SDL_CONTROLLER_AXIS_LEFTX:
    259                 {
    260                     controller_input->left_stick_x = normalized_axis_value;
    261                 } break;
    262                 case SDL_CONTROLLER_AXIS_LEFTY:
    263                 {
    264                     controller_input->left_stick_y = normalized_axis_value;
    265                 } break;
    266                 case SDL_CONTROLLER_AXIS_RIGHTX:
    267                 {
    268                     controller_input->right_stick_x = normalized_axis_value;
    269                 } break;
    270                 case SDL_CONTROLLER_AXIS_RIGHTY:
    271                 {
    272                     controller_input->right_stick_y = normalized_axis_value;
    273                 } break;
    274                 default:
    275                 {
    276                     printf("Unhandled axis input\n");
    277                 }
    278             }
    279         } break;
    280 
    281         case SDL_WINDOWEVENT:
    282         {
    283             switch(event->window.event)
    284             {
    285                 case SDL_WINDOWEVENT_SIZE_CHANGED:
    286                 {
    287                     SDL_Window *window = SDL_GetWindowFromID(event->window.windowID);
    288                     SDL_Renderer *renderer = SDL_GetRenderer(window);
    289                     printf("SDL_WINDOWEVENT_SIZE_CHANGED (%d, %d)\n", event->window.data1, event->window.data2);
    290                     sdl_resize_texture(&global_back_buffer, renderer, event->window.data1, event->window.data2);
    291                 } break;
    292 
    293                 case SDL_WINDOWEVENT_FOCUS_GAINED:
    294                 {
    295                     printf("SDL_WINDOWEVENT_FOCUS_GAINED\n");
    296                 } break;
    297 
    298                 case SDL_WINDOWEVENT_EXPOSED:
    299                 {
    300                     SDL_Window *window = SDL_GetWindowFromID(event->window.windowID);
    301                     SDL_Renderer *renderer = SDL_GetRenderer(window);
    302                     sdl_update_window(renderer, &global_back_buffer);
    303                 } break;
    304             }
    305         } break;
    306     }
    307     return(should_quit);
    308 }
    309 
    310 
    311 int main(int argc, char *argv[])
    312 {
    313     if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER))
    314     {
    315         // TODO(amin): log SDL_Init error
    316     }
    317 
    318     SDL_Window *window = SDL_CreateWindow(
    319             TITLE,
    320             SDL_WINDOWPOS_UNDEFINED,
    321             SDL_WINDOWPOS_UNDEFINED,
    322             SCREEN_WIDTH,
    323             SCREEN_HEIGHT,
    324             SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
    325 
    326 #ifdef FULLSCREEN
    327     SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP);
    328 #endif
    329 
    330     if (window)
    331     {
    332         SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, 0);
    333 
    334         if (renderer)
    335         {
    336             struct SDLWindowDimension dimension = sdl_get_window_dimension(window);
    337             sdl_resize_texture(&global_back_buffer, renderer, dimension.width, dimension.height);
    338 
    339             bool running = true;
    340 
    341 #ifdef USE_TEST_SEED
    342             srand((uint32_t)0);
    343 #else
    344             srand((uint32_t)time(NULL));
    345 #endif
    346 
    347             int num_joysticks = SDL_NumJoysticks();
    348             SDL_GameController *controller_handles[MAX_CONTROLLERS];
    349             for (int controller_index = 0; controller_index < num_joysticks; ++controller_index)
    350             {
    351                 if (!SDL_IsGameController(controller_index))
    352                 {
    353                     continue;
    354                 }
    355 
    356                 if (controller_index >= MAX_CONTROLLERS)
    357                 {
    358                     break;
    359                 }
    360 
    361                 controller_handles[controller_index] = SDL_GameControllerOpen(controller_index);
    362             }
    363 
    364             uint64_t lag = 0;
    365             uint64_t previous_ms = (SDL_GetPerformanceCounter() * SECOND) / SDL_GetPerformanceFrequency();
    366 
    367             struct GameState game_state = {0};
    368             game_init(&game_state, dimension.width, dimension.height);
    369 
    370             struct GameControllerInput controller_input = {0};
    371 
    372 #if !defined(OHSP_NO_HOT_RELOAD)
    373             struct SDLGameCode game_code = load_game_code(GAME_LIB_PATH);
    374 #endif
    375 
    376             while (running)
    377             {
    378                 uint64_t current_ms = (SDL_GetPerformanceCounter() * SECOND) / SDL_GetPerformanceFrequency();
    379                 uint64_t elapsed_ms = current_ms - previous_ms;
    380                 previous_ms = current_ms;
    381                 lag += elapsed_ms;
    382                 //printf("%" PRIu64 ", %" PRIu64 ", %f\n", elapsed_ms, lag, MS_PER_UPDATE);
    383 
    384 #if !defined(OHSP_NO_HOT_RELOAD)
    385                 time_t last_write_time = file_get_modified_time(GAME_LIB_PATH);
    386                 bool game_code_has_changed = last_write_time && (last_write_time != game_code.last_write_time);
    387                 if (game_code_has_changed)
    388                 {
    389                     unload_game_code(&game_code);
    390                     game_code = load_game_code(GAME_LIB_PATH);
    391                     if (!game_code.is_valid)
    392                     {
    393                         // TODO: fall back to backup?
    394                     }
    395                 }
    396 #endif
    397 
    398                 SDL_Event event;
    399                 while (SDL_PollEvent(&event))
    400                 {
    401                     running = !sdl_handle_event(&event, &controller_input);
    402                 }
    403                 SDL_PumpEvents();
    404 
    405                 switch (input_record.state)
    406                 {
    407                     case INACTIVE: {} break;
    408 
    409                     case RECORDING:
    410                     {
    411                         // TODO: sdl_input_record(&controller_input);
    412                         if (input_record.record_head < INPUT_RECORD_SIZE)
    413                         {
    414                             if (input_record.record_head == 0)
    415                             {
    416                                 input_record.initial_state = game_state;
    417                                 input_record.initial_state_recorded = true;
    418                             }
    419                             input_record.input_buffer[input_record.record_head] = controller_input;
    420                             ++input_record.record_head;
    421                         }
    422                         else
    423                         {
    424                             printf("Input record buffer full\n");
    425                             input_record.state = INACTIVE;
    426                         }
    427                     } break;
    428 
    429                     case PLAYING_BACK:
    430                     {
    431                         if (input_record.playback_head < input_record.record_head)
    432                         {
    433                             if (input_record.playback_head == 0 && input_record.initial_state_recorded)
    434                             {
    435                                 game_state = input_record.initial_state;
    436                             }
    437 
    438                             // TODO: sdl_input_playback(&controller_input);
    439                             controller_input = input_record.input_buffer[input_record.playback_head];
    440                             ++input_record.playback_head;
    441                         }
    442                         else
    443                         {
    444                             printf("Playback restarting\n");
    445                             input_record.playback_head = 0;
    446                         }
    447                     } break;
    448 
    449                     default:
    450                     {
    451                         printf("Unhandled SDLInputRecordState\n");
    452                     } break;
    453                 }
    454 
    455                 dimension = sdl_get_window_dimension(window);
    456 
    457                 struct OffscreenBuffer buffer =
    458                 {
    459                     // WARNING: these pointers are aliased until the end of the
    460                     // loop
    461                     .memory = global_back_buffer.memory,
    462                     .width = global_back_buffer.width,
    463                     .height = global_back_buffer.height,
    464                     .pitch = global_back_buffer.pitch,
    465                 };
    466 
    467                 if (PAUSED)
    468                 {
    469                     lag = 0;
    470                 }
    471                 else if (input_record.state == RECORDING || input_record.state == PLAYING_BACK)
    472                 {
    473                     // TODO: Make up your mind about what kind of time step and game loop this game needs.
    474 
    475                     // When recording or playing back, we do exactly one update
    476                     // per frame. This is so that recorded input played back
    477                     // over the initial state always results in the same
    478                     // sequence of states.
    479 #if !defined(OHSP_NO_HOT_RELOAD)
    480                     game_code.game_update(&game_state, controller_input, buffer.width, buffer.height);
    481 #else
    482                     game_update(&game_state, controller_input, buffer.width, buffer.height);
    483 #endif
    484                     lag -= MS_PER_UPDATE;
    485                 }
    486                 else
    487                 {
    488                     while (lag >= MS_PER_UPDATE)
    489                     {
    490 #if !defined(OHSP_NO_HOT_RELOAD)
    491                         game_code.game_update(&game_state, controller_input, buffer.width, buffer.height);
    492 #else
    493                         game_update(&game_state, controller_input, buffer.width, buffer.height);
    494 #endif
    495                         //printf("\t%" PRIu64 ", %f\n", lag, MS_PER_UPDATE);
    496                         lag -= MS_PER_UPDATE;
    497                     }
    498                 }
    499 
    500                 if (!TRACE)
    501                 {
    502                     clear_screen(&global_back_buffer, COLOR_BACKGROUND);
    503                 }
    504 #if !defined(OHSP_NO_HOT_RELOAD)
    505                 game_code.game_render(&buffer, lag/SECOND, &game_state);
    506 #else
    507                 game_render(&buffer, lag/SECOND, &game_state);
    508 #endif
    509                 sdl_update_window(renderer, &global_back_buffer);
    510                 if (elapsed_ms <= MS_PER_FRAME)
    511                 {
    512                     SDL_Delay(MS_PER_FRAME - elapsed_ms);
    513                 }
    514             }
    515 
    516             game_cleanup(&game_state);
    517 
    518             for (int controller_index = 0; controller_index < num_joysticks; ++controller_index)
    519             {
    520                 if (!SDL_IsGameController(controller_index))
    521                 {
    522                     continue;
    523                 }
    524 
    525                 if (controller_index >= MAX_CONTROLLERS)
    526                 {
    527                     break;
    528                 }
    529 
    530                 SDL_GameControllerClose(controller_handles[controller_index]);
    531             }
    532         }
    533         else
    534         {
    535             // TODO(amin): log SDL_Renderer error
    536         }
    537     }
    538     else
    539     {
    540         // TODO(amin): log SDL_Window error
    541     }
    542 
    543     SDL_Quit();
    544     return(0);
    545 }