SVN / public / code / projekt / template / OpenGLTerminalManager.cpp

Revision 8201
Date
Committercu1017
Download
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
// Copyright 2026, University of Freiburg,
// Chair of Algorithms and Data Structures.
// Author: Axel Lehmann <lehmann@cs.uni-freiburg.de>,
//         Claudius Korzen <korzen@cs.uni-freiburg.de>,
//         Johannes Kalmbach <kalmbach@cs.uni-freiburg.de>,
//         Christoph Ullinger <ullingec@cs.uni-freiburg.de>.

#include "./OpenGLTerminalManager.h"
#include <algorithm>
#include <cstring>
#include <ncurses.h> // for the COLOR_* and KEY_* constants used below
#include <stdexcept>

constexpr int PIXEL_SIZE = 300;
// Horizontal spacing (in logical columns) between consecutive characters
// drawn by drawString().
constexpr int GLYPH_ADVANCE = 50;

namespace {
// Decode the UTF-8 code point starting at s[i], advancing i past all of its
// bytes.
unsigned int decodeUtf8(const char *s, size_t len, size_t &i) {
  unsigned char lead = static_cast<unsigned char>(s[i]);
  int continuationBytes;
  unsigned int codepoint;
  if ((lead & 0x80) == 0x00) {
    codepoint = lead;
    continuationBytes = 0;
  } else if ((lead & 0xE0) == 0xC0) {
    codepoint = lead & 0x1F;
    continuationBytes = 1;
  } else if ((lead & 0xF0) == 0xE0) {
    codepoint = lead & 0x0F;
    continuationBytes = 2;
  } else if ((lead & 0xF8) == 0xF0) {
    codepoint = lead & 0x07;
    continuationBytes = 3;
  } else {
    ++i;
    return lead;
  }
  ++i;
  for (int k = 0; k < continuationBytes && i < len &&
                  (static_cast<unsigned char>(s[i]) & 0xC0) == 0x80;
       ++k, ++i) {
    codepoint = (codepoint << 6) | (static_cast<unsigned char>(s[i]) & 0x3F);
  }
  return codepoint;
}

// The same palette (the 8 basic ANSI colors identified by ncurses.h's
// COLOR_BLACK..COLOR_WHITE, plus a "bright black"/grey at 8 and orange at 9)
// that BrailleTerminalManager and NCursesTerminalManager use for their color
// arguments, translated to RGB triples in [0, 1] for OpenGL.
std::tuple<float, float, float> colorToRgb(int color) {
  switch (color) {
  case COLOR_RED:
    return {1.0f, 0.0f, 0.0f};
  case COLOR_GREEN:
    return {0.0f, 1.0f, 0.0f};
  case COLOR_YELLOW:
    return {1.0f, 1.0f, 0.0f};
  case COLOR_BLUE:
    return {0.0f, 0.0f, 1.0f};
  case COLOR_MAGENTA:
    return {1.0f, 0.0f, 1.0f};
  case COLOR_CYAN:
    return {0.0f, 1.0f, 1.0f};
  case COLOR_WHITE:
    return {1.0f, 1.0f, 1.0f};
  case 8: // grey
    return {0.5f, 0.5f, 0.5f};
  case 9: // orange
    return {1.0f, 0.65f, 0.0f};
  case 11: // bright yellow
    return {1.0f, 1.0f, 0.0f};
  case COLOR_BLACK:
  default:
    return {0.0f, 0.0f, 0.0f};
  }
}
} // namespace

