openMSX
OSDConsoleRenderer.cc
Go to the documentation of this file.
1 #include "OSDConsoleRenderer.hh"
2 #include "CommandConsole.hh"
3 #include "BooleanSetting.hh"
4 #include "SDLImage.hh"
5 #include "Display.hh"
6 #include "Timer.hh"
7 #include "FileContext.hh"
8 #include "CliComm.hh"
9 #include "Reactor.hh"
10 #include "MSXException.hh"
11 #include "openmsx.hh"
12 #include "one_of.hh"
13 #include "unreachable.hh"
14 #include "utf8_unchecked.hh"
15 #include "xrange.hh"
16 #include <algorithm>
17 #include <cassert>
18 #include <memory>
19 
20 #include "components.hh"
21 #if COMPONENT_GL
22 #include "GLImage.hh"
23 #endif
24 
25 using namespace gl;
26 
27 namespace openmsx {
28 
33 constexpr int CONSOLE_ALPHA = 180;
34 constexpr uint64_t BLINK_RATE = 500000; // us
35 constexpr int CHAR_BORDER = 4;
36 
37 
38 // class OSDConsoleRenderer
39 
40 constexpr std::string_view defaultFont = "skins/VeraMono.ttf.gz";
41 
42 OSDConsoleRenderer::OSDConsoleRenderer(
43  Reactor& reactor_, CommandConsole& console_,
44  unsigned screenW_, unsigned screenH_,
45  bool openGL_)
46  : Layer(COVER_NONE, Z_CONSOLE)
47  , reactor(reactor_)
48  , display(reactor.getDisplay()) // need to store because still needed during destructor
49  , console(console_)
50  , consoleSetting(console.getConsoleSetting())
51  , screenW(screenW_)
52  , screenH(screenH_)
53  , openGL(openGL_)
54  , consolePlacementSetting(
55  reactor.getCommandController(), "consoleplacement",
56  "position of the console within the emulator",
57  // On Android, console must by default be placed on top, in
58  // order to prevent that it overlaps with the virtual Android
59  // keyboard, which is always placed at the bottom of the screen
60  PLATFORM_ANDROID ? CP_TOP : CP_BOTTOM,
61  EnumSetting<Placement>::Map{
62  {"topleft", CP_TOPLEFT},
63  {"top", CP_TOP},
64  {"topright", CP_TOPRIGHT},
65  {"left", CP_LEFT},
66  {"center", CP_CENTER},
67  {"right", CP_RIGHT},
68  {"bottomleft", CP_BOTTOMLEFT},
69  {"bottom", CP_BOTTOM},
70  {"bottomright", CP_BOTTOMRIGHT}})
71  , fontSizeSetting(reactor.getCommandController(),
72  "consolefontsize", "Size of the console font", 12, 8, 32)
73  , fontSetting(reactor.getCommandController(),
74  "consolefont", "console font file", defaultFont)
75  , consoleColumnsSetting(reactor.getCommandController(),
76  "consolecolumns", "number of columns in the console",
77  initFontAndGetColumns(), 32, 999)
78  , consoleRowsSetting(reactor.getCommandController(),
79  "consolerows", "number of rows in the console",
80  getRows(), 1, 99)
81  , backgroundSetting(reactor.getCommandController(),
82  "consolebackground", "console background file",
83  "skins/ConsoleBackgroundGrey.png")
84 {
85 #if !COMPONENT_GL
86  assert(!openGL);
87 #endif
88  bgPos = bgSize = ivec2(); // recalc on first paint()
89  blink = false;
90  lastBlinkTime = Timer::getTime();
91  lastCursorX = lastCursorY = 0;
92 
93  active = false;
94  activeTime = 0;
95  setCoverage(COVER_PARTIAL);
96 
97  adjustColRow();
98 
99  // background (only load background on first paint())
100  backgroundSetting.setChecker([this](TclObject& value) {
101  loadBackground(value.getString());
102  });
103  // don't yet load background
104 
105  consoleSetting.attach(*this);
106  fontSetting.attach(*this);
107  fontSizeSetting.attach(*this);
108  setActive(consoleSetting.getBoolean());
109 }
110 
111 int OSDConsoleRenderer::initFontAndGetColumns()
112 {
113  // init font
114  fontSetting.setChecker([this](TclObject& value) {
115  loadFont(value.getString());
116  });
117  try {
118  loadFont(fontSetting.getString());
119  } catch (MSXException&) {
120  // This will happen when you upgrade from the old .png based
121  // fonts to the new .ttf fonts. So provide a smooth upgrade path.
122  reactor.getCliComm().printWarning(
123  "Loading selected font (", fontSetting.getString(),
124  ") failed. Reverting to default font (", defaultFont, ").");
125  fontSetting.setString(defaultFont);
126  if (font.empty()) {
127  // we can't continue without font
128  throw FatalError("Couldn't load default console font.\n"
129  "Please check your installation.");
130  }
131  }
132 
133  return (((screenW - CHAR_BORDER) / font.getWidth()) * 30) / 32;
134 }
135 int OSDConsoleRenderer::getRows()
136 {
137  // initFontAndGetColumns() must already be called
138  return ((screenH / font.getHeight()) * 6) / 15;
139 }
140 OSDConsoleRenderer::~OSDConsoleRenderer()
141 {
142  fontSizeSetting.detach(*this);
143  fontSetting.detach(*this);
144  consoleSetting.detach(*this);
145  setActive(false);
146 }
147 
148 void OSDConsoleRenderer::adjustColRow()
149 {
150  unsigned consoleColumns = std::min<unsigned>(
151  consoleColumnsSetting.getInt(),
152  (screenW - CHAR_BORDER) / font.getWidth());
153  unsigned consoleRows = std::min<unsigned>(
154  consoleRowsSetting.getInt(),
155  screenH / font.getHeight());
156  console.setColumns(consoleColumns);
157  console.setRows(consoleRows);
158 }
159 
160 void OSDConsoleRenderer::update(const Setting& setting) noexcept
161 {
162  if (&setting == &consoleSetting) {
163  setActive(consoleSetting.getBoolean());
164  } else if (&setting == one_of(&fontSetting, &fontSizeSetting)) {
165  loadFont(fontSetting.getString());
166  } else {
167  UNREACHABLE;
168  }
169 }
170 
171 void OSDConsoleRenderer::setActive(bool active_)
172 {
173  if (active == active_) return;
174  active = active_;
175 
176  display.repaintDelayed(40000); // 25 fps
177 
178  activeTime = Timer::getTime();
179 }
180 
181 byte OSDConsoleRenderer::getVisibility() const
182 {
183  const uint64_t FADE_IN_DURATION = 100000;
184  const uint64_t FADE_OUT_DURATION = 150000;
185 
186  auto now = Timer::getTime();
187  auto dur = now - activeTime;
188  if (active) {
189  if (dur > FADE_IN_DURATION) {
190  return 255;
191  } else {
192  display.repaintDelayed(40000); // 25 fps
193  return byte((dur * 255) / FADE_IN_DURATION);
194  }
195  } else {
196  if (dur > FADE_OUT_DURATION) {
197  return 0;
198  } else {
199  display.repaintDelayed(40000); // 25 fps
200  return byte(255 - ((dur * 255) / FADE_OUT_DURATION));
201  }
202  }
203 }
204 
205 bool OSDConsoleRenderer::updateConsoleRect()
206 {
207  adjustColRow();
208 
209  ivec2 size((font.getWidth() * console.getColumns()) + CHAR_BORDER,
210  font.getHeight() * console.getRows());
211 
212  // TODO use setting listener in the future
213  ivec2 pos;
214  switch (consolePlacementSetting.getEnum()) {
215  case CP_TOPLEFT:
216  case CP_LEFT:
217  case CP_BOTTOMLEFT:
218  pos[0] = 0;
219  break;
220  case CP_TOPRIGHT:
221  case CP_RIGHT:
222  case CP_BOTTOMRIGHT:
223  pos[0] = (screenW - size[0]);
224  break;
225  case CP_TOP:
226  case CP_CENTER:
227  case CP_BOTTOM:
228  default:
229  pos[0] = (screenW - size[0]) / 2;
230  break;
231  }
232  switch (consolePlacementSetting.getEnum()) {
233  case CP_TOPLEFT:
234  case CP_TOP:
235  case CP_TOPRIGHT:
236  pos[1] = 0;
237  break;
238  case CP_LEFT:
239  case CP_CENTER:
240  case CP_RIGHT:
241  pos[1] = (screenH - size[1]) / 2;
242  break;
243  case CP_BOTTOMLEFT:
244  case CP_BOTTOM:
245  case CP_BOTTOMRIGHT:
246  default:
247  pos[1] = (screenH - size[1]);
248  break;
249  }
250 
251  bool result = (pos != bgPos) || (size != bgSize);
252  bgPos = pos;
253  bgSize = size;
254  return result;
255 }
256 
257 void OSDConsoleRenderer::loadFont(std::string_view value)
258 {
259  auto filename = systemFileContext().resolve(value);
260  auto newFont = TTFFont(filename, fontSizeSetting.getInt());
261  if (!newFont.isFixedWidth()) {
262  throw MSXException(value, " is not a monospaced font");
263  }
264  font = std::move(newFont);
265  clearCache();
266 }
267 
268 void OSDConsoleRenderer::loadBackground(std::string_view value)
269 {
270  if (value.empty()) {
271  backgroundImage.reset();
272  return;
273  }
274  auto* output = display.getOutputSurface();
275  if (!output) {
276  backgroundImage.reset();
277  return;
278  }
279  auto filename = systemFileContext().resolve(value);
280  if (!openGL) {
281  backgroundImage = std::make_unique<SDLImage>(*output, filename, bgSize);
282  }
283 #if COMPONENT_GL
284  else {
285  backgroundImage = std::make_unique<GLImage>(*output, filename, bgSize);
286  }
287 #endif
288 }
289 
290 void OSDConsoleRenderer::drawText(OutputSurface& output, std::string_view text,
291  int cx, int cy, byte alpha, uint32_t rgb)
292 {
293  auto xy = getTextPos(cx, cy);
294  auto [inCache, image, width] = getFromCache(text, rgb);
295  if (!inCache) {
296  std::string textStr(text);
297  SDLSurfacePtr surf;
298  uint32_t rgb2 = openGL ? 0xffffff : rgb; // openGL -> always render white
299  try {
300  width = font.getSize(textStr)[0];
301  surf = font.render(textStr,
302  (rgb2 >> 16) & 0xff,
303  (rgb2 >> 8) & 0xff,
304  (rgb2 >> 0) & 0xff);
305  } catch (MSXException& e) {
306  static bool alreadyPrinted = false;
307  if (!alreadyPrinted) {
308  alreadyPrinted = true;
309  reactor.getCliComm().printWarning(
310  "Invalid console text (invalid UTF-8): ",
311  e.getMessage());
312  }
313  return; // don't cache negative results
314  }
315  std::unique_ptr<BaseImage> image2;
316  if (!surf) {
317  // nothing was rendered, so do nothing
318  } else if (!openGL) {
319  image2 = std::make_unique<SDLImage>(output, std::move(surf));
320  }
321 #if COMPONENT_GL
322  else {
323  image2 = std::make_unique<GLImage>(output, std::move(surf));
324  }
325 #endif
326  image = image2.get();
327  insertInCache(std::move(textStr), rgb, std::move(image2), width);
328  }
329  if (image) {
330  if (openGL) {
331  byte r = (rgb >> 16) & 0xff;
332  byte g = (rgb >> 8) & 0xff;
333  byte b = (rgb >> 0) & 0xff;
334  image->draw(output, xy, r, g, b, alpha);
335  } else {
336  image->draw(output, xy, alpha);
337  }
338  }
339 }
340 
341 std::tuple<bool, BaseImage*, unsigned> OSDConsoleRenderer::getFromCache(
342  std::string_view text, uint32_t rgb)
343 {
344  // Items are LRU sorted, so the next requested items will often be
345  // located right in front of the previously found item. (Though
346  // duplicate items (e.g. the command prompt '> ') degrade this
347  // heuristic).
348  auto it = cacheHint;
349  // For openGL ignore rgb
350  if ((it->text == text) && (openGL || (it->rgb == rgb))) {
351  goto found;
352  }
353 
354  // Search the whole cache for a match. If the cache is big enough then
355  // all N items used for rendering the previous frame should be located
356  // in the N first positions in the cache (in approx reverse order).
357  for (it = begin(textCache); it != end(textCache); ++it) {
358  if (it->text != text) continue;
359  if (!openGL && (it->rgb != rgb)) continue;
360 found: BaseImage* image = it->image.get();
361  unsigned width = it->width;
362  cacheHint = it;
363  if (it != begin(textCache)) {
364  --cacheHint; // likely candidate for next item
365  // move to front (to keep in LRU order)
366  textCache.splice(begin(textCache), textCache, it);
367  }
368  return {true, image, width};
369  }
370  return {false, nullptr, 0};
371 }
372 
373 void OSDConsoleRenderer::insertInCache(
374  std::string text, uint32_t rgb, std::unique_ptr<BaseImage> image,
375  unsigned width)
376 {
377  constexpr unsigned MAX_TEXT_CACHE_SIZE = 250;
378  if (textCache.size() == MAX_TEXT_CACHE_SIZE) {
379  // flush the least recently used entry
380  if (auto it = std::prev(std::end(textCache)); it == cacheHint) {
381  cacheHint = begin(textCache);
382  }
383  textCache.pop_back();
384  }
385  textCache.emplace_front(std::move(text), rgb, std::move(image), width);
386 }
387 
388 void OSDConsoleRenderer::clearCache()
389 {
390  // cacheHint must always point to a valid item, so insert a dummy entry
391  textCache.clear();
392  textCache.emplace_back(std::string{}, 0, nullptr, 0);
393  cacheHint = begin(textCache);
394 }
395 
396 gl::ivec2 OSDConsoleRenderer::getTextPos(int cursorX, int cursorY) const
397 {
398  return bgPos + ivec2(CHAR_BORDER + cursorX * font.getWidth(),
399  bgSize[1] - (font.getHeight() * (cursorY + 1)) - 1);
400 }
401 
402 void OSDConsoleRenderer::paint(OutputSurface& output)
403 {
404  byte visibility = getVisibility();
405  if (!visibility) return;
406 
407  if (updateConsoleRect()) {
408  try {
409  loadBackground(backgroundSetting.getString());
410  } catch (MSXException& e) {
411  reactor.getCliComm().printWarning(e.getMessage());
412  }
413  }
414 
415  // draw the background image if there is one
416  if (!backgroundImage) {
417  // no background image, try to create an empty one
418  try {
419  if (!openGL) {
420  backgroundImage = std::make_unique<SDLImage>(
421  output, bgSize, CONSOLE_ALPHA);
422  }
423 #if COMPONENT_GL
424  else {
425  backgroundImage = std::make_unique<GLImage>(
426  output, bgSize, CONSOLE_ALPHA);
427  }
428 #endif
429  } catch (MSXException&) {
430  // nothing
431  }
432  }
433  if (backgroundImage) {
434  backgroundImage->draw(output, bgPos, visibility);
435  }
436 
437  drawConsoleText(output, visibility);
438 
439  // Check if the blink period is over
440  auto now = Timer::getTime();
441  if (lastBlinkTime < now) {
442  lastBlinkTime = now + BLINK_RATE;
443  blink = !blink;
444  }
445 
446  auto [cursorX, cursorY] = console.getCursorPosition();
447  if ((unsigned(cursorX) != lastCursorX) || (unsigned(cursorY) != lastCursorY)) {
448  blink = true; // force cursor
449  lastBlinkTime = now + BLINK_RATE; // maximum time
450  lastCursorX = cursorX;
451  lastCursorY = cursorY;
452  }
453  if (blink && (console.getScrollBack() == 0)) {
454  drawText(output, "_", cursorX, cursorY, visibility, 0xffffff);
455  }
456 }
457 
458 void OSDConsoleRenderer::drawConsoleText(OutputSurface& output, byte visibility)
459 {
460  const auto rows = console.getRows();
461  const auto columns = console.getColumns();
462  const auto scrollBack = console.getScrollBack();
463  const auto& lines = console.getLines();
464 
465  // search first visible line
466  auto [cursorY_, subLine, lineIdx] = [&] {
467  size_t count = 0;
468  size_t target = rows + scrollBack;
469  for (auto idx : xrange(lines.size())) {
470  count += std::max(size_t(1), (lines[idx].numChars() + columns - 1) / columns);
471  if (count >= target) {
472  return std::tuple(int(rows - 1), int(count - target), idx);
473  }
474  }
475  int y = int(count - 1 - scrollBack);
476  return std::tuple(y, 0, lines.size() - 1);
477  }();
478  int cursorY = cursorY_; // clang workaround
479 
480  // setup for first (partial) line
481  std::string_view text = lines[lineIdx].str();
482  auto it = begin(text);
483  auto endIt = end(text);
484  utf8::unchecked::advance(it, subLine * columns);
485  std::string_view::size_type idx = it - begin(text);
486  unsigned chunkIdx = 1;
487  const auto& chunks0 = lines[lineIdx].getChunks();
488  while ((chunkIdx < chunks0.size()) && (chunks0[chunkIdx].pos <= idx)) {
489  ++chunkIdx;
490  }
491 
492  // draw each console-line (long text-lines are split over multiple console-lines)
493  while (true) {
494  auto remainingColumns = columns;
495  const auto& chunks = lines[lineIdx].getChunks();
496  if (!chunks.empty()) {
497  // draw chunks of same color
498  while (remainingColumns && (it < endIt)) {
499  auto startColumn = remainingColumns;
500  auto e = it;
501  auto nextColorIt = (chunkIdx == chunks.size())
502  ? endIt
503  : begin(text) + chunks[chunkIdx].pos;
504  auto maxIt = std::min(endIt, nextColorIt);
505  while (remainingColumns && (e < maxIt)) {
507  --remainingColumns;
508  }
509  //std::string_view subText(it, e); // c++20
510  std::string_view subText(&*it, e - it);
511  auto rgb = chunks[chunkIdx - 1].rgb;
512  auto cursorX = columns - startColumn;
513  drawText(output, subText, cursorX, cursorY, visibility, rgb);
514 
515  // next chunk
516  it = e;
517  // move to next color?
518  if (e == nextColorIt) ++chunkIdx;
519  }
520  }
521  // all console-lines drawn?
522  if (--cursorY < 0) break;
523  // move to next text-line?
524  if (it == endIt) {
525  --lineIdx;
526  text = lines[lineIdx].str();
527  it = begin(text);
528  endIt = end(text);
529  chunkIdx = 1;
530  }
531  }
532 }
533 
534 } // namespace openmsx
std::string image
Definition: HDImageCLI.cc:13
BaseSetting * setting
Definition: Interpreter.cc:27
int g
#define PLATFORM_ANDROID
Definition: build-info.hh:17
Wrapper around a SDL_Surface.
Definition: one_of.hh:7
std::string resolve(std::string_view filename) const
Definition: FileContext.cc:81
Interface for display layers.
Definition: Layer.hh:12
Contains the main loop of openMSX.
Definition: Reactor.hh:68
ALWAYS_INLINE unsigned count(const uint8_t *pIn, const uint8_t *pMatch, const uint8_t *pInLimit)
Definition: lz4.cc:207
Definition: gl_mat.hh:23
constexpr vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:269
vecN< 2, int > ivec2
Definition: gl_vec.hh:142
constexpr vecN< N, T > max(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:287
This file implemented 3 utility functions:
Definition: Autofire.cc:9
const FileContext & systemFileContext()
Definition: FileContext.cc:157
uint8_t byte
8 bit unsigned integer
Definition: openmsx.hh:26
constexpr int CONSOLE_ALPHA
How transparent is the console? (0=invisible, 255=opaque) Note that when using a background image on ...
constexpr std::string_view defaultFont
constexpr uint64_t BLINK_RATE
constexpr const char *const filename
constexpr int CHAR_BORDER
void advance(octet_iterator &it, distance_type n)
size_t size(std::string_view utf8)
uint32_t next(octet_iterator &it)
#define UNREACHABLE
Definition: unreachable.hh:38
constexpr auto xrange(T e)
Definition: xrange.hh:155
constexpr auto begin(const zstring_view &x)
Definition: zstring_view.hh:83
constexpr auto end(const zstring_view &x)
Definition: zstring_view.hh:84