OXYGENHUB
◆ main openmsx / src / video / VisibleSurface.cc
14.7 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "VisibleSurface.hh"
2
3
#include "Display.hh"
4
#include "GlobalSettings.hh"
5
#include "GLContext.hh"
6
#include "GLSnow.hh"
7
#include "GLUtil.hh"
8
#include "OffScreenSurface.hh"
9
#include "RenderSettings.hh"
10
#include "VideoSystem.hh"
11
12
#include "BooleanSetting.hh"
13
#include "CliComm.hh"
14
#include "Event.hh"
15
#include "EventDistributor.hh"
16
#include "FileContext.hh"
17
#include "FloatSetting.hh"
18
#include "Icon.hh"
19
#include "ImGuiLayer.hh"
20
#include "InitException.hh"
21
#include "InputEventGenerator.hh"
22
#include "MemBuffer.hh"
23
#include "OSDGUILayer.hh"
24
#include "PNG.hh"
25
26
#include "narrow.hh"
27
#include "outer.hh"
28
#include "small_buffer.hh"
29
30
#include "build-info.hh"
31
32
#include <imgui.h>
33
#include <imgui_impl_opengl3.h>
34
#include <imgui_impl_sdl2.h>
35
36
#include <bit>
37
#include <cassert>
38
#include <memory>
39
#include <ranges>
40
41
namespace openmsx {
42
43
VisibleSurface::VisibleSurface(
44
		Display& display_,
45
		RTScheduler& rtScheduler_,
46
		EventDistributor& eventDistributor_,
47
		InputEventGenerator& inputEventGenerator_,
48
		CliComm& cliComm_,
49
		VideoSystem& videoSystem_,
50
		BooleanSetting& pauseSetting_)
51
	: RTSchedulable(rtScheduler_)
52
	, display(display_)
53
	, eventDistributor(eventDistributor_)
54
	, inputEventGenerator(inputEventGenerator_)
55
	, cliComm(cliComm_)
56
	, videoSystem(videoSystem_)
57
	, pauseSetting(pauseSetting_)
58
{
59
	auto& renderSettings = display.getRenderSettings();
60
61
	updateCursor();
62
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
63
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0);
64
	SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 0);
65
#if OPENGL_VERSION == OPENGL_ES_2_0
66
	#define VERSION_STRING "openGL ES 2.0"
67
	const char* glsl_version = "#version 100";
68
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
69
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
70
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
71
#elif OPENGL_VERSION == OPENGL_2_1
72
	#define VERSION_STRING "openGL 2.1"
73
	const char* glsl_version = "#version 120";
74
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
75
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
76
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
77
#elif OPENGL_VERSION == OPENGL_3_3
78
	#define VERSION_STRING "openGL 3.3"
79
	const char* glsl_version = "#version 150";
80
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
81
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
82
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
83
#endif
84
85
	int flags = SDL_WINDOW_OPENGL;
86
	//flags |= SDL_RESIZABLE;
87
	auto size = display.getWindowSize();
88
	createSurface(size, flags);
89
	WindowEvent::setMainWindowId(SDL_GetWindowID(window.get()));
90
91
	glContext = SDL_GL_CreateContext(window.get());
92
	if (!glContext) {
93
		throw InitException(
94
			"Failed to create " VERSION_STRING " context: ", SDL_GetError());
95
	}
96
97
	// Setup ImGui Platform/Renderer backends
98
	ImGui_ImplSDL2_InitForOpenGL(window.get(), glContext);
99
	ImGui_ImplOpenGL3_Init(glsl_version);
100
101
	// From the glew documentation:
102
	//   GLEW obtains information on the supported extensions from the
103
	//   graphics driver. Experimental or pre-release drivers, however,
104
	//   might not report every available extension through the standard
105
	//   mechanism, in which case GLEW will report it unsupported. To
106
	//   circumvent this situation, the glewExperimental global switch can
