openMSX
Display.cc
Go to the documentation of this file.
1 #include "Display.hh"
2 #include "Layer.hh"
3 #include "VideoSystem.hh"
4 #include "PostProcessor.hh"
5 #include "EventDistributor.hh"
6 #include "FinishFrameEvent.hh"
7 #include "FileOperations.hh"
8 #include "FileContext.hh"
9 #include "InputEvents.hh"
10 #include "AlarmEvent.hh"
11 #include "Command.hh"
12 #include "InfoTopic.hh"
13 #include "OSDGUI.hh"
14 #include "CliComm.hh"
15 #include "CommandConsole.hh"
16 #include "Timer.hh"
17 #include "RenderSettings.hh"
18 #include "BooleanSetting.hh"
19 #include "IntegerSetting.hh"
20 #include "EnumSetting.hh"
21 #include "Reactor.hh"
22 #include "MSXMotherBoard.hh"
23 #include "HardwareConfig.hh"
24 #include "XMLElement.hh"
26 #include "CommandException.hh"
27 #include "StringOp.hh"
28 #include "Version.hh"
29 #include "build-info.hh"
30 #include "checked_cast.hh"
31 #include "unreachable.hh"
32 #include "memory.hh"
33 #include "openmsx.hh"
34 #include <algorithm>
35 #include <cassert>
36 
37 using std::string;
38 using std::vector;
39 
40 namespace openmsx {
41 
42 class ScreenShotCmd : public Command
43 {
44 public:
45  ScreenShotCmd(CommandController& commandController, Display& display);
46  virtual string execute(const vector<string>& tokens);
47  virtual string help(const vector<string>& tokens) const;
48  virtual void tabCompletion(vector<string>& tokens) const;
49 private:
50  Display& display;
51 };
52 
53 class FpsInfoTopic : public InfoTopic
54 {
55 public:
56  FpsInfoTopic(InfoCommand& openMSXInfoCommand, Display& display);
57  virtual void execute(const vector<TclObject>& tokens,
58  TclObject& result) const;
59  virtual string help(const vector<string>& tokens) const;
60 private:
61  Display& display;
62 };
63 
64 
66  : alarm(make_unique<AlarmEvent>(
67  reactor_.getEventDistributor(), *this,
69  , screenShotCmd(make_unique<ScreenShotCmd>(
70  reactor_.getCommandController(), *this))
71  , fpsInfo(make_unique<FpsInfoTopic>(
72  reactor_.getOpenMSXInfoCommand(), *this))
73  , osdGui(make_unique<OSDGUI>(
74  reactor_.getCommandController(), *this))
75  , reactor(reactor_)
76  , renderSettings(make_unique<RenderSettings>(
77  reactor.getCommandController()))
78  , commandConsole(make_unique<CommandConsole>(
79  reactor.getGlobalCommandController(),
80  reactor.getEventDistributor(), *this))
81  , currentRenderer(RendererFactory::UNINITIALIZED)
82  , switchInProgress(false)
83 {
84  frameDurationSum = 0;
85  for (unsigned i = 0; i < NUM_FRAME_DURATIONS; ++i) {
86  frameDurations.addFront(20);
87  frameDurationSum += 20;
88  }
89  prevTimeStamp = Timer::getTime();
90 
91  EventDistributor& eventDistributor = reactor.getEventDistributor();
93  *this);
95  *this);
97  *this);
99  *this);
100 #if PLATFORM_ANDROID
102  *this);
103 #endif
104  renderSettings->getRenderer().attach(*this);
105  renderSettings->getFullScreen().attach(*this);
106  renderSettings->getScaleFactor().attach(*this);
107  renderFrozen = false;
108 }
109 
111 {
112  renderSettings->getRenderer().detach(*this);
113  renderSettings->getFullScreen().detach(*this);
114  renderSettings->getScaleFactor().detach(*this);
115 
116  EventDistributor& eventDistributor = reactor.getEventDistributor();
117 #if PLATFORM_ANDROID
119  *this);
120 #endif
122  *this);
124  *this);
126  *this);
128  *this);
129 
130  PRT_DEBUG("Reset video system...");
131  resetVideoSystem();
132  PRT_DEBUG("Reset video system... DONE!");
133 
134  assert(listeners.empty());
135 }
136 
138 {
139  assert(!videoSystem.get());
140  assert(currentRenderer == RendererFactory::UNINITIALIZED);
141  assert(!switchInProgress);
142  currentRenderer = renderSettings->getRenderer().getValue();
143  switchInProgress = true;
144  doRendererSwitch();
145 }
146 
148 {
149  assert(videoSystem.get());
150  return *videoSystem;
151 }
152 
153 void Display::resetVideoSystem()
154 {
155  videoSystem.reset();
156  // At this point all layers expect for the Video9000 layer
157  // should be gone.
158  //assert(layers.empty());
159 }
160 
162 {
163  return reactor.getCliComm();
164 }
165 
167 {
168  return *renderSettings;
169 }
170 
172 {
173  return *osdGui;
174 }
175 
177 {
178  return *commandConsole;
179 }
180 
182 {
183  assert(count(listeners.begin(), listeners.end(), &listener) == 0);
184  listeners.push_back(&listener);
185 }
186 
188 {
189  auto it = find(listeners.begin(), listeners.end(), &listener);
190  assert(it != listeners.end());
191  listeners.erase(it);
192 }
193 
195 {
196  for (auto& l : layers) {
197  if (l->getZ() == Layer::Z_MSX_ACTIVE) {
198  return l;
199  }
200  }
201  return nullptr;
202 }
203 
204 Display::Layers::iterator Display::baseLayer()
205 {
206  // Note: It is possible to cache this, but since the number of layers is
207  // low at the moment, it's not really worth it.
208  auto it = layers.end();
209  while (true) {
210  if (it == layers.begin()) {
211  // There should always be at least one opaque layer.
212  // TODO: This is not true for DummyVideoSystem.
213  // Anyway, a missing layer will probably stand out visually,
214  // so do we really have to assert on it?
215  //UNREACHABLE;
216  return it;
217  }
218  --it;
219  if ((*it)->getCoverage() == Layer::COVER_FULL) return it;
220  }
221 }
222 
223 int Display::signalEvent(const std::shared_ptr<const Event>& event)
224 {
225  if (event->getType() == OPENMSX_FINISH_FRAME_EVENT) {
226  auto& ffe = checked_cast<const FinishFrameEvent&>(*event);
227  if (ffe.needRender()) {
228  repaint();
230  std::make_shared<SimpleEvent>(
232  }
233  } else if (event->getType() == OPENMSX_DELAYED_REPAINT_EVENT) {
234  repaint();
235  } else if (event->getType() == OPENMSX_SWITCH_RENDERER_EVENT) {
236  doRendererSwitch();
237  } else if (event->getType() == OPENMSX_MACHINE_LOADED_EVENT) {
238  setWindowTitle();
239  } else if (event->getType() == OPENMSX_EXPOSE_EVENT) {
240  // Don't render too often, and certainly not when the screen
241  // will anyway soon be rendered.
242  repaintDelayed(100 * 1000); // 10fps
243  } else if (PLATFORM_ANDROID && event->getType() == OPENMSX_FOCUS_EVENT) {
244  // On Android, the rendering must be frozen when the app is sent to
245  // the background, because Android takes away all graphics resources
246  // from the app. It simply destroys the entire graphics context.
247  // Though, a repaint() must happen within the focus-lost event
248  // so that the SDL Android port realises that the graphix context
249  // is gone and will re-build it again on the first flush to the
250  // surface after the focus has been regained.
251 
252  // Perform a repaint before updating the renderFrozen flag:
253  // -When loosing the focus, this repaint will flush a last
254  // time the SDL surface, making sure that the Android SDL
255  // port discovers that the graphics context is gone.
256  // -When gaining the focus, this repaint does nothing as
257  // the renderFrozen flag is still false
258  repaint();
259  auto& focusEvent = checked_cast<const FocusEvent&>(*event);
260  ad_printf("Setting renderFrozen to %d", !focusEvent.getGain());
261  renderFrozen = !focusEvent.getGain();
262  }
263  return 0;
264 }
265 
266 void Display::setWindowTitle()
267 {
268  string title = Version::full();
269  if (!Version::RELEASE) {
270  title += " [";
271  title += BUILD_FLAVOUR;
272  title += ']';
273  }
274  if (MSXMotherBoard* motherboard = reactor.getMotherBoard()) {
275  if (const HardwareConfig* machine = motherboard->getMachineConfig()) {
276  const XMLElement& config = machine->getConfig();
277  title += " - " +
278  config.getChild("info").getChildData("manufacturer") + ' ' +
279  config.getChild("info").getChildData("code");
280  }
281  }
282  videoSystem->setWindowTitle(title);
283 }
284 
285 void Display::update(const Setting& setting)
286 {
287  if (&setting == &renderSettings->getRenderer()) {
288  checkRendererSwitch();
289  } else if (&setting == &renderSettings->getFullScreen()) {
290  checkRendererSwitch();
291  } else if (&setting == &renderSettings->getScaleFactor()) {
292  checkRendererSwitch();
293  } else {
294  UNREACHABLE;
295  }
296 }
297 
298 void Display::checkRendererSwitch()
299 {
300  if (switchInProgress) {
301  // This method only queues a request to switch renderer (see
302  // comments below why). If there already is such a request
303  // queued we don't need to do it again.
304  return;
305  }
306  RendererFactory::RendererID newRenderer =
307  renderSettings->getRenderer().getValue();
308  if ((newRenderer != currentRenderer) ||
309  !getVideoSystem().checkSettings()) {
310  currentRenderer = newRenderer;
311  // don't do the actualing swithing in the Tcl callback
312  // it seems creating and destroying Settings (= Tcl vars)
313  // causes problems???
314  switchInProgress = true;
316  std::make_shared<SimpleEvent>(
318  }
319 }
320 
321 void Display::doRendererSwitch()
322 {
323  assert(switchInProgress);
324 
325  bool success = false;
326  while (!success) {
327  try {
328  doRendererSwitch2();
329  success = true;
330  } catch (MSXException& e) {
331  string errorMsg = "Couldn't activate renderer " +
332  renderSettings->getRenderer().getValueString() +
333  ": " + e.getMessage();
334  // now try some things that might work against this:
335  if (renderSettings->getRenderer().getValue() != RendererFactory::SDL) {
336  errorMsg += "\nTrying to switch to SDL renderer instead...";
337  renderSettings->getRenderer().changeValue(RendererFactory::SDL);
338  currentRenderer = RendererFactory::SDL;
339  } else {
340  unsigned curval = renderSettings->getScaleFactor().getValue();
341  if (curval == 1) throw MSXException(e.getMessage() + " (and I have no other ideas to try...)"); // give up and die... :(
342  errorMsg += "\nTrying to decrease scale_factor setting from " + StringOp::toString(curval) + " to " + StringOp::toString(curval - 1) + "...";
343  renderSettings->getScaleFactor().changeValue(curval - 1);
344  }
345  getCliComm().printWarning(errorMsg);
346  }
347  }
348 
349  switchInProgress = false;
350 }
351 
352 void Display::doRendererSwitch2()
353 {
354  for (auto& l : listeners) {
355  l->preVideoSystemChange();
356  }
357 
358  resetVideoSystem();
359  videoSystem = RendererFactory::createVideoSystem(reactor);
360  setWindowTitle();
361 
362  for (auto& l : listeners) {
363  l->postVideoSystemChange();
364  }
365 }
366 
368 {
369  if (switchInProgress) {
370  // The checkRendererSwitch() method will queue a
371  // SWITCH_RENDERER_EVENT, but before that event is handled
372  // we shouldn't do any repaints (with inconsistent setting
373  // values and render objects). This can happen when this
374  // method gets called because of a DELAYED_REPAINT_EVENT
375  // (DELAYED_REPAINT_EVENT was already queued before
376  // SWITCH_RENDERER_EVENT is queued).
377  return;
378  }
379 
380  alarm->cancel(); // cancel delayed repaint
381 
382  if (!renderFrozen) {
383  assert(videoSystem.get());
384  if (OutputSurface* surface = videoSystem->getOutputSurface()) {
385  repaint(*surface);
386  videoSystem->flush();
387  }
388  }
389 
390  // update fps statistics
391  auto now = Timer::getTime();
392  auto duration = now - prevTimeStamp;
393  prevTimeStamp = now;
394  frameDurationSum += duration - frameDurations.removeBack();
395  frameDurations.addFront(duration);
396 }
397 
399 {
400  for (auto it = baseLayer(); it != layers.end(); ++it) {
401  if ((*it)->getCoverage() != Layer::COVER_NONE) {
402  (*it)->paint(surface);
403  }
404  }
405 }
406 
407 void Display::repaintDelayed(uint64_t delta)
408 {
409  if (alarm->pending()) {
410  // already a pending repaint
411  return;
412  }
413  alarm->schedule(unsigned(delta));
414 }
415 
417 {
418  int z = layer.getZ();
419  auto it = layers.begin();
420  while (it != layers.end() && (*it)->getZ() < z) {
421  ++it;
422  }
423 
424  layers.insert(it, &layer);
425  layer.setDisplay(*this);
426 }
427 
429 {
430  auto it = std::find(layers.begin(), layers.end(), &layer);
431  assert(it != layers.end());
432  layers.erase(it);
433 }
434 
435 void Display::updateZ(Layer& layer)
436 {
437  // Remove at old Z-index...
438  removeLayer(layer);
439  // ...and re-insert at new Z-index.
440  addLayer(layer);
441 }
442 
443 
444 // ScreenShotCmd
445 
447  Display& display_)
448  : Command(commandController, "screenshot")
449  , display(display_)
450 {
451 }
452 
453 string ScreenShotCmd::execute(const vector<string>& tokens)
454 {
455  bool rawShot = false;
456  bool withOsd = false;
457  bool doubleSize = false;
458  string prefix = "openmsx";
459  vector<string> arguments;
460  for (unsigned i = 1; i < tokens.size(); ++i) {
461  if (StringOp::startsWith(tokens[i], '-')) {
462  if (tokens[i] == "--") {
463  arguments.insert(arguments.end(),
464  tokens.begin() + i + 1, tokens.end());
465  break;
466  }
467  if (tokens[i] == "-prefix") {
468  if (++i == tokens.size()) {
469  throw CommandException("Missing argument");
470  }
471  prefix = tokens[i];
472  } else if (tokens[i] == "-raw") {
473  rawShot = true;
474  } else if (tokens[i] == "-msxonly") {
475  display.getCliComm().printWarning(
476  "The -msxonly option has been deprecated and will "
477  "be removed in a future release. Instead, use the "
478  "-raw option for the same effect.");
479  rawShot = true;
480  } else if (tokens[i] == "-doublesize") {
481  doubleSize = true;
482  } else if (tokens[i] == "-with-osd") {
483  withOsd = true;
484  } else {
485  throw CommandException("Invalid option: " + tokens[i]);
486  }
487  } else {
488  arguments.push_back(tokens[i]);
489  }
490  }
491  if (doubleSize && !rawShot) {
492  throw CommandException("-doublesize option can only be used in "
493  "combination with -raw");
494  }
495  if (rawShot && withOsd) {
496  throw CommandException("-with-osd cannot be used in "
497  "combination with -raw");
498  }
499 
500  string filename;
501  switch (arguments.size()) {
502  case 0:
503  // nothing
504  break;
505  case 1:
506  filename = arguments[0];
507  break;
508  default:
509  throw SyntaxError();
510  }
512  filename, "screenshots", prefix, ".png");
513 
514  if (!rawShot) {
515  // include all layers (OSD stuff, console)
516  try {
517  display.getVideoSystem().takeScreenShot(filename, withOsd);
518  } catch (MSXException& e) {
519  throw CommandException(
520  "Failed to take screenshot: " + e.getMessage());
521  }
522  } else {
523  auto videoLayer = dynamic_cast<VideoLayer*>(
524  display.findActiveLayer());
525  if (!videoLayer) {
526  throw CommandException(
527  "Current renderer doesn't support taking screenshots.");
528  }
529  unsigned height = doubleSize ? 480 : 240;
530  try {
531  videoLayer->takeRawScreenShot(height, filename);
532  } catch (MSXException& e) {
533  throw CommandException(
534  "Failed to take screenshot: " + e.getMessage());
535  }
536  }
537 
538  display.getCliComm().printInfo("Screen saved to " + filename);
539  return filename;
540 }
541 
542 string ScreenShotCmd::help(const vector<string>& /*tokens*/) const
543 {
544  // Note: -no-sprites option is implemented in Tcl
545  return
546  "screenshot Write screenshot to file \"openmsxNNNN.png\"\n"
547  "screenshot <filename> Write screenshot to indicated file\n"
548  "screenshot -prefix foo Write screenshot to file \"fooNNNN.png\"\n"
549  "screenshot -raw 320x240 raw screenshot (of MSX screen only)\n"
550  "screenshot -raw -doublesize 640x480 raw screenshot (of MSX screen only)\n"
551  "screenshot -with-osd Include OSD elements in the screenshot\n"
552  "screenshot -no-sprites Don't include sprites in the screenshot\n";
553 }
554 
555 void ScreenShotCmd::tabCompletion(vector<string>& tokens) const
556 {
557  static const char* const extra[] = {
558  "-prefix", "-raw", "-doublesize", "-with-osd", "-no-sprites",
559  };
560  completeFileName(tokens, UserFileContext(), extra);
561 }
562 
563 
564 // FpsInfoTopic
565 
567  Display& display_)
568  : InfoTopic(openMSXInfoCommand, "fps")
569  , display(display_)
570 {
571 }
572 
573 void FpsInfoTopic::execute(const vector<TclObject>& /*tokens*/,
574  TclObject& result) const
575 {
576  double fps = 1000000.0 * Display::NUM_FRAME_DURATIONS / display.frameDurationSum;
577  result.setDouble(fps);
578 }
579 
580 string FpsInfoTopic::help(const vector<string>& /*tokens*/) const
581 {
582  return "Returns the current rendering speed in frames per second.";
583 }
584 
585 } // namespace openmsx