Codechange: be consistent in naming the paint function Paint()
[openttd-github.git] / src / video / cocoa / cocoa_v.mm
blob1847d53fc8b595e432752d545e5a55f95a5f2648
1 /*
2  * This file is part of OpenTTD.
3  * 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.
4  * 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.
5  * 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/>.
6  */
8 /** @file cocoa_v.mm Code related to the cocoa video driver(s). */
10 /******************************************************************************
11  *                             Cocoa video driver                             *
12  * Known things left to do:                                                   *
13  *  Nothing at the moment.                                                    *
14  ******************************************************************************/
16 #ifdef WITH_COCOA
18 #include "../../stdafx.h"
19 #include "../../os/macosx/macos.h"
21 #define Rect  OTTDRect
22 #define Point OTTDPoint
23 #import <Cocoa/Cocoa.h>
24 #undef Rect
25 #undef Point
27 #include "../../openttd.h"
28 #include "../../debug.h"
29 #include "../../core/geometry_type.hpp"
30 #include "../../core/math_func.hpp"
31 #include "cocoa_v.h"
32 #include "cocoa_wnd.h"
33 #include "../../blitter/factory.hpp"
34 #include "../../framerate_type.h"
35 #include "../../network/network.h"
36 #include "../../gfx_func.h"
37 #include "../../thread.h"
38 #include "../../core/random_func.hpp"
39 #include "../../progress.h"
40 #include "../../settings_type.h"
41 #include "../../window_func.h"
42 #include "../../window_gui.h"
44 #import <sys/param.h> /* for MAXPATHLEN */
45 #import <sys/time.h> /* gettimeofday */
47 /**
48  * Important notice regarding all modifications!!!!!!!
49  * There are certain limitations because the file is objective C++.
50  * gdb has limitations.
51  * C++ and objective C code can't be joined in all cases (classes stuff).
52  * Read http://developer.apple.com/releasenotes/Cocoa/Objective-C++.html for more information.
53  */
55 /* On some old versions of MAC OS this may not be defined.
56  * Those versions generally only produce code for PPC. So it should be safe to
57  * set this to 0. */
58 #ifndef kCGBitmapByteOrder32Host
59 #define kCGBitmapByteOrder32Host 0
60 #endif
62 bool _cocoa_video_started = false;
64 extern bool _tab_is_down;
66 #ifdef _DEBUG
67 static uint32 _tEvent;
68 #endif
71 /** List of common display/window sizes. */
72 static const Dimension _default_resolutions[] = {
73         {  640,  480 },
74         {  800,  600 },
75         { 1024,  768 },
76         { 1152,  864 },
77         { 1280,  800 },
78         { 1280,  960 },
79         { 1280, 1024 },
80         { 1400, 1050 },
81         { 1600, 1200 },
82         { 1680, 1050 },
83         { 1920, 1200 },
84         { 2560, 1440 }
87 static FVideoDriver_Cocoa iFVideoDriver_Cocoa;
90 /**
91  * Get current realtime.
92  * @return Tick time in milliseconds.
93  */
94 static uint32 GetTick()
96         struct timeval tim;
98         gettimeofday(&tim, NULL);
99         return tim.tv_usec / 1000 + tim.tv_sec * 1000;
103 /** Subclass of NSView for drawing to screen. */
104 @interface OTTD_QuartzView : NSView {
105         VideoDriver_Cocoa *driver;
107 - (instancetype)initWithFrame:(NSRect)frameRect andDriver:(VideoDriver_Cocoa *)drv;
108 @end
111 VideoDriver_Cocoa::VideoDriver_Cocoa()
113         this->window_width  = 0;
114         this->window_height = 0;
115         this->window_pitch  = 0;
116         this->buffer_depth  = 0;
117         this->window_buffer = nullptr;
118         this->pixel_buffer  = nullptr;
119         this->setup         = false;
121         this->window    = nil;
122         this->cocoaview = nil;
123         this->delegate  = nil;
125         this->color_space = nullptr;
126         this->cgcontext   = nullptr;
128         this->num_dirty_rects = lengthof(this->dirty_rects);
131 /** Stop Cocoa video driver. */
132 void VideoDriver_Cocoa::Stop()
134         if (!_cocoa_video_started) return;
136         CocoaExitApplication();
138         /* Release window mode resources */
139         if (this->window != nil) [ this->window close ];
140         [ this->cocoaview release ];
141         [ this->delegate release ];
143         CGContextRelease(this->cgcontext);
144         CGColorSpaceRelease(this->color_space);
146         free(this->window_buffer);
147         free(this->pixel_buffer);
149         _cocoa_video_started = false;
152 /** Try to start Cocoa video driver. */
153 const char *VideoDriver_Cocoa::Start(const StringList &parm)
155         if (!MacOSVersionIsAtLeast(10, 7, 0)) return "The Cocoa video driver requires Mac OS X 10.7 or later.";
157         if (_cocoa_video_started) return "Already started";
158         _cocoa_video_started = true;
160         /* Don't create a window or enter fullscreen if we're just going to show a dialog. */
161         if (!CocoaSetupApplication()) return nullptr;
163         this->UpdateAutoResolution();
164         this->orig_res = _cur_resolution;
166         int bpp = BlitterFactory::GetCurrentBlitter()->GetScreenDepth();
167         if (bpp != 8 && bpp != 32) {
168                 Stop();
169                 return "The cocoa quartz subdriver only supports 8 and 32 bpp.";
170         }
172         bool fullscreen = _fullscreen;
173         if (!this->MakeWindow(_cur_resolution.width, _cur_resolution.height)) {
174                 Stop();
175                 return "Could not create window";
176         }
178         if (fullscreen) this->ToggleFullscreen(fullscreen);
180         this->GameSizeChanged();
181         this->UpdateVideoModes();
183         return nullptr;
187  * Set dirty a rectangle managed by a cocoa video subdriver.
188  * @param left Left x cooordinate of the dirty rectangle.
189  * @param top Uppder y coordinate of the dirty rectangle.
190  * @param width Width of the dirty rectangle.
191  * @param height Height of the dirty rectangle.
192  */
193 void VideoDriver_Cocoa::MakeDirty(int left, int top, int width, int height)
195         if (this->num_dirty_rects < lengthof(this->dirty_rects)) {
196                 dirty_rects[this->num_dirty_rects].left = left;
197                 dirty_rects[this->num_dirty_rects].top = top;
198                 dirty_rects[this->num_dirty_rects].right = left + width;
199                 dirty_rects[this->num_dirty_rects].bottom = top + height;
200         }
201         this->num_dirty_rects++;
205  * Start the main programme loop when using a cocoa video driver.
206  */
207 void VideoDriver_Cocoa::MainLoop()
209         /* Restart game loop if it was already running (e.g. after bootstrapping),
210          * otherwise this call is a no-op. */
211         [ [ NSNotificationCenter defaultCenter ] postNotificationName:OTTDMainLaunchGameEngine object:nil ];
213         /* Start the main event loop. */
214         [ NSApp run ];
218  * Change the resolution when using a cocoa video driver.
219  * @param w New window width.
220  * @param h New window height.
221  * @return Whether the video driver was successfully updated.
222  */
223 bool VideoDriver_Cocoa::ChangeResolution(int w, int h)
225         NSSize screen_size = [ [ NSScreen mainScreen ] frame ].size;
226         w = std::min(w, (int)screen_size.width);
227         h = std::min(h, (int)screen_size.height);
229         NSRect contentRect = NSMakeRect(0, 0, w, h);
230         [ this->window setContentSize:contentRect.size ];
232         /* Ensure frame height - title bar height >= view height */
233         float content_height = [ this->window contentRectForFrameRect:[ this->window frame ] ].size.height;
234         contentRect.size.height = Clamp(h, 0, (int)content_height);
236         if (this->cocoaview != nil) {
237                 h = (int)contentRect.size.height;
238                 [ this->cocoaview setFrameSize:contentRect.size ];
239         }
241         this->window_width = w;
242         this->window_height = h;
244         [ (OTTD_CocoaWindow *)this->window center ];
245         this->AllocateBackingStore();
247         return true;
251  * Toggle between windowed and full screen mode for cocoa display driver.
252  * @param full_screen Whether to switch to full screen or not.
253  * @return Whether the mode switch was successful.
254  */
255 bool VideoDriver_Cocoa::ToggleFullscreen(bool full_screen)
257         if (this->IsFullscreen() == full_screen) return true;
259         if ([ this->window respondsToSelector:@selector(toggleFullScreen:) ]) {
260                 [ this->window performSelector:@selector(toggleFullScreen:) withObject:this->window ];
261                 this->UpdateVideoModes();
262                 return true;
263         }
265         return false;
269  * Callback invoked after the blitter was changed.
270  * @return True if no error.
271  */
272 bool VideoDriver_Cocoa::AfterBlitterChange()
274         this->ChangeResolution(_cur_resolution.width, _cur_resolution.height);
275         this->UpdatePalette(0, 256);
276         return true;
280  * An edit box lost the input focus. Abort character compositing if necessary.
281  */
282 void VideoDriver_Cocoa::EditBoxLostFocus()
284         [ [ this->cocoaview inputContext ] discardMarkedText ];
285         /* Clear any marked string from the current edit box. */
286         HandleTextInput(NULL, true);
290  * Get the resolution of the main screen.
291  */
292 Dimension VideoDriver_Cocoa::GetScreenSize() const
294         NSRect frame = [ [ NSScreen mainScreen ] frame ];
295         return { static_cast<uint>(NSWidth(frame)), static_cast<uint>(NSHeight(frame)) };
298 /** Get DPI scale of our window. */
299 float VideoDriver_Cocoa::GetDPIScale()
301         return this->cocoaview != nil ? [ this->cocoaview getContentsScale ] : 1.0f;
305  * Are we in fullscreen mode?
306  * @return whether fullscreen mode is currently used
307  */
308 bool VideoDriver_Cocoa::IsFullscreen()
310         return this->window != nil && ([ this->window styleMask ] & NSWindowStyleMaskFullScreen) != 0;
314  * Handle a change of the display area.
315  */
316 void VideoDriver_Cocoa::GameSizeChanged()
318         /* Tell the game that the resolution has changed */
319         _screen.width   = this->window_width;
320         _screen.height  = this->window_height;
321         _screen.pitch   = this->buffer_depth == 8 ? this->window_width : this->window_pitch;
322         _screen.dst_ptr = this->buffer_depth == 8 ? this->pixel_buffer : this->window_buffer;
324         /* Store old window size if we entered fullscreen mode. */
325         bool fullscreen = this->IsFullscreen();
326         if (fullscreen && !_fullscreen) this->orig_res = _cur_resolution;
327         _fullscreen = fullscreen;
329         BlitterFactory::GetCurrentBlitter()->PostResize();
331         ::GameSizeChanged();
333         /* We need to store the window size as non-Retina size in
334         * the config file to get same windows size on next start. */
335         _cur_resolution.width = [ this->cocoaview frame ].size.width;
336         _cur_resolution.height = [ this->cocoaview frame ].size.height;
340  * Update the video mode.
341  */
342 void VideoDriver_Cocoa::UpdateVideoModes()
344         _resolutions.clear();
346         if (this->IsFullscreen()) {
347                 /* Full screen, there is only one possible resolution. */
348                 NSSize screen = [ [ this->window screen ] frame ].size;
349                 _resolutions.emplace_back((uint)screen.width, (uint)screen.height);
350         } else {
351                 /* Windowed; offer a selection of common window sizes up until the
352                  * maximum usable screen space. This excludes the menu and dock areas. */
353                 NSSize maxSize = [ [ NSScreen mainScreen] visibleFrame ].size;
354                 for (const auto &d : _default_resolutions) {
355                         if (d.width < maxSize.width && d.height < maxSize.height) _resolutions.push_back(d);
356                 }
357                 _resolutions.emplace_back((uint)maxSize.width, (uint)maxSize.height);
358         }
362  * Build window and view with a given size.
363  * @param width Window width.
364  * @param height Window height.
365  */
366 bool VideoDriver_Cocoa::MakeWindow(int width, int height)
368         this->setup = true;
370         /* Limit window size to screen frame. */
371         NSSize screen_size = [ [ NSScreen mainScreen ] frame ].size;
372         if (width > screen_size.width) width = screen_size.width;
373         if (height > screen_size.height) height = screen_size.height;
375         NSRect contentRect = NSMakeRect(0, 0, width, height);
377         /* Create main window. */
378         unsigned int style = NSTitledWindowMask | NSResizableWindowMask | NSMiniaturizableWindowMask | NSClosableWindowMask;
379         this->window = [ [ OTTD_CocoaWindow alloc ] initWithContentRect:contentRect styleMask:style backing:NSBackingStoreBuffered defer:NO driver:this ];
380         if (this->window == nil) {
381                 DEBUG(driver, 0, "Could not create the Cocoa window.");
382                 this->setup = false;
383                 return false;
384         }
386         /* Add built in full-screen support when available (OS X 10.7 and higher)
387          * This code actually compiles for 10.5 and later, but only makes sense in conjunction
388          * with the quartz fullscreen support as found only in 10.7 and later. */
389         if ([ this->window respondsToSelector:@selector(toggleFullScreen:) ]) {
390                 NSWindowCollectionBehavior behavior = [ this->window collectionBehavior ];
391                 behavior |= NSWindowCollectionBehaviorFullScreenPrimary;
392                 [ this->window setCollectionBehavior:behavior ];
394                 NSButton* fullscreenButton = [ this->window standardWindowButton:NSWindowFullScreenButton ];
395                 [ fullscreenButton setAction:@selector(toggleFullScreen:) ];
396                 [ fullscreenButton setTarget:this->window ];
397         }
399         this->delegate = [ [ OTTD_CocoaWindowDelegate alloc ] initWithDriver:this ];
400         [ this->window setDelegate:this->delegate ];
402         [ this->window center ];
403         [ this->window makeKeyAndOrderFront:nil ];
405         /* Create wrapper view for input and event handling. */
406         NSRect view_frame = [ this->window contentRectForFrameRect:[ this->window frame ] ];
407         this->cocoaview = [ [ OTTD_CocoaView alloc ] initWithFrame:view_frame ];
408         if (this->cocoaview == nil) {
409                 DEBUG(driver, 0, "Could not create the event wrapper view.");
410                 this->setup = false;
411                 return false;
412         }
413         [ this->cocoaview setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable ];
415         /* Create content view. */
416         NSView *draw_view = [ [ OTTD_QuartzView alloc ] initWithFrame:[ this->cocoaview bounds ] andDriver:this ];
417         if (draw_view == nil) {
418                 DEBUG(driver, 0, "Could not create the drawing view.");
419                 this->setup = false;
420                 return false;
421         }
422         [ draw_view setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable ];
424         /* Create view chain: window -> input wrapper view -> content view. */
425         [ this->window setContentView:this->cocoaview ];
426         [ this->cocoaview addSubview:draw_view ];
427         [ this->window makeFirstResponder:this->cocoaview ];
428         [ draw_view release ];
430         [ this->window setColorSpace:[ NSColorSpace sRGBColorSpace ] ];
431         CGColorSpaceRelease(this->color_space);
432         this->color_space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
433         if (this->color_space == nullptr) this->color_space = CGColorSpaceCreateDeviceRGB();
434         if (this->color_space == nullptr) error("Could not get a valid colour space for drawing.");
436         this->setup = false;
438         this->UpdatePalette(0, 256);
439         this->AllocateBackingStore();
441         return true;
445  * This function copies 8bpp pixels to the screen buffer in 32bpp windowed mode.
447  * @param left The x coord for the left edge of the box to blit.
448  * @param top The y coord for the top edge of the box to blit.
449  * @param right The x coord for the right edge of the box to blit.
450  * @param bottom The y coord for the bottom edge of the box to blit.
451  */
452 void VideoDriver_Cocoa::BlitIndexedToView32(int left, int top, int right, int bottom)
454         const uint32 *pal   = this->palette;
455         const uint8  *src   = (uint8*)this->pixel_buffer;
456         uint32       *dst   = (uint32*)this->window_buffer;
457         uint          width = this->window_width;
458         uint          pitch = this->window_pitch;
460         for (int y = top; y < bottom; y++) {
461                 for (int x = left; x < right; x++) {
462                         dst[y * pitch + x] = pal[src[y * width + x]];
463                 }
464         }
468  * Paint window.
469  * @param force_update Whether to redraw unconditionally
470  */
471 void VideoDriver_Cocoa::Paint()
473         PerformanceMeasurer framerate(PFE_VIDEO);
475         /* Check if we need to do anything */
476         if (this->num_dirty_rects == 0 || [ this->window isMiniaturized ]) return;
478         if (this->num_dirty_rects >= lengthof(this->dirty_rects)) {
479                 this->num_dirty_rects = 1;
480                 this->dirty_rects[0].left = 0;
481                 this->dirty_rects[0].top = 0;
482                 this->dirty_rects[0].right = this->window_width;
483                 this->dirty_rects[0].bottom = this->window_height;
484         }
486         /* Build the region of dirty rectangles */
487         for (uint i = 0; i < this->num_dirty_rects; i++) {
488                 /* We only need to blit in indexed mode since in 32bpp mode the game draws directly to the image. */
489                 if (this->buffer_depth == 8) {
490                         BlitIndexedToView32(
491                                 this->dirty_rects[i].left,
492                                 this->dirty_rects[i].top,
493                                 this->dirty_rects[i].right,
494                                 this->dirty_rects[i].bottom
495                         );
496                 }
498                 NSRect dirtyrect;
499                 dirtyrect.origin.x = this->dirty_rects[i].left;
500                 dirtyrect.origin.y = this->window_height - this->dirty_rects[i].bottom;
501                 dirtyrect.size.width = this->dirty_rects[i].right - this->dirty_rects[i].left;
502                 dirtyrect.size.height = this->dirty_rects[i].bottom - this->dirty_rects[i].top;
504                 /* Normally drawRect will be automatically called by Mac OS X during next update cycle,
505                  * and then blitting will occur. */
506                 [ this->cocoaview setNeedsDisplayInRect:[ this->cocoaview getVirtualRect:dirtyrect ] ];
507         }
509         this->num_dirty_rects = 0;
512 /** Update the palette. */
513 void VideoDriver_Cocoa::UpdatePalette(uint first_color, uint num_colors)
515         if (this->buffer_depth != 8) return;
517         for (uint i = first_color; i < first_color + num_colors; i++) {
518                 uint32 clr = 0xff000000;
519                 clr |= (uint32)_cur_palette.palette[i].r << 16;
520                 clr |= (uint32)_cur_palette.palette[i].g << 8;
521                 clr |= (uint32)_cur_palette.palette[i].b;
522                 this->palette[i] = clr;
523         }
525         this->num_dirty_rects = lengthof(this->dirty_rects);
528 /** Clear buffer to opaque black. */
529 static void ClearWindowBuffer(uint32 *buffer, uint32 pitch, uint32 height)
531         uint32 fill = Colour(0, 0, 0).data;
532         for (uint32 y = 0; y < height; y++) {
533                 for (uint32 x = 0; x < pitch; x++) {
534                         buffer[y * pitch + x] = fill;
535                 }
536         }
539 /** Resize the window. */
540 void VideoDriver_Cocoa::AllocateBackingStore()
542         if (this->window == nil || this->cocoaview == nil || this->setup) return;
544         NSRect newframe = [ this->cocoaview getRealRect:[ this->cocoaview frame ] ];
546         this->window_width = (int)newframe.size.width;
547         this->window_height = (int)newframe.size.height;
548         this->window_pitch = Align(this->window_width, 16 / sizeof(uint32)); // Quartz likes lines that are multiple of 16-byte.
549         this->buffer_depth = BlitterFactory::GetCurrentBlitter()->GetScreenDepth();
551         /* Create Core Graphics Context */
552         free(this->window_buffer);
553         this->window_buffer = malloc(this->window_pitch * this->window_height * sizeof(uint32));
554         /* Initialize with opaque black. */
555         ClearWindowBuffer((uint32 *)this->window_buffer, this->window_pitch, this->window_height);
557         CGContextRelease(this->cgcontext);
558         this->cgcontext = CGBitmapContextCreate(
559                 this->window_buffer,       // data
560                 this->window_width,        // width
561                 this->window_height,       // height
562                 8,                         // bits per component
563                 this->window_pitch * 4,    // bytes per row
564                 this->color_space,         // color space
565                 kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host
566         );
568         assert(this->cgcontext != NULL);
569         CGContextSetShouldAntialias(this->cgcontext, FALSE);
570         CGContextSetAllowsAntialiasing(this->cgcontext, FALSE);
571         CGContextSetInterpolationQuality(this->cgcontext, kCGInterpolationNone);
573         if (this->buffer_depth == 8) {
574                 free(this->pixel_buffer);
575                 this->pixel_buffer = malloc(this->window_width * this->window_height);
576                 if (this->pixel_buffer == nullptr) usererror("Out of memory allocating pixel buffer");
577         } else {
578                 free(this->pixel_buffer);
579                 this->pixel_buffer = nullptr;
580         }
582         /* Redraw screen */
583         this->num_dirty_rects = lengthof(this->dirty_rects);
584         this->GameSizeChanged();
587 /**  Check if palette updates need to be performed. */
588 void VideoDriver_Cocoa::CheckPaletteAnim()
590         if (_cur_palette.count_dirty != 0) {
591                 Blitter *blitter = BlitterFactory::GetCurrentBlitter();
593                 switch (blitter->UsePaletteAnimation()) {
594                         case Blitter::PALETTE_ANIMATION_VIDEO_BACKEND:
595                                 this->UpdatePalette(_cur_palette.first_dirty, _cur_palette.count_dirty);
596                                 break;
598                         case Blitter::PALETTE_ANIMATION_BLITTER:
599                                 blitter->PaletteAnimate(_cur_palette);
600                                 break;
602                         case Blitter::PALETTE_ANIMATION_NONE:
603                                 break;
605                         default:
606                                 NOT_REACHED();
607                 }
608                 _cur_palette.count_dirty = 0;
609         }
614  * Poll and handle a single event from the OS.
615  * @return True if there was an event to handle.
616  */
617 bool VideoDriver_Cocoa::PollEvent()
619 #ifdef _DEBUG
620         uint32 et0 = GetTick();
621 #endif
622         NSEvent *event = [ NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[ NSDate distantPast ] inMode:NSDefaultRunLoopMode dequeue:YES ];
623 #ifdef _DEBUG
624         _tEvent += GetTick() - et0;
625 #endif
627         if (event == nil) return false;
629         [ NSApp sendEvent:event ];
631         return true;
634 void VideoDriver_Cocoa::InputLoop()
636         NSUInteger cur_mods = [ NSEvent modifierFlags ];
638         bool old_ctrl_pressed = _ctrl_pressed;
640         _ctrl_pressed = (cur_mods & ( _settings_client.gui.right_mouse_btn_emulation != RMBE_CONTROL ? NSControlKeyMask : NSCommandKeyMask)) != 0;
641         _shift_pressed = (cur_mods & NSShiftKeyMask) != 0;
643 #if defined(_DEBUG)
644         if (_shift_pressed) {
645 #else
646         if (_tab_is_down) {
647 #endif
648                 if (!_networking && _game_mode != GM_MENU) _fast_forward |= 2;
649         } else if (_fast_forward & 2) {
650                 _fast_forward = 0;
651         }
653         if (old_ctrl_pressed != _ctrl_pressed) HandleCtrlChanged();
656 /** Main game loop. */
657 void VideoDriver_Cocoa::GameLoop()
659         auto cur_ticks = std::chrono::steady_clock::now();
660         auto last_realtime_tick = cur_ticks;
661         auto next_game_tick = cur_ticks;
662         auto next_draw_tick = cur_ticks;
664         for (;;) {
665                 @autoreleasepool {
667                         InteractiveRandom(); // randomness
669                         while (this->PollEvent()) {}
671                         if (_exit_game) {
672                                 /* Restore saved resolution if in fullscreen mode. */
673                                 if (this->IsFullscreen()) _cur_resolution = this->orig_res;
674                                 break;
675                         }
678                         cur_ticks = std::chrono::steady_clock::now();
680                         /* If more than a millisecond has passed, increase the _realtime_tick. */
681                         if (cur_ticks - last_realtime_tick > std::chrono::milliseconds(1)) {
682                                 auto delta = std::chrono::duration_cast<std::chrono::milliseconds>(cur_ticks - last_realtime_tick);
683                                 _realtime_tick += delta.count();
684                                 last_realtime_tick += delta;
685                         }
687                         if (cur_ticks >= next_game_tick || (_fast_forward && !_pause_mode)) {
688                                 if (_fast_forward && !_pause_mode) {
689                                         next_game_tick = cur_ticks + this->GetGameInterval();
690                                 } else {
691                                         next_game_tick += this->GetGameInterval();
692                                         /* Avoid next_game_tick getting behind more and more if it cannot keep up. */
693                                         if (next_game_tick < cur_ticks - ALLOWED_DRIFT * this->GetGameInterval()) next_game_tick = cur_ticks;
694                                 }
696                                 ::GameLoop();
697                         }
699                         /* Prevent drawing when switching mode, as windows can be removed when they should still appear. */
700                         if (cur_ticks >= next_draw_tick && (_switch_mode == SM_NONE || HasModalProgress())) {
701                                 next_draw_tick += this->GetDrawInterval();
702                                 /* Avoid next_draw_tick getting behind more and more if it cannot keep up. */
703                                 if (next_draw_tick < cur_ticks - ALLOWED_DRIFT * this->GetDrawInterval()) next_draw_tick = cur_ticks;
705                                 this->InputLoop();
706                                 ::InputLoop();
707                                 UpdateWindows();
708                                 this->CheckPaletteAnim();
710                                 this->Paint();
711                         }
713                         /* If we are not in fast-forward, create some time between calls to ease up CPU usage. */
714                         if (!_fast_forward || _pause_mode) {
715                                 /* See how much time there is till we have to process the next event, and try to hit that as close as possible. */
716                                 auto next_tick = std::min(next_draw_tick, next_game_tick);
717                                 auto now = std::chrono::steady_clock::now();
719                                 if (next_tick > now) {
720                                         std::this_thread::sleep_for(next_tick - now);
721                                 }
722                         }
723                 }
724         }
728 @implementation OTTD_QuartzView
730 - (instancetype)initWithFrame:(NSRect)frameRect andDriver:(VideoDriver_Cocoa *)drv
732         if (self = [ super initWithFrame:frameRect ]) {
733                 self->driver = drv;
735                 /* We manage our content updates ourselves. */
736                 self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay;
737                 self.wantsLayer = YES;
739                 self.layer.magnificationFilter = kCAFilterNearest;
740         }
741         return self;
744 - (BOOL)acceptsFirstResponder
746         return NO;
749 - (BOOL)isOpaque
751         return YES;
754 - (BOOL)wantsUpdateLayer
756         return YES;
759 - (void)updateLayer
761         if (driver->cgcontext == nullptr) return;
763         /* Set layer contents to our backing buffer, which avoids needless copying. */
764         CGImageRef fullImage = CGBitmapContextCreateImage(driver->cgcontext);
765         self.layer.contents = (__bridge id)fullImage;
766         CGImageRelease(fullImage);
769 - (void)viewDidChangeBackingProperties
771         [ super viewDidChangeBackingProperties ];
773         self.layer.contentsScale = [ driver->cocoaview getContentsScale ];
776 @end
778 #endif /* WITH_COCOA */