1 /* bzflag
2  * Copyright (c) 1993-2021 Tim Riker
3  *
4  * This package is free software;  you can redistribute it and/or
5  * modify it under the terms of the license found in the file
6  * named COPYING that should have accompanied this file.
7  *
8  * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
9  * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
10  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
11  */
12 
13 // Own include
14 #include "SDL2Window.h"
15 
16 // Common includes
17 #include "OpenGLGState.h"
18 #include "TimeKeeper.h"
19 
20 #ifdef _WIN32
21 HWND SDLWindow::hwnd = NULL;
22 #endif
23 
SDLWindow(const SDLDisplay * _display,SDLVisual *)24 SDLWindow::SDLWindow(const SDLDisplay* _display, SDLVisual*)
25     : BzfWindow(_display), hasGamma(true), origGamma(-1.0f), lastGamma(1.0f),
26       windowId(NULL), glContext(NULL), canGrabMouse(true), fullScreen(false),
27       vsync(false), base_width(640), base_height(480), min_width(-1), min_height(-1)
28 {
29 }
30 
~SDLWindow()31 SDLWindow::~SDLWindow()
32 {
33     // Restore the original gamma when we exit the client
34     setGamma(origGamma);
35 
36     if (windowId != NULL)
37         SDL_DestroyWindow(windowId);
38 }
39 
setTitle(const char * _title)40 void SDLWindow::setTitle(const char *_title)
41 {
42     title = _title;
43     if (windowId)
44         SDL_SetWindowTitle(windowId, title.c_str());
45 }
46 
setFullscreen(bool on)47 void SDLWindow::setFullscreen(bool on)
48 {
49     fullScreen = on;
50 }
51 
iconify(void)52 void SDLWindow::iconify(void)
53 {
54     SDL_MinimizeWindow(windowId);
55 }
56 
57 
disableConfineToMotionbox()58 void SDLWindow::disableConfineToMotionbox()
59 {
60 #ifndef _WIN32
61     SDL_SetWindowGrab(windowId, SDL_FALSE);
62 #else
63     ClipCursor(NULL);
64 #endif
65 }
66 
67 
confineToMotionbox(int x1,int y1,int x2,int y2)68 void SDLWindow::confineToMotionbox(int x1, int y1, int x2, int y2)
69 {
70 #ifdef __APPLE__
71     // Work around an issue in macOS that caused mouse confinement to persist
72     // after minimizing. It seems that mouse/keyboard events are sent to the
73     // game even when minimized, until another other application is clicked.
74     // Moving the mouse during the 5 second pause countdown would trigger a
75     // call to confineToMotionbox and would continue to trap the mouse.
76     if (SDL_GetWindowFlags(windowId) & SDL_WINDOW_MINIMIZED)
77         return;
78 #endif
79 
80 #ifndef _WIN32
81     if (! SDL_GetWindowGrab(windowId))
82         SDL_SetWindowGrab(windowId, SDL_TRUE);
83 
84     BzfWindow::confineToMotionbox(x1, y1, x2, y2);
85 #else
86     int posx, posy;
87     SDL_GetWindowPosition(windowId, &posx, &posy);
88 
89     // Store the boundary positions as rectangle
90     RECT rect;
91     rect.top = y1 + posy;
92     rect.left = x1 + posx;
93     rect.bottom = y2 + posy;
94     rect.right = x2 + posx;
95 
96     // Restrict cursor to that rectangle
97     ClipCursor(&rect);
98 #endif
99 }
100 
101 
warpMouse(int _x,int _y)102 void SDLWindow::warpMouse(int _x, int _y)
103 {
104     SDL_WarpMouseInWindow(windowId, _x, _y);
105 }
106 
getMouse(int & _x,int & _y) const107 void SDLWindow::getMouse(int& _x, int& _y) const
108 {
109     SDL_GetMouseState(&_x, &_y);
110 }
111 
setSize(int _width,int _height)112 void SDLWindow::setSize(int _width, int _height)
113 {
114     // workaround for two issues on Linux, where resizing by dragging the window corner causes glitching, and where
115     // iconifying or switching applications while using a scaled fullscreen resolution causes the non-fullscreen
116     // window resolution to assume the fullscreen resolution
117 #ifdef __linux__
118     if(!fullScreen)
119     {
120         base_width  = _width;
121         base_height = _height;
122     }
123 #else
124     base_width  = _width;
125     base_height = _height;
126     if (!fullScreen && windowId)
127         SDL_SetWindowSize(windowId, base_width, base_height);
128 #endif // __linux__
129 }
130 
getSize(int & width,int & height) const131 void SDLWindow::getSize(int& width, int& height) const
132 {
133     if (fullScreen)
134         const_cast<SDLDisplay *>(static_cast<const SDLDisplay *>(getDisplay()))->getWindowSize(width, height);
135     else
136     {
137         width  = base_width;
138         height = base_height;
139     }
140 }
141 
setGamma(float gamma)142 void SDLWindow::setGamma(float gamma)
143 {
144     lastGamma = gamma;
145     int result = SDL_SetWindowBrightness(windowId, gamma);
146     if (result == -1)
147     {
148         printf("Could not set Gamma: %s.\n", SDL_GetError());
149         hasGamma = false;
150     }
151 }
152 
getGamma() const153 float SDLWindow::getGamma() const
154 {
155     return SDL_GetWindowBrightness(windowId);
156 }
157 
hasGammaControl() const158 bool SDLWindow::hasGammaControl() const
159 {
160     return hasGamma;
161 }
162 
swapBuffers()163 void SDLWindow::swapBuffers()
164 {
165     SDL_GL_SwapWindow(windowId);
166 
167     // workaround for SDL 2 bug on mac where an application window obstructed
168     // by another window will not honor a vsync restriction
169     // bug report: https://bugzilla.libsdl.org/show_bug.cgi?id=2998
170     // TODO: Remove this workaround when/if SDL2 includes their own workaround.
171 #ifdef __APPLE__
172     if (! SDL_GL_GetSwapInterval())
173         return;
174 
175     int maxRunawayFPS = 65;
176     SDL_DisplayMode desktopDisplayMode;
177     if (SDL_GetDesktopDisplayMode(0, &desktopDisplayMode) == 0)
178         maxRunawayFPS = desktopDisplayMode.refresh_rate + 5;
179 
180     static TimeKeeper lastFrame = TimeKeeper::getSunGenesisTime();
181     const TimeKeeper now = TimeKeeper::getCurrent();
182 
183     const double remaining = 1.0 / (double) maxRunawayFPS - (now - lastFrame);
184 
185     // this doesn't create our exact desired FPS, since our handling is
186     // frame-to-frame and some frames will be late already and will not be
187     // delayed, but it's close enough for the purposes of this workaround
188     if (remaining > 0.0)
189         TimeKeeper::sleep(remaining);
190 
191     lastFrame = now;
192 #endif //__APPLE__
193 }
194 
195 // For some reason, when creating a new fullscreen window on Linux with a different resolution than before, SDL throws
196 // a resize event with the old window resolution, which is not what we want. This function is called to filter SDL
197 // window resize events right after the resolution change and adjust the resolution to the correct one.
198 #ifdef __linux__
SDLWindowEventFilter(void * resolution,SDL_Event * event)199 int SDLWindowEventFilter(void *resolution, SDL_Event *event)
200 {
201     if(event->type == SDL_WINDOWEVENT && (event->window.event == SDL_WINDOWEVENT_RESIZED ||
202                                           event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED))
203     {
204         // adjust the window resolution to match the values passed to us
205         event->window.data1 = static_cast<int *>(resolution)[0];
206         event->window.data2 = static_cast<int *>(resolution)[1];
207     }
208 
209     return 1; // allow the event
210 }
211 #endif // __linux__
212 
create(void)213 bool SDLWindow::create(void)
214 {
215     int targetWidth, targetHeight;
216     getSize(targetWidth, targetHeight);
217     SDL_bool windowWasGrabbed = SDL_FALSE;
218     if (windowId != NULL)
219         windowWasGrabbed = SDL_GetWindowGrab(windowId);
220 
221     // if we have an existing identical window, go no further
222     if (windowId != NULL)
223     {
224         int currentWidth, currentHeight;
225         SDL_GetWindowSize(windowId, &currentWidth, &currentHeight);
226 
227         Uint32 priorWindowFlags = SDL_GetWindowFlags(windowId);
228         if (fullScreen == (priorWindowFlags & SDL_WINDOW_FULLSCREEN) &&
229                 targetWidth == currentWidth && targetHeight == currentHeight)
230             return true;
231     }
232 
233     // destroy the pre-existing window if it exists
234     if (windowId != NULL)
235     {
236         if (glContext)
237             SDL_GL_DeleteContext(glContext);
238         glContext = NULL;
239 
240         SDL_DestroyWindow(windowId);
241     }
242 
243     // (re)create the window
244 
245     // workaround for an SDL 2 bug on Linux with the GNOME Window List extension enabled, where attempting to create a
246     // fullscreen window on a lower-resolution primary display while a higher-resolution secondary display is plugged in
247     // causes an infinite loop of window creation on the secondary display
248     // bug report: https://bugzilla.libsdl.org/show_bug.cgi?id=4990
249 #ifdef __linux__
250     if(! fullScreen || SDL_GetNumVideoDisplays() < 2) // create the window with the standard logic
251     {
252 #endif // __linux__
253         windowId = SDL_CreateWindow(
254                        title.c_str(),
255                        SDL_WINDOWPOS_UNDEFINED,
256                        SDL_WINDOWPOS_UNDEFINED,
257                        targetWidth,
258                        targetHeight,
259                        SDL_WINDOW_OPENGL |
260                        (fullScreen ? SDL_WINDOW_FULLSCREEN : SDL_WINDOW_RESIZABLE) |
261                        (windowWasGrabbed ? SDL_WINDOW_INPUT_GRABBED : 0));
262 
263         // continuation of above workaround
264 #ifdef __linux__
265     }
266     else // create the window in windowed mode first and then switch to fullscreen
267     {
268         windowId = SDL_CreateWindow(
269                        title.c_str(),
270                        SDL_WINDOWPOS_UNDEFINED,
271                        SDL_WINDOWPOS_UNDEFINED,
272                        base_width,
273                        base_height,
274                        SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | (windowWasGrabbed ? SDL_WINDOW_INPUT_GRABBED : 0));
275 
276         SDL_DisplayMode displayMode;
277         if(SDL_GetDesktopDisplayMode(0, &displayMode) < 0)
278         {
279             printf("Unable to get desktop display mode: %s", SDL_GetError());
280             return false;
281         }
282         displayMode.w = targetWidth;
283         displayMode.h = targetHeight;
284         if(SDL_SetWindowDisplayMode(windowId, &displayMode))
285         {
286             printf("Unable to set display mode: %s", SDL_GetError());
287             return false;
288         }
289         if(SDL_SetWindowFullscreen(windowId, SDL_WINDOW_FULLSCREEN) < 0)
290         {
291             printf("Unable to set window to fullscreen mode: %s", SDL_GetError());
292             return false;
293         }
294     }
295 
296     // Depending on the distribution (or possibly the window manager), SDL will not recognize the new window resolution
297     // and will keep returning the previous resolution when SDL_GetWindowSize() is called, until a period of time has
298     // passed and events have been pumped. Wait up to two seconds for the correct resolution to start being returned,
299     // checking every quarter second, to avoid repeating the window destruction/re-creation process based on bad data.
300     int currentWidth, currentHeight, resCheckLoops = 0;
301 
302     do
303     {
304         SDL_PumpEvents();
305         SDL_GetWindowSize(windowId, &currentWidth, &currentHeight);
306 
307         if(currentWidth == targetWidth && currentHeight == targetHeight)
308             break;
309 
310         TimeKeeper::sleep(0.25f);
311     }
312     while (resCheckLoops++ < 8);
313 #endif // __linux__
314 
315     // Apply filters due to resize event issues on Linux (see the explanation above for SDLWindowEventFilter())
316 #ifdef __linux__
317     SDL_PumpEvents();
318     int windowResolution[] = { targetWidth, targetHeight };
319     SDL_FilterEvents(&SDLWindowEventFilter, windowResolution);
320 #endif // __linux__
321 
322     // Work around an issue with SDL on macOS where a window that gets resized by the operating system for various
323     // reasons (e.g., creating a window that doesn't fit between the dock and menu bar, or switching from a maximized
324     // window to native fullscreen then back to windowed mode) doesn't always correctly throw a resize event
325 #ifdef __APPLE__
326     SDL_PumpEvents();
327 
328     int currentWidth, currentHeight;
329     SDL_GetWindowSize(windowId, &currentWidth, &currentHeight);
330 
331     if(! fullScreen && (currentWidth != targetWidth || currentHeight != targetHeight))
332     {
333         SDL_Event fakeResizeEvent;
334         SDL_zero(fakeResizeEvent);
335 
336         fakeResizeEvent.window.type = SDL_WINDOWEVENT;
337         fakeResizeEvent.window.windowID = 0; // deliberately not matching SDL_GetWindowID() so SDL doesn't purge event
338         fakeResizeEvent.window.event = SDL_WINDOWEVENT_RESIZED;
339         fakeResizeEvent.window.data1 = currentWidth;
340         fakeResizeEvent.window.data2 = currentHeight;
341 
342         SDL_PushEvent(&fakeResizeEvent);
343     }
344 #endif // __APPLE__
345 
346     // Store the gamma immediately after creating the first window
347     if (origGamma < 0)
348         origGamma = getGamma();
349 
350     // At least on Windows, recreating the window resets the gamma, so set it
351     setGamma(lastGamma);
352 
353 #ifdef _WIN32
354     SDL_VERSION(&info.version);
355     if (SDL_GetWindowWMInfo(windowId,&info))
356     {
357         if (info.subsystem == SDL_SYSWM_WINDOWS)
358             hwnd = info.info.win.window;
359     }
360 #endif
361 
362     if (!windowId)
363     {
364         printf("Could not set Video Mode: %s.\n", SDL_GetError());
365         return false;
366     }
367 
368     if (min_width >= 0)
369         setMinSize(min_width, min_height);
370 
371     makeContext();
372     makeCurrent();
373 
374     if(SDL_GL_SetSwapInterval(vsync ? -1 : 0) == -1 && vsync)
375         // no adaptive vsync; set regular vsync
376         SDL_GL_SetSwapInterval(1);
377 
378     // init opengl context
379     OpenGLGState::initContext();
380 
381     return true;
382 }
383 
enableGrabMouse(bool on)384 void SDLWindow::enableGrabMouse(bool on)
385 {
386     canGrabMouse = on;
387     if (canGrabMouse)
388         SDL_SetWindowGrab(windowId, SDL_TRUE);
389     else
390         SDL_SetWindowGrab(windowId, SDL_FALSE);
391 }
392 
makeContext()393 void SDLWindow::makeContext()
394 {
395     glContext = SDL_GL_CreateContext(windowId);
396     if (!glContext)
397         printf("Could not Create GL Context: %s.\n", SDL_GetError());
398 }
399 
setVerticalSync(bool setting)400 void SDLWindow::setVerticalSync(bool setting)
401 {
402     vsync = setting;
403 
404     if (windowId != NULL)
405         if (glContext != NULL)
406             if(SDL_GL_SetSwapInterval(vsync ? -1 : 0) == -1 && vsync)
407                 // no adaptive vsync; set regular vsync
408                 SDL_GL_SetSwapInterval(1);
409 }
410 
setMinSize(int width,int height)411 void SDLWindow::setMinSize(int width, int height)
412 {
413     min_width  = width;
414     min_height = height;
415     if (!windowId)
416         return;
417     SDL_SetWindowMinimumSize (windowId, width, height);
418 }
419 
makeCurrent()420 void SDLWindow::makeCurrent()
421 {
422     if (!windowId)
423         return;
424     if (!glContext)
425         return;
426     int result = SDL_GL_MakeCurrent(windowId, glContext);
427     if (result < 0)
428     {
429         printf("Could not Make GL Context Current: %s.\n", SDL_GetError());
430         abort();
431     }
432 }
433 
freeContext()434 void SDLWindow::freeContext()
435 {
436     SDL_GL_DeleteContext(glContext);
437 }
438 
439 // Local Variables: ***
440 // mode: C++ ***
441 // tab-width: 4 ***
442 // c-basic-offset: 4 ***
443 // indent-tabs-mode: nil ***
444 // End: ***
445 // ex: shiftwidth=4 tabstop=4
446