[Previous post](https://metrognomes.dev/game-dev/0x2006_Issue+0x4+-+Game+Struct+and+Scaffolding) described the problem: SDL's callback pattern hid control flow and forced heap allocation through `void* appstate`. This is the resolution. What actually shipped. --- ## The Final Structure The callback pattern required four functions (`SDL_AppInit`, `SDL_AppIterate`, `SDL_AppEvent`, `SDL_AppQuit`) and a wrapper struct to hold both `Engine` and `Game` on the heap. The new structure has one function (`main`) and two stack-allocated structs: ``` // Before (callbacks) typedef struct { Engine engine; Game game; } AppState; SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) { AppState *app = calloc(1, sizeof(AppState)); // ... } SDL_AppResult SDL_AppIterate(void* appstate) { AppState *app = (AppState *)appstate; // ← Cast every frame // ... } // After (main) int main(int argc, char* argv[]) { (void)argc; (void)argv; Engine engine; // ← Stack Game game; // ← Stack // ...init... while (running) { // ...loop... } // ...cleanup... } ``` **Zero `void*` casting. Zero heap allocation. Zero wrapper struct.** --- ## What Changed |Component|Before|After| |---|---|---| |**Entry point**|`SDL_AppInit/Iterate/Quit`|`int main()` + `<SDL3/SDL_main.h>`| |**State passing**|`void* appstate` cast|Direct local variables| |**Cleanup**|`SDL_AppQuit`|Explicit `game_shutdown` → `engine_shutdown` → `SDL_Quit`| |**Control flow**|Hidden in SDL dispatcher|Linear loop, debuggable| |**Input state**|Custom `InputState` (520 bytes)|SDL's native event + polling| --- ## The Input Simplification I also scrapped a custom `InputState` struct that tracked `current[]` and `previous[]` arrays for "just pressed" detection. According to [SDL3's Best Keyboard Practices](https://wiki.libsdl.org/SDL3/BestKeyboardPractices), the hybrid approach is: - **Events** (`SDL_EVENT_KEY_DOWN`) for discrete actions (jump, attack, pause) - **Polling** (`SDL_GetKeyboardState()`) for continuous input (WASD movement) The custom state tracker was 520 bytes of redundant memory and boilerplate. I replaced it with two patterns that already existed. --- ## The Cleanup Chain One thing that required careful thought was destruction order. The original code called `SDL_Quit()` unconditionally at the end, which broke error paths. **Correct order:** ``` game_shutdown(&game); // Clean up game resources first engine_shutdown(&engine); // Then engine SDL_Quit(); // Finally SDL ``` And every early return cleans up what came before: ``` if (!game_init(&game, &engine)) { engine_shutdown(&engine); // ← Engine was allocated first, must free last return 1; } ``` --- ## Lessons From The Refactor |Observation|Lesson| |---|---| |**Callbacks forced heap**|Abstraction can hide allocation costs| |**Hidden control flow**|If you can't see the loop, you can't debug it| |**Custom input tracking**|Read docs before building (SDL already solved it)| |**520 bytes wasted**|Every byte of state must earn its keep| |**Stack allocation works**|Sometimes "simple" means "local variables"| --- ## Next Up: Issue 0x5 — Draw a square on screen --- _[[email protected]](mailto:[email protected]) | Previous: [[0x2005_Issue 0x2, 0x3 - Input]] → Next: [[0x2008_Issue 0x5 - Draw a square on screen]]_ _Transparency note: I use Lumo as a technical reviewer for these posts — checking C semantics, verifying claims, and catching formatting issues. The ideas, mistakes, and fixes are my own._