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
39 
40 static const string_view defaultFont = "skins/VeraMono.ttf.gz";
41 
42 OSDConsoleRenderer::OSDConsoleRenderer(
43  Reactor& reactor_, CommandConsole& console_,
44  unsigned screenW_, unsigned screenH_,
45  bool openGL_)
46  : Layer(COVER_NONE, Z_CONSOLE)
47  , reactor(reactor_)
48  , display(reactor.getDisplay()) // need to store because still needed during destructor
49  , console(console_)
50  , consoleSetting(console.getConsoleSetting())
51  , screenW(screenW_)
52  , screenH(screenH_)
53  , openGL(openGL_)
54  , consolePlacementSetting(
55  reactor.getCommandController(), "consoleplacement",
56  "position of the console within the emulator",
57  // On Android, console must by default be placed on top, in
58  // order to prevent that it overlaps with the virtual Android
59  // keyboard, which is always placed at the bottom of the screen
60  PLATFORM_ANDROID ? CP_TOP : CP_BOTTOM,
61  EnumSetting<Placement>::Map{
62  {"topleft", CP_TOPLEFT},
63  {"top", CP_TOP},
64  {"topright", CP_TOPRIGHT},
65  {"left", CP_LEFT},
66  {"center", CP_CENTER},
67  {"right", CP_RIGHT},
68  {"bottomleft", CP_BOTTOMLEFT},
69  {"bottom", CP_BOTTOM},
70  {"bottomright", CP_BOTTOMRIGHT}})
71  , fontSizeSetting(reactor.getCommandController(),
72  "consolefontsize", "Size of the console font", 12, 8, 32)
73  , fontSetting(reactor.getCommandController(),
74  "consolefont", "console font file", defaultFont)
75  , consoleColumnsSetting(reactor.getCommandController(),
76  "consolecolumns", "number of columns in the console",
77  initFontAndGetColumns(), 32, 999)
78  , consoleRowsSetting(reactor.getCommandController(),
79  "consolerows", "number of rows in the console",
80  getRows(), 1, 99)
81  , backgroundSetting(reactor.getCommandController(),
82  "consolebackground", "console background file",
83  "skins/ConsoleBackgroundGrey.png")
84 {
85 #if !COMPONENT_GL
86  assert(!openGL);
87 #endif
88  bgPos = bgSize = ivec2(); // recalc on first paint()
89  blink = false;
90  lastBlinkTime = Timer::getTime();
91  lastCursorX = lastCursorY = 0;
92 
93  active = false;
94  activeTime = 0;
96 
97  adjustColRow();
98 
99  // background (only load backgound on first paint())
100  backgroundSetting.setChecker([this](TclObject& value) {
101  loadBackground(value.getString());
102  });
103  // don't yet load background
104 
105  consoleSetting.attach(*this);
106  fontSetting.attach(*this);
107  fontSizeSetting.attach(*this);
108  setActive(consoleSetting.getBoolean());
109 }
110 
111 int OSDConsoleRenderer::initFontAndGetColumns()
112 {
113  // init font
114  fontSetting.setChecker([this](TclObject& value) {
115  loadFont(value.getString().str());
116  });
117  try {
118  loadFont(fontSetting.getString());
119  } catch (MSXException&) {
120  // This will happen when you upgrade from the old .png based
121  // fonts to the new .ttf fonts. So provide a smooth upgrade path.
122  reactor.getCliComm().printWarning(
123  "Loading selected font (", fontSetting.getString(),
124  ") failed. Reverting to default font (", defaultFont, ").");
125  fontSetting.setString(defaultFont);
126  if (font.empty()) {
127  // we can't continue without font
128  throw FatalError("Couldn't load default console font.\n"
129  "Please check your installation.");
130  }
131  }
132 
133  return (((screenW - CHAR_BORDER) / font.getWidth()) * 30) / 32;
134 }
135 int OSDConsoleRenderer::getRows()
136 {
137  // initFontAndGetColumns() must already be called
138  return ((screenH / font.getHeight()) * 6) / 15;
139 }
141 {
142  fontSizeSetting.detach(*this);
143  fontSetting.detach(*this);
144  consoleSetting.detach(*this);
145  setActive(false);
146 }
147 
148 void OSDConsoleRenderer::adjustColRow()
149 {
150  unsigned consoleColumns = std::min<unsigned>(
151  consoleColumnsSetting.getInt(),
152  (screenW - CHAR_BORDER) / font.getWidth());
153  unsigned consoleRows = std::min<unsigned>(
154  consoleRowsSetting.getInt(),
155  screenH / font.getHeight());
156  console.setColumns(consoleColumns);
157  console.setRows(consoleRows);
158 }
159 
160 void OSDConsoleRenderer::update(const Setting& setting)
161 {
162  if (&setting == &consoleSetting) {
163  setActive(consoleSetting.getBoolean());
164  } else if ((&setting == &fontSetting) ||
165  (&setting == &fontSizeSetting)) {
166  loadFont(fontSetting.getString());
167  } else {
168  UNREACHABLE;
169  }
170 }
171 
172 void OSDConsoleRenderer::setActive(bool active_)
173 {
174  if (active == active_) return;
175  active = active_;
176 
177  display.repaintDelayed(40000); // 25 fps
178 
179  activeTime = Timer::getTime();
180 
181  reactor.getInputEventGenerator().setKeyRepeat(active);
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, const ConsoleLine& line,
294  ivec2 pos, byte alpha)
295 {
296  for (auto i : xrange(line.numChunks())) {
297  auto rgb = line.chunkColor(i);
298  string_view text = line.chunkText(i);
299  drawText2(output, text, pos[0], pos[1], alpha, rgb);
300  }
301 }
302 
303 void OSDConsoleRenderer::drawText2(OutputSurface& output, string_view text,
304  int& x, int y, byte alpha, unsigned rgb)
305 {
306  unsigned width;
307  BaseImage* image;
308  if (!getFromCache(text, rgb, image, width)) {
309  string textStr = text.str();
310  SDLSurfacePtr surf;
311  unsigned rgb2 = openGL ? 0xffffff : rgb; // openGL -> always render white
312  try {
313  unsigned dummyHeight;
314  font.getSize(textStr, width, dummyHeight);
315  surf = font.render(textStr,
316  (rgb2 >> 16) & 0xff,
317  (rgb2 >> 8) & 0xff,
318  (rgb2 >> 0) & 0xff);
319  } catch (MSXException& e) {
320  static bool alreadyPrinted = false;
321  if (!alreadyPrinted) {
322  alreadyPrinted = true;
323  reactor.getCliComm().printWarning(
324  "Invalid console text (invalid UTF-8): ",
325  e.getMessage());
326  }
327  return; // don't cache negative results
328  }
329  std::unique_ptr<BaseImage> image2;
330  if (!surf) {
331  // nothing was rendered, so do nothing
332  } else if (!openGL) {
333  image2 = std::make_unique<SDLImage>(output, std::move(surf));
334  }
335 #if COMPONENT_GL
336  else {
337  image2 = std::make_unique<GLImage>(output, std::move(surf));
338  }
339 #endif
340  image = image2.get();
341  insertInCache(std::move(textStr), rgb, std::move(image2), width);
342  }
343  if (image) {
344  if (openGL) {
345  byte r = (rgb >> 16) & 0xff;
346  byte g = (rgb >> 8) & 0xff;
347  byte b = (rgb >> 0) & 0xff;
348  image->draw(output, ivec2(x, y), r, g, b, alpha);
349  } else {
350  image->draw(output, ivec2(x, y), alpha);
351  }
352  }
353  x += width; // in case of trailing whitespace width != image->getWidth()
354 }
355 
356 bool OSDConsoleRenderer::getFromCache(string_view text, unsigned rgb,
357  BaseImage*& image, unsigned& width)
358 {
359  // Items are LRU sorted, so the next requested items will often be
360  // located right in front of the previously found item. (Though
361  // duplicate items (e.g. the command prompt '> ') degrade this
362  // heuristic).
363  auto it = cacheHint;
364  // For openGL ignore rgb
365  if ((it->text == text) && (openGL || (it->rgb == rgb))) {
366  goto found;
367  }
368 
369  // Search the whole cache for a match. If the cache is big enough then
370  // all N items used for rendering the previous frame should be located
371  // in the N first positions in the cache (in approx reverse order).
372  for (it = begin(textCache); it != end(textCache); ++it) {
373  if (it->text != text) continue;
374  if (!openGL && (it->rgb != rgb)) continue;
375 found: image = it->image.get();
376  width = it->width;
377  cacheHint = it;
378  if (it != begin(textCache)) {
379  --cacheHint; // likely candiate for next item
380  // move to front (to keep in LRU order)
381  textCache.splice(begin(textCache), textCache, it);
382  }
383  return true;
384  }
385  return false;
386 }
387 
388 void OSDConsoleRenderer::insertInCache(
389  string text, unsigned rgb, std::unique_ptr<BaseImage> image,
390  unsigned width)
391 {
392  static const unsigned MAX_TEXT_CACHE_SIZE = 250;
393  if (textCache.size() == MAX_TEXT_CACHE_SIZE) {
394  // flush the least recently used entry
395  auto it = std::prev(std::end(textCache));
396  if (it == cacheHint) {
397  cacheHint = begin(textCache);
398  }
399  textCache.pop_back();
400  }
401  textCache.emplace_front(std::move(text), rgb, std::move(image), width);
402 }
403 
404 void OSDConsoleRenderer::clearCache()
405 {
406  // cacheHint must always point to a valid item, so insert a dummy entry
407  textCache.clear();
408  textCache.emplace_back(string{}, 0, nullptr, 0);
409  cacheHint = begin(textCache);
410 }
411 
412 gl::ivec2 OSDConsoleRenderer::getTextPos(int cursorX, int cursorY)
413 {
414  return bgPos + ivec2(CHAR_BORDER + cursorX * font.getWidth(),
415  bgSize[1] - (font.getHeight() * (cursorY + 1)) - 1);
416 }
417 
418 void OSDConsoleRenderer::paint(OutputSurface& output)
419 {
420  byte visibility = getVisibility();
421  if (!visibility) return;
422 
423  if (updateConsoleRect()) {
424  try {
425  loadBackground(backgroundSetting.getString());
426  } catch (MSXException& e) {
427  reactor.getCliComm().printWarning(e.getMessage());
428  }
429  }
430 
431  // draw the background image if there is one
432  if (!backgroundImage) {
433  // no background image, try to create an empty one
434  try {
435  if (!openGL) {
436  backgroundImage = std::make_unique<SDLImage>(
437  output, bgSize, CONSOLE_ALPHA);
438  }
439 #if COMPONENT_GL
440  else {
441  backgroundImage = std::make_unique<GLImage>(
442  output, bgSize, CONSOLE_ALPHA);
443  }
444 #endif
445  } catch (MSXException&) {
446  // nothing
447  }
448  }
449  if (backgroundImage) {
450  backgroundImage->draw(output, bgPos, visibility);
451  }
452 
453  for (auto loop : xrange(bgSize[1] / font.getHeight())) {
454  drawText(output,
455  console.getLine(loop + console.getScrollBack()),
456  getTextPos(0, loop), visibility);
457  }
458 
459  // Check if the blink period is over
460  auto now = Timer::getTime();
461  if (lastBlinkTime < now) {
462  lastBlinkTime = now + BLINK_RATE;
463  blink = !blink;
464  }
465 
466  unsigned cursorX, cursorY;
467  console.getCursorPosition(cursorX, cursorY);
468  if ((cursorX != lastCursorX) || (cursorY != lastCursorY)) {
469  blink = true; // force cursor
470  lastBlinkTime = now + BLINK_RATE; // maximum time
471  lastCursorX = cursorX;
472  lastCursorY = cursorY;
473  }
474  if (blink && (console.getScrollBack() == 0)) {
475  drawText(output, ConsoleLine("_"),
476  getTextPos(cursorX, cursorY), visibility);
477  }
478 }
479 
480 } // namespace openmsx
void setChecker(std::function< void(TclObject &)> checkFunc_)
Set value-check-callback.
Definition: Setting.hh:151
void getCursorPosition(unsigned &xPosition, unsigned &yPosition) const
T getEnum() const noexcept
Definition: EnumSetting.hh:92
Contains the main loop of openMSX.
Definition: Reactor.hh:66
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:148
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
string_view getString() const
Definition: TclObject.cc:102
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
std::string resolve(string_view filename) const
Definition: FileContext.cc:76
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:314
This class represents a single text line in the console.
auto begin(const string_view &x)
Definition: string_view.hh:151
void attach(Observer< T > &observer)
Definition: Subject.hh:50
bool getBoolean() const noexcept
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:367
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
string_view getString() const noexcept
bool empty() const
Definition: string_view.hh:45
This class implements a (close approximation) of the std::string_view class.
Definition: string_view.hh:16
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
int getInt() const noexcept
void detach(Observer< T > &observer)
Definition: Subject.hh:56
CliComm & getCliComm()
Definition: Reactor.cc:304
uint64_t getTime()
Get current (real) time in us.
Definition: Timer.cc:8
OutputSurface * getOutputSurface()
Definition: Display.cc:118
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:152
#define UNREACHABLE
Definition: unreachable.hh:38
unsigned getRows() const