Skip to content

Commit affd6e2

Browse files
committed
feat(macos): refactor Metal renderer to use afterMinimumDuration with dispatch_semaphore for improved frame pacing
- All Macs should now be able to stream with buttery smoothness when Vsync is enabled. - Updated settings allowing choice of renderer, Metal frames in flight 2 or 3. - Option to show Apple's Metal Performance HUD when viewing stats. - Improved VRR on external displays such as LG TVs. VRR is auto-detected when in borderless, and delivers frames at the same cadence as they were captured on the host, within the available VRR range. You should set Sunshine's Minimum FPS Target to 20 or less to use VRR. This requires accurate frame capture timestamps, and won't work with Mac Sunshine and some Linux backends. - Add displaylink_source vsync timing module for use by the frame pacer. - ProMotion displays are treated as fixed 120hz and won't try to use VRR. - NTSC 59.94 (available on MBP built-in displays) now tells Sunshine to only stream 59.94fps. Important: Some of these features require a recent Sunshine nightly, and probably won't work with Apollo. If you run into dropped frames due to network jitter, try running in borderless fullscreen. I'm planning to port the Display-Locked frame pacer from Xbox which I hope will be a better fix for this issue.
1 parent 2e9fbec commit affd6e2

19 files changed

+1159
-294
lines changed

