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