detached 3a4bc2a

cef: default to Vulkan dmabuf render stack

commit
3a4bc2a2613b380fd7d16437b92d88e21a171fd2
author
brice · · 2026-05-01 13:41 UTC
parent
7f761e3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +169 -25
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 | `auto` | `auto`, `vulkan-dmabuf`, `legacy-gl` |
62+| `DUMBER_RENDER_STACK` env | string | `vulkan-dmabuf` | `vulkan-dmabuf`, `auto`, `legacy-gl` |
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 | `auto` | `auto`, `vulkan-dmabuf`, `legacy-gl` |62+| `DUMBER_RENDER_STACK` env | string | `vulkan-dmabuf` | `vulkan-dmabuf`, `auto`, `legacy-gl` |
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 go.mod +2 -4
@@ -7,10 +7,10 @@ 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.12.0
10- github.com/bnema/purego-cef2gtk v0.0.0-00010101000000-000000000000
10+ github.com/bnema/purego-cef2gtk v0.0.0-20260501133909-bb0f269889fd
1111 github.com/bnema/purego-pipewire v0.1.2
1212 github.com/bnema/purego-sqlite v0.1.2
13- github.com/bnema/puregotk v0.5.1
13+ github.com/bnema/puregotk v0.5.2-0.20260501133710-759d3a128c40
1414 github.com/charmbracelet/bubbles v1.0.0
1515 github.com/charmbracelet/bubbletea v1.3.10
1616 github.com/charmbracelet/lipgloss v1.1.0
@@ -78,8 +78,6 @@ require (
7878 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
7979 )
8080
81-replace github.com/bnema/purego-cef2gtk => ../purego-cef2gtk
82-
8381 tool github.com/a-h/templ/cmd/templ
8482
8583 replace github.com/bnema/purego-pipewire => ../purego-pipewire
@@ -7,10 +7,10 @@ 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.12.09 github.com/bnema/purego-cef v0.12.0
10- github.com/bnema/purego-cef2gtk v0.0.0-00010101000000-00000000000010+ github.com/bnema/purego-cef2gtk v0.0.0-20260501133909-bb0f269889fd
11 github.com/bnema/purego-pipewire v0.1.211 github.com/bnema/purego-pipewire v0.1.2
12 github.com/bnema/purego-sqlite v0.1.212 github.com/bnema/purego-sqlite v0.1.2
13- github.com/bnema/puregotk v0.5.113+ github.com/bnema/puregotk v0.5.2-0.20260501133710-759d3a128c40
14 github.com/charmbracelet/bubbles v1.0.014 github.com/charmbracelet/bubbles v1.0.0
15 github.com/charmbracelet/bubbletea v1.3.1015 github.com/charmbracelet/bubbletea v1.3.10
16 github.com/charmbracelet/lipgloss v1.1.016 github.com/charmbracelet/lipgloss v1.1.0
@@ -78,8 +78,6 @@ require (
78 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect78 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
79 )79 )
80 80
81-replace github.com/bnema/purego-cef2gtk => ../purego-cef2gtk
82-
83 tool github.com/a-h/templ/cmd/templ81 tool github.com/a-h/templ/cmd/templ
84 82
85 replace github.com/bnema/purego-pipewire => ../purego-pipewire83 replace github.com/bnema/purego-pipewire => ../purego-pipewire
M go.sum +4 -2 generated file: collapsed
M internal/infrastructure/cef/app.go +24 -1
@@ -15,6 +15,7 @@ const (
1515 chromiumEnableFeaturesSwitch = "enable-features"
1616 chromiumDisableFeaturesSwitch = "disable-features"
1717 chromiumDisableBlinkFeaturesSwitch = "disable-blink-features"
18+ chromiumRenderNodeOverrideSwitch = "render-node-override"
1819 )
1920
2021 // Chromium's core Blink runtime flag for the Web Authentication API is
@@ -60,6 +61,7 @@ func configureCommandLine(commandLine purecef.CommandLine) {
6061
6162 configureHardwareVideoDecode(commandLine)
6263 configureEnvChromiumFlags(commandLine)
64+ configureRenderNodeOverride(commandLine)
6365 configureWebAuthnFeaturePolicy(commandLine)
6466 }
6567
@@ -90,6 +92,23 @@ func configureEnvChromiumFlags(commandLine purecef.CommandLine) {
9092 }
9193 }
9294
95+func configureRenderNodeOverride(commandLine purecef.CommandLine) {
96+ if commandLine == nil {
97+ return
98+ }
99+ value := strings.TrimSpace(os.Getenv(cefRenderNodeEnvVar))
100+ if value == "" {
101+ return
102+ }
103+ switch strings.ToLower(value) {
104+ case "auto", "default", "none", "off", "disable", "disabled":
105+ commandLine.RemoveSwitch(chromiumRenderNodeOverrideSwitch)
106+ return
107+ }
108+ commandLine.RemoveSwitch(chromiumRenderNodeOverrideSwitch)
109+ commandLine.AppendSwitchWithValue(chromiumRenderNodeOverrideSwitch, value)
110+}
111+
93112 func parseChromiumFlagsEnv(raw string) []string {
94113 // Reuse Dumber's command-line tokenizer so developer env flags can contain
95114 // quoted values without adding a second shell-like parser in the CEF adapter.
@@ -288,6 +307,7 @@ func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLin
288307 }
289308 }
290309
310+ renderNodeOverride := commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch)
291311 cmdline := commandLine.GetCommandLineString()
292312 if len(cmdline) > maxCmdLineLogLen {
293313 runes := []rune(cmdline)
@@ -297,6 +317,7 @@ func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLin
297317 }
298318 log.Debug().
299319 Str("process_type", processType).
320+ Str("render_node_override", renderNodeOverride).
300321 Str("command_line", cmdline).
301322 Msg("cef: OnBeforeCommandLineProcessing")
302323 }
@@ -337,6 +358,7 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
337358 commandLineString := ""
338359 useAngle := ""
339360 ozonePlatform := ""
361+ renderNodeOverride := ""
340362 if commandLine != nil {
341363 configureCommandLine(commandLine)
342364 appendSwitchIfMissing(commandLine, "no-zygote")
@@ -344,8 +366,9 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
344366 commandLineString = commandLine.GetCommandLineString()
345367 useAngle = commandLine.GetSwitchValue("use-angle")
346368 ozonePlatform = commandLine.GetSwitchValue("ozone-platform")
369+ renderNodeOverride = commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch)
347370 }
348- h.engine.recordChildProcessLaunch(processType, useAngle, ozonePlatform, commandLineString)
371+ h.engine.recordChildProcessLaunch(processType, useAngle, ozonePlatform, renderNodeOverride, commandLineString)
349372 }
350373
351374 func appendSwitchIfMissing(commandLine purecef.CommandLine, name string) {
@@ -15,6 +15,7 @@ const (
15 chromiumEnableFeaturesSwitch = "enable-features"15 chromiumEnableFeaturesSwitch = "enable-features"
16 chromiumDisableFeaturesSwitch = "disable-features"16 chromiumDisableFeaturesSwitch = "disable-features"
17 chromiumDisableBlinkFeaturesSwitch = "disable-blink-features"17 chromiumDisableBlinkFeaturesSwitch = "disable-blink-features"
18+ chromiumRenderNodeOverrideSwitch = "render-node-override"
18 )19 )
19 20
20 // Chromium's core Blink runtime flag for the Web Authentication API is21 // Chromium's core Blink runtime flag for the Web Authentication API is
@@ -60,6 +61,7 @@ func configureCommandLine(commandLine purecef.CommandLine) {
60 61
61 configureHardwareVideoDecode(commandLine)62 configureHardwareVideoDecode(commandLine)
62 configureEnvChromiumFlags(commandLine)63 configureEnvChromiumFlags(commandLine)
64+ configureRenderNodeOverride(commandLine)
63 configureWebAuthnFeaturePolicy(commandLine)65 configureWebAuthnFeaturePolicy(commandLine)
64 }66 }
65 67
@@ -90,6 +92,23 @@ func configureEnvChromiumFlags(commandLine purecef.CommandLine) {
90 }92 }
91 }93 }
92 94
95+func configureRenderNodeOverride(commandLine purecef.CommandLine) {
96+ if commandLine == nil {
97+ return
98+ }
99+ value := strings.TrimSpace(os.Getenv(cefRenderNodeEnvVar))
100+ if value == "" {
101+ return
102+ }
103+ switch strings.ToLower(value) {
104+ case "auto", "default", "none", "off", "disable", "disabled":
105+ commandLine.RemoveSwitch(chromiumRenderNodeOverrideSwitch)
106+ return
107+ }
108+ commandLine.RemoveSwitch(chromiumRenderNodeOverrideSwitch)
109+ commandLine.AppendSwitchWithValue(chromiumRenderNodeOverrideSwitch, value)
110+}
111+
93 func parseChromiumFlagsEnv(raw string) []string {112 func parseChromiumFlagsEnv(raw string) []string {
94 // Reuse Dumber's command-line tokenizer so developer env flags can contain113 // Reuse Dumber's command-line tokenizer so developer env flags can contain
95 // quoted values without adding a second shell-like parser in the CEF adapter.114 // quoted values without adding a second shell-like parser in the CEF adapter.
@@ -288,6 +307,7 @@ func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLin
288 }307 }
289 }308 }
290 309
310+ renderNodeOverride := commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch)
291 cmdline := commandLine.GetCommandLineString()311 cmdline := commandLine.GetCommandLineString()
292 if len(cmdline) > maxCmdLineLogLen {312 if len(cmdline) > maxCmdLineLogLen {
293 runes := []rune(cmdline)313 runes := []rune(cmdline)
@@ -297,6 +317,7 @@ func (a *dumberApp) OnBeforeCommandLineProcessing(processType string, commandLin
297 }317 }
298 log.Debug().318 log.Debug().
299 Str("process_type", processType).319 Str("process_type", processType).
320+ Str("render_node_override", renderNodeOverride).
300 Str("command_line", cmdline).321 Str("command_line", cmdline).
301 Msg("cef: OnBeforeCommandLineProcessing")322 Msg("cef: OnBeforeCommandLineProcessing")
302 }323 }
@@ -337,6 +358,7 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
337 commandLineString := ""358 commandLineString := ""
338 useAngle := ""359 useAngle := ""
339 ozonePlatform := ""360 ozonePlatform := ""
361+ renderNodeOverride := ""
340 if commandLine != nil {362 if commandLine != nil {
341 configureCommandLine(commandLine)363 configureCommandLine(commandLine)
342 appendSwitchIfMissing(commandLine, "no-zygote")364 appendSwitchIfMissing(commandLine, "no-zygote")
@@ -344,8 +366,9 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
344 commandLineString = commandLine.GetCommandLineString()366 commandLineString = commandLine.GetCommandLineString()
345 useAngle = commandLine.GetSwitchValue("use-angle")367 useAngle = commandLine.GetSwitchValue("use-angle")
346 ozonePlatform = commandLine.GetSwitchValue("ozone-platform")368 ozonePlatform = commandLine.GetSwitchValue("ozone-platform")
369+ renderNodeOverride = commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch)
347 }370 }
348- h.engine.recordChildProcessLaunch(processType, useAngle, ozonePlatform, commandLineString)371+ h.engine.recordChildProcessLaunch(processType, useAngle, ozonePlatform, renderNodeOverride, commandLineString)
349 }372 }
350 373
351 func appendSwitchIfMissing(commandLine purecef.CommandLine, name string) {374 func appendSwitchIfMissing(commandLine purecef.CommandLine, name string) {
M internal/infrastructure/cef/app_test.go +30 -0
@@ -433,6 +433,36 @@ func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) {
433433 }
434434 }
435435
436+func TestConfigureCommandLine_CEFRenderNodeEnvOverridesExistingRenderNode(t *testing.T) {
437+ t.Setenv(cefRenderNodeEnvVar, "/dev/dri/renderD129")
438+
439+ commandLine := newMutableCommandLineStub()
440+ commandLine.AppendSwitchWithValue(chromiumRenderNodeOverrideSwitch, "/dev/dri/renderD128")
441+
442+ configureCommandLine(commandLine)
443+
444+ if got := commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch); got != "/dev/dri/renderD129" {
445+ t.Fatalf("render node override = %q, want /dev/dri/renderD129", got)
446+ }
447+}
448+
449+func TestConfigureCommandLine_CEFRenderNodeEnvSentinelsRemoveExistingRenderNode(t *testing.T) {
450+ for _, value := range []string{"auto", "default", "none", "off", "disable", "disabled"} {
451+ t.Run(value, func(t *testing.T) {
452+ t.Setenv(cefRenderNodeEnvVar, value)
453+
454+ commandLine := newMutableCommandLineStub()
455+ commandLine.AppendSwitchWithValue(chromiumRenderNodeOverrideSwitch, "/dev/dri/renderD128")
456+
457+ configureCommandLine(commandLine)
458+
459+ if commandLine.HasSwitch(chromiumRenderNodeOverrideSwitch) {
460+ t.Fatalf("render node override still present: %q", commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch))
461+ }
462+ })
463+ }
464+}
465+
436466 func TestDumberBPH_OnBeforeChildProcessLaunch_AppliesRenderingAndHardwareDecodeFlags(t *testing.T) {
437467 t.Setenv(cef2gtkAngleBackendVar, "vulkan")
438468 t.Setenv(cefEnableVAAPIEnvVar, "1")
@@ -433,6 +433,36 @@ func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) {
433 }433 }
434 }434 }
435 435
436+func TestConfigureCommandLine_CEFRenderNodeEnvOverridesExistingRenderNode(t *testing.T) {
437+ t.Setenv(cefRenderNodeEnvVar, "/dev/dri/renderD129")
438+
439+ commandLine := newMutableCommandLineStub()
440+ commandLine.AppendSwitchWithValue(chromiumRenderNodeOverrideSwitch, "/dev/dri/renderD128")
441+
442+ configureCommandLine(commandLine)
443+
444+ if got := commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch); got != "/dev/dri/renderD129" {
445+ t.Fatalf("render node override = %q, want /dev/dri/renderD129", got)
446+ }
447+}
448+
449+func TestConfigureCommandLine_CEFRenderNodeEnvSentinelsRemoveExistingRenderNode(t *testing.T) {
450+ for _, value := range []string{"auto", "default", "none", "off", "disable", "disabled"} {
451+ t.Run(value, func(t *testing.T) {
452+ t.Setenv(cefRenderNodeEnvVar, value)
453+
454+ commandLine := newMutableCommandLineStub()
455+ commandLine.AppendSwitchWithValue(chromiumRenderNodeOverrideSwitch, "/dev/dri/renderD128")
456+
457+ configureCommandLine(commandLine)
458+
459+ if commandLine.HasSwitch(chromiumRenderNodeOverrideSwitch) {
460+ t.Fatalf("render node override still present: %q", commandLine.GetSwitchValue(chromiumRenderNodeOverrideSwitch))
461+ }
462+ })
463+ }
464+}
465+
436 func TestDumberBPH_OnBeforeChildProcessLaunch_AppliesRenderingAndHardwareDecodeFlags(t *testing.T) {466 func TestDumberBPH_OnBeforeChildProcessLaunch_AppliesRenderingAndHardwareDecodeFlags(t *testing.T) {
437 t.Setenv(cef2gtkAngleBackendVar, "vulkan")467 t.Setenv(cef2gtkAngleBackendVar, "vulkan")
438 t.Setenv(cefEnableVAAPIEnvVar, "1")468 t.Setenv(cefEnableVAAPIEnvVar, "1")
M internal/infrastructure/cef/engine.go +2 -1
@@ -409,7 +409,7 @@ func (e *Engine) recordContextInitialized() {
409409 Msg("cef: OnContextInitialized")
410410 }
411411
412-func (e *Engine) recordChildProcessLaunch(processType, useAngle, ozonePlatform, commandLine string) {
412+func (e *Engine) recordChildProcessLaunch(processType, useAngle, ozonePlatform, renderNodeOverride, commandLine string) {
413413 switch processType {
414414 case "renderer":
415415 e.childLaunchRenderer.Add(1)
@@ -423,6 +423,7 @@ func (e *Engine) recordChildProcessLaunch(processType, useAngle, ozonePlatform,
423423 Str("process_type", processType).
424424 Str("use_angle", useAngle).
425425 Str("ozone_platform", ozonePlatform).
426+ Str("render_node_override", renderNodeOverride).
426427 Str("command_line", commandLine).
427428 Uint64("renderer_launches", e.childLaunchRenderer.Load()).
428429 Uint64("gpu_launches", e.childLaunchGPU.Load()).
@@ -409,7 +409,7 @@ func (e *Engine) recordContextInitialized() {
409 Msg("cef: OnContextInitialized")409 Msg("cef: OnContextInitialized")
410 }410 }
411 411
412-func (e *Engine) recordChildProcessLaunch(processType, useAngle, ozonePlatform, commandLine string) {412+func (e *Engine) recordChildProcessLaunch(processType, useAngle, ozonePlatform, renderNodeOverride, commandLine string) {
413 switch processType {413 switch processType {
414 case "renderer":414 case "renderer":
415 e.childLaunchRenderer.Add(1)415 e.childLaunchRenderer.Add(1)
@@ -423,6 +423,7 @@ func (e *Engine) recordChildProcessLaunch(processType, useAngle, ozonePlatform,
423 Str("process_type", processType).423 Str("process_type", processType).
424 Str("use_angle", useAngle).424 Str("use_angle", useAngle).
425 Str("ozone_platform", ozonePlatform).425 Str("ozone_platform", ozonePlatform).
426+ Str("render_node_override", renderNodeOverride).
426 Str("command_line", commandLine).427 Str("command_line", commandLine).
427 Uint64("renderer_launches", e.childLaunchRenderer.Load()).428 Uint64("renderer_launches", e.childLaunchRenderer.Load()).
428 Uint64("gpu_launches", e.childLaunchGPU.Load()).429 Uint64("gpu_launches", e.childLaunchGPU.Load()).
M internal/infrastructure/cef/env.go +1 -0
@@ -10,6 +10,7 @@ const (
1010 cefEnableWebAuthnUnsafeEnvVar = "DUMBER_CEF_ENABLE_WEBAUTHN_UNSAFE"
1111 cefChromiumFlagsEnvVar = "DUMBER_CEF_CHROMIUM_FLAGS"
1212 cefEnableVAAPIEnvVar = "DUMBER_CEF_ENABLE_VAAPI"
13+ cefRenderNodeEnvVar = "DUMBER_CEF_RENDER_NODE"
1314 cefRenderStallRecoveryEnvVar = "DUMBER_CEF_RENDER_STALL_RECOVERY"
1415 cefRenderStallBacktraceEnvVar = "DUMBER_CEF_RENDER_STALL_BACKTRACE"
1516 )
@@ -10,6 +10,7 @@ const (
10 cefEnableWebAuthnUnsafeEnvVar = "DUMBER_CEF_ENABLE_WEBAUTHN_UNSAFE"10 cefEnableWebAuthnUnsafeEnvVar = "DUMBER_CEF_ENABLE_WEBAUTHN_UNSAFE"
11 cefChromiumFlagsEnvVar = "DUMBER_CEF_CHROMIUM_FLAGS"11 cefChromiumFlagsEnvVar = "DUMBER_CEF_CHROMIUM_FLAGS"
12 cefEnableVAAPIEnvVar = "DUMBER_CEF_ENABLE_VAAPI"12 cefEnableVAAPIEnvVar = "DUMBER_CEF_ENABLE_VAAPI"
13+ cefRenderNodeEnvVar = "DUMBER_CEF_RENDER_NODE"
13 cefRenderStallRecoveryEnvVar = "DUMBER_CEF_RENDER_STALL_RECOVERY"14 cefRenderStallRecoveryEnvVar = "DUMBER_CEF_RENDER_STALL_RECOVERY"
14 cefRenderStallBacktraceEnvVar = "DUMBER_CEF_RENDER_STALL_BACKTRACE"15 cefRenderStallBacktraceEnvVar = "DUMBER_CEF_RENDER_STALL_BACKTRACE"
15 )16 )
M internal/infrastructure/cef/render_diagnostics.go +13 -0
@@ -81,11 +81,24 @@ func (wv *WebView) logRenderDiagnosticSnapshot(
8181 Uint64("audio_packets", wv.audioPacketCount.Load()).
8282 Uint64("audio_writes", wv.audioWriteCount.Load()).
8383 Int64("last_accelerated_paint_age_ms", durationMillis(lastPaintAge)).
84+ Str("render_backend", diag.Backend).
8485 Int("accelerated_paints", diag.AcceleratedPaints).
8586 Int("unsupported_paints", diag.UnsupportedPaints).
8687 Int("accelerated_paint_errors", diag.AcceleratedPaintErrors).
8788 Int("import_failures", diag.ImportFailures).
8889 Int("render_failures", diag.RenderFailures).
90+ Int("textures_built", diag.TexturesBuilt).
91+ Int("texture_build_failures", diag.TextureBuildFailures).
92+ Int("fd_dup_failures", diag.FDDupFailures).
93+ Int("unsupported_formats", diag.UnsupportedFormats).
94+ Int("paintable_swaps", diag.PaintableSwaps).
95+ Bool("pending_frame", diag.PendingFrame).
96+ Bool("pending_scheduled", diag.PendingScheduled).
97+ Int64("pending_age_ms", durationMillis(diag.PendingAge)).
98+ Uint("pending_source_id", diag.PendingSourceID).
99+ Int("pending_reschedules", diag.PendingReschedules).
100+ Int("pending_schedule_failures", diag.PendingScheduleFailures).
101+ Int("pending_idle_callbacks", diag.PendingIdleCallbacks).
89102 Int32("surface_width", width).
90103 Int32("surface_height", height).
91104 Float32("surface_scale", scale).
@@ -81,11 +81,24 @@ func (wv *WebView) logRenderDiagnosticSnapshot(
81 Uint64("audio_packets", wv.audioPacketCount.Load()).81 Uint64("audio_packets", wv.audioPacketCount.Load()).
82 Uint64("audio_writes", wv.audioWriteCount.Load()).82 Uint64("audio_writes", wv.audioWriteCount.Load()).
83 Int64("last_accelerated_paint_age_ms", durationMillis(lastPaintAge)).83 Int64("last_accelerated_paint_age_ms", durationMillis(lastPaintAge)).
84+ Str("render_backend", diag.Backend).
84 Int("accelerated_paints", diag.AcceleratedPaints).85 Int("accelerated_paints", diag.AcceleratedPaints).
85 Int("unsupported_paints", diag.UnsupportedPaints).86 Int("unsupported_paints", diag.UnsupportedPaints).
86 Int("accelerated_paint_errors", diag.AcceleratedPaintErrors).87 Int("accelerated_paint_errors", diag.AcceleratedPaintErrors).
87 Int("import_failures", diag.ImportFailures).88 Int("import_failures", diag.ImportFailures).
88 Int("render_failures", diag.RenderFailures).89 Int("render_failures", diag.RenderFailures).
90+ Int("textures_built", diag.TexturesBuilt).
91+ Int("texture_build_failures", diag.TextureBuildFailures).
92+ Int("fd_dup_failures", diag.FDDupFailures).
93+ Int("unsupported_formats", diag.UnsupportedFormats).
94+ Int("paintable_swaps", diag.PaintableSwaps).
95+ Bool("pending_frame", diag.PendingFrame).
96+ Bool("pending_scheduled", diag.PendingScheduled).
97+ Int64("pending_age_ms", durationMillis(diag.PendingAge)).
98+ Uint("pending_source_id", diag.PendingSourceID).
99+ Int("pending_reschedules", diag.PendingReschedules).
100+ Int("pending_schedule_failures", diag.PendingScheduleFailures).
101+ Int("pending_idle_callbacks", diag.PendingIdleCallbacks).
89 Int32("surface_width", width).102 Int32("surface_width", width).
90 Int32("surface_height", height).103 Int32("surface_height", height).
91 Float32("surface_scale", scale).104 Float32("surface_scale", scale).
M internal/infrastructure/cef/render_stack_env.go +54 -15
@@ -12,7 +12,8 @@ import (
1212 )
1313
1414 const (
15- dumberRenderStackEnvVar = "DUMBER_RENDER_STACK"
15+ dumberRenderStackEnvVar = "DUMBER_RENDER_STACK"
16+ dumberRenderStackAllowSplitEnvVar = "DUMBER_RENDER_STACK_ALLOW_SPLIT"
1617
1718 renderStackAuto = "auto"
1819 renderStackVulkanDMABUF = "vulkan-dmabuf"
@@ -32,29 +33,45 @@ const (
3233 // Dumber's default CEF presentation stack before GTK/libadwaita initialize.
3334 //
3435 // DUMBER_RENDER_STACK accepts:
35-// - auto or empty: use the GPU-first GDK DMABUF stack
36-// - vulkan-dmabuf: force GDK DMABUF presentation with GSK Vulkan and CEF ANGLE Vulkan
36+// - empty/default or vulkan-dmabuf: use GDK DMABUF presentation with GSK Vulkan and CEF ANGLE Vulkan
37+// - auto: alias for vulkan-dmabuf
3738 // - legacy-gl: force the older GtkGLArea/OpenGL bridge for diagnostics
3839 //
39-// Low-level env vars remain diagnostic escape hatches: when explicitly set,
40-// they are preserved instead of overwritten.
40+// Low-level env vars are treated as library-development escape hatches only.
41+// Dumber keeps the selected stack coherent by default and overwrites conflicts;
42+// set DUMBER_RENDER_STACK_ALLOW_SPLIT=1 to preserve explicit split-stack values.
4143 func ApplyDefaultRenderStackEnvironment(ctx context.Context) string {
4244 return applyDefaultRenderStackEnvironment(ctx)
4345 }
4446
4547 func applyDefaultRenderStackEnvironment(ctx context.Context) string {
46- switch normalizeRenderStack(os.Getenv(dumberRenderStackEnvVar)) {
48+ stack := normalizeRenderStack(os.Getenv(dumberRenderStackEnvVar))
49+ switch stack {
4750 case renderStackLegacyGL:
48- setEnvDefault(ctx, gskRendererEnvVar, gskRendererOpenGL)
49- setEnvDefault(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGLArea)
50- setEnvDefault(ctx, cef2gtkAngleBackendVar, cef2gtkAngleGLEGL)
51- return renderStackLegacyGL
51+ setRenderStackEnv(ctx, gskRendererEnvVar, gskRendererOpenGL)
52+ setRenderStackEnv(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGLArea)
53+ setRenderStackEnv(ctx, cef2gtkAngleBackendVar, cef2gtkAngleGLEGL)
5254 default:
53- setEnvDefault(ctx, gskRendererEnvVar, gskRendererVulkan)
54- setEnvDefault(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGDKDMABUF)
55- setEnvDefault(ctx, cef2gtkAngleBackendVar, cef2gtkAngleVulkan)
56- return renderStackVulkanDMABUF
55+ stack = renderStackVulkanDMABUF
56+ setRenderStackEnv(ctx, gskRendererEnvVar, gskRendererVulkan)
57+ setRenderStackEnv(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGDKDMABUF)
58+ setRenderStackEnv(ctx, cef2gtkAngleBackendVar, cef2gtkAngleVulkan)
59+ }
60+ logRenderStackEnvironment(ctx, stack)
61+ return stack
62+}
63+
64+func logRenderStackEnvironment(ctx context.Context, stack string) {
65+ if ctx == nil {
66+ ctx = context.Background()
5767 }
68+ logging.FromContext(ctx).Info().
69+ Str("render_stack", stack).
70+ Str("gsk_renderer", os.Getenv(gskRendererEnvVar)).
71+ Str("cef2gtk_backend", os.Getenv(cef2gtkBackendEnvVar)).
72+ Str("cef2gtk_angle_backend", os.Getenv(cef2gtkAngleBackendVar)).
73+ Bool("split_stack_allowed", envBoolEnabled(dumberRenderStackAllowSplitEnvVar)).
74+ Msg("cef: render stack environment configured")
5875 }
5976
6077 // ApplyDefaultHardwareDecodeEnvironment maps Dumber's media config to CEF's
@@ -102,6 +119,28 @@ func normalizeRenderStack(value string) string {
102119 }
103120 }
104121
122+func setRenderStackEnv(ctx context.Context, key, value string) {
123+ if existing := strings.TrimSpace(os.Getenv(key)); existing != "" {
124+ if ctx == nil {
125+ ctx = context.Background()
126+ }
127+ if envBoolEnabled(dumberRenderStackAllowSplitEnvVar) {
128+ logging.FromContext(ctx).Warn().
129+ Str("key", key).
130+ Str("value", existing).
131+ Str("stack_value", value).
132+ Msg("cef: preserving explicit split render stack environment override")
133+ return
134+ }
135+ logging.FromContext(ctx).Warn().
136+ Str("key", key).
137+ Str("value", existing).
138+ Str("stack_value", value).
139+ Msg("cef: overriding low-level render stack environment to keep Dumber stack coherent")
140+ }
141+ _ = os.Setenv(key, value)
142+}
143+
105144 func setEnvDefault(ctx context.Context, key, value string) {
106145 if existing := strings.TrimSpace(os.Getenv(key)); existing != "" {
107146 if ctx == nil {
@@ -110,7 +149,7 @@ func setEnvDefault(ctx context.Context, key, value string) {
110149 logging.FromContext(ctx).Debug().
111150 Str("key", key).
112151 Str("value", existing).
113- Msg("cef: preserving explicit render stack environment override")
152+ Msg("cef: preserving explicit environment override")
114153 return
115154 }
116155 _ = os.Setenv(key, value)
@@ -12,7 +12,8 @@ import (
12 )12 )
13 13
14 const (14 const (
15- dumberRenderStackEnvVar = "DUMBER_RENDER_STACK"15+ dumberRenderStackEnvVar = "DUMBER_RENDER_STACK"
16+ dumberRenderStackAllowSplitEnvVar = "DUMBER_RENDER_STACK_ALLOW_SPLIT"
16 17
17 renderStackAuto = "auto"18 renderStackAuto = "auto"
18 renderStackVulkanDMABUF = "vulkan-dmabuf"19 renderStackVulkanDMABUF = "vulkan-dmabuf"
@@ -32,29 +33,45 @@ const (
32 // Dumber's default CEF presentation stack before GTK/libadwaita initialize.33 // Dumber's default CEF presentation stack before GTK/libadwaita initialize.
33 //34 //
34 // DUMBER_RENDER_STACK accepts:35 // DUMBER_RENDER_STACK accepts:
35-// - auto or empty: use the GPU-first GDK DMABUF stack36+// - empty/default or vulkan-dmabuf: use GDK DMABUF presentation with GSK Vulkan and CEF ANGLE Vulkan
36-// - vulkan-dmabuf: force GDK DMABUF presentation with GSK Vulkan and CEF ANGLE Vulkan37+// - auto: alias for vulkan-dmabuf
37 // - legacy-gl: force the older GtkGLArea/OpenGL bridge for diagnostics38 // - legacy-gl: force the older GtkGLArea/OpenGL bridge for diagnostics
38 //39 //
39-// Low-level env vars remain diagnostic escape hatches: when explicitly set,40+// Low-level env vars are treated as library-development escape hatches only.
40-// they are preserved instead of overwritten.41+// Dumber keeps the selected stack coherent by default and overwrites conflicts;
42+// set DUMBER_RENDER_STACK_ALLOW_SPLIT=1 to preserve explicit split-stack values.
41 func ApplyDefaultRenderStackEnvironment(ctx context.Context) string {43 func ApplyDefaultRenderStackEnvironment(ctx context.Context) string {
42 return applyDefaultRenderStackEnvironment(ctx)44 return applyDefaultRenderStackEnvironment(ctx)
43 }45 }
44 46
45 func applyDefaultRenderStackEnvironment(ctx context.Context) string {47 func applyDefaultRenderStackEnvironment(ctx context.Context) string {
46- switch normalizeRenderStack(os.Getenv(dumberRenderStackEnvVar)) {48+ stack := normalizeRenderStack(os.Getenv(dumberRenderStackEnvVar))
49+ switch stack {
47 case renderStackLegacyGL:50 case renderStackLegacyGL:
48- setEnvDefault(ctx, gskRendererEnvVar, gskRendererOpenGL)51+ setRenderStackEnv(ctx, gskRendererEnvVar, gskRendererOpenGL)
49- setEnvDefault(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGLArea)52+ setRenderStackEnv(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGLArea)
50- setEnvDefault(ctx, cef2gtkAngleBackendVar, cef2gtkAngleGLEGL)53+ setRenderStackEnv(ctx, cef2gtkAngleBackendVar, cef2gtkAngleGLEGL)
51- return renderStackLegacyGL
52 default:54 default:
53- setEnvDefault(ctx, gskRendererEnvVar, gskRendererVulkan)55+ stack = renderStackVulkanDMABUF
54- setEnvDefault(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGDKDMABUF)56+ setRenderStackEnv(ctx, gskRendererEnvVar, gskRendererVulkan)
55- setEnvDefault(ctx, cef2gtkAngleBackendVar, cef2gtkAngleVulkan)57+ setRenderStackEnv(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGDKDMABUF)
56- return renderStackVulkanDMABUF58+ setRenderStackEnv(ctx, cef2gtkAngleBackendVar, cef2gtkAngleVulkan)
59+ }
60+ logRenderStackEnvironment(ctx, stack)
61+ return stack
62+}
63+
64+func logRenderStackEnvironment(ctx context.Context, stack string) {
65+ if ctx == nil {
66+ ctx = context.Background()
57 }67 }
68+ logging.FromContext(ctx).Info().
69+ Str("render_stack", stack).
70+ Str("gsk_renderer", os.Getenv(gskRendererEnvVar)).
71+ Str("cef2gtk_backend", os.Getenv(cef2gtkBackendEnvVar)).
72+ Str("cef2gtk_angle_backend", os.Getenv(cef2gtkAngleBackendVar)).
73+ Bool("split_stack_allowed", envBoolEnabled(dumberRenderStackAllowSplitEnvVar)).
74+ Msg("cef: render stack environment configured")
58 }75 }
59 76
60 // ApplyDefaultHardwareDecodeEnvironment maps Dumber's media config to CEF's77 // ApplyDefaultHardwareDecodeEnvironment maps Dumber's media config to CEF's
@@ -102,6 +119,28 @@ func normalizeRenderStack(value string) string {
102 }119 }
103 }120 }
104 121
122+func setRenderStackEnv(ctx context.Context, key, value string) {
123+ if existing := strings.TrimSpace(os.Getenv(key)); existing != "" {
124+ if ctx == nil {
125+ ctx = context.Background()
126+ }
127+ if envBoolEnabled(dumberRenderStackAllowSplitEnvVar) {
128+ logging.FromContext(ctx).Warn().
129+ Str("key", key).
130+ Str("value", existing).
131+ Str("stack_value", value).
132+ Msg("cef: preserving explicit split render stack environment override")
133+ return
134+ }
135+ logging.FromContext(ctx).Warn().
136+ Str("key", key).
137+ Str("value", existing).
138+ Str("stack_value", value).
139+ Msg("cef: overriding low-level render stack environment to keep Dumber stack coherent")
140+ }
141+ _ = os.Setenv(key, value)
142+}
143+
105 func setEnvDefault(ctx context.Context, key, value string) {144 func setEnvDefault(ctx context.Context, key, value string) {
106 if existing := strings.TrimSpace(os.Getenv(key)); existing != "" {145 if existing := strings.TrimSpace(os.Getenv(key)); existing != "" {
107 if ctx == nil {146 if ctx == nil {
@@ -110,7 +149,7 @@ func setEnvDefault(ctx context.Context, key, value string) {
110 logging.FromContext(ctx).Debug().149 logging.FromContext(ctx).Debug().
111 Str("key", key).150 Str("key", key).
112 Str("value", existing).151 Str("value", existing).
113- Msg("cef: preserving explicit render stack environment override")152+ Msg("cef: preserving explicit environment override")
114 return153 return
115 }154 }
116 _ = os.Setenv(key, value)155 _ = os.Setenv(key, value)
M internal/infrastructure/cef/render_stack_env_test.go +25 -1
@@ -30,8 +30,32 @@ func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEVulkan(t
3030 }
3131 }
3232
33-func TestApplyDefaultRenderStackEnvironment_PreservesExplicitLowLevelOverrides(t *testing.T) {
33+func TestApplyDefaultRenderStackEnvironment_OverridesConflictingLowLevelDefaults(t *testing.T) {
3434 t.Setenv(dumberRenderStackEnvVar, "")
35+ t.Setenv(dumberRenderStackAllowSplitEnvVar, "")
36+ t.Setenv("GSK_RENDERER", "ngl")
37+ t.Setenv("PUREGO_CEF2GTK_BACKEND", "glarea")
38+ t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "gl-egl")
39+
40+ got := applyDefaultRenderStackEnvironment(context.Background())
41+
42+ if got != renderStackVulkanDMABUF {
43+ t.Fatalf("render stack = %q, want %q", got, renderStackVulkanDMABUF)
44+ }
45+ if got := os.Getenv("GSK_RENDERER"); got != "vulkan" {
46+ t.Fatalf("GSK_RENDERER = %q, want coherent vulkan default", got)
47+ }
48+ if got := os.Getenv("PUREGO_CEF2GTK_BACKEND"); got != "gdk-dmabuf" {
49+ t.Fatalf("PUREGO_CEF2GTK_BACKEND = %q, want coherent gdk-dmabuf default", got)
50+ }
51+ if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "vulkan" {
52+ t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want coherent vulkan default", got)
53+ }
54+}
55+
56+func TestApplyDefaultRenderStackEnvironment_AllowSplitPreservesExplicitLowLevelOverrides(t *testing.T) {
57+ t.Setenv(dumberRenderStackEnvVar, "")
58+ t.Setenv(dumberRenderStackAllowSplitEnvVar, "1")
3559 t.Setenv("GSK_RENDERER", "ngl")
3660 t.Setenv("PUREGO_CEF2GTK_BACKEND", "glarea")
3761 t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "gl-egl")
@@ -30,8 +30,32 @@ func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEVulkan(t
30 }30 }
31 }31 }
32 32
33-func TestApplyDefaultRenderStackEnvironment_PreservesExplicitLowLevelOverrides(t *testing.T) {33+func TestApplyDefaultRenderStackEnvironment_OverridesConflictingLowLevelDefaults(t *testing.T) {
34 t.Setenv(dumberRenderStackEnvVar, "")34 t.Setenv(dumberRenderStackEnvVar, "")
35+ t.Setenv(dumberRenderStackAllowSplitEnvVar, "")
36+ t.Setenv("GSK_RENDERER", "ngl")
37+ t.Setenv("PUREGO_CEF2GTK_BACKEND", "glarea")
38+ t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "gl-egl")
39+
40+ got := applyDefaultRenderStackEnvironment(context.Background())
41+
42+ if got != renderStackVulkanDMABUF {
43+ t.Fatalf("render stack = %q, want %q", got, renderStackVulkanDMABUF)
44+ }
45+ if got := os.Getenv("GSK_RENDERER"); got != "vulkan" {
46+ t.Fatalf("GSK_RENDERER = %q, want coherent vulkan default", got)
47+ }
48+ if got := os.Getenv("PUREGO_CEF2GTK_BACKEND"); got != "gdk-dmabuf" {
49+ t.Fatalf("PUREGO_CEF2GTK_BACKEND = %q, want coherent gdk-dmabuf default", got)
50+ }
51+ if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "vulkan" {
52+ t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want coherent vulkan default", got)
53+ }
54+}
55+
56+func TestApplyDefaultRenderStackEnvironment_AllowSplitPreservesExplicitLowLevelOverrides(t *testing.T) {
57+ t.Setenv(dumberRenderStackEnvVar, "")
58+ t.Setenv(dumberRenderStackAllowSplitEnvVar, "1")
35 t.Setenv("GSK_RENDERER", "ngl")59 t.Setenv("GSK_RENDERER", "ngl")
36 t.Setenv("PUREGO_CEF2GTK_BACKEND", "glarea")60 t.Setenv("PUREGO_CEF2GTK_BACKEND", "glarea")
37 t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "gl-egl")61 t.Setenv("PUREGO_CEF2GTK_ANGLE_BACKEND", "gl-egl")
M internal/infrastructure/cef/render_stall_watchdog.go +13 -0
@@ -202,11 +202,24 @@ func (wv *WebView) logRenderStall(bridge *Cef2gtkAdapter, now time.Time, diag re
202202 Uint64("profile_frames_queued", profile.FramesQueued).
203203 Uint64("profile_frames_rendered", profile.FramesRendered).
204204 Uint32("profile_gc_delta", profile.GC.NumGCDelta).
205+ Str("render_backend", diag.diag.Backend).
205206 Int("accelerated_paints", diag.diag.AcceleratedPaints).
206207 Int("unsupported_paints", diag.diag.UnsupportedPaints).
207208 Int("accelerated_paint_errors", diag.diag.AcceleratedPaintErrors).
208209 Int("import_failures", diag.diag.ImportFailures).
209210 Int("render_failures", diag.diag.RenderFailures).
211+ Int("textures_built", diag.diag.TexturesBuilt).
212+ Int("texture_build_failures", diag.diag.TextureBuildFailures).
213+ Int("fd_dup_failures", diag.diag.FDDupFailures).
214+ Int("unsupported_formats", diag.diag.UnsupportedFormats).
215+ Int("paintable_swaps", diag.diag.PaintableSwaps).
216+ Bool("pending_frame", diag.diag.PendingFrame).
217+ Bool("pending_scheduled", diag.diag.PendingScheduled).
218+ Int64("pending_age_ms", durationMillis(diag.diag.PendingAge)).
219+ Uint("pending_source_id", diag.diag.PendingSourceID).
220+ Int("pending_reschedules", diag.diag.PendingReschedules).
221+ Int("pending_schedule_failures", diag.diag.PendingScheduleFailures).
222+ Int("pending_idle_callbacks", diag.diag.PendingIdleCallbacks).
210223 Str("latest_render_event_kind", diag.latestEvent.Kind).
211224 Str("latest_render_event_message", diag.latestEvent.Message).
212225 Msg("cef: accelerated rendering appears stalled")
@@ -202,11 +202,24 @@ func (wv *WebView) logRenderStall(bridge *Cef2gtkAdapter, now time.Time, diag re
202 Uint64("profile_frames_queued", profile.FramesQueued).202 Uint64("profile_frames_queued", profile.FramesQueued).
203 Uint64("profile_frames_rendered", profile.FramesRendered).203 Uint64("profile_frames_rendered", profile.FramesRendered).
204 Uint32("profile_gc_delta", profile.GC.NumGCDelta).204 Uint32("profile_gc_delta", profile.GC.NumGCDelta).
205+ Str("render_backend", diag.diag.Backend).
205 Int("accelerated_paints", diag.diag.AcceleratedPaints).206 Int("accelerated_paints", diag.diag.AcceleratedPaints).
206 Int("unsupported_paints", diag.diag.UnsupportedPaints).207 Int("unsupported_paints", diag.diag.UnsupportedPaints).
207 Int("accelerated_paint_errors", diag.diag.AcceleratedPaintErrors).208 Int("accelerated_paint_errors", diag.diag.AcceleratedPaintErrors).
208 Int("import_failures", diag.diag.ImportFailures).209 Int("import_failures", diag.diag.ImportFailures).
209 Int("render_failures", diag.diag.RenderFailures).210 Int("render_failures", diag.diag.RenderFailures).
211+ Int("textures_built", diag.diag.TexturesBuilt).
212+ Int("texture_build_failures", diag.diag.TextureBuildFailures).
213+ Int("fd_dup_failures", diag.diag.FDDupFailures).
214+ Int("unsupported_formats", diag.diag.UnsupportedFormats).
215+ Int("paintable_swaps", diag.diag.PaintableSwaps).
216+ Bool("pending_frame", diag.diag.PendingFrame).
217+ Bool("pending_scheduled", diag.diag.PendingScheduled).
218+ Int64("pending_age_ms", durationMillis(diag.diag.PendingAge)).
219+ Uint("pending_source_id", diag.diag.PendingSourceID).
220+ Int("pending_reschedules", diag.diag.PendingReschedules).
221+ Int("pending_schedule_failures", diag.diag.PendingScheduleFailures).
222+ Int("pending_idle_callbacks", diag.diag.PendingIdleCallbacks).
210 Str("latest_render_event_kind", diag.latestEvent.Kind).223 Str("latest_render_event_kind", diag.latestEvent.Kind).
211 Str("latest_render_event_message", diag.latestEvent.Message).224 Str("latest_render_event_message", diag.latestEvent.Message).
212 Msg("cef: accelerated rendering appears stalled")225 Msg("cef: accelerated rendering appears stalled")

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