Rework the way the ReinitSeparation command is called. The old way was way too danger...
[openttd-joker.git] / src / video / cocoa / fullscreen.mm
blob416c16497c32eb592caee0ae71cb7be81f9c6713
1 /* $Id: fullscreen.mm 26108 2013-11-25 14:30:22Z rubidium $ */
3 /*
4  * This file is part of OpenTTD.
5  * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
6  * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
7  * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
8  */
10 /******************************************************************************
11  *                             Cocoa video driver                             *
12  * Known things left to do:                                                   *
13  *  Scale&copy the old pixel buffer to the new one when switching resolution. *
14  ******************************************************************************/
16 #ifdef WITH_COCOA
18 #include "../../stdafx.h"
20 #if (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9)
22 #define Rect  OTTDRect
23 #define Point OTTDPoint
24 #import <Cocoa/Cocoa.h>
25 #undef Rect
26 #undef Point
28 #include "../../debug.h"
29 #include "../../core/geometry_type.hpp"
30 #include "../../core/sort_func.hpp"
31 #include "cocoa_v.h"
32 #include "../../gfx_func.h"
33 #include "../../os/macosx/macos.h"
35 /**
36  * Important notice regarding all modifications!!!!!!!
37  * There are certain limitations because the file is objective C++.
38  * gdb has limitations.
39  * C++ and objective C code can't be joined in all cases (classes stuff).
40  * Read http://developer.apple.com/releasenotes/Cocoa/Objective-C++.html for more information.
41  */
44 /* From Menus.h (according to Xcode Developer Documentation) */
45 extern "C" void ShowMenuBar();
46 extern "C" void HideMenuBar();
49 /* Structure for rez switch gamma fades
50  * We can hide the monitor flicker by setting the gamma tables to 0
51  */
52 #define QZ_GAMMA_TABLE_SIZE 256
54 struct OTTD_QuartzGammaTable {
55         CGGammaValue red[QZ_GAMMA_TABLE_SIZE];
56         CGGammaValue green[QZ_GAMMA_TABLE_SIZE];
57         CGGammaValue blue[QZ_GAMMA_TABLE_SIZE];
60 /* Add methods to get at private members of NSScreen.
61  * Since there is a bug in Apple's screen switching code that does not update
62  * this variable when switching to fullscreen, we'll set it manually (but only
63  * for the main screen).
64  */
65 @interface NSScreen (NSScreenAccess)
66         - (void) setFrame:(NSRect)frame;
67 @end
69 @implementation NSScreen (NSScreenAccess)
70 - (void) setFrame:(NSRect)frame
72 /* The 64 bits libraries don't seem to know about _frame, so this hack won't work. */
73 #ifndef __LP64__
74         _frame = frame;
75 #endif
77 @end
79 class FullscreenSubdriver : public CocoaSubdriver {
80         CGDirectDisplayID  display_id;         ///< 0 == main display (only support single display)
81         CFDictionaryRef    cur_mode;           ///< current mode of the display
82         CFDictionaryRef    save_mode;          ///< original mode of the display
83         CGDirectPaletteRef palette;            ///< palette of an 8-bit display
86         /* Gamma functions to try to hide the flash from a res switch
87          * Fade the display from normal to black
88          * Save gamma tables for fade back to normal
89          */
90         uint32 FadeGammaOut(OTTD_QuartzGammaTable *table)
91         {
92                 CGGammaValue redTable[QZ_GAMMA_TABLE_SIZE];
93                 CGGammaValue greenTable[QZ_GAMMA_TABLE_SIZE];
94                 CGGammaValue blueTable[QZ_GAMMA_TABLE_SIZE];
96                 unsigned int actual;
97                 if (CGGetDisplayTransferByTable(this->display_id, QZ_GAMMA_TABLE_SIZE, table->red, table->green, table->blue, &actual) != CGDisplayNoErr
98                                 || actual != QZ_GAMMA_TABLE_SIZE) {
99                         return 1;
100                 }
102                 memcpy(redTable,   table->red,   sizeof(redTable));
103                 memcpy(greenTable, table->green, sizeof(greenTable));
104                 memcpy(blueTable,  table->blue,  sizeof(greenTable));
106                 for (float percent = 1.0; percent >= 0.0; percent -= 0.01) {
107                         for (int j = 0; j < QZ_GAMMA_TABLE_SIZE; j++) {
108                                 redTable[j]   = redTable[j]   * percent;
109                                 greenTable[j] = greenTable[j] * percent;
110                                 blueTable[j]  = blueTable[j]  * percent;
111                         }
113                         if (CGSetDisplayTransferByTable(this->display_id, QZ_GAMMA_TABLE_SIZE, redTable, greenTable, blueTable) != CGDisplayNoErr) {
114                                 CGDisplayRestoreColorSyncSettings();
115                                 return 1;
116                         }
118                         CSleep(10);
119                 }
121                 return 0;
122         }
124         /* Fade the display from black to normal
125          * Restore previously saved gamma values
126          */
127         uint32 FadeGammaIn(const OTTD_QuartzGammaTable *table)
128         {
129                 CGGammaValue redTable[QZ_GAMMA_TABLE_SIZE];
130                 CGGammaValue greenTable[QZ_GAMMA_TABLE_SIZE];
131                 CGGammaValue blueTable[QZ_GAMMA_TABLE_SIZE];
133                 memset(redTable, 0, sizeof(redTable));
134                 memset(greenTable, 0, sizeof(greenTable));
135                 memset(blueTable, 0, sizeof(greenTable));
137                 for (float percent = 0.0; percent <= 1.0; percent += 0.01) {
138                         for (int j = 0; j < QZ_GAMMA_TABLE_SIZE; j++) {
139                                 redTable[j]   = table->red[j]   * percent;
140                                 greenTable[j] = table->green[j] * percent;
141                                 blueTable[j]  = table->blue[j]  * percent;
142                         }
144                         if (CGSetDisplayTransferByTable(this->display_id, QZ_GAMMA_TABLE_SIZE, redTable, greenTable, blueTable) != CGDisplayNoErr) {
145                                 CGDisplayRestoreColorSyncSettings();
146                                 return 1;
147                         }
149                         CSleep(10);
150                 }
152                 return 0;
153         }
155         /** Wait for the VBL to occur (estimated since we don't have a hardware interrupt) */
156         void WaitForVerticalBlank()
157         {
158                 /* The VBL delay is based on Ian Ollmann's RezLib <iano@cco.caltech.edu> */
160                 CFNumberRef refreshRateCFNumber = (const __CFNumber*)CFDictionaryGetValue(this->cur_mode, kCGDisplayRefreshRate);
161                 if (refreshRateCFNumber == NULL) return;
163                 double refreshRate;
164                 if (CFNumberGetValue(refreshRateCFNumber, kCFNumberDoubleType, &refreshRate) == 0) return;
166                 if (refreshRate == 0) return;
168                 double linesPerSecond = refreshRate * this->device_height;
169                 double target = this->device_height;
171                 /* Figure out the first delay so we start off about right */
172                 double position = CGDisplayBeamPosition(this->display_id);
173                 if (position > target) position = 0;
175                 double adjustment = (target - position) / linesPerSecond;
177                 CSleep((uint32)(adjustment * 1000));
178         }
181         bool SetVideoMode(int w, int h, int bpp)
182         {
183                 /* Define this variables at the top (against coding style) because
184                  * otherwise GCC 4.2 barfs at the goto's jumping over variable initialization. */
185                 NSRect screen_rect;
186                 int gamma_error;
187                 NSPoint mouseLocation;
189                 /* Destroy any previous mode */
190                 if (this->pixel_buffer != NULL) {
191                         free(this->pixel_buffer);
192                         this->pixel_buffer = NULL;
193                 }
195                 /* See if requested mode exists */
196                 boolean_t exact_match;
197                 this->cur_mode = CGDisplayBestModeForParameters(this->display_id, bpp, w, h, &exact_match);
199                 /* If the mode wasn't an exact match, check if it has the right bpp, and update width and height */
200                 if (!exact_match) {
201                         int act_bpp;
202                         CFNumberRef number = (const __CFNumber*) CFDictionaryGetValue(this->cur_mode, kCGDisplayBitsPerPixel);
203                         CFNumberGetValue(number, kCFNumberSInt32Type, &act_bpp);
204                         if (act_bpp != bpp) {
205                                 DEBUG(driver, 0, "Failed to find display resolution");
206                                 goto ERR_NO_MATCH;
207                         }
209                         number = (const __CFNumber*)CFDictionaryGetValue(this->cur_mode, kCGDisplayWidth);
210                         CFNumberGetValue(number, kCFNumberSInt32Type, &w);
212                         number = (const __CFNumber*)CFDictionaryGetValue(this->cur_mode, kCGDisplayHeight);
213                         CFNumberGetValue(number, kCFNumberSInt32Type, &h);
214                 }
216                 /* Capture the main screen */
217                 CGDisplayCapture(this->display_id);
219                 /* Store the mouse coordinates relative to the total screen */
220                 mouseLocation = [ NSEvent mouseLocation ];
221                 mouseLocation.x /= this->device_width;
222                 mouseLocation.y /= this->device_height;
224                 /* Fade display to zero gamma */
225                 OTTD_QuartzGammaTable gamma_table;
226                 gamma_error = this->FadeGammaOut(&gamma_table);
228                 /* Put up the blanking window (a window above all other windows) */
229                 if (CGDisplayCapture(this->display_id) != CGDisplayNoErr ) {
230                         DEBUG(driver, 0, "Failed capturing display");
231                         goto ERR_NO_CAPTURE;
232                 }
234                 /* Do the physical switch */
235                 if (CGDisplaySwitchToMode(this->display_id, this->cur_mode) != CGDisplayNoErr) {
236                         DEBUG(driver, 0, "Failed switching display resolution");
237                         goto ERR_NO_SWITCH;
238                 }
240                 /* Since CGDisplayBaseAddress and CGDisplayBytesPerRow are no longer available on 10.7,
241                  * disable until a replacement can be found. */
242                 if (MacOSVersionIsAtLeast(10, 7, 0)) {
243                         this->window_buffer = NULL;
244                         this->window_pitch  = 0;
245                 } else {
246 #if (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7)
247                         this->window_buffer = CGDisplayBaseAddress(this->display_id);
248                         this->window_pitch  = CGDisplayBytesPerRow(this->display_id);
249 #endif
250                 }
252                 this->device_width  = CGDisplayPixelsWide(this->display_id);
253                 this->device_height = CGDisplayPixelsHigh(this->display_id);
254                 this->device_depth  = bpp;
256                 /* Setup double-buffer emulation */
257                 this->pixel_buffer = malloc(this->device_width * this->device_height * this->device_depth / 8);
258                 if (this->pixel_buffer == NULL) {
259                         DEBUG(driver, 0, "Failed to allocate memory for double buffering");
260                         goto ERR_DOUBLEBUF;
261                 }
263                 if (this->device_depth == 8 && !CGDisplayCanSetPalette(this->display_id)) {
264                         DEBUG(driver, 0, "Not an indexed display mode.");
265                         goto ERR_NOT_INDEXED;
266                 }
268                 /* If we don't hide menu bar, it will get events and interrupt the program */
269                 HideMenuBar();
271                 /* Hide the OS cursor */
272                 CGDisplayHideCursor(this->display_id);
274                 /* Fade the display to original gamma */
275                 if (!gamma_error) FadeGammaIn(&gamma_table);
277                 /* There is a bug in Cocoa where NSScreen doesn't synchronize
278                  * with CGDirectDisplay, so the main screen's frame is wrong.
279                  * As a result, coordinate translation produces incorrect results.
280                  * We can hack around this bug by setting the screen rect ourselves.
281                  * This hack should be removed if/when the bug is fixed.
282                  */
283                 screen_rect = NSMakeRect(0, 0, this->device_width, this->device_height);
284                 [ [ NSScreen mainScreen ] setFrame:screen_rect ];
286                 this->UpdatePalette(0, 256);
288                 /* Move the mouse cursor to approx. the same location */
289                 CGPoint display_mouseLocation;
290                 display_mouseLocation.x = mouseLocation.x * this->device_width;
291                 display_mouseLocation.y = this->device_height - (mouseLocation.y * this->device_height);
293                 _cursor.in_window = true;
295                 CGDisplayMoveCursorToPoint(this->display_id, display_mouseLocation);
297                 return true;
299                 /* Since the blanking window covers *all* windows (even force quit) correct recovery is crucial */
300 ERR_NOT_INDEXED:
301                 free(this->pixel_buffer);
302                 this->pixel_buffer = NULL;
303 ERR_DOUBLEBUF:
304                 CGDisplaySwitchToMode(this->display_id, this->save_mode);
305 ERR_NO_SWITCH:
306                 CGReleaseAllDisplays();
307 ERR_NO_CAPTURE:
308                 if (!gamma_error) this->FadeGammaIn(&gamma_table);
309 ERR_NO_MATCH:
310                 this->device_width = 0;
311                 this->device_height = 0;
313                 return false;
314         }
316         void RestoreVideoMode()
317         {
318                 /* Release fullscreen resources */
319                 OTTD_QuartzGammaTable gamma_table;
320                 int gamma_error = this->FadeGammaOut(&gamma_table);
322                 /* Restore original screen resolution/bpp */
323                 CGDisplaySwitchToMode(this->display_id, this->save_mode);
325                 CGReleaseAllDisplays();
327                 /* Bring back the cursor */
328                 CGDisplayShowCursor(this->display_id);
330                 ShowMenuBar();
332                 /* Reset the main screen's rectangle
333                  * See comment in SetVideoMode for why we do this */
334                 NSRect screen_rect = NSMakeRect(0, 0, CGDisplayPixelsWide(this->display_id), CGDisplayPixelsHigh(this->display_id));
335                 [ [ NSScreen mainScreen ] setFrame:screen_rect ];
337                 /* Destroy the pixel buffer */
338                 if (this->pixel_buffer != NULL) {
339                         free(this->pixel_buffer);
340                         this->pixel_buffer = NULL;
341                 }
343                 if (!gamma_error) this->FadeGammaIn(&gamma_table);
345                 this->device_width  = CGDisplayPixelsWide(this->display_id);
346                 this->device_height = CGDisplayPixelsHigh(this->display_id);
347         }
349 public:
350         FullscreenSubdriver()
351         {
352                 /* Initialize the video settings; this data persists between mode switches */
353                 this->display_id = kCGDirectMainDisplay;
354                 this->save_mode  = CGDisplayCurrentMode(this->display_id);
356                 this->palette = CGPaletteCreateDefaultColorPalette();
358                 this->device_width  = CGDisplayPixelsWide(this->display_id);
359                 this->device_height = CGDisplayPixelsHigh(this->display_id);
360                 this->device_depth  = 0;
361                 this->pixel_buffer  = NULL;
363                 this->num_dirty_rects = MAX_DIRTY_RECTS;
364         }
366         virtual ~FullscreenSubdriver()
367         {
368                 this->RestoreVideoMode();
369         }
371         virtual void Draw(bool force_update)
372         {
373                 const uint8 *src   = (uint8 *)this->pixel_buffer;
374                 uint8 *dst         = (uint8 *)this->window_buffer;
375                 uint pitch         = this->window_pitch;
376                 uint width         = this->device_width;
377                 uint num_dirty     = this->num_dirty_rects;
378                 uint bytesperpixel = this->device_depth / 8;
380                 /* Check if we need to do anything */
381                 if (num_dirty == 0) return;
383                 if (num_dirty >= MAX_DIRTY_RECTS) {
384                         num_dirty = 1;
385                         this->dirty_rects[0].left   = 0;
386                         this->dirty_rects[0].top    = 0;
387                         this->dirty_rects[0].right  = this->device_width;
388                         this->dirty_rects[0].bottom = this->device_height;
389                 }
391                 WaitForVerticalBlank();
392                 /* Build the region of dirty rectangles */
393                 for (uint i = 0; i < num_dirty; i++) {
394                         uint y      = this->dirty_rects[i].top;
395                         uint left   = this->dirty_rects[i].left;
396                         uint length = this->dirty_rects[i].right - left;
397                         uint bottom = this->dirty_rects[i].bottom;
399                         for (; y < bottom; y++) {
400                                 memcpy(dst + y * pitch + left * bytesperpixel, src + y * width * bytesperpixel + left * bytesperpixel, length * bytesperpixel);
401                         }
402                 }
404                 this->num_dirty_rects = 0;
405         }
407         virtual void MakeDirty(int left, int top, int width, int height)
408         {
409                 if (this->num_dirty_rects < MAX_DIRTY_RECTS) {
410                         this->dirty_rects[this->num_dirty_rects].left = left;
411                         this->dirty_rects[this->num_dirty_rects].top = top;
412                         this->dirty_rects[this->num_dirty_rects].right = left + width;
413                         this->dirty_rects[this->num_dirty_rects].bottom = top + height;
414                 }
415                 this->num_dirty_rects++;
416         }
418         virtual void UpdatePalette(uint first_color, uint num_colors)
419         {
420                 if (this->device_depth != 8) return;
422                 for (uint32_t index = first_color; index < first_color + num_colors; index++) {
423                         /* Clamp colors between 0.0 and 1.0 */
424                         CGDeviceColor color;
425                         color.red   = _cur_palette.palette[index].r / 255.0;
426                         color.blue  = _cur_palette.palette[index].b / 255.0;
427                         color.green = _cur_palette.palette[index].g / 255.0;
429                         CGPaletteSetColorAtIndex(this->palette, color, index);
430                 }
432                 CGDisplaySetPalette(this->display_id, this->palette);
433         }
435         virtual uint ListModes(OTTD_Point *modes, uint max_modes)
436         {
437                 return QZ_ListModes(modes, max_modes, this->display_id, this->device_depth);
438         }
440         virtual bool ChangeResolution(int w, int h, int bpp)
441         {
442                 int old_width  = this->device_width;
443                 int old_height = this->device_height;
444                 int old_bpp    = this->device_depth;
446                 if (bpp != 8 && bpp != 32) error("Cocoa: This video driver only supports 8 and 32 bpp blitters.");
448                 if (SetVideoMode(w, h, bpp)) return true;
449                 if (old_width != 0 && old_height != 0) SetVideoMode(old_width, old_height, old_bpp);
451                 return false;
452         }
454         virtual bool IsFullscreen()
455         {
456                 return true;
457         }
459         virtual int GetWidth()
460         {
461                 return this->device_width;
462         }
464         virtual int GetHeight()
465         {
466                 return this->device_height;
467         }
469         virtual void *GetPixelBuffer()
470         {
471                 return this->pixel_buffer;
472         }
474         /*
475          * Convert local coordinate to window server (CoreGraphics) coordinate.
476          * In fullscreen mode this just means copying the coords.
477          */
478         virtual CGPoint PrivateLocalToCG(NSPoint *p)
479         {
480                 return CGPointMake(p->x, p->y);
481         }
483         virtual NSPoint GetMouseLocation(NSEvent *event)
484         {
485                 NSPoint pt = [ NSEvent mouseLocation ];
486                 pt.y = this->device_height - pt.y;
488                 return pt;
489         }
491         virtual bool MouseIsInsideView(NSPoint *pt)
492         {
493                 return pt->x >= 0 && pt->y >= 0 && pt->x < this->device_width && pt->y < this->device_height;
494         }
496         virtual bool IsActive()
497         {
498                 return true;
499         }
502 CocoaSubdriver *QZ_CreateFullscreenSubdriver(int width, int height, int bpp)
504         /* OSX 10.7 doesn't support this way of the fullscreen driver. If we end up here
505          * OpenTTD was compiled without SDK 10.7 available and - and thus we don't support
506          * fullscreen mode in OSX 10.7 or higher, as necessary elements for this way have
507          * been removed from the API.
508          */
509         if (MacOSVersionIsAtLeast(10, 7, 0)) {
510                 return NULL;
511         }
513         FullscreenSubdriver *ret = new FullscreenSubdriver();
515         if (!ret->ChangeResolution(width, height, bpp)) {
516                 delete ret;
517                 return NULL;
518         }
520         return ret;
523 #endif /* (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9) */
524 #endif /* WITH_COCOA */