OXYGENHUB
◆ main openmsx / src / video / Display.cc
14.3 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "Display.hh"
2
3
#include "ImGuiManager.hh"
4
#include "Layer.hh"
5
#include "OutputSurface.hh"
6
#include "RendererFactory.hh"
7
#include "VideoLayer.hh"
8
#include "VideoSystem.hh"
9
#include "VideoSystemChangeListener.hh"
10
11
#include "BooleanSetting.hh"
12
#include "CliComm.hh"
13
#include "CommandException.hh"
14
#include "EnumSetting.hh"
15
#include "Event.hh"
16
#include "EventDistributor.hh"
17
#include "FileContext.hh"
18
#include "FileOperations.hh"
19
#include "HardwareConfig.hh"
20
#include "IntegerSetting.hh"
21
#include "MSXMotherBoard.hh"
22
#include "Reactor.hh"
23
#include "TclArgParser.hh"
24
#include "Timer.hh"
25
#include "Version.hh"
26
#include "XMLElement.hh"
27
28
#include "join.hh"
29
#include "narrow.hh"
30
#include "outer.hh"
31
#include "stl.hh"
32
#include "unreachable.hh"
33
#include "xrange.hh"
34
35
#include "build-info.hh"
36
37
#include <algorithm>
38
#include <array>
39
#include <cassert>
40
41
namespace openmsx {
42
43
Display::Display(Reactor& reactor_)
44
	: RTSchedulable(reactor_.getRTScheduler())
45
	, screenShotCmd(reactor_.getCommandController())
46
	, fpsInfo(reactor_.getOpenMSXInfoCommand())
47
	, osdGui(reactor_.getCommandController(), *this)
48
	, reactor(reactor_)
49
	, renderSettings(reactor.getCommandController())
50
{
51
	frameDurationSum = 0;
52
	repeat(NUM_FRAME_DURATIONS, [&] {
53
		frameDurations.push_front(20);
54
		frameDurationSum += 20;
55
	});
56
	prevTimeStamp = Timer::getTime();
57
58
	EventDistributor& eventDistributor = reactor.getEventDistributor();
59
	using enum EventType;
60
	for (auto type : {FINISH_FRAME, SWITCH_RENDERER, MACHINE_LOADED, WINDOW}) {
61
		eventDistributor.registerEventListener(type, *this);
62
	}
63
64
	renderSettings.getRendererSetting().attach(*this);
65
}
66
67
Display::~Display()
68
{
69
	renderSettings.getRendererSetting().detach(*this);
70
71
	EventDistributor& eventDistributor = reactor.getEventDistributor();
72
	using enum EventType;
73
	for (auto type : {WINDOW, MACHINE_LOADED, SWITCH_RENDERER, FINISH_FRAME}) {
74
		eventDistributor.unregisterEventListener(type, *this);
75
	}
76
77
	resetVideoSystem();
78
79
	assert(listeners.empty());
80
}
81
82
void Display::createVideoSystem()
83
{
84
	assert(!videoSystem);
85
	assert(currentRenderer == RenderSettings::RendererID::UNINITIALIZED);
86
	assert(!switchInProgress);
87
	switchInProgress = true;
88
	doRendererSwitch(renderSettings.getRenderer());
89
}
90
91
VideoSystem& Display::getVideoSystem()
92
{
93
	assert(videoSystem);
94
	return *videoSystem;
95
}
96
97
OutputSurface* Display::getOutputSurface()
98
{
99
	return videoSystem ? videoSystem->getOutputSurface() : nullptr;
100
}
101
102
std::optional<gl::vec2> Display::getMsxPixelSize()
103
{
104
	return videoSystem ? videoSystem->getMsxPixelSize() : std::nullopt;
105
}
106
107
void Display::resetVideoSystem()
108
{
109
	videoSystem.reset();
110
	// At this point all layers except for the Video9000 layer
111
	// should be gone.
112
	//assert(layers.empty());
113
}
114
115
CliComm& Display::getCliComm() const
116
{
117
	return reactor.getCliComm();
118
}
119
120
void Display::attach(VideoSystemChangeListener& listener)
121
{
122
	assert(!contains(listeners, &listener));
123
	listeners.push_back(&listener);
124
}
125
126
void Display::detach(VideoSystemChangeListener& listener)
127
{
128
	move_pop_back(listeners, rfind_unguarded(listeners, &listener));
129
}
130
131
Layer* Display::findActiveLayer() const
132
{
133
	auto it = std::ranges::find_if(layers, &Layer::isActive);
134
	return (it != layers.end()) ? *it : nullptr;
135
}
136
137
Display::Layers::iterator Display::baseLayer()
138
{
139
	// Note: It is possible to cache this, but since the number of layers is
140
	//       low at the moment, it's not really worth it.
141
	auto it = end(layers);
142
	while (true) {
143
		if (it == begin(layers)) {
144
			// There should always be at least one opaque layer.
145
			// TODO: This is not true for DummyVideoSystem.
146
			//       Anyway, a missing layer will probably stand out visually,
147
			//       so do we really have to assert on it?
148
			//UNREACHABLE;
149
			return it;
150
		}
151
		--it;
152
		if ((*it)->getCoverage() == Layer::Coverage::FULL) return it;
153
	}
154
}
155
156
void Display::executeRT()
157
{
158
	repaint();
159
}
160
161
bool Display::signalEvent(const Event& event)
162
{
163
	std::visit(overloaded{
164
		[&](const FinishFrameEvent& e) {
165
			if (e.needRender()) {
166
				repaint();
167
				reactor.getEventDistributor().distributeEvent(FrameDrawnEvent());
168
			}
169
		},
170
		[&](const SwitchRendererEvent& e) {
171
			doRendererSwitch(e.getRenderer()); // might throw
172
		},
173
		[&](const MachineLoadedEvent& /*e*/) {
174
			videoSystem->updateWindowTitle();
175
		},
176
		[&](const WindowEvent& e) {
177
			const auto& evt = e.getSdlWindowEvent();
178
			if (evt.event == SDL_WINDOWEVENT_EXPOSED) {
179
				// Don't render too often, and certainly not when the screen
180
				// will anyway soon be rendered.
181
				repaintDelayed(100 * 1000); // 10fps
182
			}
183
			if (PLATFORM_ANDROID && e.isMainWindow() &&
184
			    evt.event == one_of(SDL_WINDOWEVENT_FOCUS_GAINED, SDL_WINDOWEVENT_FOCUS_LOST)) {
185
				// On Android, the rendering must be frozen when the app is sent to
186
				// the background, because Android takes away all graphics resources
187
				// from the app. It simply destroys the entire graphics context.
188
				// Though, a repaint() must happen within the focus-lost event
189
				// so that the SDL Android port realizes that the graphics context
190
				// is gone and will re-build it again on the first flush to the
191
				// surface after the focus has been regained.
192
193
				// Perform a repaint before updating the renderFrozen flag:
194
				// -When loosing the focus, this repaint will flush a last
195
				//  time the SDL surface, making sure that the Android SDL
196
				//  port discovers that the graphics context is gone.
197
				// -When gaining the focus, this repaint does nothing as
198
				//  the renderFrozen flag is still false
199
				repaint();
200
				bool lost = evt.event == SDL_WINDOWEVENT_FOCUS_LOST;
201
				renderFrozen = lost;
202
			}
203
		},
204
		[](const EventBase&) { /*ignore*/ }
205
	}, event);
206
	return false;
207
}
208
209
std::string Display::getWindowTitle()
210
{
211
	auto title = strCat(Version::full(), strCat_if(!Version::RELEASE, " [", BUILD_FLAVOUR, ']'));
212
	if (MSXMotherBoard* motherboard = reactor.getMotherBoard()) {
213
		if (const HardwareConfig* machine = motherboard->getMachineConfig()) {
214
			const auto& config = machine->getConfig();
215
			strAppend(title, " - ",
216
			          config.getChild("info").getChildData("manufacturer"), ' ',
217
			          config.getChild("info").getChildData("code"));
218
		}
219
	}
220
	return title;
221
}
222
223
gl::ivec2 Display::getWindowPosition()
224
{
225
	if (auto pos = videoSystem->getWindowPosition()) {
226
		storeWindowPosition(*pos);
227
	}
228
	return retrieveWindowPosition();
229
}
230
231
void Display::setWindowPosition(gl::ivec2 pos)
232
{
233
	storeWindowPosition(pos);
234
	videoSystem->setWindowPosition(pos);
235
}
236
237
void Display::storeWindowPosition(gl::ivec2 pos)
238
{
239
	reactor.getImGuiManager().storeWindowPosition(pos);
240
}
241
242
gl::ivec2 Display::retrieveWindowPosition()
243
{
244
	return reactor.getImGuiManager().retrieveWindowPosition();
245
}
246
247
gl::ivec2 Display::getWindowSize() const
248
{
249
	int factor = renderSettings.getScaleFactor();
250
	return {320 * factor, 240 * factor};
251
}
252
253
float Display::getFps() const
254
{
255
	return 1000000.0f * NUM_FRAME_DURATIONS / narrow_cast<float>(frameDurationSum);
256
}
257
258
void Display::update(const Setting& setting) noexcept
259
{
260
	assert(&setting == &renderSettings.getRendererSetting()); (void)setting;
261
	checkRendererSwitch();
262
}
263
264
void Display::checkRendererSwitch()
265
{
266
	if (switchInProgress) {
267
		// This method only queues a request to switch renderer (see
268
		// comments below why). If there already is such a request
269
		// queued we don't need to do it again.
270
		return;
271
	}
272
	auto newRenderer = renderSettings.getRenderer();
273
	if (newRenderer != currentRenderer) {
274
		// don't do the actual switching in the Tcl callback
275
		// it seems creating and destroying Settings (= Tcl vars)
276
		// causes problems???
277
		switchInProgress = true;
278
		reactor.getEventDistributor().distributeEvent(SwitchRendererEvent(newRenderer));
279
	}
280
}
281
282
void Display::doRendererSwitch(RenderSettings::RendererID newRenderer)
283
{
284
	assert(switchInProgress);
285
286
	bool success = false;
287
	while (!success) {
288
		try {
289
			doRendererSwitch2(newRenderer);
290
			success = true;
291
		} catch (MSXException& e) {
292
			auto& rendererSetting = renderSettings.getRendererSetting();
293
			std::string errorMsg = strCat(
294
				"Couldn't activate renderer ",
295
				rendererSetting.getString(),
296
				": ", e.getMessage());
297
			// now try some things that might work against this:
298
			auto& scaleFactorSetting = renderSettings.getScaleFactorSetting();
299
			auto curVal = scaleFactorSetting.getInt();
300
			if (curVal == MIN_SCALE_FACTOR) {
301
				throw FatalError(
302
					e.getMessage(),
303
					" (and I have no other ideas to try...)"); // give up and die... :(
304
			}
305
			strAppend(errorMsg, "\nTrying to decrease scale_factor setting from ",
306
					curVal, " to ", curVal - 1, "...");
307
			scaleFactorSetting.setInt(curVal - 1);
308
			getCliComm().printWarning(errorMsg);
309
		}
310
	}
311
312
	switchInProgress = false;
313
}
314
315
void Display::doRendererSwitch2(RenderSettings::RendererID newRenderer)
316
{
317
	for (auto& l : listeners) {
318
		l->preVideoSystemChange();
319
	}
320
321
	resetVideoSystem();
322
	currentRenderer = newRenderer;
323
	videoSystem = RendererFactory::createVideoSystem(reactor);
324
325
	for (auto& l : listeners) {
326
		l->postVideoSystemChange();
327
	}
328
}
329
330
void Display::repaintImpl()
331
{
332
	if (switchInProgress) {
333
		// The checkRendererSwitch() method will queue a
334
		// SWITCH_RENDERER_EVENT, but before that event is handled
335
		// we shouldn't do any repaints (with inconsistent setting
336
		// values and render objects). This can happen when this
337
		// method gets called because of a DELAYED_REPAINT_EVENT
338
		// (DELAYED_REPAINT_EVENT was already queued before
339
		// SWITCH_RENDERER_EVENT is queued).
340
		return;
341
	}
342
343
	cancelRT(); // cancel delayed repaint
344
345
	if (!renderFrozen) {
346
		assert(videoSystem);
347
		if (OutputSurface* surface = videoSystem->getOutputSurface()) {
348
			repaintImpl(*surface);
349
			videoSystem->flush();
350
		}
351
	}
352
353
	// update fps statistics
354
	auto now = Timer::getTime();
355
	auto duration = now - prevTimeStamp;
356
	prevTimeStamp = now;
357
	frameDurationSum += duration - frameDurations.pop_back();
358
	frameDurations.push_front(duration);
359
360
	// TODO maybe revisit this later (and/or simplify other calls to repaintDelayed())
361
	// This ensures a minimum framerate for ImGui
362
	repaintDelayed(40 * 1000); // 25fps
363
}
364
365
void Display::repaintImpl(OutputSurface& surface)
366
{
367
	for (auto it = baseLayer(); it != end(layers); ++it) {
368
		if ((*it)->getCoverage() != Layer::Coverage::NONE) {
369
			(*it)->paint(surface);
370
		}
371
	}
372
}
373
374
void Display::repaint()
375
{
376
	// Request a repaint from the VideoSystem. This may call repaintImpl()
377
	// directly or for example defer to a signal callback on VisibleSurface.
378
	videoSystem->repaint();
379
}
380
381
void Display::repaintDelayed(uint64_t delta)
382
{
383
	if (isPendingRT()) {
384
		// already a pending repaint
385
		return;
386
	}
387
	scheduleRT(unsigned(delta));
388
}
389
390
void Display::addLayer(Layer& layer)
391
{
392
	auto z = layer.getZ();
393
	auto it = std::ranges::find_if(layers, [&](const Layer* l) { return l->getZ() > z; });
394
	layers.insert(it, &layer);
395
	layer.setDisplay(*this);
396
}
397
398
void Display::removeLayer(Layer& layer)
399
{
400
	layers.erase(rfind_unguarded(layers, &layer));
401
}
402
403
void Display::updateZ(Layer& layer)
404
{
405
	auto oldPos = rfind_unguarded(layers, &layer);
406
	auto z = layer.getZ();
407
	auto newPos = std::ranges::find_if(layers, [&](const Layer* l) { return l->getZ() >= z; });
408
409
	if (oldPos == newPos) {
410
		return;
411
	} else if (oldPos < newPos) {
412
		std::rotate(oldPos, oldPos + 1, newPos);
413
	} else {
414
		std::rotate(newPos, oldPos, oldPos + 1);
415
	}
416
}
417
418
419
// ScreenShotCmd
420
421
Display::ScreenShotCmd::ScreenShotCmd(CommandController& commandController_)
422
	: Command(commandController_, "openmsx::internal_screenshot")
423
{
424
}
425
426
void Display::ScreenShotCmd::execute(std::span<const TclObject> tokens, TclObject& result)
427
{
428
	std::string_view prefix = "openmsx";
429
	bool rawShot = false;
430
	bool doubleSize = false;
431
	bool withOsd = false;
432
	std::string size;
433
	std::array info = {
434
		valueArg("-prefix", prefix),
435
		flagArg("-raw", rawShot),
436
		flagArg("-doublesize", doubleSize), // bwcompat, alias for -size 640
437
		flagArg("-with-osd", withOsd),
438
		valueArg("-size", size)
439
	};
440
	auto arguments = parseTclArgs(getInterpreter(), tokens.subspan(1), info);
441
442
	auto& display = OUTER(Display, screenShotCmd);
443
	if (doubleSize && !rawShot) {
444
		throw CommandException("-doublesize option can only be used in "
445
		                       "combination with -raw");
446
	}
447
	if (rawShot && withOsd) {
448
		throw CommandException("-with-osd cannot be used in "
449
		                       "combination with -raw");
450
	}
451
	if (!size.empty()) {
452
		if (!rawShot) {
453
			throw CommandException("-size option can only be used in "
454
			                       "combination with -raw");
455
		}
456
		static constexpr std::array<std::string_view, 3> validSizes = { "auto", "320", "640" };
457
		if (!contains(validSizes, size)) {
458
			throw CommandException(strCat("-size option must specify one of: ", join(validSizes, ", ")));
459
		}
460
		if (doubleSize) {
461
			throw CommandException("Only specify either -size or -doublesize");
462
		}
463
	}
464
465
	// backwards compatiblity
466
	if (doubleSize) {
467
		size = "640";
468
	}
469
	// default size for raw
470
	if (rawShot && size.empty()) {
471
		size = "auto";
472
	}
473
474
	std::string_view fname;
475
	switch (arguments.size()) {
476
	case 0:
477
		// nothing
478
		break;
479
	case 1:
480
		fname = arguments[0].getString();
481
		break;
482
	default:
483
		throw SyntaxError();
484
	}
485
	std::string filename = FileOperations::parseCommandFileArgument(
486
		fname, SCREENSHOT_DIR, prefix, SCREENSHOT_EXTENSION);
487
488
	if (!rawShot) {
489
		// take screenshot as displayed, possibly with other layers (OSD stuff, ImGUI)
490
		try {
491
			display.getVideoSystem().takeScreenShot(filename, withOsd);
492
		} catch (MSXException& e) {
493
			throw CommandException(
494
				"Failed to take screenshot: ", e.getMessage());
495
		}
496
	} else {
497
		auto* videoLayer = dynamic_cast<VideoLayer*>(
498
			display.findActiveLayer());
499
		if (!videoLayer) {
500
			throw CommandException(
501
				"Current renderer doesn't support taking screenshots.");
502
		}
503
		std::optional<unsigned> height = size == "auto" ? std::nullopt : size == "640" ? std::optional(480) : std::optional(240);
504
		try {
505
			videoLayer->takeRawScreenShot(height, filename);
506
		} catch (MSXException& e) {
507
			throw CommandException(
508
				"Failed to take screenshot: ", e.getMessage());
509
		}
510
	}
511
512
	result = filename;
513
}
514
515
std::string Display::ScreenShotCmd::help(std::span<const TclObject> /*tokens*/) const
516
{
517
	return "This is a low-level internal command, you probably want to use 'screenshot' instead.";
518
}
519
520
521
// FpsInfoTopic
522
523
Display::FpsInfoTopic::FpsInfoTopic(InfoCommand& openMSXInfoCommand)
524
	: InfoTopic(openMSXInfoCommand, "fps")
525
{
526
}
527
528
void Display::FpsInfoTopic::execute(std::span<const TclObject> /*tokens*/,
529
                                    TclObject& result) const
530
{
531
	auto& display = OUTER(Display, fpsInfo);
532
	result = display.getFps();
533
}
534
535
std::string Display::FpsInfoTopic::help(std::span<const TclObject> /*tokens*/) const
536
{
537
	return "Returns the current rendering speed in frames per second.";
538
}
539
540
} // namespace openmsx