|
Advice for game loops with lengthy computations |
xsquid
Member #16,498
July 2016
|
My game loop uses a timer to run at 60 FPS: When it comes to rendering, the following condition is checked beforehand: It's my understanding that checking that the event queue is empty before drawing is important so that rendering doesn't "fall behind". But this poses a problem when the update portion of the loop takes longer than 1/60th of a second to complete-- the event queue is never empty, because (I think) the update took long enough that another timer event was triggered. Thus, nothing is ever rendered. Could someone offer me some advice for dealing with this problem? If I render after every update without checking if the event queue is empty, would I be at risk for any major problems? Would it just decrease the "actual" framerate when under load, and then recover afterwards? Is there something important that I'm not considering? One potential issue I see is that the timer would generate more events than could be processed, since the update is slower than 60 FPS. Is this a valid concern? Removing the emptiness check seems to work fine (I end up with around 50 FPS) but I'm worried there might be something I'm not seeing that will come back to haunt me... Update: It seems to be as I feared; the timer events block other events from being processed! |
Edgar Reynaldo
Major Reynaldo
May 2007
|
It is best to empty the queue every time. You can drop rendering frames as you need to. But if you're getting behind on logic processing you have other problems. I have a wrapper around the Allegro event queue that lets me do things like take all of one type of event off of the queue. This allows me to take as many logic ticks off the queue as I want. I can then process them all or drop as many as needed. My Website! | EAGLE GUI Library Demos | My Deviant Art Gallery | Spiraloid Preview | A4 FontMaker | Skyline! (Missile Defense) Eagle and Allegro 5 binaries | Older Allegro 4 and 5 binaries | Allegro 5 compile guide |
xsquid
Member #16,498
July 2016
|
That's what I thought... If the logic portion takes longer to complete than it takes the timer to tick, how can that be helped? Short of decreasing the level of processing, is the best solution here just to clear the event queue of any extra ticks after each logic update? This problem came about as a side-effect of stress-testing my collision system. Rendering stops because the logic cannot be processed at 60 FPS (with thousands of bodies; the system itself is fine at a reasonable scale), so the event queue fills up with timer events as a result and is never empty to allow rendering to occur. Obviously, it's better to have the game start lagging than to have it stop rendering altogether. So, dropping the extra timer events should do it without any hidden consequences? Update: Hmmm, maybe not. I added this at the end of the logic update: 1ALLEGRO_EVENT timer_event;
2while (al_peek_next_event(__event_queue, &timer_event) && timer_event.type == ALLEGRO_EVENT_TIMER)
3 al_drop_next_event(__event_queue);
But now if any other type of event is triggered (e.g., ALLEGRO_EVENT_MOUSE_AXES) rendering stops until that event stops occurring. I'm not sure why that is; I'm still investigating. |
Edgar Reynaldo
Major Reynaldo
May 2007
|
That only works if the next event is a timer event. You want to drain the queue completely, count the number of logic ticks, and drop them as necessary. You might not want to ignore them completely, rather multiply them by some ratio less than one according to the strain on the system, and interpolate. What are you using for timing? Delta, or fixed step? My Website! | EAGLE GUI Library Demos | My Deviant Art Gallery | Spiraloid Preview | A4 FontMaker | Skyline! (Missile Defense) Eagle and Allegro 5 binaries | Older Allegro 4 and 5 binaries | Allegro 5 compile guide |
xsquid
Member #16,498
July 2016
|
That's true, but if I dump the whole thing, I lose everything that's behind the timer events and it stops responding to most input. I'm using a fixed time-step. Here's a simplification of what the loop looks like: 1__timer = al_create_timer(1.0f / 60.0f);
2al_start_timer(__timer);
3__event_queue = al_create_event_queue();
4al_register_event_source(__event_queue, al_get_timer_event_source(__timer));
5
6while (true) {
7
8 ALLEGRO_EVENT ev;
9 al_wait_for_event(__event_queue, &ev);
10
11 if (ev.type == ALLEGRO_EVENT_TIMER) {
12
13 // process game logic (takes longer than 1/60th of a second)
14 __redraw = true;
15
16 }
17 if (__redraw && al_is_event_queue_empty(__event_queue)) {
18
19 // render everything (never happens because there's always a new ALLEGRO_EVENT_TIMER event)
20
21 }
22
23}
I want to avoid interpolation and other fancy things for now-- My only goal at the moment is just to have the game continue rendering, even if it's stuttering. I get what you're saying-- Count the logic ticks, and if they're above a certain threshold, drop all remaining timer events so it can render, and also prevent the timer events from continually accumulating (which would probably cause an overflow eventually). Right? At this rate, it seems like I'm going to have to use a wrapper myself to have more control over the events. I was hoping there would be some way to do it using Allegro directly, but it doesn't look like there's an easy way to drop the remaining timer events. |
Mark Oates
Member #1,146
March 2001
|
Your fixed-step timer events are clogging up and causing a bottleneck in your queue. You can fix this by dropping any backed up events, but, you don't want to drop all events. Rather you should drop any sequential timer events that are coming from that source. The function you need is al_peek_next_event(), which will fetch the next event in the queue. If it's an ALLEGRO_EVENT_TIMER and it comes from the same timer source, then you can remove it with al_drop_next_event(). This is a common technique and is the one used in AllegroFlare. -- |
Audric
Member #907
January 2001
|
If even the logic part alone takes 100% CPU, there is no way you can additionally DRAW something. |
xsquid
Member #16,498
July 2016
|
Mark Oates said: If it's an ALLEGRO_EVENT_TIMER and it comes from the same timer source, then you can remove it with al_drop_next_event(). That's what I'm trying to do now (mentioned in this post), but since doing it that way will only clear sequential ALLEGRO_EVENT_TIMER events, it introduces another issue: If I do something that also generates frequent events, such as moving the mouse and introducing new ALLEGRO_EVENT_MOUSE_AXES events, the queue starts looking like this: ... ... and so on, so rendering remains blocked until I stop moving the mouse. Since the event following a timer event is always ALLEGRO_EVENT_MOUSE_AXES, the timer events can't be cleared properly. Your loop in Allegro Flare is practically identical to what I'm using, so how do you manage to avoid that, if at all? Update: Maybe I should cause it to ignore all following timer events after a logic update until it gets a chance to render the last frame? I'm going to try doing that when I get the chance later today. I was so focused on the idea of dropping them that I forgot I could just ignore them... Audric said:
If even the logic part alone takes 100% CPU, there is no way you can additionally DRAW something. It's supposed to be ridiculously expensive; like I said, this came about because I wanted to stress-test my collision system. These conditions would likely never exist in the finished product (i.e., thousands of objects colliding at once), but I want to make sure that it stutters while still being able to render, albeit at a slower rate. It's not using 100% of the CPU, it's just that since there's so much going on in the logic update that more timer events are generated than can be satisfied. Dropping them seems to be the right idea, but it only works if they're sequential and there's nothing in-between... |
Audric
Member #907
January 2001
|
IMO, it's not a good thing to modify the system to perform better in these cases which-never-happen, if it performs worse in cases-that-happen. For example, the typical frame-dropping system performs very good in case of tiny hiccup (ie. Antivirus hogs resources for 1/10th of a second), because it catches up the missed ticks : over 10 seconds, you keep exactly 600 logic steps. |
Polybios
Member #12,293
October 2010
|
In theory, you could also use two separate event queues for input and timers... |
Edgar Reynaldo
Major Reynaldo
May 2007
|
Using two separate event queues would mean that the order of events would change, and events would be de-synchronized with the game as it played. Earlier, when I said you should empty the queue, I was talking about a game loop something like this : 1list<ALLEGRO_EVENT> current_events;
2
3while (!quit) {
4
5 if (redraw) {Redraw();redraw = false;}
6
7 ALLEGRO_EVENT ev;
8
9 /// Wait for events as normal
10 al_wait_for_event(queue , &ev);
11 current_events.push_back(ev);
12
13 /// Add all other events in the queue to our list
14 while (al_get_next_event(queue , &ev)) {
15 current_events.push_back(ev);
16 }
17
18 /// Process all current events
19 while (!current_events.empty()) {
20 ev = current_events.front();
21 current_events.pop_front();
22 if (ev.type == ALLEGRO_EVENT_TIMER) {
23 Logic();
24 redraw = true;
25 /// Drop all extranneous timer events at the back of the queue
26 /// without dropping any input events
27 if (!current_events.empty()) {
28 while (current_events.back().type == ALLEGRO_EVENT_TIMER) {
29 current_events.pop_back();
30 }
31 }
32 }
33 else {
34 HandleInput(ev);
35 }
36 }
37
38};
This has the advantage of dropping all extra timer events that may have piled up without losing any input. And because you're processing every single event in the queue you can't get behind. It should slow down somewhat gracefully, without destroying your fixed step determinism. I'm curious to see how this loop would perform in your collision stress test. EDIT At some point, you're just going to have to slow down your timer. Ideally you want your logic to take up somewhere around your timer rate minus a few milliseconds to allow for drawing. EDIT2 My Website! | EAGLE GUI Library Demos | My Deviant Art Gallery | Spiraloid Preview | A4 FontMaker | Skyline! (Missile Defense) Eagle and Allegro 5 binaries | Older Allegro 4 and 5 binaries | Allegro 5 compile guide |
xsquid
Member #16,498
July 2016
|
Alright, I think I've solved it. The input I received from you guys was extremely helpful; it turns out I was waaaaaay overthinking it. In case it helps someone else, here's what I did: I added a constant to represent the maximum number of frames that rendering can skip (e.g., 10 frames). I also kept track of the number of frames skipped, the value of which is incremented after each logic update. If the maximum allowed number of frames is skipped, then all following ALLEGRO_EVENT_TIMER events are ignored. This allows the event queue to empty so things can be rendered while preserving all other events. The value is then reset back to 0 after rendering. This results in the slowdown/stuttering I was looking for without ceasing to render altogether. I can't believe it took all of this to realize I could just skip the superfluous events. For the record: This was important to me because while more intensive portions of the game may run fine for me, I can't guarantee that they'll be perfect on anyone else's PC. It's much better to have it slow down from 60 FPS to 40 FPS than to risk losing rendering, imo. Thanks for the quick help, everyone! I definitely appreciate it! |
Rodolfo Lam
Member #16,045
August 2015
|
If you are willing to, you could add a freestanding version of your new game loop to the allegro wiki's Timer tutorial, as a second example. That way you could help new users starting to use the API (and veterans as well) have a better way to implement a game loop with Allegro. Or you can post the code here and either me or someone else can add it then on the tutorial page. Last time it was updated was way back on 2013. Countless Allegro applications have been built using that code as a starting point.
|
beoran
Member #12,636
March 2011
|
As an aside to this particular problem, but if you are doing something that really takes more than a frame, say, complex AI, then you will need to program an interruptible algorithm that keeps it's state and intermediate results and only does a bit of work every frame. Or use a separate thread to run that long work. |
Edgar Reynaldo
Major Reynaldo
May 2007
|
xsquid said: For the record: This was important to me because while more intensive portions of the game may run fine for me, I can't guarantee that they'll be perfect on anyone else's PC. It's much better to have it slow down from 60 FPS to 40 FPS than to risk losing rendering, imo. If you drop up to 10 frames, your game could end up running at around 5-6FPS. Personally, I think you would be better off with either an adaptive timer rate, or simply dropping all timer events after the first. That way, your game would run as fast as possible, while still keeping your fixed step determinism. My Website! | EAGLE GUI Library Demos | My Deviant Art Gallery | Spiraloid Preview | A4 FontMaker | Skyline! (Missile Defense) Eagle and Allegro 5 binaries | Older Allegro 4 and 5 binaries | Allegro 5 compile guide |
xsquid
Member #16,498
July 2016
|
Edgar Reynaldo said: simply dropping all timer events after the first You're right, and that's what I ended up doing. Skipping 10 frames was far too much (and unnecessary for my purposes). Again, another case of overthinking it... Eventually I just made it so all extra timer events are ignored until rendering has occurred. The frame rate was much better and it did exactly what I wanted. I can see the value of an adaptive timer, and maybe I'll try seeing how that goes at some point, but I'm happy with the current results for the time being. rlam12 said: If you are willing to, you could add a freestanding version of your new game loop to the allegro wiki's Timer tutorial, as a second example. That way you could help new users starting to use the API (and veterans as well) have a better way to implement a game loop with Allegro. To have the same results as my current loop logic, only one line of the existing example would need to be altered: if(ev.type == ALLEGRO_EVENT_TIMER) { Would become if(ev.type == ALLEGRO_EVENT_TIMER && !redraw) { to make sure every frame is drawn and extra timer events are ignored. Because I prepared it anyway, here's another modified version of the example loop that allows you to set a max frame skip value: 1#include <allegro5/allegro.h>
2#include <allegro5/allegro_primitives.h>
3
4const float FPS = 60.0F;
5const int MAX_FRAME_SKIP = 3;
6
7int main(int argc, char **argv) {
8
9 ALLEGRO_DISPLAY *display = NULL;
10 ALLEGRO_EVENT_QUEUE *event_queue = NULL;
11 ALLEGRO_TIMER *timer = NULL;
12 bool redraw = true;
13 int frames_skipped = 0;
14
15 if (!al_init()) {
16 fprintf(stderr, "failed to initialize allegro!\n");
17 return -1;
18 }
19
20 timer = al_create_timer(1.0F / FPS);
21 if (!timer) {
22 fprintf(stderr, "failed to create timer!\n");
23 return -1;
24 }
25
26 display = al_create_display(640, 480);
27 if (!display) {
28 fprintf(stderr, "failed to create display!\n");
29 al_destroy_timer(timer);
30 return -1;
31 }
32
33 event_queue = al_create_event_queue();
34 if (!event_queue) {
35 fprintf(stderr, "failed to create event_queue!\n");
36 al_destroy_display(display);
37 al_destroy_timer(timer);
38 return -1;
39 }
40
41 al_register_event_source(event_queue, al_get_display_event_source(display));
42
43 al_register_event_source(event_queue, al_get_timer_event_source(timer));
44
45 al_clear_to_color(al_map_rgb(0, 0, 0));
46
47 al_flip_display();
48
49 al_start_timer(timer);
50
51 while (1)
52 {
53 ALLEGRO_EVENT ev;
54 al_wait_for_event(event_queue, &ev);
55
56 if (ev.type == ALLEGRO_EVENT_TIMER && frames_skipped++ <= MAX_FRAME_SKIP) {
57 redraw = true;
58 }
59 else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
60 break;
61 }
62
63 if (redraw && al_is_event_queue_empty(event_queue)) {
64 redraw = false;
65 frames_skipped = 0;
66 al_clear_to_color(al_map_rgb(0, 0, 0));
67 al_flip_display();
68 }
69 }
70
71 al_destroy_timer(timer);
72 al_destroy_display(display);
73 al_destroy_event_queue(event_queue);
74
75 return 0;
76}
|
-koro-
Member #16,207
February 2016
|
How about not using a timer at all, and just redrawing whenever a redraw is needed? 1do{
2 while(!al_is_event_queue_empty()){
3 // process events
4 }
5
6 new_time = al_get_time();
7 if(old_time - new_time > 1.0/FPS)
8 {
9 redraw = 1;
10 old_time = new_time;
11 }
12
13 if(redraw)
14 draw_stuff();
15}
|
Edgar Reynaldo
Major Reynaldo
May 2007
|
If you do that, it will have the effect of using 100% CPU. Even on computers that can actually handle the work load. It will just sit and spin until dt is greater than the desired SPF. My Website! | EAGLE GUI Library Demos | My Deviant Art Gallery | Spiraloid Preview | A4 FontMaker | Skyline! (Missile Defense) Eagle and Allegro 5 binaries | Older Allegro 4 and 5 binaries | Allegro 5 compile guide |
-koro-
Member #16,207
February 2016
|
Right, right, I forgot... but you fix that adding an al_rest() somewhere. You can check the difference between old_time - new_time and 1.0/FPS to get an idea how much to rest, or something like that.
|
Polybios
Member #12,293
October 2010
|
-koro- said: .. but you fix that adding an al_rest() somewhere
Allegro 5.2 manual said: With some operating systems, the accuracy can be in the order of 10ms. That is, even al_rest(0.000001) might pause for something like 10ms. So, no, you shouldn't. Using 100% CPU all the time is not acceptable, especially not on mobile platforms. |
Edgar Reynaldo
Major Reynaldo
May 2007
|
If you look at the timer thread process for allegro's timers, it uses al_rest internally to wait for timers to tick. So the timers are just as accurate as the granularity of al_rest. And for example, on Windows, al_rest uses Sleep internally : 120/* al_rest:
121 * Rests the specified amount of milliseconds.
122 * Does nothing with values <= 0.
123 */
124void al_rest(double seconds)
125{
126 if (seconds <= 0)
127 return;
128
129 Sleep((DWORD)(seconds * 1000.0));
130}
https://msdn.microsoft.com/en-us/library/windows/desktop/ms686298%28v=vs.85%29.aspx So there's really not much point (on Windows at least) to use anything other than al_rest, as that is what is going to be used anyway. At least that's my understanding of what the code is doing. EDIT EDIT2 My Website! | EAGLE GUI Library Demos | My Deviant Art Gallery | Spiraloid Preview | A4 FontMaker | Skyline! (Missile Defense) Eagle and Allegro 5 binaries | Older Allegro 4 and 5 binaries | Allegro 5 compile guide |
-koro-
Member #16,207
February 2016
|
Right, the granularity of al_rest() (or sleep() or whatever you use) may or may not be a problem, but the point is you can sleep (using whatever method you want) for the required time. To me using an allegro timer seem to be a solution to a non-issue, and then there's the potential trouble they may cause (clogging, as you said).
|
Edgar Reynaldo
Major Reynaldo
May 2007
|
-koro- said: To me using an allegro timer seem to be a solution to a non-issue, and then there's the potential trouble they may cause (clogging, as you said). There's no trouble if you simply process all the events in the queue at once. See my earlier post for a solution. My Website! | EAGLE GUI Library Demos | My Deviant Art Gallery | Spiraloid Preview | A4 FontMaker | Skyline! (Missile Defense) Eagle and Allegro 5 binaries | Older Allegro 4 and 5 binaries | Allegro 5 compile guide |
Polybios
Member #12,293
October 2010
|
Edgar Reynaldo said: So the timers are just as accurate as the granularity of al_rest. Ah, didn't know that. Could have checked the code myself. -koro- said: but the point is you can sleep (using whatever method you want) for the required time Well the advantage of events is that you will wake up on input events while sleeping. Mouse-moved events tend to be quite numerous, too, for example. Maybe you could use al_wait_for_event_timed() for that, though. |
Edgar Reynaldo
Major Reynaldo
May 2007
|
Another advantage of timers is being able to have multiple timers active. Trying to simulate that with al_rest would be a nightmare to do yourself, always trying to figure out which "timer" would tick next. Say you had different animation rates or different logic and drawing rates. These would be easier with a timer than with al_rest. My Website! | EAGLE GUI Library Demos | My Deviant Art Gallery | Spiraloid Preview | A4 FontMaker | Skyline! (Missile Defense) Eagle and Allegro 5 binaries | Older Allegro 4 and 5 binaries | Allegro 5 compile guide |
|