2

I have a tui application written in C where the simplified mainloop look something like this:

enterrawmode();
while(1) {
    clearscreen(); //I clear the screen using a ANSI code. 
    checkforresizeterminal(); //Using ioctl
    draw();
    getinput();
    usleep(10000);
}

When running it in some terminals, it is flickering a lot, while other programs, such as vim/neovim are updating without any flickering. How are they achieving this?

Also, when running the program, the cpu usage is high. Is there a more efficient way to check if the terminal was resized (rather than checking for it every time the screen is drawn)? When nvim is running, it is at 0% cpu usage when idle, and it goes up when I resize the terminal. How is it getting low latency input and resizing without very high cpu usage? How does it "know" when to update? Can curses do this, if so how? Is there any way to get low latency input and respond to terminal resize with low latency without high cpu usage and checking for it in a loop such as this?

adsf
  • 59
  • 5
  • You could use a flag to tell you whether or not an update to the screen is actually needed, which will skip most of the clearscreen(); and draw() calls. `if (repaint_needed) { clearscreen(); draw(); }` – Dave S Jan 10 '23 at 21:56
  • 3
    You don't call `clearscreen`, ever. You repaint the entire screen each time. Or, for faster update, you keep track of what's on the screen in one memory buffer, and what you want in another memory buffer. Then only update the characters that need to be changed. – user3386109 Jan 10 '23 at 22:13
  • How could I deal with ansi codes for colors which uses ansi codes and unicode characters which takes up more memory that other characters? – adsf Jan 10 '23 at 22:24
  • Perhaps download the source for vim and neovim from github, and see exactly how they do it. The links to the repositories can be found in [this wikipedia article](https://en.wikipedia.org/wiki/Vim_(text_editor)). – user3386109 Jan 10 '23 at 22:39
  • 1
    I believe that `ncurses` is exactly right for this. You draw on a virtual screen, the library knows what's there now and knows how to update optimally. Input handling is typically done properly as well, including support for function keys and the like. Well worth looking into this. – Steve Friedl Jan 11 '23 at 01:40
  • Seems like Synchronized rendering might be the solution, for example take a look through this link and see how the st terminal does it: https://st.suckless.org/patches/sync/. – Maximilian Ballard Jan 20 '23 at 11:37

2 Answers2

0

Typically, do double buffering. One buffer stores what is on the screen. Your program updates the content of the second buffer.

Each, 10000us check for differences between the buffers. If there is a difference, only update the differences and update the first buffer to the content of the second one. If there are no differences, there is nothing to do.

checkforresizeterminal(); //Using ioctl

Handle SIGWINCH signal instead.

Is there any other way to get keyboard input without checking for it in a loop?

Typically, create a thread that does blocking read() on the input. That thread communicates with the second display buffer and with anything else to update the state depending on user input. Remember about proper locking.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • Is there any other way to get keyboard input without checking for it in a loop? – adsf Jan 13 '23 at 14:38
  • https://en.wikipedia.org/wiki/Asynchronous_I/O . What you are doing is https://en.wikipedia.org/wiki/Asynchronous_I/O#Polling . You can do https://en.wikipedia.org/wiki/Asynchronous_I/O#Light-weight_processes_or_threads and implement https://en.wikipedia.org/wiki/Synchronization_(computer_science)#Implementation_of_Synchronization between threads. – KamilCuk Jan 13 '23 at 14:41
  • Is there any way to to it only using one thread? – adsf Jan 13 '23 at 15:01
  • Do I need to add two threads? One to wait for SIGWINCH, one to wait for keypress and one main thread that draws to the screen? Or is there some kind of getevent functionality that I can use like ncurses' getch? – adsf Jan 13 '23 at 16:43
  • `Is there any way to to it only using one thread?` Yes, you can `select` with a timeout https://en.wikipedia.org/wiki/Asynchronous_I/O#Select(/poll)_loops . Using a separate thread sounds more flexible in the long run. `One to wait for SIGWINCH` SIGWINCH is a signal that your program receives. `is there some kind of getevent` You can write to evetnfd from a signal handler and receive that from event. I.e. https://en.wikipedia.org/wiki/Event-driven_programming . The main loop for event-driven programming becomes complicated fast. Consider threads. – KamilCuk Jan 13 '23 at 16:52
0

The terminal is really fast, but you need to understand the buffering logic. When you're sending render instruction to the terminal, you're basically writing characters and escape codes to a pty socket. A pty socket is very similar to a TCP socket, just a bit more specialised.

If you're updating many things at once, you need to make sure your writes are buffered and flushed properly, such that multiple instructions can be handled by the terminal emulator in one read() system call. If you flush the socket with every escape code instruction, such as the case with unbuffered writes, the terminal emulator on the other side might have to do one read() syscall to read a single character, and perform the screen update, thousands of times, to render a single logical frame.

Instead, you want your draw commands for a single frame, including the command to clear screen, to be queued/buffered and flushed in a single write() syscall.

Lie Ryan
  • 62,238
  • 13
  • 100
  • 144