openMSX
VisibleSurface.cc
Go to the documentation of this file.
1#include "VisibleSurface.hh"
2
3#include "BooleanSetting.hh"
4#include "CliComm.hh"
5#include "Display.hh"
6#include "Event.hh"
7#include "EventDistributor.hh"
8#include "FileContext.hh"
9#include "FloatSetting.hh"
10#include "GLContext.hh"
11#include "GLSnow.hh"
12#include "GLUtil.hh"
13#include "Icon.hh"
14#include "ImGuiLayer.hh"
15#include "InitException.hh"
17#include "MemBuffer.hh"
18#include "OffScreenSurface.hh"
19#include "OSDGUILayer.hh"
20#include "PNG.hh"
21#include "RenderSettings.hh"
22#include "VideoSystem.hh"
23
24#include "narrow.hh"
25#include "one_of.hh"
26#include "outer.hh"
27#include "vla.hh"
28
29#include "build-info.hh"
30
31#include <imgui.h>
32#include <imgui_impl_sdl2.h>
33#include <imgui_impl_opengl3.h>
34
35#include <cassert>
36#include <memory>
37
38namespace openmsx {
39
41 Display& display_,
42 RTScheduler& rtScheduler_,
43 EventDistributor& eventDistributor_,
44 InputEventGenerator& inputEventGenerator_,
45 CliComm& cliComm_,
46 VideoSystem& videoSystem_)
47 : RTSchedulable(rtScheduler_)
48 , display(display_)
49 , eventDistributor(eventDistributor_)
50 , inputEventGenerator(inputEventGenerator_)
51 , cliComm(cliComm_)
52 , videoSystem(videoSystem_)
53{
54 auto& renderSettings = display.getRenderSettings();
55
56 inputEventGenerator.getGrabInput().attach(*this);
57 renderSettings.getPointerHideDelaySetting().attach(*this);
58 renderSettings.getFullScreenSetting().attach(*this);
59 eventDistributor.registerEventListener(EventType::MOUSE_MOTION, *this);
62 eventDistributor.registerEventListener(EventType::IMGUI_ACTIVE, *this);
63
64 updateCursor();
65 SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
66 SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0);
67 SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 0);
68#if OPENGL_VERSION == OPENGL_ES_2_0
69 #define VERSION_STRING "openGL ES 2.0"
70 const char* glsl_version = "#version 100";
71 SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
72 SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
73 SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
74#elif OPENGL_VERSION == OPENGL_2_1
75 #define VERSION_STRING "openGL 2.1"
76 const char* glsl_version = "#version 120";
77 SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
78 SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
79 SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
80#elif OPENGL_VERSION == OPENGL_3_3
81 #define VERSION_STRING "openGL 3.3"
82 const char* glsl_version = "#version 150";
83 SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
84 SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
85 SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
86#endif
87
88 int flags = SDL_WINDOW_OPENGL;
89 //flags |= SDL_RESIZABLE;
90 auto [width, height] = getWindowSize();
91 createSurface(width, height, flags);
92 WindowEvent::setMainWindowId(SDL_GetWindowID(window.get()));
93
94 glContext = SDL_GL_CreateContext(window.get());
95 if (!glContext) {
96 throw InitException(
97 "Failed to create " VERSION_STRING " context: ", SDL_GetError());
98 }
99
100 // Setup ImGui Platform/Renderer backends
101 ImGui_ImplSDL2_InitForOpenGL(window.get(), glContext);
102 ImGui_ImplOpenGL3_Init(glsl_version);
103
104 // From the glew documentation:
105 // GLEW obtains information on the supported extensions from the
106 // graphics driver. Experimental or pre-release drivers, however,
107 // might not report every available extension through the standard
108 // mechanism, in which case GLEW will report it unsupported. To
109 // circumvent this situation, the glewExperimental global switch can
110 // be turned on by setting it to GL_TRUE before calling glewInit(),
111 // which ensures that all extensions with valid entry points will be
112 // exposed.
113 // The 'glewinfo' utility also sets this flag before reporting results,
114 // so I believe it would cause less confusion to do the same here.
115 glewExperimental = GL_TRUE;
116
117 // Initialise GLEW library.
118 GLenum glew_error = glewInit();
119
120 // GLEW fails to initialise on Wayland because it has no GLX, since the
121 // one provided by the distros was built to use GLX instead of EGL. We
122 // ignore the GLEW_ERROR_NO_GLX_DISPLAY error with this temporary fix
123 // until it is fixed by GLEW upstream and released by major distros.
124 // See https://github.com/nigels-com/glew/issues/172
125 if (glew_error != GLEW_OK && glew_error != GLEW_ERROR_NO_GLX_DISPLAY) {
126 throw InitException(
127 "Failed to init GLEW: ",
128 reinterpret_cast<const char*>(
129 glewGetErrorString(glew_error)));
130 }
131 if (!GLEW_VERSION_2_1) {
132 throw InitException(
133 "Need at least OpenGL version " VERSION_STRING);
134 }
135 gl::context.emplace();
136
137 bool fullScreen = renderSettings.getFullScreen();
138 setViewPort(gl::ivec2(width, height), fullScreen); // set initial values
139
140 renderSettings.getVSyncSetting().attach(vSyncObserver);
141 // set initial value
142 vSyncObserver.update(renderSettings.getVSyncSetting());
143
144#if OPENGL_VERSION == OPENGL_3_3
145 // We don't (yet/anymore) use VAO, but apparently it's required in openGL 3.3.
146 // Luckily this workaround is sufficient: create one global VAO and then don't care anymore.
147 GLuint vao;
148 glGenVertexArrays(1, &vao);
149 glBindVertexArray(vao);
150#endif
151}
152
154{
155 auto& renderSettings = display.getRenderSettings();
156 renderSettings.getVSyncSetting().detach(vSyncObserver);
157
160
161 gl::context.reset();
162 SDL_GL_DeleteContext(glContext);
163
164 // store last known position for when we recreate it
165 // the window gets recreated when changing renderers, for instance.
166 // Do not store if we're full screen, the location is the top-left
167 if (auto pos = getWindowPosition()) {
168 display.storeWindowPosition(*pos);
169 }
170
171 eventDistributor.unregisterEventListener(EventType::IMGUI_ACTIVE, *this);
174 eventDistributor.unregisterEventListener(EventType::MOUSE_MOTION, *this);
175 inputEventGenerator.getGrabInput().detach(*this);
176 renderSettings.getPointerHideDelaySetting().detach(*this);
177 renderSettings.getFullScreenSetting().detach(*this);
178}
179
180std::optional<gl::ivec2> VisibleSurface::getWindowPosition() const
181{
182 if (SDL_GetWindowFlags(window.get()) & SDL_WINDOW_FULLSCREEN) return {};
183 int x, y;
184 SDL_GetWindowPosition(window.get(), &x, &y);
185 return gl::ivec2{x, y};
186}
187
189{
190 if (SDL_GetWindowFlags(window.get()) & SDL_WINDOW_FULLSCREEN) return;
191 SDL_SetWindowPosition(window.get(), pos.x, pos.y);
192}
193
194// TODO: The video subsystem is not de-initialized on errors.
195// While it would be consistent to do so, doing it in this class is
196// not ideal since the init doesn't happen here.
197void VisibleSurface::createSurface(int width, int height, unsigned flags)
198{
199 if (getDisplay().getRenderSettings().getFullScreen()) {
200 flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
201 }
202#ifdef __APPLE__
203 // See VisibleSurface::setViewPort() for why only macos (for now).
204 flags |= SDL_WINDOW_ALLOW_HIGHDPI;
205#endif
206
207 assert(!window);
208 auto pos = display.retrieveWindowPosition();
209 window.reset(SDL_CreateWindow(
210 getDisplay().getWindowTitle().c_str(),
211 pos.x, pos.y,
212 width, height,
213 flags));
214 if (!window) {
215 std::string err = SDL_GetError();
216 throw InitException("Could not create window: ", err);
217 }
218
220
221 // prefer linear filtering (instead of nearest neighbour)
222 SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
223
224 // set icon
225 if constexpr (OPENMSX_SET_WINDOW_ICON) {
226 SDLSurfacePtr iconSurf;
227 // always use 32x32 icon on Windows, for some reason you get badly scaled icons there
228#ifndef _WIN32
229 try {
230 iconSurf = PNG::load(preferSystemFileContext().resolve("icons/openMSX-logo-256.png"), true);
231 } catch (MSXException& e) {
233 "Falling back to built in 32x32 icon, because failed to load icon: ",
234 e.getMessage());
235#endif
236 PixelOperations pixelOps;
237 iconSurf.reset(SDL_CreateRGBSurfaceFrom(
238 const_cast<char*>(openMSX_icon.pixel_data),
239 narrow<int>(openMSX_icon.width),
240 narrow<int>(openMSX_icon.height),
241 narrow<int>(openMSX_icon.bytes_per_pixel * 8),
243 pixelOps.getRmask(),
244 pixelOps.getGmask(),
245 pixelOps.getBmask(),
246 pixelOps.getAmask()));
247#ifndef _WIN32
248 }
249#endif
250 SDL_SetColorKey(iconSurf.get(), SDL_TRUE, 0);
251 SDL_SetWindowIcon(window.get(), iconSurf.get());
252 }
253}
254
255void VisibleSurface::update(const Setting& /*setting*/) noexcept
256{
257 updateCursor();
258}
259
261{
262 // timer expired, hide cursor
263 videoSystem.showCursor(false);
264 inputEventGenerator.updateGrab(grab);
265}
266
268{
269 if (getType(event) == EventType::IMGUI_ACTIVE) {
270 guiActive = get_event<ImGuiActiveEvent>(event).getActive();
271 }
272 updateCursor();
273 return 0;
274}
275
276void VisibleSurface::updateCursor()
277{
278 cancelRT();
279 auto& renderSettings = display.getRenderSettings();
280 grab = !guiActive &&
281 (renderSettings.getFullScreen() ||
282 inputEventGenerator.getGrabInput().getBoolean());
283 if (grab) {
284 // always hide cursor in fullscreen or grab-input mode, but do it
285 // after the derived class is constructed to avoid an SDL bug.
286 scheduleRT(0);
287 return;
288 }
289 inputEventGenerator.updateGrab(grab);
290 float delay = renderSettings.getPointerHideDelay();
291 if (delay == 0.0f) {
292 videoSystem.showCursor(false);
293 } else {
294 videoSystem.showCursor(true);
295 if (delay > 0.0f) {
296 scheduleRT(int(delay * 1e6f)); // delay in s, schedule in us
297 }
298 }
299}
300
301bool VisibleSurface::setFullScreen(bool fullscreen)
302{
303 auto flags = SDL_GetWindowFlags(window.get());
304 // Note: SDL_WINDOW_FULLSCREEN_DESKTOP also has the SDL_WINDOW_FULLSCREEN
305 // bit set.
306 bool currentState = (flags & SDL_WINDOW_FULLSCREEN) != 0;
307 if (currentState == fullscreen) {
308 // already wanted stated
309 return true;
310 }
311
312 if (SDL_SetWindowFullscreen(window.get(),
313 fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0) != 0) {
314 return false; // error, try re-creating the window
315 }
316 fullScreenUpdated(fullscreen);
317 return true; // success
318}
319
320gl::ivec2 VisibleSurface::getWindowSize() const
321{
322 auto& renderSettings = display.getRenderSettings();
323 int factor = renderSettings.getScaleFactor();
324 return {320 * factor, 240 * factor};
325}
326
328{
329 auto size = getWindowSize();
330 SDL_SetWindowSize(window.get(), size.x, size.y);
331
332 bool fullScreen = display.getRenderSettings().getFullScreen();
333 setViewPort(size, fullScreen);
334}
335
337{
338 assert(window);
339 SDL_SetWindowTitle(window.get(), getDisplay().getWindowTitle().c_str());
340}
341
342void VisibleSurface::saveScreenshot(const std::string& filename)
343{
344 saveScreenshotGL(*this, filename);
345}
346
348 const OutputSurface& output, const std::string& filename)
349{
350 auto [x, y] = output.getViewOffset();
351 auto [w, h] = output.getViewSize();
352
353 // OpenGL ES only supports reading RGBA (not RGB)
354 MemBuffer<uint8_t> buffer(4 * size_t(w) * size_t(h));
355 glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, buffer.data());
356
357 VLA(const void*, rowPointers, h);
358 for (auto i : xrange(size_t(h))) {
359 rowPointers[h - 1 - i] = &buffer[4 * size_t(w) * i];
360 }
361
362 PNG::saveRGBA(w, rowPointers, filename);
363}
364
366{
367 SDL_GL_SwapWindow(window.get());
368}
369
370std::unique_ptr<Layer> VisibleSurface::createSnowLayer()
371{
372 return std::make_unique<GLSnow>(getDisplay());
373}
374
375std::unique_ptr<Layer> VisibleSurface::createOSDGUILayer(OSDGUI& gui)
376{
377 return std::make_unique<OSDGUILayer>(gui);
378}
379
380std::unique_ptr<Layer> VisibleSurface::createImGUILayer(ImGuiManager& manager)
381{
382 return std::make_unique<ImGuiLayer>(manager);
383}
384
385std::unique_ptr<OutputSurface> VisibleSurface::createOffScreenSurface()
386{
387 return std::make_unique<OffScreenSurface>(*this);
388}
389
390void VisibleSurface::VSyncObserver::update(const Setting& setting) noexcept
391{
392 auto& visSurface = OUTER(VisibleSurface, vSyncObserver);
393 auto& syncSetting = visSurface.getDisplay().getRenderSettings().getVSyncSetting();
394 assert(&setting == &syncSetting); (void)setting;
395
396 // for now, we assume that adaptive vsync is the best kind of vsync, so when
397 // vsync is enabled, we attempt adaptive vsync.
398 int interval = syncSetting.getBoolean() ? -1 : 0;
399
400 if ((SDL_GL_SetSwapInterval(interval) < 0) && (interval == -1)) {
401 // "Adaptive vsync" is not supported by all drivers. SDL
402 // documentation suggests to fallback to "regular vsync" in
403 // that case.
404 SDL_GL_SetSwapInterval(1);
405 }
406}
407
408void VisibleSurface::setViewPort(gl::ivec2 logicalSize, bool fullScreen)
409{
410 gl::ivec2 physicalSize = [&] {
411#ifndef __APPLE__
412 // On macos we set 'SDL_WINDOW_ALLOW_HIGHDPI', and in that case
413 // it's required to use SDL_GL_GetDrawableSize(), but then this
414 // 'full screen'-workaround/hack is counter-productive.
415 if (!fullScreen) {
416 // ??? When switching back from full screen to windowed mode,
417 // SDL_GL_GetDrawableSize() still returns the dimensions of the
418 // full screen window ??? Is this a bug ???
419 // But we know that in windowed mode, physical and logical size
420 // must be the same, so enforce that.
421 return logicalSize;
422 }
423#endif
424 (void)fullScreen;
425 int w, h;
426 SDL_GL_GetDrawableSize(window.get(), &w, &h);
427 return gl::ivec2(w, h);
428 }();
429
430 // The created surface may be larger than requested.
431 // If that happens, center the area that we actually use.
432 calculateViewPort(logicalSize, physicalSize);
433 // actually setting the viewport is done in PostProcessor::paint()
434
435 gl::context->setupMvpMatrix(gl::vec2(logicalSize));
436}
437
439{
440 setViewPort(getLogicalSize(), fullScreen);
441}
442
443} // namespace openmsx
BaseSetting * setting
#define VERSION_STRING
Wrapper around a SDL_Surface.
void reset(SDL_Surface *surface_=nullptr)
SDL_Surface * get()
bool getBoolean() const noexcept
void printWarning(std::string_view message)
Definition CliComm.cc:10
Represents the output window/screen of openMSX.
Definition Display.hh:32
gl::ivec2 retrieveWindowPosition()
Definition Display.cc:248
void storeWindowPosition(gl::ivec2 pos)
Definition Display.cc:243
RenderSettings & getRenderSettings()
Definition Display.hh:43
void unregisterEventListener(EventType type, EventListener &listener)
Unregisters a previously registered event listener.
void registerEventListener(EventType type, EventListener &listener, Priority priority=OTHER)
Registers a given object to receive certain events.
Thrown when a subsystem initialisation fails.
BooleanSetting & getGrabInput()
Input Grab on or off.
void updateGrab(bool grab)
Must be called when 'grabinput' or 'fullscreen' setting changes.
This class manages the lifetime of a block of memory.
Definition MemBuffer.hh:29
const T * data() const
Returns pointer to the start of the memory buffer.
Definition MemBuffer.hh:81
A frame buffer where pixels can be written to.
gl::ivec2 getLogicalSize() const
gl::ivec2 getViewOffset() const
void calculateViewPort(gl::ivec2 logSize, gl::ivec2 physSize)
gl::ivec2 getViewSize() const
void scheduleRT(uint64_t delta)
BooleanSetting & getVSyncSetting()
VSync [on, off].
void detach(Observer< T > &observer)
Definition Subject.hh:56
void attach(Observer< T > &observer)
Definition Subject.hh:50
Video back-end system.
virtual void showCursor(bool show)=0
An OutputSurface which is visible to the user, such as a window or a full screen display.
void finish()
When a complete frame is finished, call this method.
VisibleSurface(Display &display, RTScheduler &rtScheduler, EventDistributor &eventDistributor, InputEventGenerator &inputEventGenerator, CliComm &cliComm, VideoSystem &videoSystem)
std::unique_ptr< Layer > createImGUILayer(ImGuiManager &manager)
Display & getDisplay() const
static void saveScreenshotGL(const OutputSurface &output, const std::string &filename)
void update(const Setting &setting) noexcept override
void setWindowPosition(gl::ivec2 pos)
void fullScreenUpdated(bool fullScreen)
std::unique_ptr< OutputSurface > createOffScreenSurface()
Create an off-screen OutputSurface which has similar properties as this VisibleSurface.
std::unique_ptr< Layer > createSnowLayer()
void saveScreenshot(const std::string &filename) override
Save the content of this OutputSurface to a PNG file.
std::optional< gl::ivec2 > getWindowPosition() const
Returns x,y coordinates of top-left window corner, or returns a nullopt when in fullscreen mode.
bool setFullScreen(bool fullscreen)
CliComm & getCliComm() const
int signalEvent(const Event &event) override
This method gets called when an event you are subscribed to occurs.
std::unique_ptr< Layer > createOSDGUILayer(OSDGUI &gui)
static void setMainWindowId(uint32_t id)
Definition Event.hh:226
bool ImGui_ImplOpenGL3_Init(const char *glsl_version)
void ImGui_ImplOpenGL3_Shutdown()
bool ImGui_ImplSDL2_InitForOpenGL(SDL_Window *window, void *sdl_gl_context)
void ImGui_ImplSDL2_Shutdown()
constexpr double e
Definition Math.hh:21
vecN< 2, int > ivec2
Definition gl_vec.hh:193
std::optional< Context > context
Definition GLContext.cc:10
void saveRGBA(size_t width, std::span< const void * > rowPointers, const std::string &filename)
Definition PNG.cc:318
SDLSurfacePtr load(const std::string &filename, bool want32bpp)
Load the given PNG file in a SDL_Surface.
Definition PNG.cc:99
This file implemented 3 utility functions:
Definition Autofire.cc:9
const OpenMSX_Icon openMSX_icon
Definition Icon.cc:17
EventType getType(const Event &event)
Definition Event.hh:526
std::variant< KeyUpEvent, KeyDownEvent, MouseMotionEvent, MouseButtonUpEvent, MouseButtonDownEvent, MouseWheelEvent, JoystickAxisMotionEvent, JoystickHatEvent, JoystickButtonUpEvent, JoystickButtonDownEvent, OsdControlReleaseEvent, OsdControlPressEvent, WindowEvent, TextEvent, FileDropEvent, QuitEvent, FinishFrameEvent, CliCommandEvent, GroupEvent, BootEvent, FrameDrawnEvent, BreakEvent, SwitchRendererEvent, TakeReverseSnapshotEvent, AfterTimedEvent, MachineLoadedEvent, MachineActivatedEvent, MachineDeactivatedEvent, MidiInReaderEvent, MidiInWindowsEvent, MidiInCoreMidiEvent, MidiInCoreMidiVirtualEvent, MidiInALSAEvent, Rs232TesterEvent, Rs232NetEvent, ImGuiDelayedActionEvent, ImGuiActiveEvent > Event
Definition Event.hh:454
const FileContext & preferSystemFileContext()
#define OUTER(type, member)
Definition outer.hh:41
const char * pixel_data
Definition Icon.hh:11
unsigned width
Definition Icon.hh:8
unsigned height
Definition Icon.hh:9
unsigned bytes_per_pixel
Definition Icon.hh:10
#define VLA(TYPE, NAME, LENGTH)
Definition vla.hh:12
constexpr auto xrange(T e)
Definition xrange.hh:132