Allegro.cc - Online Community

Allegro.cc Forums » Programming Questions » FPS locking is easy - not taking 100% CPU is hard

This thread is locked; no one can reply to it. rss feed Print
 1   2 
FPS locking is easy - not taking 100% CPU is hard
Ninkazu
Member #7,861
October 2006

...or at least foreign to Allegro coding standards. Everywhere I look whether it's this forum or the first few hundred hits on Google, all the sample code for frame locking does a poor job of not using 100% CPU.

I think we all know the FAQ's blurb on FPS locking. [url=http://alleg.sourceforge.net/faq.html#How_can_I_make_my_game_run_at_the_same_speed_on_any_computer_]Not a word is meantioned about CPU usage[/url].

The question is simple - how do I make my simple tile-drawing program not take 100% CPU? rest(0) does nothing. rest(10) or any other static number makes the program run choppily and it still takes close to 70% CPU.

It's annoying to see all these FPS threads, I know. I've waded through quite a few, but once more to be absolutely sure on the matter, ey?

Matthew Leverton
Supreme Loser
January 1999
avatar

You could rest(1) or Sleep(1) (under windows) whenever you are ahead:

1while (!finished)
2{
3 while (ticks)
4 {
5 /* ... */
6 drawgfx = TRUE;
7 --ticks;
8 }
9 
10 if (drawgfx)
11 {
12 /* ... */
13 drawgfx = FALSE;
14 }
15 else
16 rest(1); // Sleep(1);
17}

Or you could look at variable timing, where you let it run as fast as possible and compensate by using delta values.

Kris Asick
Member #1,424
July 2001

PixelShips Retro uses far less than 100% CPU resources on every computer I've tested it on. Actually, I've had one particular system measure it as low as a measly 2%, which couldn't possibly be right but hey, if that's what it wanted to believe.

I switched to using Sleep(1) on Windows over Allegro's rest(1) because it seems to agree with the OS better.

If rest(10) is still making your game use 70% CPU, then the problem is two-fold. One, you need to give some CPU time back to the computer. Two, you're program is already using tons of CPU power just to do its thing.

Let me guess: You're running at 800x600 resolution, 24/32 bpp, and are doing a full-redraw of the screen every frame.

Allegro isn't accelerated beyond some very simple optimizations, so if you're only using Allegro to render everything you need to set some boundaries to achieve a more balanced game. Such as 640x480, 16 bpp, etc.

.EDIT: There is one other thing I nearly forgot to mention. If you are programming on Windows all video memory must be locked before you can draw to it. By default, the only video surface in Allegro is the screen, and it will automatically lock and unlock for you... but that automatic process will wreak havoc if you rely on it for more than one write at a time. Either buffer all drawing to a memory bitmap and then draw that single bitmap to the screen, or call acquire_screen() before rendering everything and release_screen() when done rendering.

--- Kris Asick (Gemini)
--- http://www.pixelships.com

--- Kris Asick (Gemini)
--- http://www.pixelships.com

Matthew Dalrymple
Member #7,922
October 2006
avatar

Kris and Matthew, the Sleep function works wonderfully on Windows :-D. I'm going to use that for now until I need it to be changed, as I would like some kind of cross-platform capability. Thanks for the info :-D. It dropped mine down ~70-80%.

=-----===-----===-----=
I like signatures that only the signer would understand. Inside jokes are always the best, because they exclude everyone else.

Evert
Member #794
November 2000
avatar

Quote:

...or at least foreign to Allegro coding standards.

Not at all! Check the source for the test program or the grabber.

Quote:

all the sample code for frame locking does a poor job of not using 100% CPU.

That's because it's written to show how to run the same speed on any system, not to show how not to use 100% of teh CPU power. The best way to do that will depend on the game anyway.

Quote:

rest(0) does nothing.

Correction: rest(0) gives up the rest of the timeslice to other processes that need time, allowing them to run. If no other process is running, it will return immediately. In other words, it will never reduce CPU usage.

Quote:

rest(10)

Try rest(1); the result you see will also depend on where you call this. I typically call rest() if the logic does not need updating and the screen has already been drawn (ie, in situations where the game would busy-wait otherwise).

