프로그래밍/C

C] 콘솔 프로그램 플리커 현상 개선하기 (더블 버퍼링 & 오프스크린 버퍼)

eteo 2025. 1. 5. 22:54

 

 

 

콘솔 화면을 주기적으로 갱신하는 프로그램을 개발 시 화면이 깜빡이는 플리커(flicker) 현상이 발생할 수 있다. 이번 글에서는 이런 플리커 현상을 개선하기 위한 전략을 알아보자.

 

 

먼저 대략 60fps로 7세그먼트 형식의 디지털시계를 출력하는 C언어 프로그램을 작성해보았다. 프로그램을 실행시키면 아래 이미지 처럼 화면이 깜빡이면서 출력되는 것을 볼 수 있다.

 

 

 

 

플리커 현상의 원인은?

 

플리커 현상은 화면 갱신 과정에서 발생하는 중간 상태가 사용자에게 노출되면서 발생한다. 아래 코드를 보면 system("cls")를 사용해 화면을 완전히 지운 후, 초기화된 화면에 새로운 데이터를 출력하고 있다. 이 과정이 빠르게 반복되면서 빈 화면과 새 데이터를 그리는 중간 상태가 짧은 시간 동안 화면에 보여지게 되고 사용자는 이를 깜빡임으로 인식하는 것이다.

 

아래 코드는 플리커 현상을 발생시키는 코드이다.

 

#include <stdio.h>
#include <time.h>
#include <Windows.h>
#include <string.h>

#define WIDTH 8
#define HEIGHT 7

// 7 segment 숫자
const char* SEGMENTS[10][HEIGHT] = {
	{"■■■■■", "■    ■", "■    ■", "■    ■", "■    ■", "■    ■", "■■■■■"}, // 0
	{"  ■  ", " ■■  ", "  ■  ", "  ■  ", "  ■  ", "  ■  ", " ■■■ "},       // 1
	{"■■■■■", "     ■", "     ■", "■■■■■", "■     ", "■     ", "■■■■■"}, // 2
	{"■■■■■", "     ■", "     ■", "■■■■■", "     ■", "     ■", "■■■■■"}, // 3
	{"■    ■", "■    ■", "■    ■", "■■■■■", "     ■", "     ■", "     ■"}, // 4
	{"■■■■■", "■     ", "■     ", "■■■■■", "     ■", "     ■", "■■■■■"}, // 5
	{"■■■■■", "■     ", "■     ", "■■■■■", "■    ■", "■    ■", "■■■■■"}, // 6
	{"■■■■■", "     ■", "     ■", "    ■ ", "   ■  ", "   ■  ", "   ■  "}, // 7
	{"■■■■■", "■    ■", "■    ■", "■■■■■", "■    ■", "■    ■", "■■■■■"}, // 8
	{"■■■■■", "■    ■", "■    ■", "■■■■■", "     ■", "     ■", "■■■■■"}  // 9
};

// 콜론
const char* COLON[HEIGHT] = { "     ", "  ■  ", "  ■  ", "     ", "  ■  ", "  ■  ", "     " };

// 화면 지우는 함수
void clear_screen() {
	system("cls");
}

// 현재 시간 가져오는 함수
void get_current_time(int* hour, int* minute, int* second) {
	time_t t = time(NULL);
	struct tm* tm_info = localtime(&t);

	*hour = tm_info->tm_hour;
	*minute = tm_info->tm_min;
	*second = tm_info->tm_sec;
}

// x, y 위치에서 세븐 세그먼트를 그리는 함수
void draw_segment(const char* segment[], int x, int y) {
	for (int i = 0; i < HEIGHT; i++) {
		COORD coord = { x, y + i };
		SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
		printf("%s", segment[i]);
	}
}

// 시간을 출력하는 함수
void draw_time(int hour, int minute, int second) {
	char buffer[9];
	sprintf(buffer, "%02d:%02d:%02d", hour, minute, second); // HH:MM:SS 형식

	for (int i = 0; i < 8; i++) {
		if (buffer[i] == ':') {
			draw_segment(COLON, 10 + (i * WIDTH), 5); // 콜론 출력
		}
		else {
			int digit = buffer[i] - '0';
			draw_segment(SEGMENTS[digit], 10 + (i * WIDTH), 5); // 숫자 출력
		}
	}
}

