multithreadingrustsdl-2game-loopbusy-waiting

How to avoid busy wait in a single threaded event loop


I'm coding a simple piano roll app that uses the SDL2 bindings for Rust to do its event handling and rendering. I have something very similar to the following code:

let fps = 60;
let accumulator = 0; // Time in seconds

'running: loop {
    let t0 = std::time::Instant::now();
    poll_events();

    if accumulator > 1.0 / fps {
        update();
        render();
        counter -= 1.0 / fps;
    }
    let t1 = std::time::Instant::now();
    let delta = (t1 - t0).as_secs_f64();
    accumulator += delta;

    // Busy wait?
}

Generally, the application is running fine and it doesn't have any noticeable artifacts, at least to my untrained eye. However, the CPU usage is through the roof, using nearly 25% in average (plus some GPU usage for the rendering).

I've checked the CPU usage of a very similar program which has many more features and also has better graphics, and when given the same MIDI notes to display, it averages at 2% CPU usage, plus 10% on the GPU side.

I also benchmarked my code, and found out the following approximated timings:

Given that I'm aiming for 60 fps at both the logic level and the rendering level, I have roughly 16 milliseconds to do a full poll/update/render cycle. At the moment I'm using about 2 milliseconds (being generous) of the full range, so my conclusion is that the main loop has some busy wait going on, which I want to get rid of.

I've tried mainly sleep-based solutions which are quite unreliable, since the sleeping time depends on the operating system and at least in my machine (Windows 11) it's around 10-20 milliseconds, causing noticeable delays in the animation.

From what I read there are some thread-related solutions to avoid this kind of situation, but I feel like it's a totally unnecessary area to get into since I'm way below the point of needing any concurrency to squeeze more performance out of the machine.

I've been learning Rust for a couple of weeks, and although I've used SDL2 before in a smaller project using C++, I had the same problem and I couldn't find a suitable solution.

I'm not sure if this is a problem specifically related to SDL2 or if it also happens using other libraries, but any help would be very appreciated.


Solution

  • As another post had already discussed some versions of Windows use a 15ms sleep time by default, but the OS does have a more precise sleep time which can be configured down to about 0.5ms.

    There is a Rust crate that allows you to access a more precise timer by using the OS timer plus a little bit of busy wait for the remainders. That said, I didn't use this feature, as the native_sleep() function already gives me the resolution I needed.

    The updated code looks something like this:

    let fps = 60.;
    let accumulator = 0.; // Time in seconds
    
    'running: loop {
        let t0 = std::time::Instant::now();
        poll_events();
    
        if accumulator > 1.0 / fps {
            update();
            render();
            counter -= 1.0 / fps;
        }
        // Fix
        let sleep_time = std::time::Duration::from_millis(1);
        spin_sleep::native_sleep(sleep_time);
    
        let t1 = std::time::Instant::now();
        let delta = (t1 - t0).as_secs_f64();
        accumulator += delta;
    }