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