Quote:

I switched to using Sleep(1) on Windows over Allegro's rest(1) because it seems to agree with the OS better.

Er... rest() calls Sleep() in Windows:

1/* tim_win32_rest:
2 * Rests the specified amount of milliseconds.
3 */
4static void tim_win32_rest(unsigned int time, AL_METHOD(void, callback, (void)))
5{
6 unsigned int start;
7 unsigned int ms = time;
8 
9 if (callback) {
10 start = timeGetTime();
11 while (timeGetTime() - start < ms)
12 (*callback)();
13 }
14 else {
15 Sleep(ms);
16 }
17}

so I have no idea why you think Sleep() works better than rest().

Goalie Ca
Member #2,579
July 2002
avatar

Ideally what you want to have is the ability to change the state of the process to blocking (waiting in some queue... can't be executed until an event occurs). Then when the timer goes "pop" you want the process to wake up (timer elapsing causes the os to send a signal SIGALRM to wake up the blocked process). Allegro timers use a different thread so your main process will not block.

Quote:

Under other platforms, they are usually implemented using threads, which run parallel to the main thread. Therefore timer callbacks on such platforms will not block the main thread when called, so you may need to use appropriate synchronisation devices (eg. mutexes, semaphores, etc.) when accessing data that is shared by a callback and the main thread.

You should be able to get around this by using the timer thread (probably have to be in the nasty callback) to wake up your main thread. Or you can use a different timer system to achieve ideal results.

A simple way to block your process is to use vsync().

-------------
Bah weep granah weep nini bong!

psycho
Member #5,042
September 2004

Well, you could also take the time one message loop takes and name it dTime
if you want to e.g. get a framerate of 75 Hz, one frame is shown 1/75 seconds, so you

 rest((1/75 - dTime) * 1000)

milliseconds.

The only problem in this solution is that it can't handle situations with dTime > 1/75, but I think this should be easily to solve.

Thomas Harte
Member #33
April 2000
avatar

In my code I usually do essentially what you're saying albeit with some modular stuff to ensure that I handle integer underflow correctly. Actually nowadays I mostly write my games to run with the vsync and employ a separate fixed rate logic. Assuming vsync is blocking (and at least in OpenGL, it seems to be on all three major platforms), that fixes the CPU time problem.

However in my emulators that doesn't make sense since you're starting from the position of "I must produce exactly 50 frame per second" and so dTime type stuff applies.

Quote:

The only problem in this solution is that it can't handle situations with dTime > 1/75, but I think this should be easily to solve.

I do this: if dTime is too large (i.e. you took too long to draw this frame) then do the logic and keep time counting but don't draw the next one. So on machines that aren't quite fast enough you get frame skipping and if you can do 2 frames of logic, one drawn, one not, in the time you have to produce 1.5 frames of video then you can still sleep for 0.5 frames and give the other OS systems time to run.

The only problem comes when you end up perpetually behind, e.g. on systems that aren't fast enough or are just fast enough to run your program most of the time but some abnormal event (e.g. file access) has but you behind a small portion and you can't quite seem to catch up. I get around this quite easily. No more than 8 (or some other number I plucked out of thin air) frames are skipped. If the program finds itself trying to skip an 8th frame in a row then it just draws the frame and resets its timing.

Of course all dependent "real time" systems have to be able to handle the occasional time inconsistency too, but I don't suppose the average Allegro game has any of those implemented in their own code. I guess AUDIOSTREAMs that rely on the game processing reaching a certain stage before they know what sound to generate are the only example I can think of.

Ninkazu
Member #7,861
October 2006

I tried most the methods in here and none really worked. Granted I did lose CPU usage when I dropped the window size from 640x480 to 256x240, but that's just annoyingly small. You can't be serious to tell me that Allegro is THAT slow. I'm just doing a few blits each frame.

Leverton: Sorry mate, your method just didn't work.

Asick: rest(1) didn't help, and I already am doing acquire_bitmap/release_bitmap [page flipping]

Dalrymple: Didn't know quite how I should have done yours. I set up a callback with BPS_TO_TIMER(TARGET_FPS) and then go into a while(!done) { vsync(); }? The callback handling the game of course.. and it just crashed.

psycho/Harte: Your method seems to have worked the best, however it still took 50% CPU even with 256x240 resolution. Closer, but still not good enough.

If it turns out that Allegro is just slower than my a bowel movement, I'm going to have to rethink how I teach this blasted seminar. OpenGL for 2D just seems like too much complication than these students need. I just can't believe that it's that slow, Asick.

Jonatan Hedborg
Member #4,886
July 2004
avatar

post some code; perhaps you are doing something crazy to cause the bad performance.
EDIT: Like having a different color depth than your desktop in windowed mode for example.

Ninkazu
Member #7,861
October 2006

1#include <allegro.h>
2#include <stdio.h>
3#include "rect.h"
4 
5/* *** DEFINES *** */
6 
7#define TARGET_FPS 60
8 
9//size of the tiles
10#define TILE_W 16
11#define TILE_H 16
12 
13//max tiles that can fit in the viewport
14#define TILE_MX (viewport.Width() / TILE_W)
15#define TILE_MY (viewport.Height() / TILE_H)
16 
17//map tile and pixel dimensions
18#define MAP_X 100
19#define MAP_Y 100
20#define MAP_PX (MAP_X * TILE_W)
21#define MAP_PY (MAP_Y * TILE_H)
22 
23/* *** GLOBALS *** */
24 
25BITMAP *buffer;
26BITMAP *pages[2];
27CRect viewport;
28CRect player(1, 1, TILE_W, TILE_H);
29int close_button_pressed = FALSE;
30int page = 0;
31volatile int t = 0;
32 
33/* *** FUNCTION PROTOTYPES *** */
34 
35void init();
36void deinit();
37void input();
38void tick();
39void close_button_handler(void);
40 
41void input() {
42 //make sure we're looking at the keys pressed currently
43 poll_keyboard();
44 
45 if(key[KEY_UP]) {
46 player.SetTop(player.top - 1);
47 }
48 if(key[KEY_DOWN]) {
49 player.SetTop(player.top + 1);
50 }
51 if(key[KEY_LEFT]) {
52 player.SetLeft(player.left - 1);
53 }
54 if(key[KEY_RIGHT]) {
55 player.SetLeft(player.left + 1);
56 }
57}
58 
59int main() {
60 init();
61
62 PALETTE pal;
63 BITMAP *tiles = load_bitmap("tiles.bmp", pal);
64 bool draw = false;
65 int begintime, endtime;
66 
67 //create a 2D map full of 4 tiles randomly
68 int map[100][100], x, y, tick;
69 for(y = 0; y < 100; y++) {
70 for(x = 0; x < 100; x++) {
71 map[y][x] = rand() % 4; //random number 0-3
72 }
73 }
74 
75 //enter the main loop
76 while (!key[KEY_ESC] && !close_button_pressed) {
77 begintime = t;
78 //catch keypresses to move around our map
79 input();
80 
81 if(draw) {
82 buffer = pages[page];
83 acquire_bitmap(buffer); //used with release_screen() to prevent resource loss
84 
85 //center the view around the moving player
86 viewport.SetCenter(player.GetCenter());
87 
88 //Check viewport bounds
89 if(viewport.left < 0) { viewport.SetLeft(0); }
90 if(viewport.top < 0) { viewport.SetTop(0); }
91 if(viewport.right >= MAP_PX) { viewport.SetRight(MAP_PX-1); }
92 if(viewport.bottom >= MAP_PY) { viewport.SetBottom(MAP_PY-1); }
93 
94 //Calculate tile position and the number of pixels to shift
95 CPoint camera(viewport.left / TILE_W, viewport.top / TILE_H),
96 shift(viewport.left % TILE_W, viewport.top % TILE_H);
97 
98 //draw our beautiful map
99 for(y = 0; y <= TILE_MY; y++) {
100 for(x = 0; x <= TILE_MX; x++) {
101 blit(tiles, buffer, map[camera.y + y][camera.x + x] * TILE_W, 0, (x * TILE_W) - shift.x, (y * TILE_H) - shift.y, TILE_W, TILE_H);
102 }
103 }
104 //draw our player
105 rectfill(buffer, player.left - viewport.left, player.top - viewport.top, player.right - viewport.left, player.bottom - viewport.top, makecol(200,200,200));
106 
107 release_bitmap(buffer);
108 show_video_bitmap(buffer);
109 page = 1 - page;
110 }
111 endtime = t - begintime;
112 if( endtime >= (1000 / TARGET_FPS) ) { draw = false; }
113 else {
114 draw = true;
115 rest( (1000 / TARGET_FPS) - endtime);
116 }
117 }
118 
119 destroy_bitmap(tiles);
120 
121 deinit();
122 return 0;
123}
124END_OF_MAIN()
125 
126void tick()
127{
128 t++;
129}
130END_OF_FUNCTION(tick)
131 
132void close_button_handler(void)
133{
134 close_button_pressed = TRUE;
135}
136END_OF_FUNCTION(close_button_handler)
137 
138void init() {
139 int depth, res;
140 allegro_init();
141 depth = desktop_color_depth();
142 if (depth == 0) depth = 32;
143 set_color_depth(depth);
144 res = set_gfx_mode(GFX_AUTODETECT_WINDOWED, 256, 240, 0, 0);
145 if (res != 0) {
146 allegro_message(allegro_error);
147 exit(-1);
148 }
149 
150 install_timer();
151 install_keyboard();
152 
153 set_window_title("Making Mario - Lesson 2");
154 
155 //create the back buffer
156 pages[0] = create_video_bitmap(SCREEN_W, SCREEN_H);
157 pages[1] = create_video_bitmap(SCREEN_W, SCREEN_H);
158 
159 //set viewport to view the entire screen
160 viewport.SetDimensions(SCREEN_W, SCREEN_H);
161 
162 //make sure the buffer stays within the viewport (VIEWPORT CAN'T BE BIGGER THAN SCREEN)
163 set_clip_rect(pages[0], 0, 0, viewport.right, viewport.bottom);
164 set_clip_rect(pages[1], 0, 0, viewport.right, viewport.bottom);
165 
166 //put the player in the center of the screen
167 player.SetCenter(viewport.GetCenter());
168 
169 set_close_button_callback(close_button_handler);
170
171 //Do timer initializations
172 LOCK_VARIABLE(t);
173 LOCK_FUNCTION(increment_t);
174 //allow the user to close the app
175 LOCK_FUNCTION(close_button_handler);
176 install_int_ex(tick, 1);//BPS_TO_TIMER(60)); //lock FPS to 60
177 
178 srand(time(NULL)); //randomize to timer
179}
180 
181void deinit() {
182 //release the memory used for the back buffer
183 destroy_bitmap(pages[0]);
184 destroy_bitmap(pages[1]);
185 clear_keybuf();
186}

Change the resolution back to 640x480 to see what I mean.

LennyLen
Member #5,313
December 2004
avatar

Quote:

Change the resolution back to 640x480 to see what I mean.

Your code can't be compiled without rect.h.

Jonatan Hedborg
Member #4,886
July 2004
avatar

We need the rest of your source if you want us to test it :)

