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