openMSX
Display.cc
Go to the documentation of this file.
1#include "Display.hh"
2
3#include "RendererFactory.hh"
4#include "ImGuiManager.hh"
5#include "Layer.hh"
6#include "OutputSurface.hh"
7#include "VideoLayer.hh"
8#include "VideoSystem.hh"
10
11#include "BooleanSetting.hh"
12#include "CommandException.hh"
13#include "EventDistributor.hh"
14#include "Event.hh"
15#include "FileOperations.hh"
16#include "FileContext.hh"
17#include "CliComm.hh"
18#include "Timer.hh"
19#include "IntegerSetting.hh"
20#include "EnumSetting.hh"
21#include "Reactor.hh"
22#include "MSXMotherBoard.hh"
23#include "HardwareConfig.hh"
24#include "TclArgParser.hh"
25#include "XMLElement.hh"
26#include "Version.hh"
27
28#include "narrow.hh"
29#include "outer.hh"
30#include "ranges.hh"
31#include "stl.hh"
32#include "unreachable.hh"
33#include "xrange.hh"
34
35#include <array>
36#include <cassert>
37
38using std::string;
39
40namespace openmsx {
41
43 : RTSchedulable(reactor_.getRTScheduler())
44 , screenShotCmd(reactor_.getCommandController())
45 , fpsInfo(reactor_.getOpenMSXInfoCommand())
46 , osdGui(reactor_.getCommandController(), *this)
47 , reactor(reactor_)
48 , renderSettings(reactor.getCommandController())
49{
50 frameDurationSum = 0;
51 repeat(NUM_FRAME_DURATIONS, [&] {
52 frameDurations.push_front(20);
53 frameDurationSum += 20;
54 });
55 prevTimeStamp = Timer::getTime();
56
57 EventDistributor& eventDistributor = reactor.getEventDistributor();
58 using enum EventType;
59 for (auto type : {FINISH_FRAME, SWITCH_RENDERER, MACHINE_LOADED, WINDOW}) {
60 eventDistributor.registerEventListener(type, *this);
61 }
62
63 renderSettings.getRendererSetting().attach(*this);
64}
65
67{
68 renderSettings.getRendererSetting().detach(*this);
69
70 EventDistributor& eventDistributor = reactor.getEventDistributor();
71 using enum EventType;
72 for (auto type : {WINDOW, MACHINE_LOADED, SWITCH_RENDERER, FINISH_FRAME}) {
73 eventDistributor.unregisterEventListener(type, *this);
74 }
75
76 resetVideoSystem();
77
78 assert(listeners.empty());
79}
80
82{
83 assert(!videoSystem);
84 assert(currentRenderer == RenderSettings::RendererID::UNINITIALIZED);
85 assert(!switchInProgress);
86 currentRenderer = renderSettings.getRenderer();
87 switchInProgress = true;
88 doRendererSwitch();
89}
90
92{
93 assert(videoSystem);
94 return *videoSystem;
95}
96
98{
99 return videoSystem ? videoSystem->getOutputSurface() : nullptr;
100}
101
102void Display::resetVideoSystem()
103{
104 videoSystem.reset();
105 // At this point all layers except for the Video9000 layer
106 // should be gone.
107 //assert(layers.empty());
108}
109
111{
112 return reactor.getCliComm();
113}
114
116{
117 assert(!contains(listeners, &listener));
118 listeners.push_back(&listener);
119}
120
122{
123 move_pop_back(listeners, rfind_unguarded(listeners, &listener));
124}
125
127{
128 auto it = ranges::find_if(layers, &Layer::isActive);
129 return (it != layers.end()) ? *it : nullptr;
130}
131
132Display::Layers::iterator Display::baseLayer()
133{
134 // Note: It is possible to cache this, but since the number of layers is
135 // low at the moment, it's not really worth it.
136 auto it = end(layers);
137 while (true) {
138 if (it == begin(layers)) {
139 // There should always be at least one opaque layer.
140 // TODO: This is not true for DummyVideoSystem.
141 // Anyway, a missing layer will probably stand out visually,
142 // so do we really have to assert on it?
143 //UNREACHABLE;
144 return it;
145 }
146 --it;
147 if ((*it)->getCoverage() == Layer::Coverage::FULL) return it;
148 }
149}
150
151void Display::executeRT()
152{
153 repaint();
154}
155
156bool Display::signalEvent(const Event& event)
157{
158 std::visit(overloaded{
159 [&](const FinishFrameEvent& e) {
160 if (e.needRender()) {
161 repaint();
162 reactor.getEventDistributor().distributeEvent(FrameDrawnEvent());
163 }
164 },
165 [&](const SwitchRendererEvent& /*e*/) {
166 doRendererSwitch(); // might throw
167 },
168 [&](const MachineLoadedEvent& /*e*/) {
169 videoSystem->updateWindowTitle();
170 },
171 [&](const WindowEvent& e) {
172 const auto& evt = e.getSdlWindowEvent();
173 if (evt.event == SDL_WINDOWEVENT_EXPOSED) {
174 // Don't render too often, and certainly not when the screen
175 // will anyway soon be rendered.
176 repaintDelayed(100 * 1000); // 10fps
177 }
178 if (PLATFORM_ANDROID && e.isMainWindow() &&
179 evt.event == one_of(SDL_WINDOWEVENT_FOCUS_GAINED, SDL_WINDOWEVENT_FOCUS_LOST)) {
180 // On Android, the rendering must be frozen when the app is sent to
181 // the background, because Android takes away all graphics resources
182 // from the app. It simply destroys the entire graphics context.
183 // Though, a repaint() must happen within the focus-lost event
184 // so that the SDL Android port realizes that the graphics context
185 // is gone and will re-build it again on the first flush to the
186 // surface after the focus has been regained.
187
188 // Perform a repaint before updating the renderFrozen flag:
189 // -When loosing the focus, this repaint will flush a last
190 // time the SDL surface, making sure that the Android SDL
191 // port discovers that the graphics context is gone.
192 // -When gaining the focus, this repaint does nothing as
193 // the renderFrozen flag is still false
194 repaint();
195 bool lost = evt.event == SDL_WINDOWEVENT_FOCUS_LOST;
196 ad_printf("Setting renderFrozen to %d", lost);
197 renderFrozen = lost;
198 }
199 },
200 [](const EventBase&) { /*ignore*/ }
201 }, event);
202 return false;
203}
204
206{
207 string title = Version::full();
208 if (!Version::RELEASE) {
209 strAppend(title, " [", BUILD_FLAVOUR, ']');
210 }
211 if (MSXMotherBoard* motherboard = reactor.getMotherBoard()) {
212 if (const HardwareConfig* machine = motherboard->getMachineConfig()) {
213 const auto& config = machine->getConfig();
214 strAppend(title, " - ",
215 config.getChild("info").getChildData("manufacturer"), ' ',
216 config.getChild("info").getChildData("code"));
217 }
218 }
219 return title;
220}
221
223{
224 if (auto pos = videoSystem->getWindowPosition()) {
226 }
227 return retrieveWindowPosition();
228}
229
231{
233 videoSystem->setWindowPosition(pos);
234}
235
240
245
247{
248 int factor = renderSettings.getScaleFactor();
249 return {320 * factor, 240 * factor};
250}
251
252float Display::getFps() const
253{
254 return 1000000.0f * NUM_FRAME_DURATIONS / narrow_cast<float>(frameDurationSum);
255}
256
257void Display::update(const Setting& setting) noexcept
258{
259 assert(&setting == &renderSettings.getRendererSetting()); (void)setting;
260 checkRendererSwitch();
261}
262
263void Display::checkRendererSwitch()
264{
265 if (switchInProgress) {
266 // This method only queues a request to switch renderer (see
267 // comments below why). If there already is such a request
268 // queued we don't need to do it again.
269 return;
270 }
271 auto newRenderer = renderSettings.getRenderer();
272 if (newRenderer != currentRenderer) {
273 currentRenderer = newRenderer;
274 // don't do the actual switching in the Tcl callback
275 // it seems creating and destroying Settings (= Tcl vars)
276 // causes problems???
277 switchInProgress = true;
278 reactor.getEventDistributor().distributeEvent(SwitchRendererEvent());
279 }
280}
281
282void Display::doRendererSwitch()
283{
284 assert(switchInProgress);
285
286 bool success = false;
287 while (!success) {
288 try {
289 doRendererSwitch2();
290 success = true;
291 } catch (MSXException& e) {
292 auto& rendererSetting = renderSettings.getRendererSetting();
293 string errorMsg = strCat(
294 "Couldn't activate renderer ",
295 rendererSetting.getString(),
296 ": ", e.getMessage());
297 // now try some things that might work against this:
298 auto& scaleFactorSetting = renderSettings.getScaleFactorSetting();
299 auto curVal = scaleFactorSetting.getInt();
300 if (curVal == MIN_SCALE_FACTOR) {
301 throw FatalError(
302 e.getMessage(),
303 " (and I have no other ideas to try...)"); // give up and die... :(
304 }
305 strAppend(errorMsg, "\nTrying to decrease scale_factor setting from ",
306 curVal, " to ", curVal - 1, "...");
307 scaleFactorSetting.setInt(curVal - 1);
308 getCliComm().printWarning(errorMsg);
309 }
310 }
311
312 switchInProgress = false;
313}
314
315void Display::doRendererSwitch2()
316{
317 for (auto& l : listeners) {
318 l->preVideoSystemChange();
319 }
320
321 resetVideoSystem();
322 videoSystem = RendererFactory::createVideoSystem(reactor);
323
324 for (auto& l : listeners) {
325 l->postVideoSystemChange();
326 }
327}
328
330{
331 if (switchInProgress) {
332 // The checkRendererSwitch() method will queue a
333 // SWITCH_RENDERER_EVENT, but before that event is handled
334 // we shouldn't do any repaints (with inconsistent setting
335 // values and render objects). This can happen when this
336 // method gets called because of a DELAYED_REPAINT_EVENT
337 // (DELAYED_REPAINT_EVENT was already queued before
338 // SWITCH_RENDERER_EVENT is queued).
339 return;
340 }
341
342 cancelRT(); // cancel delayed repaint
343
344 if (!renderFrozen) {
345 assert(videoSystem);
346 if (OutputSurface* surface = videoSystem->getOutputSurface()) {
347 repaintImpl(*surface);
348 videoSystem->flush();
349 }
350 }
351
352 // update fps statistics
353 auto now = Timer::getTime();
354 auto duration = now - prevTimeStamp;
355 prevTimeStamp = now;
356 frameDurationSum += duration - frameDurations.pop_back();
357 frameDurations.push_front(duration);
358
359 // TODO maybe revisit this later (and/or simplify other calls to repaintDelayed())
360 // This ensures a minimum framerate for ImGui
361 repaintDelayed(40 * 1000); // 25fps
362}
363
365{
366 for (auto it = baseLayer(); it != end(layers); ++it) {
367 if ((*it)->getCoverage() != Layer::Coverage::NONE) {
368 (*it)->paint(surface);
369 }
370 }
371}
372
374{
375 // Request a repaint from the VideoSystem. This may call repaintImpl()
376 // directly or for example defer to a signal callback on VisibleSurface.
377 videoSystem->repaint();
378}
379
380void Display::repaintDelayed(uint64_t delta)
381{
382 if (isPendingRT()) {
383 // already a pending repaint
384 return;
385 }
386 scheduleRT(unsigned(delta));
387}
388
390{
391 auto z = layer.getZ();
392 auto it = ranges::find_if(layers, [&](const Layer* l) { return l->getZ() > z; });
393 layers.insert(it, &layer);
394 layer.setDisplay(*this);
395}
396
398{
399 layers.erase(rfind_unguarded(layers, &layer));
400}
401
403{
404 auto oldPos = rfind_unguarded(layers, &layer);
405 auto z = layer.getZ();
406 auto newPos = ranges::find_if(layers, [&](const Layer* l) { return l->getZ() >= z; });
407
408 if (oldPos == newPos) {
409 return;
410 } else if (oldPos < newPos) {
411 std::rotate(oldPos, oldPos + 1, newPos);
412 } else {
413 std::rotate(newPos, oldPos, oldPos + 1);
414 }
415}
416
417
418// ScreenShotCmd
419
420Display::ScreenShotCmd::ScreenShotCmd(CommandController& commandController_)
421 : Command(commandController_, "screenshot")
422{
423}
424
425void Display::ScreenShotCmd::execute(std::span<const TclObject> tokens, TclObject& result)
426{
427 std::string_view prefix = "openmsx";
428 bool rawShot = false;
429 bool msxOnly = false;
430 bool doubleSize = false;
431 bool withOsd = false;
432 std::array info = {
433 valueArg("-prefix", prefix),
434 flagArg("-raw", rawShot),
435 flagArg("-msxonly", msxOnly),
436 flagArg("-doublesize", doubleSize),
437 flagArg("-with-osd", withOsd)
438 };
439 auto arguments = parseTclArgs(getInterpreter(), tokens.subspan(1), info);
440
441 auto& display = OUTER(Display, screenShotCmd);
442 if (msxOnly) {
443 display.getCliComm().printWarning(
444 "The -msxonly option has been deprecated and will "
445 "be removed in a future release. Instead, use the "
446 "-raw option for the same effect.");
447 rawShot = true;
448 }
449 if (doubleSize && !rawShot) {
450 throw CommandException("-doublesize option can only be used in "
451 "combination with -raw");
452 }
453 if (rawShot && withOsd) {
454 throw CommandException("-with-osd cannot be used in "
455 "combination with -raw");
456 }
457
458 std::string_view fname;
459 switch (arguments.size()) {
460 case 0:
461 // nothing
462 break;
463 case 1:
464 fname = arguments[0].getString();
465 break;
466 default:
467 throw SyntaxError();
468 }
470 fname, SCREENSHOT_DIR, prefix, SCREENSHOT_EXTENSION);
471
472 if (!rawShot) {
473 // take screenshot as displayed, possibly with other layers (OSD stuff, ImGUI)
474 try {
475 display.getVideoSystem().takeScreenShot(filename, withOsd);
476 } catch (MSXException& e) {
477 throw CommandException(
478 "Failed to take screenshot: ", e.getMessage());
479 }
480 } else {
481 auto* videoLayer = dynamic_cast<VideoLayer*>(
482 display.findActiveLayer());
483 if (!videoLayer) {
484 throw CommandException(
485 "Current renderer doesn't support taking screenshots.");
486 }
487 unsigned height = doubleSize ? 480 : 240;
488 try {
489 videoLayer->takeRawScreenShot(height, filename);
490 } catch (MSXException& e) {
491 throw CommandException(
492 "Failed to take screenshot: ", e.getMessage());
493 }
494 }
495
496 display.getCliComm().printInfo("Screen saved to ", filename);
497 result = filename;
498}
499
500string Display::ScreenShotCmd::help(std::span<const TclObject> /*tokens*/) const
501{
502 // Note: -no-sprites and -guess-name options are implemented in Tcl.
503 // TODO: find a way to extend the help and completion for a command
504 // when extending it in Tcl
505 return "screenshot Write screenshot to file \"openmsxNNNN.png\"\n"
506 "screenshot <filename> Write screenshot to indicated file\n"
507 "screenshot -prefix foo Write screenshot to file \"fooNNNN.png\"\n"
508 "screenshot -raw 320x240 raw screenshot (of MSX screen only)\n"
509 "screenshot -raw -doublesize 640x480 raw screenshot (of MSX screen only)\n"
510 "screenshot -with-osd Include OSD elements in the screenshot\n"
511 "screenshot -no-sprites Don't include sprites in the screenshot\n"
512 "screenshot -guess-name Guess the name of the running software and use it as prefix\n";
513}
514
515void Display::ScreenShotCmd::tabCompletion(std::vector<string>& tokens) const
516{
517 using namespace std::literals;
518 static constexpr std::array extra = {
519 "-prefix"sv, "-raw"sv, "-doublesize"sv, "-with-osd"sv, "-no-sprites"sv, "-guess-name"sv,
520 };
521 completeFileName(tokens, userFileContext(), extra);
522}
523
524
525// FpsInfoTopic
526
527Display::FpsInfoTopic::FpsInfoTopic(InfoCommand& openMSXInfoCommand)
528 : InfoTopic(openMSXInfoCommand, "fps")
529{
530}
531
532void Display::FpsInfoTopic::execute(std::span<const TclObject> /*tokens*/,
533 TclObject& result) const
534{
535 auto& display = OUTER(Display, fpsInfo);
536 result = display.getFps();
537}
538
539string Display::FpsInfoTopic::help(std::span<const TclObject> /*tokens*/) const
540{
541 return "Returns the current rendering speed in frames per second.";
542}
543
544} // namespace openmsx
BaseSetting * setting
#define MIN_SCALE_FACTOR
Definition build-info.hh:18
#define PLATFORM_ANDROID
Definition build-info.hh:17
constexpr void push_front(const T &element)
void printWarning(std::string_view message)
Definition CliComm.cc:12
std::string getWindowTitle()
Definition Display.cc:205
void repaint()
Redraw the display.
Definition Display.cc:373
void repaintImpl()
Definition Display.cc:329
gl::ivec2 retrieveWindowPosition()
Definition Display.cc:241
void detach(VideoSystemChangeListener &listener)
Definition Display.cc:121
CliComm & getCliComm() const
Definition Display.cc:110
void storeWindowPosition(gl::ivec2 pos)
Definition Display.cc:236
void updateZ(Layer &layer)
Definition Display.cc:402
void removeLayer(Layer &layer)
Definition Display.cc:397
VideoSystem & getVideoSystem()
Definition Display.cc:91
Display(Reactor &reactor)
Definition Display.cc:42
void attach(VideoSystemChangeListener &listener)
Definition Display.cc:115
void createVideoSystem()
Definition Display.cc:81
gl::ivec2 getWindowSize() const
Definition Display.cc:246
float getFps() const
Definition Display.cc:252
void repaintDelayed(uint64_t delta)
Definition Display.cc:380
void setWindowPosition(gl::ivec2 pos)
Definition Display.cc:230
Layer * findActiveLayer() const
Definition Display.cc:126
OutputSurface * getOutputSurface()
Definition Display.cc:97
gl::ivec2 getWindowPosition()
Get/set x,y coordinates of top-left window corner.
Definition Display.cc:222
void addLayer(Layer &layer)
Definition Display.cc:389
void unregisterEventListener(EventType type, EventListener &listener)
Unregisters a previously registered event listener.
void distributeEvent(Event &&event)
Schedule the given event for delivery.
void registerEventListener(EventType type, EventListener &listener, Priority priority=Priority::OTHER)
Registers a given object to receive certain events.
void storeWindowPosition(gl::ivec2 pos)
gl::ivec2 retrieveWindowPosition() const
int getInt() const noexcept
Interface for display layers.
Definition Layer.hh:14
@ NONE
Layer is not visible, that is completely transparent.
@ FULL
Layer fully covers the screen: any underlying layers are invisible.
void setDisplay(Display &display_)
Store pointer to Display.
Definition Layer.hh:62
ZIndex getZ() const
Query the Z-index of this layer.
Definition Layer.hh:52
bool isActive() const
Definition Layer.hh:53
A frame buffer where pixels can be written to.
void scheduleRT(uint64_t delta)
Contains the main loop of openMSX.
Definition Reactor.hh:75
ImGuiManager & getImGuiManager()
Definition Reactor.hh:99
MSXMotherBoard * getMotherBoard() const
Definition Reactor.cc:409
CliComm & getCliComm()
Definition Reactor.cc:323
EventDistributor & getEventDistributor()
Definition Reactor.hh:89
RendererSetting & getRendererSetting()
The current renderer.
IntegerSetting & getScaleFactorSetting()
The current scaling factor.
RendererID getRenderer() const
void detach(Observer< T > &observer)
Definition Subject.hh:60
void attach(Observer< T > &observer)
Definition Subject.hh:54
static std::string full()
Definition Version.cc:8
static const bool RELEASE
Definition Version.hh:12
Video back-end system.
constexpr double e
Definition Math.hh:21
string parseCommandFileArgument(string_view argument, string_view directory, string_view prefix, string_view extension)
Helper function for parsing filename arguments in Tcl commands.
std::unique_ptr< VideoSystem > createVideoSystem(Reactor &reactor)
Create the video system required by the current renderer setting.
uint64_t getTime()
Get current (real) time in us.
Definition Timer.cc:7
This file implemented 3 utility functions:
Definition Autofire.cc:11
EventType
Definition Event.hh:454
ArgsInfo valueArg(std::string_view name, T &value)
std::vector< TclObject > parseTclArgs(Interpreter &interp, std::span< const TclObject > inArgs, std::span< const ArgsInfo > table)
const FileContext & userFileContext()
std::variant< KeyUpEvent, KeyDownEvent, MouseMotionEvent, MouseButtonUpEvent, MouseButtonDownEvent, MouseWheelEvent, JoystickAxisMotionEvent, JoystickHatEvent, JoystickButtonUpEvent, JoystickButtonDownEvent, OsdControlReleaseEvent, OsdControlPressEvent, WindowEvent, TextEvent, FileDropEvent, QuitEvent, FinishFrameEvent, CliCommandEvent, GroupEvent, BootEvent, FrameDrawnEvent, BreakEvent, SwitchRendererEvent, TakeReverseSnapshotEvent, AfterTimedEvent, MachineLoadedEvent, MachineActivatedEvent, MachineDeactivatedEvent, MidiInReaderEvent, MidiInWindowsEvent, MidiInCoreMidiEvent, MidiInCoreMidiVirtualEvent, MidiInALSAEvent, Rs232TesterEvent, Rs232NetEvent, ImGuiDelayedActionEvent, ImGuiActiveEvent > Event
Definition Event.hh:445
ArgsInfo flagArg(std::string_view name, bool &flag)
auto find_if(InputRange &&range, UnaryPredicate pred)
Definition ranges.hh:175
#define ad_printf(...)
Definition openmsx.hh:11
#define OUTER(type, member)
Definition outer.hh:42
void move_pop_back(VECTOR &v, typename VECTOR::iterator it)
Erase the pointed to element from the given vector.
Definition stl.hh:137
auto rfind_unguarded(RANGE &range, const VAL &val, Proj proj={})
Similar to the find(_if)_unguarded functions above, but searches from the back to front.
Definition stl.hh:112
constexpr bool contains(ITER first, ITER last, const VAL &val)
Check if a range contains a given value, using linear search.
Definition stl.hh:35
std::string strCat()
Definition strCat.hh:703
void strAppend(std::string &result, Ts &&...ts)
Definition strCat.hh:752
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.
Definition xrange.hh:147
constexpr auto begin(const zstring_view &x)
constexpr auto end(const zstring_view &x)