But at a glance, that aquire_bitmap looks wrong. If you need it, put it just before you start your blit's, and release it just after.

Thomas Harte
Member #33
April 2000
avatar

The problem is likely to be install_int_ex. Allegro's timers are very bad at anything particularly high resolution (including the every ms tick you want), and will clump together calls to your callback. In practice you often get a better result from using a much lower resolution timer and being more coarse about your rest breaks. Being careful to allow spare time to accumulate before you blow it all on a rest, of course.

In my "Nuclear War!" for last year's Christmas hack I used a 200 BPS timer to compensate for a 50 FPS display and tend to see 20-30% CPU usage on machines I've tested. Another alternative, if you don't mind being Windows-only, is to use the Win32 function GetTickCount(), which just returns how many ms it has been since your program started and should work much better than an Allegro timer. SDL provides SDL_GetTicks() to do the same if you want to look into doing the same on non-Windows platforms.

Evert
Member #794
November 2000
avatar

Post tiles.bmp while you're at it.

Thomas Harte
Member #33
April 2000
avatar

Oh, also note that 1000/60 (i.e. ms / your wanted FPS) isn't an integer so as you have it, you'll actually run at 62.5 fps assuming integer divide underflows rather than rounds (which I have to admit I have no idea about).

Ninkazu
Member #7,861
October 2006

