From 78d43740f52db817d98bcf24fb30a76ab6fa13ff Mon Sep 17 00:00:00 2001 From: der richter Date: Sat, 30 Sep 2023 16:01:04 +0200 Subject: [PATCH] vo_gpu/vo_gpu_next: add vulkan support for macOS add support for vulkan through metal and a translation layer like MoltenVK. also add the possibility to use different render timing modes for testing. i still consider this experimental atm. --- DOCS/man/options.rst | 14 +++ meson.build | 15 ++- osdep/macOS_swift_bridge.h | 5 +- osdep/macos/precise_timer.swift | 153 +++++++++++++++++++++++++++ osdep/macosx_application.h | 7 ++ osdep/macosx_application.m | 3 + video/out/gpu/context.c | 4 + video/out/mac/metal_layer.swift | 43 ++++++++ video/out/mac_common.swift | 177 ++++++++++++++++++++++++++++++++ video/out/vulkan/common.h | 4 + video/out/vulkan/context_mac.m | 119 +++++++++++++++++++++ 11 files changed, 538 insertions(+), 6 deletions(-) create mode 100644 osdep/macos/precise_timer.swift create mode 100644 video/out/mac/metal_layer.swift create mode 100644 video/out/mac_common.swift create mode 100644 video/out/vulkan/context_mac.m diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index bee5aa0b10ac7..5b57d4d2c91f2 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -6273,6 +6273,18 @@ them. macOS only. +``--macos-render-timer=`` + Sets the mode (default: callback) for syncing the rendering of frames to the display's + vertical refresh rate. + macOS and Vulkan (macvk) only. + + ```` can be one of the following: + + :callback: Syncs to the CVDisplayLink callback + :precise: Syncs to the time of the next vertical display refresh reported by the + CVDisplayLink callback provided information + :system: No manual syncing, depend on the layer mechanic and the next drawable + ``--android-surface-size=`` Set dimensions of the rendering surface used by the Android gpu context. Needs to be set by the embedding application if the dimensions change during @@ -6324,6 +6336,8 @@ them. X11/EGL android Android/EGL. Requires ``--wid`` be set to an ``android.view.Surface``. + macvk + Vulkan on macOS with a metal surface through a translation layer (experimental) ``--gpu-api=`` Controls which type of graphics APIs will be accepted: diff --git a/meson.build b/meson.build index 4485c74a56be1..a56bb1f37c09b 100644 --- a/meson.build +++ b/meson.build @@ -1546,12 +1546,14 @@ swift = get_option('swift-build').require( darwin and macos_sdk_version.version_compare('>=10.10') and swift_ver.version_compare('>=4.1'), error_message: 'A suitable macos sdk version or swift version could not be found!', ) +features += {'swift': swift.allowed()} swift_sources = [] -if cocoa.found() and swift.allowed() +if features['cocoa'] and features['swift'] swift_sources += files('osdep/macos/libmpv_helper.swift', 'osdep/macos/log_helper.swift', 'osdep/macos/mpv_helper.swift', + 'osdep/macos/precise_timer.swift', 'osdep/macos/swift_compat.swift', 'osdep/macos/swift_extensions.swift', 'video/out/mac/common.swift', @@ -1561,7 +1563,7 @@ if cocoa.found() and swift.allowed() endif macos_cocoa_cb = get_option('macos-cocoa-cb').require( - features['cocoa'] and swift.allowed(), + features['cocoa'] and features['swift'], error_message: 'Either cocoa or swift could not be found!', ) features += {'macos-cocoa-cb': macos_cocoa_cb.allowed()} @@ -1569,9 +1571,14 @@ if features['macos-cocoa-cb'] swift_sources += files('video/out/cocoa_cb_common.swift', 'video/out/mac/gl_layer.swift') endif +if features['cocoa'] and features['vulkan'] and features['swift'] + swift_sources += files('video/out/mac_common.swift', + 'video/out/mac/metal_layer.swift') + sources += files('video/out/vulkan/context_mac.m') +endif macos_media_player = get_option('macos-media-player').require( - macos_10_12_2_features.allowed() and swift.allowed(), + macos_10_12_2_features.allowed() and features['swift'], error_message: 'Either the macos sdk version is not at least 10.12.2 or swift was not found!', ) features += {'macos-media-player': macos_media_player.allowed()} @@ -1579,7 +1586,7 @@ if features['macos-media-player'] swift_sources += files('osdep/macos/remote_command_center.swift') endif -if swift.allowed() and swift_sources.length() > 0 +if features['swift'] and swift_sources.length() > 0 subdir('osdep') endif diff --git a/osdep/macOS_swift_bridge.h b/osdep/macOS_swift_bridge.h index 29cd8bf016a84..9407b6fc9b26e 100644 --- a/osdep/macOS_swift_bridge.h +++ b/osdep/macOS_swift_bridge.h @@ -15,9 +15,10 @@ * License along with mpv. If not, see . */ -// including IOKit here again doesn't make sense, but otherwise the swift -// compiler doesn't include the needed header in our generated header file +// including frameworks here again doesn't make sense, but otherwise the swift +// compiler doesn't include the needed headers in our generated header file #import +#import #include "player/client.h" #include "video/out/libmpv.h" diff --git a/osdep/macos/precise_timer.swift b/osdep/macos/precise_timer.swift new file mode 100644 index 0000000000000..f4ad3bb6b6394 --- /dev/null +++ b/osdep/macos/precise_timer.swift @@ -0,0 +1,153 @@ +/* + * 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 . + */ + +import Cocoa + +struct Timing { + let time: UInt64 + let closure: () -> () +} + +class PreciseTimer { + unowned var common: Common + var mpv: MPVHelper? { get { return common.mpv } } + + let nanoPerSecond: Double = 1e+9 + let machToNano: Double = { + var timebase: mach_timebase_info = mach_timebase_info() + mach_timebase_info(&timebase) + return Double(timebase.numer) / Double(timebase.denom) + }() + + let condition = NSCondition() + var events: [Timing] = [] + var isRunning: Bool = true + var isHighPrecision: Bool = false + + var thread: pthread_t! + var threadPort: thread_port_t = thread_port_t() + let policyFlavor = thread_policy_flavor_t(THREAD_TIME_CONSTRAINT_POLICY) + let policyCount = MemoryLayout.size / + MemoryLayout.size + var typeNumber: mach_msg_type_number_t { + return mach_msg_type_number_t(policyCount) + } + var threadAttr: pthread_attr_t = { + var attr = pthread_attr_t() + var param = sched_param() + pthread_attr_init(&attr) + param.sched_priority = sched_get_priority_max(SCHED_FIFO) + pthread_attr_setschedparam(&attr, ¶m) + pthread_attr_setschedpolicy(&attr, SCHED_FIFO) + return attr + }() + + init?(common com: Common) { + common = com + + pthread_create(&thread, &threadAttr, entryC, MPVHelper.bridge(obj: self)) + if thread == nil { + common.log.sendWarning("Couldn't create pthread for high precision timer") + return nil + } + + threadPort = pthread_mach_thread_np(thread) + } + + func updatePolicy(periodSeconds: Double = 1 / 60.0) { + let period = periodSeconds * nanoPerSecond / machToNano + var policy = thread_time_constraint_policy( + period: UInt32(period), + computation: UInt32(0.75 * period), + constraint: UInt32(0.85 * period), + preemptible: 1 + ) + + let success = withUnsafeMutablePointer(to: &policy) { + $0.withMemoryRebound(to: integer_t.self, capacity: policyCount) { + thread_policy_set(threadPort, policyFlavor, $0, typeNumber) + } + } + + isHighPrecision = success == KERN_SUCCESS + if !isHighPrecision { + common.log.sendWarning("Couldn't create a high precision timer") + } + } + + func terminate() { + condition.lock() + isRunning = false + condition.signal() + condition.unlock() + pthread_kill(thread, SIGALRM) + pthread_join(thread, nil) + } + + func scheduleAt(time: UInt64, closure: @escaping () -> ()) { + condition.lock() + let firstEventTime = events.first?.time ?? 0 + let lastEventTime = events.last?.time ?? 0 + events.append(Timing(time: time, closure: closure)) + + if lastEventTime > time { + events.sort{ $0.time < $1.time } + } + + condition.signal() + condition.unlock() + + if firstEventTime > time { + pthread_kill(thread, SIGALRM) + } + } + + let threadSignal: @convention(c) (Int32) -> () = { (sig: Int32) in } + + let entryC: @convention(c) (UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? = { (ptr: UnsafeMutableRawPointer) in + let ptimer: PreciseTimer = MPVHelper.bridge(ptr: ptr) + ptimer.entry() + return nil + } + + func entry() { + signal(SIGALRM, threadSignal) + + while isRunning { + condition.lock() + while events.count == 0 && isRunning { + condition.wait() + } + + if !isRunning { break } + + guard let event = events.first else { + continue + } + condition.unlock() + + mach_wait_until(event.time) + + condition.lock() + if events.first?.time == event.time && isRunning { + event.closure() + events.removeFirst() + } + condition.unlock() + } + } +} diff --git a/osdep/macosx_application.h b/osdep/macosx_application.h index d95940fd3134a..753b9f033fdab 100644 --- a/osdep/macosx_application.h +++ b/osdep/macosx_application.h @@ -26,6 +26,12 @@ enum { FRAME_WHOLE, }; +enum { + RENDER_TIMER_CALLBACK = 0, + RENDER_TIMER_PRECISE, + RENDER_TIMER_SYSTEM, +}; + struct macos_opts { int macos_title_bar_style; int macos_title_bar_appearance; @@ -35,6 +41,7 @@ struct macos_opts { bool macos_force_dedicated_gpu; int macos_app_activation_policy; int macos_geometry_calculation; + int macos_render_timer; int cocoa_cb_sw_renderer; bool cocoa_cb_10bit_context; }; diff --git a/osdep/macosx_application.m b/osdep/macosx_application.m index fc1da01d39542..bf60e05389679 100644 --- a/osdep/macosx_application.m +++ b/osdep/macosx_application.m @@ -67,6 +67,9 @@ {"regular", 0}, {"accessory", 1}, {"prohibited", 2})}, {"macos-geometry-calculation", OPT_CHOICE(macos_geometry_calculation, {"visible", FRAME_VISIBLE}, {"whole", FRAME_WHOLE})}, + {"macos-render-timer", OPT_CHOICE(macos_render_timer, + {"callback", RENDER_TIMER_CALLBACK}, {"precise", RENDER_TIMER_PRECISE}, + {"system", RENDER_TIMER_SYSTEM})}, {"cocoa-cb-sw-renderer", OPT_CHOICE(cocoa_cb_sw_renderer, {"auto", -1}, {"no", 0}, {"yes", 1})}, {"cocoa-cb-10bit-context", OPT_BOOL(cocoa_cb_10bit_context)}, diff --git a/video/out/gpu/context.c b/video/out/gpu/context.c index 5b4987eeef55d..28f88014aa2d0 100644 --- a/video/out/gpu/context.c +++ b/video/out/gpu/context.c @@ -51,6 +51,7 @@ extern const struct ra_ctx_fns ra_ctx_vulkan_win; extern const struct ra_ctx_fns ra_ctx_vulkan_xlib; extern const struct ra_ctx_fns ra_ctx_vulkan_android; extern const struct ra_ctx_fns ra_ctx_vulkan_display; +extern const struct ra_ctx_fns ra_ctx_vulkan_mac; /* Direct3D 11 */ extern const struct ra_ctx_fns ra_ctx_d3d11; @@ -113,6 +114,9 @@ static const struct ra_ctx_fns *contexts[] = { #if HAVE_VK_KHR_DISPLAY &ra_ctx_vulkan_display, #endif +#if HAVE_COCOA && HAVE_SWIFT + &ra_ctx_vulkan_mac, +#endif #endif /* No API contexts: */ diff --git a/video/out/mac/metal_layer.swift b/video/out/mac/metal_layer.swift new file mode 100644 index 0000000000000..7cea87c0b4df8 --- /dev/null +++ b/video/out/mac/metal_layer.swift @@ -0,0 +1,43 @@ +/* + * 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 . + */ + +import Cocoa + +class MetalLayer: CAMetalLayer { + unowned var common: MacCommon + + init(common com: MacCommon) { + common = com + super.init() + + pixelFormat = .rgba16Float + backgroundColor = NSColor.black.cgColor + } + + // necessary for when the layer containing window changes the screen + override init(layer: Any) { + guard let oldLayer = layer as? MetalLayer else { + fatalError("init(layer: Any) passed an invalid layer") + } + common = oldLayer.common + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/video/out/mac_common.swift b/video/out/mac_common.swift new file mode 100644 index 0000000000000..12d2870add504 --- /dev/null +++ b/video/out/mac_common.swift @@ -0,0 +1,177 @@ +/* + * 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 . + */ + +import Cocoa + +class MacCommon: Common { + @objc var layer: MetalLayer? + + var timer: PreciseTimer? + var swapTime: UInt64 = 0 + let swapLock: NSCondition = NSCondition() + + var needsICCUpdate: Bool = false + + @objc init(_ vo: UnsafeMutablePointer) { + let newlog = mp_log_new(vo, vo.pointee.log, "mac") + super.init(newlog) + mpv = MPVHelper(vo, log) + timer = PreciseTimer(common: self) + + DispatchQueue.main.sync { + layer = MetalLayer(common: self) + initMisc(vo) + } + } + + @objc func config(_ vo: UnsafeMutablePointer) -> Bool { + mpv?.vo = vo + + DispatchQueue.main.sync { + let previousActiveApp = getActiveApp() + initApp() + + let (_, _, wr) = getInitProperties(vo) + + guard let layer = self.layer else { + log.sendError("Something went wrong, no MetalLayer was initialized") + exit(1) + } + + if window == nil { + initView(vo, layer) + initWindow(vo, previousActiveApp) + initWindowState() + } + + if !NSEqualSizes(window?.unfsContentFramePixel.size ?? NSZeroSize, wr.size) { + window?.updateSize(wr.size) + } + + windowDidResize() + needsICCUpdate = true + } + + return true + } + + @objc func uninit(_ vo: UnsafeMutablePointer) { + window?.waitForAnimation() + + timer?.terminate() + + DispatchQueue.main.sync { + window?.delegate = nil + window?.close() + + uninitCommon() + } + } + + @objc func swapBuffer() { + if mpv?.macOpts.macos_render_timer ?? Int32(RENDER_TIMER_CALLBACK) != RENDER_TIMER_SYSTEM { + swapLock.lock() + while(swapTime < 1) { + swapLock.wait() + } + swapTime = 0 + swapLock.unlock() + } + + if needsICCUpdate { + needsICCUpdate = false + updateICCProfile() + } + } + + func updateRenderSize(_ size: NSSize) { + mpv?.vo.pointee.dwidth = Int32(size.width) + mpv?.vo.pointee.dheight = Int32(size.height) + flagEvents(VO_EVENT_RESIZE | VO_EVENT_EXPOSE) + } + + override func displayLinkCallback(_ displayLink: CVDisplayLink, + _ inNow: UnsafePointer, + _ inOutputTime: UnsafePointer, + _ flagsIn: CVOptionFlags, + _ flagsOut: UnsafeMutablePointer) -> CVReturn + { + let frameTimer = mpv?.macOpts.macos_render_timer ?? Int32(RENDER_TIMER_CALLBACK) + let signalSwap = { [self] in + swapLock.lock() + swapTime += 1 + swapLock.signal() + swapLock.unlock() + } + + if frameTimer != RENDER_TIMER_SYSTEM { + if let timer = self.timer, frameTimer == RENDER_TIMER_PRECISE { + timer.scheduleAt(time: inOutputTime.pointee.hostTime, closure: signalSwap) + return kCVReturnSuccess + } + + signalSwap() + } + + return kCVReturnSuccess + } + + override func startDisplayLink(_ vo: UnsafeMutablePointer) { + super.startDisplayLink(vo) + timer?.updatePolicy(periodSeconds: 1 / currentFps()) + } + + override func updateDisplaylink() { + super.updateDisplaylink() + timer?.updatePolicy(periodSeconds: 1 / currentFps()) + } + + override func lightSensorUpdate() { + flagEvents(VO_EVENT_AMBIENT_LIGHTING_CHANGED) + } + + @objc override func updateICCProfile() { + guard let colorSpace = window?.screen?.colorSpace else { + log.sendWarning("Couldn't update ICC Profile, no color space available") + return + } + + if #available(macOS 10.11, *) { + layer?.colorspace = colorSpace.cgColorSpace + } + + flagEvents(VO_EVENT_ICC_PROFILE_CHANGED) + } + + override func windowDidResize() { + guard let window = window else { + log.sendWarning("No window available on window resize event") + return + } + + updateRenderSize(window.framePixel.size) + } + + override func windowDidChangeScreenProfile() { + needsICCUpdate = true + } + + override func windowDidChangeBackingProperties() { + layer?.contentsScale = window?.backingScaleFactor ?? 1 + windowDidResize() + } +} diff --git a/video/out/vulkan/common.h b/video/out/vulkan/common.h index 85e6c50f4b491..d006942d41ff0 100644 --- a/video/out/vulkan/common.h +++ b/video/out/vulkan/common.h @@ -22,6 +22,10 @@ #if HAVE_WIN32_DESKTOP #define VK_USE_PLATFORM_WIN32_KHR #endif +#if HAVE_COCOA +#define VK_USE_PLATFORM_MACOS_MVK +#define VK_USE_PLATFORM_METAL_EXT +#endif #include diff --git a/video/out/vulkan/context_mac.m b/video/out/vulkan/context_mac.m new file mode 100644 index 0000000000000..8ac6e169f6e53 --- /dev/null +++ b/video/out/vulkan/context_mac.m @@ -0,0 +1,119 @@ +/* + * 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 "video/out/gpu/context.h" +#include "osdep/macOS_swift.h" + +#include "common.h" +#include "context.h" +#include "utils.h" + +struct priv { + struct mpvk_ctx vk; + MacCommon *vo_mac; +}; + +static void mac_vk_uninit(struct ra_ctx *ctx) +{ + struct priv *p = ctx->priv; + + ra_vk_ctx_uninit(ctx); + mpvk_uninit(&p->vk); + [p->vo_mac uninit:ctx->vo]; +} + +static void mac_vk_swap_buffers(struct ra_ctx *ctx) +{ + struct priv *p = ctx->priv; + [p->vo_mac swapBuffer]; +} + +static bool mac_vk_init(struct ra_ctx *ctx) +{ + struct priv *p = ctx->priv = talloc_zero(ctx, struct priv); + struct mpvk_ctx *vk = &p->vk; + int msgl = ctx->opts.probing ? MSGL_V : MSGL_ERR; + + if (!mpvk_init(vk, ctx, VK_EXT_METAL_SURFACE_EXTENSION_NAME)) + goto error; + + p->vo_mac = [[MacCommon alloc] init:ctx->vo]; + if (!p->vo_mac) + goto error; + + VkMetalSurfaceCreateInfoEXT mac_info = { + .sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK, + .pNext = NULL, + .flags = 0, + .pLayer = p->vo_mac.layer, + }; + + struct ra_vk_ctx_params params = { + .swap_buffers = mac_vk_swap_buffers, + }; + + VkInstance inst = vk->vkinst->instance; + VkResult res = vkCreateMetalSurfaceEXT(inst, &mac_info, NULL, &vk->surface); + if (res != VK_SUCCESS) { + MP_MSG(ctx, msgl, "Failed creating metal surface\n"); + goto error; + } + + if (!ra_vk_ctx_init(ctx, vk, params, VK_PRESENT_MODE_FIFO_KHR)) + goto error; + + return true; +error: + if (p->vo_mac) + [p->vo_mac uninit:ctx->vo]; + return false; +} + +static bool resize(struct ra_ctx *ctx) +{ + return ra_vk_ctx_resize(ctx, ctx->vo->dwidth, ctx->vo->dheight); +} + +static bool mac_vk_reconfig(struct ra_ctx *ctx) +{ + struct priv *p = ctx->priv; + if (![p->vo_mac config:ctx->vo]) + return false; + return true; +} + +static int mac_vk_control(struct ra_ctx *ctx, int *events, int request, void *arg) +{ + struct priv *p = ctx->priv; + int ret = [p->vo_mac control:ctx->vo events:events request:request data:arg]; + + if (*events & VO_EVENT_RESIZE) { + if (!resize(ctx)) + return VO_ERROR; + } + + return ret; +} + +const struct ra_ctx_fns ra_ctx_vulkan_mac = { + .type = "vulkan", + .name = "macvk", + .reconfig = mac_vk_reconfig, + .control = mac_vk_control, + .init = mac_vk_init, + .uninit = mac_vk_uninit, +};