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