Rust, SDL2 and raw textures help

Hi,

I just chose Rust to get into programming, the road is a quite a bit harder but I wanted to start with a language that I think has the most promise.

I have started to dabble with the rust-sdl2 crate and I am stuck on a specific issue and struggling to understand the documentation.

I'm trying to render pixels to the screen at a decent frame rate so I started playing using sdl2::render::Canvas::draw_point and sdl2::render::Canvas::set_draw_color but it's dreadfully slow.
I did a bit of research and it seems that the fastest way to do it is to use a texture and then copy the texture to the renderer. Unfornately, I can only find examples on how to load textures from files and I can't figure out how to actually write to a raw texture.

I would really appreciate some help, I guess the fact that I'm a beginner at both Rust and SDL doesn't help but it's the only way I found to get myself some motivation.

Thanks in advance!

Sometimes, the best place to look for procedural information is the original library’s documentation: There’s a C example on the SDL_CreateTexture page; the basic approach seems to be:

  • Call SDL_CreateTexture to allocate a texture.
  • Call SDL_SetRenderTarget to start drawing to the texture
  • Use the same SDL_Render* calls you’re already using to draw the texture contents
  • Call SDL_SetRenderTarget again to draw to the screen
  • Call SDL_RenderCopy to draw a copy of the texture onto the screen.

I’m sure that all of these functions have fairly direct Rust equivalents, but I don’t have any experience with the Rust bindings.

What do you want to do?

SDL may not be the best solution.

EDI: Now that I read your post again, how about https://lib.rs/crates/pixel-canvas ?
Drawing images pixel by pixel with each call passing via SDL will never be fast. Too much overhead.

Thanks @2e71828 that was my first idea (well second, after looking at the crate documentation) but it's a bit obscure to a noob like me, I haven't touched C in close to 30 years.

I'm trying to figure out how to write to the texture itself, it created it as follow.

    let sdl_context = sdl2::init().unwrap();
    let video_subsystem = sdl_context.video().unwrap();
 
    let window = video_subsystem.window("rust-sdl2", 320, 240)
        .position_centered()
        .build()
        .unwrap();
 
    let mut canvas : Canvas<Window> = window.into_canvas()
        .present_vsync()
        .accelerated()
        .build()
        .unwrap();

    let creator = canvas.texture_creator();
    let mut texture = creator.create_texture_static(PixelFormatEnum::RGBA8888, 640, 480).unwrap();

But from there, I don't know how to write data to the texture.

@s3bk I'm trying to recreate some 2D computer graphics effect I use to do in ASMx86 at a time where I could just write pixels directly to the screen :slight_smile:

I'd rather stick to SDL2 because it gives me access to OpenGL and Vulkan too and works well with Linux.

It has to be faster than loading it from disk (only examples I could find) :wink:

It looks like you need Canvas.with_texture_canvas, which will let you draw onto the created texture.

Thanks a lot for that, the thing I don't understand now is how to write into that texture structure, how to I actually write the ARGB definition of each pixel into that texture?

Inside the closure, drawing calls on the Canvas will go to the texture instead of the screen. There’s an example if you scroll down the page a bit:

let texture_creator = canvas.texture_creator();
let mut texture = texture_creator
    .create_texture_target(texture_creator.default_pixel_format(), 150, 150)
    .unwrap();
let result = canvas.with_texture_canvas(&mut texture, |texture_canvas| {
    texture_canvas.set_draw_color(Color::RGBA(0, 0, 0, 255));
    texture_canvas.clear();
    texture_canvas.set_draw_color(Color::RGBA(255, 0, 0, 255));
    texture_canvas.fill_rect(Rect::new(50, 50, 50, 50)).unwrap();
});

I will give that a try tonight, thank you very much!

@fernando Probably a dumb question but... I guess you're calling canvas.present() just once at the end of the loop, right?

I'm beginner so there are no dumb questions :smile:
Yes, I'm only calling it once per frame.

Isn't that the opposite of what @fernando wants?

My understanding is that @fernando wants (for performance reasons) to write to an offscreen buffer, and then blit that in one shot to the screen. I don’t know if this way is actually faster than drawing to the framebuffer directly, but it’s at least plausible.

Yes, that's exactly it, I tried doing it this way first but it's super slow

    let video_subsystem = sdl_context.video().unwrap();
 
    let window = video_subsystem.window("rust-sdl2 keftals", screen_size[0], screen_size[1])
        .position_centered()
        // .fullscreen()
        .build()
        .unwrap();
 
    let mut canvas : Canvas<Window> = window.into_canvas()
        .present_vsync()
        .accelerated()
        .build()
        .unwrap();

Then a loop doing something like

        for y in 0..599 {
            for x in 0..1023 {
                // some computation happening to rrr, gee, bee
                canvas.set_draw_color(Color::RGB(rrr, gee, bee));
                canvas.draw_point(Point::new(x as i32, y as i32)).unwrap();
            }
        }        

With a canvas.clear() and canvas.present() at the beginning and end of each frame.

This is extremely slow, on my laptop each frame takes over 90ms to render.

It looks like you can also build the pixel data as a &[u8] and then do something like this:

Surface::from_data(pixels, width, height, width*3, PixelFormatEnum::RGB24)
        .as_texture(canvas.texture_creator())

That’s often a last resort, as you have to give up SDL’s drawing routines. It might not be too bad for you, though, since you’re calculating every pixel in order anyway.

As far as I can see, that is 612k heap allocations per frame (introduced by Point::new()), so I think it's of course slow. Maybe storing them in an array would make it faster?

That's the reason why I want to use textures, so I can copy the entire screen in 1 operation. Thank you, that very informative :slight_smile:

It looks like Point::new() is just a stack allocation, and never touches the heap— it probably gets inlined by the compiler.

The slow part is draw_point itself: when it’s writing directly to the framebuffer, it has to store that value on the video card. The interconnect between main and video memory is relatively slow and optimized more for bulk transfers than random access.

Mm.. I was under the impression that any of those operations would be relatively fast, as long as you don't call canvas.present(), where, at that point, everything is sent to the framebuffer?

Are you using --release flag or opt-level in dev profile? It can make Rust 10-100 times faster.

1 Like