http://ninkazu.the-infidel.net/lesson2.zip

Sorry about the rect.h thing. Bleh.

::EDIT::
And I want cross-platform compatability, so no GetTickCount() for me. I'd like to hold off on SDL or any other lib if possible too.

Evert
Member #794
November 2000
avatar

Quote:

And I want cross-platform compatability, so no GetTickCount() for me.

You need to check ALLEGRO_VRAM_SINGLE_SURFACE in that case, and if it is defined, allocate a virtual screen large enough to accomodate multiple pages.

With that change, I tested your program, in 640x480. I never saw more than 5% CPU usage. Tested on an AMD64-3200 running Linux with the plain X11 driver (ie, no hardware acceleration whatsoever).

Ninkazu
Member #7,861
October 2006

Well I'm not sure how the single page functionality is supposed to work. How are you supposed to draw to it and how are you supposed to blit the backbuffer? Is there a standard or what's the deal?

Strange that it got 5%... I don't have nearly as good a processor, but Centrino 2ghz should be able to draw that much with no problems. Geh, I really don't want to have to do this in OpenGL. That would require me to learn that crap before I have to learn it in my graphics class next semester :/

Should I even be worrying about CPU? It's nice to have something you can play wirelessly in class without having just 30 minutes of battery life, but that's really not what these lessons are about.