107
	//   be turned on by setting it to GL_TRUE before calling glewInit(),
108
	//   which ensures that all extensions with valid entry points will be
109
	//   exposed.
110
	// The 'glewinfo' utility also sets this flag before reporting results,
111
	// so I believe it would cause less confusion to do the same here.
112
	glewExperimental = GL_TRUE;
113
114
	// Initialise GLEW library.
115
	// GLEW fails to initialise on Wayland because it has no GLX, since the
116
	// one provided by the distros was built to use GLX instead of EGL. We
117
	// ignore the GLEW_ERROR_NO_GLX_DISPLAY error with this temporary fix
118
	// until it is fixed by GLEW upstream and released by major distros.
119
	// See https://github.com/nigels-com/glew/issues/172
120
	if (GLenum glew_error = glewInit();
121
	    glew_error != GLEW_OK && glew_error != GLEW_ERROR_NO_GLX_DISPLAY) {
122
		throw InitException(
123
			"Failed to init GLEW: ",
124
			std::bit_cast<const char*>(glewGetErrorString(glew_error)));
125
	}
126
	if (!GLEW_VERSION_2_1) {
127
		throw InitException(
128
			"Need at least OpenGL version " VERSION_STRING);
129
	}
130
	gl::context.emplace();
131
132
	bool fullScreen = renderSettings.getFullScreen();
133
	setViewPort(size, fullScreen); // set initial values
134
135
	renderSettings.getVSyncSetting().attach(vSyncObserver);
136
	// set initial value
137
	vSyncObserver.update(renderSettings.getVSyncSetting());
138
139
#if OPENGL_VERSION == OPENGL_3_3
140
	// We don't (yet/anymore) use VAO, but apparently it's required in openGL 3.3.
141
	// Luckily this workaround is sufficient: create one global VAO and then don't care anymore.
142
	GLuint vao;
143
	glGenVertexArrays(1, &vao);
144
	glBindVertexArray(vao);
145
#endif
146
	inputEventGenerator.initializeGrab();
147
148
	// Keep last: if the ctor throws, ~VisibleSurface won't run to detach.
149
	inputEventGenerator.getGrabInput().attach(*this);
150
	renderSettings.getPointerHideDelaySetting().attach(*this);
151
	renderSettings.getFullScreenSetting().attach(*this);
152
	pauseSetting.attach(*this);
153
154
	for (auto type : {EventType::MOUSE_MOTION,
155
	                  EventType::MOUSE_BUTTON_DOWN,
156
	                  EventType::MOUSE_BUTTON_UP,
157
	                  EventType::IMGUI_ACTIVE}) {
158
		eventDistributor.registerEventListener(type, *this);
159
	}
