rust-sfml icon indicating copy to clipboard operation
rust-sfml copied to clipboard

Can't share window between threads

Open tavurth opened this issue 6 years ago • 20 comments

While attempting to setup a threaded render-input system, I ran across the following:

extern crate sfml;
extern crate crossbeam;

use sfml::window::{ContextSettings, Style, Event, Key};
use sfml::graphics::{RenderWindow};

fn input(_scope: &crossbeam::Scope, mut window: RenderWindow) {
    while let Some(event) = window.wait_event() {
        println!("{:?}", event);

        match event {
            Event::Closed |
            Event::KeyPressed {
                code: Key::Escape, ..
            } => break,

            _ => {}
        }
    }
}

fn main() {
    let mut main_window = RenderWindow::new(
        (800, 500),
        "Test window",
        Style::CLOSE,
        &ContextSettings { ..Default::default() }
    );

    crossbeam::scope(|scope| {
        input(scope, main_window);
    });

    main_window.display()
}

Which gives the rustc error:


error[E0382]: use of moved value: `main_window`
  --> src/main.rs:37:5
   |
33 |     crossbeam::scope(|scope| {
   |                      ------- value moved (into closure) here
...
37 |     main_window.display()
   |     ^^^^^^^^^^^ value used here after move
   |
   = note: move occurs because `main_window` has type `sfml::graphics::RenderWindow`, 
     which does not implement the `Copy` trait

Can you suggest how I could get access to the window for both rendering and input processing?

I tried Arc::new(main_window) and it's giving me the same issue.

tavurth avatar Nov 11 '17 16:11 tavurth

Any reason you are moving the window into the scoped thread, instead of taking a mutable reference?

crumblingstatue avatar Nov 11 '17 16:11 crumblingstatue

@crumblingstatue Ah, that's fixed it thank you. The reason I wasn't passing a mutable reference is because I'm rather new to Rust :)

However, due to crossbeam's thread joining, we're not actually getting to the display functionality immediately here, just joining the input thread, and then running the render call.

If I try to do something like this:

extern crate sfml;
extern crate crossbeam;

use std::{time, thread};
use sfml::window::{ContextSettings, Style, Event, Key};
use sfml::graphics::{RenderWindow};

fn input(_scope: &crossbeam::Scope, window: &mut RenderWindow) {
    while let &Some(event) = &window.wait_event() {
        println!("{:?}", event);

        match event {
            Event::Closed |
            Event::KeyPressed {
                code: Key::Escape, ..
            } => break,

            _ => {}
        }
    }
}

fn render(_scope: &crossbeam::Scope, window: &mut RenderWindow) {
    loop {
        println!("In render loop!");
        thread::sleep(time::Duration::from_millis(1000));
    }
}

fn main() {
    let mut main_window = RenderWindow::new(
        (800, 500),
        "Test window",
        Style::CLOSE,
        &ContextSettings { ..Default::default() }
    );
    main_window.set_vertical_sync_enabled(true);

    crossbeam::scope(|scope| {
        let input_thread = scope.spawn(|| { input(scope, &mut main_window) });
        let render_thread = scope.spawn(|| { render(scope, &mut main_window) });
    });

    main_window.display()
}

We're now failing with:

40 |         let input_thread = scope.spawn(|| { input(scope, &mut main_window) });
   |                                  ^^^^^ `*mut csfml_graphics_sys::sfRenderWindow` 
   |                                  cannot be sent between threads safely
   |
   = help: within `sfml::graphics::RenderWindow`, the trait `std::marker::Sync` 
           is not implemented for `*mut csfml_graphics_sys::sfRenderWindow`

   = note: required because it appears within the type `sfml::graphics::RenderWindow`

   = note: required because of the requirements on the impl of `std::marker::Send` for 
           `&sfml::graphics::RenderWindow`

   = note: required because it appears within the type 
           `[closure@src/main.rs:40:40: 40:73 
           scope:&&crossbeam::Scope<'_>, main_window:&sfml::graphics::RenderWindow]`

Any ideas?

tavurth avatar Nov 11 '17 16:11 tavurth

*mut csfml_graphics_sys::sfRenderWindow` cannot be sent between threads safely

I'm actually not sure if it really is safe to send a sf::RenderWindow between threads or not. I'll have to do some research on this.

In the meantime, I'm not sure what advice to give, other than try not to have input handling on a different thread. It's usually not required.

crumblingstatue avatar Nov 11 '17 16:11 crumblingstatue

Thanks for your help!

Coming from C++, poll_event is not recommended, as it drags heavily on the CPU and makes input response times sluggish. It's usually preferred to have input in a separate thread, and use wait_event to get input events posted to the thread.

More on SO

tavurth avatar Nov 11 '17 16:11 tavurth

That SO article is talking about SDL. Are you sure this also applies to SFML? I never had any sluggish input with SFML (But then again, not with SDL either), even with single threaded event handling.

crumblingstatue avatar Nov 11 '17 16:11 crumblingstatue

You may be right, I've not worked with SFML before this.

I can use Key::{}.is_pressed() for movement etc, but for actions I still have to loop through pollEvent():

while let Some(event) = window.poll_event() { ... }

Which could take a long time (relatively) if I've not polled this frame and the user has been pressing lots of keys.

I'll make some tests just now and see how it performs.

tavurth avatar Nov 11 '17 16:11 tavurth

So I ran some tests using poll input, and the results were as follows:

I ran the same test 3 times, for the tests I used only the keys WASD, E, and SPACE. No mouse movement took place during the tests.

Item Time taken (ns) Time taken (ms)
Max input latency 4782494 4.78
Max render latency 12596199 12.59
Average input latency 235032 0.23
Average render latency 2435552 2.43

So while 0.23ms is really fast enough, 4.78ms starts to eat into our 16ms/frame render cycle for 60FPS.

Here's the code I used to extract the data to JSON format:

extern crate sfml;
extern crate crossbeam;

use std::time;
use std::fs::File;
use std::io::Write;
use std::ops::{Add, Div};

use sfml::window::{ContextSettings, Style, Event, Key};
use sfml::graphics::{RenderWindow};

fn input(window: &mut RenderWindow) -> bool {
    while let &Some(event) = &window.poll_event() {
        match event {
            Event::Closed |
            Event::KeyPressed {
                code: Key::Escape, ..
            } => return true,

            _ => {}
        }
    }

    return false;
}

fn main_loop(window: &mut RenderWindow) {
    // Count frames
    let mut counter = 0;
    let mut is_first = true;

    let mut max_input = time::Duration::from_millis(0);
    let mut max_render = time::Duration::from_millis(0);
    let mut total_input_time = time::Duration::from_millis(0);
    let mut total_render_time = time::Duration::from_millis(0);

    let mut f = File::create("input_states.json").expect("Unable to create file");

    let mut write_to_file = |string: String| -> () {f.write_all(string.as_bytes()).expect("Unable to write data")};

    // Opening json brace
    write_to_file("[".to_string());

    loop {
        let start = time::Instant::now();
        if input(window) == true {
            break;
        };
        let time_after_input = start.elapsed();

        // Re-render the screen
        window.display();

        let time_after_render = start.elapsed();

        // Now we've got the times ASAP, run slower processing
        total_input_time = total_input_time.add(time_after_input);
        total_render_time = total_render_time.add(time_after_render);

        // Increment our max counter if needed
        if time_after_input.gt(&max_input) {
            max_input = time_after_input;
        }

        // Increment our max counter if needed
        if time_after_render.gt(&max_render) {
            max_render = time_after_render;
        }

        counter += 1;
        if counter > 10 {

            let avg_input = total_input_time.div(counter).subsec_nanos();
            let avg_render = total_render_time.div(counter).subsec_nanos();

            let mut to_write = "".to_string();

            if is_first == false {
                to_write.push_str(",\r\n");
            }

            to_write.push_str("{");
            to_write.push_str("\"input\":");
            to_write.push_str(&avg_input.to_string());
            to_write.push_str(", \"render\":");
            to_write.push_str(&avg_render.to_string());
            to_write.push_str("}");
            write_to_file(to_write);

            // Zero everything out
            counter = 0;
            total_input_time = time::Duration::from_millis(0);
            total_render_time = time::Duration::from_millis(0);

            is_first = false;
        }
    }

    // Closing json brace
    write_to_file("]".to_string());

    println!("--------------------------------------------------");
    println!("Finished:");

    println!("Input time MAX: {:?}", max_input);
    println!("Render time MAX: {:?}", max_render);
}

fn main() {
    let mut main_window = RenderWindow::new(
        (800, 500),
        "Test window",
        Style::CLOSE,
        &ContextSettings { ..Default::default() }
    );

    main_loop(&mut main_window);
}

And the python code for formatting the JSON:

import math
import json

with open("input_states.json") as fin:
    data = json.loads(fin.read());

inputs = ()
renders = ()
for item in data:
    inputs += (item['input'],)
    renders += (item['render'],)

avgInput = sum(inputs) / len(data)
avgRender = sum(renders) / len(data)

maxInput = max(inputs)
maxRender = max(renders)

print("Item | Time taken (ns) | Time taken (ms)")
print("------------ | ------------- | -------------")
print("Max input latency |", math.floor(maxInput), "|", math.floor(maxInput / 1e4) / 100)
print("Max render latency |", math.floor(maxRender), "|", math.floor(maxRender / 1e4) / 100)
print("Average input latency |", math.floor(avgInput), "|", math.floor(avgInput / 1e4) / 100)
print("Average render latency |", math.floor(avgRender), "|", math.floor(avgRender / 1e4) / 100)

tavurth avatar Nov 11 '17 18:11 tavurth

Looks like maybe only the first render & input are causing the problem. Removing the first input timing, causes a reduction down to 2.22 ms from 4.78 ms, which is more manageable.

Item Time taken (ns) Time taken (ms)
Max input latency 2228038 2.22
Max render latency 4172918 4.17
Average input latency 221663 0.22
Average render latency 1737821 1.73

tavurth avatar Nov 11 '17 18:11 tavurth

this may be noteworthy:

On OS X, windows and events must be managed in the main thread Yep, that's true. Mac OS X just won't agree if you try to create a window or handle events in a thread other than the main one.

from this tutorial on the SFML website

ghost avatar Apr 20 '18 04:04 ghost

So, how to renderer in loop, if loop blocks main thread?

TheRadioGuy avatar Jan 04 '19 14:01 TheRadioGuy

So, how to renderer in loop, if loop blocks main thread?

Not sure what you're asking here. If you're already doing all the rendering within the loop, why is it a problem if it blocks the main thread?

crumblingstatue avatar Jan 04 '19 16:01 crumblingstatue

 loop{
 // render
}
println!(123);

println!(123) will never execute.

But I want to execute code after loop

TheRadioGuy avatar Jan 04 '19 18:01 TheRadioGuy

@DuckerMan Well, I don't know enough about your specific problem to help, but you should consider redesigning your code so that it doesn't need sharing of RenderWindow between threads. Even if it was possible, it would be very rarely needed.

crumblingstatue avatar Jan 04 '19 23:01 crumblingstatue

Ok, thank you very much :smiley:

TheRadioGuy avatar Jan 05 '19 09:01 TheRadioGuy

I managed to do this in code but, it isn't very clean code:

use sfml::window::*;
use sfml::graphics::*;

use std::thread;

use crate::input::InputHandler;
use std::sync::{Mutex, Arc};
use std::time::{SystemTime, Duration};
use std::thread::sleep;

mod input;
mod entity;
mod component;


unsafe fn render_thread(mut window: WindowBox) {
    (*window.0).set_vertical_sync_enabled(true);

    (*window.0).set_active(true);

    while (*window.0).is_open() {
        let now = SystemTime::now();
        (*window.0).clear(Color::BLACK);

        // draw calls

        (*window.0).display();
        println!("FPS: {:.2}", 1e9/now.elapsed().unwrap().as_nanos() as f32)
    }
}

struct WindowBox(*mut RenderWindow);

unsafe impl Send for WindowBox {}
unsafe impl Sync for WindowBox {}

fn main() {
    let mut window = RenderWindow::new(

        (800, 600),
        "SFML works!",
        Style::CLOSE | Style::RESIZE,
        &Default::default()
    );

    window.set_active(false);
    let mut wb = WindowBox(&mut window as *mut _);

    let rthread = thread::spawn(
        || unsafe{
                render_thread(wb)
        });

    let mut input_handler = InputHandler::new(0);

    while window.is_open() {
        let now = SystemTime::now();
        while let Some(event) = window.poll_event() {
            match event {
                Event::Closed | Event::KeyPressed { code: Key::Escape, .. } => return,
                _ => {}
            }
        }

        input_handler.handle_input();
        sleep(Duration::from_millis(10));
        println!("TPS: {:.2}", 1000./now.elapsed().unwrap().as_millis() as f32);
    }
    rthread.join();
}

BinaryAura avatar Feb 18 '20 17:02 BinaryAura

@BinaryAura How can you be sure that no race conditions occur? Also from what I know, rendering with opengl only works on the main thread, so if my information is correct that code shouldn't work.

MagicRB avatar Feb 20 '20 15:02 MagicRB

@MagicRB You're right. OpenGL can be rendered only in the main thread

TheRadioGuy avatar Feb 21 '20 17:02 TheRadioGuy

Oh and for those wanting to share windows between threads, command buffers might be of use to you folk

MagicRB avatar Feb 21 '20 18:02 MagicRB

@MagicRB. Technecally, you can't. To do this properly would require breaking up the RenderWindow object into three parts. The event loop actions (only main), the render actions (only Render), World (a.k.a. objects to draw) (mut in main and ref in Render). As for OpenGL, it's not that it can only be done in the main thread. OpenGL has to have the current context owned by the running thread. Of coarse to fix this would be a total pain.

BinaryAura avatar Mar 31 '20 15:03 BinaryAura

Well, if you want to control some parts of the window, like resizing and stuff, a command buffer can be used

MagicRB avatar Mar 31 '20 17:03 MagicRB