Hi! First, great work on SDL3. It took me all of about three hours to get everything up and running after upgrading from 2.30.0 to 3.2.0. Most of the issues I ran into were related to creating the window. One specific one was CreateWindow() using flags caused internal data to be null, but switching to CreateWindowWithProperties() fixed it.
Anyway. I have a question about the callback structure. After reading through SDL3/README/main-functions - SDL Wiki , I’m unsure which is better. “The primary way we expect people to write SDL apps is still with SDL_main, and this is not intended to replace it.” What’s the drawback to using callbacks instead of starting in main? I didn’t follow the development of SDL3, so maybe there’s some context I’m missing about why it supports both entry points and why a project would benefit from using main.
Thanks again for all of the effort from everyone involved.
I read the documentation early this week. On linux at least callbacks seemed to do almost nothing which I think is great. I never done mobile development or wasm but it seems like those platform can’t have a function that runs forevery which is why SDL provides callback. I guess if you want any of those platforms you should try callbacks. I had a concern about multiple threads calling event at the same time but I haven’t written enough SDL code to know if its a problem
Thanks for that! Yeah, from a design perspective, I don’t see a reason why developers wouldn’t just use callbacks since my code in main is basically doing what the documentation is describing, so this is primarily why I’m curious.
In a game I’m working on I have made some poor design decisions. The scripts can do certain actions that could take multiple frames to complete but during this time most things should still run as normal and when that’s done it should continue running the original script from where it left off. The way I have implemented this is basically by running the main loop recursively inside the scripts.
Example
actor.say("Hello!"); // pushes a text game state that will not get popped until the user press Enter. Events still need to be handled as normal for things like window resizing and keyboard shortcuts to still work, and to feed the key presses to the text state to allow it to detect the Enter press. This is why the game loop needs to run inside the this function.
actor.turn(left); // just change the state of the actor and continue immediately
wait(5); // will run the game for 5 frames before continuing...
...
I admit that this is not the best way to do it, and it has certain consequences, but I have spent a lot of time to make it work satisfactory so redesigning it now when the game is almost complete would just be a lot of extra work for very little gain so I don’t think it’s worth it.
This approach is of course totally incompatible with the SDL3 callback approach so when I decide it’s time to upgrade to SDL3 I won’t use the callbacks.
If I started a new game using SDL3, I don’t know which approach I would pick. I’m leaning towards the classic main function approach, because that’s what I’m used to and it means I won’t need to pass objects using globals or void pointers, but if there are advantages that I haven’t thought about I might consider it. It seems like the callbacks might help making window resizing look more responsive on some platforms but I’m not sure if it actually works better than the event watcher workaround. It’s also a bit unclear to me if and how the callbacks are meant to handle frame pacing. If I write the code myself it’s more obvious what is going on and easier to adjust. In any case, I would definitely try to stay away from having “recursive main loops” which should make switching approach much easier.
I chose SDL (then it was SDL2) to create my game because this library didn’t impose any framework on me. I could write all the engine code from scratch myself (which was and always be very important to me), and SDL only provided me with an API to handle various things, such as a window, input devices, GPU, and mixer.
In addition, the API is low-level, procedural-structural, without unnecessary abstractions and treating the user like an underdeveloped child who needs to be supervised at every step. Just pointers and buffers everywhere — do what you need to do, and if you produce bugs, it’s your problem. “We are all dumb, own your dumbness” — apt words. If the code you write has bugs, educate yourself and write the code correctly, instead of blaming the language/API for your incompetence.
I implemented the main loop the way I wanted, I have full control over its operation, no one imposed anything on me. I have fixed update rate, variable frame rate, event processing in groups that are time-matched to a given logic step, spiral of death protection, freezing the game thread to save CPU time, performance counters and many other things. Only what I wanted and the way I wanted, control over convenience. I don’t need anyone to implement such important things for me.
That’s why I don’t touch any automatic mechanisms that I don’t have full control over, including callbacks for updating logic or gamepad mapping. I’m not interested in the mobile market, so I have complete freedom of action.
also, beware that I’m still noob, so I haven’t written anything complex. I’ve seen that one advantage of main loop in big project is to have several loops, depending on which screen you are: main menu, inventory, loading screen, options, cutscene, game etc. I lost the link to an open source game on GitHub that utilizes this strategy, but it struck me as it is really really neat.
Event processing in groups that are time-matched to a given logic step
My engine runs with a constant update rate (60ups), maintaining 60 discrete states. If there is no lag, the logic is updated once, processing all events from the queue. However, if there is a lag and the logic must be updated several times (within the same frame), each update step processes a portion of events from the queue.
Suppose there is a lag and the logic needs to be updated 4 times to catch up with real time. The time window from the previous update is divided into 4 time slots, and then each step of the logic pulls events from the queue according to these time slots. The last step always processes all the remaining events so as to empty the queue and not cause input lag.
This is because my engine first processes events and collects data in various objects (windows, mouse, keyboard, joysticks, etc.), then updates the logic and renders a frame. It works like retro consoles (e.g. NES, SNES). More info here:
Spiral of death protection
Also known as the spiral of doom, this is a situation where the generation of a game frame (mostly logic update) takes longer than the time window intended for it (e.g. 16.7ms at 60fps). Each such overly long update causes the game time to become more and more out of sync with real time and ultimately leads to the game freezing for a very long time.
To fix this, the loop checks how many times the game logic needs to be updated to catch up with real time, and if there are too many updates (over 10 in my engine, which is 1/6 of a second), then the entire event queue is processed at once (to empty it), and the logic is updated only once. This way, instead of falling into an infinite lagging loop, the game will catch up with real time immediately. The effect on the screen is that the simulation will slow down for a very short moment, but will never freeze.
Freezing the game thread to save CPU time
My loop is programmed in such a way that if generating a given game frame takes less than the time window for it (16.7ms for 60fps), the thread sleeps the remaining time, giving CPU time to other processes.
Let’s assume that it took 3ms to generate a frame. In this case, the game thread will sleep for another 13.7ms, using SDL_Delay and a short spinlock (with assembly pause instruction) to maintain timing precision.
Thanks to this, the game process uses only as much CPU time as it needs. This means that other running processes in the system can also use the CPU (very important for old and low-end devices); the CPU does not increase the clock speed unnecessarily and does not enable turbo mode; the battery life of the device (mainly laptops) is extended to the maximum.
My game will be in a retro style, strongly imitating those for consoles from the 90s, which is why I use a constant update rate of 60ups and a maximum of 60fps, without any rendering interpolation (firstly, it does not fit the style of the game, and secondly, in most cases it is an unnecessary feature that only complicates the implementation).