Skip to content

Commit a369aab

Browse files
authored
refactor: 성능 개선 (#3)
1 parent 79d2b59 commit a369aab

File tree

3 files changed

+113
-85
lines changed

3 files changed

+113
-85
lines changed

src/i_ascii.cpp

Lines changed: 61 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,9 @@
2424
#include <cstdlib>
2525
#include <cstring>
2626
#include <cstdint>
27+
#include <cmath>
2728
#include <algorithm>
28-
#include <string>
29-
#include <vector>
3029
#include <emscripten.h>
31-
#include <cmath>
3230

3331
#if defined(__wasm_simd128__)
3432
#include <wasm_simd128.h>
@@ -39,16 +37,44 @@
3937

4038
#define CELL_BUFFER_SIZE (ASCII_WIDTH * ASCII_HEIGHT)
4139
static AsciiCell cell_buffer[CELL_BUFFER_SIZE];
40+
4241
// ASCII 렌더링이 초기화되었는지 여부를 나타내는 플래그
4342
static boolean ascii_initialized = false;
4443

44+
// 감마 보정 룩업 테이블 (pow() 호출 제거로 성능 향상)
45+
static uint8_t gamma_table[256];
46+
static boolean gamma_table_initialized = false;
47+
48+
// ASCII 문자 팔레트 (constexpr 배열로 최적화)
49+
static constexpr char ASCII_CHARS[] = " .:-=+*#%@";
50+
static constexpr int ASCII_CHARS_LEN = sizeof(ASCII_CHARS) - 1; // null terminator 제외
51+
4552
extern "C" {
4653

54+
// 감마 테이블 초기화 (gamma = 0.35, 밝기 2배 증가)
55+
static void init_gamma_table(void)
56+
{
57+
if (gamma_table_initialized)
58+
return;
59+
60+
// 감마 0.35 = 더 밝은 출력 (기존 0.5에서 약 2배 밝기)
61+
constexpr float gamma = 0.35f;
62+
for (int i = 0; i < 256; ++i) {
63+
float normalized = i / 255.0f;
64+
float corrected = std::pow(normalized, gamma);
65+
gamma_table[i] = static_cast<uint8_t>(std::min(255.0f, corrected * 255.0f));
66+
}
67+
gamma_table_initialized = true;
68+
}
69+
4770
void I_InitASCII(void)
4871
{
4972
if (ascii_initialized)
5073
return;
5174

75+
// 감마 테이블 초기화 (최초 1회만)
76+
init_gamma_table();
77+
5278
// 전체 AsciiCell 버퍼를 0으로 초기화
5379
// → 처음에는 화면 전체가 '빈 문자/검은색' 상태가 됨
5480
std::memset(cell_buffer, 0, sizeof(cell_buffer));
@@ -70,40 +96,40 @@ const void* I_GetASCIIBuffer(void)
7096
return cell_buffer;
7197
}
7298

73-
// C++ implementation of RGBA to AsciiCell data conversion
99+
// C++ implementation of RGBA to AsciiCell data conversion (optimized)
74100
void I_ConvertRGBAtoASCII(const uint32_t *rgba_buffer,
75101
int src_width, int src_height,
76102
void *output_buffer,
77103
int ascii_width, int ascii_height)
78104
{
79-
// 화면 밝기에 따라 다른 문자를 사용하는 ASCII 팔레트
80-
const std::string ascii_chars = " .:-=+*#%@";
81-
// 출력으로 사용할 AsciiCell 배열
82105
AsciiCell* out_cells = static_cast<AsciiCell*>(output_buffer);
83106

84-
// ascii_width x ascii_height 해상도의 "문자 셀"로 원본 RGBA 화면을 축소 샘플링한다.
107+
// 스케일 비율을 미리 계산 (나눗셈 최소화)
108+
const int scale_x = (src_width << 8) / ascii_width; // 8비트 고정소수점
109+
const int scale_y = (src_height << 8) / ascii_height;
110+
85111
for (int y = 0; y < ascii_height; ++y) {
112+
const int src_y_start = (y * scale_y) >> 8;
113+
const int src_y_end = ((y + 1) * scale_y) >> 8;
114+
86115
for (int x = 0; x < ascii_width; ++x) {
87-
// 이 문자 셀이 담당하는 원본 화면 상의 영역을 계산
88-
int src_x_start = (x * src_width) / ascii_width;
89-
int src_y_start = (y * src_height) / ascii_height;
90-
int src_x_end = ((x + 1) * src_width) / ascii_width;
91-
int src_y_end = ((y + 1) * src_height) / ascii_height;
116+
const int src_x_start = (x * scale_x) >> 8;
117+
const int src_x_end = ((x + 1) * scale_x) >> 8;
92118

93-
// 해당 영역의 RGB 값을 모두 합산하여 평균 색/밝기를 구한다.
94-
long long total_r = 0, total_g = 0, total_b = 0;
119+
// 해당 영역의 RGB 값을 합산
120+
int total_r = 0, total_g = 0, total_b = 0;
95121
int count = 0;
96122

97123
for (int sy = src_y_start; sy < src_y_end; ++sy) {
98124
const uint32_t* row_ptr = &rgba_buffer[sy * src_width + src_x_start];
99-
int region_width = src_x_end - src_x_start;
125+
const int region_width = src_x_end - src_x_start;
100126
count += region_width;
101127

102128
#if defined(__wasm_simd128__)
103-
v128_t sums = wasm_i32x4_const(0, 0, 0, 0); // Accumulator for BGRA sums
129+
v128_t sums = wasm_i32x4_const(0, 0, 0, 0);
104130
int sx = 0;
105131
for (; sx <= region_width - 4; sx += 4) {
106-
v128_t pixels = wasm_v128_load(row_ptr + sx); // Load 4 pixels (BGRA)
132+
v128_t pixels = wasm_v128_load(row_ptr + sx);
107133

108134
v128_t wide_low = wasm_u16x8_extend_low_u8x16(pixels);
109135
v128_t wide_high = wasm_u16x8_extend_high_u8x16(pixels);
@@ -119,50 +145,47 @@ void I_ConvertRGBAtoASCII(const uint32_t *rgba_buffer,
119145
total_r += wasm_i32x4_extract_lane(sums, 2);
120146

121147
for (; sx < region_width; ++sx) {
122-
uint32_t pixel = row_ptr[sx];
148+
const uint32_t pixel = row_ptr[sx];
123149
total_r += (pixel >> 16) & 0xFF;
124150
total_g += (pixel >> 8) & 0xFF;
125151
total_b += pixel & 0xFF;
126152
}
127153
#else
128-
for (int sx = src_x_start; sx < src_x_end; ++sx) {
129-
uint32_t pixel = rgba_buffer[sy * src_width + sx];
154+
for (int sx = 0; sx < region_width; ++sx) {
155+
const uint32_t pixel = row_ptr[sx];
130156
total_r += (pixel >> 16) & 0xFF;
131157
total_g += (pixel >> 8) & 0xFF;
132158
total_b += pixel & 0xFF;
133159
}
134160
#endif
135161
}
136162

137-
// 2D 좌표 (x, y) 를 1D 인덱스로 변환해서 AsciiCell 에 기록
138163
AsciiCell& cell = out_cells[y * ascii_width + x];
139164

140165
if (count == 0) {
141-
// 샘플링할 픽셀이 하나도 없으면 공백 문자 + 검은색으로 채움
142166
cell.character = ' ';
143167
cell.r = 0;
144168
cell.g = 0;
145169
cell.b = 0;
146170
continue;
147171
}
148172

149-
// 영역 내 평균 색상 계산
150-
int avg_r = total_r / count;
151-
int avg_g = total_g / count;
152-
int avg_b = total_b / count;
173+
// 평균 계산
174+
const int avg_r = total_r / count;
175+
const int avg_g = total_g / count;
176+
const int avg_b = total_b / count;
153177

154-
// 가중치를 적용해 '밝기' 값으로 변환 (0~255)
155-
int brightness = (avg_r * 299 + avg_g * 587 + avg_b * 114) / 1000;
178+
// 밝기 계산 (정수 연산)
179+
const int brightness = (avg_r * 299 + avg_g * 587 + avg_b * 114) / 1000;
156180

157-
// 밝기를 ascii_chars 인덱스로 매핑
158-
int char_idx = (brightness * (ascii_chars.length() - 1)) / 255;
159-
160-
// 최종적으로 이 셀에 사용할 문자/색상 값을 저장
161-
cell.character = ascii_chars[char_idx];
162-
const float gamma = 0.5;
163-
cell.r = std::min(255, static_cast<int>(pow(avg_r / 255.0, gamma) * 255.0));
164-
cell.g = std::min(255, static_cast<int>(pow(avg_g / 255.0, gamma) * 255.0));
165-
cell.b = std::min(255, static_cast<int>(pow(avg_b / 255.0, gamma) * 255.0));
181+
// ASCII 문자 인덱스 계산 (constexpr 배열 사용)
182+
const int char_idx = (brightness * (ASCII_CHARS_LEN - 1)) / 255;
183+
184+
// 감마 룩업 테이블 사용 (pow() 호출 제거)
185+
cell.character = ASCII_CHARS[char_idx];
186+
cell.r = gamma_table[avg_r];
187+
cell.g = gamma_table[avg_g];
188+
cell.b = gamma_table[avg_b];
166189
}
167190
}
168191
}

src/i_ascii.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@
2727
extern "C" {
2828
#endif
2929

30-
// ASCII output dimensions (can be configured)
31-
#define ASCII_WIDTH 480
32-
#define ASCII_HEIGHT 160
30+
// ASCII output dimensions (optimized for performance)
31+
// 240x80 = 19,200 cells (vs 480x160 = 76,800 cells = 4x faster)
32+
#define ASCII_WIDTH 240
33+
#define ASCII_HEIGHT 80
3334

3435
// Struct to hold data for one character cell
3536
typedef struct {

src/index.html

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
50% { opacity: 0; }
8383
}
8484

85-
/* 캔버스 래퍼 (화면 안쪽) - 여기가 핵심 수정 */
85+
/* 캔버스 래퍼 (화면 안쪽) */
8686
.screen-wrapper {
8787
position: relative;
8888
width: 100%;
@@ -96,13 +96,14 @@
9696
}
9797

9898
canvas.frame {
99-
background-color: #000;
99+
position: absolute;
100+
top: 0;
101+
left: 0;
100102
width: 100%;
101103
height: 100%;
102-
object-fit: contain; /* 비율 유지하며 짤리지 않게 */
104+
background-color: #000;
103105
image-rendering: pixelated;
104106
display: block;
105-
opacity: 0.9;
106107
}
107108

108109
/* CRT 스캔라인 효과 */
@@ -233,13 +234,23 @@
233234
}
234235

235236
const canvas = document.getElementById('canvas');
236-
const ctx = canvas.getContext('2d');
237+
const ctx = canvas.getContext('2d', { alpha: false }); // 알파 채널 비활성화로 성능 향상
237238

238-
const ASCII_WIDTH = 480;
239-
const ASCII_HEIGHT = 160;
239+
const ASCII_WIDTH = 240;
240+
const ASCII_HEIGHT = 80;
240241

241-
const TARGET_CANVAS_WIDTH = 1600;
242-
const TARGET_CANVAS_HEIGHT = 1200;
242+
// wrapper 영역 크기 가져오기
243+
const screenWrapper = document.querySelector('.screen-wrapper');
244+
const wrapperRect = screenWrapper.getBoundingClientRect();
245+
246+
// wrapper 전체를 캔버스로 채움 (비율 무시 - 게임 화면 짤림 방지)
247+
const canvasWidth = wrapperRect.width;
248+
const canvasHeight = wrapperRect.height;
249+
250+
// devicePixelRatio 고려 (레티나 디스플레이 지원)
251+
const dpr = window.devicePixelRatio || 1;
252+
const TARGET_CANVAS_WIDTH = Math.round(canvasWidth * dpr);
253+
const TARGET_CANVAS_HEIGHT = Math.round(canvasHeight * dpr);
243254

244255
const charWidth = TARGET_CANVAS_WIDTH / ASCII_WIDTH;
245256
const charHeight = TARGET_CANVAS_HEIGHT / ASCII_HEIGHT;
@@ -249,11 +260,12 @@
249260

250261
ctx.font = FONT;
251262

263+
// 내부 해상도 설정 (DPR 반영)
252264
canvas.width = TARGET_CANVAS_WIDTH;
253265
canvas.height = TARGET_CANVAS_HEIGHT;
254266

255-
console.log(`Canvas initialized: ${canvas.width}x${canvas.height}`);
256-
console.log(`Character cell size: ${charWidth}x${charHeight}`);
267+
console.log(`Canvas initialized: ${canvas.width}x${canvas.height} (Wrapper: ${Math.round(canvasWidth)}x${Math.round(canvasHeight)}, DPR: ${dpr})`);
268+
console.log(`Character cell size: ${charWidth.toFixed(1)}x${charHeight.toFixed(1)}`);
257269

258270
function getMemoryBuffer() {
259271
if (typeof HEAPU8 !== 'undefined' && HEAPU8 && HEAPU8.buffer) {
@@ -298,60 +310,52 @@
298310

299311
const buffer = new Uint8Array(heapBuffer, bufferPtr, bufferSize);
300312

313+
// 디버그 로깅 (2초마다)
301314
if (frameCount === 0 || Date.now() - lastLogTime > 2000) {
302-
const sampleData = Array.from(buffer.slice(0, 20));
303-
console.log("Buffer sample:", sampleData, "Size:", bufferSize, "Expected:", ASCII_WIDTH * ASCII_HEIGHT * 4);
315+
console.log("Buffer size:", bufferSize, "Expected:", ASCII_WIDTH * ASCII_HEIGHT * 4);
304316
lastLogTime = Date.now();
305317
}
306-
307-
ctx.fillStyle = 'black';
308-
ctx.fillRect(0, 0, canvas.width, canvas.height);
309-
ctx.font = FONT;
318+
319+
// 배경 클리어
320+
ctx.fillStyle = '#000';
321+
ctx.fillRect(0, 0, canvas.width, canvas.height);
322+
ctx.font = FONT;
310323

311324
const expectedSize = ASCII_WIDTH * ASCII_HEIGHT * 4;
312325
if (buffer.length < expectedSize) {
313326
if (frameCount % 60 === 0) {
314327
console.warn(`Buffer size mismatch: got ${buffer.length}, expected ${expectedSize}`);
315328
}
329+
return;
316330
}
317331

332+
// 최적화된 렌더링 루프
318333
let i = 0;
319-
let hasData = false;
320-
let nonZeroCount = 0;
334+
const totalCells = ASCII_WIDTH * ASCII_HEIGHT;
321335

322-
for (let y = 0; y < ASCII_HEIGHT; y++) {
323-
for (let x = 0; x < ASCII_WIDTH; x++) {
324-
if (i + 3 >= buffer.length) break;
325-
326-
const charCode = buffer[i];
327-
const r = buffer[i + 1];
328-
const g = buffer[i + 2];
329-
const b = buffer[i + 3];
330-
331-
if (charCode !== 0 || r !== 0 || g !== 0 || b !== 0) {
332-
hasData = true;
333-
nonZeroCount++;
334-
}
335-
336-
if (charCode !== 0 || r !== 0 || g !== 0 || b !== 0) {
337-
const char = charCode > 0 && charCode < 256 ? String.fromCharCode(charCode) : ' ';
338-
336+
for (let y = 0; y < ASCII_HEIGHT; y++) {
337+
const yPos = y * charHeight + charHeight;
338+
339+
for (let x = 0; x < ASCII_WIDTH; x++) {
340+
const charCode = buffer[i];
341+
const r = buffer[i + 1];
342+
const g = buffer[i + 2];
343+
const b = buffer[i + 3];
344+
i += 4;
345+
346+
// 비어있지 않은 셀만 렌더링 (조건 체크 1회로 통합)
347+
if (charCode > 32 || r > 10 || g > 10 || b > 10) {
339348
ctx.fillStyle = `rgb(${r},${g},${b})`;
340-
ctx.fillText(char, x * charWidth, y * charHeight + charHeight);
341-
}
342-
343-
i += 4;
349+
ctx.fillText(String.fromCharCode(charCode), x * charWidth, yPos);
344350
}
345351
}
346-
347-
if (frameCount % 60 === 0 && hasData) {
348-
console.log(`Rendering frame ${frameCount}, non-zero cells: ${nonZeroCount}/${ASCII_WIDTH * ASCII_HEIGHT}`);
349352
}
350353

351354
frameCount++;
352355

356+
// 첫 프레임 로깅
353357
if (frameCount === 1) {
354-
console.log("First frame rendered, hasData:", hasData);
358+
console.log("First frame rendered successfully");
355359
}
356360
} catch (e) {
357361
console.error("Error during canvas rendering:", e);

0 commit comments

Comments
 (0)