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 }