fix(cef): address adaptive frame review findings
- commit
- ac5bf2113032bdcb1f202a42a9dbd3d9084dff17
- author
- brice · · 2026-05-20 12:20 UTC
- parent
- f0ca3ee
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +159 -10
M docs/reference/configuration.md +1 -1
| @@ -62,7 +62,7 @@ | |||
| 62 | 62 | | `engine.cef.render_stack` | string | `vulkan` | `vulkan`, `egl` | | |
| 63 | 63 | | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | | | |
| 64 | 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 | 66 | | `default_ui_scale` | float | `1.0` | > 0 | | |
| 67 | 67 | | `default_webpage_zoom` | float | `1.2` | > 0 | | |
| 68 | 68 | | `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 ( | |||
| 7 | 7 | github.com/andybalholm/brotli v1.2.1 | |
| 8 | 8 | github.com/bnema/purego v0.11.0-bnema.2 | |
| 9 | 9 | 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 | |
| 11 | 11 | github.com/bnema/purego-pipewire v0.1.3 | |
| 12 | 12 | github.com/bnema/purego-sqlite v0.1.2 | |
| 13 | 13 | github.com/bnema/puregotk v0.5.2 | |
| @@ -7,7 +7,7 @@ require ( | |||||
| 7 | github.com/andybalholm/brotli v1.2.1 | 7 | github.com/andybalholm/brotli v1.2.1 | ||
| 8 | github.com/bnema/purego v0.11.0-bnema.2 | 8 | github.com/bnema/purego v0.11.0-bnema.2 | ||
| 9 | github.com/bnema/purego-cef v0.13.0 | 9 | 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 |
| 11 | github.com/bnema/purego-pipewire v0.1.3 | 11 | github.com/bnema/purego-pipewire v0.1.3 | ||
| 12 | github.com/bnema/purego-sqlite v0.1.2 | 12 | github.com/bnema/purego-sqlite v0.1.2 | ||
| 13 | github.com/bnema/puregotk v0.5.2 | 13 | github.com/bnema/puregotk v0.5.2 | ||
M go.sum +2 -2
| @@ -14,8 +14,8 @@ github.com/bnema/purego v0.11.0-bnema.2 h1:7pzcdOJ8bGtLrCz6V582oyleP6css1ICQoGgj | |||
| 14 | 14 | github.com/bnema/purego v0.11.0-bnema.2/go.mod h1:5BnOIUj+0fvk/vwv45iottFoGfkDT7cnLXnpM2s6Hng= | |
| 15 | 15 | github.com/bnema/purego-cef v0.13.0 h1:TNPY19mBOqlJeFi5CZCLJIDfAgTEMp9tviQEyZgnvJI= | |
| 16 | 16 | github.com/bnema/purego-cef v0.13.0/go.mod h1:AIW4lrXr4f2vesO4WnMDN49yAyfivYHV4x6hxpu5SmM= | |
| 17 | - | github.com/bnema/purego-cef2gtk v0.4.0 h1:emUiblQffMWumfaRo/Ib8NYHtga10mliIXSrhdxFkmE= | |
| 18 | - | github.com/bnema/purego-cef2gtk v0.4.0/go.mod h1:xDVdLTepKdPngYlwT/BscGHh5ohaCB4LLUeThW/Ml7c= | |
| 17 | + | github.com/bnema/purego-cef2gtk v0.5.0 h1:qRCVQ/p5mnBDfKLCmD94OnzL6uGu5EPsJxvJ7CTR59Y= | |
| 18 | + | github.com/bnema/purego-cef2gtk v0.5.0/go.mod h1:xDVdLTepKdPngYlwT/BscGHh5ohaCB4LLUeThW/Ml7c= | |
| 19 | 19 | github.com/bnema/purego-pipewire v0.1.3 h1:A3jYkuMe1KseJ8yr5Am1iLvRFu6yDWM2AwUIpl88460= | |
| 20 | 20 | github.com/bnema/purego-pipewire v0.1.3/go.mod h1:kchW7Bkfjc+NkX2IZw3LnlYfQxk6jxhl32Id88u04y8= | |
| 21 | 21 | github.com/bnema/purego-sqlite v0.1.2 h1:dhqfe3KfNRBh7tlrwxL7bf4FPZk2VCajdBESMbrvmoE= | |
| @@ -14,8 +14,8 @@ github.com/bnema/purego v0.11.0-bnema.2 h1:7pzcdOJ8bGtLrCz6V582oyleP6css1ICQoGgj | |||||
| 14 | github.com/bnema/purego v0.11.0-bnema.2/go.mod h1:5BnOIUj+0fvk/vwv45iottFoGfkDT7cnLXnpM2s6Hng= | 14 | github.com/bnema/purego v0.11.0-bnema.2/go.mod h1:5BnOIUj+0fvk/vwv45iottFoGfkDT7cnLXnpM2s6Hng= | ||
| 15 | github.com/bnema/purego-cef v0.13.0 h1:TNPY19mBOqlJeFi5CZCLJIDfAgTEMp9tviQEyZgnvJI= | 15 | github.com/bnema/purego-cef v0.13.0 h1:TNPY19mBOqlJeFi5CZCLJIDfAgTEMp9tviQEyZgnvJI= | ||
| 16 | github.com/bnema/purego-cef v0.13.0/go.mod h1:AIW4lrXr4f2vesO4WnMDN49yAyfivYHV4x6hxpu5SmM= | 16 | github.com/bnema/purego-cef v0.13.0/go.mod h1:AIW4lrXr4f2vesO4WnMDN49yAyfivYHV4x6hxpu5SmM= | ||
| 17 | - | github.com/bnema/purego-cef2gtk v0.4.0 h1:emUiblQffMWumfaRo/Ib8NYHtga10mliIXSrhdxFkmE= | 17 | + | github.com/bnema/purego-cef2gtk v0.5.0 h1:qRCVQ/p5mnBDfKLCmD94OnzL6uGu5EPsJxvJ7CTR59Y= |
| 18 | - | github.com/bnema/purego-cef2gtk v0.4.0/go.mod h1:xDVdLTepKdPngYlwT/BscGHh5ohaCB4LLUeThW/Ml7c= | 18 | + | github.com/bnema/purego-cef2gtk v0.5.0/go.mod h1:xDVdLTepKdPngYlwT/BscGHh5ohaCB4LLUeThW/Ml7c= |
| 19 | github.com/bnema/purego-pipewire v0.1.3 h1:A3jYkuMe1KseJ8yr5Am1iLvRFu6yDWM2AwUIpl88460= | 19 | github.com/bnema/purego-pipewire v0.1.3 h1:A3jYkuMe1KseJ8yr5Am1iLvRFu6yDWM2AwUIpl88460= | ||
| 20 | github.com/bnema/purego-pipewire v0.1.3/go.mod h1:kchW7Bkfjc+NkX2IZw3LnlYfQxk6jxhl32Id88u04y8= | 20 | github.com/bnema/purego-pipewire v0.1.3/go.mod h1:kchW7Bkfjc+NkX2IZw3LnlYfQxk6jxhl32Id88u04y8= | ||
| 21 | github.com/bnema/purego-sqlite v0.1.2 h1:dhqfe3KfNRBh7tlrwxL7bf4FPZk2VCajdBESMbrvmoE= | 21 | github.com/bnema/purego-sqlite v0.1.2 h1:dhqfe3KfNRBh7tlrwxL7bf4FPZk2VCajdBESMbrvmoE= | ||
M internal/infrastructure/cef/adaptive_frame_rate.go +5 -2
| @@ -10,12 +10,15 @@ import ( | |||
| 10 | 10 | "github.com/bnema/dumber/internal/logging" | |
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | - | const adaptiveFrameRatePollIntervalMS = 1000 | |
| 13 | + | const ( | |
| 14 | + | adaptiveFrameRatePollIntervalMS = 1000 | |
| 15 | + | adaptiveFrameRateMinFPS = 30 | |
| 16 | + | ) | |
| 14 | 17 | ||
| 15 | 18 | func adaptiveFrameRateForRefresh(refreshRateMilliHz int, fallbackFPS, maxFPS int32) int32 { | |
| 16 | 19 | return cef2gtk.WindowlessFrameRateForMonitorRefresh(refreshRateMilliHz, cef2gtk.RefreshRateOptions{ | |
| 17 | 20 | DefaultFPS: fallbackFPS, | |
| 18 | - | MinFPS: fallbackFPS, | |
| 21 | + | MinFPS: adaptiveFrameRateMinFPS, | |
| 19 | 22 | MaxFPS: maxFPS, | |
| 20 | 23 | }) | |
| 21 | 24 | } | |
| @@ -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 = 1000 | 13 | + | 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) { | |||
| 12 | 12 | }{ | |
| 13 | 13 | {name: "fallback unknown", refreshMilliHz: 0, fallbackFPS: 60, maxFPS: 240, want: 60}, | |
| 14 | 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 | 17 | {name: "hard caps extreme monitor", refreshMilliHz: 500000, fallbackFPS: 60, maxFPS: 240, want: 240}, | |
| 16 | 18 | } | |
| 17 | 19 | ||
| @@ -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 | |||
| 219 | 219 | if len(combined) == 0 { | |
| 220 | 220 | return | |
| 221 | 221 | } | |
| 222 | + | commandLine.RemoveSwitch(name) | |
| 222 | 223 | commandLine.AppendSwitchWithValue(name, strings.Join(combined, ",")) | |
| 223 | 224 | } | |
| 224 | 225 | ||
| @@ -261,6 +262,7 @@ func removeCommaSeparatedSwitchValues(commandLine purecef.CommandLine, name stri | |||
| 261 | 262 | return | |
| 262 | 263 | } | |
| 263 | 264 | ||
| 265 | + | commandLine.RemoveSwitch(name) | |
| 264 | 266 | commandLine.AppendSwitchWithValue(name, strings.Join(cleaned, ",")) | |
| 265 | 267 | } | |
| 266 | 268 | ||
| @@ -219,6 +219,7 @@ func appendUniqueCommaSeparatedSwitchValues(commandLine purecef.CommandLine, nam | |||||
| 219 | if len(combined) == 0 { | 219 | if len(combined) == 0 { | ||
| 220 | return | 220 | 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 | return | 262 | 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) | |||
| 32 | 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 | + | } | |
| @@ -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 ( | |||
| 69 | 69 | // Performance defaults | |
| 70 | 70 | defaultZoomCacheSize = 256 // domains to cache (~20KB memory) | |
| 71 | 71 | 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 | |
| 73 | 73 | defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap | |
| 74 | 74 | ||
| 75 | 75 | // Skia threading defaults (0 = unset, -1 = unset for GPU threads) | |
| @@ -69,7 +69,7 @@ const ( | |||||
| 69 | // Performance defaults | 69 | // 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 startup | 71 | 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 |
| 73 | defaultCEFWindowlessFrameRateMax = 240 // adaptive OSR frame-rate hard cap | 73 | 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 { | |||
| 103 | 103 | // AdaptiveWindowlessFrameRate polls the active Wayland monitor refresh rate | |
| 104 | 104 | // and updates CEF's OSR frame-rate while the browser moves across monitors. | |
| 105 | 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 | 107 | // WindowlessFrameRate is the maximum frame rate for off-screen rendering. | |
| 108 | 108 | // When 0 and adaptive_windowless_frame_rate is true, Dumber adapts to the | |
| 109 | 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 | 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 | 113 | // EnableAudioHandler opts into the experimental CEF AudioHandler bridge. | |
| 114 | 114 | EnableAudioHandler bool `mapstructure:"enable_audio_handler" toml:"enable_audio_handler" yaml:"enable_audio_handler"` | |
| 115 | 115 | // TraceHandlers enables purego-cef handler/refcount tracing for diagnostics. | |
| @@ -103,13 +103,13 @@ type CEFEngineConfig struct { | |||||
| 103 | // AdaptiveWindowlessFrameRate polls the active Wayland monitor refresh rate | 103 | // 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 the | 108 | // 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 | |||
| 54 | 54 | } | |
| 55 | 55 | ||
| 56 | 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 | 105 | 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 | |||
| 188 | 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 | 247 | func TestTransformLegacyEngineConfig_MissingCefSection(t *testing.T) { | |
| 192 | 248 | transformer := NewLegacyConfigTransformer() | |
| 193 | 249 | 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{ | ||