fix(macos): stop CPU spin when display sleeps with video paused

When the display/system sleeps with a video paused, the window server
stops completing CAMetalLayer presents and mpv's MoltenVK swapchain wedges,
pinning one core at 100% until the app exits. occlusionState does not
reliably change on display sleep, so observe NSWorkspace screensDidSleep/
Wake to hide the metal layer (and drop the playback activity) while dark,
restoring on wake with a forced redraw when paused to avoid a stale frame.

Pairs with MPVKit v1.0.10, which gates libmpv presentation on the layer's
hidden state (check_visible) so presents actually stop while hidden.
This commit is contained in:
edde746
2026-07-09 11:56:13 +02:00
parent 574b8d524f
commit 71d644c4ae
7 changed files with 97 additions and 17 deletions
+1 -1
View File
@@ -891,7 +891,7 @@
repositoryURL = "https://github.com/edde746/MPVKit";
requirement = {
kind = exactVersion;
version = 1.0.9;
version = 1.0.10;
};
};
/* End XCRemoteSwiftPackageReference section */
@@ -13,8 +13,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit",
"state" : {
"revision" : "206ba4a9f8f8191f18cabfdc025bfa512729af58",
"version" : "1.0.9"
"revision" : "bdc3a651c17c01d3985828bdc07b716786992259",
"version" : "1.0.10"
}
},
{
@@ -13,8 +13,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit",
"state" : {
"revision" : "206ba4a9f8f8191f18cabfdc025bfa512729af58",
"version" : "1.0.9"
"revision" : "bdc3a651c17c01d3985828bdc07b716786992259",
"version" : "1.0.10"
}
},
{
+87 -7
View File
@@ -8,8 +8,14 @@ class MpvPlayerCore: MpvPlayerCoreBase {
private weak var window: NSWindow?
private var playbackActivity: NSObjectProtocol?
private var layerHiddenForOcclusion = false
private var layerHiddenForScreenSleep = false
private var isDisposed = false
/// True while any reason (occlusion, screen sleep) requires the layer hidden.
private var hasLayerHideReason: Bool {
layerHiddenForOcclusion || layerHiddenForScreenSleep
}
func initialize(in window: NSWindow) -> Bool {
guard !isInitialized else {
print("[MpvPlayerCore] Already initialized")
@@ -73,6 +79,24 @@ class MpvPlayerCore: MpvPlayerCoreBase {
object: window
)
// Display/system sleep does not reliably change occlusionState, so observe
// NSWorkspace screen sleep/wake directly to gate presentation (prevents a
// pinned CPU core after long display sleep). This is a DIFFERENT
// notification center than NotificationCenter.default used above.
let workspaceCenter = NSWorkspace.shared.notificationCenter
workspaceCenter.addObserver(
self,
selector: #selector(screensDidSleep),
name: NSWorkspace.screensDidSleepNotification,
object: nil
)
workspaceCenter.addObserver(
self,
selector: #selector(screensDidWake),
name: NSWorkspace.screensDidWakeNotification,
object: nil
)
isInitialized = true
print("[MpvPlayerCore] Initialized successfully with MPV")
return true
@@ -107,8 +131,9 @@ class MpvPlayerCore: MpvPlayerCoreBase {
if visible && isVisible && !shouldRestoreOnWindowVisible {
isBackgrounded = false
if metalLayer?.isHidden == true {
if metalLayer?.isHidden == true && !hasLayerHideReason {
setMetalLayerHidden(false)
redrawIfPausedAndVisible()
}
beginPlaybackActivity()
print("[MpvPlayerCore] setVisible(true) skipped - already visible")
@@ -132,7 +157,10 @@ class MpvPlayerCore: MpvPlayerCoreBase {
endPlaybackActivity()
}
setMetalLayerHidden(!visible)
setMetalLayerHidden(!visible || hasLayerHideReason)
if visible {
redrawIfPausedAndVisible()
}
print("[MpvPlayerCore] setVisible(\(visible), restoreOnWindowVisible: \(restoreOnWindowVisible))")
}
@@ -189,6 +217,7 @@ class MpvPlayerCore: MpvPlayerCoreBase {
endPlaybackActivity()
NotificationCenter.default.removeObserver(self)
NSWorkspace.shared.notificationCenter.removeObserver(self)
disposeSharedState(destroySynchronously: false)
metalLayer?.removeFromSuperlayer()
@@ -224,10 +253,13 @@ class MpvPlayerCore: MpvPlayerCoreBase {
} else if windowVisible && layerHiddenForOcclusion {
print("[MpvPlayerCore] Window visible - showing Metal layer")
layerHiddenForOcclusion = false
if shouldRestoreOnWindowVisible {
restoreMetalLayerAfterOcclusion()
} else {
setMetalLayerHidden(!isVisible)
if !layerHiddenForScreenSleep {
if shouldRestoreOnWindowVisible {
restoreMetalLayerAfterOcclusion()
} else {
setMetalLayerHidden(!isVisible)
}
redrawIfPausedAndVisible()
}
isBackgrounded = false
if !pausedState {
@@ -236,6 +268,54 @@ class MpvPlayerCore: MpvPlayerCoreBase {
}
}
@objc private func screensDidSleep(_ notification: Notification) {
guard metalLayer != nil, mpv != nil, !layerHiddenForScreenSleep else { return }
print("[MpvPlayerCore] Screens did sleep - hiding Metal layer")
layerHiddenForScreenSleep = true
// Hide even during PiP: nothing is visible while the displays are dark, and
// the hidden layer is what gates libmpv presentation (MPVKit >= 1.0.10).
setMetalLayerHidden(true)
isBackgrounded = true
endPlaybackActivity()
}
@objc private func screensDidWake(_ notification: Notification) {
guard metalLayer != nil, mpv != nil, layerHiddenForScreenSleep else { return }
print("[MpvPlayerCore] Screens did wake - restoring Metal layer")
layerHiddenForScreenSleep = false
if isPipActive {
// Layer is hosted by the PiP window; just unhide it there. Attach/frame
// logic is owned by the PiP controller.
setMetalLayerHidden(false)
isBackgrounded = false
} else if !layerHiddenForOcclusion {
if shouldRestoreOnWindowVisible {
restoreMetalLayerAfterOcclusion()
} else {
setMetalLayerHidden(!isVisible)
}
isBackgrounded = !isVisible
}
// else: window still occluded; windowOcclusionDidChange owns the restore.
if !layerHiddenForOcclusion && !pausedState {
beginPlaybackActivity()
}
redrawIfPausedAndVisible()
}
/// With MPVKit >= 1.0.10, frames produced while the layer was hidden are
/// skipped at the swapchain, so a paused video would otherwise show a stale
/// frame after unhiding (upstream regression mpv#16693). Playing video
/// repaints itself on the next frame; only paused needs a forced draw. The
/// isHidden guard makes multi-path wake ordering safe only the path that
/// actually unhides the layer triggers the single redraw.
private func redrawIfPausedAndVisible() {
guard pausedState, metalLayer?.isHidden == false else { return }
forceDraw()
}
private func beginPlaybackActivity() {
guard playbackActivity == nil else { return }
playbackActivity = ProcessInfo.processInfo.beginActivity(
@@ -267,7 +347,7 @@ class MpvPlayerCore: MpvPlayerCoreBase {
}
isVisible = true
shouldRestoreOnWindowVisible = false
setMetalLayerHidden(false)
setMetalLayerHidden(hasLayerHideReason)
}
private func attachMetalLayer(to superlayer: CALayer, frame: CGRect) {