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 "FinishFrameEvent.hh"
8 #include "FileOperations.hh"
9 #include "FileContext.hh"
10 #include "InputEvents.hh"
11 #include "CliComm.hh"
12 #include "Timer.hh"
13 #include "BooleanSetting.hh"
14 #include "IntegerSetting.hh"
15 #include "EnumSetting.hh"
16 #include "Reactor.hh"
17 #include "MSXMotherBoard.hh"
18 #include "HardwareConfig.hh"
19 #include "XMLElement.hh"
21 #include "CommandException.hh"
22 #include "StringOp.hh"
23 #include "Version.hh"
24 #include "build-info.hh"
25 #include "checked_cast.hh"
26 #include "outer.hh"
27 #include "stl.hh"
28 #include "unreachable.hh"
29 #include <algorithm>
30 #include <cassert>
31 
32 using std::string;
33 using std::vector;
34 
35 namespace openmsx {
36 
38  : RTSchedulable(reactor_.getRTScheduler())
39  , screenShotCmd(reactor_.getCommandController())
40  , fpsInfo(reactor_.getOpenMSXInfoCommand())
41  , osdGui(reactor_.getCommandController(), *this)
42  , reactor(reactor_)
43  , renderSettings(reactor.getCommandController())
44  , commandConsole(reactor.getGlobalCommandController(),
45  reactor.getEventDistributor(), *this)
46  , currentRenderer(RenderSettings::UNINITIALIZED)
47  , switchInProgress(false)
48 {
49  frameDurationSum = 0;
50  for (unsigned i = 0; i < NUM_FRAME_DURATIONS; ++i) {
51  frameDurations.addFront(20);
52  frameDurationSum += 20;
53  }
54  prevTimeStamp = Timer::getTime();
55 
56  EventDistributor& eventDistributor = reactor.getEventDistributor();
58  *this);
60  *this);
62  *this);
64  *this);
65 #if PLATFORM_ANDROID
67  *this);
68 #endif
69  renderSettings.getRendererSetting().attach(*this);
70  renderSettings.getFullScreenSetting().attach(*this);
71  renderSettings.getScaleFactorSetting().attach(*this);
72  renderFrozen = false;
73 }
74 
76 {
77  renderSettings.getRendererSetting().detach(*this);
78  renderSettings.getFullScreenSetting().detach(*this);
79  renderSettings.getScaleFactorSetting().detach(*this);
80 
81  EventDistributor& eventDistributor = reactor.getEventDistributor();
82 #if PLATFORM_ANDROID
84  *this);
85 #endif
87  *this);
89  *this);
91  *this);
93  *this);
94 
95  resetVideoSystem();
96 
97  assert(listeners.empty());
98 }
99 
101 {
102  assert(!videoSystem);
103  assert(currentRenderer == RenderSettings::UNINITIALIZED);
104  assert(!switchInProgress);
105  currentRenderer = renderSettings.getRenderer();
106  switchInProgress = true;
107  doRendererSwitch();
108 }
109 
111 {
112  assert(videoSystem);
113  return *videoSystem;
114 }
115 
117 {
118  return videoSystem ? videoSystem->getOutputSurface() : nullptr;
119 }
120 
121 void Display::resetVideoSystem()
122 {
123  videoSystem.reset();
124  // At this point all layers expect for the Video9000 layer
125  // should be gone.
126  //assert(layers.empty());
127 }
128 
130 {
131  return reactor.getCliComm();
132 }
133 
135 {
136  assert(!contains(listeners, &listener));
137  listeners.push_back(&listener);
138 }
139 
141 {
142  move_pop_back(listeners, rfind_unguarded(listeners, &listener));
143 }
144 
146 {
147  for (auto& l : layers) {
148  if (l->getZ() == Layer::Z_MSX_ACTIVE) {
149  return l;
150  }
151  }
152  return nullptr;
153 }
154 
155 Display::Layers::iterator Display::baseLayer()
156 {
157  // Note: It is possible to cache this, but since the number of layers is
158  // low at the moment, it's not really worth it.
159  auto it = end(layers);
160  while (true) {
161  if (it == begin(layers)) {
162  // There should always be at least one opaque layer.
163  // TODO: This is not true for DummyVideoSystem.
164  // Anyway, a missing layer will probably stand out visually,
165  // so do we really have to assert on it?
166  //UNREACHABLE;
167  return it;
168  }
169  --it;
170  if ((*it)->getCoverage() == Layer::COVER_FULL) return it;
171  }
172 }
173 
174 void Display::executeRT()
175 {
176  repaint();
177 }
178 
179 int Display::signalEvent(const std::shared_ptr<const Event>& event)
180 {
181  if (event->getType() == OPENMSX_FINISH_FRAME_EVENT) {
182  auto& ffe = checked_cast<const FinishFrameEvent&>(*event);
183  if (ffe.needRender()) {
184  repaint();
186  std::make_shared<SimpleEvent>(
188  }
189  } else if (event->getType() == OPENMSX_SWITCH_RENDERER_EVENT) {
190  doRendererSwitch();
191  } else if (event->getType() == OPENMSX_MACHINE_LOADED_EVENT) {
192  videoSystem->updateWindowTitle();
193  } else if (event->getType() == OPENMSX_EXPOSE_EVENT) {
194  // Don't render too often, and certainly not when the screen
195  // will anyway soon be rendered.
196  repaintDelayed(100 * 1000); // 10fps
197  } else if (PLATFORM_ANDROID && event->getType() == OPENMSX_FOCUS_EVENT) {
198  // On Android, the rendering must be frozen when the app is sent to
199  // the background, because Android takes away all graphics resources
200  // from the app. It simply destroys the entire graphics context.
201  // Though, a repaint() must happen within the focus-lost event
202  // so that the SDL Android port realises that the graphix context
203  // is gone and will re-build it again on the first flush to the
204  // surface after the focus has been regained.
205 
206  // Perform a repaint before updating the renderFrozen flag:
207  // -When loosing the focus, this repaint will flush a last
208  // time the SDL surface, making sure that the Android SDL
209  // port discovers that the graphics context is gone.
210  // -When gaining the focus, this repaint does nothing as
211  // the renderFrozen flag is still false
212  repaint();
213  auto& focusEvent = checked_cast<const FocusEvent&>(*event);
214  ad_printf("Setting renderFrozen to %d", !focusEvent.getGain());
215  renderFrozen = !focusEvent.getGain();
216  }
217  return 0;
218 }
219 
221 {
222  string title = Version::full();
223  if (!Version::RELEASE) {
224  strAppend(title, " [", BUILD_FLAVOUR, ']');
225  }
226  if (MSXMotherBoard* motherboard = reactor.getMotherBoard()) {
227  if (const HardwareConfig* machine = motherboard->getMachineConfig()) {
228  const XMLElement& config = machine->getConfig();
229  strAppend(title, " - ",
230  config.getChild("info").getChildData("manufacturer"), ' ',
231  config.getChild("info").getChildData("code"));
232  }
233  }
234  return title;
235 }
236 
237 void Display::update(const Setting& setting)
238 {
239  if (&setting == &renderSettings.getRendererSetting()) {
240  checkRendererSwitch();
241  } else if (&setting == &renderSettings.getFullScreenSetting()) {
242  checkRendererSwitch();
243  } else if (&setting == &renderSettings.getScaleFactorSetting()) {
244  checkRendererSwitch();
245  } else {
246  UNREACHABLE;
247  }
248 }
249 
250 void Display::checkRendererSwitch()
251 {
252  if (switchInProgress) {
253  // This method only queues a request to switch renderer (see
254  // comments below why). If there already is such a request
255  // queued we don't need to do it again.
256  return;
257  }
258  auto newRenderer = renderSettings.getRenderer();
259  if ((newRenderer != currentRenderer) ||
260  !getVideoSystem().checkSettings()) {
261  currentRenderer = newRenderer;
262  // don't do the actualing swithing in the Tcl callback
263  // it seems creating and destroying Settings (= Tcl vars)
264  // causes problems???
265  switchInProgress = true;
267  std::make_shared<SimpleEvent>(
269  }
270 }
271 
272 void Display::doRendererSwitch()
273 {
274  assert(switchInProgress);
275 
276  bool success = false;
277  while (!success) {
278  try {
279  doRendererSwitch2();
280  success = true;
281  } catch (MSXException& e) {
282  auto& rendererSetting = renderSettings.getRendererSetting();
283  string errorMsg = strCat(
284  "Couldn't activate renderer ",
285  rendererSetting.getString(),
286  ": ", e.getMessage());
287  // now try some things that might work against this:
288  if (rendererSetting.getEnum() != RenderSettings::SDL) {
289  errorMsg += "\nTrying to switch to SDL renderer instead...";
290  rendererSetting.setEnum(RenderSettings::SDL);
291  currentRenderer = RenderSettings::SDL;
292  } else {
293  auto& scaleFactorSetting = renderSettings.getScaleFactorSetting();
294  unsigned curval = scaleFactorSetting.getInt();
295  if (curval == 1) {
296  throw MSXException(
297  e.getMessage(),
298  " (and I have no other ideas to try...)"); // give up and die... :(
299  }
300  strAppend(errorMsg, "\nTrying to decrease scale_factor setting from ",
301  curval, " to ", curval - 1, "...");
302  scaleFactorSetting.setInt(curval - 1);
303  }
304  getCliComm().printWarning(errorMsg);
305  }
306  }
307 
308  switchInProgress = false;
309 }
310 
311 void 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  repaint(*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 
357 {
358  for (auto it = baseLayer(); it != end(layers); ++it) {
359  if ((*it)->getCoverage() != Layer::COVER_NONE) {
360  (*it)->paint(surface);
361  }
362  }
363 }
364 
365 void Display::repaintDelayed(uint64_t delta)
366 {
367  if (isPendingRT()) {
368  // already a pending repaint
369  return;
370  }
371  scheduleRT(unsigned(delta));
372 }
373 
375 {
376  int z = layer.getZ();
377  auto it = find_if(begin(layers), end(layers),
378  [&](Layer* l) { return l->getZ() > z; });
379  layers.insert(it, &layer);
380  layer.setDisplay(*this);
381 }
382 
384 {
385  layers.erase(rfind_unguarded(layers, &layer));
386 }
387 
388 void Display::updateZ(Layer& layer)
389 {
390  // Remove at old Z-index...
391  removeLayer(layer);
392  // ...and re-insert at new Z-index.
393  addLayer(layer);
394 }
395 
396 
397 // ScreenShotCmd
398 
399 Display::ScreenShotCmd::ScreenShotCmd(CommandController& commandController_)
400  : Command(commandController_, "screenshot")
401 {
402 }
403 
404 void Display::ScreenShotCmd::execute(array_ref<TclObject> tokens, TclObject& result)
405 {
406  auto& display = OUTER(Display, screenShotCmd);
407  bool rawShot = false;
408  bool withOsd = false;
409  bool doubleSize = false;
410  string_view prefix = "openmsx";
411  vector<TclObject> arguments;
412  for (unsigned i = 1; i < tokens.size(); ++i) {
413  string_view tok = tokens[i].getString();
414  if (StringOp::startsWith(tok, '-')) {
415  if (tok == "--") {
416  arguments.insert(end(arguments),
417  std::begin(tokens) + i + 1, std::end(tokens));
418  break;
419  }
420  if (tok == "-prefix") {
421  if (++i == tokens.size()) {
422  throw CommandException("Missing argument");
423  }
424  prefix = tokens[i].getString();
425  } else if (tok == "-raw") {
426  rawShot = true;
427  } else if (tok == "-msxonly") {
428  display.getCliComm().printWarning(
429  "The -msxonly option has been deprecated and will "
430  "be removed in a future release. Instead, use the "
431  "-raw option for the same effect.");
432  rawShot = true;
433  } else if (tok == "-doublesize") {
434  doubleSize = true;
435  } else if (tok == "-with-osd") {
436  withOsd = true;
437  } else {
438  throw CommandException("Invalid option: ", tok);
439  }
440  } else {
441  arguments.push_back(tokens[i]);
442  }
443  }
444  if (doubleSize && !rawShot) {
445  throw CommandException("-doublesize option can only be used in "
446  "combination with -raw");
447  }
448  if (rawShot && withOsd) {
449  throw CommandException("-with-osd cannot be used in "
450  "combination with -raw");
451  }
452 
453  string_view fname;
454  switch (arguments.size()) {
455  case 0:
456  // nothing
457  break;
458  case 1:
459  fname = arguments[0].getString();
460  break;
461  default:
462  throw SyntaxError();
463  }
465  fname, "screenshots", prefix, ".png");
466 
467  if (!rawShot) {
468  // include all layers (OSD stuff, console)
469  try {
470  display.getVideoSystem().takeScreenShot(filename, withOsd);
471  } catch (MSXException& e) {
472  throw CommandException(
473  "Failed to take screenshot: ", e.getMessage());
474  }
475  } else {
476  auto videoLayer = dynamic_cast<VideoLayer*>(
477  display.findActiveLayer());
478  if (!videoLayer) {
479  throw CommandException(
480  "Current renderer doesn't support taking screenshots.");
481  }
482  unsigned height = doubleSize ? 480 : 240;
483  try {
484  videoLayer->takeRawScreenShot(height, filename);
485  } catch (MSXException& e) {
486  throw CommandException(
487  "Failed to take screenshot: ", e.getMessage());
488  }
489  }
490 
491  display.getCliComm().printInfo("Screen saved to ", filename);
492  result.setString(filename);
493 }
494 
495 string Display::ScreenShotCmd::help(const vector<string>& /*tokens*/) const
496 {
497  // Note: -no-sprites option is implemented in Tcl
498  return "screenshot Write screenshot to file \"openmsxNNNN.png\"\n"
499  "screenshot <filename> Write screenshot to indicated file\n"
500  "screenshot -prefix foo Write screenshot to file \"fooNNNN.png\"\n"
501  "screenshot -raw 320x240 raw screenshot (of MSX screen only)\n"
502  "screenshot -raw -doublesize 640x480 raw screenshot (of MSX screen only)\n"
503  "screenshot -with-osd Include OSD elements in the screenshot\n"
504  "screenshot -no-sprites Don't include sprites in the screenshot\n";
505 }
506 
507 void Display::ScreenShotCmd::tabCompletion(vector<string>& tokens) const
508 {
509  static const char* const extra[] = {
510  "-prefix", "-raw", "-doublesize", "-with-osd", "-no-sprites",
511  };
512  completeFileName(tokens, userFileContext(), extra);
513 }
514 
515 
516 // FpsInfoTopic
517 
518 Display::FpsInfoTopic::FpsInfoTopic(InfoCommand& openMSXInfoCommand)
519  : InfoTopic(openMSXInfoCommand, "fps")
520 {
521 }
522 
523 void Display::FpsInfoTopic::execute(array_ref<TclObject> /*tokens*/,
524  TclObject& result) const
525 {
526  auto& display = OUTER(Display, fpsInfo);
527  float fps = 1000000.0f * Display::NUM_FRAME_DURATIONS / display.frameDurationSum;
528  result.setDouble(fps);
529 }
530 
531 string Display::FpsInfoTopic::help(const vector<string>& /*tokens*/) const
532 {
533  return "Returns the current rendering speed in frames per second.";
534 }
535 
536 } // namespace openmsx
void setDouble(double value)
Definition: TclObject.cc:47
void setDisplay(LayerListener &display_)
Store pointer to Display.
Definition: Layer.hh:59
Contains the main loop of openMSX.
Definition: Reactor.hh:64
Sent when a OPENMSX_FINISH_FRAME_EVENT caused a redraw of the screen.
Definition: Event.hh:36
void createVideoSystem()
Definition: Display.cc:100
string_view::const_iterator begin(const string_view &x)
Definition: string_view.hh:152
Layer is not visible, that is completely transparent.
Definition: Layer.hh:39
const std::string & getMessage() const &
Definition: MSXException.hh:23
Represents the output window/screen of openMSX.
Definition: Display.hh:31
std::string getWindowTitle()
Definition: Display.cc:220
Send when a (new) machine configuration is loaded.
Definition: Event.hh:51
string parseCommandFileArgument(string_view argument, string_view directory, string_view prefix, string_view extension)
Helper function for parsing filename arguments in Tcl commands.
void registerEventListener(EventType type, EventListener &listener, Priority priority=OTHER)
Registers a given object to receive certain events.
void addLayer(Layer &layer)
Definition: Display.cc:374
void unregisterEventListener(EventType type, EventListener &listener)
Unregisters a previously registered event listener.
Interface for display layers.
Definition: Layer.hh:11
Display(Reactor &reactor)
Definition: Display.cc:37
Layer fully covers the screen: any underlying layers are invisible.
Definition: Layer.hh:32
bool contains(ITER first, ITER last, const VAL &val)
Check if a range contains a given value, using linear search.
Definition: stl.hh:102
bool startsWith(string_view total, string_view part)
Definition: StringOp.cc:71
void distributeEvent(const EventPtr &event)
Schedule the given event for delivery.
A frame buffer where pixels can be written to.
unique_ptr< VideoSystem > createVideoSystem(Reactor &reactor)
Create the video system required by the current renderer setting.
void move_pop_back(VECTOR &v, typename VECTOR::iterator it)
Erase the pointed to element from the given vector.
Definition: stl.hh:192
CliComm & getCliComm() const
Definition: Display.cc:129
static const bool RELEASE
Definition: Version.hh:12
EventDistributor & getEventDistributor()
Definition: Reactor.hh:79
MSXMotherBoard * getMotherBoard() const
Definition: Reactor.cc:343
VideoSystem & getVideoSystem()
Definition: Display.cc:110
void strAppend(std::string &result, Ts &&...ts)
Definition: strCat.hh:648
void attach(Observer< T > &observer)
Definition: Subject.hh:52
void repaint()
Redraw the display.
Definition: Display.cc:325
bool isPendingRT() const
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:161
Sent when VDP (V99x8 or V9990) reaches the end of a frame.
Definition: Event.hh:30
void removeLayer(Layer &layer)
Definition: Display.cc:383
auto rfind_unguarded(RANGE &range, const VAL &val) -> decltype(std::begin(range))
Similar to the find(_if)_unguarded functions above, but searches from the back to front...
Definition: stl.hh:164
This class implements a subset of the proposal for std::array_ref (proposed for the next c++ standard...
Definition: array_ref.hh:19
void detach(VideoSystemChangeListener &listener)
Definition: Display.cc:140
RendererSetting & getRendererSetting()
The current renderer.
string_view::const_iterator end(const string_view &x)
Definition: string_view.hh:153
Layer * findActiveLayer() const
Definition: Display.cc:145
Video back-end system.
Definition: VideoSystem.hh:20
void repaintDelayed(uint64_t delta)
Definition: Display.cc:365
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
const std::string & getChildData(string_view name) const
Definition: XMLElement.cc:192
Send when (part of) the openMSX window gets exposed, and thus should be repainted.
Definition: Event.hh:60
void setString(string_view value)
Definition: TclObject.cc:14
ZIndex getZ() const
Query the Z-index of this layer.
Definition: Layer.hh:50
void scheduleRT(uint64_t delta)
size_type size() const
Definition: array_ref.hh:60
This class implements a (close approximation) of the std::string_view class.
Definition: string_view.hh:15
void attach(VideoSystemChangeListener &listener)
Definition: Display.cc:134
RendererID getRenderer() const
Class containing all settings for renderers.
void detach(Observer< T > &observer)
Definition: Subject.hh:58
static std::string full()
Definition: Version.cc:8
CliComm & getCliComm()
Definition: Reactor.cc:280
BooleanSetting & getFullScreenSetting()
Full screen [on, off].
std::string strCat(Ts &&...ts)
Definition: strCat.hh:577
uint64_t getTime()
Get current (real) time in us.
Definition: Timer.cc:8
#define OUTER(type, member)
Definition: outer.hh:38
OutputSurface * getOutputSurface()
Definition: Display.cc:116
void addFront(const T &element)
const XMLElement & getChild(string_view name) const
Definition: XMLElement.cc:166
void printWarning(string_view message)
Definition: CliComm.cc:20
#define ad_printf(...)
Definition: openmsx.hh:11
IntegerSetting & getScaleFactorSetting()
The current scaling factor.
This event is send when a device (v99x8, v9990, video9000, laserdisc) reaches the end of a frame...
#define UNREACHABLE
Definition: unreachable.hh:38