detached ac5bf21

fix(cef): address adaptive frame review findings

commit
ac5bf2113032bdcb1f202a42a9dbd3d9084dff17
author
brice · · 2026-05-20 12:20 UTC
parent
f0ca3ee
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +159 -10
M docs/reference/configuration.md +1 -1
@@ -62,7 +62,7 @@
6262 | `engine.cef.render_stack` | string | `vulkan` | `vulkan`, `egl` |
6363 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | |
6464 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |
65-| `engine.cef.windowless_frame_rate_max` | int32 | `240` | > 0 |
65+| `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 |
6666 | `default_ui_scale` | float | `1.0` | > 0 |
6767 | `default_webpage_zoom` | float | `1.2` | > 0 |
6868 | `workspace.new_pane_url` | string | `about:blank` | |
@@ -62,7 +62,7 @@
62 | `engine.cef.render_stack` | string | `vulkan` | `vulkan`, `egl` |62 | `engine.cef.render_stack` | string | `vulkan` | `vulkan`, `egl` |
63 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | |63 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | |
64 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |64 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |
65-| `engine.cef.windowless_frame_rate_max` | int32 | `240` | > 0 |65+| `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 |
66 | `default_ui_scale` | float | `1.0` | > 0 |66 | `default_ui_scale` | float | `1.0` | > 0 |
67 | `default_webpage_zoom` | float | `1.2` | > 0 |67 | `default_webpage_zoom` | float | `1.2` | > 0 |
68 | `workspace.new_pane_url` | string | `about:blank` | |68 | `workspace.new_pane_url` | string | `about:blank` | |
M go.mod +1 -1
@@ -7,7 +7,7 @@ require (
77 github.com/andybalholm/brotli v1.2.1
88 github.com/bnema/purego v0.11.0-bnema.2
99 github.com/bnema/purego-cef v0.13.0
10- github.com/bnema/purego-cef2gtk v0.4.0
10+ github.com/bnema/purego-cef2gtk v0.5.0
1111 github.com/bnema/purego-pipewire v0.1.3
1212 github.com/bnema/purego-sqlite v0.1.2
1313 github.com/bnema/puregotk v0.5.2
@@ -7,7 +7,7 @@ require (
7 github.com/andybalholm/brotli v1.2.17 github.com/andybalholm/brotli v1.2.1
8 github.com/bnema/purego v0.11.0-bnema.28 github.com/bnema/purego v0.11.0-bnema.2
9 github.com/bnema/purego-cef v0.13.09 github.com/bnema/purego-cef v0.13.0
10- github.com/bnema/purego-cef2gtk v0.4.010+ github.com/bnema/purego-cef2gtk v0.5.0
11 github.com/bnema/purego-pipewire v0.1.311 github.com/bnema/purego-pipewire v0.1.3
12 github.com/bnema/purego-sqlite v0.1.212 github.com/bnema/purego-sqlite v0.1.2
13 github.com/bnema/puregotk v0.5.213 github.com/bnema/puregotk v0.5.2
M go.sum +2 -2 generated file: collapsed
M internal/infrastructure/cef/adaptive_frame_rate.go +5 -2
@@ -10,12 +10,15 @@ import (
1010 "github.com/bnema/dumber/internal/logging"
1111 )
1212
13-const adaptiveFrameRatePollIntervalMS = 1000
13+const (
14+ adaptiveFrameRatePollIntervalMS = 1000
15+ adaptiveFrameRateMinFPS = 30
16+)
1417
1518 func adaptiveFrameRateForRefresh(refreshRateMilliHz int, fallbackFPS, maxFPS int32) int32 {
1619 return cef2gtk.WindowlessFrameRateForMonitorRefresh(refreshRateMilliHz, cef2gtk.RefreshRateOptions{
1720 DefaultFPS: fallbackFPS,
18- MinFPS: fallbackFPS,
21+ MinFPS: adaptiveFrameRateMinFPS,
1922 MaxFPS: maxFPS,
2023 })
2124 }
@@ -10,12 +10,15 @@ import (
10 "github.com/bnema/dumber/internal/logging"10 "github.com/bnema/dumber/internal/logging"
11 )11 )
12 12
13-const adaptiveFrameRatePollIntervalMS = 100013+const (
14+ adaptiveFrameRatePollIntervalMS = 1000
15+ adaptiveFrameRateMinFPS = 30
16+)
14 17
15 func adaptiveFrameRateForRefresh(refreshRateMilliHz int, fallbackFPS, maxFPS int32) int32 {18 func adaptiveFrameRateForRefresh(refreshRateMilliHz int, fallbackFPS, maxFPS int32) int32 {
16 return cef2gtk.WindowlessFrameRateForMonitorRefresh(refreshRateMilliHz, cef2gtk.RefreshRateOptions{19 return cef2gtk.WindowlessFrameRateForMonitorRefresh(refreshRateMilliHz, cef2gtk.RefreshRateOptions{
17 DefaultFPS: fallbackFPS,20 DefaultFPS: fallbackFPS,
18- MinFPS: fallbackFPS,21+ MinFPS: adaptiveFrameRateMinFPS,
19 MaxFPS: maxFPS,22 MaxFPS: maxFPS,
20 })23 })
21 }24 }
M internal/infrastructure/cef/adaptive_frame_rate_test.go +2 -0
@@ -12,6 +12,8 @@ func TestAdaptiveFrameRateForRefresh(t *testing.T) {
1212 }{
1313 {name: "fallback unknown", refreshMilliHz: 0, fallbackFPS: 60, maxFPS: 240, want: 60},
1414 {name: "uses monitor refresh", refreshMilliHz: 144000, fallbackFPS: 60, maxFPS: 240, want: 144},
15+ {name: "uses lower refresh monitor", refreshMilliHz: 50000, fallbackFPS: 60, maxFPS: 240, want: 50},
16+ {name: "floors very low refresh monitor", refreshMilliHz: 24000, fallbackFPS: 60, maxFPS: 240, want: 30},
1517 {name: "hard caps extreme monitor", refreshMilliHz: 500000, fallbackFPS: 60, maxFPS: 240, want: 240},
1618 }
1719
@@ -12,6 +12,8 @@ func TestAdaptiveFrameRateForRefresh(t *testing.T) {
12 }{12 }{
13 {name: "fallback unknown", refreshMilliHz: 0, fallbackFPS: 60, maxFPS: 240, want: 60},13 {name: "fallback unknown", refreshMilliHz: 0, fallbackFPS: 60, maxFPS: 240, want: 60},
14 {name: "uses monitor refresh", refreshMilliHz: 144000, fallbackFPS: 60, maxFPS: 240, want: 144},14 {name: "uses monitor refresh", refreshMilliHz: 144000, fallbackFPS: 60, maxFPS: 240, want: 144},
15+ {name: "uses lower refresh monitor", refreshMilliHz: 50000, fallbackFPS: 60, maxFPS: 240, want: 50},
16+ {name: "floors very low refresh monitor", refreshMilliHz: 24000, fallbackFPS: 60, maxFPS: 240, want: 30},
15 {name: "hard caps extreme monitor", refreshMilliHz: 500000, fallbackFPS: 60, maxFPS: 240, want: 240},17 {name: "hard caps extreme monitor", refreshMilliHz: 500000, fallbackFPS: 60, maxFPS: 240, want: 240},
16 }18 }
17 19
M internal/infrastructure/cef/app.go +2 -0
@@ -219,6 +219,7 @@ func appendUniqueCommaSeparatedSwitchValues(commandLine purecef.CommandLine, nam
219219 if len(combined) == 0 {
220220 return
221221 }
222+ commandLine.RemoveSwitch(name)
222223 commandLine.AppendSwitchWithValue(name, strings.Join(combined, ","))
223224 }
224225
@@ -261,6 +262,7 @@ func removeCommaSeparatedSwitchValues(commandLine purecef.CommandLine, name stri
261262 return
262263 }
263264
265+ commandLine.RemoveSwitch(name)
264266 commandLine.AppendSwitchWithValue(name, strings.Join(cleaned, ","))
265267 }
266268
@@ -219,6 +219,7 @@ func appendUniqueCommaSeparatedSwitchValues(commandLine purecef.CommandLine, nam
219 if len(combined) == 0 {219 if len(combined) == 0 {
220 return220 return
221 }221 }
222+ commandLine.RemoveSwitch(name)
222 commandLine.AppendSwitchWithValue(name, strings.Join(combined, ","))223 commandLine.AppendSwitchWithValue(name, strings.Join(combined, ","))
223 }224 }
224 225
@@ -261,6 +262,7 @@ func removeCommaSeparatedSwitchValues(commandLine purecef.CommandLine, name stri
261 return262 return
262 }263 }
263 264
265+ commandLine.RemoveSwitch(name)
264 commandLine.AppendSwitchWithValue(name, strings.Join(cleaned, ","))266 commandLine.AppendSwitchWithValue(name, strings.Join(cleaned, ","))
265 }267 }
266 268
M internal/infrastructure/config/cef_frame_rate_test.go +40 -0
@@ -32,3 +32,43 @@ func TestCEFAdaptiveWindowlessFrameRateDisabledByExplicitFrameRate(t *testing.T)
3232 t.Fatalf("explicit frame-rate = %d, want 144", got)
3333 }
3434 }
35+
36+func TestCEFWindowlessFrameRateFallbackWhenAdaptiveDisabled(t *testing.T) {
37+ cfg := CEFEngineConfig{AdaptiveWindowlessFrameRate: false, WindowlessFrameRate: 0}
38+
39+ if cfg.CEFAdaptiveWindowlessFrameRate() {
40+ t.Fatal("adaptive should be disabled when adaptive flag is false")
41+ }
42+ if got := cfg.CEFWindowlessFrameRate(); got != defaultCEFWindowlessFrameRate {
43+ t.Fatalf("frame-rate = %d, want default %d", got, defaultCEFWindowlessFrameRate)
44+ }
45+}
46+
47+func TestCEFWindowlessFrameRateUsesExplicitValueWhenAdaptiveDisabled(t *testing.T) {
48+ cfg := CEFEngineConfig{AdaptiveWindowlessFrameRate: false, WindowlessFrameRate: 75}
49+
50+ if cfg.CEFAdaptiveWindowlessFrameRate() {
51+ t.Fatal("adaptive should remain disabled with explicit frame-rate")
52+ }
53+ if got := cfg.CEFWindowlessFrameRate(); got != 75 {
54+ t.Fatalf("frame-rate = %d, want explicit 75", got)
55+ }
56+}
57+
58+func TestCEFWindowlessFrameRateBoundaries(t *testing.T) {
59+ tests := []struct {
60+ name string
61+ cfg CEFEngineConfig
62+ want int32
63+ }{
64+ {name: "negative falls back", cfg: CEFEngineConfig{WindowlessFrameRate: -1}, want: defaultCEFWindowlessFrameRate},
65+ {name: "above max remains explicit", cfg: CEFEngineConfig{WindowlessFrameRate: 300, WindowlessFrameRateMax: 240}, want: 300},
66+ }
67+ for _, tt := range tests {
68+ t.Run(tt.name, func(t *testing.T) {
69+ if got := tt.cfg.CEFWindowlessFrameRate(); got != tt.want {
70+ t.Fatalf("CEFWindowlessFrameRate() = %d, want %d", got, tt.want)
71+ }
72+ })
73+ }
74+}
@@ -32,3 +32,43 @@ func TestCEFAdaptiveWindowlessFrameRateDisabledByExplicitFrameRate(t *testing.T)
32 t.Fatalf("explicit frame-rate = %d, want 144", got)32 t.Fatalf("explicit frame-rate = %d, want 144", got)
33 }33 }
34 }34 }
35+
36+func TestCEFWindowlessFrameRateFallbackWhenAdaptiveDisabled(t *testing.T) {
37+ cfg := CEFEngineConfig{AdaptiveWindowlessFrameRate: false, WindowlessFrameRate: 0}
38+
39+ if cfg.CEFAdaptiveWindowlessFrameRate() {
40+ t.Fatal("adaptive should be disabled when adaptive flag is false")
41+ }
42+ if got := cfg.CEFWindowlessFrameRate(); got != defaultCEFWindowlessFrameRate {
43+ t.Fatalf("frame-rate = %d, want default %d", got, defaultCEFWindowlessFrameRate)
44+ }
45+}
46+
47+func TestCEFWindowlessFrameRateUsesExplicitValueWhenAdaptiveDisabled(t *testing.T) {
48+ cfg := CEFEngineConfig{AdaptiveWindowlessFrameRate: false, WindowlessFrameRate: 75}
49+
50+ if cfg.CEFAdaptiveWindowlessFrameRate() {
51+ t.Fatal("adaptive should remain disabled with explicit frame-rate")
52+ }
53+ if got := cfg.CEFWindowlessFrameRate(); got != 75 {
54+ t.Fatalf("frame-rate = %d, want explicit 75", got)
55+ }
56+}
57+
58+func TestCEFWindowlessFrameRateBoundaries(t *testing.T) {
59+ tests := []struct {
60+ name string
61+ cfg CEFEngineConfig
62+ want int32
63+ }{
64+ {name: "negative falls back", cfg: CEFEngineConfig{WindowlessFrameRate: -1}, want: defaultCEFWindowlessFrameRate},
65+ {name: "above max remains explicit", cfg: CEFEngineConfig{WindowlessFrameRate: 300, WindowlessFrameRateMax: 240}, want: 300},
66+ }
67+ for _, tt := range tests {
68+ t.Run(tt.name, func(t *testing.T) {
69+ if got := tt.cfg.CEFWindowlessFrameRate(); got != tt.want {
70+ t.Fatalf("CEFWindowlessFrameRate() = %d, want %d", got, tt.want)
71+ }
72+ })
73+ }
74+}
M internal/infrastructure/config/defaults.go +1 -1
@@ -69,7 +69,7 @@ const (
6969 // Performance defaults
7070 defaultZoomCacheSize = 256 // domains to cache (~20KB memory)
7171 defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup
72- defaultCEFWindowlessFrameRate = 60 // fallback OSR frame rate for CEF
72+ defaultCEFWindowlessFrameRate = 60 // static OSR frame rate when adaptive CEF pacing is disabled
7373 defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap
7474
7575 // Skia threading defaults (0 = unset, -1 = unset for GPU threads)
@@ -69,7 +69,7 @@ const (
69 // Performance defaults69 // Performance defaults
70 defaultZoomCacheSize = 256 // domains to cache (~20KB memory)70 defaultZoomCacheSize = 256 // domains to cache (~20KB memory)
71 defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup71 defaultWebViewPoolPrewarmCount = 4 // WebViews to pre-create at startup
72- defaultCEFWindowlessFrameRate = 60 // fallback OSR frame rate for CEF72+ defaultCEFWindowlessFrameRate = 60 // static OSR frame rate when adaptive CEF pacing is disabled
73 defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap73 defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap
74 74
75 // Skia threading defaults (0 = unset, -1 = unset for GPU threads)75 // Skia threading defaults (0 = unset, -1 = unset for GPU threads)
M internal/infrastructure/config/engine.go +3 -3
@@ -103,13 +103,13 @@ type CEFEngineConfig struct {
103103 // AdaptiveWindowlessFrameRate polls the active Wayland monitor refresh rate
104104 // and updates CEF's OSR frame-rate while the browser moves across monitors.
105105 // It is ignored when WindowlessFrameRate is explicitly set.
106- AdaptiveWindowlessFrameRate bool `mapstructure:"adaptive_windowless_frame_rate" toml:"adaptive_windowless_frame_rate" yaml:"adaptive_windowless_frame_rate"`
106+ AdaptiveWindowlessFrameRate bool `mapstructure:"adaptive_windowless_frame_rate" toml:"adaptive_windowless_frame_rate" yaml:"adaptive_windowless_frame_rate"` //nolint:lll // struct tags exceed lll limit
107107 // WindowlessFrameRate is the maximum frame rate for off-screen rendering.
108108 // When 0 and adaptive_windowless_frame_rate is true, Dumber adapts to the
109109 // active Wayland monitor refresh rate.
110- WindowlessFrameRate int32 `mapstructure:"windowless_frame_rate" toml:"windowless_frame_rate" yaml:"windowless_frame_rate"`
110+ WindowlessFrameRate int32 `mapstructure:"windowless_frame_rate" toml:"windowless_frame_rate" yaml:"windowless_frame_rate"` //nolint:lll // struct tags exceed lll limit
111111 // WindowlessFrameRateMax is the hard cap for adaptive monitor refresh rates.
112- WindowlessFrameRateMax int32 `mapstructure:"windowless_frame_rate_max" toml:"windowless_frame_rate_max" yaml:"windowless_frame_rate_max"`
112+ WindowlessFrameRateMax int32 `mapstructure:"windowless_frame_rate_max" toml:"windowless_frame_rate_max" yaml:"windowless_frame_rate_max"` //nolint:lll // struct tags exceed lll limit
113113 // EnableAudioHandler opts into the experimental CEF AudioHandler bridge.
114114 EnableAudioHandler bool `mapstructure:"enable_audio_handler" toml:"enable_audio_handler" yaml:"enable_audio_handler"`
115115 // TraceHandlers enables purego-cef handler/refcount tracing for diagnostics.
@@ -103,13 +103,13 @@ type CEFEngineConfig struct {
103 // AdaptiveWindowlessFrameRate polls the active Wayland monitor refresh rate103 // AdaptiveWindowlessFrameRate polls the active Wayland monitor refresh rate
104 // and updates CEF's OSR frame-rate while the browser moves across monitors.104 // and updates CEF's OSR frame-rate while the browser moves across monitors.
105 // It is ignored when WindowlessFrameRate is explicitly set.105 // It is ignored when WindowlessFrameRate is explicitly set.
106- AdaptiveWindowlessFrameRate bool `mapstructure:"adaptive_windowless_frame_rate" toml:"adaptive_windowless_frame_rate" yaml:"adaptive_windowless_frame_rate"`106+ AdaptiveWindowlessFrameRate bool `mapstructure:"adaptive_windowless_frame_rate" toml:"adaptive_windowless_frame_rate" yaml:"adaptive_windowless_frame_rate"` //nolint:lll // struct tags exceed lll limit
107 // WindowlessFrameRate is the maximum frame rate for off-screen rendering.107 // WindowlessFrameRate is the maximum frame rate for off-screen rendering.
108 // When 0 and adaptive_windowless_frame_rate is true, Dumber adapts to the108 // When 0 and adaptive_windowless_frame_rate is true, Dumber adapts to the
109 // active Wayland monitor refresh rate.109 // active Wayland monitor refresh rate.
110- WindowlessFrameRate int32 `mapstructure:"windowless_frame_rate" toml:"windowless_frame_rate" yaml:"windowless_frame_rate"`110+ WindowlessFrameRate int32 `mapstructure:"windowless_frame_rate" toml:"windowless_frame_rate" yaml:"windowless_frame_rate"` //nolint:lll // struct tags exceed lll limit
111 // WindowlessFrameRateMax is the hard cap for adaptive monitor refresh rates.111 // WindowlessFrameRateMax is the hard cap for adaptive monitor refresh rates.
112- WindowlessFrameRateMax int32 `mapstructure:"windowless_frame_rate_max" toml:"windowless_frame_rate_max" yaml:"windowless_frame_rate_max"`112+ WindowlessFrameRateMax int32 `mapstructure:"windowless_frame_rate_max" toml:"windowless_frame_rate_max" yaml:"windowless_frame_rate_max"` //nolint:lll // struct tags exceed lll limit
113 // EnableAudioHandler opts into the experimental CEF AudioHandler bridge.113 // EnableAudioHandler opts into the experimental CEF AudioHandler bridge.
114 EnableAudioHandler bool `mapstructure:"enable_audio_handler" toml:"enable_audio_handler" yaml:"enable_audio_handler"`114 EnableAudioHandler bool `mapstructure:"enable_audio_handler" toml:"enable_audio_handler" yaml:"enable_audio_handler"`
115 // TraceHandlers enables purego-cef handler/refcount tracing for diagnostics.115 // TraceHandlers enables purego-cef handler/refcount tracing for diagnostics.
M internal/infrastructure/config/transform_legacy.go +46 -0
@@ -54,6 +54,52 @@ func (*LegacyConfigTransformer) TransformLegacyEngineConfig(rawConfig map[string
5454 }
5555
5656 delete(cef, "enable_context_menu_handler")
57+ migrateLegacyCEFWindowlessFrameRateDefault(cef)
58+}
59+
60+func migrateLegacyCEFWindowlessFrameRateDefault(cef map[string]any) {
61+ if _, hasAdaptive := cef["adaptive_windowless_frame_rate"]; hasAdaptive {
62+ return
63+ }
64+ frameRate, ok := int64ConfigValue(cef["windowless_frame_rate"])
65+ if !ok || frameRate != defaultCEFWindowlessFrameRate {
66+ return
67+ }
68+ cef["adaptive_windowless_frame_rate"] = true
69+ cef["windowless_frame_rate"] = 0
70+ if _, hasMax := cef["windowless_frame_rate_max"]; !hasMax {
71+ cef["windowless_frame_rate_max"] = defaultCEFWindowlessFrameRateMax
72+ }
73+}
74+
75+func int64ConfigValue(value any) (int64, bool) {
76+ switch v := value.(type) {
77+ case int:
78+ return int64(v), true
79+ case int8:
80+ return int64(v), true
81+ case int16:
82+ return int64(v), true
83+ case int32:
84+ return int64(v), true
85+ case int64:
86+ return v, true
87+ case uint:
88+ return int64(v), true
89+ case uint8:
90+ return int64(v), true
91+ case uint16:
92+ return int64(v), true
93+ case uint32:
94+ return int64(v), true
95+ case uint64:
96+ if v > 1<<63-1 {
97+ return 0, false
98+ }
99+ return int64(v), true
100+ default:
101+ return 0, false
102+ }
57103 }
58104
59105 func (t *LegacyConfigTransformer) transformActionSection(rawConfig map[string]any, path []string) {
@@ -54,6 +54,52 @@ func (*LegacyConfigTransformer) TransformLegacyEngineConfig(rawConfig map[string
54 }54 }
55 55
56 delete(cef, "enable_context_menu_handler")56 delete(cef, "enable_context_menu_handler")
57+ migrateLegacyCEFWindowlessFrameRateDefault(cef)
58+}
59+
60+func migrateLegacyCEFWindowlessFrameRateDefault(cef map[string]any) {
61+ if _, hasAdaptive := cef["adaptive_windowless_frame_rate"]; hasAdaptive {
62+ return
63+ }
64+ frameRate, ok := int64ConfigValue(cef["windowless_frame_rate"])
65+ if !ok || frameRate != defaultCEFWindowlessFrameRate {
66+ return
67+ }
68+ cef["adaptive_windowless_frame_rate"] = true
69+ cef["windowless_frame_rate"] = 0
70+ if _, hasMax := cef["windowless_frame_rate_max"]; !hasMax {
71+ cef["windowless_frame_rate_max"] = defaultCEFWindowlessFrameRateMax
72+ }
73+}
74+
75+func int64ConfigValue(value any) (int64, bool) {
76+ switch v := value.(type) {
77+ case int:
78+ return int64(v), true
79+ case int8:
80+ return int64(v), true
81+ case int16:
82+ return int64(v), true
83+ case int32:
84+ return int64(v), true
85+ case int64:
86+ return v, true
87+ case uint:
88+ return int64(v), true
89+ case uint8:
90+ return int64(v), true
91+ case uint16:
92+ return int64(v), true
93+ case uint32:
94+ return int64(v), true
95+ case uint64:
96+ if v > 1<<63-1 {
97+ return 0, false
98+ }
99+ return int64(v), true
100+ default:
101+ return 0, false
102+ }
57 }103 }
58 104
59 func (t *LegacyConfigTransformer) transformActionSection(rawConfig map[string]any, path []string) {105 func (t *LegacyConfigTransformer) transformActionSection(rawConfig map[string]any, path []string) {
M internal/infrastructure/config/transform_legacy_test.go +56 -0
@@ -188,6 +188,62 @@ func TestTransformLegacyEngineConfig_RemovesDeprecatedContextMenuHandler(t *test
188188 assert.False(t, exists)
189189 }
190190
191+func TestTransformLegacyEngineConfig_MigratesLegacyCEFWindowlessFrameRateDefault(t *testing.T) {
192+ transformer := NewLegacyConfigTransformer()
193+ rawConfig := map[string]any{
194+ "engine": map[string]any{
195+ "cef": map[string]any{
196+ "windowless_frame_rate": 60,
197+ },
198+ },
199+ }
200+
201+ transformer.TransformLegacyEngineConfig(rawConfig)
202+
203+ cef := rawConfig["engine"].(map[string]any)["cef"].(map[string]any)
204+ assert.Equal(t, true, cef["adaptive_windowless_frame_rate"])
205+ assert.Equal(t, 0, cef["windowless_frame_rate"])
206+ assert.Equal(t, defaultCEFWindowlessFrameRateMax, cef["windowless_frame_rate_max"])
207+}
208+
209+func TestTransformLegacyEngineConfig_PreservesExplicitCEFWindowlessFrameRate(t *testing.T) {
210+ transformer := NewLegacyConfigTransformer()
211+ rawConfig := map[string]any{
212+ "engine": map[string]any{
213+ "cef": map[string]any{
214+ "adaptive_windowless_frame_rate": false,
215+ "windowless_frame_rate": 60,
216+ },
217+ },
218+ }
219+
220+ transformer.TransformLegacyEngineConfig(rawConfig)
221+
222+ cef := rawConfig["engine"].(map[string]any)["cef"].(map[string]any)
223+ assert.Equal(t, false, cef["adaptive_windowless_frame_rate"])
224+ assert.Equal(t, 60, cef["windowless_frame_rate"])
225+ _, hasMax := cef["windowless_frame_rate_max"]
226+ assert.False(t, hasMax)
227+}
228+
229+func TestTransformLegacyEngineConfig_PreservesNonDefaultCEFWindowlessFrameRate(t *testing.T) {
230+ transformer := NewLegacyConfigTransformer()
231+ rawConfig := map[string]any{
232+ "engine": map[string]any{
233+ "cef": map[string]any{
234+ "windowless_frame_rate": 144,
235+ },
236+ },
237+ }
238+
239+ transformer.TransformLegacyEngineConfig(rawConfig)
240+
241+ cef := rawConfig["engine"].(map[string]any)["cef"].(map[string]any)
242+ _, hasAdaptive := cef["adaptive_windowless_frame_rate"]
243+ assert.False(t, hasAdaptive)
244+ assert.Equal(t, 144, cef["windowless_frame_rate"])
245+}
246+
191247 func TestTransformLegacyEngineConfig_MissingCefSection(t *testing.T) {
192248 transformer := NewLegacyConfigTransformer()
193249 rawConfig := map[string]any{
@@ -188,6 +188,62 @@ func TestTransformLegacyEngineConfig_RemovesDeprecatedContextMenuHandler(t *test
188 assert.False(t, exists)188 assert.False(t, exists)
189 }189 }
190 190
191+func TestTransformLegacyEngineConfig_MigratesLegacyCEFWindowlessFrameRateDefault(t *testing.T) {
192+ transformer := NewLegacyConfigTransformer()
193+ rawConfig := map[string]any{
194+ "engine": map[string]any{
195+ "cef": map[string]any{
196+ "windowless_frame_rate": 60,
197+ },
198+ },
199+ }
200+
201+ transformer.TransformLegacyEngineConfig(rawConfig)
202+
203+ cef := rawConfig["engine"].(map[string]any)["cef"].(map[string]any)
204+ assert.Equal(t, true, cef["adaptive_windowless_frame_rate"])
205+ assert.Equal(t, 0, cef["windowless_frame_rate"])
206+ assert.Equal(t, defaultCEFWindowlessFrameRateMax, cef["windowless_frame_rate_max"])
207+}
208+
209+func TestTransformLegacyEngineConfig_PreservesExplicitCEFWindowlessFrameRate(t *testing.T) {
210+ transformer := NewLegacyConfigTransformer()
211+ rawConfig := map[string]any{
212+ "engine": map[string]any{
213+ "cef": map[string]any{
214+ "adaptive_windowless_frame_rate": false,
215+ "windowless_frame_rate": 60,
216+ },
217+ },
218+ }
219+
220+ transformer.TransformLegacyEngineConfig(rawConfig)
221+
222+ cef := rawConfig["engine"].(map[string]any)["cef"].(map[string]any)
223+ assert.Equal(t, false, cef["adaptive_windowless_frame_rate"])
224+ assert.Equal(t, 60, cef["windowless_frame_rate"])
225+ _, hasMax := cef["windowless_frame_rate_max"]
226+ assert.False(t, hasMax)
227+}
228+
229+func TestTransformLegacyEngineConfig_PreservesNonDefaultCEFWindowlessFrameRate(t *testing.T) {
230+ transformer := NewLegacyConfigTransformer()
231+ rawConfig := map[string]any{
232+ "engine": map[string]any{
233+ "cef": map[string]any{
234+ "windowless_frame_rate": 144,
235+ },
236+ },
237+ }
238+
239+ transformer.TransformLegacyEngineConfig(rawConfig)
240+
241+ cef := rawConfig["engine"].(map[string]any)["cef"].(map[string]any)
242+ _, hasAdaptive := cef["adaptive_windowless_frame_rate"]
243+ assert.False(t, hasAdaptive)
244+ assert.Equal(t, 144, cef["windowless_frame_rate"])
245+}
246+
191 func TestTransformLegacyEngineConfig_MissingCefSection(t *testing.T) {247 func TestTransformLegacyEngineConfig_MissingCefSection(t *testing.T) {
192 transformer := NewLegacyConfigTransformer()248 transformer := NewLegacyConfigTransformer()
193 rawConfig := map[string]any{249 rawConfig := map[string]any{

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close