Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I stop Piston from making the screen flash when I don't call `graphics::clear` every time the screen is rendered?

Consider two programs, and the difference between them:

$ diff flashes/src/main.rs doesnt_flash/src/main.rs
22,23c22
<
<     let mut i = 0;
---
>     let mut cursor_poses: Vec<(f64, f64)> = Vec::new();
28c27
<             mx = x; my = y;
---
>             cursor_poses.push((x,y));
32,33c31,33
<                     if i == 0 {
<                         graphics::clear([1.0; 4], g);
---
>                     graphics::clear([1.0; 4], g);
>                     for &(x, y) in cursor_poses.iter() {
>                         draw_cursor_pos([x, y], &c, g);
35,36d34
<                     draw_cursor_pos([mx, my], &c, g);
<                     i+=1;

Video demonstration of the two programs.

The program is an extremely basic paint program, with only one brush width, brush stroke color, canvas size, no saving, etc; oh and to stop drawing, move your mouse out of the window, since every time you go over the window this counts as drawing ;-)

flashes.rs does not draw every pixel every time e.render_args() is reached, except the first time. doesnt_flash.rs does draw every pixel every time e.render_args() is reached. This is the only difference between the two programs.

While it does not take long to generate the content in this program, so it is acceptable to re-generate it hundreds of times as the mouse moves over the window, this seems inefficient. In theory, as more and more points are added to the screen, each iteration of gl.draw takes longer and longer. In practice, the difference between calling graphics::ellipse one time vs. ten thousand times is not significant on modern hardware.

Other programs I'd want to write won't have that luxury as it will take longer to generate the result to put on the screen.

While perusing the API, I came up with no obvious way to just "do nothing". I assume that I would have to write my screen changes to some buffer object, then feed GlGraphics back this buffer object if e.render_args() is called but I don't need to update the screen.

The problem is, I can't seem to find this buffer object. :-(

How can I "do nothing" without getting screen flashing? If my theory is correct, how can I draw to a GlGraphics buffer instead of the screen, then feed my buffer back to the screen when I don't have anything new to draw?


Cargo.toml

[package]
name = "stackoverflow-piston-example"
version = "0.0.0"
authors = ["Fred"]
description = "Note: This program can be used for both of the programs below. Simply use `cargo new` and save either of the below files as `src/main.rs`"
keywords = []

[dependencies]
piston = "0.35.0"
piston2d-opengl_graphics = "0.50.0"
piston2d-graphics = "0.24.0"
piston2d-touch_visualizer = "0.8.0"
pistoncore-sdl2_window = "0.47.0"

doesnt_flash.rs

extern crate piston;
extern crate opengl_graphics;
extern crate graphics;
extern crate touch_visualizer;
extern crate sdl2_window;

use opengl_graphics::{ GlGraphics, OpenGL };
use graphics::{ Context, Graphics };
use piston::input::*;
use piston::event_loop::*;
use sdl2_window::Sdl2Window as AppWindow;

static CURSOR_POS_COLOR: [f32; 4] = [0.0, 0.0, 0.0, 1.0];

fn main() {
    let opengl = OpenGL::V3_2;
    let mut window: AppWindow = piston::window::WindowSettings::new("Example for StackOverflow", [600, 600])
        .exit_on_esc(true).opengl(opengl).build().unwrap();

    let ref mut gl = GlGraphics::new(opengl);
    let (mut mx, mut my) = (0., 0.);
    let mut cursor_poses: Vec<(f64, f64)> = Vec::new();

    let mut events = Events::new(EventSettings::new().lazy(true));
    while let Some(e) = events.next(&mut window) {
        e.mouse_cursor(|x, y| {
            cursor_poses.push((x,y));
        });
        if let Some(args) = e.render_args() {
            gl.draw(args.viewport(), |c, g| {
                    graphics::clear([1.0; 4], g);
                    for &(x, y) in cursor_poses.iter() {
                        draw_cursor_pos([x, y], &c, g);
                    }
                }
            );
        }
    }
}

fn draw_cursor_pos<G: Graphics>(
    cursor: [f64; 2],
    c: &Context,
    g: &mut G,
) {
    graphics::ellipse(
        CURSOR_POS_COLOR,
        graphics::ellipse::circle(cursor[0], cursor[1], 4.0),
        c.transform,
        g
    );
}

flashes.rs

extern crate piston;
extern crate opengl_graphics;
extern crate graphics;
extern crate touch_visualizer;
extern crate sdl2_window;

use opengl_graphics::{ GlGraphics, OpenGL };
use graphics::{ Context, Graphics };
use piston::input::*;
use piston::event_loop::*;
use sdl2_window::Sdl2Window as AppWindow;

static CURSOR_POS_COLOR: [f32; 4] = [0.0, 0.0, 0.0, 1.0];

fn main() {
    let opengl = OpenGL::V3_2;
    let mut window: AppWindow = piston::window::WindowSettings::new("Example for StackOverflow", [600, 600])
        .exit_on_esc(true).opengl(opengl).build().unwrap();

    let ref mut gl = GlGraphics::new(opengl);
    let (mut mx, mut my) = (0., 0.);

    let mut i = 0;

    let mut events = Events::new(EventSettings::new().lazy(true));
    while let Some(e) = events.next(&mut window) {
        e.mouse_cursor(|x, y| {
            mx = x; my = y;
        });
        if let Some(args) = e.render_args() {
            gl.draw(args.viewport(), |c, g| {
                    if i == 0 {
                        graphics::clear([1.0; 4], g);
                    }
                    draw_cursor_pos([mx, my], &c, g);
                    i+=1;
                }
            );
        }
    }
}

fn draw_cursor_pos<G: Graphics>(
    cursor: [f64; 2],
    c: &Context,
    g: &mut G,
) {
    graphics::ellipse(
        CURSOR_POS_COLOR,
        graphics::ellipse::circle(cursor[0], cursor[1], 4.0),
        c.transform,
        g
    );
}
like image 475
Fredrick Brennan Avatar asked Dec 17 '17 12:12

Fredrick Brennan


1 Answers

I think the flashing is caused by buffer swapping: in flashes.rs only the first buffer to be drawn into is cleared. The second one will be all zeros, or leftover gpu memory if you're unlucky. According to the OpenGL wiki there's no good way around calling graphics::clear:

A modern OpenGL program should always use double buffering. . . The buffers should always be cleared. On much older hardware, there was a technique to get away without clearing the scene, but on even semi-recent hardware, this will actually make things slower. So always do the clear.

Instead, the usual method is to accumulate your changes to a texture or renderbuffer, and then draw that to the screen, exactly as you described.

I couldn't find any way to do this from within opengl_graphics either (there are no calls to gl::GenFramebuffers anywhere in it) but it's relatively straightforward to set up using raw gl calls. (I've used textures instead of renderbuffers because they have the significant advantage of being supported by high-level methods like Image::draw.)

extern crate piston;
extern crate opengl_graphics;
extern crate graphics;
extern crate sdl2_window;
extern crate gl;

use opengl_graphics::{ GlGraphics, OpenGL, Texture, TextureSettings };
use graphics::{ Context, Graphics, Transformed };
use graphics::image::Image;
use piston::input::*;
use piston::event_loop::*;
use piston::window::Window;
use sdl2_window::Sdl2Window as AppWindow;
use gl::types::GLuint;

static CURSOR_POS_COLOR: [f32; 4] = [0.0, 0.0, 0.0, 1.0];

fn main() {
    let opengl = OpenGL::V3_2;
    let mut window: AppWindow = piston::window::WindowSettings::new("Example for StackOverflow", [600, 600])
        .exit_on_esc(true).opengl(opengl).build().expect("window");

    let ref mut gl = GlGraphics::new(opengl);
    let (mut mx, mut my) = (0., 0.);

    let draw_size = window.draw_size();
    // It would also be possible to create a texture by hand using gl::GenTextures and call
    // gl::TexImage2D with a null pointer for the data argument, which would require another unsafe
    // block but would save this allocation
    let texture_buf = vec![0u8; draw_size.width as usize * draw_size.height as usize];
    let texture = Texture::from_memory_alpha(&texture_buf, draw_size.width, draw_size.height,
                                             &TextureSettings::new()).expect("texture");

    let fbo;
    unsafe {
        let mut fbos: [GLuint; 1] = [0];
        // Create a Framebuffer Object that we can draw to later
        gl::GenFramebuffers(1, fbos.as_mut_ptr());
        fbo = fbos[0];
        // Switch to it as the active framebuffer
        gl::BindFramebuffer(gl::FRAMEBUFFER, fbo);
        // Set up the framebuffer object so that draws to it will go to the texture
        gl::FramebufferTexture2D(gl::FRAMEBUFFER,
                                 gl::COLOR_ATTACHMENT0, // draw colors, not depth or stencil data
                                 gl::TEXTURE_2D, // the texture's type
                                 texture.get_id(),
                                 0); // mipmap level
    }

    let mut events = Events::new(EventSettings::new().lazy(true));
    while let Some(e) = events.next(&mut window) {
        e.mouse_cursor(|x, y| {
            mx = x; my = y;
        });
        e.render(|args| {
            // Switch to the texture framebuffer and draw the cursor
            unsafe {
                gl::BindFramebuffer(gl::FRAMEBUFFER, fbo);
            }
            gl.draw(args.viewport(), |c, g| {
                draw_cursor_pos([mx, my], &c, g);
            });

            // Switch to the window framebuffer and draw the texture
            unsafe {
                gl::BindFramebuffer(gl::FRAMEBUFFER, 0);
            }
            gl.draw(args.viewport(), |c, g| {
                graphics::clear([1f32, 1f32, 1f32, 0f32], g);
                // I can't entirely explain this.  We already applied the viewport transform when
                // we were rendering the cursor, so I think the texture is right-side-up for GL,
                // but piston::Image is expecting an image laid out in screen coordinates.
                // Since there is an offset in the viewport transform, the flip has to be applied
                // first, otherwise it would flip across the origin.
                let flipped = c.transform.prepend_transform(graphics::math::scale(1., -1.));
                Image::new().draw(&texture, &c.draw_state, flipped, g);
            });
        });
    }
}

fn draw_cursor_pos<G: Graphics>(
    cursor: [f64; 2],
    c: &Context,
    g: &mut G,
) {
    graphics::ellipse(
        CURSOR_POS_COLOR,
        graphics::ellipse::circle(cursor[0], cursor[1], 4.0),
        c.transform,
        g
    );
}

Alternatively, the gfx backend has the promising-sounding Factory::CreateRenderTarget method. My hardware doesn't support it, but I believe using it would look approximately like this:

extern crate piston;
extern crate graphics;
extern crate piston_window;
extern crate gfx_core;

use graphics::{ Context, Graphics, Transformed };
use graphics::image::Image;
use piston::input::*;
use piston::event_loop::*;
use piston::window::Window;
use piston_window::{ PistonWindow, OpenGL, G2dTexture };
use gfx_core::factory::Factory;
use gfx_core::texture::{ SamplerInfo, FilterMethod, WrapMode, Size };

static CURSOR_POS_COLOR: [f32; 4] = [0.0, 0.0, 0.0, 1.0];

fn main() {
    let opengl = OpenGL::V2_1;
    let window_settings =
        piston::window::WindowSettings::new("Example for StackOverflow", [600, 600])
        .opengl(opengl)
        .exit_on_esc(true);
    let mut window: PistonWindow = window_settings.build().expect("window");
    window.set_lazy(true);

    let size = window.draw_size();

    let (texture_handle, shader_view, target) = window.factory.create_render_target(size.width as Size, size.height as Size)
        .expect("render target");
    let sampler = window.factory.create_sampler(SamplerInfo::new(FilterMethod::Scale, WrapMode::Tile));
    let texture = G2dTexture {
        surface: texture_handle,
        sampler: sampler,
        view: shader_view,
    };
    let stencil = window.factory.create_depth_stencil_view_only(size.width as Size, size.height as Size)
        .expect("stencil");

    let (mut mx, mut my) = (0., 0.);

    while let Some(e) = window.next() {
        e.mouse_cursor(|x, y| {
            mx = x; my = y;
        });
        if let Some(args) = e.render_args() {
            window.g2d.draw(&mut window.encoder, &target, &stencil, args.viewport(), |c, g| {
                draw_cursor_pos([mx, my], &c, g);
            });

            window.draw_2d(&e, |c, g| {
                graphics::clear([1f32, 1f32, 1f32, 0f32], g);
                let flipped = c.transform.prepend_transform(graphics::math::scale(1., -1.));
                Image::new().draw(&texture, &c.draw_state, flipped, g);
            });
        }
    }
}

fn draw_cursor_pos<G: Graphics>(
    cursor: [f64; 2],
    c: &Context,
    g: &mut G,
) {
    graphics::ellipse(
        CURSOR_POS_COLOR,
        graphics::ellipse::circle(cursor[0], cursor[1], 4.0),
        c.transform,
        g
    );
}
like image 148
wartmanm Avatar answered Oct 31 '22 16:10

wartmanm