160
}
161
162
VisibleSurface::~VisibleSurface()
163
{
164
	auto& renderSettings = display.getRenderSettings();
165
	renderSettings.getVSyncSetting().detach(vSyncObserver);
166
167
	ImGui_ImplOpenGL3_Shutdown();
168
	ImGui_ImplSDL2_Shutdown();
169
170
	gl::context.reset();
171
	SDL_GL_DeleteContext(glContext);
172
173
	// store last known position for when we recreate it
174
	// the window gets recreated when changing renderers, for instance.
175
	// Do not store if we're full screen, the location is the top-left
176
	if (auto pos = getWindowPosition()) {
177
		display.storeWindowPosition(*pos);
178
	}
179
180
	for (auto type : {EventType::IMGUI_ACTIVE,
181
	                  EventType::MOUSE_BUTTON_UP,
182
	                  EventType::MOUSE_BUTTON_DOWN,
183
	                  EventType::MOUSE_MOTION}) {
184
		eventDistributor.unregisterEventListener(type, *this);
185
	}
186
187
	inputEventGenerator.getGrabInput().detach(*this);
188
	renderSettings.getPointerHideDelaySetting().detach(*this);
189
	renderSettings.getFullScreenSetting().detach(*this);
190
	pauseSetting.detach(*this);
191
}
192
193
std::optional<gl::ivec2> VisibleSurface::getWindowPosition() const
194
{
195
	if (SDL_GetWindowFlags(window.get()) & SDL_WINDOW_FULLSCREEN) return {};
196
	int x, y;
197
	SDL_GetWindowPosition(window.get(), &x, &y);
198
	return gl::ivec2{x, y};
199
}
200
201
void VisibleSurface::setWindowPosition(gl::ivec2 pos)
202
{
203
	if (SDL_GetWindowFlags(window.get()) & SDL_WINDOW_FULLSCREEN) return;
204
	SDL_SetWindowPosition(window.get(), pos.x, pos.y);
205
}
206
207
// TODO: The video subsystem is not de-initialized on errors.
208
//       While it would be consistent to do so, doing it in this class is
209
//       not ideal since the init doesn't happen here.
210
void VisibleSurface::createSurface(gl::ivec2 size, unsigned flags)
211
{
212
	if (getDisplay().getRenderSettings().getFullScreen()) {
213
		flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
214
	}
215
#ifdef __APPLE__
216
	// See VisibleSurface::setViewPort() for why only macos (for now).
217
	flags |= SDL_WINDOW_ALLOW_HIGHDPI;
218
#endif
219
220
	assert(!window);
221
	auto pos = display.retrieveWindowPosition();
222
	window.reset(SDL_CreateWindow(
223
			getDisplay().getWindowTitle().c_str(),
224
			pos.x, pos.y,
225
			size.x, size.y,
226
			flags));
227
	if (!window) {
228
		std::string err = SDL_GetError();
229
		throw InitException("Could not create window: ", err);
230
	}
231
232
	updateWindowTitle();
233
234
	// prefer linear filtering (instead of nearest neighbour)
235
	SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
236
237
	// set icon
238
	if constexpr (OPENMSX_SET_WINDOW_ICON) {
239
		SDLSurfacePtr iconSurf;
240
		// always use 32x32 icon on Windows, for some reason you get badly scaled icons there
241
#ifndef _WIN32
242
		try {
243
			iconSurf = PNG::load(preferSystemFileContext().resolve("icons/openMSX-logo-256.png"), true);
244
		} catch (MSXException& e) {
245
			getCliComm().printWarning(
246
				"Falling back to built in 32x32 icon, because failed to load icon: ",
247
				e.getMessage());
248
#endif
249
			PixelOperations pixelOps;
250
			iconSurf.reset(SDL_CreateRGBSurfaceFrom(
251
				const_cast<char*>(openMSX_icon.pixel_data),
252
				narrow<int>(openMSX_icon.width),
253
				narrow<int>(openMSX_icon.height),
254
				narrow<int>(openMSX_icon.bytes_per_pixel * 8),
255
				narrow<int>(openMSX_icon.bytes_per_pixel * openMSX_icon.width),
256
				pixelOps.getRmask(),
257
				pixelOps.getGmask(),
258
				pixelOps.getBmask(),
259
				pixelOps.getAmask()));
260
#ifndef _WIN32
261
		}
262
#endif
263
		SDL_SetColorKey(iconSurf.get(), SDL_TRUE, 0);
264
		SDL_SetWindowIcon(window.get(), iconSurf.get());
265
	}
266
}
267
268
void VisibleSurface::update(const Setting& /*setting*/) noexcept
269
{
270
	updateCursor();
271
}
272
273
void VisibleSurface::executeRT()
274
{
275
	// timer expired, hide cursor
276
	videoSystem.showCursor(false);
277
	inputEventGenerator.updateGrab(grab);
278
}
279
280
bool VisibleSurface::signalEvent(const Event& event)
281
{
282
	if (getType(event) == EventType::IMGUI_ACTIVE) {
283
		guiActive = get_event<ImGuiActiveEvent>(event).getActive();
284
	}
285
	updateCursor();
286
	return false;
287
}
288
289
void VisibleSurface::updateCursor()
290
{
291
	cancelRT();
292
	const auto& renderSettings = display.getRenderSettings();
293
	grab = !guiActive && !pauseSetting.getBoolean() &&
294
	       (renderSettings.getFullScreen() ||
295
	        inputEventGenerator.getGrabInput().getBoolean());
296
	if (grab) {
297
		// always hide cursor in fullscreen or grab-input mode, but do it
298
		// after the derived class is constructed to avoid an SDL bug.
299
		scheduleRT(0);
300
		return;
301
	}
302
	inputEventGenerator.updateGrab(grab);
303
	float delay = renderSettings.getPointerHideDelay();
304
	if (delay == 0.0f) {
305
		videoSystem.showCursor(false);
306
	} else {
307
		videoSystem.showCursor(true);
308
		if (delay > 0.0f) {
309
			scheduleRT(int(delay * 1e6f)); // delay in s, schedule in us
310
		}
311
	}
312
}
313
314
bool VisibleSurface::setFullScreen(bool fullscreen)
315
{
316
	auto flags = SDL_GetWindowFlags(window.get());
317
	// Note: SDL_WINDOW_FULLSCREEN_DESKTOP also has the SDL_WINDOW_FULLSCREEN
318
	//       bit set.
319
	if (bool currentState = (flags & SDL_WINDOW_FULLSCREEN) != 0;
320
	    currentState == fullscreen) {
321
		// already wanted stated
322
		return true;
323
	}
324
325
	if (SDL_SetWindowFullscreen(window.get(),
326
			fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0) != 0) {
327
		return false; // error, try re-creating the window
328
	}
329
	fullScreenUpdated(fullscreen);
330
	return true; // success
331
}
332
333
void VisibleSurface::resize()
334
{
335
	auto size = display.getWindowSize();
336
	SDL_SetWindowSize(window.get(), size.x, size.y);
337
338
	bool fullScreen = display.getRenderSettings().getFullScreen();
339
	setViewPort(size, fullScreen);
340
}
341
342
void VisibleSurface::updateWindowTitle()
343
{
344
	assert(window);
345
	SDL_SetWindowTitle(window.get(), getDisplay().getWindowTitle().c_str());
346
}
347
348
std::optional<gl::ivec2> VisibleSurface::getMouseCoord() const
349
{
350
	if (SDL_GetMouseFocus() != window.get()) return {};
351
352
	int mouseX, mouseY;
353
	SDL_GetMouseState(&mouseX, &mouseY);
354
	return gl::ivec2{mouseX, mouseY};
355
}
356
357
gl::vec2 VisibleSurface::getMsxPixelSize() const
358
{
359
	// SDL reports mouse coordinates in window coordinates ('points').
360
	int windowW, windowH;
361
	SDL_GetWindowSize(window.get(), &windowW, &windowH);
362
	auto windowSize = gl::vec2(narrow<float>(windowW), narrow<float>(windowH));
363
364
	// The image shows 'horizontal_stretch' of the 320 MSX pixels per line,
365
	// and always all 240 lines.
366
	const auto& renderSettings = display.getRenderSettings();
367
	auto msxSize = gl::vec2(renderSettings.getHorizontalStretch(), 240.0f);
368
369
	if (renderSettings.getFullStretch()) {
370
		return windowSize / msxSize; // image covers the whole window
371
	}
372
	// The viewport is expressed in physical pixels, which on high-DPI
373
	// displays are not the same as points, so convert via the window size.
374
	return (gl::vec2(getViewSize()) * windowSize) /
375
	       (gl::vec2(getPhysicalSize()) * msxSize);
376
}
377
378
379
void VisibleSurface::saveScreenshot(const std::string& filename)
380
{
381
	saveScreenshotGL(*this, filename);
382
}
383
384
void VisibleSurface::saveScreenshotGL(
385
	const OutputSurface& output, const std::string& filename)
