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
26func (wv *WebView) scheduleStartAdaptiveFrameRatePolling() {
27 if wv == nil || !wv.adaptiveWindowlessFrameRate {
28 return
29 }
30 wv.runOnGTK(func() { wv.startAdaptiveFrameRatePolling() })
31}
32
33func (wv *WebView) scheduleStopAdaptiveFrameRatePolling() {
34 if wv == nil {
35 return
36 }
37 wv.runOnGTK(func() { wv.stopAdaptiveFrameRatePolling() })
38}
39
40func (wv *WebView) startAdaptiveFrameRatePolling() {
41 if wv == nil || wv.destroyed.Load() || !wv.adaptiveWindowlessFrameRate || wv.viewBridge == nil || wv.viewBridge.Widget() == nil {
42 return
43 }
44
45 wv.mu.Lock()
46 if wv.adaptiveFrameRatePollID != 0 || wv.host == nil {
47 wv.mu.Unlock()
48 return
49 }
50 cb := new(glib.SourceFunc)
51 *cb = func(uintptr) bool {
52 if wv.destroyed.Load() {
53 return false
54 }
55 wv.applyAdaptiveWindowlessFrameRate()
56 return true
57 }
58 wv.adaptiveFrameRatePoll = cb
59 wv.adaptiveFrameRatePollID = glib.TimeoutAdd(adaptiveFrameRatePollIntervalMS, cb, 0)
60 wv.mu.Unlock()
61
62 wv.applyAdaptiveWindowlessFrameRate()
63}
64
65func (wv *WebView) stopAdaptiveFrameRatePolling() {
66 if wv == nil {
67 return
68 }
69 wv.mu.Lock()
70 pollID := wv.adaptiveFrameRatePollID
71 wv.adaptiveFrameRatePollID = 0
72 wv.adaptiveFrameRatePoll = nil
73 wv.mu.Unlock()
74 if pollID != 0 {
75 glib.SourceRemove(pollID)
76 }
77}
78
79func (wv *WebView) applyAdaptiveWindowlessFrameRate() {
80 if wv == nil || !wv.adaptiveWindowlessFrameRate || wv.viewBridge == nil {
81 return
82 }
83 widget := wv.viewBridge.Widget()
84 refreshRateMilliHz, displayName, ok := widgetMonitorRefreshRateMilliHz(widget)
85 if !ok || !isWaylandDisplayName(displayName) {
86 return
87 }
88 fps := adaptiveFrameRateForRefresh(refreshRateMilliHz, wv.windowlessFrameRate, wv.windowlessFrameRateMax)
89
90 wv.mu.Lock()
91 host := wv.host
92 if host == nil || fps == wv.lastAdaptiveFrameRate {
93 wv.mu.Unlock()
94 return
95 }
96 old := wv.lastAdaptiveFrameRate
97 wv.lastAdaptiveFrameRate = fps
98 wv.mu.Unlock()
99
100 host.SetWindowlessFrameRate(fps)
101 if wv.ctx != nil {
102 logging.FromContext(wv.ctx).Info().
103 Int32("old_windowless_frame_rate", old).
104 Int32("windowless_frame_rate", fps).
105 Int("monitor_refresh_millihz", refreshRateMilliHz).
106 Str("display", displayName).
107 Msg("cef: adaptive windowless frame rate updated")
108 }
109}
110
111func widgetMonitorRefreshRateMilliHz(widget *gtk.Widget) (int, string, bool) {
112 if widget == nil {
113 return 0, "", false
114 }
115 native := widget.GetNative()
116 if native == nil {
117 return 0, "", false
118 }
119 surface := native.GetSurface()
120 if surface == nil {
121 return 0, "", false
122 }
123 display := surface.GetDisplay()
124 if display == nil {
125 return 0, "", false
126 }
127 monitor := display.GetMonitorAtSurface(surface)
128 if monitor == nil {
129 return 0, display.GetName(), false
130 }
131 return monitor.GetRefreshRate(), display.GetName(), true
132}
133
134func isWaylandDisplayName(name string) bool {
135 return strings.Contains(strings.ToLower(name), "wayland")
136}