0

I'm trying to write some code which would allow to render 3D graphics in console using characters and escape sequences (for color). I need it for one specific program I want to write, but, if possible, I would like to make it more universal. I'm experiencing something like screen tearing and I want to get rid of it (that the whole screen would be printed "at once"). The test is simply displaying screen filled with spaces with wite and black background (one full white frame then one full black one) in one second interval.

I have tried:

  1. At the begging I thought about line buffering on stdout. Tried both disabling it and creating full buffor with size sufficient enough to hold every char on the screen. Second option provides better results, and by that I mean that less frames are teared, but they still are.

  2. I thought it might be a problem with my terminal emulator (this question gave me the idea) so I started to mess around with other ones. I've got best result with Kitty but it's not there yet.

  3. The next thing was to mess with Kitty configuration. I've noticed that if I would increase the input_delay setting to about 20 ms the problem would be almost gone. Just few of, and not every frame would be teared.

So, I came into the conclusion that in fact terminal emulators (or at least kitty) are being too fast and there might be some sort of race condition here, where buffer is not flushed yet fully and TE display both what was partially flushed and is part of old frame. Am I wrong? If not is there any way I can enforce terminals to wait for input to finnish before displaying it, or at least enforce input delay in C?

here is the relevant part of the code: main.c


#include "TermCTRL/termCTRL.h"
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>


int main()
{
  termcell_t cell;
  int k;
  uint16_t x,y;
  termCTRL_get_term_size(&x, &y);
  sleep(1);
  termCTRL_init();
  uint8_t a = 0;
  for(k=0; k<200; k++)
    {
      a^=255;

      cell.bg.B = a;
      cell.bg.G = a;
      cell.bg.R = a;
      cell.fg.B = a;
      cell.fg.G = a;
      cell.fg.R = a;
      cell.symbol[0] = ' '; //symbol is in fact a string, because I want to use UTF chars too
      cell.symbol[1] = '\0';

      for(int xd=0; xd<x; xd++)
        for(int yd=0; yd<y; yd++)
        {
           termCTRL_load_termcell(xd, yd, &cell); 
        }
      termCTRL_update_screen();
      sleep(1);
    }
 termCTRL_close();
 return 0;
}

termCTRL.h

#pragma once 
#include <stdint.h>

#define INPLACE_TERMCELL(FG_R, FG_G, FG_B, BG_R, BG_G, BG_B, SYMBOL)           \
  (termcell_t) { {FG_R, FG_G, FG_B}, {BG_R, BG_G, BG_B}, SYMBOL }
#define termCTRL_black_fill_screen()        \
  termCTRL_fill_screen(&INPLACE_TERMCELL(0, 0, 0, 0, 0, 0, " "))

typedef struct termcell_color_t
{
  uint16_t R;
  uint16_t G;
  uint16_t B;
} termcell_color_t;


typedef struct termcell_t
{
  termcell_color_t fg;
  termcell_color_t bg;
  char symbol[4];
} termcell_t;


typedef enum termCTRL_ERRNO
  {
   termCTRL_OUT_OF_BORDER = -2,
   termCTRL_INVALID_TERMCELL = -1,
   termCTRL_INTERNAL_ERROR = 0,
   termCTRL_OK = 1,
  } termCTRL_ERRNO;

void termCTRL_init();
void termCTRL_close();
void termCTRL_get_term_size(uint16_t *col, uint16_t *row);
termCTRL_ERRNO termCTRL_load_termcell(uint16_t x, uint16_t y, termcell_t *in);
void termCTRL_update_screen();
termCTRL_ERRNO termCTRL_fill_screen(termcell_t *cell);

termCTRL.c

#include "termCTRL.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define CONVERTED_TERMCELL_SIZE 44
#define CAST_SCREEN_TO_BUFFER                       \
  char (*screen_buffer)[term_xsize][term_ysize][CONVERTED_TERMCELL_SIZE]; \
  screen_buffer = _screen_buffer

static void *_screen_buffer = NULL;
static uint16_t term_xsize, term_ysize;
static char *IO_buff = NULL;

void termCTRL_get_term_size(uint16_t *col, uint16_t *row)
{
  struct winsize w;

  ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
  *col = w.ws_col;
  *row = w.ws_row;
}

void int_decompose(uint8_t in, char *out)
{
  uint8_t x = in/100;

  out[0] = x + '0';
  in -= x*100;
  x = in/10;
  out[1] = x + '0';
  in -= x*10;
  out[2] = in + '0';
}

