Since using something like this \x1b[38;2;255;242;0m to change the color for each character makes printing to console really slow, something around 2–3 fps and I need to have at least 30 fps, so just wanted to ask if there is any faster alternative way to change the color that I can use?

Note: I just need it to work on Windows, in case that you have a specific solution that is not cross-platform.


Update: As an example, you can check this sample code:

int main() {
  string text = "\x1b[48;2;0;255;255m";
  for (int i = 0; i < 160; i++)
  {
    for (int j = 0; j < 512; j++)
    {
      // This line is the problem
      // text += "\x1b[48;2;0;255;255mX";
      text += "X";
    }
    text += "\n";
  }
  text += "\x1b[0m";
  
  // You just need to measure the time of running only this line
  cout << text;
}

If I run this code on my (slow) computer, I will get ~15 fps if I only set the color once, but if I want to change the color for each character (with the commented line that you can see in the code above), I will get ~1-2 fps.

But after more testing and sending broken data to the console, I figured out that it still takes the same amount of time. So perhaps the problem is not related to parsing VT sequence, and it's about the size of data that I send. Because if I want to have different color for each character, then the size of data will increase ~20 time.

1

There are 1 best solutions below

1
On

I suggest you first make sure the escape sequences are actually the bottleneck. I'm able to write 140+ frames per second (25 rows by 80 columns of ASCII text, plus 19 bytes of escape sequence per character, plus a newline per row, plus the escape sequence to home the cursor per frame).

// Quick hack to determine feasibility of high frame rates to console.
#define _CRT_SECURE_NO_WARNINGS  // to allow strcpy without a warning
#include <chrono>
#include <iostream>
#include <string>
#include <string_view>

constexpr std::string_view character_sequence = "\x1B[38;2;rrr;ggg;bbbmX";
constexpr auto bytes_per_character = character_sequence.size();

void ClearScreen() {
    static constexpr std::string_view sequence = "\x1B[2";
    std::cout.write(sequence.data(), sequence.size());
}

void Home() {
    static constexpr std::string_view sequence = "\x1B[H";
    std::cout.write(sequence.data(), sequence.size());
}

void FillWindowRaw(char *text, int rows, int cols) {
    Home();
    std::cout.write(text, rows * (bytes_per_character*cols + 1));
}

int main() {
    constexpr int kFrames = 1000;
    constexpr int kRows = 25;
    constexpr int kCols = 80;
    char text1[kRows*(bytes_per_character*kCols + 1)];
    char text2[kRows*(bytes_per_character*kCols + 1)];
    char *p1 = text1;
    char *p2 = text2;
    for (int row = 0; row < kRows; ++row) {
        for (int col = 0; col < kCols; ++col) {
            std::strcpy(p1, "\x1B[38;2;255;255;255mX");
            p1 += bytes_per_character;
            std::strcpy(p2, "\x1B[38;2;000;255;000mO");
            p2 += bytes_per_character;
        }
        *p1++ = '\n';
        *p2++ = '\n';
    }

    const auto started = std::chrono::high_resolution_clock::now();
    ClearScreen();
    for (int i = 0; i < kFrames/2; ++i) {
        FillWindowRaw(text1, kRows, kCols);
        FillWindowRaw(text2, kRows, kCols);
    }
    const auto finished = std::chrono::high_resolution_clock::now();
    const auto elapsed = finished - started;
    const auto elapsed_ms =
        std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
    std::cout << "\x1B[37m"; // restore foreground color
    std::cout << "Frame:   " << kFrames << '\n'
              << "Elapsed: " << elapsed_ms << " ms\n";
    if (elapsed_ms > 0) {
        const auto fps = 1000.0 * kFrames / elapsed_ms;
        std::cout << "FPS:     " << fps << std::endl;
    }

    return 0;
}

I suspect your bottleneck is elsewhere. Perhaps you're using lots of individual calls to send the data to the console or using formatted output to generate the escape sequences on the fly. If that's the case, you might consider building up a screen-sized buffer so that the output is just one call to std::cout.write per frame. Rewriting the in-memory buffer is likely to be much faster than trying to generate the byte stream on the fly.


That said, the Win32 API does have a programmatic console interface that doesn't put the rendering options into the character sequence. But you'd have to do some profiling to see if that will be fast enough. Microsoft has been decoupling the terminal emulation from the console host so it's now easy to get escape sequence support in terminal windows.