openMSX
Display.cc
Go to the documentation of this file.
1 #include "Display.hh"
2 #include "RendererFactory.hh"
3 #include "Layer.hh"
4 #include "VideoSystem.hh"
5 #include "VideoLayer.hh"
6 #include "EventDistributor.hh"
7 #include "Event.hh"
8 #include "FileOperations.hh"
9 #include "FileContext.hh"
10 #include "CliComm.hh"
11 #include "Timer.hh"
12 #include "BooleanSetting.hh"
13 #include "IntegerSetting.hh"
14 #include "EnumSetting.hh"
15 #include "Reactor.hh"
16 #include "MSXMotherBoard.hh"
17 #include "HardwareConfig.hh"
18 #include "TclArgParser.hh"
19 #include "XMLElement.hh"
21 #include "CommandException.hh"
22 #include "Version.hh"
23 #include "build-info.hh"
24 #include "outer.hh"
25 #include "ranges.hh"
26 #include "stl.hh"
27 #include "unreachable.hh"
28 #include "xrange.hh"
29 #include <cassert>
30 
31 using std::string;
32 
33 namespace openmsx {
34 
36  : RTSchedulable(reactor_.getRTScheduler())
37  , screenShotCmd(reactor_.getCommandController())
38  , fpsInfo(reactor_.getOpenMSXInfoCommand())
39  , osdGui(reactor_.getCommandController(), *this)
40  , reactor(reactor_)
41  , renderSettings(reactor.getCommandController())
42  , commandConsole(reactor.getGlobalCommandController(),
43  reactor.getEventDistributor(), *this)
44  , currentRenderer(RenderSettings::UNINITIALIZED)
45  , switchInProgress(false)
46 {
47  frameDurationSum = 0;
48  repeat(NUM_FRAME_DURATIONS, [&] {
49  frameDurations.addFront(20);
50  frameDurationSum += 20;
51  });
52  prevTimeStamp = Timer::getTime();
53 
54  EventDistributor& eventDistributor = reactor.getEventDistributor();
56  *this);
58  *this);
60  *this);
62  *this);
63 #if PLATFORM_ANDROID
64  eventDistributor.registerEventListener(EventType::FOCUS,
65  *this);
66 #endif
67  renderSettings.getRendererSetting().attach(*this);
68  renderSettings.getFullScreenSetting().attach(*this);
69  renderSettings.getScaleFactorSetting().attach(*this);
70  renderFrozen = false;
71 }
72 
74 {
75  renderSettings.getRendererSetting().detach(*this);
76  renderSettings.getFullScreenSetting().detach(*this);
77  renderSettings.getScaleFactorSetting().detach(*this);
78 
79  EventDistributor& eventDistributor = reactor.getEventDistributor();
80 #if PLATFORM_ANDROID
82  *this);
83 #endif
85  *this);
87  *this);
89  *this);
91  *this);
92 
93  resetVideoSystem();
94 
95  assert(listeners.empty());
96 }
97 
99 {
100  assert(!videoSystem);
101  assert(currentRenderer == RenderSettings::UNINITIALIZED);
102  assert(!switchInProgress);
103  currentRenderer = renderSettings.getRenderer();
104  switchInProgress = true;
105  doRendererSwitch();
106 }
107 
109 {
110  assert(videoSystem);
111  return *videoSystem;
112 }
113 
115 {
116  return videoSystem ? videoSystem->getOutputSurface() : nullptr;
117 }
118 
119 void Display::resetVideoSystem()
120 {
121  videoSystem.reset();
122  // At this point all layers except for the Video9000 layer
123  // should be gone.
124  //assert(layers.empty());
125 }
126 
128 {
129  return reactor.getCliComm();
130 }
131 
133 {
134  assert(!contains(listeners, &listener));
135  listeners.push_back(&listener);
136 }
137 
139 {
140  move_pop_back(listeners, rfind_unguarded(listeners, &listener));
141 }
142 
144 {
145  for (auto* l : layers) {
146  if (l->getZ() == Layer::Z_MSX_ACTIVE) {
147  return l;
148  }
149  }
150  return nullptr;
151 }
152 
153 Display::Layers::iterator Display::baseLayer()
154 {
155  // Note: It is possible to cache this, but since the number of layers is
156  // low at the moment, it's not really worth it.
157  auto it = end(layers);
158  while (true) {
159  if (it == begin(layers)) {
160  // There should always be at least one opaque layer.
161  // TODO: This is not true for DummyVideoSystem.
162  // Anyway, a missing layer will probably stand out visually,
163  // so do we really have to assert on it?
164  //UNREACHABLE;
165  return it;
166  }
167  --it;
168  if ((*it)->getCoverage() == Layer::COVER_FULL) return it;
169  }
170 }
171 
172 void Display::executeRT()
173 {
174  repaint();
175 }
176 
177 int Display::signalEvent(const Event& event) noexcept
178 {
180  [&](const FinishFrameEvent& e) {
181  if (e.needRender()) {
182  repaint();
183  reactor.getEventDistributor().distributeEvent(
184  Event::create<FrameDrawnEvent>());
185  }
186  },
187  [&](const SwitchRendererEvent& /*e*/) {
188  doRendererSwitch();
189  },
190  [&](const MachineLoadedEvent& /*e*/) {
191  videoSystem->updateWindowTitle();
192  },
193  [&](const ExposeEvent& /*e*/) {
194  // Don't render too often, and certainly not when the screen
195  // will anyway soon be rendered.
196  repaintDelayed(100 * 1000); // 10fps
197  },
198  [&](const FocusEvent& e) {
199  (void)e;
200  if (PLATFORM_ANDROID) {
201  // On Android, the rendering must be frozen when the app is sent to
202  // the background, because Android takes away all graphics resources
203  // from the app. It simply destroys the entire graphics context.
204  // Though, a repaint() must happen within the focus-lost event
205  // so that the SDL Android port realizes that the graphics context
206  // is gone and will re-build it again on the first flush to the
207  // surface after the focus has been regained.
208 
209  // Perform a repaint before updating the renderFrozen flag:
210  // -When loosing the focus, this repaint will flush a last
211  // time the SDL surface, making sure that the Android SDL
212  // port discovers that the graphics context is gone.
213  // -When gaining the focus, this repaint does nothing as
214  // the renderFrozen flag is still false
215  repaint();
216  ad_printf("Setting renderFrozen to %d", !e.getGain());
217  renderFrozen = !e.getGain();
218  }
219  },
220  [](const EventBase&) { /*ignore*/ }
221  }, event);
222  return 0;
223 }
224 
226 {
227  string title = Version::full();
228  if (!Version::RELEASE) {
229  strAppend(title, " [", BUILD_FLAVOUR, ']');
230  }
231  if (MSXMotherBoard* motherboard = reactor.getMotherBoard()) {
232  if (const HardwareConfig* machine = motherboard->getMachineConfig()) {
233  const auto& config = machine->getConfig();
234  strAppend(title, " - ",
235  config.getChild("info").getChildData("manufacturer"), ' ',
236  config.getChild("info").getChildData("code"));
237  }
238  }
239  return title;
240 }
241 
242 void Display::update(const Setting& setting) noexcept
243 {
244  if (&setting == &renderSettings.getRendererSetting()) {
245  checkRendererSwitch();
246  } else if (&setting == &renderSettings.getFullScreenSetting()) {
247  checkRendererSwitch();
248  } else if (&setting == &renderSettings.getScaleFactorSetting()) {
249  checkRendererSwitch();
250  } else {
251  UNREACHABLE;
252  }
253 }
254 
255 void Display::checkRendererSwitch()
256 {
257  if (switchInProgress) {
258  // This method only queues a request to switch renderer (see
259  // comments below why). If there already is such a request
260  // queued we don't need to do it again.
261  return;
262  }
263  auto newRenderer = renderSettings.getRenderer();
264  if ((newRenderer != currentRenderer) ||
265  !getVideoSystem().checkSettings()) {
266  currentRenderer = newRenderer;
267  // don't do the actual switching in the Tcl callback
268  // it seems creating and destroying Settings (= Tcl vars)
269  // causes problems???
270  switchInProgress = true;
272  Event::create<SwitchRendererEvent>());
273  }
274 }
275 
276 void Display::doRendererSwitch()
277 {
278  assert(switchInProgress);
279 
280  bool success = false;
281  while (!success) {
282  try {
283  doRendererSwitch2();
284  success = true;
285  } catch (MSXException& e) {
286  auto& rendererSetting = renderSettings.getRendererSetting();
287  string errorMsg = strCat(
288  "Couldn't activate renderer ",
289  rendererSetting.getString(),
290  ": ", e.getMessage());
291  // now try some things that might work against this:
292  if (rendererSetting.getEnum() != RenderSettings::SDL) {
293  errorMsg += "\nTrying to switch to SDL renderer instead...";
294  rendererSetting.setEnum(RenderSettings::SDL);
295  currentRenderer = RenderSettings::SDL;
296  } else {
297  auto& scaleFactorSetting = renderSettings.getScaleFactorSetting();
298  unsigned curval = scaleFactorSetting.getInt();
299  if (curval == 1) {
300  throw MSXException(
301  e.getMessage(),
302  " (and I have no other ideas to try...)"); // give up and die... :(
303  }
304  strAppend(errorMsg, "\nTrying to decrease scale_factor setting from ",
305  curval, " to ", curval - 1, "...");
306  scaleFactorSetting.setInt(curval - 1);
307  }
308  getCliComm().printWarning(errorMsg);
309  }
310  }
311 
312  switchInProgress = false;
313 }
314 
315 void 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.removeBack();
357  frameDurations.addFront(duration);
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 
376 void 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, [&](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 
398 void 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 
409 Display::ScreenShotCmd::ScreenShotCmd(CommandController& commandController_)
410  : Command(commandController_, "screenshot")
411 {
412 }
413 
414 void Display::ScreenShotCmd::execute(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  ArgsInfo 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, "screenshots", prefix, ".png");
460 
461  if (!rawShot) {
462  // include all layers (OSD stuff, console)
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 
489 string Display::ScreenShotCmd::help(span<const TclObject> /*tokens*/) const
490 {
491  // Note: -no-sprites option is implemented in Tcl
492  return "screenshot Write screenshot to file \"openmsxNNNN.png\"\n"
493  "screenshot <filename> Write screenshot to indicated file\n"
494  "screenshot -prefix foo Write screenshot to file \"fooNNNN.png\"\n"
495  "screenshot -raw 320x240 raw screenshot (of MSX screen only)\n"
496  "screenshot -raw -doublesize 640x480 raw screenshot (of MSX screen only)\n"
497  "screenshot -with-osd Include OSD elements in the screenshot\n"
498  "screenshot -no-sprites Don't include sprites in the screenshot\n";
499 }
500 
501 void Display::ScreenShotCmd::tabCompletion(std::vector<string>& tokens) const
502 {
503  using namespace std::literals;
504  static constexpr std::array extra = {
505  "-prefix"sv, "-raw"sv, "-doublesize"sv, "-with-osd"sv, "-no-sprites"sv,
506  };
507  completeFileName(tokens, userFileContext(), extra);
508 }
509 
510 
511 // FpsInfoTopic
512 
513 Display::FpsInfoTopic::FpsInfoTopic(InfoCommand& openMSXInfoCommand)
514  : InfoTopic(openMSXInfoCommand, "fps")
515 {
516 }
517 
518 void Display::FpsInfoTopic::execute(span<const TclObject> /*tokens*/,
519  TclObject& result) const
520 {
521  auto& display = OUTER(Display, fpsInfo);
522  result = 1000000.0f * Display::NUM_FRAME_DURATIONS / display.frameDurationSum;
523 }
524 
525 string Display::FpsInfoTopic::help(span<const TclObject> /*tokens*/) const
526 {
527  return "Returns the current rendering speed in frames per second.";
528 }
529 
530 } // namespace openmsx
BaseSetting * setting
Definition: Interpreter.cc:27
#define PLATFORM_ANDROID
Definition: build-info.hh:17
constexpr void addFront(const T &element)
constexpr T & removeBack()
void printWarning(std::string_view message)
Definition: CliComm.cc:10
std::string getWindowTitle()
Definition: Display.cc:225
void repaint()
Redraw the display.
Definition: Display.cc:369
void repaintImpl()
Definition: Display.cc:329
void detach(VideoSystemChangeListener &listener)
Definition: Display.cc:138
CliComm & getCliComm() const
Definition: Display.cc:127
void removeLayer(Layer &layer)
Definition: Display.cc:393
VideoSystem & getVideoSystem()
Definition: Display.cc:108
Display(Reactor &reactor)
Definition: Display.cc:35
void attach(VideoSystemChangeListener &listener)
Definition: Display.cc:132
void createVideoSystem()
Definition: Display.cc:98
void repaintDelayed(uint64_t delta)
Definition: Display.cc:376
Layer * findActiveLayer() const
Definition: Display.cc:143
OutputSurface * getOutputSurface()
Definition: Display.cc:114
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.
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:59
@ Z_MSX_ACTIVE
Definition: Layer.hh:21
ZIndex getZ() const
Query the Z-index of this layer.
Definition: Layer.hh:50
A frame buffer where pixels can be written to.
void scheduleRT(uint64_t delta)
bool isPendingRT() const
Contains the main loop of openMSX.
Definition: Reactor.hh:68
MSXMotherBoard * getMotherBoard() const
Definition: Reactor.cc:374
EventDistributor & getEventDistributor()
Definition: Reactor.hh:82
CliComm & getCliComm()
Definition: Reactor.cc:312
Class containing all settings for renderers.
BooleanSetting & getFullScreenSetting()
Full screen [on, off].
RendererID getRenderer() const
RendererSetting & getRendererSetting()
The current renderer.
IntegerSetting & getScaleFactorSetting()
The current scaling factor.
void detach(Observer< T > &observer)
Definition: Subject.hh:56
void attach(Observer< T > &observer)
Definition: Subject.hh:50
static std::string full()
Definition: Version.cc:8
static const bool RELEASE
Definition: Version.hh:12
Video back-end system.
Definition: VideoSystem.hh:23
Definition: span.hh:126
constexpr subspan_return_t< Offset, Count > subspan() const
Definition: span.hh:266
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:9
std::vector< TclObject > parseTclArgs(Interpreter &interp, span< const TclObject > inArgs, span< const ArgsInfo > table)
auto visit(Visitor &&visitor, const Event &event)
Definition: Event.hh:653
ArgsInfo valueArg(std::string_view name, T &value)
Definition: TclArgParser.hh:85
constexpr const char *const filename
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:173
ArgsInfo flagArg(std::string_view name, bool &flag)
Definition: TclArgParser.hh:72
auto find_if(InputRange &&range, UnaryPredicate pred)
Definition: ranges.hh:142
#define ad_printf(...)
Definition: openmsx.hh:11
#define OUTER(type, member)
Definition: outer.hh:41
void move_pop_back(VECTOR &v, typename VECTOR::iterator it)
Erase the pointed to element from the given vector.
Definition: stl.hh:133
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:108
constexpr bool contains(ITER first, ITER last, const VAL &val)
Check if a range contains a given value, using linear search.
Definition: stl.hh:31
std::string strCat(Ts &&...ts)
Definition: strCat.hh:591
void strAppend(std::string &result, Ts &&...ts)
Definition: strCat.hh:669
#define UNREACHABLE
Definition: unreachable.hh:38
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.
Definition: xrange.hh:170
constexpr auto begin(const zstring_view &x)
Definition: zstring_view.hh:83
constexpr auto end(const zstring_view &x)
Definition: zstring_view.hh:84