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