I suppose I haven't stated my goals - I'm creating a series of lessons and exercises to informally teach students at my university how to make a simple game like Super Mario Bros. So looking at this "lesson 2" I'm still wondering where I should go for lesson 3. I'm thinking about introducing collision detection, jumping physics and organizing the code into a more structured MVC pattern. I just hope that's not too much for one lesson. Maybe I'll take out the jumping physics and just add a simple frame-based animation structure.

What do you guys think? I'm open to suggestion (just don't bicker about source control and programming language like they did on the last forum I asked this).

::EDIT::
Just to see how this current timing is fairing, I boosted the resolution to 640x480 again and only saw a jump of a couple percentage points of CPU. There has to be a way to get it down. The drawing should not be an issue, just when and how long to rest..

::EDIT 2::
I tried with the tick at every 5ms instead of 1ms and I'm getting pretty stable framerates at 60 fps, which is about the same as the 1ms. Still getting the same CPU usage though. Here's the updated relevant code

    endtime = t - begintime;
    if( endtime >= (200 / TARGET_FPS) ) { draw = false; }
    else {
      draw = true;
      rest( ((200 / TARGET_FPS) - endtime) * 5);
    }

begintime = t before the logic and rendering.
I realized my timer wasn't called with the right value (5 instead of MSEC_TO_TIMER(5)) so that's fixed too.

Kris Asick
Member #1,424
July 2001

Evert: I know about rest() calling Sleep(), but that doesn't negate that there was a marked performance increase when I switched from rest() and Allegro timers to directly calling Sleep() and using the Windows high-performance timers. (Which Allegro should theoretically also be using after checking the same file that routine was in.) I can't explain it but there must be a reason why because the difference is there.

Ninkazu: First of all, when you do page flipping you should not be doing any game logic while you've got a video page locked. The only thing you should be doing between calls to the acquire and release commands are drawing commands. The longer you lock a page of video memory for the longer everything running on your computer has to wait for that process to finish before memory handling can go back to normal.

The second thing is that if your desktop is set to 32-bit colour your code is defaulting to it as well. 32-bit colour is twice slower to draw in memory than 16-bit, and has to be done entirely by the CPU. The speed of your computer will make a huge difference. When it comes down to the graphics card however, there are a number of factors that come into play for how quickly the CPU can transmit data to video bitmaps. You may have a particular combination that doesn't like 32-bit colour so much, further slowing down the draw operation.

