Skip to content

Commit 7808a9f

Browse files
committed
feat(linux)!: use experimental renderer by default
This commit introduces an experimental offscreen renderer for Linux that decouples UI rendering from video rendering as much as possible to improve UI responsiveness. The new renderer is enabled by default on Linux and can be toggled via the MPVQC_LINUX_EXPERIMENTAL_RENDERER environment variable. Updated Video Rendering: - Windows: unchanged - mpvQC uses winId/HWND embedding as before - Linux: Offscreen renderer (experimental) by default. Set MPVQC_LINUX_EXPERIMENTAL_RENDERER=false to use the old renderer Technical Differences: - Legacy renderer: Renders directly into the UI's framebuffer. Without tweaking other configuration parameters, this has the limitation that the UI is constrained to the framerate of the video file. - Offscreen renderer: Uses two additional framebuffers and renders in a separate thread, independent of the screen's refresh rate: mpv → _render_fbo → _display_fbo → Qt Quick's FBO → screen _render_fbo → _display_fbo: pointer swap _display_fbo → Qt Quick's FBO: blit in render() Audio Delay Compensation: The offscreen renderer decouples video rendering from UI framerate, which means mpv renders frames independently of what's displayed on screen. This can cause audio desynchronization as the audio continues at real-time while video frames may be presented at the display's refresh rate. To address this, mpvQC now automatically adjusts the audio delay based on the current monitor's refresh rate. The delay is calculated as the frame time (1 / refresh_rate) rounded to millisecond precision. The adjustment updates dynamically when the window moves to a different monitor or when the refresh rate changes. Breaking Change: Users upgrading from previous versions of mpvQC on Linux must update their mpv settings via Options -> Edit mpv.conf: - When using the NEW offscreen renderer (default): Remove any video-timing-offset configuration. The new renderer handles timing automatically through audio delay compensation based on the monitor's refresh rate. Having video-timing-offset configured will cause timing issues. - When using the OLD legacy renderer (MPVQC_LINUX_EXPERIMENTAL_RENDERER=false): Keep or add video-timing-offset=0.016 (or adjust based on your monitor's refresh rate). The legacy renderer requires this setting for proper synchronization.
1 parent df0b480 commit 7808a9f

File tree

8 files changed

+400
-10
lines changed

8 files changed

+400
-10
lines changed

data/config/mpv-linux.conf

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@
55
# HQ preset, uses Spline36 for upscaling and Mitchell-Netravali for downscaling
66
profile=gpu-hq
77

8-
# Configure offset to match a frame rate of 60 fps; default 0.050
9-
# More info: https://github.com/mpvqc/mpvQC/issues/26
10-
video-timing-offset=0.016
11-
128
# Debanding is disabled because we don't want to alter the video while doing quality control
139
deband=no
1410

docs/configuration.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,10 @@ mpvQC can be configured using the following environment variables:
2121
- **Operating System:** All
2222
- **Description:** Enables mpv player logging
2323
- **Possible Values:** *not set* or *any value*
24+
25+
- **`MPVQC_LINUX_EXPERIMENTAL_RENDERER`**
26+
27+
- **Default Value:** true
28+
- **Operating System:** Linux
29+
- **Description:** Enables rendering in background thread
30+
- **Possible Values:** *false* to revert back to stable renderer, unset to enable

mpvqc/services/host_integration/__init__.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22
#
33
# SPDX-License-Identifier: GPL-3.0-or-later
44

5+
from __future__ import annotations
6+
57
import platform
68
from dataclasses import dataclass
9+
from typing import TYPE_CHECKING
10+
11+
from PySide6.QtCore import QEvent, QObject, Signal, Slot
12+
from PySide6.QtGui import QScreen
713

8-
from PySide6.QtCore import QEvent, QObject, Signal
14+
if TYPE_CHECKING:
15+
from PySide6.QtGui import QWindow
916

1017

1118
@dataclass(frozen=True)
@@ -28,27 +35,51 @@ class HostIntegrationService(QObject):
2835
DEFAULT_WINDOW_BUTTON_PREFERENCE = WindowButtonPreference(minimize=True, maximize=True, close=True)
2936

3037
display_zoom_factor_changed = Signal(float)
38+
refresh_rate_changed = Signal(float)
3139

3240
def __init__(self):
3341
super().__init__()
3442
self._zoom_factor = get_display_zoom_factor()
43+
self._refresh_rate = get_refresh_rate()
3544

3645
self._zoom_factor_change_listener = ZoomFactorChangeEventListener()
3746
self._zoom_factor_change_listener.device_pixel_ratio_changed.connect(self._invalidate_zoom_factor)
3847

3948
from mpvqc.utility import get_main_window
4049

41-
self._window = get_main_window()
50+
self._window: QWindow = get_main_window()
4251
self._window.installEventFilter(self._zoom_factor_change_listener)
52+
self._window.screenChanged.connect(self._on_screen_changed)
53+
54+
self._screen: QScreen = self._window.screen()
55+
self._screen.refreshRateChanged.connect(self._invalidate_refresh_rate)
56+
57+
@property
58+
def display_zoom_factor(self) -> float:
59+
return self._zoom_factor
4360

61+
@Slot(float)
4462
def _invalidate_zoom_factor(self, *_) -> None:
4563
if (zoom_factor := get_display_zoom_factor()) != self._zoom_factor:
4664
self._zoom_factor = zoom_factor
4765
self.display_zoom_factor_changed.emit(zoom_factor)
4866

4967
@property
50-
def display_zoom_factor(self) -> float:
51-
return self._zoom_factor
68+
def refresh_rate(self) -> float:
69+
return self._refresh_rate
70+
71+
@Slot(QScreen)
72+
def _on_screen_changed(self, screen: QScreen) -> None:
73+
self._screen.refreshRateChanged.disconnect(self._invalidate_refresh_rate)
74+
self._screen = screen
75+
self._screen.refreshRateChanged.connect(self._invalidate_refresh_rate)
76+
self._invalidate_refresh_rate()
77+
78+
@Slot(float)
79+
def _invalidate_refresh_rate(self, *_) -> None:
80+
if (refresh_rate := get_refresh_rate()) != self._refresh_rate:
81+
self._refresh_rate = refresh_rate
82+
self.refresh_rate_changed.emit(refresh_rate)
5283

5384
def get_window_button_preference(self) -> WindowButtonPreference:
5485
match platform.system():
@@ -66,6 +97,12 @@ def get_display_zoom_factor() -> float:
6697
return get_main_window().devicePixelRatio()
6798

6899

100+
def get_refresh_rate() -> float:
101+
from mpvqc.utility import get_main_window
102+
103+
return get_main_window().screen().refreshRate()
104+
105+
69106
def read_linux_window_button_preference() -> WindowButtonPreference:
70107
from mpvqc.services.host_integration.portals import SettingsPortal
71108

mpvqc/services/player.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66

77
import logging
88
import os
9+
import platform
910
from dataclasses import dataclass
1011
from pathlib import Path
1112
from typing import TYPE_CHECKING
1213

1314
import inject
14-
from PySide6.QtCore import QObject, Qt, Signal
15+
from PySide6.QtCore import QObject, Qt, Signal, Slot
1516

1617
from .application_paths import ApplicationPathsService
1718
from .host_integration import HostIntegrationService
@@ -106,6 +107,10 @@ def init(self, win_id: int | None = None):
106107
self._mpv.observe_property("height", self._on_player_height_changed)
107108
self._mpv.observe_property("width", self._on_player_width_changed)
108109

110+
if platform.system() == "Linux" and os.getenv("MPVQC_LINUX_EXPERIMENTAL_RENDERER", "").lower() != "false":
111+
self._host_integration.refresh_rate_changed.connect(self._on_refresh_rate_changed)
112+
self._on_refresh_rate_changed(self._host_integration.refresh_rate)
113+
109114
@property
110115
def mpv(self) -> MPV | None:
111116
return self._mpv
@@ -115,6 +120,11 @@ def _get_mpv_attr(self, attr: str) -> Any | None:
115120
return None
116121
return getattr(self._mpv, attr, None)
117122

123+
def _set_mpv_attr(self, attr: str, value: Any) -> None:
124+
if self._mpv is None:
125+
return
126+
setattr(self._mpv, attr, value)
127+
118128
@property
119129
def mpv_version(self) -> str:
120130
return self._get_mpv_attr("mpv_version") or ""
@@ -173,6 +183,14 @@ def current_time(self) -> int:
173183
def duration(self) -> float:
174184
return self._get_mpv_attr("duration") or 0.0
175185

186+
@property
187+
def is_paused(self) -> bool:
188+
return not self.is_playing
189+
190+
@property
191+
def is_playing(self) -> bool:
192+
return not self._mpv.pause and not self._mpv.idle_active if self._mpv else False
193+
176194
@property
177195
def _track_list(self) -> list[TrackListEntry]:
178196
if self._mpv is None:
@@ -230,6 +248,17 @@ def _on_player_width_changed(self, _, value: int) -> None:
230248
if value is not None:
231249
self.width_changed.emit(value)
232250

251+
@Slot(float)
252+
def _on_refresh_rate_changed(self, new_refresh_rate: float) -> None:
253+
if new_refresh_rate is None:
254+
return
255+
256+
refresh_rate = round(new_refresh_rate, 4)
257+
audio_delay = round(1 / refresh_rate, 4)
258+
259+
logger.info("Adjusting audio_delay to %s due to updated refresh rate %s", audio_delay, refresh_rate)
260+
self._set_mpv_attr("audio_delay", audio_delay)
261+
233262
def move_mouse(self, x: int, y: int) -> None:
234263
zoom_factor = self._host_integration.display_zoom_factor
235264
x = int(x * zoom_factor)

mpvqc/startup.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,19 @@ def perform_startup():
1717

1818

1919
def configure_qt_application_data():
20+
import platform
21+
2022
from PySide6.QtCore import QCoreApplication
2123

2224
QCoreApplication.setApplicationName("mpvQC")
2325
QCoreApplication.setOrganizationName("mpvQC")
2426
QCoreApplication.setApplicationVersion(">>>tag<<<")
2527

28+
if platform.system() == "Linux":
29+
from PySide6.QtGui import QGuiApplication, Qt
30+
31+
QGuiApplication.setAttribute(Qt.ApplicationAttribute.AA_ShareOpenGLContexts)
32+
2633

2734
def configure_qt_style():
2835
from PySide6.QtQuickControls2 import QQuickStyle

mpvqc/views/__init__.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,21 @@
33
# SPDX-License-Identifier: GPL-3.0-or-later
44

55
# ruff: noqa: F401
6-
from .player_framebuffer_object import MpvqcMpvFrameBufferObjectPyObject
6+
7+
import logging
8+
import os
9+
import platform
10+
11+
logger = logging.getLogger(__name__)
12+
13+
if platform.system() == "Linux":
14+
if os.getenv("MPVQC_LINUX_EXPERIMENTAL_RENDERER", "").lower() == "false":
15+
logger.info("Using default renderer. Unset MPVQC_LINUX_EXPERIMENTAL_RENDERER to enable experimental renderer")
16+
from .player_framebuffer_object import MpvqcMpvFrameBufferObjectPyObject
17+
else:
18+
logger.info("Using experimental renderer. Set MPVQC_LINUX_EXPERIMENTAL_RENDERER=false to disable")
19+
from .player_framebuffer_object_offscreen import MpvqcMpvFrameBufferObjectPyObject
20+
else:
21+
from .player_framebuffer_object import MpvqcMpvFrameBufferObjectPyObject
22+
723
from .player_win_id import MpvWindowPyObject

0 commit comments

Comments
 (0)