Summary: When working with _update60(), a jump in cpu load which causes it to exceed 1.0 will SOMETIMES cause the cpu load to get stuck above 1, dropping the fps to 30. To test above, tap "Z" to toggle extra computations. The CPU load should be below 1 when overload is off, and above when overload is on. cpu load tends to get "stuck" above 1.
I've come across what I think is a strange bug. I created the cart above to test it. If you are interested, I suggest looking at the code - it's less than 50 lines.
I am performing arbitrary calculations in the update step, and just drawing random pixels in the draw step. Normally, the CPU load should be below 1. By pressing Z, I add extra calculations to the update step, causing the cpu load to exceed 1 and dropping the fps to 30. If you try mashing Z, sometimes the CPU load gets "stuck" above 1, even when there is no overload going on.
Does anyone have any ideas what might be happening?
What's most likely happening is that when you go over 1/60th, pico starts calling _update60() twice before each draw() to simulate 30fps. When you disable the extra work, the remaining work will still get run twice in the first following frame, because the mode was determined from the previous frame and will not be downgraded until pico sees that you can run 60fps again. Running the update twice, even with the reduced workload, is still enough to take over 1/60th, so pico thinks there's still a reason to stay at 30fps.
It's a common gotcha in variable-update-rate games. It's happened in several I've worked on.
What's depressing is that if you were simply allowed to run a single update at least once, pico would see that you could do a frame in under 1/60th. But that doesn't happen.
This probably depends on when exactly you turn off the extra work, thus the inconsistency. Sometimes it may be able to do one _update60() instead of two.
I think you hit the nail on the head. I suspected something like this might have been happening, but the exact mechanism was unclear to me.
What was puzzling is that, in the game I am currently working on, occasionally it would START in this 30fps situation, which is a problem. What was weird is that this new game tends to have similar CPU loads to another game I worked on, and I had never experienced this problem. However, after your explanation, I traced it to the problem - on the FIRST FRAME of my new game, I have an extremely high load (running some tile cleanup and instantiating my actors, in addition to the main loop). This causes my new game to get stuck right off the bat. I plan to have a few frames of low-load and just write "GAME START" or something like that to make sure the new game starts at 60.
I uploaded a new version of my tester below. In this one, you can also press "X" to toggle the base update load. I have seen that, if you are stuck at 30fps, if you can drop the CPU load enough, you can guaranteed "jolt" pico8 back to 60 fps. This appears to work 100%.
I might leave the name of this thread as "BUG," since it kind of is a bug. Thanks Felice! I was pulling my hair out looking for leaking tables in my new game before you swooped in.
here's the main loop (pulled from pico8.exe):
if (_init ~= nil) then _init() end _set_mainloop_exists(0) if (_mainloop ~= nil) then _set_mainloop_exists(1) end if (_update60 ~= nil) then _set_fps(60) end if (_mainloop == nil and (_draw ~= nil or _update ~= nil or _update60 ~= nil)) then _mainloop = function() _set_mainloop_exists(2) while (true) do local num=min(1, _get_frames_skipped()) while (num >= 0) do _update_buttons() if (_update60 ~= nil) then _update60() elseif (_update ~= nil) then _update() end num = num - 1 end holdframe() if (_draw ~= nil) then _draw() end flip() for i=1,5 do if (_get_menu_item_selected(i)) then _pausemenu[i].callback() end end end end end if (_mainloop ~= nil) then _mainloop() end
one nice thing is, you can override it!
a barebone version would be this:
_mainloop = function() _set_mainloop_exists(2) while (true) do for i=0,_get_frames_skipped() do _update_buttons() _update60() end holdframe() _draw() flip() end end
here's your cart, with my alternate _mainloop plus a little graph to show what _get_frame_skipped() returns
so, on overload, _get_frame_skipped() alternates between 1 and 0. 0 is where there's a chance that things get back to normal. but when they do, it seems _get_frame_skipped() gets stuck on its last return value, either 0 or 1, instead of returning 0.
looks like a bug, indeed. (maybe move this thread to "support", could be more easily noticed by zep?)
Awesome! Thanks for the insight, and the super-cool plot. At your suggestion I moved this to support.
As for now, in the post I submitted just before yours, I found an (ugly) workaround - by dropping the CPU enough during the 'stuck' frames, I can force it back to 60fps. In my application, the first few frames of my game were causing this bug to come around pretty frequently, so I am just adding a low-load "GAME START" state to make sure the game gets started at low load.
I am reading your explanation more closely, and it is strange that the function _get_frame_skipped() gets stuck. That piece definitely looks like a bug. as Felice mentioned, I guess this sometimes happens in video games overall, but I am kind of shocked that this is the first discussion of it in pico-8. I have to go to work now :( but I hope to revisit this when I get out. Thanks for the insight - this was really interesting.
Ideally, while 30fps, _mainloop() would time the two _update60 calls, take the max of the two, time the _draw call and add it to that, and see if the total is well under 1/60th. If so, switch back to 60fps.
Alas, I'm not sure time() has enough granularity to do that. I seem to recall that it's only updated either once or twice per 60th. I suspect it's the same clock used for music, which updates twice per 60th, but I'm not 100% sure of that.
Edit: Might be able to use stat(1), come to think of it. I think it updates live and with much finer granularity.
@Felice -- you're right, and this is completely a bug in 0.1.10c. I've fixed it for 0.1.11 using the method you described, but leaving a small margin under 1.0 it still needs to cross to return to full speed. (otherwise it feels quite jerky when the cpu load is fluctuating just above and below 1.0).
Woo, good to hear. And yeah, a buffer zone sounds like a good idea.
There's a term for that, which I'm forgetting ...uhh... google google google ...ah right,
hysteresis. Great word, shame I can never remember it.
For any budding game devs reading this... you should know about that word. It applies to a lot of stuff in game development. For instance, when we convert an analog input, like a trigger, into a digital one (on/off), we debounce it. Debouncing is done by putting the activation point below the deactivation point, so the result can't bounce back and forth if they're both in the same place and someone holds the trigger just about there. Debouncing creates hysteresis. Applies to tons and tons of things. Learn about it! :)
[Please log in to post a comment]