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 "outer.hh"
26#include "small_buffer.hh"
27#include "view.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 size = display.getWindowSize();
95 createSurface(size, 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(size, 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(gl::ivec2 size, 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 size.x, size.y,
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 false;
279}
280
281void VisibleSurface::updateCursor()
282{
283 cancelRT();
284 const 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
326{
327 auto size = display.getWindowSize();
328 SDL_SetWindowSize(window.get(), size.x, size.y);
329
330 bool fullScreen = display.getRenderSettings().getFullScreen();
331 setViewPort(size, fullScreen);
332}
333
335{
336 assert(window);
337 SDL_SetWindowTitle(window.get(), getDisplay().getWindowTitle().c_str());
338}
339
340void VisibleSurface::saveScreenshot(const std::string& filename)
341{
342 saveScreenshotGL(*this, filename);
343}
344
346 const OutputSurface& output, const std::string& filename)
347{
348 auto [x, y] = output.getViewOffset();
349 auto [w_, h_] = output.getViewSize();
350 auto w = w_; // pre-clang-16 workaround
351 auto h = h_;
352
353 // OpenGL ES only supports reading RGBA (not RGB)
354 MemBuffer<uint32_t> buffer(size_t(w) * size_t(h));
355 glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, buffer.data());
356
358 [&](auto i) { return &buffer[size_t(w) * (h - 1 - i)]; }));
359
360 PNG::saveRGBA(w, rowPointers, filename);
361}
362
364{
365 SDL_GL_SwapWindow(window.get());
366}
367
368std::unique_ptr<Layer> VisibleSurface::createSnowLayer()
369{
370 return std::make_unique<GLSnow>(getDisplay());
371}
372
373std::unique_ptr<Layer> VisibleSurface::createOSDGUILayer(OSDGUI& gui)
374{
375 return std::make_unique<OSDGUILayer>(gui);
376}
377
378std::unique_ptr<Layer> VisibleSurface::createImGUILayer(ImGuiManager& manager)
379{
380 return std::make_unique<ImGuiLayer>(manager);
381}
382
383std::unique_ptr<OutputSurface> VisibleSurface::createOffScreenSurface()
384{
385 return std::make_unique<OffScreenSurface>(*this);
386}
387
388void VisibleSurface::VSyncObserver::update(const Setting& setting) noexcept
389{
390 const auto& visSurface = OUTER(VisibleSurface, vSyncObserver);
391 const auto& syncSetting = visSurface.getDisplay().getRenderSettings().getVSyncSetting();
392 assert(&setting == &syncSetting); (void)setting;
393
394 // for now, we assume that adaptive vsync is the best kind of vsync, so when
395 // vsync is enabled, we attempt adaptive vsync.
396 int interval = syncSetting.getBoolean() ? -1 : 0;
397
398 if ((SDL_GL_SetSwapInterval(interval) < 0) && (interval == -1)) {
399 // "Adaptive vsync" is not supported by all drivers. SDL
400 // documentation suggests to fallback to "regular vsync" in
401 // that case.
402 SDL_GL_SetSwapInterval(1);
403 }
404}
405
406void VisibleSurface::setViewPort(gl::ivec2 logicalSize, bool fullScreen)
407{
408 gl::ivec2 physicalSize = [&] {
409#ifndef __APPLE__
410 // On macos we set 'SDL_WINDOW_ALLOW_HIGHDPI', and in that case
411 // it's required to use SDL_GL_GetDrawableSize(), but then this
412 // 'full screen'-workaround/hack is counter-productive.
413 if (!fullScreen) {
414 // ??? When switching back from full screen to windowed mode,
415 // SDL_GL_GetDrawableSize() still returns the dimensions of the
416 // full screen window ??? Is this a bug ???
417 // But we know that in windowed mode, physical and logical size
418 // must be the same, so enforce that.
419 return logicalSize;
420 }
421#endif
422 (void)fullScreen;
423 int w, h;
424 SDL_GL_GetDrawableSize(window.get(), &w, &h);
425 return gl::ivec2(w, h);
426 }();
427
428 // The created surface may be larger than requested.
429 // If that happens, center the area that we actually use.
430 calculateViewPort(logicalSize, physicalSize);
431 // actually setting the viewport is done in PostProcessor::paint()
432
433 gl::context->setupMvpMatrix(gl::vec2(logicalSize));
434}
435
437{
438 setViewPort(getLogicalSize(), fullScreen);
439}
440
441} // 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:12
Represents the output window/screen of openMSX.
Definition Display.hh:31
gl::ivec2 retrieveWindowPosition()
Definition Display.cc:241
void storeWindowPosition(gl::ivec2 pos)
Definition Display.cc:236
RenderSettings & getRenderSettings()
Definition Display.hh:46
gl::ivec2 getWindowSize() const
Definition Display.cc:246
void unregisterEventListener(EventType type, EventListener &listener)
Unregisters a previously registered event listener.
void registerEventListener(EventType type, EventListener &listener, Priority 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:32
const T * data() const
Returns pointer to the start of the memory buffer.
Definition MemBuffer.hh:79
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)
bool signalEvent(const Event &event) override
This method gets called when an event you are subscribed to occurs.
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
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:385
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:322
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:517
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:445
const FileContext & preferSystemFileContext()
constexpr auto transform(Range &&range, UnaryOp op)
Definition view.hh:520
#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
constexpr auto xrange(T e)
Definition xrange.hh:132