#include "engine/core.h" #include "engine/input.h" #include "engine/renderer.h" #include "engine/audio.h" #include "engine/assets.h" #include "engine/debuglog.h" #include #ifdef __EMSCRIPTEN__ #include #endif Engine g_engine = {0}; static GameCallbacks s_callbacks = {0}; void engine_set_callbacks(GameCallbacks cb) { s_callbacks = cb; } bool engine_init(void) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER) != 0) { fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); return false; } int win_w = SCREEN_WIDTH * WINDOW_SCALE; int win_h = SCREEN_HEIGHT * WINDOW_SCALE; g_engine.window = SDL_CreateWindow( WINDOW_TITLE, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, win_w, win_h, SDL_WINDOW_SHOWN ); if (!g_engine.window) { fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError()); return false; } g_engine.renderer = SDL_CreateRenderer( g_engine.window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC ); if (!g_engine.renderer) { fprintf(stderr, "SDL_CreateRenderer failed: %s\n", SDL_GetError()); return false; } /* Set logical size for pixel-perfect scaling */ SDL_RenderSetLogicalSize(g_engine.renderer, SCREEN_WIDTH, SCREEN_HEIGHT); SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0"); /* nearest neighbor */ /* Initialize subsystems */ input_init(); renderer_init(g_engine.renderer); audio_init(); assets_init(g_engine.renderer); g_engine.running = true; g_engine.tick = 0; g_engine.fps = 0.0f; printf("Engine initialized: %dx%d (x%d scale)\n", SCREEN_WIDTH, SCREEN_HEIGHT, WINDOW_SCALE); return true; } /* ── Frame-loop state (file-scope so engine_frame can access it) ── */ static uint64_t s_freq; static uint64_t s_prev_time; static float s_accumulator; static float s_fps_timer; static int s_fps_frames; /* * engine_frame -- execute exactly one iteration of the game loop. * * Called repeatedly by the platform layer: * Native : tight while-loop in engine_run() * Browser : emscripten_set_main_loop() via requestAnimationFrame */ static void engine_frame(void) { uint64_t current_time = SDL_GetPerformanceCounter(); float frame_time = (float)(current_time - s_prev_time) / (float)s_freq; s_prev_time = current_time; /* Clamp frame time to avoid spiral of death */ if (frame_time > MAX_FRAME_SKIP * DT) { frame_time = MAX_FRAME_SKIP * DT; } /* FPS counter */ s_fps_timer += frame_time; s_fps_frames++; if (s_fps_timer >= 1.0f) { g_engine.fps = (float)s_fps_frames / s_fps_timer; s_fps_timer = 0.0f; s_fps_frames = 0; } /* ── Input ────────────────────────────── */ input_poll(); if (input_quit_requested()) { g_engine.running = false; #ifdef __EMSCRIPTEN__ if (s_callbacks.shutdown) { s_callbacks.shutdown(); } emscripten_cancel_main_loop(); #endif return; } /* ── Fixed timestep update ────────────── */ s_accumulator += frame_time; while (s_accumulator >= DT) { if (s_callbacks.update) { s_callbacks.update(DT); } debuglog_record_tick(); input_consume(); g_engine.tick++; s_accumulator -= DT; } /* ── Render ───────────────────────────── */ g_engine.interpolation = s_accumulator / DT; renderer_clear(); if (s_callbacks.render) { s_callbacks.render(g_engine.interpolation); } renderer_present(); } void engine_run(void) { if (s_callbacks.init) { s_callbacks.init(); } /* Initialise frame-loop timing state */ s_freq = SDL_GetPerformanceFrequency(); s_prev_time = SDL_GetPerformanceCounter(); s_accumulator = 0.0f; s_fps_timer = 0.0f; s_fps_frames = 0; #ifdef __EMSCRIPTEN__ /* * Hand control to the browser's requestAnimationFrame loop. * fps=0 : match display refresh rate * simulate_infinite_loop=1 : don't return from this function * (so the rest of engine_run never executes until shutdown) */ emscripten_set_main_loop(engine_frame, 0, 1); #else while (g_engine.running) { engine_frame(); } #endif if (s_callbacks.shutdown) { s_callbacks.shutdown(); } } void engine_shutdown(void) { assets_free_all(); audio_shutdown(); renderer_shutdown(); input_shutdown(); if (g_engine.renderer) SDL_DestroyRenderer(g_engine.renderer); if (g_engine.window) SDL_DestroyWindow(g_engine.window); SDL_Quit(); printf("Engine shut down cleanly.\n"); }