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