app/app.pro

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,11 +396,14 @@ macx {
396396
message(VideoToolbox renderer selected)
397397

398398
SOURCES += \
399+
streaming/streamutils_mac.mm \
400+
streaming/video/ffmpeg-renderers/pacer/displaylink_source.mm \
399401
streaming/video/ffmpeg-renderers/vt_base.mm \
400402
streaming/video/ffmpeg-renderers/vt_avsamplelayer.mm \
401403
streaming/video/ffmpeg-renderers/vt_metal.mm
402404

403405
HEADERS += \
406+
streaming/video/ffmpeg-renderers/pacer/displaylink_source.h \
404407
streaming/video/ffmpeg-renderers/vt.h
405408
}
406409
discord-rpc {
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import QtQuick 2.9
2+
import QtQuick.Controls 2.2
3+
import QtQuick.Layouts 1.2
4+
5+
import StreamingPreferences 1.0
6+
import SystemProperties 1.0
7+
8+
Column {
9+
id: root
10+
width: parent ? parent.width : 400
11+
spacing: 8
12+
13+
property var languageChangedSignal
14+
15+
visible: SystemProperties.hasDesktopEnvironment
16+
17+
function reinitializeWindowMode() {
18+
if (!windowModeComboBox.visible) {
19+
return
20+
}
21+
22+
windowModeComboBox.model = windowModeComboBox.createModel()
23+
windowModeComboBox.currentIndex = 0
24+
25+
var savedWm = StreamingPreferences.windowMode
26+
for (var i = 0; i < windowModeComboBox.model.count; i++) {
27+
var thisWm = windowModeComboBox.model.get(i).val
28+
if (savedWm === thisWm) {
29+
windowModeComboBox.currentIndex = i
30+
break
31+
}
32+
}
33+
34+
windowModeComboBox.activated(windowModeComboBox.currentIndex)
35+
}
36+
37+
GridLayout {
38+
id: grid
39+
width: parent.width
40+
columns: 2
41+
columnSpacing: 10
42+
rowSpacing: 8
43+
44+
Label {
45+
text: qsTr("Renderer")
46+
font.pointSize: 12
47+
wrapMode: Text.Wrap
48+
Layout.fillWidth: true
49+
}
50+
51+
Label {
52+
text: qsTr("Renderer options")
53+
font.pointSize: 12
54+
wrapMode: Text.Wrap
55+
Layout.fillWidth: true
56+
visible: Qt.platform.os === "osx"
57+
}
58+
59+
AutoResizingComboBox {
60+
id: rendererComboBox
61+
Layout.fillWidth: true
62+
textRole: "text"
63+
visible: Qt.platform.os === "osx"
64+
65+
model: ListModel {
66+
id: rendererListModel
67+
68+
ListElement {
69+
text: qsTr("Metal (recommended)")
70+
val: StreamingPreferences.RENDERER_VT_METAL
71+
}
72+
73+
ListElement {
74+
text: qsTr("AVSampleBuffer")
75+
val: StreamingPreferences.RENDERER_AVSAMPLEBUFFER
76+
}
77+
}
78+
79+
Component.onCompleted: {
80+
if (!visible) {
81+
return
82+
}
83+
84+
currentIndex = 0
85+
for (var i = 0; i < rendererListModel.count; i++) {
86+
if (StreamingPreferences.renderer === rendererListModel.get(i).val) {
87+
currentIndex = i
88+
break
89+
}
90+
}
91+
92+
activated(currentIndex)
93+
}
94+
95+
onActivated: {
96+
StreamingPreferences.renderer = rendererListModel.get(currentIndex).val
97+
}
98+
99+
ToolTip.delay: 1000
100+
ToolTip.timeout: 5000
101+
ToolTip.visible: hovered
102+
ToolTip.text: qsTr("Choose the macOS renderer backend.")
103+
}
104+
105+
AutoResizingComboBox {
106+
id: rendererOptionsComboBox
107+
Layout.fillWidth: true
108+
textRole: "text"
109+
visible: Qt.platform.os === "osx"
110+
enabled: !rendererComboBox.visible ||
111+
StreamingPreferences.renderer === StreamingPreferences.RENDERER_VT_METAL
112+
113+
model: ListModel {
114+
id: rendererOptionsListModel
115+
116+
ListElement {
117+
text: qsTr("Frames in flight: 3 (recommended)")
118+
val: 3
119+
}
120+
121+
ListElement {
122+
text: qsTr("Frames in flight: 2")
123+
val: 2
124+
}
125+
}
126+
127+
Component.onCompleted: {
128+
if (!visible) {
129+
return
130+
}
131+
132+
currentIndex = 0
133+
for (var i = 0; i < rendererOptionsListModel.count; i++) {
134+
if (StreamingPreferences.vtMetalFramesInFlight === rendererOptionsListModel.get(i).val) {
135+
currentIndex = i
136+
break
137+
}
138+
}
139+
140+
activated(currentIndex)
141+
}
142+
143+
onActivated: {
144+
StreamingPreferences.vtMetalFramesInFlight = rendererOptionsListModel.get(currentIndex).val
145+
}
146+
147+
ToolTip.delay: 1000
148+
ToolTip.timeout: 5000
149+
ToolTip.visible: hovered
150+
ToolTip.text: qsTr("Sets the maximum number of frames in flight for the Metal CPU to GPU pipeline. 2 frames has lower latency. 3 frames is often more stable at high frame rates.")
151+
}
152+
153+
Label {
154+
text: qsTr("Display mode")
155+
font.pointSize: 12
156+
wrapMode: Text.Wrap
157+
Layout.fillWidth: true
158+
}
159+
160+
Item {
161+
Layout.fillWidth: true
162+
Layout.preferredHeight: metalHudCheck.visible ? metalHudCheck.implicitHeight : 0
163+
164+
CheckBox {
165+
id: metalHudCheck
166+
anchors.left: parent.left
167+
anchors.verticalCenter: parent.verticalCenter
168+
169+
text: qsTr("Show Metal Performance HUD")
170+
font.pointSize: 12
171+
visible: Qt.platform.os === "osx" &&
172+
(!rendererComboBox.visible ||
173+
StreamingPreferences.renderer === StreamingPreferences.RENDERER_VT_METAL)
174+
checked: StreamingPreferences.showMetalPerformanceHud
175+
onCheckedChanged: {
176+
StreamingPreferences.showMetalPerformanceHud = checked
177+
}
178+
179+
ToolTip.delay: 1000
180+
ToolTip.timeout: 5000
181+
ToolTip.visible: hovered
182+
ToolTip.text: qsTr("Display Apple's Metal Performance HUD when viewing stats. Use Shift-F10 to cycle additional views.")
183+
}
184+
}
185+
186+
AutoResizingComboBox {
187+
id: windowModeComboBox
188+
Layout.fillWidth: true
189+
enabled: !SystemProperties.rendererAlwaysFullScreen
190+
visible: SystemProperties.hasDesktopEnvironment
191+
hoverEnabled: true
192+
textRole: "text"
193+
194+
function createModel() {
195+
var model = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', root, '')
196+
197+
if (Qt.platform.os !== "osx") {
198+
model.append({
199+
text: qsTr("Fullscreen"),
200+
val: StreamingPreferences.WM_FULLSCREEN
201+
})
202+
}
203+
204+
model.append({
205+
text: qsTr("Borderless windowed"),
206+
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
207+
})
208+
209+
model.append({
210+
text: qsTr("Windowed"),
211+
val: StreamingPreferences.WM_WINDOWED
212+
})
213+
214+
for (var i = 0; i < model.count; i++) {
215+
var thisWm = model.get(i).val
216+
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
217+
model.get(i).text += " " + qsTr("(Recommended)")
218+
model.move(i, 0, 1)
219+
break
220+
}
221+
}
222+
223+
return model
224+
}
225+
226+
Component.onCompleted: {
227+
reinitializeWindowMode()
228+
229+
if (languageChangedSignal) {
230+
languageChangedSignal.connect(reinitializeWindowMode)
231+
}
232+
}
233+
234+
onActivated: {
235+
StreamingPreferences.windowMode = model.get(currentIndex).val
236+
}
237+
238+
ToolTip.delay: 1000
239+
ToolTip.timeout: 5000
240+
ToolTip.visible: hovered
241+
ToolTip.text: Qt.platform.os === "osx"
242+
? qsTr("Borderless windowed generally provides the best performance. When used with an external VRR display, VRR will be enabled automatically.")
243+
: qsTr("Fullscreen generally provides the best performance, but borderless windowed may work better with features like Alt+Tab, screenshot tools, and overlays.")
244+
}
245+
246+
Item {
247+
Layout.fillWidth: true
248+
}
249+
250+
CheckBox {
251+
id: vsyncCheck
252+
Layout.columnSpan: 2
253+
Layout.fillWidth: true
254+
hoverEnabled: true
255+
text: qsTr("V-Sync")
256+
font.pointSize: 12
257+
checked: StreamingPreferences.enableVsync
258+
enabled: Qt.platform.os === "osx"
259+
? (StreamingPreferences.windowMode === StreamingPreferences.WM_FULLSCREEN_DESKTOP)
260+
: true
261+
262+
onCheckedChanged: {
263+
StreamingPreferences.enableVsync = checked
264+
}
265+
266+
ToolTip.delay: 1000
267+
ToolTip.timeout: 5000
268+
ToolTip.visible: hovered
269+
ToolTip.text: Qt.platform.os === "osx"
270+
? qsTr("V-Sync is always enabled on macOS in windowed mode and when using VRR. It can be disabled in borderless mode.")
271+
: qsTr("Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing")
272+
}
273+
274+
CheckBox {
275+
id: framePacingCheck
276+
Layout.columnSpan: 2
277+
Layout.fillWidth: true
278+
hoverEnabled: true
279+
text: Qt.platform.os === "osx" ? qsTr("Frame pacing (always enabled)") : qsTr("Frame pacing")
280+
font.pointSize: 12
281+
enabled: Qt.platform.os === "osx" ? false : StreamingPreferences.enableVsync
282+
checked: Qt.platform.os === "osx" || (StreamingPreferences.enableVsync && StreamingPreferences.framePacing)
283+
284+
onCheckedChanged: {
285+
StreamingPreferences.framePacing = checked
286+
}
287+
288+
ToolTip.delay: 1000
289+
ToolTip.timeout: 5000
290+
ToolTip.visible: hovered
291+
ToolTip.text: Qt.platform.os === "osx"
292+
? qsTr("Frame pacing is always enabled on macOS")
293+
: qsTr("Frame pacing reduces micro-stutter by delaying frames that come in too early")
294+
}
295+
}
296+
}

0 commit comments

Comments
 (0)