1 /* $Id: fullscreen.mm 26108 2013-11-25 14:30:22Z rubidium $ */
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/>.
10 /******************************************************************************
11 * Cocoa video driver *
12 * Known things left to do: *
13 * Scale© the old pixel buffer to the new one when switching resolution. *
14 ******************************************************************************/
18 #include "../../stdafx.h"
20 #if (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9)
23 #define Point OTTDPoint
24 #import <Cocoa/Cocoa.h>
28 #include "../../debug.h"
29 #include "../../core/geometry_type.hpp"
30 #include "../../core/sort_func.hpp"
32 #include "../../gfx_func.h"
33 #include "../../os/macosx/macos.h"
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.
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
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).
65 @interface NSScreen (NSScreenAccess)
66 - (void) setFrame:(NSRect)frame;
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. */
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
90 uint32 FadeGammaOut(OTTD_QuartzGammaTable *table)
92 CGGammaValue redTable[QZ_GAMMA_TABLE_SIZE];
93 CGGammaValue greenTable[QZ_GAMMA_TABLE_SIZE];
94 CGGammaValue blueTable[QZ_GAMMA_TABLE_SIZE];
97 if (CGGetDisplayTransferByTable(this->display_id, QZ_GAMMA_TABLE_SIZE, table->red, table->green, table->blue, &actual) != CGDisplayNoErr
98 || actual != QZ_GAMMA_TABLE_SIZE) {
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;
113 if (CGSetDisplayTransferByTable(this->display_id, QZ_GAMMA_TABLE_SIZE, redTable, greenTable, blueTable) != CGDisplayNoErr) {
114 CGDisplayRestoreColorSyncSettings();
124 /* Fade the display from black to normal
125 * Restore previously saved gamma values
127 uint32 FadeGammaIn(const OTTD_QuartzGammaTable *table)
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;
144 if (CGSetDisplayTransferByTable(this->display_id, QZ_GAMMA_TABLE_SIZE, redTable, greenTable, blueTable) != CGDisplayNoErr) {
145 CGDisplayRestoreColorSyncSettings();
155 /** Wait for the VBL to occur (estimated since we don't have a hardware interrupt) */
156 void WaitForVerticalBlank()
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;
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));
181 bool SetVideoMode(int w, int h, int bpp)
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. */
187 NSPoint mouseLocation;
189 /* Destroy any previous mode */
190 if (this->pixel_buffer != NULL) {
191 free(this->pixel_buffer);
192 this->pixel_buffer = NULL;
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 */
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");
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);
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");
234 /* Do the physical switch */
235 if (CGDisplaySwitchToMode(this->display_id, this->cur_mode) != CGDisplayNoErr) {
236 DEBUG(driver, 0, "Failed switching display resolution");
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;
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);
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");
263 if (this->device_depth == 8 && !CGDisplayCanSetPalette(this->display_id)) {
264 DEBUG(driver, 0, "Not an indexed display mode.");
265 goto ERR_NOT_INDEXED;
268 /* If we don't hide menu bar, it will get events and interrupt the program */
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.
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);
299 /* Since the blanking window covers *all* windows (even force quit) correct recovery is crucial */
301 free(this->pixel_buffer);
302 this->pixel_buffer = NULL;
304 CGDisplaySwitchToMode(this->display_id, this->save_mode);
306 CGReleaseAllDisplays();
308 if (!gamma_error) this->FadeGammaIn(&gamma_table);
310 this->device_width = 0;
311 this->device_height = 0;
316 void RestoreVideoMode()
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);
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;
343 if (!gamma_error) this->FadeGammaIn(&gamma_table);
345 this->device_width = CGDisplayPixelsWide(this->display_id);
346 this->device_height = CGDisplayPixelsHigh(this->display_id);
350 FullscreenSubdriver()
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;
366 virtual ~FullscreenSubdriver()
368 this->RestoreVideoMode();
371 virtual void Draw(bool force_update)
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) {
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;
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);
404 this->num_dirty_rects = 0;
407 virtual void MakeDirty(int left, int top, int width, int height)
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;
415 this->num_dirty_rects++;
418 virtual void UpdatePalette(uint first_color, uint num_colors)
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 */
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);
432 CGDisplaySetPalette(this->display_id, this->palette);
435 virtual uint ListModes(OTTD_Point *modes, uint max_modes)
437 return QZ_ListModes(modes, max_modes, this->display_id, this->device_depth);
440 virtual bool ChangeResolution(int w, int h, int bpp)
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);
454 virtual bool IsFullscreen()
459 virtual int GetWidth()
461 return this->device_width;
464 virtual int GetHeight()
466 return this->device_height;
469 virtual void *GetPixelBuffer()
471 return this->pixel_buffer;
475 * Convert local coordinate to window server (CoreGraphics) coordinate.
476 * In fullscreen mode this just means copying the coords.
478 virtual CGPoint PrivateLocalToCG(NSPoint *p)
480 return CGPointMake(p->x, p->y);
483 virtual NSPoint GetMouseLocation(NSEvent *event)
485 NSPoint pt = [ NSEvent mouseLocation ];
486 pt.y = this->device_height - pt.y;
491 virtual bool MouseIsInsideView(NSPoint *pt)
493 return pt->x >= 0 && pt->y >= 0 && pt->x < this->device_width && pt->y < this->device_height;
496 virtual bool IsActive()
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.
509 if (MacOSVersionIsAtLeast(10, 7, 0)) {
513 FullscreenSubdriver *ret = new FullscreenSubdriver();
515 if (!ret->ChangeResolution(width, height, bpp)) {
523 #endif /* (MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9) */
524 #endif /* WITH_COCOA */