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