// ____________________________________________________________________________
OpenGLTerminalManager::OpenGLTerminalManager() {
  pixels_.assign(static_cast<size_t>(size_) * size_ * 3, 0.0f);
  /* Initialize the library */
  if (!glfwInit()) {
    throw std::runtime_error{"Could not initialize GLFW!"};
  }

  /* Create a windowed mode window and its OpenGL context */
  window_ = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
  if (!window_) {
    glfwTerminate();
    throw std::runtime_error{"Could not create Window!"};
  }
  glfwSetWindowUserPointer(window_, static_cast<void *>(this));
  /* Make the window's context current */
  glfwMakeContextCurrent(window_);

  // Seed the tracked cursor position (see the cursor-pos callback below) so
  // a click before the very first mouse-move event still has a real
  // position to read, instead of the (0, 0) default.
  glfwGetCursorPos(window_, &lastCursorX_, &lastCursorY_);

  glEnable(GL_TEXTURE_2D);

  glGenTextures(1, &texture_);
  glBindTexture(GL_TEXTURE_2D, texture_);

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  // The logical pixel grid (size_ x size_) is usually much higher-resolution
  // than the actual window, so displaying it is a minification. Nearest-
  // neighbor minification just samples one texel per screen pixel and skips
  // the rest, which can skip a whole thin (1-texel-wide) polygon boundary
  // line between samples, leaving visible gaps. Mipmapped linear filtering
  // averages every texel into the sampled result instead, so a thin line
  // still shows up (softened) rather than disappearing.
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                  GL_LINEAR_MIPMAP_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  // Auto-regenerate the mipmap chain whenever glTexImage2D uploads a new
  // base level (every frame, since pixels_ just changed). This legacy
  // (pre-GL3) mechanism is used instead of calling glGenerateMipmap()
  // directly because that function isn't available without an extension
  // loader, which this project (all fixed-function/immediate-mode GL calls)
  // doesn't otherwise need.
  glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);

  glBindTexture(GL_TEXTURE_2D, 0);

  // Track the cursor position as GLFW reports it move, rather than querying
  // it live from inside the button callback below: glfwPollEvents() (called
  // once per frame, not continuously) can dispatch a backlog of queued OS
  // events at once, and by the time it gets around to the button-press
  // callback, glfwGetCursorPos() would report wherever the mouse is *right
  // now* -- which, if the player moved the mouse in the meantime, is no
  // longer where it was at the moment of the actual click. This callback's
  // parameters are the position as of that specific (already historical)
  // motion event, so as long as GLFW dispatches events in the order they
  // occurred, the button-press callback below always sees the position as of
  // the most recent motion event *before* the click, not after it.
  glfwSetCursorPosCallback(window_, [](GLFWwindow *window, double x, double y) {
    auto self =
        static_cast<OpenGLTerminalManager *>(glfwGetWindowUserPointer(window));
    self->lastCursorX_ = x;
    self->lastCursorY_ = y;
  });

  glfwSetMouseButtonCallback(window_, [](GLFWwindow *window, int button,
                                         int action,
                                         [[maybe_unused]] int mods) {
    if (button != GLFW_MOUSE_BUTTON_1 || action != GLFW_PRESS) {
      return;
    }
    auto self =
        static_cast<OpenGLTerminalManager *>(glfwGetWindowUserPointer(window));
    double cursorX = self->lastCursorX_, cursorY = self->lastCursorY_;
    int windowWidth, windowHeight;
    glfwGetWindowSize(window, &windowWidth, &windowHeight);
    auto minmax = std::minmax(windowWidth, windowHeight);
    double viewportX =
        (cursorX - ((minmax.second - windowHeight) / 2)) / minmax.first;
    double viewportY =
        (cursorY - ((minmax.second - windowWidth) / 2)) / minmax.first;
    if (viewportX >= 1 || viewportY >= 1 || viewportX < 0 || viewportY < 0) {
      return;
    }
    self->mouseClicks_.push({static_cast<int>(self->numRows() * viewportY),
                             static_cast<int>(self->numCols() * viewportX)});
  });

  glfwSetCharCallback(window_, [](GLFWwindow *window, unsigned int codepoint) {
    if (codepoint > 127) {
      return;
    }
    auto self =
        static_cast<OpenGLTerminalManager *>(glfwGetWindowUserPointer(window));
    self->keypresses_.push(static_cast<int>(codepoint));
  });

  glfwSetKeyCallback(window_, [](GLFWwindow *window, int key,
                                 [[maybe_unused]] int scancode, int action,
                                 [[maybe_unused]] int mods) {
    if (action != GLFW_PRESS) {
      return;
    }
    auto self =
        static_cast<OpenGLTerminalManager *>(glfwGetWindowUserPointer(window));
    // Translate GLFW's key codes to the ncurses key codes that the shared
    // UserInput::isKeyUp() etc. (defined once in TerminalManager.cpp) check
    // against, so the same UserInput class works for this GLFW-based backend
    // as well as the ncurses-based ones.
    switch (key) {
    case GLFW_KEY_UP:
      self->keypresses_.push(KEY_UP);
      break;
    case GLFW_KEY_DOWN:
      self->keypresses_.push(KEY_DOWN);
      break;
    case GLFW_KEY_LEFT:
      self->keypresses_.push(KEY_LEFT);
      break;
    case GLFW_KEY_RIGHT:
      self->keypresses_.push(KEY_RIGHT);
      break;
    case GLFW_KEY_ESCAPE:
      self->keypresses_.push(27);
      break;
    default:
      break;
    }
  });

  if (FT_Init_FreeType(&ft_)) {
    throw std::runtime_error{"Could not init FreeType Library"};
  }

  if (FT_New_Face(ft_, "CascadiaMono.ttf", 0, &face_)) {
    throw std::runtime_error{"Failed to load font"};
  }
  FT_Set_Pixel_Sizes(face_, 0, PIXEL_SIZE);

  glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // disable byte-alignment restriction

  // Warm the glyph cache with ASCII, the common case; anything else (e.g. an
  // umlaut) is loaded lazily by getCharacter() on first use. ft_/face_ stay
  // alive for the object's lifetime (torn down in the destructor, not here)
  // to make that possible.
  for (unsigned char c = 0; c < 128; c++) {
    getCharacter(c);
  }
}