termCTRL_ERRNO termCTRL_move_cursor(uint16_t x, uint16_t y)
{
  char mov_str[] = "\x1b[000;000H";
  if(x<term_xsize && y<term_ysize)
    {
      int_decompose(y, &mov_str[2]);
      int_decompose(x, &mov_str[6]);
      if(fputs(mov_str, stdout) == EOF) return termCTRL_INTERNAL_ERROR;
      else return termCTRL_OK;
    }
  else
    {
      return termCTRL_OUT_OF_BORDER;
    }
}


termCTRL_ERRNO termCTRL_load_termcell(uint16_t x, uint16_t y, termcell_t *in)
{
  CAST_SCREEN_TO_BUFFER;

  if(in == NULL) return termCTRL_INVALID_TERMCELL;
  if(x >= term_xsize || y >= term_ysize) return termCTRL_OUT_OF_BORDER;
  //because screen buffer was initialized, it is only needed to replace RGB values and symbol.
  //whole escape sequence is already there
  int_decompose(in->fg.R, &(*screen_buffer)[x][y][7]);
  int_decompose(in->fg.G, &(*screen_buffer)[x][y][11]);
  int_decompose(in->fg.B, &(*screen_buffer)[x][y][15]);
  int_decompose(in->bg.R, &(*screen_buffer)[x][y][26]);
  int_decompose(in->bg.G, &(*screen_buffer)[x][y][30]);
  int_decompose(in->bg.B, &(*screen_buffer)[x][y][34]);
  strcpy(&(*screen_buffer)[x][y][38], in->symbol); //copy symbol, note that it could be UTF char
  return termCTRL_OK;
}

termCTRL_ERRNO termCTRL_fill_screen(termcell_t *cell)
{
  uint16_t x, y;
  termCTRL_ERRNO ret;
  for(y=0; y<term_ysize; y++)
    for(x=0; x<term_xsize; x++)
      {
    ret = termCTRL_load_termcell(x, y, cell);
    if(ret != termCTRL_OK)
      return ret;
      }
  return ret;
}


void termCTRL_update_screen()
{
  uint16_t x, y;
  CAST_SCREEN_TO_BUFFER;
  termCTRL_move_cursor(0, 0);
  for(y=0; y<term_ysize-1; y++)
    {
      for(x=0; x<term_xsize; x++)
    fputs((*screen_buffer)[x][y], stdout);
      fputs("\n", stdout);
    }
  //last line got special treatment because it can't have \n 
  for(x=0; x<term_xsize; x++)
    fputs((*screen_buffer)[x][y], stdout);

  fflush(stdout);
}

void termCTRL_init()
{
  uint16_t x, y;
  termCTRL_get_term_size(&term_xsize, &term_ysize);
  IO_buff = calloc(term_xsize*term_ysize, CONVERTED_TERMCELL_SIZE);
  setvbuf(stdout, IO_buff, _IOFBF, term_xsize*term_ysize*CONVERTED_TERMCELL_SIZE);
  _screen_buffer = calloc(term_xsize*term_ysize, CONVERTED_TERMCELL_SIZE);
  fputs("\e[?25l", stdout); //hide cursor
  fputs("\x1b[2J", stdout); //clear screen
  CAST_SCREEN_TO_BUFFER;
  for(y=0; y<term_ysize; y++)
    for (x=0; x<term_xsize; x++)
      sprintf( (*screen_buffer)[x][y], "\x1b[38;2;200;200;000m\x1b[48;2;000;000;000m ");
  termCTRL_update_screen();

}

void termCTRL_close()
{
  free(_screen_buffer);
  setvbuf(stdout, NULL, _IONBF, 0);
  free(IO_buff);
  printf("\e[?25h"); //show cursor
  printf("\x1b[m"); //reset colors
  printf("\x1b[2J"); //clear screen
}

Qpieloq
  • 33
  • 7
  • Some terminals have a feature to switch to/from an alternate terminal buffer, often used by editors/"fullscreen" apps to leave your shell session visible when you suspend/exit. In many ways this is like page flipping, which is the canonical solution to your problem. But I'm not aware of a way to write to the inactive buffer. – R.. GitHub STOP HELPING ICE Mar 14 '20 at 20:10
  • 1
    I suspect you'll find that this is a problem with no satisfactory solution. – R.. GitHub STOP HELPING ICE Mar 14 '20 at 20:11
  • Ok, I will check out this alternate terminal buffor. Thanks. – Qpieloq Mar 15 '20 at 12:33

0 Answers0