Real simple and stable variable-rate refresh loop

Life has slowed my progress way down, but I'm still noodling around with Chipmunk2D physics, trying to gracefully expose the library to Lua and see how much perf I can squeeze out of it on device. It's going pretty well, even if the device's CPU limitations definitely come into play. The thing I wanted to share early, though, was this nugget that was linked in one of the Chipmunk SDK samples:

Gaffer on Games: Fix Your Timestep!

So as @matt has demonstrated, unlocking the Playdate's refresh rate with setRefreshRate(0) adds a little homework but unlocks some tremendous super powers in terms of smoothness and responsiveness. The Playdate's display is an absolute champion of variable refresh rate. So simple, just unlock your frame rate, find out how much time elapsed since the last frame, advance the engine however much time passed, and render the next frame! If you're going too fast, you can always set a frame rate limit and skip the update loop if not enough time has passed (which is what I think is all pd.display.setRefreshRate() is doing).

Trouble is, your game might not be built to handle arbitrary spans of elapsed time that change every frame. Physics engines, as I found out the hard way, really do not like this, but I'm sure there are plenty of game engines that would be significantly harder to write if you had to make them able to advance arbitrary amounts of time every frame. If your engine is easier to write where it's based on a nominal world update speed (I'm using 100Hz now), just call that fixed step however many times you need to to fill the time that's elapsed since the last frame. Throw the rounding error in an accumulator variable so that partial steps get used up as needed. You're at risk of ugly judder (temporal quantization artifacts!) if the steps length is just the wrong distance from your display refresh rate, but I haven't detected any visible judder running a 100Hz physics refresh with a display going usually 40-80Hz. Fiedler suggests using position interpolation if judder is an issue, which is a pretty baller move, but I haven't needed to.

Here's what it looks like in my Chipmunk prototype:

FixedStepMs = 10 --100fps/10ms physics
StepAccumulator = 0
MaxStepsPerFrame = 7 --allow slowdown if it frame time is over 70ms - 15fps may be tolerable
LastUpdate = playdate.getCurrentTimeMilliseconds()

function playdate.update() 
    fixedRefresh()
end

function fixedRefresh --derived from https://gafferongames.com/post/fix_your_timestep/
    local now = playdate.getCurrentTimeMilliseconds()
    if now < LastUpdate + fixedStepMs then return end -- simple rate limiter
    updateInputs()

    local frameTime = now - LastUpdate
    StepAccumulator += frameTime
    LastUpdate = now
    
    local steps = 0

    while StepAccumulator >= FixedStepMs and steps < MaxStepsPerFrame do
        steps = steps + 1
        StepAccumulator -= FixedStepMs
    end

    for i=1, steps do
        updateWorld(fixedStepMs)
    end

    updateGraphics()
end
6 Likes

I don't know how to low-power, but I suspect my brainless frame limiter (just return from update() if it's not time yet) is the wrong way. Does playdate.wait() allow for more energy-efficient idling?

In my experience, yes.

But it's more complicated than that.

I found that wait() winds down the CPU to low power state and then it takes a moment to get back up to speed. Tiny fraction of a second, of course, but the effect was noticeable in my performance. I'm not sure if in the long run there were any efficiency gains when toggling the CPU speed so frequently.

Instead I do while/end checking for elapsed time, very similar to yours but I'm targeting 60fps using setRefreshRate(0). This keeps the CPU active and I noticed much more consistent, higher performance. I'd have to do some sort of power draw test to see how different the approaches are in terms of battery usage.

1 Like

Yeah, I'm effectively targeting 100fps and never succeeding (on device anyway) with setRefreshRate(0); the thing that I really wanted to share is how gracefully the fixed-step remainder algorithm handles it when you come up short of making your target consistently and to varying degrees. Like, it's all of two lines to allow slowdown if the framerate tanks too hard!

Thanks for having already done the research for me! IIRC the SDK docs said that lower refresh rates would lead to lower power consumption, so I wonder if part of the judder one sometimes sees with the built-in refresh rate limiter under demanding loads is because it is using wait() (or similar) and the CPU takes a while to warm up.

Mostly I just feel guilty implementing what is basically for i = 1 to 1000 next i to wait, but I shouldn't be too precious about that when I'm trying to maximize utilization of an extremely underpowered single-threaded computer. In gameplay I guess it's best to be brutal, and if you're feeling like optimizing for battery just turn on a framerate cap when you're doing less response-critical stuff like menus.

1 Like

Exactly! We're on the same page.

I don't think it's as black and white as that. It possible for a 30fps game to be using 100% of the CPU and a 50fps game to be using 30% of the CPU (Gun Trails is exactly the later scenario). Which of those uses less power? I don't know the answer, but I do know it's not clear cut.

Multiplying frame rate by CPU shows the 50fps scenario is half as power hungry, even if you factor in the screen updating 1.666 times as frequently it still seems to be lower power to me.