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 }