diff options
Diffstat (limited to 'src/video/cocoa/fullscreen.mm')
-rw-r--r-- | src/video/cocoa/fullscreen.mm | 617 |
1 files changed, 617 insertions, 0 deletions
diff --git a/src/video/cocoa/fullscreen.mm b/src/video/cocoa/fullscreen.mm new file mode 100644 index 000000000..c60e0c58a --- /dev/null +++ b/src/video/cocoa/fullscreen.mm @@ -0,0 +1,617 @@ +/* $Id$ */ + +/****************************************************************************** + * Cocoa video driver * + * Known things left to do: * + * Scale© the old pixel buffer to the new one when switching resolution. * + ******************************************************************************/ + +#ifdef WITH_COCOA + +#include <AvailabilityMacros.h> + +#import <Cocoa/Cocoa.h> +#import <sys/time.h> /* gettimeofday */ +#import <sys/param.h> /* for MAXPATHLEN */ +#import <unistd.h> + +/** + * Important notice regarding all modifications!!!!!!! + * There are certain limitations because the file is objective C++. + * gdb has limitations. + * C++ and objective C code can't be joined in all cases (classes stuff). + * Read http://developer.apple.com/releasenotes/Cocoa/Objective-C++.html for more information. + */ + + +/* From Menus.h (according to Xcode Developer Documentation) */ +extern "C" void ShowMenuBar(); +extern "C" void HideMenuBar(); + +/* Defined in stdbool.h */ +#ifndef __cplusplus +# ifndef __BEOS__ +# undef bool +# undef false +# undef true +# endif +#endif + + +#include "../../stdafx.h" +#include "../../openttd.h" +#include "../../debug.h" +#include "../../macros.h" +#include "../../os/macosx/splash.h" +#include "../../variables.h" +#include "../../gfx.h" +#include "cocoa_v.h" +#include "cocoa_keys.h" +#include "../../blitter/factory.hpp" +#include "../../fileio.h" + +#undef Point +#undef Rect + + +/* Structure for rez switch gamma fades + * We can hide the monitor flicker by setting the gamma tables to 0 + */ +#define QZ_GAMMA_TABLE_SIZE 256 + +struct OTTD_QuartzGammaTable { + CGGammaValue red[QZ_GAMMA_TABLE_SIZE]; + CGGammaValue green[QZ_GAMMA_TABLE_SIZE]; + CGGammaValue blue[QZ_GAMMA_TABLE_SIZE]; +}; + +/* Add methods to get at private members of NSScreen. + * Since there is a bug in Apple's screen switching code that does not update + * this variable when switching to fullscreen, we'll set it manually (but only + * for the main screen). + */ +@interface NSScreen (NSScreenAccess) + - (void) setFrame:(NSRect)frame; +@end + +@implementation NSScreen (NSScreenAccess) +- (void) setFrame:(NSRect)frame; +{ + _frame = frame; +} +@end + + +class FullscreenSubdriver: public CocoaSubdriver { + int display_width; + int display_height; + int display_depth; + int screen_pitch; + void* screen_buffer; + void* pixel_buffer; + + CGDirectDisplayID display_id; /* 0 == main display (only support single display) */ + CFDictionaryRef cur_mode; /* current mode of the display */ + CFDictionaryRef save_mode; /* original mode of the display */ + CGDirectPaletteRef palette; /* palette of an 8-bit display */ + + #define MAX_DIRTY_RECTS 100 + Rect dirty_rects[MAX_DIRTY_RECTS]; + int num_dirty_rects; + + + /* Gamma functions to try to hide the flash from a rez switch + * Fade the display from normal to black + * Save gamma tables for fade back to normal + */ + uint32 FadeGammaOut(OTTD_QuartzGammaTable* table) + { + CGGammaValue redTable[QZ_GAMMA_TABLE_SIZE]; + CGGammaValue greenTable[QZ_GAMMA_TABLE_SIZE]; + CGGammaValue blueTable[QZ_GAMMA_TABLE_SIZE]; + float percent; + int j; + unsigned int actual; + + if (CGGetDisplayTransferByTable( + display_id, QZ_GAMMA_TABLE_SIZE, + table->red, table->green, table->blue, &actual + ) != CGDisplayNoErr || + actual != QZ_GAMMA_TABLE_SIZE) { + return 1; + } + + memcpy(redTable, table->red, sizeof(redTable)); + memcpy(greenTable, table->green, sizeof(greenTable)); + memcpy(blueTable, table->blue, sizeof(greenTable)); + + for (percent = 1.0; percent >= 0.0; percent -= 0.01) { + for (j = 0; j < QZ_GAMMA_TABLE_SIZE; j++) { + redTable[j] = redTable[j] * percent; + greenTable[j] = greenTable[j] * percent; + blueTable[j] = blueTable[j] * percent; + } + + if (CGSetDisplayTransferByTable( + display_id, QZ_GAMMA_TABLE_SIZE, + redTable, greenTable, blueTable + ) != CGDisplayNoErr) { + CGDisplayRestoreColorSyncSettings(); + return 1; + } + + CSleep(10); + } + + return 0; + } + + /* Fade the display from black to normal + * Restore previously saved gamma values + */ + uint32 FadeGammaIn(const OTTD_QuartzGammaTable* table) + { + CGGammaValue redTable[QZ_GAMMA_TABLE_SIZE]; + CGGammaValue greenTable[QZ_GAMMA_TABLE_SIZE]; + CGGammaValue blueTable[QZ_GAMMA_TABLE_SIZE]; + float percent; + int j; + + memset(redTable, 0, sizeof(redTable)); + memset(greenTable, 0, sizeof(greenTable)); + memset(blueTable, 0, sizeof(greenTable)); + + for (percent = 0.0; percent <= 1.0; percent += 0.01) { + for (j = 0; j < QZ_GAMMA_TABLE_SIZE; j++) { + redTable[j] = table->red[j] * percent; + greenTable[j] = table->green[j] * percent; + blueTable[j] = table->blue[j] * percent; + } + + if (CGSetDisplayTransferByTable( + display_id, QZ_GAMMA_TABLE_SIZE, + redTable, greenTable, blueTable + ) != CGDisplayNoErr) { + CGDisplayRestoreColorSyncSettings(); + return 1; + } + + CSleep(10); + } + + return 0; + } + + /* Wait for the VBL to occur (estimated since we don't have a hardware interrupt) */ + void WaitForVerticalBlank() + { + /* The VBL delay is based on Ian Ollmann's RezLib <iano@cco.caltech.edu> */ + double refreshRate; + double linesPerSecond; + double target; + double position; + double adjustment; + CFNumberRef refreshRateCFNumber; + + refreshRateCFNumber = (const __CFNumber*)CFDictionaryGetValue(cur_mode, kCGDisplayRefreshRate); + if (refreshRateCFNumber == NULL) return; + + if (CFNumberGetValue(refreshRateCFNumber, kCFNumberDoubleType, &refreshRate) == 0) + return; + + if (refreshRate == 0) return; + + linesPerSecond = refreshRate * display_height; + target = display_height; + + /* Figure out the first delay so we start off about right */ + position = CGDisplayBeamPosition(display_id); + if (position > target) position = 0; + + adjustment = (target - position) / linesPerSecond; + + CSleep((uint32)(adjustment * 1000)); + } + + + bool SetVideoMode(int w, int h) + { + int exact_match; + CFNumberRef number; + int bpp; + int gamma_error; + OTTD_QuartzGammaTable gamma_table; + NSRect screen_rect; + CGError error; + NSPoint pt; + + /* Destroy any previous mode */ + if (pixel_buffer != NULL) { + free(pixel_buffer); + pixel_buffer = NULL; + } + + /* See if requested mode exists */ + cur_mode = CGDisplayBestModeForParameters(display_id, display_depth, w, h, &exact_match); + + /* If the mode wasn't an exact match, check if it has the right bpp, and update width and height */ + if (!exact_match) { + number = (const __CFNumber*) CFDictionaryGetValue(cur_mode, kCGDisplayBitsPerPixel); + CFNumberGetValue(number, kCFNumberSInt32Type, &bpp); + if (bpp != display_depth) { + DEBUG(driver, 0, "Failed to find display resolution"); + goto ERR_NO_MATCH; + } + + number = (const __CFNumber*)CFDictionaryGetValue(cur_mode, kCGDisplayWidth); + CFNumberGetValue(number, kCFNumberSInt32Type, &w); + + number = (const __CFNumber*)CFDictionaryGetValue(cur_mode, kCGDisplayHeight); + CFNumberGetValue(number, kCFNumberSInt32Type, &h); + } + + /* Fade display to zero gamma */ + gamma_error = FadeGammaOut(&gamma_table); + + /* Put up the blanking window (a window above all other windows) */ + error = CGDisplayCapture(display_id); + + if (CGDisplayNoErr != error) { + DEBUG(driver, 0, "Failed capturing display"); + goto ERR_NO_CAPTURE; + } + + /* Do the physical switch */ + if (CGDisplaySwitchToMode(display_id, cur_mode) != CGDisplayNoErr) { + DEBUG(driver, 0, "Failed switching display resolution"); + goto ERR_NO_SWITCH; + } + + screen_buffer = CGDisplayBaseAddress(display_id); + screen_pitch = CGDisplayBytesPerRow(display_id); + + display_width = CGDisplayPixelsWide(display_id); + display_height = CGDisplayPixelsHigh(display_id); + + /* Setup double-buffer emulation */ + pixel_buffer = malloc(display_width * display_height * display_depth / 8); + if (pixel_buffer == NULL) { + DEBUG(driver, 0, "Failed to allocate memory for double buffering"); + goto ERR_DOUBLEBUF; + } + + if (display_depth == 8 && !CGDisplayCanSetPalette(display_id)) { + DEBUG(driver, 0, "Not an indexed display mode."); + goto ERR_NOT_INDEXED; + } + + /* If we don't hide menu bar, it will get events and interrupt the program */ + HideMenuBar(); + + /* Fade the display to original gamma */ + if (!gamma_error) FadeGammaIn(&gamma_table); + + /* There is a bug in Cocoa where NSScreen doesn't synchronize + * with CGDirectDisplay, so the main screen's frame is wrong. + * As a result, coordinate translation produces incorrect results. + * We can hack around this bug by setting the screen rect ourselves. + * This hack should be removed if/when the bug is fixed. + */ + screen_rect = NSMakeRect(0, 0, display_width, display_height); + [ [ NSScreen mainScreen ] setFrame:screen_rect ]; + + + pt = [ NSEvent mouseLocation ]; + pt.y = display_height - pt.y; + if (MouseIsInsideView(&pt)) QZ_HideMouse(); + + UpdatePalette(0, 256); + + return true; + + /* Since the blanking window covers *all* windows (even force quit) correct recovery is crucial */ +ERR_NOT_INDEXED: + free(pixel_buffer); + pixel_buffer = NULL; +ERR_DOUBLEBUF: + CGDisplaySwitchToMode(display_id, save_mode); +ERR_NO_SWITCH: + CGReleaseAllDisplays(); +ERR_NO_CAPTURE: + if (!gamma_error) FadeGammaIn(&gamma_table); +ERR_NO_MATCH: + display_width = 0; + display_height = 0; + + return false; + } + + void RestoreVideoMode() + { + /* Release fullscreen resources */ + OTTD_QuartzGammaTable gamma_table; + int gamma_error; + NSRect screen_rect; + + gamma_error = FadeGammaOut(&gamma_table); + + /* Restore original screen resolution/bpp */ + CGDisplaySwitchToMode(display_id, save_mode); + CGReleaseAllDisplays(); + ShowMenuBar(); + /* Reset the main screen's rectangle + * See comment in SetVideoMode for why we do this + */ + screen_rect = NSMakeRect(0, 0, CGDisplayPixelsWide(display_id), CGDisplayPixelsHigh(display_id)); + [ [ NSScreen mainScreen ] setFrame:screen_rect ]; + + QZ_ShowMouse(); + + /* Destroy the pixel buffer */ + if (pixel_buffer != NULL) { + free(pixel_buffer); + pixel_buffer = NULL; + } + + if (!gamma_error) FadeGammaIn(&gamma_table); + + display_width = 0; + display_height = 0; + } + +public: + FullscreenSubdriver(int bpp) + { + if (bpp != 8 && bpp != 32) { + error("Cocoa: This video driver only supports 8 and 32 bpp blitters."); + } + + /* Initialize the video settings; this data persists between mode switches */ + display_id = kCGDirectMainDisplay; + save_mode = CGDisplayCurrentMode(display_id); + + if (bpp == 8) palette = CGPaletteCreateDefaultColorPalette(); + + display_width = 0; + display_height = 0; + display_depth = bpp; + pixel_buffer = NULL; + + num_dirty_rects = MAX_DIRTY_RECTS; + } + + virtual ~FullscreenSubdriver() + { + RestoreVideoMode(); + } + + virtual void Draw() + { + const uint8* src = (uint8*) pixel_buffer; + uint8* dst = (uint8*) screen_buffer; + uint pitch = screen_pitch; + uint width = display_width; + uint num_dirty = num_dirty_rects; + uint bytesperpixel = display_depth / 8; + uint i; + + /* Check if we need to do anything */ + if (num_dirty == 0) return; + + if (num_dirty >= MAX_DIRTY_RECTS) { + num_dirty = 1; + dirty_rects[0].left = 0; + dirty_rects[0].top = 0; + dirty_rects[0].right = display_width; + dirty_rects[0].bottom = display_height; + } + + WaitForVerticalBlank(); + /* Build the region of dirty rectangles */ + for (i = 0; i < num_dirty; i++) { + uint y = dirty_rects[i].top; + uint left = dirty_rects[i].left; + uint length = dirty_rects[i].right - left; + uint bottom = dirty_rects[i].bottom; + + for (; y < bottom; y++) { + memcpy(dst + y * pitch + left * bytesperpixel, src + y * width * bytesperpixel + left * bytesperpixel, length * bytesperpixel); + } + } + + num_dirty_rects = 0; + } + + virtual void MakeDirty(int left, int top, int width, int height) + { + if (num_dirty_rects < MAX_DIRTY_RECTS) { + dirty_rects[num_dirty_rects].left = left; + dirty_rects[num_dirty_rects].top = top; + dirty_rects[num_dirty_rects].right = left + width; + dirty_rects[num_dirty_rects].bottom = top + height; + } + num_dirty_rects++; + } + + virtual void UpdatePalette(uint first_color, uint num_colors) + { + CGTableCount index; + CGDeviceColor color; + + if (display_depth != 8) + return; + + for (index = first_color; index < first_color+num_colors; index++) { + /* Clamp colors between 0.0 and 1.0 */ + color.red = _cur_palette[index].r / 255.0; + color.blue = _cur_palette[index].b / 255.0; + color.green = _cur_palette[index].g / 255.0; + + CGPaletteSetColorAtIndex(palette, color, index); + } + + CGDisplaySetPalette(display_id, palette); + } + + virtual uint ListModes(OTTDPoint* modes, uint max_modes) + { + CFArrayRef mode_list; + CFIndex num_modes; + CFIndex i; + uint count = 0; + + mode_list = CGDisplayAvailableModes(display_id); + num_modes = CFArrayGetCount(mode_list); + + /* Build list of modes with the requested bpp */ + for (i = 0; i < num_modes && count < max_modes; i++) { + CFDictionaryRef onemode; + CFNumberRef number; + int bpp; + int intvalue; + bool hasMode; + uint16 width, height; + + onemode = (const __CFDictionary*)CFArrayGetValueAtIndex(mode_list, i); + number = (const __CFNumber*)CFDictionaryGetValue(onemode, kCGDisplayBitsPerPixel); + CFNumberGetValue (number, kCFNumberSInt32Type, &bpp); + + if (bpp != display_depth) continue; + + number = (const __CFNumber*)CFDictionaryGetValue(onemode, kCGDisplayWidth); + CFNumberGetValue(number, kCFNumberSInt32Type, &intvalue); + width = (uint16)intvalue; + + number = (const __CFNumber*)CFDictionaryGetValue(onemode, kCGDisplayHeight); + CFNumberGetValue(number, kCFNumberSInt32Type, &intvalue); + height = (uint16)intvalue; + + /* Check if mode is already in the list */ + { + uint i; + hasMode = false; + for (i = 0; i < count; i++) { + if (modes[i].x == width && modes[i].y == height) { + hasMode = true; + break; + } + } + } + + if (hasMode) continue; + + /* Add mode to the list */ + modes[count].x = width; + modes[count].y = height; + count++; + } + + /* Sort list smallest to largest */ + { + uint i, j; + for (i = 0; i < count; i++) { + for (j = 0; j < count-1; j++) { + if (modes[j].x > modes[j + 1].x || ( + modes[j].x == modes[j + 1].x && + modes[j].y > modes[j + 1].y + )) { + uint tmpw = modes[j].x; + uint tmph = modes[j].y; + + modes[j].x = modes[j + 1].x; + modes[j].y = modes[j + 1].y; + + modes[j + 1].x = tmpw; + modes[j + 1].y = tmph; + } + } + } + } + + return count; + } + + virtual bool ChangeResolution(int w, int h) + { + int old_width = display_width; + int old_height = display_height; + + if (SetVideoMode(w, h)) + return true; + + if (old_width != 0 && old_height != 0) + SetVideoMode(old_width, old_height); + + return false; + } + + virtual bool IsFullscreen() + { + return true; + } + + virtual int GetWidth() + { + return display_width; + } + + virtual int GetHeight() + { + return display_height; + } + + virtual void *GetPixelBuffer() + { + return pixel_buffer; + } + + /* + Convert local coordinate to window server (CoreGraphics) coordinate. + In fullscreen mode this just means copying the coords. + */ + virtual CGPoint PrivateLocalToCG(NSPoint* p) + { + CGPoint cgp; + + cgp.x = p->x; + cgp.y = p->y; + + return cgp; + } + + virtual NSPoint GetMouseLocation(NSEvent *event) + { + NSPoint pt; + + pt = [ NSEvent mouseLocation ]; + pt.y = display_height - pt.y; + + return pt; + } + + virtual bool MouseIsInsideView(NSPoint *pt) + { + return pt->x >= 0 && pt->y >= 0 && pt->x < display_width && pt->y < display_height; + } + + virtual bool IsActive() + { + return true; + } +}; + +CocoaSubdriver *QZ_CreateFullscreenSubdriver(int width, int height, int bpp) +{ + FullscreenSubdriver *ret; + + ret = new FullscreenSubdriver(bpp); + + if (!ret->ChangeResolution(width, height)) { + delete ret; + return NULL; + } + + return ret; +} + +#endif /* WITH_COCOA */ |