int main() {
	int hour, minitue, second;

	while (1) {
		// 현재 시간 가져오기
		get_current_time(&hour, &minitue, &second);

		// 화면 초기화
		clear_screen();

		// 시간 그리기
		draw_time(hour, minitue, second);

		// 16ms 대기
		Sleep(16);
	}

	return 0;
}

 

 

 

 

 

 


 

다음은 더블 버퍼링 기법을 사용해서 플리커 현상을 개선한 프로그램 화면이다. 보시다시피 깜빡임이 전혀 느껴지지 않는다.

 

 

 

 

Double Buffering

 

더블 버퍼링은 두 개의 화면 버퍼(출력 가능한 버퍼)를 번갈아 사용하는 방식이다. 새 데이터를 작성할 때는 비활성 버퍼(Back Buffer)에 작업을 수행하며, 작업이 완료되면 활성 버퍼(Active Buffer)와 비활성 버퍼를 교체(Flipping)한다. 이렇게 교체된 활성 버퍼가 사용자에게 출력되므로, 화면 갱신 중 발생하는 중간 상태가 사용자에게 노출되지 않고, 항상 완성된 데이터만 화면에 표시되기 때문에 깜빡임 없이 부드러운 화면 전환이 가능하다.

 

 

아래 코드가 더블 버퍼링을 사용해서 플리커 현상을 개선한 코드이다.

 

#include <stdio.h>
#include <Windows.h>
#include <time.h>

#define WIDTH 8
#define HEIGHT 7

// 더블 버퍼링 구조체
typedef struct {
	HANDLE buffers[2];    // 두 개의 콘솔 버퍼 핸들
	int current;          // 현재 활성 버퍼의 인덱스
} DoubleBuffer;

// 세븐 세그먼트 숫자 데이터
const char* SEGMENTS[10][HEIGHT] = {
	{"■■■■■", "■    ■", "■    ■", "■    ■", "■    ■", "■    ■", "■■■■■"},
	{"  ■  ", " ■■  ", "  ■  ", "  ■  ", "  ■  ", "  ■  ", " ■■■ "},
	{"■■■■■", "     ■", "     ■", "■■■■■", "■     ", "■     ", "■■■■■"},
	{"■■■■■", "     ■", "     ■", "■■■■■", "     ■", "     ■", "■■■■■"},
	{"■    ■", "■    ■", "■    ■", "■■■■■", "     ■", "     ■", "     ■"},
	{"■■■■■", "■     ", "■     ", "■■■■■", "     ■", "     ■", "■■■■■"},
	{"■■■■■", "■     ", "■     ", "■■■■■", "■    ■", "■    ■", "■■■■■"},
	{"■■■■■", "     ■", "     ■", "    ■ ", "   ■  ", "   ■  ", "   ■  "},
	{"■■■■■", "■    ■", "■    ■", "■■■■■", "■    ■", "■    ■", "■■■■■"},
	{"■■■■■", "■    ■", "■    ■", "■■■■■", "     ■", "     ■", "■■■■■"}
};

// 콜론 데이터
const char* COLON[HEIGHT] = { "     ", "  ■  ", "  ■  ", "     ", "  ■  ", "  ■  ", "     " };

// 현재 시간 가져오는 함수
void get_current_time(int* hour, int* minute, int* second) {
	time_t t = time(NULL);
	struct tm* tm_info = localtime(&t);

	*hour = tm_info->tm_hour;
	*minute = tm_info->tm_min;
	*second = tm_info->tm_sec;
}

// 더블 버퍼링 초기화
void initialize_double_buffer(DoubleBuffer* db) {
	db->buffers[0] = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
	db->buffers[1] = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
	db->current = 0;

	if (db->buffers[0] == INVALID_HANDLE_VALUE || db->buffers[1] == INVALID_HANDLE_VALUE) {
		fprintf(stderr, "Failed to create console buffers.\n");
		exit(1);
	}

	// 커서 숨기기
	CONSOLE_CURSOR_INFO cursorInfo = { 1, FALSE };
	SetConsoleCursorInfo(db->buffers[0], &cursorInfo);
	SetConsoleCursorInfo(db->buffers[1], &cursorInfo);
}

