Codechange: [OSX] Separate video driver into a base and a Quartz implementation.

This commit is contained in:
Michael Lutz 2021-02-07 20:50:41 +01:00
parent 421b599541
commit 2f25e9bdf8
2 changed files with 277 additions and 240 deletions

View File

@ -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 &param) 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;
private:
bool PollEvent();
bool IsFullscreen();
void GameSizeChanged();
const char *Initialize();
void UpdateVideoModes();
bool MakeWindow(int width, int height);
void UpdatePalette(uint first_color, uint num_colors);
virtual NSView* AllocateDrawView() = 0;
void BlitIndexedToView32(int left, int top, int right, int bottom);
private:
bool PollEvent();
bool IsFullscreen();
};
class FVideoDriver_Cocoa : public DriverFactoryBase {
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
int window_width; ///< Current window width in pixel
int window_height; ///< Current window height in pixel
int window_pitch;
uint32 palette[256]; ///< Colour Palette
void BlitIndexedToView32(int left, int top, int right, int bottom);
void UpdatePalette(uint first_color, uint num_colors);
public:
FVideoDriver_Cocoa() : DriverFactoryBase(Driver::DT_VIDEO, 10, "cocoa", "Cocoa Video Driver") {}
Driver *CreateInstance() const override { return new VideoDriver_Cocoa(); }
CGContextRef cgcontext; ///< Context reference for Quartz subdriver
VideoDriver_CocoaQuartz();
const char *Start(const StringList &param) 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_CocoaQuartz : public DriverFactoryBase {
public:
FVideoDriver_CocoaQuartz() : DriverFactoryBase(Driver::DT_VIDEO, 10, "cocoa", "Cocoa Video Driver") {}
Driver *CreateInstance() const override { return new VideoDriver_CocoaQuartz(); }
};
#endif /* VIDEO_COCOA_H */

View File

@ -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 &param)
{
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 */