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