// 더블 버퍼링 해제
void release_double_buffer(DoubleBuffer* db) {
	for (int i = 0; i < 2; i++) {
		if (db->buffers[i] != INVALID_HANDLE_VALUE) {
			CloseHandle(db->buffers[i]);
		}
	}
}

// 활성 버퍼 가져오기
HANDLE get_active_buffer(DoubleBuffer* db) {
	return db->buffers[db->current];
}

// 비활성 버퍼 가져오기
HANDLE get_inactive_buffer(DoubleBuffer* db) {
	return db->buffers[1 - db->current];
}

// 버퍼 전환
void flip_buffers(DoubleBuffer* db) {
	db->current = 1 - db->current;
	SetConsoleActiveScreenBuffer(db->buffers[db->current]);
}

// 버퍼 초기화
void clear_buffer(HANDLE buffer) {
	CONSOLE_SCREEN_BUFFER_INFO csbi;
	GetConsoleScreenBufferInfo(buffer, &csbi);

	DWORD written;
	FillConsoleOutputCharacter(buffer, ' ', csbi.dwSize.X * csbi.dwSize.Y, (COORD) { 0, 0 }, & written);
	SetConsoleCursorPosition(buffer, (COORD) { 0, 0 });
}

// 세그먼트 그리기
void draw_segment(const char* segment[], HANDLE buffer, COORD start) {
	DWORD written;
	for (int i = 0; i < HEIGHT; i++) {
		COORD coord = { start.X, start.Y + i };
		WriteConsoleOutputCharacter(buffer, segment[i], (DWORD)strlen(segment[i]), coord, &written);
	}
}

// 시간 그리기
void draw_to_buffer(HANDLE buffer, int hour, int minute, int second) {
	char time_str[9];
	sprintf(time_str, "%02d:%02d:%02d", hour, minute, second);

	for (int i = 0; i < 8; i++) {
		COORD start = { 10 + (i * WIDTH), 5 };
		if (time_str[i] == ':') {
			draw_segment(COLON, buffer, start);
		}
		else {
			int digit = time_str[i] - '0';
			draw_segment(SEGMENTS[digit], buffer, start);
		}
	}
}

int main() {
	DoubleBuffer db;
	initialize_double_buffer(&db);

	int hour, minute, second;

	while (1) {
		// 현재 시간 가져오기
		get_current_time(&hour, &minute, &second);

		// 비활성 버퍼 초기화 및 그리기
		HANDLE inactive = get_inactive_buffer(&db);
		clear_buffer(inactive);
		draw_to_buffer(inactive, hour, minute, second);

		// 버퍼 전환
		flip_buffers(&db);

        // 16ms 대기
		Sleep(16);
	}

	release_double_buffer(&db);

	return 0;
}

 

 

 

 

 

 

 


아래는 더블 버퍼링이 아니라 메모리 상의 준비용 버퍼에 데이터를 작성한 후 완성된 내용을 한 번에 콘솔로 출력하는 방식으로 플리커 현상을 개선해본 것이다. 역시 깜빡임이 전혀 느껴지지 않는다.

 

 

 

One Off-screen Buffering

 

단일 오프스크린 버퍼를 사용하는 방법으로 화면 버퍼(출력 가능한 버퍼)는 하나만 활성화되어 있고, 준비용 메모리 버퍼에 데이터를 먼저 작성한 뒤, 이를 화면 버퍼로 복사하는 방식이다. 이 방식 또한 오프스크린 버퍼에서 작업이 완료된 데이터만 화면에 표시되므로 플리커 현상이 개선된다.

 

 

 


아래 코드가 단일 오프 스크린 버퍼를 사용해서 플리커 현상을 개선한 코드이다.

 

참고로 아래 코드에선 메모리 준비 버퍼로 CHAR_INFO 구조체를 사용하고 있는데 CHAR_INFO는 텍스트 문자와 속성을 저장할 수 있는 구조체로 유니코드와 아스키 두 가지 문자 형식을 둘 다 지원한다.

위 코드에서 7세그먼트 출력시 사용된 ■ 문자는 유니코드 문자이므로 이를 출력하려면 유니코드 문자 집합을 사용하고 출력 데이터를 wchar_t 형식으로 준비한 뒤 CHAR_INFO의 UnicodeChar 필드에 할당하면 ■ 문자를 사용할 수 있다. 다만, 아래 코드에서는 간단히 작성하기 위해 ■ 대신 #로 대체하였다.

 

