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