1package cef
2
3import (
4 "strings"
5
6 cef2gtk "github.com/bnema/purego-cef2gtk"
7 "github.com/bnema/puregotk/v4/glib"
8 "github.com/bnema/puregotk/v4/gtk"
9
10 "github.com/bnema/dumber/internal/logging"
11)
12
13const (
14 adaptiveFrameRatePollIntervalMS = 1000
15 adaptiveFrameRateMinFPS = 30
16)
17
18func adaptiveFrameRateForRefresh(refreshRateMilliHz int, fallbackFPS, maxFPS int32) int32 {
19 return cef2gtk.WindowlessFrameRateForMonitorRefresh(refreshRateMilliHz, cef2gtk.RefreshRateOptions{
20 DefaultFPS: fallbackFPS,
21 MinFPS: adaptiveFrameRateMinFPS,
22 MaxFPS: maxFPS,
23 })
24}
25
26// resolveWindowlessFrameRate applies a pinned rate on top of the configured OSR
27// frame rate. A pinned rate wins over both the configured value and the adaptive
28// monitor polling, which is suspended so the pinned cadence is what CEF keeps.
29func resolveWindowlessFrameRate(configRate int32, adaptive bool) (int32, bool) {
30 if pinned, ok := windowlessFrameRateOverride(); ok {
31 return pinned, false
32 }
33 return normalizedWindowlessFrameRate(configRate), adaptive
34}
35
36func (wv *WebView) scheduleStartAdaptiveFrameRatePolling() {
37 if wv == nil || !wv.adaptiveWindowlessFrameRate {
38 return
39 }
40 wv.runOnGTK(func() { wv.startAdaptiveFrameRatePolling() })
41}
42
43func (wv *WebView) scheduleStopAdaptiveFrameRatePolling() {
44 if wv == nil {
45 return
46 }
47 wv.runOnGTK(func() { wv.stopAdaptiveFrameRatePolling() })
48}
49
50func (wv *WebView) startAdaptiveFrameRatePolling() {
51 if wv == nil || wv.destroyed.Load() || !wv.adaptiveWindowlessFrameRate || wv.viewBridge == nil || wv.viewBridge.Widget() == nil {
52 return
53 }
54
55 wv.mu.Lock()
56 if wv.adaptiveFrameRatePollID != 0 || wv.host == nil {
57 wv.mu.Unlock()
58 return
59 }
60 cb := new(glib.SourceFunc)
61 *cb = func(uintptr) bool {
62 if wv.destroyed.Load() {
63 return false
64 }
65 wv.applyAdaptiveWindowlessFrameRate()
66 return true
67 }
68 wv.adaptiveFrameRatePoll = cb
69 wv.adaptiveFrameRatePollID = glib.TimeoutAdd(adaptiveFrameRatePollIntervalMS, cb, 0)
70 wv.mu.Unlock()
71
72 wv.applyAdaptiveWindowlessFrameRate()
73}
74
75func (wv *WebView) stopAdaptiveFrameRatePolling() {
76 if wv == nil {
77 return
78 }
79 wv.mu.Lock()
80 pollID := wv.adaptiveFrameRatePollID
81 wv.adaptiveFrameRatePollID = 0
82 wv.adaptiveFrameRatePoll = nil
83 wv.mu.Unlock()
84 if pollID != 0 {
85 glib.SourceRemove(pollID)
86 }
87}
88
89func (wv *WebView) applyAdaptiveWindowlessFrameRate() {
90 if wv == nil || !wv.adaptiveWindowlessFrameRate || wv.viewBridge == nil {
91 return
92 }
93 widget := wv.viewBridge.Widget()
94 refreshRateMilliHz, displayName, ok := widgetMonitorRefreshRateMilliHz(widget)
95 if !ok || !isWaylandDisplayName(displayName) {
96 return
97 }
98 fps := adaptiveFrameRateForRefresh(refreshRateMilliHz, wv.windowlessFrameRate, wv.windowlessFrameRateMax)
99
100 wv.mu.Lock()
101 host := wv.host
102 if host == nil || fps == wv.lastAdaptiveFrameRate {
103 wv.mu.Unlock()
104 return
105 }
106 old := wv.lastAdaptiveFrameRate
107 wv.lastAdaptiveFrameRate = fps
108 wv.mu.Unlock()
109
110 host.SetWindowlessFrameRate(fps)
111 if wv.ctx != nil {
112 logging.FromContext(wv.ctx).Info().
113 Int32("old_windowless_frame_rate", old).
114 Int32("windowless_frame_rate", fps).
115 Int("monitor_refresh_millihz", refreshRateMilliHz).
116 Str("display", displayName).
117 Msg("cef: adaptive windowless frame rate updated")
118 }
119}
120
121func widgetMonitorRefreshRateMilliHz(widget *gtk.Widget) (int, string, bool) {
122 if widget == nil {
123 return 0, "", false
124 }
125 native := widget.GetNative()
126 if native == nil {
127 return 0, "", false
128 }
129 surface := native.GetSurface()
130 if surface == nil {
131 return 0, "", false
132 }
133 display := surface.GetDisplay()
134 if display == nil {
135 return 0, "", false
136 }
137 monitor := display.GetMonitorAtSurface(surface)
138 if monitor == nil {
139 return 0, display.GetName(), false
140 }
141 return monitor.GetRefreshRate(), display.GetName(), true
142}
143
144func isWaylandDisplayName(name string) bool {
145 return strings.Contains(strings.ToLower(name), "wayland")
146}