#include <stdio.h>
#include <Windows.h>
#include <time.h>

#define WIDTH 8	// 글자간격 포함 너비
#define HEIGHT 7

// 7 segment 숫자 데이터
const char* SEGMENTS[10][HEIGHT] = {
	{"#####", "#    #", "#    #", "#    #", "#    #", "#    #", "#####"}, // 0
	{"     #", "     #", "     #", "     #", "     #", "     #", "     #"}, // 1
	{"#####", "     #", "     #", "#####", "#     ", "#     ", "#####"},   // 2
	{"#####", "     #", "     #", "#####", "     #", "     #", "#####"},   // 3
	{"#    #", "#    #", "#    #", "#####", "     #", "     #", "     #"}, // 4
	{"#####", "#     ", "#     ", "#####", "     #", "     #", "#####"},   // 5
	{"#####", "#     ", "#     ", "#####", "#    #", "#    #", "#####"},   // 6
	{"#####", "     #", "     #", "     #", "     #", "     #", "     #"}, // 7
	{"#####", "#    #", "#    #", "#####", "#    #", "#    #", "#####"},   // 8
	{"#####", "#    #", "#    #", "#####", "     #", "     #", "#####"}    // 9
};

// 콜론 데이터
const char* COLON[HEIGHT] = { "     ", "  #  ", "     ", "     ", "  #  ", "     ", "     " };

// CHAR_INFO 기반 메모리 버퍼
CHAR_INFO screenBuffer[80 * 25];
COORD bufferSize = { 80, 25 };
COORD bufferCoord = { 0, 0 };
SMALL_RECT writeRegion = { 0, 0, 79, 24 };

// 현재 시간 가져오기
void get_current_time(int* hour, int* minute, int* second) {
	time_t t = time(NULL);
	struct tm* tm_info = localtime(&t);

	*hour = tm_info->tm_hour;
	*minute = tm_info->tm_min;
	*second = tm_info->tm_sec;
}

// 화면 초기화
void clear_screen() {
	for (int i = 0; i < 80 * 25; i++) {
		screenBuffer[i].Char.AsciiChar = ' ';
		screenBuffer[i].Attributes = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
	}
}

// 세그먼트 그리기
void draw_segment(const char* segment[], int x, int y) {
	for (int i = 0; i < HEIGHT; i++) {
		int pos = (y + i) * 80 + x; // CHAR_INFO 배열의 1차원 인덱스 계산
		for (int j = 0; j < WIDTH; j++) {
			if (segment[i][j] != ' ') { // 비어 있지 않은 경우만 그리기
				screenBuffer[pos + j].Char.AsciiChar = segment[i][j];
				screenBuffer[pos + j].Attributes = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
			}
		}
	}
}

// 시간 그리기
void draw_time(int hour, int minute, int second) {
	char buffer[9];
	sprintf(buffer, "%02d:%02d:%02d", hour, minute, second);

	for (int i = 0; i < 8; i++) {
		int x = 10 + (i * WIDTH); // 글자 간격 추가
		int y = 5;

		if (buffer[i] == ':') {
			draw_segment(COLON, x, y);
		}
		else {
			int digit = buffer[i] - '0';
			draw_segment(SEGMENTS[digit], x, y);
		}
	}
}

// 메모리 버퍼를 콘솔로 출력
void render_screen(HANDLE hConsole) {
	WriteConsoleOutput(hConsole, screenBuffer, bufferSize, bufferCoord, &writeRegion);
}

int main() {
	// 콘솔 핸들 가져오기
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

	// 콘솔 커서 숨기기
	CONSOLE_CURSOR_INFO cursorInfo = { 1, FALSE };
	SetConsoleCursorInfo(hConsole, &cursorInfo);

	int hour, minute, second;

	while (1) {
		// 현재 시간 가져오기
		get_current_time(&hour, &minute, &second);

		// 메모리 버퍼 초기화
		clear_screen();

		// 메모리 버퍼에 시간 그리기
		draw_time(hour, minute, second);

		// 콘솔로 출력
		render_screen(hConsole);

		// 16ms 대기
		Sleep(16);
	}

	return 0;
}