From 2f25e9bdf8c8c2952ba7f8adb9bd73302fef37c2 Mon Sep 17 00:00:00 2001 From: Michael Lutz Date: Sun, 7 Feb 2021 20:50:41 +0100 Subject: Codechange: [OSX] Separate video driver into a base and a Quartz implementation. --- src/video/cocoa/cocoa_v.h | 71 +++++--- src/video/cocoa/cocoa_v.mm | 442 +++++++++++++++++++++++---------------------- 2 files changed, 275 insertions(+), 238 deletions(-) (limited to 'src/video') diff --git a/src/video/cocoa/cocoa_v.h b/src/video/cocoa/cocoa_v.h index f9f678157..4d683d622 100644 --- a/src/video/cocoa/cocoa_v.h +++ b/src/video/cocoa/cocoa_v.h @@ -24,32 +24,18 @@ class VideoDriver_Cocoa : public VideoDriver { private: Dimension orig_res; ///< Saved window size for non-fullscreen mode. - int window_width; ///< Current window width in pixel - int window_height; ///< Current window height in pixel - int window_pitch; - - int buffer_depth; ///< Colour depth of used frame buffer - void *pixel_buffer; ///< used for direct pixel access - void *window_buffer; ///< Colour translation from palette to screen - - Rect dirty_rect; ///< Region of the screen that needs redrawing. - - uint32 palette[256]; ///< Colour Palette - public: bool setup; ///< Window is currently being created. OTTD_CocoaWindow *window; ///< Pointer to window object OTTD_CocoaView *cocoaview; ///< Pointer to view object CGColorSpaceRef color_space; ///< Window color space - CGContextRef cgcontext; ///< Context reference for Quartz subdriver OTTD_CocoaWindowDelegate *delegate; //!< Window delegate object public: VideoDriver_Cocoa(); - const char *Start(const StringList ¶m) override; void Stop() override; void MainLoop() override; @@ -61,40 +47,73 @@ public: void EditBoxLostFocus() override; - const char *GetName() const override { return "cocoa"; } - /* --- The following methods should be private, but can't be due to Obj-C limitations. --- */ void GameLoop(); - void AllocateBackingStore(); + virtual void AllocateBackingStore() = 0; protected: + Rect dirty_rect; ///< Region of the screen that needs redrawing. Dimension GetScreenSize() const override; float GetDPIScale() override; void InputLoop() override; - void Paint() override; - void CheckPaletteAnim() override; + + void GameSizeChanged(); + + const char *Initialize(); + + void UpdateVideoModes(); + + bool MakeWindow(int width, int height); + + virtual NSView* AllocateDrawView() = 0; private: bool PollEvent(); bool IsFullscreen(); - void GameSizeChanged(); +}; - void UpdateVideoModes(); +class VideoDriver_CocoaQuartz : public VideoDriver_Cocoa { +private: + int buffer_depth; ///< Colour depth of used frame buffer + void *pixel_buffer; ///< used for direct pixel access + void *window_buffer; ///< Colour translation from palette to screen - bool MakeWindow(int width, int height); + int window_width; ///< Current window width in pixel + int window_height; ///< Current window height in pixel + int window_pitch; - void UpdatePalette(uint first_color, uint num_colors); + uint32 palette[256]; ///< Colour Palette void BlitIndexedToView32(int left, int top, int right, int bottom); + void UpdatePalette(uint first_color, uint num_colors); + +public: + CGContextRef cgcontext; ///< Context reference for Quartz subdriver + + VideoDriver_CocoaQuartz(); + + const char *Start(const StringList ¶m) override; + void Stop() override; + + /** Return driver name */ + const char *GetName() const override { return "cocoa"; } + + void AllocateBackingStore() override; + +protected: + void Paint() override; + void CheckPaletteAnim() override; + + NSView* AllocateDrawView() override; }; -class FVideoDriver_Cocoa : public DriverFactoryBase { +class FVideoDriver_CocoaQuartz : public DriverFactoryBase { public: - FVideoDriver_Cocoa() : DriverFactoryBase(Driver::DT_VIDEO, 10, "cocoa", "Cocoa Video Driver") {} - Driver *CreateInstance() const override { return new VideoDriver_Cocoa(); } + FVideoDriver_CocoaQuartz() : DriverFactoryBase(Driver::DT_VIDEO, 10, "cocoa", "Cocoa Video Driver") {} + Driver *CreateInstance() const override { return new VideoDriver_CocoaQuartz(); } }; #endif /* VIDEO_COCOA_H */ diff --git a/src/video/cocoa/cocoa_v.mm b/src/video/cocoa/cocoa_v.mm index 7baed796f..36ad0cafb 100644 --- a/src/video/cocoa/cocoa_v.mm +++ b/src/video/cocoa/cocoa_v.mm @@ -81,25 +81,9 @@ static const Dimension _default_resolutions[] = { { 2560, 1440 } }; -static FVideoDriver_Cocoa iFVideoDriver_Cocoa; - - -/** Subclass of NSView for drawing to screen. */ -@interface OTTD_QuartzView : NSView { - VideoDriver_Cocoa *driver; -} -- (instancetype)initWithFrame:(NSRect)frameRect andDriver:(VideoDriver_Cocoa *)drv; -@end - VideoDriver_Cocoa::VideoDriver_Cocoa() { - this->window_width = 0; - this->window_height = 0; - this->window_pitch = 0; - this->buffer_depth = 0; - this->window_buffer = nullptr; - this->pixel_buffer = nullptr; this->setup = false; this->window = nil; @@ -107,7 +91,6 @@ VideoDriver_Cocoa::VideoDriver_Cocoa() this->delegate = nil; this->color_space = nullptr; - this->cgcontext = nullptr; this->dirty_rect = {}; } @@ -124,17 +107,13 @@ void VideoDriver_Cocoa::Stop() [ this->cocoaview release ]; [ this->delegate release ]; - CGContextRelease(this->cgcontext); CGColorSpaceRelease(this->color_space); - free(this->window_buffer); - free(this->pixel_buffer); - _cocoa_video_started = false; } -/** Try to start Cocoa video driver. */ -const char *VideoDriver_Cocoa::Start(const StringList &parm) +/** Common driver initialization. */ +const char *VideoDriver_Cocoa::Initialize() { if (!MacOSVersionIsAtLeast(10, 7, 0)) return "The Cocoa video driver requires Mac OS X 10.7 or later."; @@ -147,23 +126,6 @@ const char *VideoDriver_Cocoa::Start(const StringList &parm) this->UpdateAutoResolution(); this->orig_res = _cur_resolution; - int bpp = BlitterFactory::GetCurrentBlitter()->GetScreenDepth(); - if (bpp != 8 && bpp != 32) { - Stop(); - return "The cocoa quartz subdriver only supports 8 and 32 bpp."; - } - - bool fullscreen = _fullscreen; - if (!this->MakeWindow(_cur_resolution.width, _cur_resolution.height)) { - Stop(); - return "Could not create window"; - } - - if (fullscreen) this->ToggleFullscreen(fullscreen); - - this->GameSizeChanged(); - this->UpdateVideoModes(); - return nullptr; } @@ -217,9 +179,6 @@ bool VideoDriver_Cocoa::ChangeResolution(int w, int h) [ this->cocoaview setFrameSize:contentRect.size ]; } - this->window_width = w; - this->window_height = h; - [ (OTTD_CocoaWindow *)this->window center ]; this->AllocateBackingStore(); @@ -251,7 +210,6 @@ bool VideoDriver_Cocoa::ToggleFullscreen(bool full_screen) bool VideoDriver_Cocoa::AfterBlitterChange() { this->ChangeResolution(_cur_resolution.width, _cur_resolution.height); - this->UpdatePalette(0, 256); return true; } @@ -294,12 +252,6 @@ bool VideoDriver_Cocoa::IsFullscreen() */ void VideoDriver_Cocoa::GameSizeChanged() { - /* Tell the game that the resolution has changed */ - _screen.width = this->window_width; - _screen.height = this->window_height; - _screen.pitch = this->buffer_depth == 8 ? this->window_width : this->window_pitch; - _screen.dst_ptr = this->buffer_depth == 8 ? this->pixel_buffer : this->window_buffer; - /* Store old window size if we entered fullscreen mode. */ bool fullscreen = this->IsFullscreen(); if (fullscreen && !_fullscreen) this->orig_res = _cur_resolution; @@ -392,7 +344,7 @@ bool VideoDriver_Cocoa::MakeWindow(int width, int height) [ this->cocoaview setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable ]; /* Create content view. */ - NSView *draw_view = [ [ OTTD_QuartzView alloc ] initWithFrame:[ this->cocoaview bounds ] andDriver:this ]; + NSView *draw_view = this->AllocateDrawView(); if (draw_view == nil) { DEBUG(driver, 0, "Could not create the drawing view."); this->setup = false; @@ -414,171 +366,11 @@ bool VideoDriver_Cocoa::MakeWindow(int width, int height) this->setup = false; - this->UpdatePalette(0, 256); this->AllocateBackingStore(); return true; } -/** - * This function copies 8bpp pixels to the screen buffer in 32bpp windowed mode. - * - * @param left The x coord for the left edge of the box to blit. - * @param top The y coord for the top edge of the box to blit. - * @param right The x coord for the right edge of the box to blit. - * @param bottom The y coord for the bottom edge of the box to blit. - */ -void VideoDriver_Cocoa::BlitIndexedToView32(int left, int top, int right, int bottom) -{ - const uint32 *pal = this->palette; - const uint8 *src = (uint8*)this->pixel_buffer; - uint32 *dst = (uint32*)this->window_buffer; - uint width = this->window_width; - uint pitch = this->window_pitch; - - for (int y = top; y < bottom; y++) { - for (int x = left; x < right; x++) { - dst[y * pitch + x] = pal[src[y * width + x]]; - } - } -} - -/** - * Paint window. - * @param force_update Whether to redraw unconditionally - */ -void VideoDriver_Cocoa::Paint() -{ - PerformanceMeasurer framerate(PFE_VIDEO); - - /* Check if we need to do anything */ - if (IsEmptyRect(this->dirty_rect) || [ this->window isMiniaturized ]) return; - - /* We only need to blit in indexed mode since in 32bpp mode the game draws directly to the image. */ - if (this->buffer_depth == 8) { - BlitIndexedToView32( - this->dirty_rect.left, - this->dirty_rect.top, - this->dirty_rect.right, - this->dirty_rect.bottom - ); - } - - NSRect dirtyrect; - dirtyrect.origin.x = this->dirty_rect.left; - dirtyrect.origin.y = this->window_height - this->dirty_rect.bottom; - dirtyrect.size.width = this->dirty_rect.right - this->dirty_rect.left; - dirtyrect.size.height = this->dirty_rect.bottom - this->dirty_rect.top; - - /* Notify OS X that we have new content to show. */ - [ this->cocoaview setNeedsDisplayInRect:[ this->cocoaview getVirtualRect:dirtyrect ] ]; - - /* Tell the OS to get our contents to screen as soon as possible. */ - [ CATransaction flush ]; - - this->dirty_rect = {}; -} - -/** Update the palette. */ -void VideoDriver_Cocoa::UpdatePalette(uint first_color, uint num_colors) -{ - if (this->buffer_depth != 8) return; - - for (uint i = first_color; i < first_color + num_colors; i++) { - uint32 clr = 0xff000000; - clr |= (uint32)_cur_palette.palette[i].r << 16; - clr |= (uint32)_cur_palette.palette[i].g << 8; - clr |= (uint32)_cur_palette.palette[i].b; - this->palette[i] = clr; - } - - this->MakeDirty(0, 0, _screen.width, _screen.height); -} - -/** Clear buffer to opaque black. */ -static void ClearWindowBuffer(uint32 *buffer, uint32 pitch, uint32 height) -{ - uint32 fill = Colour(0, 0, 0).data; - for (uint32 y = 0; y < height; y++) { - for (uint32 x = 0; x < pitch; x++) { - buffer[y * pitch + x] = fill; - } - } -} - -/** Resize the window. */ -void VideoDriver_Cocoa::AllocateBackingStore() -{ - if (this->window == nil || this->cocoaview == nil || this->setup) return; - - NSRect newframe = [ this->cocoaview getRealRect:[ this->cocoaview frame ] ]; - - this->window_width = (int)newframe.size.width; - this->window_height = (int)newframe.size.height; - this->window_pitch = Align(this->window_width, 16 / sizeof(uint32)); // Quartz likes lines that are multiple of 16-byte. - this->buffer_depth = BlitterFactory::GetCurrentBlitter()->GetScreenDepth(); - - /* Create Core Graphics Context */ - free(this->window_buffer); - this->window_buffer = malloc(this->window_pitch * this->window_height * sizeof(uint32)); - /* Initialize with opaque black. */ - ClearWindowBuffer((uint32 *)this->window_buffer, this->window_pitch, this->window_height); - - CGContextRelease(this->cgcontext); - this->cgcontext = CGBitmapContextCreate( - this->window_buffer, // data - this->window_width, // width - this->window_height, // height - 8, // bits per component - this->window_pitch * 4, // bytes per row - this->color_space, // color space - kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host - ); - - assert(this->cgcontext != NULL); - CGContextSetShouldAntialias(this->cgcontext, FALSE); - CGContextSetAllowsAntialiasing(this->cgcontext, FALSE); - CGContextSetInterpolationQuality(this->cgcontext, kCGInterpolationNone); - - if (this->buffer_depth == 8) { - free(this->pixel_buffer); - this->pixel_buffer = malloc(this->window_width * this->window_height); - if (this->pixel_buffer == nullptr) usererror("Out of memory allocating pixel buffer"); - } else { - free(this->pixel_buffer); - this->pixel_buffer = nullptr; - } - - /* Redraw screen */ - this->GameSizeChanged(); - this->MakeDirty(0, 0, _screen.width, _screen.height); -} - -/** Check if palette updates need to be performed. */ -void VideoDriver_Cocoa::CheckPaletteAnim() -{ - if (_cur_palette.count_dirty != 0) { - Blitter *blitter = BlitterFactory::GetCurrentBlitter(); - - switch (blitter->UsePaletteAnimation()) { - case Blitter::PALETTE_ANIMATION_VIDEO_BACKEND: - this->UpdatePalette(_cur_palette.first_dirty, _cur_palette.count_dirty); - break; - - case Blitter::PALETTE_ANIMATION_BLITTER: - blitter->PaletteAnimate(_cur_palette); - break; - - case Blitter::PALETTE_ANIMATION_NONE: - break; - - default: - NOT_REACHED(); - } - _cur_palette.count_dirty = 0; - } -} - /** * Poll and handle a single event from the OS. @@ -642,9 +434,16 @@ void VideoDriver_Cocoa::GameLoop() } +/* Subclass of OTTD_CocoaView to fix Quartz rendering */ +@interface OTTD_QuartzView : NSView { + VideoDriver_CocoaQuartz *driver; +} +- (instancetype)initWithFrame:(NSRect)frameRect andDriver:(VideoDriver_CocoaQuartz *)drv; +@end + @implementation OTTD_QuartzView -- (instancetype)initWithFrame:(NSRect)frameRect andDriver:(VideoDriver_Cocoa *)drv +- (instancetype)initWithFrame:(NSRect)frameRect andDriver:(VideoDriver_CocoaQuartz *)drv { if (self = [ super initWithFrame:frameRect ]) { self->driver = drv; @@ -693,4 +492,223 @@ void VideoDriver_Cocoa::GameLoop() @end + +static FVideoDriver_CocoaQuartz iFVideoDriver_CocoaQuartz; + +/** Clear buffer to opaque black. */ +static void ClearWindowBuffer(uint32 *buffer, uint32 pitch, uint32 height) +{ + uint32 fill = Colour(0, 0, 0).data; + for (uint32 y = 0; y < height; y++) { + for (uint32 x = 0; x < pitch; x++) { + buffer[y * pitch + x] = fill; + } + } +} + +VideoDriver_CocoaQuartz::VideoDriver_CocoaQuartz() +{ + this->window_width = 0; + this->window_height = 0; + this->window_pitch = 0; + this->buffer_depth = 0; + this->window_buffer = nullptr; + this->pixel_buffer = nullptr; + + this->cgcontext = nullptr; +} + +const char *VideoDriver_CocoaQuartz::Start(const StringList ¶m) +{ + const char *err = this->Initialize(); + if (err != nullptr) return err; + + int bpp = BlitterFactory::GetCurrentBlitter()->GetScreenDepth(); + if (bpp != 8 && bpp != 32) { + Stop(); + return "The cocoa quartz subdriver only supports 8 and 32 bpp."; + } + + bool fullscreen = _fullscreen; + if (!this->MakeWindow(_cur_resolution.width, _cur_resolution.height)) { + Stop(); + return "Could not create window"; + } + + if (fullscreen) this->ToggleFullscreen(fullscreen); + + this->GameSizeChanged(); + this->UpdateVideoModes(); + + return nullptr; + +} + +void VideoDriver_CocoaQuartz::Stop() +{ + this->VideoDriver_Cocoa::Stop(); + + CGContextRelease(this->cgcontext); + + free(this->window_buffer); + free(this->pixel_buffer); +} + +NSView *VideoDriver_CocoaQuartz::AllocateDrawView() +{ + return [ [ OTTD_QuartzView alloc ] initWithFrame:[ this->cocoaview bounds ] andDriver:this ]; +} + +/** Resize the window. */ +void VideoDriver_CocoaQuartz::AllocateBackingStore() +{ + if (this->window == nil || this->cocoaview == nil || this->setup) return; + + this->UpdatePalette(0, 256); + + NSRect newframe = [ this->cocoaview getRealRect:[ this->cocoaview frame ] ]; + + this->window_width = (int)newframe.size.width; + this->window_height = (int)newframe.size.height; + this->window_pitch = Align(this->window_width, 16 / sizeof(uint32)); // Quartz likes lines that are multiple of 16-byte. + this->buffer_depth = BlitterFactory::GetCurrentBlitter()->GetScreenDepth(); + + /* Create Core Graphics Context */ + free(this->window_buffer); + this->window_buffer = malloc(this->window_pitch * this->window_height * sizeof(uint32)); + /* Initialize with opaque black. */ + ClearWindowBuffer((uint32 *)this->window_buffer, this->window_pitch, this->window_height); + + CGContextRelease(this->cgcontext); + this->cgcontext = CGBitmapContextCreate( + this->window_buffer, // data + this->window_width, // width + this->window_height, // height + 8, // bits per component + this->window_pitch * 4, // bytes per row + this->color_space, // color space + kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host + ); + + assert(this->cgcontext != NULL); + CGContextSetShouldAntialias(this->cgcontext, FALSE); + CGContextSetAllowsAntialiasing(this->cgcontext, FALSE); + CGContextSetInterpolationQuality(this->cgcontext, kCGInterpolationNone); + + if (this->buffer_depth == 8) { + free(this->pixel_buffer); + this->pixel_buffer = malloc(this->window_width * this->window_height); + if (this->pixel_buffer == nullptr) usererror("Out of memory allocating pixel buffer"); + } else { + free(this->pixel_buffer); + this->pixel_buffer = nullptr; + } + + /* Tell the game that the resolution has changed */ + _screen.width = this->window_width; + _screen.height = this->window_height; + _screen.pitch = this->buffer_depth == 8 ? this->window_width : this->window_pitch; + _screen.dst_ptr = this->buffer_depth == 8 ? this->pixel_buffer : this->window_buffer; + + /* Redraw screen */ + this->MakeDirty(0, 0, _screen.width, _screen.height); + this->GameSizeChanged(); +} + +/** + * This function copies 8bpp pixels from the screen buffer in 32bpp windowed mode. + * + * @param left The x coord for the left edge of the box to blit. + * @param top The y coord for the top edge of the box to blit. + * @param right The x coord for the right edge of the box to blit. + * @param bottom The y coord for the bottom edge of the box to blit. + */ +void VideoDriver_CocoaQuartz::BlitIndexedToView32(int left, int top, int right, int bottom) +{ + const uint32 *pal = this->palette; + const uint8 *src = (uint8*)this->pixel_buffer; + uint32 *dst = (uint32*)this->window_buffer; + uint width = this->window_width; + uint pitch = this->window_pitch; + + for (int y = top; y < bottom; y++) { + for (int x = left; x < right; x++) { + dst[y * pitch + x] = pal[src[y * width + x]]; + } + } +} + +/** Update the palette */ +void VideoDriver_CocoaQuartz::UpdatePalette(uint first_color, uint num_colors) +{ + if (this->buffer_depth != 8) return; + + for (uint i = first_color; i < first_color + num_colors; i++) { + uint32 clr = 0xff000000; + clr |= (uint32)_cur_palette.palette[i].r << 16; + clr |= (uint32)_cur_palette.palette[i].g << 8; + clr |= (uint32)_cur_palette.palette[i].b; + this->palette[i] = clr; + } + + this->MakeDirty(0, 0, _screen.width, _screen.height); +} + +void VideoDriver_CocoaQuartz::CheckPaletteAnim() +{ + if (_cur_palette.count_dirty != 0) { + Blitter *blitter = BlitterFactory::GetCurrentBlitter(); + + switch (blitter->UsePaletteAnimation()) { + case Blitter::PALETTE_ANIMATION_VIDEO_BACKEND: + this->UpdatePalette(_cur_palette.first_dirty, _cur_palette.count_dirty); + break; + + case Blitter::PALETTE_ANIMATION_BLITTER: + blitter->PaletteAnimate(_cur_palette); + break; + + case Blitter::PALETTE_ANIMATION_NONE: + break; + + default: + NOT_REACHED(); + } + _cur_palette.count_dirty = 0; + } +} + +/** Draw window */ +void VideoDriver_CocoaQuartz::Paint() +{ + PerformanceMeasurer framerate(PFE_VIDEO); + + /* Check if we need to do anything */ + if (IsEmptyRect(this->dirty_rect) || [ this->window isMiniaturized ]) return; + + /* We only need to blit in indexed mode since in 32bpp mode the game draws directly to the image. */ + if (this->buffer_depth == 8) { + BlitIndexedToView32( + this->dirty_rect.left, + this->dirty_rect.top, + this->dirty_rect.right, + this->dirty_rect.bottom + ); + } + + NSRect dirtyrect; + dirtyrect.origin.x = this->dirty_rect.left; + dirtyrect.origin.y = this->window_height - this->dirty_rect.bottom; + dirtyrect.size.width = this->dirty_rect.right - this->dirty_rect.left; + dirtyrect.size.height = this->dirty_rect.bottom - this->dirty_rect.top; + + /* Notify OS X that we have new content to show. */ + [ this->cocoaview setNeedsDisplayInRect:[ this->cocoaview getVirtualRect:dirtyrect ] ]; + + /* Tell the OS to get our contents to screen as soon as possible. */ + [ CATransaction flush ]; + + this->dirty_rect = {}; +} + #endif /* WITH_COCOA */ -- cgit v1.2.3-54-g00ecf