ohsp

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

platform_sdl.c (17445B)


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