detached f52de71

Merge pull request #256 from bnema/feature/cef-render-stack-config

feat(cef): add render stack config
commit
f52de71984015d2118d3c2fe89bde76bf21d2d1c
author
bnema · · 2026-05-20 04:02 UTC
committer
GitHub · · 2026-05-20 04:02 UTC
merge of
9db7462 + 8bc6a41 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
25 files · +220 -27
M README.md +1 -1
@@ -101,7 +101,7 @@ CEF_DIR=/custom/path dumber browse
101101
102102 Then set `engine.type = "cef"` in your config.
103103
104-CEF uses Dumber's GPU-first Wayland render stack by default: GDK DMABUF presentation with ANGLE/GSK Vulkan. You normally do not need render environment variables or helper scripts. For diagnostics only, set `DUMBER_RENDER_STACK=legacy-gl` to use the older GtkGLArea/OpenGL bridge, or `DUMBER_RENDER_STACK=vulkan-dmabuf` to force the default explicitly.
104+CEF uses Dumber's GPU-first Wayland render stack by default: GDK DMABUF presentation with ANGLE/GSK Vulkan. For driver compatibility, switch to the EGL/OpenGL stack with `engine.cef.render_stack = "egl"`; the default is `"vulkan"`.
105105
106106 ## Keyboard-Driven
107107
@@ -101,7 +101,7 @@ CEF_DIR=/custom/path dumber browse
101 101
102 Then set `engine.type = "cef"` in your config.102 Then set `engine.type = "cef"` in your config.
103 103
104-CEF uses Dumber's GPU-first Wayland render stack by default: GDK DMABUF presentation with ANGLE/GSK Vulkan. You normally do not need render environment variables or helper scripts. For diagnostics only, set `DUMBER_RENDER_STACK=legacy-gl` to use the older GtkGLArea/OpenGL bridge, or `DUMBER_RENDER_STACK=vulkan-dmabuf` to force the default explicitly.104+CEF uses Dumber's GPU-first Wayland render stack by default: GDK DMABUF presentation with ANGLE/GSK Vulkan. For driver compatibility, switch to the EGL/OpenGL stack with `engine.cef.render_stack = "egl"`; the default is `"vulkan"`.
105 105
106 ## Keyboard-Driven106 ## Keyboard-Driven
107 107
M cmd/dumber/main.go +1 -1
@@ -328,7 +328,7 @@ func applyCEFRenderStackDefault(ctx context.Context, cfg *config.Config) {
328328 return
329329 }
330330 logger := logging.NewPortLogger(ctx)
331- infracef.ApplyDefaultRenderStackEnvironment(logger)
331+ infracef.ApplyConfiguredRenderStackEnvironment(string(cfg.Engine.CEF.CEFRenderStack()), logger)
332332 infracef.ApplyDefaultHardwareDecodeEnvironment(ctx, infracef.HardwareDecodeEnvironmentOptions{
333333 EngineType: cfg.Engine.ResolveEngineType(),
334334 HardwareDecodingDisabled: cfg.Media.HardwareDecodingMode == config.HardwareDecodingDisable,
@@ -328,7 +328,7 @@ func applyCEFRenderStackDefault(ctx context.Context, cfg *config.Config) {
328 return328 return
329 }329 }
330 logger := logging.NewPortLogger(ctx)330 logger := logging.NewPortLogger(ctx)
331- infracef.ApplyDefaultRenderStackEnvironment(logger)331+ infracef.ApplyConfiguredRenderStackEnvironment(string(cfg.Engine.CEF.CEFRenderStack()), logger)
332 infracef.ApplyDefaultHardwareDecodeEnvironment(ctx, infracef.HardwareDecodeEnvironmentOptions{332 infracef.ApplyDefaultHardwareDecodeEnvironment(ctx, infracef.HardwareDecodeEnvironmentOptions{
333 EngineType: cfg.Engine.ResolveEngineType(),333 EngineType: cfg.Engine.ResolveEngineType(),
334 HardwareDecodingDisabled: cfg.Media.HardwareDecodingMode == config.HardwareDecodingDisable,334 HardwareDecodingDisabled: cfg.Media.HardwareDecodingMode == config.HardwareDecodingDisable,
M docs/config/index.md +3 -2
@@ -189,6 +189,7 @@ border = "#363636"
189189 | `rendering.show_fps` | bool | `false` | - | Show WebKit FPS counter (`WEBKIT_SHOW_FPS`) |
190190 | `rendering.sample_memory` | bool | `false` | - | Enable WebKit memory sampling (`WEBKIT_SAMPLE_MEMORY`) |
191191 | `rendering.debug_frames` | bool | `false` | - | Enable GTK frame timing debug (`GDK_DEBUG=frames`) |
192+| `engine.cef.render_stack` | string | `"vulkan"` | `vulkan`, `egl` | CEF GPU render stack |
192193 | `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |
193194 | `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |
194195
@@ -691,8 +692,8 @@ DUMBER_DATABASE_PATH=/custom/path/db.sqlite
691692
692693 # Rendering (examples)
693694 # CEF defaults to the GPU-first Vulkan DMABUF stack.
694-# Use legacy-gl only as a diagnostic fallback.
695-# DUMBER_RENDER_STACK=legacy-gl
695+# Use EGL/OpenGL for driver compatibility:
696+# DUMBER_ENGINE_CEF_RENDER_STACK=egl
696697 DUMBER_RENDERING_MODE=cpu
697698 # WebKit-specific rendering overrides:
698699 # DUMBER_RENDERING_DISABLE_DMABUF_RENDERER=true
@@ -189,6 +189,7 @@ border = "#363636"
189 | `rendering.show_fps` | bool | `false` | - | Show WebKit FPS counter (`WEBKIT_SHOW_FPS`) |189 | `rendering.show_fps` | bool | `false` | - | Show WebKit FPS counter (`WEBKIT_SHOW_FPS`) |
190 | `rendering.sample_memory` | bool | `false` | - | Enable WebKit memory sampling (`WEBKIT_SAMPLE_MEMORY`) |190 | `rendering.sample_memory` | bool | `false` | - | Enable WebKit memory sampling (`WEBKIT_SAMPLE_MEMORY`) |
191 | `rendering.debug_frames` | bool | `false` | - | Enable GTK frame timing debug (`GDK_DEBUG=frames`) |191 | `rendering.debug_frames` | bool | `false` | - | Enable GTK frame timing debug (`GDK_DEBUG=frames`) |
192+| `engine.cef.render_stack` | string | `"vulkan"` | `vulkan`, `egl` | CEF GPU render stack |
192 | `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |193 | `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |
193 | `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |194 | `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |
194 195
@@ -691,8 +692,8 @@ DUMBER_DATABASE_PATH=/custom/path/db.sqlite
691 692
692 # Rendering (examples)693 # Rendering (examples)
693 # CEF defaults to the GPU-first Vulkan DMABUF stack.694 # CEF defaults to the GPU-first Vulkan DMABUF stack.
694-# Use legacy-gl only as a diagnostic fallback.695+# Use EGL/OpenGL for driver compatibility:
695-# DUMBER_RENDER_STACK=legacy-gl696+# DUMBER_ENGINE_CEF_RENDER_STACK=egl
696 DUMBER_RENDERING_MODE=cpu697 DUMBER_RENDERING_MODE=cpu
697 # WebKit-specific rendering overrides:698 # WebKit-specific rendering overrides:
698 # DUMBER_RENDERING_DISABLE_DMABUF_RENDERER=true699 # DUMBER_RENDERING_DISABLE_DMABUF_RENDERER=true
M docs/reference/configuration.md +1 -1
@@ -59,7 +59,7 @@
5959 | `rendering.show_fps` | bool | `false` | |
6060 | `rendering.sample_memory` | bool | `false` | |
6161 | `rendering.debug_frames` | bool | `false` | |
62-| `DUMBER_RENDER_STACK` env | string | `vulkan-dmabuf` | `vulkan-dmabuf`, `auto`, `legacy-gl` |
62+| `engine.cef.render_stack` | string | `vulkan` | `vulkan`, `egl` |
6363 | `default_ui_scale` | float | `1.0` | > 0 |
6464 | `default_webpage_zoom` | float | `1.2` | > 0 |
6565 | `workspace.new_pane_url` | string | `about:blank` | |
@@ -59,7 +59,7 @@
59 | `rendering.show_fps` | bool | `false` | |59 | `rendering.show_fps` | bool | `false` | |
60 | `rendering.sample_memory` | bool | `false` | |60 | `rendering.sample_memory` | bool | `false` | |
61 | `rendering.debug_frames` | bool | `false` | |61 | `rendering.debug_frames` | bool | `false` | |
62-| `DUMBER_RENDER_STACK` env | string | `vulkan-dmabuf` | `vulkan-dmabuf`, `auto`, `legacy-gl` |62+| `engine.cef.render_stack` | string | `vulkan` | `vulkan`, `egl` |
63 | `default_ui_scale` | float | `1.0` | > 0 |63 | `default_ui_scale` | float | `1.0` | > 0 |
64 | `default_webpage_zoom` | float | `1.2` | > 0 |64 | `default_webpage_zoom` | float | `1.2` | > 0 |
65 | `workspace.new_pane_url` | string | `about:blank` | |65 | `workspace.new_pane_url` | string | `about:blank` | |
M docs/reference/dependencies.md +1 -1
@@ -73,7 +73,7 @@ sudo pacman -S vulkan-tools # for vulkaninfo
7373
7474 ### CEF Wayland rendering
7575
76-When `engine.type = "cef"`, Dumber defaults to the GPU-first Wayland stack: GDK DMABUF presentation with ANGLE/GSK Vulkan. No launch wrapper or render environment variables are required for normal use. Set `DUMBER_RENDER_STACK=legacy-gl` only when diagnosing the older GtkGLArea/OpenGL fallback path.
76+When `engine.type = "cef"`, Dumber defaults to the GPU-first Wayland stack: GDK DMABUF presentation with ANGLE/GSK Vulkan. No launch wrapper or render environment variables are required for normal use. For driver compatibility, set `engine.cef.render_stack = "egl"` (or `DUMBER_ENGINE_CEF_RENDER_STACK=egl`) to use the GtkGLArea/EGL/OpenGL stack.
7777
7878 ## Debian/Ubuntu
7979
@@ -73,7 +73,7 @@ sudo pacman -S vulkan-tools # for vulkaninfo
73 73
74 ### CEF Wayland rendering74 ### CEF Wayland rendering
75 75
76-When `engine.type = "cef"`, Dumber defaults to the GPU-first Wayland stack: GDK DMABUF presentation with ANGLE/GSK Vulkan. No launch wrapper or render environment variables are required for normal use. Set `DUMBER_RENDER_STACK=legacy-gl` only when diagnosing the older GtkGLArea/OpenGL fallback path.76+When `engine.type = "cef"`, Dumber defaults to the GPU-first Wayland stack: GDK DMABUF presentation with ANGLE/GSK Vulkan. No launch wrapper or render environment variables are required for normal use. For driver compatibility, set `engine.cef.render_stack = "egl"` (or `DUMBER_ENGINE_CEF_RENDER_STACK=egl`) to use the GtkGLArea/EGL/OpenGL stack.
77 77
78 ## Debian/Ubuntu78 ## Debian/Ubuntu
79 79
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.2.0
10+ github.com/bnema/purego-cef2gtk v0.3.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.2.010+ github.com/bnema/purego-cef2gtk v0.3.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/bootstrap/engine.go +1 -0
@@ -80,6 +80,7 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
8080 CEFDir: cfg.Engine.CEF.CEFDir,
8181 LogFile: cfg.Engine.CEF.LogFile,
8282 LogSeverity: cfg.Engine.CEF.LogSeverity,
83+ RenderStack: string(cfg.Engine.CEF.CEFRenderStack()),
8384 WindowlessFrameRate: cfg.Engine.CEF.WindowlessFrameRate,
8485 EnableAudioHandler: cfg.Engine.CEF.EnableAudioHandler,
8586 TraceHandlers: cfg.Engine.CEF.TraceHandlers,
@@ -80,6 +80,7 @@ func BuildEngine(input EngineInput) (port.Engine, error) {
80 CEFDir: cfg.Engine.CEF.CEFDir,80 CEFDir: cfg.Engine.CEF.CEFDir,
81 LogFile: cfg.Engine.CEF.LogFile,81 LogFile: cfg.Engine.CEF.LogFile,
82 LogSeverity: cfg.Engine.CEF.LogSeverity,82 LogSeverity: cfg.Engine.CEF.LogSeverity,
83+ RenderStack: string(cfg.Engine.CEF.CEFRenderStack()),
83 WindowlessFrameRate: cfg.Engine.CEF.WindowlessFrameRate,84 WindowlessFrameRate: cfg.Engine.CEF.WindowlessFrameRate,
84 EnableAudioHandler: cfg.Engine.CEF.EnableAudioHandler,85 EnableAudioHandler: cfg.Engine.CEF.EnableAudioHandler,
85 TraceHandlers: cfg.Engine.CEF.TraceHandlers,86 TraceHandlers: cfg.Engine.CEF.TraceHandlers,
M internal/infrastructure/cef/app.go +7 -3
@@ -41,6 +41,10 @@ func registerDumbScheme(registrar purecef.SchemeRegistrar) {
4141 }
4242
4343 func configureCommandLine(commandLine purecef.CommandLine) {
44+ configureCommandLineWithRenderStack(commandLine, cef2gtk.RenderStackPlan{})
45+}
46+
47+func configureCommandLineWithRenderStack(commandLine purecef.CommandLine, renderStackPlan cef2gtk.RenderStackPlan) {
4448 if commandLine == nil {
4549 return
4650 }
@@ -48,7 +52,7 @@ func configureCommandLine(commandLine purecef.CommandLine) {
4852 // Delegate Wayland accelerated rendering setup to the GTK bridge.
4953 // Preserves any pre-existing ozone-platform value (e.g. set via
5054 // CEF command-line args) — the bridge is a no-op when already present.
51- cef2gtk.ConfigureCommandLine(commandLine, cef2gtk.CommandLineOptions{})
55+ cef2gtk.ConfigureCommandLine(commandLine, cef2gtk.CommandLineOptions{RenderStackPlan: renderStackPlan})
5256
5357 // Enable Chromium's built-in smooth scrolling animation — without this,
5458 // mouse wheel scroll jumps in discrete steps with no momentum/easing.
@@ -292,7 +296,7 @@ const maxCmdLineLogLen = 200
292296 func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLine purecef.CommandLine) {
293297 log := logging.FromContext(a.engine.ctx)
294298 if commandLine != nil {
295- configureCommandLine(commandLine)
299+ configureCommandLineWithRenderStack(commandLine, a.engine.renderStackPlan)
296300
297301 if processType == "" {
298302 if cefWebAuthnUnsafeEnabled() {
@@ -360,7 +364,7 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
360364 ozonePlatform := ""
361365 renderNodeOverride := ""
362366 if commandLine != nil {
363- configureCommandLine(commandLine)
367+ configureCommandLineWithRenderStack(commandLine, h.engine.renderStackPlan)
364368 appendSwitchIfMissing(commandLine, "no-zygote")
365369 processType = commandLine.GetSwitchValue("type")
366370 commandLineString = commandLine.GetCommandLineString()
@@ -41,6 +41,10 @@ func registerDumbScheme(registrar purecef.SchemeRegistrar) {
41 }41 }
42 42
43 func configureCommandLine(commandLine purecef.CommandLine) {43 func configureCommandLine(commandLine purecef.CommandLine) {
44+ configureCommandLineWithRenderStack(commandLine, cef2gtk.RenderStackPlan{})
45+}
46+
47+func configureCommandLineWithRenderStack(commandLine purecef.CommandLine, renderStackPlan cef2gtk.RenderStackPlan) {
44 if commandLine == nil {48 if commandLine == nil {
45 return49 return
46 }50 }
@@ -48,7 +52,7 @@ func configureCommandLine(commandLine purecef.CommandLine) {
48 // Delegate Wayland accelerated rendering setup to the GTK bridge.52 // Delegate Wayland accelerated rendering setup to the GTK bridge.
49 // Preserves any pre-existing ozone-platform value (e.g. set via53 // Preserves any pre-existing ozone-platform value (e.g. set via
50 // CEF command-line args) — the bridge is a no-op when already present.54 // CEF command-line args) — the bridge is a no-op when already present.
51- cef2gtk.ConfigureCommandLine(commandLine, cef2gtk.CommandLineOptions{})55+ cef2gtk.ConfigureCommandLine(commandLine, cef2gtk.CommandLineOptions{RenderStackPlan: renderStackPlan})
52 56
53 // Enable Chromium's built-in smooth scrolling animation — without this,57 // Enable Chromium's built-in smooth scrolling animation — without this,
54 // mouse wheel scroll jumps in discrete steps with no momentum/easing.58 // mouse wheel scroll jumps in discrete steps with no momentum/easing.
@@ -292,7 +296,7 @@ const maxCmdLineLogLen = 200
292 func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLine purecef.CommandLine) {296 func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLine purecef.CommandLine) {
293 log := logging.FromContext(a.engine.ctx)297 log := logging.FromContext(a.engine.ctx)
294 if commandLine != nil {298 if commandLine != nil {
295- configureCommandLine(commandLine)299+ configureCommandLineWithRenderStack(commandLine, a.engine.renderStackPlan)
296 300
297 if processType == "" {301 if processType == "" {
298 if cefWebAuthnUnsafeEnabled() {302 if cefWebAuthnUnsafeEnabled() {
@@ -360,7 +364,7 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
360 ozonePlatform := ""364 ozonePlatform := ""
361 renderNodeOverride := ""365 renderNodeOverride := ""
362 if commandLine != nil {366 if commandLine != nil {
363- configureCommandLine(commandLine)367+ configureCommandLineWithRenderStack(commandLine, h.engine.renderStackPlan)
364 appendSwitchIfMissing(commandLine, "no-zygote")368 appendSwitchIfMissing(commandLine, "no-zygote")
365 processType = commandLine.GetSwitchValue("type")369 processType = commandLine.GetSwitchValue("type")
366 commandLineString = commandLine.GetCommandLineString()370 commandLineString = commandLine.GetCommandLineString()
M internal/infrastructure/cef/app_test.go +13 -2
@@ -297,6 +297,17 @@ func TestConfigureCommandLine_PreservesExistingOzonePlatform(t *testing.T) {
297297 }
298298 }
299299
300+func TestConfigureCommandLine_PreservesInheritedANGLEBackend(t *testing.T) {
301+ commandLine := newMutableCommandLineStub()
302+ commandLine.AppendSwitchWithValue("use-angle", "gl-egl")
303+
304+ configureCommandLine(commandLine)
305+
306+ if got := commandLine.GetSwitchValue("use-angle"); got != "gl-egl" {
307+ t.Fatalf("use-angle = %q, want inherited gl-egl", got)
308+ }
309+}
310+
300311 func TestConfigureCommandLine_EnableVAAPIEnvAppendsHardwareDecodeFlags(t *testing.T) {
301312 t.Setenv(cefEnableVAAPIEnvVar, "1")
302313
@@ -337,8 +348,8 @@ func TestConfigureCommandLine_ChromiumFlagsEnvAppendsSwitches(t *testing.T) {
337348
338349 configureCommandLine(commandLine)
339350
340- if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); got != "ExistingFeature,VaapiVideoDecoder,VaapiIgnoreDriverChecks" {
341- t.Fatalf("enable-features = %q, want env features appended", got)
351+ if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); got != "ExistingFeature,Vulkan,DefaultANGLEVulkan,VulkanFromANGLE,VaapiVideoDecoder,VaapiIgnoreDriverChecks" {
352+ t.Fatalf("enable-features = %q, want render stack and env features appended", got)
342353 }
343354 if !commandLine.HasSwitch("ignore-gpu-blocklist") {
344355 t.Fatal("expected ignore-gpu-blocklist switch")
@@ -297,6 +297,17 @@ func TestConfigureCommandLine_PreservesExistingOzonePlatform(t *testing.T) {
297 }297 }
298 }298 }
299 299
300+func TestConfigureCommandLine_PreservesInheritedANGLEBackend(t *testing.T) {
301+ commandLine := newMutableCommandLineStub()
302+ commandLine.AppendSwitchWithValue("use-angle", "gl-egl")
303+
304+ configureCommandLine(commandLine)
305+
306+ if got := commandLine.GetSwitchValue("use-angle"); got != "gl-egl" {
307+ t.Fatalf("use-angle = %q, want inherited gl-egl", got)
308+ }
309+}
310+
300 func TestConfigureCommandLine_EnableVAAPIEnvAppendsHardwareDecodeFlags(t *testing.T) {311 func TestConfigureCommandLine_EnableVAAPIEnvAppendsHardwareDecodeFlags(t *testing.T) {
301 t.Setenv(cefEnableVAAPIEnvVar, "1")312 t.Setenv(cefEnableVAAPIEnvVar, "1")
302 313
@@ -337,8 +348,8 @@ func TestConfigureCommandLine_ChromiumFlagsEnvAppendsSwitches(t *testing.T) {
337 348
338 configureCommandLine(commandLine)349 configureCommandLine(commandLine)
339 350
340- if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); got != "ExistingFeature,VaapiVideoDecoder,VaapiIgnoreDriverChecks" {351+ if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); got != "ExistingFeature,Vulkan,DefaultANGLEVulkan,VulkanFromANGLE,VaapiVideoDecoder,VaapiIgnoreDriverChecks" {
341- t.Fatalf("enable-features = %q, want env features appended", got)352+ t.Fatalf("enable-features = %q, want render stack and env features appended", got)
342 }353 }
343 if !commandLine.HasSwitch("ignore-gpu-blocklist") {354 if !commandLine.HasSwitch("ignore-gpu-blocklist") {
344 t.Fatal("expected ignore-gpu-blocklist switch")355 t.Fatal("expected ignore-gpu-blocklist switch")
M internal/infrastructure/cef/cef2gtk_adapter.go +2 -2
@@ -33,8 +33,8 @@ type Cef2gtkAdapter struct {
3333
3434 // NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin
3535 // Dumber adapter.
36-func NewCef2gtkAdapter() *Cef2gtkAdapter {
37- v := cef2gtk.NewView()
36+func NewCef2gtkAdapter(renderStackPlan cef2gtk.RenderStackPlan) *Cef2gtkAdapter {
37+ v := cef2gtk.NewViewWithOptions(cef2gtk.ViewOptions{RenderStackPlan: renderStackPlan})
3838 if v == nil {
3939 return nil
4040 }
@@ -33,8 +33,8 @@ type Cef2gtkAdapter struct {
33 33
34 // NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin34 // NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin
35 // Dumber adapter.35 // Dumber adapter.
36-func NewCef2gtkAdapter() *Cef2gtkAdapter {36+func NewCef2gtkAdapter(renderStackPlan cef2gtk.RenderStackPlan) *Cef2gtkAdapter {
37- v := cef2gtk.NewView()37+ v := cef2gtk.NewViewWithOptions(cef2gtk.ViewOptions{RenderStackPlan: renderStackPlan})
38 if v == nil {38 if v == nil {
39 return nil39 return nil
40 }40 }
M internal/infrastructure/cef/engine.go +7 -5
@@ -11,6 +11,7 @@ import (
1111 "unicode/utf8"
1212
1313 purecef "github.com/bnema/purego-cef/cef"
14+ cef2gtk "github.com/bnema/purego-cef2gtk"
1415
1516 "github.com/bnema/dumber/internal/application/dto"
1617 "github.com/bnema/dumber/internal/application/port"
@@ -24,11 +25,12 @@ var _ port.AlreadyRunningAppRelaunchHandlerSetter = (*Engine)(nil)
2425 // Engine implements port.Engine for the CEF browser backend.
2526 // It manages the CEF lifecycle and provides access to all engine subsystems.
2627 type Engine struct {
27- ctx context.Context
28- factory *WebViewFactory
29- pool *WebViewPool
30- profileLogDir string
31- runtimeCEFDir string
28+ ctx context.Context
29+ factory *WebViewFactory
30+ pool *WebViewPool
31+ profileLogDir string
32+ runtimeCEFDir string
33+ renderStackPlan cef2gtk.RenderStackPlan
3234
3335 messageRouter *MessageRouter
3436 schemeHandler *dumbSchemeHandler
@@ -11,6 +11,7 @@ import (
11 "unicode/utf8"11 "unicode/utf8"
12 12
13 purecef "github.com/bnema/purego-cef/cef"13 purecef "github.com/bnema/purego-cef/cef"
14+ cef2gtk "github.com/bnema/purego-cef2gtk"
14 15
15 "github.com/bnema/dumber/internal/application/dto"16 "github.com/bnema/dumber/internal/application/dto"
16 "github.com/bnema/dumber/internal/application/port"17 "github.com/bnema/dumber/internal/application/port"
@@ -24,11 +25,12 @@ var _ port.AlreadyRunningAppRelaunchHandlerSetter = (*Engine)(nil)
24 // Engine implements port.Engine for the CEF browser backend.25 // Engine implements port.Engine for the CEF browser backend.
25 // It manages the CEF lifecycle and provides access to all engine subsystems.26 // It manages the CEF lifecycle and provides access to all engine subsystems.
26 type Engine struct {27 type Engine struct {
27- ctx context.Context28+ ctx context.Context
28- factory *WebViewFactory29+ factory *WebViewFactory
29- pool *WebViewPool30+ pool *WebViewPool
30- profileLogDir string31+ profileLogDir string
31- runtimeCEFDir string32+ runtimeCEFDir string
33+ renderStackPlan cef2gtk.RenderStackPlan
32 34
33 messageRouter *MessageRouter35 messageRouter *MessageRouter
34 schemeHandler *dumbSchemeHandler36 schemeHandler *dumbSchemeHandler
M internal/infrastructure/cef/engine_init.go +11 -0
@@ -11,6 +11,7 @@ import (
1111 "syscall"
1212
1313 purecef "github.com/bnema/purego-cef/cef"
14+ cef2gtk "github.com/bnema/purego-cef2gtk"
1415 "github.com/bnema/puregotk/v4/gdk"
1516 "github.com/bnema/puregotk/v4/glib"
1617 "github.com/rs/zerolog"
@@ -44,6 +45,11 @@ func NewEngine(
4445 stateRoot := resolvedStateRoot(paths.StateRoot, opts)
4546 cleanStaleSingletonLocks(logger, stateRoot)
4647 windowlessFrameRate := normalizedWindowlessFrameRate(cfg.WindowlessFrameRate)
48+ renderStackPlan, err := resolveCEFRenderStackPlan(cfg.RenderStack)
49+ if err != nil {
50+ return nil, err
51+ }
52+ cef2gtk.ConfigureRenderStackEnvironment(renderStackPlan)
4753
4854 settings, err := prepareCEFSettings(opts, paths, cfg, logger)
4955 if err != nil {
@@ -59,6 +65,7 @@ func NewEngine(
5965 ctx: ctx,
6066 profileLogDir: paths.ProfileLogDir,
6167 runtimeCEFDir: settings.CEFDir,
68+ renderStackPlan: renderStackPlan,
6269 registerHandlers: deps.RegisterHandlers,
6370 registerAccentHandlers: deps.RegisterAccentHandlers,
6471 currentConfigPayload: deps.CurrentConfigPayload,
@@ -72,6 +79,10 @@ func NewEngine(
7279
7380 logger.Info().
7481 Int32("windowless_frame_rate", windowlessFrameRate).
82+ Str("render_stack", string(renderStackPlan.Stack)).
83+ Str("render_backend", renderStackPlan.Backend.String()).
84+ Str("angle_backend", renderStackPlan.ANGLEBackend).
85+ Str("gsk_renderer", renderStackPlan.GSKRenderer).
7586 Bool("external_begin_frame", externalBeginFrameEnabled()).
7687 Bool("trace_handlers", cfg.TraceHandlers).
7788 Bool("enable_audio_handler", cfg.EnableAudioHandler).
@@ -11,6 +11,7 @@ import (
11 "syscall"11 "syscall"
12 12
13 purecef "github.com/bnema/purego-cef/cef"13 purecef "github.com/bnema/purego-cef/cef"
14+ cef2gtk "github.com/bnema/purego-cef2gtk"
14 "github.com/bnema/puregotk/v4/gdk"15 "github.com/bnema/puregotk/v4/gdk"
15 "github.com/bnema/puregotk/v4/glib"16 "github.com/bnema/puregotk/v4/glib"
16 "github.com/rs/zerolog"17 "github.com/rs/zerolog"
@@ -44,6 +45,11 @@ func NewEngine(
44 stateRoot := resolvedStateRoot(paths.StateRoot, opts)45 stateRoot := resolvedStateRoot(paths.StateRoot, opts)
45 cleanStaleSingletonLocks(logger, stateRoot)46 cleanStaleSingletonLocks(logger, stateRoot)
46 windowlessFrameRate := normalizedWindowlessFrameRate(cfg.WindowlessFrameRate)47 windowlessFrameRate := normalizedWindowlessFrameRate(cfg.WindowlessFrameRate)
48+ renderStackPlan, err := resolveCEFRenderStackPlan(cfg.RenderStack)
49+ if err != nil {
50+ return nil, err
51+ }
52+ cef2gtk.ConfigureRenderStackEnvironment(renderStackPlan)
47 53
48 settings, err := prepareCEFSettings(opts, paths, cfg, logger)54 settings, err := prepareCEFSettings(opts, paths, cfg, logger)
49 if err != nil {55 if err != nil {
@@ -59,6 +65,7 @@ func NewEngine(
59 ctx: ctx,65 ctx: ctx,
60 profileLogDir: paths.ProfileLogDir,66 profileLogDir: paths.ProfileLogDir,
61 runtimeCEFDir: settings.CEFDir,67 runtimeCEFDir: settings.CEFDir,
68+ renderStackPlan: renderStackPlan,
62 registerHandlers: deps.RegisterHandlers,69 registerHandlers: deps.RegisterHandlers,
63 registerAccentHandlers: deps.RegisterAccentHandlers,70 registerAccentHandlers: deps.RegisterAccentHandlers,
64 currentConfigPayload: deps.CurrentConfigPayload,71 currentConfigPayload: deps.CurrentConfigPayload,
@@ -72,6 +79,10 @@ func NewEngine(
72 79
73 logger.Info().80 logger.Info().
74 Int32("windowless_frame_rate", windowlessFrameRate).81 Int32("windowless_frame_rate", windowlessFrameRate).
82+ Str("render_stack", string(renderStackPlan.Stack)).
83+ Str("render_backend", renderStackPlan.Backend.String()).
84+ Str("angle_backend", renderStackPlan.ANGLEBackend).
85+ Str("gsk_renderer", renderStackPlan.GSKRenderer).
75 Bool("external_begin_frame", externalBeginFrameEnabled()).86 Bool("external_begin_frame", externalBeginFrameEnabled()).
76 Bool("trace_handlers", cfg.TraceHandlers).87 Bool("trace_handlers", cfg.TraceHandlers).
77 Bool("enable_audio_handler", cfg.EnableAudioHandler).88 Bool("enable_audio_handler", cfg.EnableAudioHandler).
M internal/infrastructure/cef/factory.go +3 -3
@@ -81,7 +81,7 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
8181
8282 // Configure BrowserSettings.
8383 settings := purecef.NewBrowserSettings()
84- settings.WindowlessFrameRate = f.windowlessFrameRate
84+ cef2gtk.ConfigureBrowserSettings(&settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: f.windowlessFrameRate})
8585 settings.LocalStorage = 1 // CEF_STATE_ENABLED
8686 if bg := f.bgColor.Load(); bg != 0 {
8787 settings.BackgroundColor = bg
@@ -97,7 +97,7 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
9797
9898 func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) {
9999 id := port.WebViewID(f.nextID.Add(1))
100- viewBridge := NewCef2gtkAdapter()
100+ viewBridge := NewCef2gtkAdapter(f.engine.renderStackPlan)
101101 if viewBridge == nil {
102102 return nil, fmt.Errorf("create cef2gtk view")
103103 }
@@ -300,7 +300,7 @@ func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebVie
300300 }
301301
302302 settings := purecef.NewBrowserSettings()
303- settings.WindowlessFrameRate = f.windowlessFrameRate
303+ cef2gtk.ConfigureBrowserSettings(&settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: f.windowlessFrameRate})
304304 settings.LocalStorage = 1 // CEF_STATE_ENABLED
305305 if bg := f.bgColor.Load(); bg != 0 {
306306 settings.BackgroundColor = bg
@@ -81,7 +81,7 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
81 81
82 // Configure BrowserSettings.82 // Configure BrowserSettings.
83 settings := purecef.NewBrowserSettings()83 settings := purecef.NewBrowserSettings()
84- settings.WindowlessFrameRate = f.windowlessFrameRate84+ cef2gtk.ConfigureBrowserSettings(&settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: f.windowlessFrameRate})
85 settings.LocalStorage = 1 // CEF_STATE_ENABLED85 settings.LocalStorage = 1 // CEF_STATE_ENABLED
86 if bg := f.bgColor.Load(); bg != 0 {86 if bg := f.bgColor.Load(); bg != 0 {
87 settings.BackgroundColor = bg87 settings.BackgroundColor = bg
@@ -97,7 +97,7 @@ func (f *WebViewFactory) Create(ctx context.Context) (port.WebView, error) {
97 97
98 func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) {98 func (f *WebViewFactory) newWebView(ctx context.Context) (*WebView, error) {
99 id := port.WebViewID(f.nextID.Add(1))99 id := port.WebViewID(f.nextID.Add(1))
100- viewBridge := NewCef2gtkAdapter()100+ viewBridge := NewCef2gtkAdapter(f.engine.renderStackPlan)
101 if viewBridge == nil {101 if viewBridge == nil {
102 return nil, fmt.Errorf("create cef2gtk view")102 return nil, fmt.Errorf("create cef2gtk view")
103 }103 }
@@ -300,7 +300,7 @@ func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebVie
300 }300 }
301 301
302 settings := purecef.NewBrowserSettings()302 settings := purecef.NewBrowserSettings()
303- settings.WindowlessFrameRate = f.windowlessFrameRate303+ cef2gtk.ConfigureBrowserSettings(&settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: f.windowlessFrameRate})
304 settings.LocalStorage = 1 // CEF_STATE_ENABLED304 settings.LocalStorage = 1 // CEF_STATE_ENABLED
305 if bg := f.bgColor.Load(); bg != 0 {305 if bg := f.bgColor.Load(); bg != 0 {
306 settings.BackgroundColor = bg306 settings.BackgroundColor = bg
A internal/infrastructure/cef/render_stack.go +36 -0
@@ -0,0 +1,36 @@
1+package cef
2+
3+import (
4+ "fmt"
5+
6+ cef2gtk "github.com/bnema/purego-cef2gtk"
7+
8+ "github.com/bnema/dumber/internal/application/port"
9+)
10+
11+func resolveCEFRenderStackPlan(stack string) (cef2gtk.RenderStackPlan, error) {
12+ plan, err := cef2gtk.ResolveRenderStack(cef2gtk.RenderStack(stack))
13+ if err != nil {
14+ return cef2gtk.RenderStackPlan{}, fmt.Errorf("resolve CEF render stack: %w", err)
15+ }
16+ return plan, nil
17+}
18+
19+// ApplyConfiguredRenderStackEnvironment applies Dumber's CEF render-stack
20+// process environment before GTK/libadwaita initialize.
21+func ApplyConfiguredRenderStackEnvironment(stack string, logger port.Logger) cef2gtk.RenderStackPlan {
22+ plan, err := resolveCEFRenderStackPlan(stack)
23+ if err != nil {
24+ logWarn(logger, "cef: invalid render stack, falling back to Vulkan", port.Field("error", err.Error()))
25+ plan, _ = cef2gtk.ResolveRenderStack(cef2gtk.RenderStackVulkan)
26+ }
27+ cef2gtk.ConfigureRenderStackEnvironment(plan)
28+ logInfo(logger, "cef: render stack environment configured",
29+ port.Field("render_stack", string(plan.Stack)),
30+ port.Field("render_backend", plan.Backend.String()),
31+ port.Field("angle_backend", plan.ANGLEBackend),
32+ port.Field("gsk_renderer", plan.GSKRenderer),
33+ port.Field("osr_backing_scale", plan.OSRBackingScale),
34+ )
35+ return plan
36+}
@@ -0,0 +1,36 @@
1+package cef
2+
3+import (
4+ "fmt"
5+
6+ cef2gtk "github.com/bnema/purego-cef2gtk"
7+
8+ "github.com/bnema/dumber/internal/application/port"
9+)
10+
11+func resolveCEFRenderStackPlan(stack string) (cef2gtk.RenderStackPlan, error) {
12+ plan, err := cef2gtk.ResolveRenderStack(cef2gtk.RenderStack(stack))
13+ if err != nil {
14+ return cef2gtk.RenderStackPlan{}, fmt.Errorf("resolve CEF render stack: %w", err)
15+ }
16+ return plan, nil
17+}
18+
19+// ApplyConfiguredRenderStackEnvironment applies Dumber's CEF render-stack
20+// process environment before GTK/libadwaita initialize.
21+func ApplyConfiguredRenderStackEnvironment(stack string, logger port.Logger) cef2gtk.RenderStackPlan {
22+ plan, err := resolveCEFRenderStackPlan(stack)
23+ if err != nil {
24+ logWarn(logger, "cef: invalid render stack, falling back to Vulkan", port.Field("error", err.Error()))
25+ plan, _ = cef2gtk.ResolveRenderStack(cef2gtk.RenderStackVulkan)
26+ }
27+ cef2gtk.ConfigureRenderStackEnvironment(plan)
28+ logInfo(logger, "cef: render stack environment configured",
29+ port.Field("render_stack", string(plan.Stack)),
30+ port.Field("render_backend", plan.Backend.String()),
31+ port.Field("angle_backend", plan.ANGLEBackend),
32+ port.Field("gsk_renderer", plan.GSKRenderer),
33+ port.Field("osr_backing_scale", plan.OSRBackingScale),
34+ )
35+ return plan
36+}
A internal/infrastructure/cef/render_stack_plan_test.go +48 -0
@@ -0,0 +1,48 @@
1+package cef
2+
3+import (
4+ "testing"
5+
6+ cef2gtk "github.com/bnema/purego-cef2gtk"
7+)
8+
9+func TestResolveCEFRenderStackPlan(t *testing.T) {
10+ tests := []struct {
11+ name string
12+ stack string
13+ wantStack cef2gtk.RenderStack
14+ wantAngle string
15+ }{
16+ {name: "vulkan", stack: "vulkan", wantStack: cef2gtk.RenderStackVulkan, wantAngle: "vulkan"},
17+ {name: "egl", stack: "egl", wantStack: cef2gtk.RenderStackEGL, wantAngle: "gl-egl"},
18+ }
19+
20+ for _, tt := range tests {
21+ t.Run(tt.name, func(t *testing.T) {
22+ plan, err := resolveCEFRenderStackPlan(tt.stack)
23+ if err != nil {
24+ t.Fatalf("resolveCEFRenderStackPlan(%q) error = %v", tt.stack, err)
25+ }
26+ if plan.Stack != tt.wantStack {
27+ t.Fatalf("Stack = %q, want %q", plan.Stack, tt.wantStack)
28+ }
29+ if plan.ANGLEBackend != tt.wantAngle {
30+ t.Fatalf("ANGLEBackend = %q, want %q", plan.ANGLEBackend, tt.wantAngle)
31+ }
32+ })
33+ }
34+}
35+
36+func TestConfigureCommandLineWithRenderStackUsesPlan(t *testing.T) {
37+ plan, err := resolveCEFRenderStackPlan("egl")
38+ if err != nil {
39+ t.Fatalf("resolveCEFRenderStackPlan(egl) error = %v", err)
40+ }
41+ commandLine := newMutableCommandLineStub()
42+
43+ configureCommandLineWithRenderStack(commandLine, plan)
44+
45+ if got := commandLine.GetSwitchValue("use-angle"); got != "gl-egl" {
46+ t.Fatalf("use-angle = %q, want gl-egl", got)
47+ }
48+}
@@ -0,0 +1,48 @@
1+package cef
2+
3+import (
4+ "testing"
5+
6+ cef2gtk "github.com/bnema/purego-cef2gtk"
7+)
8+
9+func TestResolveCEFRenderStackPlan(t *testing.T) {
10+ tests := []struct {
11+ name string
12+ stack string
13+ wantStack cef2gtk.RenderStack
14+ wantAngle string
15+ }{
16+ {name: "vulkan", stack: "vulkan", wantStack: cef2gtk.RenderStackVulkan, wantAngle: "vulkan"},
17+ {name: "egl", stack: "egl", wantStack: cef2gtk.RenderStackEGL, wantAngle: "gl-egl"},
18+ }
19+
20+ for _, tt := range tests {
21+ t.Run(tt.name, func(t *testing.T) {
22+ plan, err := resolveCEFRenderStackPlan(tt.stack)
23+ if err != nil {
24+ t.Fatalf("resolveCEFRenderStackPlan(%q) error = %v", tt.stack, err)
25+ }
26+ if plan.Stack != tt.wantStack {
27+ t.Fatalf("Stack = %q, want %q", plan.Stack, tt.wantStack)
28+ }
29+ if plan.ANGLEBackend != tt.wantAngle {
30+ t.Fatalf("ANGLEBackend = %q, want %q", plan.ANGLEBackend, tt.wantAngle)
31+ }
32+ })
33+ }
34+}
35+
36+func TestConfigureCommandLineWithRenderStackUsesPlan(t *testing.T) {
37+ plan, err := resolveCEFRenderStackPlan("egl")
38+ if err != nil {
39+ t.Fatalf("resolveCEFRenderStackPlan(egl) error = %v", err)
40+ }
41+ commandLine := newMutableCommandLineStub()
42+
43+ configureCommandLineWithRenderStack(commandLine, plan)
44+
45+ if got := commandLine.GetSwitchValue("use-angle"); got != "gl-egl" {
46+ t.Fatalf("use-angle = %q, want gl-egl", got)
47+ }
48+}
M internal/infrastructure/cef/runtime_config.go +1 -0
@@ -18,6 +18,7 @@ type RuntimeConfig struct {
1818 CEFDir string
1919 LogFile string
2020 LogSeverity int32
21+ RenderStack string
2122 WindowlessFrameRate int32
2223 EnableAudioHandler bool
2324 TraceHandlers bool
@@ -18,6 +18,7 @@ type RuntimeConfig struct {
18 CEFDir string18 CEFDir string
19 LogFile string19 LogFile string
20 LogSeverity int3220 LogSeverity int32
21+ RenderStack string
21 WindowlessFrameRate int3222 WindowlessFrameRate int32
22 EnableAudioHandler bool23 EnableAudioHandler bool
23 TraceHandlers bool24 TraceHandlers bool
M internal/infrastructure/cef/webview.go +1 -3
@@ -965,9 +965,7 @@ func configureNativePopupWindow(
965965 if settings == nil {
966966 return
967967 }
968- if frameRate > 0 {
969- settings.WindowlessFrameRate = frameRate
970- }
968+ cef2gtk.ConfigureBrowserSettings(settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: frameRate})
971969 settings.LocalStorage = 1
972970 if backgroundColor != 0 {
973971 settings.BackgroundColor = backgroundColor
@@ -965,9 +965,7 @@ func configureNativePopupWindow(
965 if settings == nil {965 if settings == nil {
966 return966 return
967 }967 }
968- if frameRate > 0 {968+ cef2gtk.ConfigureBrowserSettings(settings, cef2gtk.BrowserSettingsOptions{WindowlessFrameRate: frameRate})
969- settings.WindowlessFrameRate = frameRate
970- }
971 settings.LocalStorage = 1969 settings.LocalStorage = 1
972 if backgroundColor != 0 {970 if backgroundColor != 0 {
973 settings.BackgroundColor = backgroundColor971 settings.BackgroundColor = backgroundColor
A internal/infrastructure/config/cef_render_stack_test.go +42 -0
@@ -0,0 +1,42 @@
1+package config
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/assert"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestCEFRenderStackDefaultIsVulkan(t *testing.T) {
11+ cfg := DefaultConfig()
12+
13+ assert.Equal(t, CEFRenderStackVulkan, cfg.Engine.CEF.CEFRenderStack())
14+}
15+
16+func TestValidateConfig_CEFRenderStack(t *testing.T) {
17+ tests := []struct {
18+ name string
19+ stack CEFRenderStack
20+ wantErr bool
21+ }{
22+ {name: "vulkan", stack: CEFRenderStackVulkan},
23+ {name: "egl", stack: CEFRenderStackEGL},
24+ {name: "empty uses default", stack: ""},
25+ {name: "invalid", stack: CEFRenderStack("auto"), wantErr: true},
26+ }
27+
28+ for _, tt := range tests {
29+ t.Run(tt.name, func(t *testing.T) {
30+ cfg := DefaultConfig()
31+ cfg.Engine.CEF.RenderStack = tt.stack
32+
33+ err := validateConfig(cfg)
34+ if tt.wantErr {
35+ require.Error(t, err)
36+ assert.Contains(t, err.Error(), "engine.cef.render_stack")
37+ return
38+ }
39+ require.NoError(t, err)
40+ })
41+ }
42+}
@@ -0,0 +1,42 @@
1+package config
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/assert"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestCEFRenderStackDefaultIsVulkan(t *testing.T) {
11+ cfg := DefaultConfig()
12+
13+ assert.Equal(t, CEFRenderStackVulkan, cfg.Engine.CEF.CEFRenderStack())
14+}
15+
16+func TestValidateConfig_CEFRenderStack(t *testing.T) {
17+ tests := []struct {
18+ name string
19+ stack CEFRenderStack
20+ wantErr bool
21+ }{
22+ {name: "vulkan", stack: CEFRenderStackVulkan},
23+ {name: "egl", stack: CEFRenderStackEGL},
24+ {name: "empty uses default", stack: ""},
25+ {name: "invalid", stack: CEFRenderStack("auto"), wantErr: true},
26+ }
27+
28+ for _, tt := range tests {
29+ t.Run(tt.name, func(t *testing.T) {
30+ cfg := DefaultConfig()
31+ cfg.Engine.CEF.RenderStack = tt.stack
32+
33+ err := validateConfig(cfg)
34+ if tt.wantErr {
35+ require.Error(t, err)
36+ assert.Contains(t, err.Error(), "engine.cef.render_stack")
37+ return
38+ }
39+ require.NoError(t, err)
40+ })
41+ }
42+}
M internal/infrastructure/config/defaults.go +1 -0
@@ -171,6 +171,7 @@ func DefaultConfig() *Config {
171171 // matches Epiphany's model and avoids a misleading setting.
172172 CookiePolicy: CookiePolicyAlways,
173173 CEF: CEFEngineConfig{
174+ RenderStack: CEFRenderStackVulkan,
174175 WindowlessFrameRate: defaultCEFWindowlessFrameRate,
175176 LogFile: "",
176177 EnableAudioHandler: true,
@@ -171,6 +171,7 @@ func DefaultConfig() *Config {
171 // matches Epiphany's model and avoids a misleading setting.171 // matches Epiphany's model and avoids a misleading setting.
172 CookiePolicy: CookiePolicyAlways,172 CookiePolicy: CookiePolicyAlways,
173 CEF: CEFEngineConfig{173 CEF: CEFEngineConfig{
174+ RenderStack: CEFRenderStackVulkan,
174 WindowlessFrameRate: defaultCEFWindowlessFrameRate,175 WindowlessFrameRate: defaultCEFWindowlessFrameRate,
175 LogFile: "",176 LogFile: "",
176 EnableAudioHandler: true,177 EnableAudioHandler: true,
M internal/infrastructure/config/defaults_test.go +1 -0
@@ -18,6 +18,7 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) {
1818 assert.Equal(t, "webkit", cfg.Engine.Type)
1919 assert.Equal(t, ProfileDefault, cfg.Engine.Profile)
2020 assert.Equal(t, CookiePolicyAlways, cfg.Engine.CookiePolicy)
21+ assert.Equal(t, CEFRenderStackVulkan, cfg.Engine.CEF.CEFRenderStack())
2122 assert.Equal(t, int32(defaultCEFWindowlessFrameRate), cfg.Engine.CEF.CEFWindowlessFrameRate())
2223 assert.Empty(t, cfg.Engine.CEF.LogFile)
2324 assert.True(t, cfg.Engine.CEF.EnableAudioHandler)
@@ -18,6 +18,7 @@ func TestDefaultConfig_CoreDefaults(t *testing.T) {
18 assert.Equal(t, "webkit", cfg.Engine.Type)18 assert.Equal(t, "webkit", cfg.Engine.Type)
19 assert.Equal(t, ProfileDefault, cfg.Engine.Profile)19 assert.Equal(t, ProfileDefault, cfg.Engine.Profile)
20 assert.Equal(t, CookiePolicyAlways, cfg.Engine.CookiePolicy)20 assert.Equal(t, CookiePolicyAlways, cfg.Engine.CookiePolicy)
21+ assert.Equal(t, CEFRenderStackVulkan, cfg.Engine.CEF.CEFRenderStack())
21 assert.Equal(t, int32(defaultCEFWindowlessFrameRate), cfg.Engine.CEF.CEFWindowlessFrameRate())22 assert.Equal(t, int32(defaultCEFWindowlessFrameRate), cfg.Engine.CEF.CEFWindowlessFrameRate())
22 assert.Empty(t, cfg.Engine.CEF.LogFile)23 assert.Empty(t, cfg.Engine.CEF.LogFile)
23 assert.True(t, cfg.Engine.CEF.EnableAudioHandler)24 assert.True(t, cfg.Engine.CEF.EnableAudioHandler)
M internal/infrastructure/config/engine.go +18 -0
@@ -7,6 +7,14 @@ const (
77 EngineTypeCEF = "cef"
88 )
99
10+// CEFRenderStack selects Dumber's CEF GPU render stack.
11+type CEFRenderStack string
12+
13+const (
14+ CEFRenderStackVulkan CEFRenderStack = "vulkan"
15+ CEFRenderStackEGL CEFRenderStack = "egl"
16+)
17+
1018 // EngineConfig holds engine selection and universal engine options.
1119 type EngineConfig struct {
1220 Type string `mapstructure:"type" toml:"type" yaml:"type"`
@@ -90,6 +98,8 @@ type CEFEngineConfig struct {
9098 // LogSeverity controls CEF's internal log verbosity.
9199 // 0 = default, 1 = verbose, 2 = info, 3 = warning, 4 = error, 99 = disable.
92100 LogSeverity int32 `mapstructure:"log_severity" toml:"log_severity" yaml:"log_severity"`
101+ // RenderStack selects the CEF GPU render stack. Empty falls back to vulkan.
102+ RenderStack CEFRenderStack `mapstructure:"render_stack" toml:"render_stack" yaml:"render_stack"`
93103 // WindowlessFrameRate is the maximum frame rate for off-screen rendering.
94104 // Default: 60. Higher values increase CPU usage.
95105 WindowlessFrameRate int32 `mapstructure:"windowless_frame_rate" toml:"windowless_frame_rate" yaml:"windowless_frame_rate"`
@@ -99,6 +109,14 @@ type CEFEngineConfig struct {
99109 TraceHandlers bool `mapstructure:"trace_handlers" toml:"trace_handlers" yaml:"trace_handlers"`
100110 }
101111
112+// CEFRenderStack returns the effective CEF GPU render stack.
113+func (c CEFEngineConfig) CEFRenderStack() CEFRenderStack {
114+ if c.RenderStack != "" {
115+ return c.RenderStack
116+ }
117+ return CEFRenderStackVulkan
118+}
119+
102120 // CEFWindowlessFrameRate returns the effective OSR frame rate.
103121 // A non-positive value falls back to the built-in default to avoid accidentally
104122 // matching high-refresh monitors and burning CPU while idling.
@@ -7,6 +7,14 @@ const (
7 EngineTypeCEF = "cef"7 EngineTypeCEF = "cef"
8 )8 )
9 9
10+// CEFRenderStack selects Dumber's CEF GPU render stack.
11+type CEFRenderStack string
12+
13+const (
14+ CEFRenderStackVulkan CEFRenderStack = "vulkan"
15+ CEFRenderStackEGL CEFRenderStack = "egl"
16+)
17+
10 // EngineConfig holds engine selection and universal engine options.18 // EngineConfig holds engine selection and universal engine options.
11 type EngineConfig struct {19 type EngineConfig struct {
12 Type string `mapstructure:"type" toml:"type" yaml:"type"`20 Type string `mapstructure:"type" toml:"type" yaml:"type"`
@@ -90,6 +98,8 @@ type CEFEngineConfig struct {
90 // LogSeverity controls CEF's internal log verbosity.98 // LogSeverity controls CEF's internal log verbosity.
91 // 0 = default, 1 = verbose, 2 = info, 3 = warning, 4 = error, 99 = disable.99 // 0 = default, 1 = verbose, 2 = info, 3 = warning, 4 = error, 99 = disable.
92 LogSeverity int32 `mapstructure:"log_severity" toml:"log_severity" yaml:"log_severity"`100 LogSeverity int32 `mapstructure:"log_severity" toml:"log_severity" yaml:"log_severity"`
101+ // RenderStack selects the CEF GPU render stack. Empty falls back to vulkan.
102+ RenderStack CEFRenderStack `mapstructure:"render_stack" toml:"render_stack" yaml:"render_stack"`
93 // WindowlessFrameRate is the maximum frame rate for off-screen rendering.103 // WindowlessFrameRate is the maximum frame rate for off-screen rendering.
94 // Default: 60. Higher values increase CPU usage.104 // Default: 60. Higher values increase CPU usage.
95 WindowlessFrameRate int32 `mapstructure:"windowless_frame_rate" toml:"windowless_frame_rate" yaml:"windowless_frame_rate"`105 WindowlessFrameRate int32 `mapstructure:"windowless_frame_rate" toml:"windowless_frame_rate" yaml:"windowless_frame_rate"`
@@ -99,6 +109,14 @@ type CEFEngineConfig struct {
99 TraceHandlers bool `mapstructure:"trace_handlers" toml:"trace_handlers" yaml:"trace_handlers"`109 TraceHandlers bool `mapstructure:"trace_handlers" toml:"trace_handlers" yaml:"trace_handlers"`
100 }110 }
101 111
112+// CEFRenderStack returns the effective CEF GPU render stack.
113+func (c CEFEngineConfig) CEFRenderStack() CEFRenderStack {
114+ if c.RenderStack != "" {
115+ return c.RenderStack
116+ }
117+ return CEFRenderStackVulkan
118+}
119+
102 // CEFWindowlessFrameRate returns the effective OSR frame rate.120 // CEFWindowlessFrameRate returns the effective OSR frame rate.
103 // A non-positive value falls back to the built-in default to avoid accidentally121 // A non-positive value falls back to the built-in default to avoid accidentally
104 // matching high-refresh monitors and burning CPU while idling.122 // matching high-refresh monitors and burning CPU while idling.
M internal/infrastructure/config/loader.go +1 -0
@@ -691,6 +691,7 @@ func (m *Manager) setEngineDefaults(defaults *Config) {
691691
692692 ce := e.CEF
693693 m.viper.SetDefault("engine.cef.cef_dir", ce.CEFDir)
694+ m.viper.SetDefault("engine.cef.render_stack", string(ce.CEFRenderStack()))
694695 m.viper.SetDefault("engine.cef.log_file", ce.LogFile)
695696 m.viper.SetDefault("engine.cef.log_severity", ce.LogSeverity)
696697 m.viper.SetDefault("engine.cef.windowless_frame_rate", ce.CEFWindowlessFrameRate())
@@ -691,6 +691,7 @@ func (m *Manager) setEngineDefaults(defaults *Config) {
691 691
692 ce := e.CEF692 ce := e.CEF
693 m.viper.SetDefault("engine.cef.cef_dir", ce.CEFDir)693 m.viper.SetDefault("engine.cef.cef_dir", ce.CEFDir)
694+ m.viper.SetDefault("engine.cef.render_stack", string(ce.CEFRenderStack()))
694 m.viper.SetDefault("engine.cef.log_file", ce.LogFile)695 m.viper.SetDefault("engine.cef.log_file", ce.LogFile)
695 m.viper.SetDefault("engine.cef.log_severity", ce.LogSeverity)696 m.viper.SetDefault("engine.cef.log_severity", ce.LogSeverity)
696 m.viper.SetDefault("engine.cef.windowless_frame_rate", ce.CEFWindowlessFrameRate())697 m.viper.SetDefault("engine.cef.windowless_frame_rate", ce.CEFWindowlessFrameRate())
M internal/infrastructure/config/schema_provider.go +8 -0
@@ -931,6 +931,14 @@ func (*SchemaProvider) getPerformanceKeys(defaults *Config) []entity.ConfigKeyIn
931931 Range: ">=0",
932932 Section: SectionPerformance,
933933 },
934+ {
935+ Key: "engine.cef.render_stack",
936+ Type: "string",
937+ Default: string(defaults.Engine.CEF.CEFRenderStack()),
938+ Description: "CEF GPU render stack",
939+ Values: []string{"vulkan", "egl"},
940+ Section: SectionPerformance,
941+ },
934942 {
935943 Key: "engine.cef.windowless_frame_rate",
936944 Type: "int32",
@@ -931,6 +931,14 @@ func (*SchemaProvider) getPerformanceKeys(defaults *Config) []entity.ConfigKeyIn
931 Range: ">=0",931 Range: ">=0",
932 Section: SectionPerformance,932 Section: SectionPerformance,
933 },933 },
934+ {
935+ Key: "engine.cef.render_stack",
936+ Type: "string",
937+ Default: string(defaults.Engine.CEF.CEFRenderStack()),
938+ Description: "CEF GPU render stack",
939+ Values: []string{"vulkan", "egl"},
940+ Section: SectionPerformance,
941+ },
934 {942 {
935 Key: "engine.cef.windowless_frame_rate",943 Key: "engine.cef.windowless_frame_rate",
936 Type: "int32",944 Type: "int32",
M internal/infrastructure/config/validation.go +9 -0
@@ -336,6 +336,15 @@ func validateLogging(config *Config) []string {
336336 func validateCEF(config *Config) []string {
337337 var validationErrors []string
338338
339+ switch config.Engine.CEF.CEFRenderStack() {
340+ case CEFRenderStackVulkan, CEFRenderStackEGL:
341+ default:
342+ validationErrors = append(validationErrors, fmt.Sprintf(
343+ "engine.cef.render_stack must be one of: vulkan, egl (got: %s)",
344+ config.Engine.CEF.RenderStack,
345+ ))
346+ }
347+
339348 switch config.Engine.CEF.LogSeverity {
340349 case 0, 1, 2, 3, 4, cefLogSeverityDisabled:
341350 default:
@@ -336,6 +336,15 @@ func validateLogging(config *Config) []string {
336 func validateCEF(config *Config) []string {336 func validateCEF(config *Config) []string {
337 var validationErrors []string337 var validationErrors []string
338 338
339+ switch config.Engine.CEF.CEFRenderStack() {
340+ case CEFRenderStackVulkan, CEFRenderStackEGL:
341+ default:
342+ validationErrors = append(validationErrors, fmt.Sprintf(
343+ "engine.cef.render_stack must be one of: vulkan, egl (got: %s)",
344+ config.Engine.CEF.RenderStack,
345+ ))
346+ }
347+
339 switch config.Engine.CEF.LogSeverity {348 switch config.Engine.CEF.LogSeverity {
340 case 0, 1, 2, 3, 4, cefLogSeverityDisabled:349 case 0, 1, 2, 3, 4, cefLogSeverityDisabled:
341 default:350 default:

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