SDL2, Emscripten/asmjs, and 'Invalid renderer' panic

It seems like the combination of Rust, SDL2, and Emscripten is something that some people have made to work at various points in the past.

I have an issue that has me a bit stumped. I'm a complete newbie when it comes to Rust, so I can't discount that factor. I borrowed a tiny program from:

https://puddleofcode.com/story/definitive-guide-to-rust-sdl2-and-emscriptem

...that with a little coaxing, I was able to compile with --target=asmjs-unknown-emscripten, but when run in firefox, I get a message in the console:

thread 'main' panicked at 'Invalid renderer', /home/blah/.cargo/registry/src/github.com-blah-blah/sdl2-0.35.1/src/sdl2/render.rs:990:13

...which if you look at line 990 in render.rs, it has the obvious panic in :

    /// Sets the color used for drawing operations (Rect, Line and Clear).
    #[doc(alias = "SDL_SetRenderDrawColor")]
    pub fn set_draw_color<C: Into<pixels::Color>>(&mut self, color: C) {
        let (r, g, b, a) = color.into().rgba();
        let ret = unsafe { sys::SDL_SetRenderDrawColor(self.raw, r, g, b, a) };
        // Should only fail on an invalid renderer
        if ret != 0 {
            panic!("{}", get_error()) //LINE 990
        }
    }