// ____________________________________________________________________________
OpenGLTerminalManager::~OpenGLTerminalManager() {
  FT_Done_Face(face_);
  FT_Done_FreeType(ft_);
  glfwTerminate();
}

// ____________________________________________________________________________
const OpenGLTerminalManager::Character &
OpenGLTerminalManager::getCharacter(unsigned int codepoint) {
  auto it = characters_.find(codepoint);
  if (it != characters_.end())
    return it->second;

  if (FT_Load_Char(face_, codepoint, FT_LOAD_RENDER)) {
    throw std::runtime_error{"ERROR::FREETYPE: Failed to load Glyph"};
  }
  GLuint texture;
  glGenTextures(1, &texture);
  glBindTexture(GL_TEXTURE_2D, texture);
  // GL_LUMINANCE (not GL_RED) so the fixed-function GL_MODULATE texture env
  // (see renderText()) broadcasts the glyph's coverage value into all of
  // R/G/B rather than just R -- otherwise glColor3f()'s green/blue
  // components would always get multiplied by 0 and text would render as
  // shades of red regardless of the requested color.
  glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, face_->glyph->bitmap.width,
               face_->glyph->bitmap.rows, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE,
               face_->glyph->bitmap.buffer);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  Character character = {
      texture,
      {face_->glyph->bitmap.width, face_->glyph->bitmap.rows},
      {face_->glyph->bitmap_left, face_->glyph->bitmap_top},
      face_->glyph->advance.x};
  return characters_.emplace(codepoint, character).first->second;
}

// ____________________________________________________________________________
void OpenGLTerminalManager::drawPixel(int row, int col, int color) {
  if (row < 0 || row >= size_ || col < 0 || col >= size_)
    return;
  auto [r, g, b] = colorToRgb(color);
  size_t offset = 3 * (static_cast<size_t>(row) * size_ + col);
  pixels_[offset + 0] = r;
  pixels_[offset + 1] = g;
  pixels_[offset + 2] = b;
}

// ____________________________________________________________________________
void OpenGLTerminalManager::refresh() {
  int width, height;

  glfwGetFramebufferSize(window_, &width, &height);

  auto minmax = std::minmax(width, height);

  int xOffset = (minmax.second - height) / 2;
  int yOffset = (minmax.second - width) / 2;

  // Make viewport guaranteed to be square
  glViewport(xOffset, yOffset, minmax.first, minmax.first);
  /* Render here */
  glClear(GL_COLOR_BUFFER_BIT);

  glColor3f(1.0f, 1.0f, 1.0f);
  glBindTexture(GL_TEXTURE_2D, texture_);
  // GL_GENERATE_MIPMAP (set in the ctor) rebuilds the mipmap chain right
  // here, automatically, since this call updates the base mip level.
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, size_, size_, 0, GL_RGB, GL_FLOAT,
               pixels_.data());

  glBegin(GL_QUADS);
  glNormal3f(0, 0, 1);
  glTexCoord2f(0.0f, 1.0f);
  glVertex2f(-1.0f, -1.0f);
  glTexCoord2f(1.0f, 1.0f);
  glVertex2f(1.0f, -1.0f);
  glTexCoord2f(1.0f, 0.0f);
  glVertex2f(1.0f, 1.0f);
  glTexCoord2f(0.0f, 0.0f);
  glVertex2f(-1.0f, 1.0f);
  glEnd();
  glBindTexture(GL_TEXTURE_2D, 0);

  for (const auto &[cell, charDraw] : charsToDraw_) {
    renderText(charDraw.codepoint, static_cast<int>(cell.col_),
               static_cast<int>(cell.row_), charDraw.color, charDraw.scale);
  }
  // Buffered text is only valid for the frame it was drawn in, same as
  // BrailleTerminalManager's pendingText_.
  charsToDraw_.clear();

  /* Swap front and back buffers */
  glfwSwapBuffers(window_);

  /* Poll for and process events */
  glfwPollEvents();
}

