From d4b419347b785461a8da6440ed62923398794a10 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Tue, 3 Mar 2026 12:41:31 +0000 Subject: [PATCH] video/out: add Cocoa OpenGL backend for macOS 10.6 Add a simplified Cocoa implementation based on mpv v0.28.0 for legacy macOS systems (10.6 Snow Leopard). Changes: - Add cocoa_common.h/m for Cocoa window management and event handling - Add context_cocoa_legacy.m as OpenGL context provider - Use CGL (Core Graphics Layer) with Legacy profile for GL 2.0/2.1 - Remove modern features not available in 10.6: * Touch Bar support (requires 10.12.2+) * Retina/HiDPI APIs (requires 10.7+) * CVDisplayLink for vsync (simplified) * Native fullscreen transitions (uses simple style mask change) - Integrate into meson.build when gl-cocoa feature is enabled - Register ra_ctx_cocoa_legacy in GPU context list The simplified implementation maintains core functionality: - Window creation, positioning, and fullscreen - OpenGL context creation and buffer swapping - Basic event handling and cursor management - Power management (screensaver prevention) - Compatible with macOS 10.6 APIs Implementation is ~507 lines (vs 1125 in v0.28.0), removing features that require newer macOS versions or are unnecessary for basic playback. --- meson.build | 2 + video/out/cocoa_common.h | 40 ++ video/out/cocoa_common.m | 507 ++++++++++++++++++++++++ video/out/gpu/context.c | 4 + video/out/opengl/context_cocoa_legacy.m | 165 ++++++++ 5 files changed, 718 insertions(+) create mode 100644 video/out/cocoa_common.h create mode 100644 video/out/cocoa_common.m create mode 100644 video/out/opengl/context_cocoa_legacy.m diff --git a/meson.build b/meson.build index 5ffe559768..9304fa223a 100644 --- a/meson.build +++ b/meson.build @@ -1151,6 +1151,8 @@ features += {'gl-cocoa': gl_cocoa.allowed()} if features['gl-cocoa'] dependencies += GL features += {'gl': true} + sources += files('video/out/cocoa_common.m', + 'video/out/opengl/context_cocoa_legacy.m') endif gl_win32 = get_option('gl-win32').require( diff --git a/video/out/cocoa_common.h b/video/out/cocoa_common.h new file mode 100644 index 0000000000..076136dabe --- /dev/null +++ b/video/out/cocoa_common.h @@ -0,0 +1,40 @@ +/* + * Cocoa OpenGL Backend + * + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv 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. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#ifndef MPLAYER_COCOA_COMMON_H +#define MPLAYER_COCOA_COMMON_H + +#include +#include +#include + +struct vo; +struct vo_cocoa_state; + +void vo_cocoa_init(struct vo *vo); +void vo_cocoa_uninit(struct vo *vo); + +int vo_cocoa_config_window(struct vo *vo); + +int vo_cocoa_control(struct vo *vo, int *events, int request, void *arg); + +void vo_cocoa_swap_buffers(struct vo *vo); +void vo_cocoa_set_opengl_ctx(struct vo *vo, CGLContextObj ctx); + +#endif /* MPLAYER_COCOA_COMMON_H */ diff --git a/video/out/cocoa_common.m b/video/out/cocoa_common.m new file mode 100644 index 0000000000..4bae851738 --- /dev/null +++ b/video/out/cocoa_common.m @@ -0,0 +1,507 @@ +/* + * Cocoa OpenGL Backend (macOS 10.6 Compatible) + * + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv 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. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + * + * Simplified version for macOS 10.6 Snow Leopard compatibility. + * Removed: Touch Bar, Retina display support, modern Cocoa APIs. + */ + +#import +#import +#import +#include + +#import "cocoa_common.h" + +#include "config.h" +#include "options/options.h" +#include "video/out/vo.h" +#include "common/msg.h" +#include "input/input.h" +#include "osdep/threads.h" +#include "osdep/timer.h" + +// Forward declarations +static void vo_cocoa_fullscreen(struct vo *vo); +static void vo_cocoa_update_screen_info(struct vo *vo); + +// Simple NSView subclass for OpenGL rendering +@interface MpvGLView : NSView +@end + +@implementation MpvGLView +- (BOOL)isOpaque { return YES; } +- (BOOL)acceptsFirstResponder { return YES; } +@end + +// Simple window class +@interface MpvWindow : NSWindow +{ + struct vo *_vo; +} +- (void)setVideoOutput:(struct vo *)vo; +@end + +@implementation MpvWindow + +- (void)setVideoOutput:(struct vo *)vo +{ + _vo = vo; +} + +- (BOOL)canBecomeKeyWindow { return YES; } +- (BOOL)canBecomeMainWindow { return YES; } + +@end + +struct vo_cocoa_state { + // Window and view + NSWindow *window; + NSView *view; + + // OpenGL context + CGLContextObj cgl_ctx; + NSOpenGLContext *nsgl_ctx; + + // Screen info + NSScreen *current_screen; + CGDirectDisplayID display_id; + + // Window state + bool embedded; + bool fullscreen; + NSRect unfs_window; + NSRect unfs_content; + NSInteger window_level; + + // Cursor visibility + bool cursor_visible; + bool cursor_visibility_wanted; + + // Power management + IOPMAssertionID power_mgmt_assertion; + + // Synchronization + pthread_mutex_t lock; + pthread_cond_t wakeup; + + // Pending events + int pending_events; + + // VO size + int vo_dwidth; + int vo_dheight; + + // Ready flag + bool vo_ready; + + // Window title + char *window_title; + + // Log + struct mp_log *log; +}; + +static void flag_events(struct vo *vo, int events) +{ + struct vo_cocoa_state *s = vo->cocoa; + pthread_mutex_lock(&s->lock); + s->pending_events |= events; + pthread_mutex_unlock(&s->lock); + if (events) + vo_wakeup(vo); +} + +static void enable_power_management(struct vo_cocoa_state *s) +{ + if (!s->power_mgmt_assertion) return; + IOPMAssertionRelease(s->power_mgmt_assertion); + s->power_mgmt_assertion = kIOPMNullAssertionID; +} + +static void disable_power_management(struct vo_cocoa_state *s) +{ + if (s->power_mgmt_assertion) return; + IOPMAssertionCreateWithName( + kIOPMAssertionTypePreventUserIdleDisplaySleep, + kIOPMAssertionLevelOn, + CFSTR("io.mpv.video_playing_back"), + &s->power_mgmt_assertion); +} + +static void run_on_main_thread(struct vo *vo, void(^block)(void)) +{ + dispatch_sync(dispatch_get_main_queue(), block); +} + +static NSScreen *get_screen_by_id(struct vo *vo, int screen_id) +{ + NSArray *screens = [NSScreen screens]; + int n_of_displays = [screens count]; + + if (screen_id >= n_of_displays || screen_id < 0) { + return nil; + } + + return [screens objectAtIndex:screen_id]; +} + +static void vo_cocoa_update_screen_info(struct vo *vo) +{ + struct vo_cocoa_state *s = vo->cocoa; + struct mp_vo_opts *opts = vo->opts; + + if (s->embedded) + return; + + if (s->current_screen && s->window) { + s->current_screen = [s->window screen]; + } else if (!s->current_screen) { + s->current_screen = get_screen_by_id(vo, opts->screen_id); + if (!s->current_screen) + s->current_screen = [NSScreen mainScreen]; + } + + NSDictionary *sinfo = [s->current_screen deviceDescription]; + s->display_id = [[sinfo objectForKey:@"NSScreenNumber"] longValue]; +} + +static void vo_cocoa_update_cursor_visibility(struct vo *vo) +{ + struct vo_cocoa_state *s = vo->cocoa; + + if (s->embedded) + return; + + if (s->cursor_visibility_wanted && !s->cursor_visible) { + [NSCursor unhide]; + s->cursor_visible = true; + } else if (!s->cursor_visibility_wanted && s->cursor_visible) { + [NSCursor hide]; + s->cursor_visible = false; + } +} + +static void vo_set_level(struct vo *vo, int ontop) +{ + struct vo_cocoa_state *s = vo->cocoa; + + if (ontop) { + s->window_level = NSFloatingWindowLevel; + } else { + s->window_level = NSNormalWindowLevel; + } + + [s->window setLevel:s->window_level]; +} + +static MpvWindow *create_window(NSRect rect, NSScreen *s, bool border) +{ + int window_mask = 0; + if (border) { + window_mask = NSTitledWindowMask | NSClosableWindowMask | + NSMiniaturizableWindowMask | NSResizableWindowMask; + } else { + window_mask = NSBorderlessWindowMask | NSResizableWindowMask; + } + + MpvWindow *w = [[MpvWindow alloc] initWithContentRect:rect + styleMask:window_mask + backing:NSBackingStoreBuffered + defer:NO + screen:s]; + return w; +} + +static void create_ui(struct vo *vo, NSRect *win_rect) +{ + struct vo_cocoa_state *s = vo->cocoa; + struct mp_vo_opts *opts = vo->opts; + + NSView *parent; + if (s->embedded) { + parent = (NSView *)(intptr_t)opts->WinID; + } else { + s->window = create_window(*win_rect, s->current_screen, opts->border); + [(MpvWindow *)s->window setVideoOutput:vo]; + [s->window setDelegate:(id)s->window]; + parent = [s->window contentView]; + } + + s->view = [[MpvGLView alloc] initWithFrame:[parent bounds]]; + [parent addSubview:s->view]; + + [s->nsgl_ctx setView:s->view]; + + if (!s->embedded) { + [s->window makeMainWindow]; + [s->window makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; + } +} + +static void cocoa_set_window_title(struct vo *vo) +{ + struct vo_cocoa_state *s = vo->cocoa; + + if (s->embedded || !s->window_title) + return; + + NSString *nstitle = [NSString stringWithUTF8String:s->window_title]; + if (nstitle) { + [s->window setTitle:nstitle]; + } +} + +void vo_cocoa_init(struct vo *vo) +{ + struct vo_cocoa_state *s = talloc_zero(NULL, struct vo_cocoa_state); + *s = (struct vo_cocoa_state){ + .power_mgmt_assertion = kIOPMNullAssertionID, + .log = mp_log_new(s, vo->log, "cocoa"), + .embedded = vo->opts->WinID >= 0, + .cursor_visible = true, + .cursor_visibility_wanted = true, + .fullscreen = false, + }; + + pthread_mutex_init(&s->lock, NULL); + pthread_cond_init(&s->wakeup, NULL); + + vo->cocoa = s; + + vo_cocoa_update_screen_info(vo); + + if (!s->embedded) { + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + } +} + +void vo_cocoa_uninit(struct vo *vo) +{ + struct vo_cocoa_state *s = vo->cocoa; + + if (!s) + return; + + run_on_main_thread(vo, ^{ + if (s->window) { + vo_cocoa_update_cursor_visibility(vo); + [s->window setDelegate:nil]; + [s->window close]; + } + + enable_power_management(s); + + [s->nsgl_ctx release]; + CGLReleaseContext(s->cgl_ctx); + + [s->view removeFromSuperview]; + [s->view release]; + }); + + pthread_cond_destroy(&s->wakeup); + pthread_mutex_destroy(&s->lock); + + talloc_free(s); + vo->cocoa = NULL; +} + +void vo_cocoa_set_opengl_ctx(struct vo *vo, CGLContextObj ctx) +{ + struct vo_cocoa_state *s = vo->cocoa; + run_on_main_thread(vo, ^{ + s->cgl_ctx = CGLRetainContext(ctx); + s->nsgl_ctx = [[NSOpenGLContext alloc] initWithCGLContextObj:s->cgl_ctx]; + }); +} + +int vo_cocoa_config_window(struct vo *vo) +{ + struct vo_cocoa_state *s = vo->cocoa; + struct mp_vo_opts *opts = vo->opts; + + run_on_main_thread(vo, ^{ + NSRect screen_frame = [s->current_screen frame]; + + // Calculate window size + int width = vo->dwidth; + int height = vo->dheight; + + // Center the window on screen + int x = (screen_frame.size.width - width) / 2; + int y = (screen_frame.size.height - height) / 2; + + NSRect win_rect = NSMakeRect(x, y, width, height); + + if (!s->view) { + create_ui(vo, &win_rect); + } + + s->unfs_window = win_rect; + s->unfs_content = NSMakeRect(0, 0, width, height); + + if (!s->embedded && s->window) { + cocoa_set_window_title(vo); + vo_set_level(vo, opts->ontop); + + if (opts->fullscreen && !s->fullscreen) { + vo_cocoa_fullscreen(vo); + } + } + + s->vo_ready = true; + s->vo_dwidth = width; + s->vo_dheight = height; + + [s->nsgl_ctx update]; + }); + + return 0; +} + +static void vo_cocoa_fullscreen(struct vo *vo) +{ + struct vo_cocoa_state *s = vo->cocoa; + + if (s->embedded) + return; + + NSScreen *screen = s->current_screen; + NSRect screen_frame = [screen frame]; + + if (!s->fullscreen) { + // Enter fullscreen + s->unfs_window = [s->window frame]; + s->unfs_content = [s->view frame]; + + [s->window setStyleMask:NSBorderlessWindowMask]; + [s->window setFrame:screen_frame display:YES]; + [s->window setLevel:NSMainMenuWindowLevel + 1]; + + s->fullscreen = true; + } else { + // Exit fullscreen + [s->window setStyleMask:NSTitledWindowMask | NSClosableWindowMask | + NSMiniaturizableWindowMask | NSResizableWindowMask]; + [s->window setFrame:s->unfs_window display:YES]; + [s->window setLevel:s->window_level]; + + s->fullscreen = false; + } + + flag_events(vo, VO_EVENT_RESIZE); +} + +void vo_cocoa_swap_buffers(struct vo *vo) +{ + struct vo_cocoa_state *s = vo->cocoa; + + // Simple synchronous swap + [s->nsgl_ctx flushBuffer]; +} + +static int vo_cocoa_check_events(struct vo *vo) +{ + struct vo_cocoa_state *s = vo->cocoa; + + pthread_mutex_lock(&s->lock); + int events = s->pending_events; + s->pending_events = 0; + + if (events & VO_EVENT_RESIZE) { + vo->dwidth = s->vo_dwidth; + vo->dheight = s->vo_dheight; + } + pthread_mutex_unlock(&s->lock); + + return events; +} + +static int vo_cocoa_control_on_main_thread(struct vo *vo, int request, void *arg) +{ + struct vo_cocoa_state *s = vo->cocoa; + + switch (request) { + case VOCTRL_FULLSCREEN: + vo_cocoa_fullscreen(vo); + return VO_TRUE; + case VOCTRL_GET_FULLSCREEN: + *(int *)arg = s->fullscreen; + return VO_TRUE; + case VOCTRL_ONTOP: + vo_set_level(vo, vo->opts->ontop); + return VO_TRUE; + case VOCTRL_BORDER: + // Border changes not supported in this simplified version + return VO_NOTIMPL; + case VOCTRL_GET_UNFS_WINDOW_SIZE: { + int *sz = arg; + NSRect rect = s->fullscreen ? s->unfs_content : [s->view frame]; + sz[0] = rect.size.width; + sz[1] = rect.size.height; + return VO_TRUE; + } + case VOCTRL_SET_UNFS_WINDOW_SIZE: { + int *sz = arg; + NSRect r = NSMakeRect(0, 0, sz[0], sz[1]); + if (!s->fullscreen && s->window) { + [s->window setContentSize:r.size]; + s->vo_dwidth = sz[0]; + s->vo_dheight = sz[1]; + flag_events(vo, VO_EVENT_RESIZE); + } + return VO_TRUE; + } + case VOCTRL_SET_CURSOR_VISIBILITY: + s->cursor_visibility_wanted = *(bool *)arg; + vo_cocoa_update_cursor_visibility(vo); + return VO_TRUE; + case VOCTRL_UPDATE_WINDOW_TITLE: + talloc_free(s->window_title); + s->window_title = talloc_strdup(s, (char *)arg); + cocoa_set_window_title(vo); + return VO_TRUE; + case VOCTRL_RESTORE_SCREENSAVER: + enable_power_management(s); + return VO_TRUE; + case VOCTRL_KILL_SCREENSAVER: + disable_power_management(s); + return VO_TRUE; + case VOCTRL_GET_DISPLAY_FPS: + // Return a default 60Hz for simplicity + *(double *)arg = 60.0; + return VO_TRUE; + } + + return VO_NOTIMPL; +} + +int vo_cocoa_control(struct vo *vo, int *events, int request, void *arg) +{ + if (request == VOCTRL_CHECK_EVENTS) { + *events |= vo_cocoa_check_events(vo); + return VO_TRUE; + } + + __block int r = VO_NOTIMPL; + run_on_main_thread(vo, ^{ + r = vo_cocoa_control_on_main_thread(vo, request, arg); + }); + + return r; +} diff --git a/video/out/gpu/context.c b/video/out/gpu/context.c index 60c0641a70..4c08ad72ba 100644 --- a/video/out/gpu/context.c +++ b/video/out/gpu/context.c @@ -42,6 +42,7 @@ extern const struct ra_ctx_fns ra_ctx_wgl; extern const struct ra_ctx_fns ra_ctx_angle; extern const struct ra_ctx_fns ra_ctx_dxgl; extern const struct ra_ctx_fns ra_ctx_android; +extern const struct ra_ctx_fns ra_ctx_cocoa_legacy; /* Vulkan */ extern const struct ra_ctx_fns ra_ctx_vulkan_wayland; @@ -102,6 +103,9 @@ static const struct ra_ctx_fns *const contexts[] = { #if HAVE_EGL_ANDROID &ra_ctx_android, #endif +#if HAVE_GL_COCOA + &ra_ctx_cocoa_legacy, +#endif #if HAVE_EGL_ANGLE_WIN32 &ra_ctx_angle, #endif diff --git a/video/out/opengl/context_cocoa_legacy.m b/video/out/opengl/context_cocoa_legacy.m new file mode 100644 index 0000000000..6383001615 --- /dev/null +++ b/video/out/opengl/context_cocoa_legacy.m @@ -0,0 +1,165 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv 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. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include +#include +#include "video/out/cocoa_common.h" +#include "context.h" + +struct priv { + GL gl; + CGLPixelFormatObj pix; + CGLContextObj ctx; +}; + +static int set_swap_interval(int enabled) +{ + CGLContextObj ctx = CGLGetCurrentContext(); + CGLError err = CGLSetParameter(ctx, kCGLCPSwapInterval, &enabled); + return (err == kCGLNoError) ? 0 : -1; +} + +static void *cocoa_glgetaddr(const char *s) +{ + void *ret = NULL; + void *handle = dlopen( + "/System/Library/Frameworks/OpenGL.framework/OpenGL", + RTLD_LAZY | RTLD_LOCAL); + if (!handle) + return NULL; + ret = dlsym(handle, s); + dlclose(handle); + return ret; +} + +static bool create_gl_context(struct ra_ctx *ctx) +{ + struct priv *p = ctx->priv; + GL *gl = &p->gl; + + // For macOS 10.6, use Legacy profile to get OpenGL 2.0/2.1 + CGLPixelFormatAttribute attrs[] = { + kCGLPFAOpenGLProfile, + (CGLPixelFormatAttribute) kCGLOGLPVersion_Legacy, + kCGLPFAAccelerated, + kCGLPFADoubleBuffer, + kCGLPFAColorSize, 24, + kCGLPFAAlphaSize, 8, + kCGLPFADepthSize, 16, + 0 + }; + + GLint npix; + CGLError err = CGLChoosePixelFormat(attrs, &p->pix, &npix); + if (err != kCGLNoError) { + MP_ERR(ctx->vo, "error creating CGL pixel format: %s (%d)\n", + CGLErrorString(err), err); + return false; + } + + err = CGLCreateContext(p->pix, 0, &p->ctx); + if (err != kCGLNoError) { + MP_FATAL(ctx->vo, "error creating CGL context: %s (%d)\n", + CGLErrorString(err), err); + CGLReleasePixelFormat(p->pix); + return false; + } + + vo_cocoa_set_opengl_ctx(ctx->vo, p->ctx); + CGLSetCurrentContext(p->ctx); + + if (ctx->opts.want_alpha) + CGLSetParameter(p->ctx, kCGLCPSurfaceOpacity, &(GLint){0}); + + mpgl_load_functions(gl, (void *)cocoa_glgetaddr, NULL, ctx->vo->log); + gl->SwapInterval = set_swap_interval; + + CGLReleasePixelFormat(p->pix); + + return true; +} + +static void cocoa_uninit(struct ra_ctx *ctx) +{ + struct priv *p = ctx->priv; + ra_gl_ctx_uninit(ctx); + if (p->ctx) + CGLReleaseContext(p->ctx); + vo_cocoa_uninit(ctx->vo); +} + +static void cocoa_swap_buffers(struct ra_ctx *ctx) +{ + struct priv *p = ctx->priv; + GL *gl = &p->gl; + vo_cocoa_swap_buffers(ctx->vo); + gl->Flush(); +} + +static bool cocoa_init(struct ra_ctx *ctx) +{ + struct priv *p = ctx->priv = talloc_zero(ctx, struct priv); + GL *gl = &p->gl; + + vo_cocoa_init(ctx->vo); + + if (!create_gl_context(ctx)) + goto fail; + + struct ra_ctx_params params = { + .swap_buffers = cocoa_swap_buffers, + }; + + if (!ra_gl_ctx_init(ctx, gl, params)) + goto fail; + + return true; + +fail: + cocoa_uninit(ctx); + return false; +} + +static void resize(struct ra_ctx *ctx) +{ + ra_gl_ctx_resize(ctx->swapchain, ctx->vo->dwidth, ctx->vo->dheight, 0); +} + +static bool cocoa_reconfig(struct ra_ctx *ctx) +{ + vo_cocoa_config_window(ctx->vo); + resize(ctx); + return true; +} + +static int cocoa_control(struct ra_ctx *ctx, int *events, int request, + void *arg) +{ + int ret = vo_cocoa_control(ctx->vo, events, request, arg); + if (*events & VO_EVENT_RESIZE) + resize(ctx); + return ret; +} + +const struct ra_ctx_fns ra_ctx_cocoa_legacy = { + .type = "opengl", + .name = "cocoa", + .init = cocoa_init, + .reconfig = cocoa_reconfig, + .control = cocoa_control, + .uninit = cocoa_uninit, +}; -- 2.43.0