386
{
387
	auto [x, y] = output.getViewOffset();
388
	auto [w, h] = output.getViewSize();
389
390
	// OpenGL ES only supports reading RGBA (not RGB)
391
	MemBuffer<uint32_t> buffer(size_t(w) * size_t(h));
392
	glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, buffer.data());
393
394
	small_buffer<const uint32_t*, 1080> rowPointers(std::views::transform(xrange(size_t(h)),
395
		[&](auto i) { return &buffer[size_t(w) * (h - 1 - i)]; }));
396
397
	PNG::saveRGBA(w, rowPointers, filename);
398
}
399
400
void VisibleSurface::finish()
401
{
402
	SDL_GL_SwapWindow(window.get());
403
}
404
405
std::unique_ptr<Layer> VisibleSurface::createSnowLayer()
406
{
407
	return std::make_unique<GLSnow>(getDisplay());
408
}
409
410
std::unique_ptr<Layer> VisibleSurface::createOSDGUILayer(OSDGUI& gui)
411
{
412
	return std::make_unique<OSDGUILayer>(gui);
413
}
414
415
std::unique_ptr<Layer> VisibleSurface::createImGUILayer(ImGuiManager& manager)
416
{
417
	return std::make_unique<ImGuiLayer>(manager);
418
}
419
420
std::unique_ptr<OutputSurface> VisibleSurface::createOffScreenSurface()
421
{
422
	return std::make_unique<OffScreenSurface>(*this);
423
}
424
425
void VisibleSurface::VSyncObserver::update(const Setting& setting) noexcept
426
{
427
	const auto& visSurface = OUTER(VisibleSurface, vSyncObserver);
428
	const auto& syncSetting = visSurface.getDisplay().getRenderSettings().getVSyncSetting();
429
	assert(&setting == &syncSetting); (void)setting;
430
431
	// for now, we assume that adaptive vsync is the best kind of vsync, so when
432
	// vsync is enabled, we attempt adaptive vsync.
433
	int interval = syncSetting.getBoolean() ? -1 : 0;
434
435
	if ((SDL_GL_SetSwapInterval(interval) < 0) && (interval == -1)) {
436
		// "Adaptive vsync" is not supported by all drivers. SDL
437
		// documentation suggests to fallback to "regular vsync" in
438
		// that case.
439
		SDL_GL_SetSwapInterval(1);
440
	}
441
}
442
443
void VisibleSurface::setViewPort(gl::ivec2 logicalSize, bool fullScreen)
444
{
445
	gl::ivec2 physicalSize = [&] {
446
#ifndef __APPLE__
447
		// On macos we set 'SDL_WINDOW_ALLOW_HIGHDPI', and in that case
448
		// it's required to use SDL_GL_GetDrawableSize(), but then this
449
		// 'full screen'-workaround/hack is counter-productive.
450
		if (!fullScreen) {
451
			// ??? When switching  back from full screen to windowed mode,
452
			// SDL_GL_GetDrawableSize() still returns the dimensions of the
453
			// full screen window ??? Is this a bug ???
454
			// But we know that in windowed mode, physical and logical size
455
			// must be the same, so enforce that.
456
			return logicalSize;
457
		}
458
#endif
459
		(void)fullScreen;
460
		int w, h;
461
		SDL_GL_GetDrawableSize(window.get(), &w, &h);
462
		return gl::ivec2(w, h);
463
	}();
464
465
	// The created surface may be larger than requested.
466
	// If that happens, center the area that we actually use.
467
	calculateViewPort(logicalSize, physicalSize);
468
	// actually setting the viewport is done in PostProcessor::paint()
469
470
	gl::context->setupMvpMatrix(gl::vec2(logicalSize));
471
}
472
473
void VisibleSurface::fullScreenUpdated(bool fullScreen)
474
{
475
	setViewPort(getLogicalSize(), fullScreen);
476
}
477
478
} // namespace openmsx