Force your program into 8-bit or 16-bit, depending on how you want to go about it, then see if that helps. There are almost no reasons to need 32-bit over 16-bit in a non-accelerated program.

Lastly, your calculations for your rest periods are completely out of whack. To understand this and modify your code properly, consider that 1000 / 60 = 16.666666667, while endtime will almost never be anything but 0 or 1 unless your framerate drops below 60. Thus all of your calculations using endtime must be completely off. Scratch that, the timing code is ok.

But your calls to rest() should be simple. One thing you could do is reset your timer to go at a rate of 60 ticks a second, then time your game logic to the timer. Then, call rest(1) in a loop until another timer tick has passed, then process it. It's one way to go about doing it.

Two other last things. Try updating your graphics drivers and switching to full-screen instead of Windowed. It could be that your graphics card does not handle Windowed drawing very well.

--- Kris Asick (Gemini)
--- http://www.pixelships.com

--- Kris Asick (Gemini)
--- http://www.pixelships.com

Ninkazu
Member #7,861
October 2006

Fullscreen 640x480 16-bit still runs at around 45-55%.
I think the code I uploaded didn't have the acquire moved, but the code I've been testing on does, so that's taken care of too.

Can you post an example of the logic and rest loop? It sounds like the first bit of code posted in this thread, which I tried at no help (100% CPU)

Kris Asick
Member #1,424
July 2001

You still haven't mentioned what kind of CPU and graphics card you have. 55% might be as good as you can hope for depending on the complexity of what you're doing.

Here is some mock-up code to demonstrate a fixed-time game loop along with the ability to drop frames if the framerate dies.

1volatile int tick;
2 
3void tick_timer (void)
4{
5 tick++;
6}
7END_OF_FUNCTION(tick_timer);
8 
9void main (void)
10{
11 int quit_loop = 0;
12 int draw_frame = 0;
13 
14 // Initialize Everything
15 
16 LOCK_VARIABLE(tick);
17 LOCK_FUNCTION(tick_timer);
18 
19 install_timer(tick_timer,BPS_TO_TIMER(60));
20 
21 // Game Loop Follows:
22 
23 do {
24 while (tick > 0)
25 {
26 // Game Logic goes here
27 
28 draw_frame = 1;
29 tick--;
30 }
31 if (draw_frame)
32 {
33 // Lock video memory (if page flipping) and
34 // draw the screen.
35 
36 draw_frame = 0;
37 rest(0); // Give at least something back to Windows.
38 }
39 else
40 rest(1); // Or Sleep(1) if you're in Windows
41 } while (!quit_loop);
42 
43 // Uninit Everything
44}
45END_OF_MAIN()

That's so ugly to me... I'm too used to real-time anymore. In either case, that's the way I would aim for what you're trying to do.

One other thing to consider is that 60 is a tough framerate to maintain depending on how complex your game is. You might want to try aiming for 30 instead. (In fact, if you implement frame dropping as I have above, you may see things go at 30 instead of 60!)

--- Kris Asick (Gemini)
--- http://www.pixelships.com

--- Kris Asick (Gemini)
--- http://www.pixelships.com

Ninkazu
Member #7,861
October 2006

I must have been doing something wrong the first time I tried that method. I tried it this time with a 30 fps target at fullscreen 640x480x16 and hovered around 20%.

Cool. Thanks for the help.
[ATI Radeon X300 256mb... it's sad, my laptop is my gaming computer... I need to upgrade this summer when I'm working at NVIDIA]

Matthew Dalrymple
Member #7,922
October 2006
avatar

Evert said:

Er... rest() calls Sleep() in Windows:

Well then why when I ctrl alt delete does my game crash when I call rest, but when I call sleep it works flawlessly?

=-----===-----===-----=
I like signatures that only the signer would understand. Inside jokes are always the best, because they exclude everyone else.

Audric
Member #907
January 2001

Quote:

Well then why when I ctrl alt delete does my game crash when I call rest, but when I call sleep it works flawlessly?

That's how you recognize you have made a big memory overwrite: when making irrelevent changes DOES change the result.

 1   2 


Go to: