2

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:

  • poll_events() : ~0.002 ms
  • update() : ~0.1 ms
  • render() : ~1 ms

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.

Valazo
  • 31
  • 3
  • Does your loop really need to poll continuously? You can have the polling in a separate thread and have it sleep for a few ms every iteration. Is there a way you can have interrupts (event-driven) instead of polling for new events? – Slava Knyazev Apr 16 '22 at 04:32
  • I tried polling only before every update but for some reason the program slows down to 80-90% speed, and I haven't been able to figure out why... regarding polling in a different thread, the SDL documentation discourages it, and instead recommends using the main thread to handle rendering and polling, so I'm guessing it's not very viable in this case. – Valazo Apr 16 '22 at 05:13
  • 1
    I wonder if it's not a Windows issue? On my machine (see https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e55664f92447a3b227a005e9e82232ec) I can reliable sleep for say 14ms if I ask for 14ms, usually with an error of at most 0.2ms, which I don't think is noticeable (you can always try sleeping for a bit less). – jthulhu Apr 16 '22 at 07:25
  • 1
    Since you're using SDL2, you could always just turn on vsync to avoid pinning a CPU thread. If that's not an option and you need to manually limit frame rate, my understanding is that using `sleep` is the go-to solution. – JMAA Apr 17 '22 at 20:56
  • Looks like it might be a Windows-specific problem, I'll look better into that. I'll check on the vsync option as well, I hadn't tried it before cause I'm still a bit clueless on the concept. – Valazo Apr 18 '22 at 01:34

1 Answers1

0

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;
}
Valazo
  • 31
  • 3