// ___________________________________________________________________________
void OpenGLTerminalManager::drawString(int row, int col, const char *output,
                                       int color) {
  if (row < 0 || row >= numRows()) {
    return;
  }

  // Decode the whole string up front: the per-character column step below
  // depends on how many characters there are in total (to fit a long string
  // into the available columns instead of cutting it off), which isn't known
  // until all of them have been decoded.
  size_t len = strlen(output);
  std::vector<unsigned int> codepoints;
  for (size_t i = 0; i < len;)
    codepoints.push_back(decodeUtf8(output, len, i));
  if (codepoints.empty())
    return;

  // GLYPH_ADVANCE columns per character is the normal spacing; if the full
  // string wouldn't fit in the columns available from `col` to the edge of
  // the screen at that spacing, shrink both the spacing and the glyphs
  // themselves (via `scale`) just enough to make it fit, rather than
  // truncating the string.
  int available = numCols() - col;
  int advance = GLYPH_ADVANCE;
  if (available > 0)
    advance = std::min(GLYPH_ADVANCE,
                       available / static_cast<int>(codepoints.size()));
  advance = std::max(advance, 1);
  float scale = static_cast<float>(advance) / GLYPH_ADVANCE;

  for (size_t i = 0; i < codepoints.size(); ++i) {
    int c = col + static_cast<int>(i) * advance;
    if (c < 0 || c >= numCols())
      break;
    charsToDraw_[OpenGLCell{static_cast<size_t>(row), static_cast<size_t>(c)}] =
        {codepoints[i], color, scale};
  }
}

// ___________________________________________________________________________
UserInput OpenGLTerminalManager::getUserInput() {
  // Purely reads events already collected by the GLFW callbacks (populated
  // the last time refresh() called glfwPollEvents()); it does NOT render or
  // swap buffers itself. The caller draws a frame and then calls refresh()
  // exactly once, same as with the ncurses-based backends -- if
  // getUserInput() also rendered a frame (as blatt-10's original did), every
  // loop iteration would flash an empty, not-yet-drawn frame before the
  // caller's real one, which is what made the status text appear to blink.
  UserInput input;
  input.keycode_ = -1;
  if (!keypresses_.empty()) {
    input.keycode_ = keypresses_.front();
    keypresses_.pop();
  } else if (!mouseClicks_.empty()) {
    input.mouseRow_ = mouseClicks_.front().first;
    input.mouseCol_ = mouseClicks_.front().second;
    mouseClicks_.pop();
  }

  if (glfwWindowShouldClose(window_)) {
    // Treat closing the window the same as pressing ESC.
    input.keycode_ = 27;
  }

  return input;
}

// ____________________________________________________________________________
void OpenGLTerminalManager::renderText(unsigned int codepoint, int col, int row,
                                       int color, float scale) {
  // Each cell in charsToDraw_ already holds exactly one code point (see
  // drawString()), so this draws exactly one glyph -- no need for its own
  // horizontal-advance loop.
  glPushMatrix();
  // move to top left from center
  glTranslatef(-1, 1, 0);
  glScalef(2.0f / numRows(), 2.0f / numCols(), 1.0f);
  glTranslatef(col, -row, 0);
  // scale to grid, shrunk further by `scale` if drawString() had to shrink
  // this string to make it fit in its available columns
  glScalef(scale * 50.0 / PIXEL_SIZE, scale * 50.0 / PIXEL_SIZE, 0);
  glTranslatef(50, -230, 0);

  auto [r, g, b] = colorToRgb(color);
  glColor3f(r, g, b);

  const Character &ch = getCharacter(codepoint);

  float xpos = std::get<0>(ch.bearing);
  float ypos =
      static_cast<float>(std::get<1>(ch.bearing)) - std::get<1>(ch.size);
  float w = std::get<0>(ch.size);
  float h = std::get<1>(ch.size);

  glBindTexture(GL_TEXTURE_2D, ch.textureId);
  glBegin(GL_QUADS);
  glNormal3f(0, 0, 1);
  glTexCoord2f(0.0f, 1.0f);
  glVertex2f(xpos, ypos);
  glTexCoord2f(1.0f, 1.0f);
  glVertex2f(xpos + w, ypos);
  glTexCoord2f(1.0f, 0.0f);
  glVertex2f(xpos + w, ypos + h);
  glTexCoord2f(0.0f, 0.0f);
  glVertex2f(xpos, ypos + h);
  glEnd();
  glBindTexture(GL_TEXTURE_2D, 0);
  glPopMatrix();
}