openMSX
OSDConsoleRenderer.cc
Go to the documentation of this file.
2#include "CommandConsole.hh"
3#include "BooleanSetting.hh"
4#include "SDLImage.hh"
5#include "Display.hh"
6#include "Timer.hh"
7#include "FileContext.hh"
8#include "CliComm.hh"
9#include "Reactor.hh"
10#include "MSXException.hh"
11#include "narrow.hh"
12#include "openmsx.hh"
13#include "one_of.hh"
14#include "unreachable.hh"
15#include "utf8_unchecked.hh"
16#include "xrange.hh"
17#include <algorithm>
18#include <cassert>
19#include <memory>
20
21#include "components.hh"
22#if COMPONENT_GL
23#include "GLImage.hh"
24#endif
25
26using namespace gl;
27
28namespace openmsx {
29
34static constexpr int CONSOLE_ALPHA = 180;
35static constexpr uint64_t BLINK_RATE = 500000; // us
36static constexpr int CHAR_BORDER = 4;
37
38
39// class OSDConsoleRenderer
40
41static constexpr std::string_view defaultFont = "skins/VeraMono.ttf.gz";
42
44 Reactor& reactor_, CommandConsole& console_,
45 int screenW_, int screenH_, 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_TOP_LEFT},
63 {"top", CP_TOP},
64 {"topright", CP_TOP_RIGHT},
65 {"left", CP_LEFT},
66 {"center", CP_CENTER},
67 {"right", CP_RIGHT},
68 {"bottomleft", CP_BOTTOM_LEFT},
69 {"bottom", CP_BOTTOM},
70 {"bottomright", CP_BOTTOM_RIGHT}})
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 , lastBlinkTime(Timer::getTime())
85{
86#if !COMPONENT_GL
87 assert(!openGL);
88#endif
89 setCoverage(COVER_PARTIAL);
90
91 adjustColRow();
92
93 // background (only load background on first paint())
94 backgroundSetting.setChecker([this](TclObject& value) {
95 loadBackground(value.getString());
96 });
97 // don't yet load background
98
99 consoleSetting.attach(*this);
100 fontSetting.attach(*this);
101 fontSizeSetting.attach(*this);
102 setActive(consoleSetting.getBoolean());
103}
104
105int OSDConsoleRenderer::initFontAndGetColumns()
106{
107 // init font
108 fontSetting.setChecker([this](TclObject& value) {
109 loadFont(value.getString());
110 });
111 try {
112 loadFont(fontSetting.getString());
113 } catch (MSXException&) {
114 // This will happen when you upgrade from the old .png based
115 // fonts to the new .ttf fonts. So provide a smooth upgrade path.
116 reactor.getCliComm().printWarning(
117 "Loading selected font (", fontSetting.getString(),
118 ") failed. Reverting to default font (", defaultFont, ").");
119 fontSetting.setString(defaultFont);
120 if (font.empty()) {
121 // we can't continue without font
122 throw FatalError("Couldn't load default console font.\n"
123 "Please check your installation.");
124 }
125 }
126
127 return (((screenW - CHAR_BORDER) / font.getWidth()) * 30) / 32;
128}
129int OSDConsoleRenderer::getRows()
130{
131 // initFontAndGetColumns() must already be called
132 return ((screenH / font.getHeight()) * 6) / 15;
133}
134OSDConsoleRenderer::~OSDConsoleRenderer()
135{
136 fontSizeSetting.detach(*this);
137 fontSetting.detach(*this);
138 consoleSetting.detach(*this);
139 setActive(false);
140}
141
142void OSDConsoleRenderer::adjustColRow()
143{
144 unsigned consoleColumns = std::min<unsigned>(
145 consoleColumnsSetting.getInt(),
146 (screenW - CHAR_BORDER) / font.getWidth());
147 unsigned consoleRows = std::min<unsigned>(
148 consoleRowsSetting.getInt(),
149 screenH / font.getHeight());
150 console.setColumns(consoleColumns);
151 console.setRows(consoleRows);
152}
153
154void OSDConsoleRenderer::update(const Setting& setting) noexcept
155{
156 if (&setting == &consoleSetting) {
157 setActive(consoleSetting.getBoolean());
158 } else if (&setting == one_of(&fontSetting, &fontSizeSetting)) {
159 loadFont(fontSetting.getString());
160 } else {
162 }
163}
164
165void OSDConsoleRenderer::setActive(bool active_)
166{
167 if (active == active_) return;
168 active = active_;
169
170 display.repaintDelayed(40000); // 25 fps
171
172 activeTime = Timer::getTime();
173}
174
175byte OSDConsoleRenderer::getVisibility() const
176{
177 const uint64_t FADE_IN_DURATION = 100000;
178 const uint64_t FADE_OUT_DURATION = 150000;
179
180 auto now = Timer::getTime();
181 auto dur = now - activeTime;
182 if (active) {
183 if (dur > FADE_IN_DURATION) {
184 return 255;
185 } else {
186 display.repaintDelayed(40000); // 25 fps
187 return byte((dur * 255) / FADE_IN_DURATION);
188 }
189 } else {
190 if (dur > FADE_OUT_DURATION) {
191 return 0;
192 } else {
193 display.repaintDelayed(40000); // 25 fps
194 return byte(255 - ((dur * 255) / FADE_OUT_DURATION));
195 }
196 }
197}
198
199bool OSDConsoleRenderer::updateConsoleRect()
200{
201 adjustColRow();
202
203 ivec2 size((font.getWidth() * narrow<int>(console.getColumns())) + CHAR_BORDER,
204 font.getHeight() * narrow<int>(console.getRows()));
205
206 // TODO use setting listener in the future
207 ivec2 pos;
208 switch (consolePlacementSetting.getEnum()) {
209 case CP_TOP_LEFT:
210 case CP_LEFT:
211 case CP_BOTTOM_LEFT:
212 pos[0] = 0;
213 break;
214 case CP_TOP_RIGHT:
215 case CP_RIGHT:
216 case CP_BOTTOM_RIGHT:
217 pos[0] = (screenW - size[0]);
218 break;
219 case CP_TOP:
220 case CP_CENTER:
221 case CP_BOTTOM:
222 default:
223 pos[0] = (screenW - size[0]) / 2;
224 break;
225 }
226 switch (consolePlacementSetting.getEnum()) {
227 case CP_TOP_LEFT:
228 case CP_TOP:
229 case CP_TOP_RIGHT:
230 pos[1] = 0;
231 break;
232 case CP_LEFT:
233 case CP_CENTER:
234 case CP_RIGHT:
235 pos[1] = (screenH - size[1]) / 2;
236 break;
237 case CP_BOTTOM_LEFT:
238 case CP_BOTTOM:
239 case CP_BOTTOM_RIGHT:
240 default:
241 pos[1] = (screenH - size[1]);
242 break;
243 }
244
245 bool result = (pos != bgPos) || (size != bgSize);
246 bgPos = pos;
247 bgSize = size;
248 return result;
249}
250
251void OSDConsoleRenderer::loadFont(std::string_view value)
252{
253 auto filename = systemFileContext().resolve(value);
254 auto newFont = TTFFont(filename, fontSizeSetting.getInt());
255 if (!newFont.isFixedWidth()) {
256 throw MSXException(value, " is not a monospaced font");
257 }
258 font = std::move(newFont);
259 clearCache();
260}
261
262void OSDConsoleRenderer::loadBackground(std::string_view value)
263{
264 if (value.empty()) {
265 backgroundImage.reset();
266 return;
267 }
268 auto* output = display.getOutputSurface();
269 if (!output) {
270 backgroundImage.reset();
271 return;
272 }
273 auto filename = systemFileContext().resolve(value);
274 if (!openGL) {
275 backgroundImage = std::make_unique<SDLImage>(*output, filename, bgSize);
276 }
277#if COMPONENT_GL
278 else {
279 backgroundImage = std::make_unique<GLImage>(*output, filename, bgSize);
280 }
281#endif
282}
283
284void OSDConsoleRenderer::drawText(OutputSurface& output, std::string_view text,
285 int cx, int cy, byte alpha, uint32_t rgb)
286{
287 auto xy = getTextPos(cx, cy);
288 auto [inCache, image, width] = getFromCache(text, rgb);
289 if (!inCache) {
290 std::string textStr(text);
291 SDLSurfacePtr surf;
292 uint32_t rgb2 = openGL ? 0xffffff : rgb; // openGL -> always render white
293 try {
294 width = font.getSize(textStr)[0];
295 surf = font.render(textStr,
296 (rgb2 >> 16) & 0xff,
297 (rgb2 >> 8) & 0xff,
298 (rgb2 >> 0) & 0xff);
299 } catch (MSXException& e) {
300 static bool alreadyPrinted = false;
301 if (!alreadyPrinted) {
302 alreadyPrinted = true;
303 reactor.getCliComm().printWarning(
304 "Invalid console text (invalid UTF-8): ",
305 e.getMessage());
306 }
307 return; // don't cache negative results
308 }
309 std::unique_ptr<BaseImage> image2;
310 if (!surf) {
311 // nothing was rendered, so do nothing
312 } else if (!openGL) {
313 image2 = std::make_unique<SDLImage>(output, std::move(surf));
314 }
315#if COMPONENT_GL
316 else {
317 image2 = std::make_unique<GLImage>(output, std::move(surf));
318 }
319#endif
320 image = image2.get();
321 insertInCache(std::move(textStr), rgb, std::move(image2), width);
322 }
323 if (image) {
324 if (openGL) {
325 byte r = (rgb >> 16) & 0xff;
326 byte g = (rgb >> 8) & 0xff;
327 byte b = (rgb >> 0) & 0xff;
328 image->draw(output, xy, r, g, b, alpha);
329 } else {
330 image->draw(output, xy, alpha);
331 }
332 }
333}
334
335std::tuple<bool, BaseImage*, unsigned> OSDConsoleRenderer::getFromCache(
336 std::string_view text, uint32_t rgb)
337{
338 // Items are LRU sorted, so the next requested items will often be
339 // located right in front of the previously found item. (Though
340 // duplicate items (e.g. the command prompt '> ') degrade this
341 // heuristic).
342 auto it = cacheHint;
343 // For openGL ignore rgb
344 if ((it->text == text) && (openGL || (it->rgb == rgb))) {
345 goto found;
346 }
347
348 // Search the whole cache for a match. If the cache is big enough then
349 // all N items used for rendering the previous frame should be located
350 // in the N first positions in the cache (in approx reverse order).
351 for (it = begin(textCache); it != end(textCache); ++it) {
352 if (it->text != text) continue;
353 if (!openGL && (it->rgb != rgb)) continue;
354found: BaseImage* image = it->image.get();
355 unsigned width = it->width;
356 cacheHint = it;
357 if (it != begin(textCache)) {
358 --cacheHint; // likely candidate for next item
359 // move to front (to keep in LRU order)
360 textCache.splice(begin(textCache), textCache, it);
361 }
362 return {true, image, width};
363 }
364 return {false, nullptr, 0};
365}
366
367void OSDConsoleRenderer::insertInCache(
368 std::string text, uint32_t rgb, std::unique_ptr<BaseImage> image,
369 unsigned width)
370{
371 constexpr unsigned MAX_TEXT_CACHE_SIZE = 250;
372 if (textCache.size() == MAX_TEXT_CACHE_SIZE) {
373 // flush the least recently used entry
374 if (auto it = std::prev(std::end(textCache)); it == cacheHint) {
375 cacheHint = begin(textCache);
376 }
377 textCache.pop_back();
378 }
379 textCache.emplace_front(std::move(text), rgb, std::move(image), width);
380}
381
382void OSDConsoleRenderer::clearCache()
383{
384 // cacheHint must always point to a valid item, so insert a dummy entry
385 textCache.clear();
386 textCache.emplace_back(std::string{}, 0, nullptr, 0);
387 cacheHint = begin(textCache);
388}
389
390gl::ivec2 OSDConsoleRenderer::getTextPos(int cursorX, int cursorY) const
391{
392 return bgPos + ivec2(CHAR_BORDER + cursorX * font.getWidth(),
393 bgSize[1] - (font.getHeight() * (cursorY + 1)) - 1);
394}
395
396void OSDConsoleRenderer::paint(OutputSurface& output)
397{
398 byte visibility = getVisibility();
399 if (!visibility) return;
400
401 if (updateConsoleRect()) {
402 try {
403 loadBackground(backgroundSetting.getString());
404 } catch (MSXException& e) {
405 reactor.getCliComm().printWarning(e.getMessage());
406 }
407 }
408
409 // draw the background image if there is one
410 if (!backgroundImage) {
411 // no background image, try to create an empty one
412 try {
413 if (!openGL) {
414 backgroundImage = std::make_unique<SDLImage>(
415 output, bgSize, CONSOLE_ALPHA);
416 }
417#if COMPONENT_GL
418 else {
419 backgroundImage = std::make_unique<GLImage>(
420 output, bgSize, CONSOLE_ALPHA);
421 }
422#endif
423 } catch (MSXException&) {
424 // nothing
425 }
426 }
427 if (backgroundImage) {
428 backgroundImage->draw(output, bgPos, visibility);
429 }
430
431 drawConsoleText(output, visibility);
432
433 // Check if the blink period is over
434 auto now = Timer::getTime();
435 if (lastBlinkTime < now) {
436 lastBlinkTime = now + BLINK_RATE;
437 blink = !blink;
438 }
439
440 auto [cursorX, cursorY] = console.getCursorPosition();
441 if ((unsigned(cursorX) != lastCursorX) || (unsigned(cursorY) != lastCursorY)) {
442 blink = true; // force cursor
443 lastBlinkTime = now + BLINK_RATE; // maximum time
444 lastCursorX = cursorX;
445 lastCursorY = cursorY;
446 }
447 if (blink && (console.getScrollBack() == 0)) {
448 drawText(output, "_", cursorX, cursorY, visibility, 0xffffff);
449 }
450}
451
452void OSDConsoleRenderer::drawConsoleText(OutputSurface& output, byte visibility)
453{
454 const auto rows = console.getRows();
455 const auto columns = console.getColumns();
456 const auto scrollBack = console.getScrollBack();
457 const auto& lines = console.getLines();
458
459 // search first visible line
460 auto [cursorY_, subLine, lineIdx] = [&] {
461 size_t count = 0;
462 size_t target = rows + scrollBack;
463 for (auto idx : xrange(lines.size())) {
464 count += std::max(size_t(1), (lines[idx].numChars() + columns - 1) / columns);
465 if (count >= target) {
466 return std::tuple(int(rows - 1), int(count - target), idx);
467 }
468 }
469 int y = int(count - 1 - scrollBack);
470 return std::tuple(y, 0, lines.size() - 1);
471 }();
472 int cursorY = cursorY_; // clang workaround
473
474 // setup for first (partial) line
475 std::string_view text = lines[lineIdx].str();
476 auto it = begin(text);
477 auto endIt = end(text);
478 utf8::unchecked::advance(it, subLine * columns);
479 std::string_view::size_type idx = it - begin(text);
480 unsigned chunkIdx = 1;
481 const auto& chunks0 = lines[lineIdx].getChunks();
482 while ((chunkIdx < chunks0.size()) && (chunks0[chunkIdx].pos <= idx)) {
483 ++chunkIdx;
484 }
485
486 // draw each console-line (long text-lines are split over multiple console-lines)
487 while (true) {
488 auto remainingColumns = columns;
489 const auto& chunks = lines[lineIdx].getChunks();
490 if (!chunks.empty()) {
491 // draw chunks of same color
492 while (remainingColumns && (it < endIt)) {
493 auto startColumn = remainingColumns;
494 auto e = it;
495 auto nextColorIt = (chunkIdx == chunks.size())
496 ? endIt
497 : begin(text) + chunks[chunkIdx].pos;
498 auto maxIt = std::min(endIt, nextColorIt);
499 while (remainingColumns && (e < maxIt)) {
501 --remainingColumns;
502 }
503 //std::string_view subText(it, e); // c++20
504 std::string_view subText(&*it, e - it);
505 auto rgb = chunks[chunkIdx - 1].rgb;
506 auto cursorX = narrow<int>(columns - startColumn);
507 drawText(output, subText, cursorX, cursorY, visibility, rgb);
508
509 // next chunk
510 it = e;
511 // move to next color?
512 if (e == nextColorIt) ++chunkIdx;
513 }
514 }
515 // all console-lines drawn?
516 if (--cursorY < 0) break;
517 // move to next text-line?
518 if (it == endIt) {
519 --lineIdx;
520 text = lines[lineIdx].str();
521 it = begin(text);
522 endIt = end(text);
523 chunkIdx = 1;
524 }
525 }
526}
527
528} // namespace openmsx
std::string image
Definition: HDImageCLI.cc:13
BaseSetting * setting
Definition: Interpreter.cc:28
int g
#define PLATFORM_ANDROID
Definition: build-info.hh:17
Wrapper around a SDL_Surface.
Definition: one_of.hh:7
std::string resolve(std::string_view filename) const
Definition: FileContext.cc:79
Interface for display layers.
Definition: Layer.hh:12
OSDConsoleRenderer(Reactor &reactor, CommandConsole &console, int screenW, int screenH, bool openGL)
Contains the main loop of openMSX.
Definition: Reactor.hh:68
ALWAYS_INLINE unsigned count(const uint8_t *pIn, const uint8_t *pMatch, const uint8_t *pInLimit)
Definition: lz4.cc:147
constexpr double e
Definition: Math.hh:20
Definition: gl_mat.hh:23
vecN< 2, int > ivec2
Definition: gl_vec.hh:152
constexpr vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:266
constexpr vecN< N, T > max(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:284
This file implemented 3 utility functions:
Definition: Autofire.cc:9
const FileContext & systemFileContext()
Definition: FileContext.cc:155
uint8_t byte
8 bit unsigned integer
Definition: openmsx.hh:26
void advance(octet_iterator &it, distance_type n)
size_t size(std::string_view utf8)
uint32_t next(octet_iterator &it)
#define UNREACHABLE
Definition: unreachable.hh:38
constexpr auto xrange(T e)
Definition: xrange.hh:133
constexpr auto begin(const zstring_view &x)
constexpr auto end(const zstring_view &x)