crossterm-rs / crossterm

Cross platform terminal library rust
MIT License
3.29k stars 280 forks source link

feat: add synchronized output/update #756

Closed jcdickinson closed 1 year ago

jcdickinson commented 1 year ago

The Contour terminal describes a VT extension for pausing rendering in the terminal during updates, preventing tearing and other artifacts. This extension is already supported by several terminals, with support planned for many others.

See: https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036

This adds the two commands related to pausing and resuming rendering: BeginSynchronizedUpdate and EndSynchronizedUpdate. It also adds a utility function that performs a function within a synchronized update.

A demo of this is also provided in the interactive example, that slowly renders a list of numbers - once without synchronized updates, and then once with.

jcdickinson commented 1 year ago

@TimonPost Thanks for the review! I have fixed the failing doctests

David-Else commented 11 months ago

@jcdickinson Sorry for asking here, but there is no discussions area in this repo.

I am trying to decide how I will render my game and am not sure the practical difference between using crosterm's stdout.queue followed by stdout.flush()?; and this new execute!(io::stdout(), BeginSynchronizedUpdate)?; functionality? Could they both work together, or if I abandon using the queue command and simply pause rendering at the start of the function and resume at the end maybe I could get better performance and less chance of tearing or jitter?

Thanks for any input you may have!

My render function is very basic, no async, I currently have the following:

pub fn render_screen(&mut self, mut stdout: &Stdout) -> Result<()> {
        stdout.execute(terminal::Clear(terminal::ClearType::All))?;

        // print the wall
        for y in 0..self.screen_size.y {
            for x in 0..self.screen_size.x {
                if (y == 0 || y == self.screen_size.y - 1)
                    || (x == 0 || x == self.screen_size.x - 1)
                {
                    stdout
                        .queue(cursor::MoveTo(y as u16, x as u16))?
                        .queue(style::PrintStyledContent("█".grey()))?;
                }
            }
        }

        // print the zombies
        for zombie in self.zombies.iter() {
            std::thread::sleep(std::time::Duration::from_millis(50));
            stdout
                .queue(cursor::MoveTo(
                    zombie.position.y as u16,
                    zombie.position.x as u16,
                ))?
                .queue(style::PrintStyledContent("z".green()))?;
        }

        // print the hero
        for hero in self.heroes.iter() {
            stdout
                .queue(cursor::MoveTo(
                    hero.position.y as u16,
                    hero.position.x as u16,
                ))?
                .queue(style::PrintStyledContent("h".red()))?;
        }

        // draw screen from queued buffer
        stdout.flush()?;
        Ok(())
    }
jcdickinson commented 11 months ago

You should probably use them together. The queue is about efficiency, this command prevents "terminal tearing" (almost like vsync tearing). Issue/queue this at the start of your frame, and terminate it at the end.