...details on how SDL gets initialized and the renderer get created look like:

    fn main() {
        let ctx = sdl2::init().unwrap();
        let video_ctx = ctx.video().unwrap();
    
        let window  = match video_ctx
            .window("updating SDL2 example with rust and asmjs...", 640, 480)
            .position_centered()
            .opengl()  //same results whether .opengl is used or not
            .build() {
                Ok(window) => window,
                Err(err)   => panic!("failed to create window: {}", err)
            };

        let mut renderer = match window
            // replaced "renderer()" with "into_canvas()" since the "renderer()" method no 
            // longer seems available in the upgrade from rust-SDL2 v0.29.0 to v0.35.0. 
            // Compiling with v0.29.0 is apparently incompatible with my recent 
            // version of emscripten (2.0.31).
            //.renderer()
            .into_canvas()
            .build() {
                Ok(renderer) => renderer,
                Err(err) => panic!("failed to create renderer: {}", err)
            };
    //...

...With the above running without issue. What I do know is that the program works as expected when compiled as a Linux native app. And I also know that my Emscripten installation is working, since I can create working C++ and SDL2 apps that work in the browser. And it only panics when dealing with the graphics system. The event loop in the Rust->asmjs app works for getting mouse and keyboard events. The source files for the small example with the panic are over at:

https://gist.github.com/gregbuchholz/7d731191fcea95b7450859aeb0f4eb20

...(which includes the main.rs, Cargo.toml, etc. files). The panic occurs in main.rs at line 68:

        println!("I'm going to panic on the next statement..."); 
        // with an "Invalid renderer" at sdl2/render.rs:990...
        let _ = renderer.set_draw_color(black);  //LINE 68
        let _ = renderer.clear();

...anyone have thoughts on how to go about debugging this? Println!() debugging would normally seem to be the name of the game here, but currently the various "renderer" objects don't support the debug fmt. (adding that may be the next step).

versions of interest:

    $ rustc --version
    rustc 1.55.0 (c8dfcfe04 2021-09-06)
    
    $ emcc --version
    emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 2.0.31 (4fcbf0239ccca29771f9044c990b0d34fac6e2e7)
    Copyright (C) 2014 the Emscripten authors (see AUTHORS.txt)
    This is free and open source software under the MIT license.
    There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

...there are various tutorials out there which attempt to get rust, sdl2, and some flavor of asmjs/wasm working, but they all seem to have bit-rotted. Any help would be greatly appreciated!

Thanks!

I've solved this problem. It turns out it was probably an overly "unsafe" version of the "emscripten.rs" wrapper that called the "emscripten_set_main_loop()". This didn't allow me to see that I probably needed a "move" for the closure. When switching to a slightly different "emscripten.rs" wrapper, this became apparent. So here's the better "emscripten.rs":

// Based on emscripten.rs from https://github.com/therocode/rust_emscripten_main_loop
// This file interacts with the Emscripten API to provide a scheduling mechanism for main looping.

// Since Emscripten only schedules the looping to be executed later by the browser, we need to make sure that the
// data object looped upon lives as long as the looping is scheduled, as well as being properly destroyed afterwards.

// The Emscripten function used for this is emscripten_set_main_loop which will do the scheduling as well as terminate the current code flow
// to prevent scopes from being exited which would cause objects to be destroyed prematurely. To be able to destroy the data object properly
// as looping is terminated, the object is stored in thread_local storage.

#[cfg(target_os = "emscripten")]
pub mod emscripten {
    use std::cell::RefCell;
    use std::os::raw::c_int;

    // Declare our FFI to the Emscripten functions we need. These will be linked in when building for Emscripten targets.
    #[allow(non_camel_case_types)]
    type em_callback_func = unsafe extern "C" fn();

    extern "C" {
        pub fn emscripten_set_main_loop(
            func: em_callback_func,
            fps: c_int,
            simulate_infinite_loop: c_int,
        );
        pub fn emscripten_cancel_main_loop();
    }

    thread_local! {
        // This is where the data object will be kept during the scheduled looping. The storage structure is justified as follows

        // thread_local - we need it outside of function scope. thread_local is enough since we only expect interactions from the same thread.
        // RefCell<..> - allows for mutable access from anywhere which we need to store and then terminate. Still borrow-checked in runtime.
        // Option<..> - we don't always have anything scheduled
        // Box<dyn ...> - make it work generically for any closure passed in

        static MAIN_LOOP_CLOSURE: RefCell<Option<Box<dyn FnMut()>>> = RefCell::new(None);
    }

    // Schedules the given callback to be run over and over in a loop until it returns MainLoopEvent::Terminate.
    // Retains ownership of the passed callback
    pub fn set_main_loop_callback<F: 'static>(callback: F)
    where
        F: FnMut(),
    {
        // Move the callback into the data storage for safe-keeping
        MAIN_LOOP_CLOSURE.with(|d| {
            *d.borrow_mut() = Some(Box::new(callback));
        });

        // Define a wrapper function that is compatible with the emscripten_set_main_loop function.
        // This function will take care of extracting and executing our closure.
        unsafe extern "C" fn wrapper<F>()
        where
            F: FnMut(),
        {
            // Access and run the stashed away closure
            MAIN_LOOP_CLOSURE.with(|z| {
                if let Some(closure) = &mut *z.borrow_mut() {
                    (*closure)();
                }
            });

        }

        // Schedule the above wrapper function to be called regularly with Emscripten
        unsafe {
            emscripten_set_main_loop(wrapper::<F>, 0, 1);
        }
    }

    // This is used to de-schedule the main loop function and destroy the kept closure object
    pub fn cancel_main_loop() {
        // De-schedule
        unsafe {
            emscripten_cancel_main_loop();
        }

        // Remove the stored closure object
        MAIN_LOOP_CLOSURE.with(|d| {
            *d.borrow_mut() = None;
        });
    }
}

...and here is the update "main.rs" that has "move" added before the main_loop closure:

//Original from: https://puddleofcode.com/story/definitive-guide-to-rust-sdl2-and-emscriptem
extern crate sdl2;

use std::process;
use sdl2::rect::{Rect};
use sdl2::event::{Event};
use sdl2::keyboard::Keycode;

#[cfg(target_os = "emscripten")]
pub mod emscripten;

fn main() {
    let ctx = sdl2::init().unwrap();
    let video_ctx = ctx.video().unwrap();

    let window  = match video_ctx
        .window("updating SDL2 example with rust and asmjs...", 640, 480)
        .position_centered()
        //.opengl()
        .build() {
            Ok(window) => window,
            Err(err)   => panic!("failed to create window: {}", err)
        };

    let mut renderer = match window
        // replace "renderer()" with "into_canvas()" since the "renderer()" method no 
        // longer seems available in the upgrade from rust-SDL2 v0.29.0 to v0.35.0. 
        // Compiling with v0.29.0 is apparently incompatible with my recent 
        // version of emscripten (2.0.31).
        //.renderer()
        .into_canvas()
        .build() {
            Ok(renderer) => renderer,
            Err(err) => panic!("failed to create renderer: {}", err)
        };

    let mut rect = Rect::new(10, 10, 10, 10);

    let black = sdl2::pixels::Color::RGB(0, 0, 0);
    let white = sdl2::pixels::Color::RGB(255, 255, 255);

    let mut events = ctx.event_pump().unwrap();

    let mut main_loop = move || {
        for event in events.poll_iter() {
            match event {
                Event::Quit {..} | Event::KeyDown {keycode: Some(Keycode::Escape), ..} => {
                    process::exit(1);
                },
                Event::KeyDown { keycode: Some(Keycode::Left), ..} => {
                    rect.x -= 10;
                },
                Event::KeyDown { keycode: Some(Keycode::Right), ..} => {
                    rect.x += 10;
                },
                Event::KeyDown { keycode: Some(Keycode::Up), ..} => {
                    rect.y -= 10;
                },
                Event::KeyDown { keycode: Some(Keycode::Down), ..} => {
                    rect.y += 10;
                },
                _ => {}
            }
        }

        let _ = renderer.set_draw_color(black);
        let _ = renderer.clear();
        let _ = renderer.set_draw_color(white);
        let _ = renderer.fill_rect(rect);
        let _ = renderer.present();
    };

    #[cfg(target_os = "emscripten")]
    use emscripten::{emscripten};

    #[cfg(target_os = "emscripten")]
    emscripten::set_main_loop_callback(main_loop);

    #[cfg(not(target_os = "emscripten"))]
    loop { main_loop(); }
}
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.