detached 7f761e3

cef: harden shutdown and default VAAPI Vulkan stack

commit
7f761e37e880f6e1677977d64b6f77a0b4416f6e
author
brice · · 2026-05-01 10:51 UTC
parent
c460950
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
16 files · +330 -33
M Makefile +5 -5
@@ -42,7 +42,7 @@ help: ## Show this help message
4242 build: build-systemviews ## Build the application (pure Go, no CGO)
4343 @echo "Building $(BINARY_NAME) $(VERSION) using $(NPROCS) cores..."
4444 @mkdir -p $(DIST_DIR)
45- CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
45+ GOFLAGS=-mod=mod CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
4646 @rm -f $(DIST_DIR)/cef-helper
4747 @echo "Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)"
4848
@@ -63,7 +63,7 @@ build-systemviews: generate-systemviews ## Build the WASM systemviews runtime
6363 build-quick: ## Build quickly for backend development
6464 @echo "Building $(BINARY_NAME) $(VERSION) (quick)..."
6565 @mkdir -p $(DIST_DIR)
66- CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
66+ GOFLAGS=-mod=mod CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
6767 @rm -f $(DIST_DIR)/cef-helper
6868 @echo "Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)"
6969
@@ -102,15 +102,15 @@ mocks: ## Generate mock implementations with mockery
102102 # Testing
103103 test: ## Run tests
104104 @echo "Running tests..."
105- go test -v ./...
105+ GOFLAGS=-mod=mod go test -v ./...
106106
107107 test-race: ## Run tests with race detection
108108 @echo "Running tests with race detection..."
109- go test -race -v ./...
109+ GOFLAGS=-mod=mod go test -race -v ./...
110110
111111 test-cover: ## Run tests with coverage
112112 @echo "Running tests with coverage..."
113- go test -coverprofile=coverage.out ./...
113+ GOFLAGS=-mod=mod go test -coverprofile=coverage.out ./...
114114 go tool cover -html=coverage.out -o coverage.html
115115 @echo "Coverage report: coverage.html"
116116
@@ -42,7 +42,7 @@ help: ## Show this help message
42 build: build-systemviews ## Build the application (pure Go, no CGO)42 build: build-systemviews ## Build the application (pure Go, no CGO)
43 @echo "Building $(BINARY_NAME) $(VERSION) using $(NPROCS) cores..."43 @echo "Building $(BINARY_NAME) $(VERSION) using $(NPROCS) cores..."
44 @mkdir -p $(DIST_DIR)44 @mkdir -p $(DIST_DIR)
45- CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)45+ GOFLAGS=-mod=mod CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
46 @rm -f $(DIST_DIR)/cef-helper46 @rm -f $(DIST_DIR)/cef-helper
47 @echo "Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)"47 @echo "Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)"
48 48
@@ -63,7 +63,7 @@ build-systemviews: generate-systemviews ## Build the WASM systemviews runtime
63 build-quick: ## Build quickly for backend development63 build-quick: ## Build quickly for backend development
64 @echo "Building $(BINARY_NAME) $(VERSION) (quick)..."64 @echo "Building $(BINARY_NAME) $(VERSION) (quick)..."
65 @mkdir -p $(DIST_DIR)65 @mkdir -p $(DIST_DIR)
66- CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)66+ GOFLAGS=-mod=mod CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH)
67 @rm -f $(DIST_DIR)/cef-helper67 @rm -f $(DIST_DIR)/cef-helper
68 @echo "Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)"68 @echo "Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)"
69 69
@@ -102,15 +102,15 @@ mocks: ## Generate mock implementations with mockery
102 # Testing102 # Testing
103 test: ## Run tests103 test: ## Run tests
104 @echo "Running tests..."104 @echo "Running tests..."
105- go test -v ./...105+ GOFLAGS=-mod=mod go test -v ./...
106 106
107 test-race: ## Run tests with race detection107 test-race: ## Run tests with race detection
108 @echo "Running tests with race detection..."108 @echo "Running tests with race detection..."
109- go test -race -v ./...109+ GOFLAGS=-mod=mod go test -race -v ./...
110 110
111 test-cover: ## Run tests with coverage111 test-cover: ## Run tests with coverage
112 @echo "Running tests with coverage..."112 @echo "Running tests with coverage..."
113- go test -coverprofile=coverage.out ./...113+ GOFLAGS=-mod=mod go test -coverprofile=coverage.out ./...
114 go tool cover -html=coverage.out -o coverage.html114 go tool cover -html=coverage.out -o coverage.html
115 @echo "Coverage report: coverage.html"115 @echo "Coverage report: coverage.html"
116 116
M cmd/dumber/main.go +1 -0
@@ -327,6 +327,7 @@ func applyCEFRenderStackDefault(ctx context.Context, cfg *config.Config) {
327327 return
328328 }
329329 infracef.ApplyDefaultRenderStackEnvironment(ctx)
330+ infracef.ApplyDefaultHardwareDecodeEnvironment(ctx, cfg)
330331 }
331332
332333 func preInitializeAdwaitaForCEF(cfg *config.Config, initResult *bootstrap.ParallelInitResult, initAdwaita func()) {
@@ -327,6 +327,7 @@ func applyCEFRenderStackDefault(ctx context.Context, cfg *config.Config) {
327 return327 return
328 }328 }
329 infracef.ApplyDefaultRenderStackEnvironment(ctx)329 infracef.ApplyDefaultRenderStackEnvironment(ctx)
330+ infracef.ApplyDefaultHardwareDecodeEnvironment(ctx, cfg)
330 }331 }
331 332
332 func preInitializeAdwaitaForCEF(cfg *config.Config, initResult *bootstrap.ParallelInitResult, initAdwaita func()) {333 func preInitializeAdwaitaForCEF(cfg *config.Config, initResult *bootstrap.ParallelInitResult, initAdwaita func()) {
M go.sum +0 -2 generated file: collapsed
M internal/infrastructure/cef/app.go +3 -1
@@ -70,12 +70,14 @@ func configureHardwareVideoDecode(commandLine purecef.CommandLine) {
7070
7171 appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumEnableFeaturesSwitch,
7272 "AcceleratedVideoDecoder",
73+ "AcceleratedVideoEncoder",
7374 "AcceleratedVideoDecodeLinuxGL",
7475 "AcceleratedVideoDecodeLinuxZeroCopyGL",
7576 "VaapiIgnoreDriverChecks",
7677 )
7778 appendSwitchIfMissing(commandLine, "ignore-gpu-blocklist")
7879 appendSwitchIfMissing(commandLine, "enable-zero-copy")
80+ appendSwitchIfMissing(commandLine, "disable-gpu-driver-bug-workaround")
7981 }
8082
8183 func configureEnvChromiumFlags(commandLine purecef.CommandLine) {
@@ -336,7 +338,7 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
336338 useAngle := ""
337339 ozonePlatform := ""
338340 if commandLine != nil {
339- configureWebAuthnFeaturePolicy(commandLine)
341+ configureCommandLine(commandLine)
340342 appendSwitchIfMissing(commandLine, "no-zygote")
341343 processType = commandLine.GetSwitchValue("type")
342344 commandLineString = commandLine.GetCommandLineString()
@@ -70,12 +70,14 @@ func configureHardwareVideoDecode(commandLine purecef.CommandLine) {
70 70
71 appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumEnableFeaturesSwitch,71 appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumEnableFeaturesSwitch,
72 "AcceleratedVideoDecoder",72 "AcceleratedVideoDecoder",
73+ "AcceleratedVideoEncoder",
73 "AcceleratedVideoDecodeLinuxGL",74 "AcceleratedVideoDecodeLinuxGL",
74 "AcceleratedVideoDecodeLinuxZeroCopyGL",75 "AcceleratedVideoDecodeLinuxZeroCopyGL",
75 "VaapiIgnoreDriverChecks",76 "VaapiIgnoreDriverChecks",
76 )77 )
77 appendSwitchIfMissing(commandLine, "ignore-gpu-blocklist")78 appendSwitchIfMissing(commandLine, "ignore-gpu-blocklist")
78 appendSwitchIfMissing(commandLine, "enable-zero-copy")79 appendSwitchIfMissing(commandLine, "enable-zero-copy")
80+ appendSwitchIfMissing(commandLine, "disable-gpu-driver-bug-workaround")
79 }81 }
80 82
81 func configureEnvChromiumFlags(commandLine purecef.CommandLine) {83 func configureEnvChromiumFlags(commandLine purecef.CommandLine) {
@@ -336,7 +338,7 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine)
336 useAngle := ""338 useAngle := ""
337 ozonePlatform := ""339 ozonePlatform := ""
338 if commandLine != nil {340 if commandLine != nil {
339- configureWebAuthnFeaturePolicy(commandLine)341+ configureCommandLine(commandLine)
340 appendSwitchIfMissing(commandLine, "no-zygote")342 appendSwitchIfMissing(commandLine, "no-zygote")
341 processType = commandLine.GetSwitchValue("type")343 processType = commandLine.GetSwitchValue("type")
342 commandLineString = commandLine.GetCommandLineString()344 commandLineString = commandLine.GetCommandLineString()
M internal/infrastructure/cef/app_test.go +42 -2
@@ -305,8 +305,18 @@ func TestConfigureCommandLine_EnableVAAPIEnvAppendsHardwareDecodeFlags(t *testin
305305
306306 configureCommandLine(commandLine)
307307
308- if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); got != "ExistingFeature,AcceleratedVideoDecoder,AcceleratedVideoDecodeLinuxGL,AcceleratedVideoDecodeLinuxZeroCopyGL,VaapiIgnoreDriverChecks" {
309- t.Fatalf("enable-features = %q, want VAAPI features appended", got)
308+ gotFeatures := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch)
309+ for _, feature := range []string{
310+ "ExistingFeature",
311+ "AcceleratedVideoDecoder",
312+ "AcceleratedVideoEncoder",
313+ "AcceleratedVideoDecodeLinuxGL",
314+ "AcceleratedVideoDecodeLinuxZeroCopyGL",
315+ "VaapiIgnoreDriverChecks",
316+ } {
317+ if !strings.Contains(gotFeatures, feature) {
318+ t.Fatalf("enable-features = %q, missing %q", gotFeatures, feature)
319+ }
310320 }
311321 if !commandLine.HasSwitch("ignore-gpu-blocklist") {
312322 t.Fatal("expected ignore-gpu-blocklist switch")
@@ -314,6 +324,9 @@ func TestConfigureCommandLine_EnableVAAPIEnvAppendsHardwareDecodeFlags(t *testin
314324 if !commandLine.HasSwitch("enable-zero-copy") {
315325 t.Fatal("expected enable-zero-copy switch")
316326 }
327+ if !commandLine.HasSwitch("disable-gpu-driver-bug-workaround") {
328+ t.Fatal("expected disable-gpu-driver-bug-workaround switch")
329+ }
317330 }
318331
319332 func TestConfigureCommandLine_ChromiumFlagsEnvAppendsSwitches(t *testing.T) {
@@ -420,6 +433,33 @@ func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) {
420433 }
421434 }
422435
436+func TestDumberBPH_OnBeforeChildProcessLaunch_AppliesRenderingAndHardwareDecodeFlags(t *testing.T) {
437+ t.Setenv(cef2gtkAngleBackendVar, "vulkan")
438+ t.Setenv(cefEnableVAAPIEnvVar, "1")
439+
440+ commandLine := newMutableCommandLineStub()
441+ commandLine.AppendSwitchWithValue("type", "gpu-process")
442+ commandLine.AppendSwitchWithValue("use-angle", "gl-egl")
443+
444+ (&dumberBPH{engine: &Engine{ctx: context.Background()}}).OnBeforeChildProcessLaunch(commandLine)
445+
446+ if got := commandLine.GetSwitchValue("use-angle"); got != "vulkan" {
447+ t.Fatalf("use-angle = %q, want vulkan", got)
448+ }
449+ if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); !strings.Contains(got, "AcceleratedVideoDecoder") || !strings.Contains(got, "VulkanFromANGLE") {
450+ t.Fatalf("enable-features = %q, want hardware decode and Vulkan ANGLE features", got)
451+ }
452+ if !commandLine.HasSwitch("ignore-gpu-blocklist") {
453+ t.Fatal("expected ignore-gpu-blocklist switch")
454+ }
455+ if !commandLine.HasSwitch("enable-zero-copy") {
456+ t.Fatal("expected enable-zero-copy switch")
457+ }
458+ if !commandLine.HasSwitch("no-zygote") {
459+ t.Fatal("expected no-zygote switch")
460+ }
461+}
462+
423463 func TestDumberBPH_OnBeforeChildProcessLaunch_DisablesWebAuthnByDefault(t *testing.T) {
424464 t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "")
425465
@@ -305,8 +305,18 @@ func TestConfigureCommandLine_EnableVAAPIEnvAppendsHardwareDecodeFlags(t *testin
305 305
306 configureCommandLine(commandLine)306 configureCommandLine(commandLine)
307 307
308- if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); got != "ExistingFeature,AcceleratedVideoDecoder,AcceleratedVideoDecodeLinuxGL,AcceleratedVideoDecodeLinuxZeroCopyGL,VaapiIgnoreDriverChecks" {308+ gotFeatures := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch)
309- t.Fatalf("enable-features = %q, want VAAPI features appended", got)309+ for _, feature := range []string{
310+ "ExistingFeature",
311+ "AcceleratedVideoDecoder",
312+ "AcceleratedVideoEncoder",
313+ "AcceleratedVideoDecodeLinuxGL",
314+ "AcceleratedVideoDecodeLinuxZeroCopyGL",
315+ "VaapiIgnoreDriverChecks",
316+ } {
317+ if !strings.Contains(gotFeatures, feature) {
318+ t.Fatalf("enable-features = %q, missing %q", gotFeatures, feature)
319+ }
310 }320 }
311 if !commandLine.HasSwitch("ignore-gpu-blocklist") {321 if !commandLine.HasSwitch("ignore-gpu-blocklist") {
312 t.Fatal("expected ignore-gpu-blocklist switch")322 t.Fatal("expected ignore-gpu-blocklist switch")
@@ -314,6 +324,9 @@ func TestConfigureCommandLine_EnableVAAPIEnvAppendsHardwareDecodeFlags(t *testin
314 if !commandLine.HasSwitch("enable-zero-copy") {324 if !commandLine.HasSwitch("enable-zero-copy") {
315 t.Fatal("expected enable-zero-copy switch")325 t.Fatal("expected enable-zero-copy switch")
316 }326 }
327+ if !commandLine.HasSwitch("disable-gpu-driver-bug-workaround") {
328+ t.Fatal("expected disable-gpu-driver-bug-workaround switch")
329+ }
317 }330 }
318 331
319 func TestConfigureCommandLine_ChromiumFlagsEnvAppendsSwitches(t *testing.T) {332 func TestConfigureCommandLine_ChromiumFlagsEnvAppendsSwitches(t *testing.T) {
@@ -420,6 +433,33 @@ func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) {
420 }433 }
421 }434 }
422 435
436+func TestDumberBPH_OnBeforeChildProcessLaunch_AppliesRenderingAndHardwareDecodeFlags(t *testing.T) {
437+ t.Setenv(cef2gtkAngleBackendVar, "vulkan")
438+ t.Setenv(cefEnableVAAPIEnvVar, "1")
439+
440+ commandLine := newMutableCommandLineStub()
441+ commandLine.AppendSwitchWithValue("type", "gpu-process")
442+ commandLine.AppendSwitchWithValue("use-angle", "gl-egl")
443+
444+ (&dumberBPH{engine: &Engine{ctx: context.Background()}}).OnBeforeChildProcessLaunch(commandLine)
445+
446+ if got := commandLine.GetSwitchValue("use-angle"); got != "vulkan" {
447+ t.Fatalf("use-angle = %q, want vulkan", got)
448+ }
449+ if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); !strings.Contains(got, "AcceleratedVideoDecoder") || !strings.Contains(got, "VulkanFromANGLE") {
450+ t.Fatalf("enable-features = %q, want hardware decode and Vulkan ANGLE features", got)
451+ }
452+ if !commandLine.HasSwitch("ignore-gpu-blocklist") {
453+ t.Fatal("expected ignore-gpu-blocklist switch")
454+ }
455+ if !commandLine.HasSwitch("enable-zero-copy") {
456+ t.Fatal("expected enable-zero-copy switch")
457+ }
458+ if !commandLine.HasSwitch("no-zygote") {
459+ t.Fatal("expected no-zygote switch")
460+ }
461+}
462+
423 func TestDumberBPH_OnBeforeChildProcessLaunch_DisablesWebAuthnByDefault(t *testing.T) {463 func TestDumberBPH_OnBeforeChildProcessLaunch_DisablesWebAuthnByDefault(t *testing.T) {
424 t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "")464 t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "")
425 465
M internal/infrastructure/cef/cef2gtk_adapter.go +11 -2
@@ -24,8 +24,9 @@ var ErrAdapterDestroyed = errors.New("cef2gtk_adapter: adapter is destroyed")
2424 // The adapter does NOT own GL/PBO staging, dirty-rect uploads, or GTK input
2525 // controller implementation — those belong to purego-cef2gtk.
2626 type Cef2gtkAdapter struct {
27- view *cef2gtk.View
28- destroyed atomic.Bool
27+ view *cef2gtk.View
28+ destroyed atomic.Bool
29+ destroyCnt atomic.Uint64
2930 }
3031
3132 // NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin
@@ -196,6 +197,7 @@ func (a *Cef2gtkAdapter) Destroy() error {
196197 }
197198 err := a.view.Destroy()
198199 a.view = nil
200+ a.destroyCnt.Add(1)
199201 return err
200202 }
201203
@@ -203,3 +205,10 @@ func (a *Cef2gtkAdapter) Destroy() error {
203205 func (a *Cef2gtkAdapter) IsDestroyed() bool {
204206 return a == nil || a.destroyed.Load()
205207 }
208+
209+func (a *Cef2gtkAdapter) destroyCount() uint64 {
210+ if a == nil {
211+ return 0
212+ }
213+ return a.destroyCnt.Load()
214+}
@@ -24,8 +24,9 @@ var ErrAdapterDestroyed = errors.New("cef2gtk_adapter: adapter is destroyed")
24 // The adapter does NOT own GL/PBO staging, dirty-rect uploads, or GTK input24 // The adapter does NOT own GL/PBO staging, dirty-rect uploads, or GTK input
25 // controller implementation — those belong to purego-cef2gtk.25 // controller implementation — those belong to purego-cef2gtk.
26 type Cef2gtkAdapter struct {26 type Cef2gtkAdapter struct {
27- view *cef2gtk.View27+ view *cef2gtk.View
28- destroyed atomic.Bool28+ destroyed atomic.Bool
29+ destroyCnt atomic.Uint64
29 }30 }
30 31
31 // NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin32 // NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin
@@ -196,6 +197,7 @@ func (a *Cef2gtkAdapter) Destroy() error {
196 }197 }
197 err := a.view.Destroy()198 err := a.view.Destroy()
198 a.view = nil199 a.view = nil
200+ a.destroyCnt.Add(1)
199 return err201 return err
200 }202 }
201 203
@@ -203,3 +205,10 @@ func (a *Cef2gtkAdapter) Destroy() error {
203 func (a *Cef2gtkAdapter) IsDestroyed() bool {205 func (a *Cef2gtkAdapter) IsDestroyed() bool {
204 return a == nil || a.destroyed.Load()206 return a == nil || a.destroyed.Load()
205 }207 }
208+
209+func (a *Cef2gtkAdapter) destroyCount() uint64 {
210+ if a == nil {
211+ return 0
212+ }
213+ return a.destroyCnt.Load()
214+}
M internal/infrastructure/cef/engine.go +23 -1
@@ -4,6 +4,7 @@ import (
44 "context"
55 "crypto/subtle"
66 "fmt"
7+ "os"
78 "sync"
89 "sync/atomic"
910 "time"
@@ -27,6 +28,7 @@ type Engine struct {
2728 factory *WebViewFactory
2829 pool *WebViewPool
2930 profileLogDir string
31+ runtimeCEFDir string
3032
3133 messageRouter *MessageRouter
3234 schemeHandler *dumbSchemeHandler
@@ -215,7 +217,7 @@ func (e *Engine) Close() error {
215217 return nil
216218 }
217219
218-const cefShutdownWaitTimeout = 2 * time.Second
220+const cefShutdownWaitTimeout = 10 * time.Second
219221
220222 func (e *Engine) activeWebViewCount() int {
221223 return int(e.activeCount.Load())
@@ -252,6 +254,7 @@ func (e *Engine) closeActiveWebViews() {
252254 // Check immediately in case all webviews closed synchronously.
253255 if e.activeWebViewCount() == 0 {
254256 log.Debug().Msg("cef: all active webviews closed before shutdown")
257+ e.destroyClosedWebViewBridges(webViews)
255258 return
256259 }
257260
@@ -265,6 +268,15 @@ func (e *Engine) closeActiveWebViews() {
265268 Str("timeout", cefShutdownWaitTimeout.String()).
266269 Msg("cef: timed out waiting for OnBeforeClose before shutdown")
267270 }
271+ e.destroyClosedWebViewBridges(webViews)
272+}
273+
274+func (e *Engine) destroyClosedWebViewBridges(webViews []*WebView) {
275+ for _, wv := range webViews {
276+ if wv != nil && wv.destroyed.Load() {
277+ wv.destroyViewBridgeOnGTKSync()
278+ }
279+ }
268280 }
269281
270282 // RegisterHandlers registers all WebUI message bridge handlers with the message router.
@@ -375,6 +387,16 @@ func (e *Engine) notifyClipboardCopied(text string) {
375387 // SetHandlerContext sets the base context for message handler dispatch.
376388 func (e *Engine) SetHandlerContext(ctx context.Context) {
377389 e.ctx = ctx
390+ logger := logging.FromContext(ctx)
391+ logger.Info().
392+ Str("settings_cef_dir", e.runtimeCEFDir).
393+ Str("env_cef_dir", os.Getenv("CEF_DIR")).
394+ Msg("cef: runtime selection")
395+ if libcefPath := loadedLibCEFPath(); libcefPath != "" {
396+ logger.Info().Str("libcef_path", libcefPath).Msg("cef: runtime library loaded")
397+ } else {
398+ logger.Warn().Msg("cef: runtime library loaded but libcef path was not found in /proc/self/maps")
399+ }
378400 if e.messageRouter != nil {
379401 e.messageRouter.SetBaseContext(ctx)
380402 }
@@ -4,6 +4,7 @@ import (
4 "context"4 "context"
5 "crypto/subtle"5 "crypto/subtle"
6 "fmt"6 "fmt"
7+ "os"
7 "sync"8 "sync"
8 "sync/atomic"9 "sync/atomic"
9 "time"10 "time"
@@ -27,6 +28,7 @@ type Engine struct {
27 factory *WebViewFactory28 factory *WebViewFactory
28 pool *WebViewPool29 pool *WebViewPool
29 profileLogDir string30 profileLogDir string
31+ runtimeCEFDir string
30 32
31 messageRouter *MessageRouter33 messageRouter *MessageRouter
32 schemeHandler *dumbSchemeHandler34 schemeHandler *dumbSchemeHandler
@@ -215,7 +217,7 @@ func (e *Engine) Close() error {
215 return nil217 return nil
216 }218 }
217 219
218-const cefShutdownWaitTimeout = 2 * time.Second220+const cefShutdownWaitTimeout = 10 * time.Second
219 221
220 func (e *Engine) activeWebViewCount() int {222 func (e *Engine) activeWebViewCount() int {
221 return int(e.activeCount.Load())223 return int(e.activeCount.Load())
@@ -252,6 +254,7 @@ func (e *Engine) closeActiveWebViews() {
252 // Check immediately in case all webviews closed synchronously.254 // Check immediately in case all webviews closed synchronously.
253 if e.activeWebViewCount() == 0 {255 if e.activeWebViewCount() == 0 {
254 log.Debug().Msg("cef: all active webviews closed before shutdown")256 log.Debug().Msg("cef: all active webviews closed before shutdown")
257+ e.destroyClosedWebViewBridges(webViews)
255 return258 return
256 }259 }
257 260
@@ -265,6 +268,15 @@ func (e *Engine) closeActiveWebViews() {
265 Str("timeout", cefShutdownWaitTimeout.String()).268 Str("timeout", cefShutdownWaitTimeout.String()).
266 Msg("cef: timed out waiting for OnBeforeClose before shutdown")269 Msg("cef: timed out waiting for OnBeforeClose before shutdown")
267 }270 }
271+ e.destroyClosedWebViewBridges(webViews)
272+}
273+
274+func (e *Engine) destroyClosedWebViewBridges(webViews []*WebView) {
275+ for _, wv := range webViews {
276+ if wv != nil && wv.destroyed.Load() {
277+ wv.destroyViewBridgeOnGTKSync()
278+ }
279+ }
268 }280 }
269 281
270 // RegisterHandlers registers all WebUI message bridge handlers with the message router.282 // RegisterHandlers registers all WebUI message bridge handlers with the message router.
@@ -375,6 +387,16 @@ func (e *Engine) notifyClipboardCopied(text string) {
375 // SetHandlerContext sets the base context for message handler dispatch.387 // SetHandlerContext sets the base context for message handler dispatch.
376 func (e *Engine) SetHandlerContext(ctx context.Context) {388 func (e *Engine) SetHandlerContext(ctx context.Context) {
377 e.ctx = ctx389 e.ctx = ctx
390+ logger := logging.FromContext(ctx)
391+ logger.Info().
392+ Str("settings_cef_dir", e.runtimeCEFDir).
393+ Str("env_cef_dir", os.Getenv("CEF_DIR")).
394+ Msg("cef: runtime selection")
395+ if libcefPath := loadedLibCEFPath(); libcefPath != "" {
396+ logger.Info().Str("libcef_path", libcefPath).Msg("cef: runtime library loaded")
397+ } else {
398+ logger.Warn().Msg("cef: runtime library loaded but libcef path was not found in /proc/self/maps")
399+ }
378 if e.messageRouter != nil {400 if e.messageRouter != nil {
379 e.messageRouter.SetBaseContext(ctx)401 e.messageRouter.SetBaseContext(ctx)
380 }402 }
M internal/infrastructure/cef/engine_init.go +32 -0
@@ -58,6 +58,7 @@ func NewEngine(
5858 eng := &Engine{
5959 ctx: ctx,
6060 profileLogDir: paths.ProfileLogDir,
61+ runtimeCEFDir: settings.CEFDir,
6162 registerHandlers: deps.RegisterHandlers,
6263 registerAccentHandlers: deps.RegisterAccentHandlers,
6364 currentConfigPayload: deps.CurrentConfigPayload,
@@ -153,14 +154,45 @@ func prepareCEFSettings(
153154 // and browser-process callbacks (OnBeforeCommandLineProcessing, etc.).
154155 func initializeCEF(eng *Engine, settings purecef.Settings, logger *zerolog.Logger) error {
155156 app := newDumberApp(eng)
157+ logger.Info().
158+ Str("settings_cef_dir", settings.CEFDir).
159+ Str("env_cef_dir", os.Getenv("CEF_DIR")).
160+ Msg("cef: runtime selection")
156161 logger.Debug().Msg("cef: calling InitWithApp")
157162 if err := purecef.InitWithApp(settings, app); err != nil {
158163 return fmt.Errorf("cef.InitWithApp: %w", err)
159164 }
165+ if libcefPath := loadedLibCEFPath(); libcefPath != "" {
166+ logger.Info().Str("libcef_path", libcefPath).Msg("cef: runtime library loaded")
167+ } else {
168+ logger.Warn().Msg("cef: runtime library loaded but libcef path was not found in /proc/self/maps")
169+ }
160170 logger.Debug().Msg("cef: InitWithApp returned OK")
161171 return nil
162172 }
163173
174+func loadedLibCEFPath() string {
175+ data, err := os.ReadFile("/proc/self/maps")
176+ if err != nil {
177+ return ""
178+ }
179+ return parseLoadedLibCEFPath(string(data))
180+}
181+
182+func parseLoadedLibCEFPath(maps string) string {
183+ for _, line := range strings.Split(maps, "\n") {
184+ if !strings.Contains(line, "libcef.so") {
185+ continue
186+ }
187+ pathStart := strings.Index(line, "/")
188+ if pathStart < 0 {
189+ continue
190+ }
191+ return strings.TrimSpace(line[pathStart:])
192+ }
193+ return ""
194+}
195+
164196 // wireEngine creates factory, pool, and scheme handler after CEF init.
165197 func wireEngine(
166198 ctx context.Context, eng *Engine,
@@ -58,6 +58,7 @@ func NewEngine(
58 eng := &Engine{58 eng := &Engine{
59 ctx: ctx,59 ctx: ctx,
60 profileLogDir: paths.ProfileLogDir,60 profileLogDir: paths.ProfileLogDir,
61+ runtimeCEFDir: settings.CEFDir,
61 registerHandlers: deps.RegisterHandlers,62 registerHandlers: deps.RegisterHandlers,
62 registerAccentHandlers: deps.RegisterAccentHandlers,63 registerAccentHandlers: deps.RegisterAccentHandlers,
63 currentConfigPayload: deps.CurrentConfigPayload,64 currentConfigPayload: deps.CurrentConfigPayload,
@@ -153,14 +154,45 @@ func prepareCEFSettings(
153 // and browser-process callbacks (OnBeforeCommandLineProcessing, etc.).154 // and browser-process callbacks (OnBeforeCommandLineProcessing, etc.).
154 func initializeCEF(eng *Engine, settings purecef.Settings, logger *zerolog.Logger) error {155 func initializeCEF(eng *Engine, settings purecef.Settings, logger *zerolog.Logger) error {
155 app := newDumberApp(eng)156 app := newDumberApp(eng)
157+ logger.Info().
158+ Str("settings_cef_dir", settings.CEFDir).
159+ Str("env_cef_dir", os.Getenv("CEF_DIR")).
160+ Msg("cef: runtime selection")
156 logger.Debug().Msg("cef: calling InitWithApp")161 logger.Debug().Msg("cef: calling InitWithApp")
157 if err := purecef.InitWithApp(settings, app); err != nil {162 if err := purecef.InitWithApp(settings, app); err != nil {
158 return fmt.Errorf("cef.InitWithApp: %w", err)163 return fmt.Errorf("cef.InitWithApp: %w", err)
159 }164 }
165+ if libcefPath := loadedLibCEFPath(); libcefPath != "" {
166+ logger.Info().Str("libcef_path", libcefPath).Msg("cef: runtime library loaded")
167+ } else {
168+ logger.Warn().Msg("cef: runtime library loaded but libcef path was not found in /proc/self/maps")
169+ }
160 logger.Debug().Msg("cef: InitWithApp returned OK")170 logger.Debug().Msg("cef: InitWithApp returned OK")
161 return nil171 return nil
162 }172 }
163 173
174+func loadedLibCEFPath() string {
175+ data, err := os.ReadFile("/proc/self/maps")
176+ if err != nil {
177+ return ""
178+ }
179+ return parseLoadedLibCEFPath(string(data))
180+}
181+
182+func parseLoadedLibCEFPath(maps string) string {
183+ for _, line := range strings.Split(maps, "\n") {
184+ if !strings.Contains(line, "libcef.so") {
185+ continue
186+ }
187+ pathStart := strings.Index(line, "/")
188+ if pathStart < 0 {
189+ continue
190+ }
191+ return strings.TrimSpace(line[pathStart:])
192+ }
193+ return ""
194+}
195+
164 // wireEngine creates factory, pool, and scheme handler after CEF init.196 // wireEngine creates factory, pool, and scheme handler after CEF init.
165 func wireEngine(197 func wireEngine(
166 ctx context.Context, eng *Engine,198 ctx context.Context, eng *Engine,
M internal/infrastructure/cef/engine_init_test.go +12 -0
@@ -46,6 +46,18 @@ func TestPrepareCEFSettings_UsesResolvedProfilePaths(t *testing.T) {
4646 require.Empty(t, settings.BrowserSubprocessPath)
4747 }
4848
49+func TestParseLoadedLibCEFPath(t *testing.T) {
50+ maps := "7f5fd4000000-7f5fd4200000 r--p 00000000 00:00 0 /usr/lib/libvulkan.so.1\n" +
51+ "7f5fe0000000-7f5fe8200000 r-xp 00000000 00:00 0 /opt/cef-vaapi-runtime/usr/lib/cef/libcef.so\n" +
52+ "7f5fe8200000-7f5fe8400000 r--p 08200000 00:00 0 /opt/cef-vaapi-runtime/usr/lib/cef/libcef.so\n"
53+
54+ require.Equal(t, "/opt/cef-vaapi-runtime/usr/lib/cef/libcef.so", parseLoadedLibCEFPath(maps))
55+}
56+
57+func TestParseLoadedLibCEFPath_ReturnsEmptyWhenAbsent(t *testing.T) {
58+ require.Empty(t, parseLoadedLibCEFPath("7f5fd4000000-7f5fd4200000 r--p 00000000 00:00 0 /usr/lib/libvulkan.so.1\n"))
59+}
60+
4961 func TestPrepareCEFSettings_RejectsNonDefaultCookiePolicy(t *testing.T) {
5062 logger := zerolog.Nop()
5163 profile := testCEFDevProfile(t)
@@ -46,6 +46,18 @@ func TestPrepareCEFSettings_UsesResolvedProfilePaths(t *testing.T) {
46 require.Empty(t, settings.BrowserSubprocessPath)46 require.Empty(t, settings.BrowserSubprocessPath)
47 }47 }
48 48
49+func TestParseLoadedLibCEFPath(t *testing.T) {
50+ maps := "7f5fd4000000-7f5fd4200000 r--p 00000000 00:00 0 /usr/lib/libvulkan.so.1\n" +
51+ "7f5fe0000000-7f5fe8200000 r-xp 00000000 00:00 0 /opt/cef-vaapi-runtime/usr/lib/cef/libcef.so\n" +
52+ "7f5fe8200000-7f5fe8400000 r--p 08200000 00:00 0 /opt/cef-vaapi-runtime/usr/lib/cef/libcef.so\n"
53+
54+ require.Equal(t, "/opt/cef-vaapi-runtime/usr/lib/cef/libcef.so", parseLoadedLibCEFPath(maps))
55+}
56+
57+func TestParseLoadedLibCEFPath_ReturnsEmptyWhenAbsent(t *testing.T) {
58+ require.Empty(t, parseLoadedLibCEFPath("7f5fd4000000-7f5fd4200000 r--p 00000000 00:00 0 /usr/lib/libvulkan.so.1\n"))
59+}
60+
49 func TestPrepareCEFSettings_RejectsNonDefaultCookiePolicy(t *testing.T) {61 func TestPrepareCEFSettings_RejectsNonDefaultCookiePolicy(t *testing.T) {
50 logger := zerolog.Nop()62 logger := zerolog.Nop()
51 profile := testCEFDevProfile(t)63 profile := testCEFDevProfile(t)
M internal/infrastructure/cef/render_stack_env.go +39 -2
@@ -5,6 +5,9 @@ import (
55 "os"
66 "strings"
77
8+ "github.com/bnema/dumber/internal/application/port"
9+ "github.com/bnema/dumber/internal/infrastructure/config"
10+ renderenv "github.com/bnema/dumber/internal/infrastructure/env"
811 "github.com/bnema/dumber/internal/logging"
912 )
1013
@@ -30,7 +33,7 @@ const (
3033 //
3134 // DUMBER_RENDER_STACK accepts:
3235 // - auto or empty: use the GPU-first GDK DMABUF stack
33-// - vulkan-dmabuf: force GDK DMABUF presentation with GSK Vulkan and CEF ANGLE GL/EGL
36+// - vulkan-dmabuf: force GDK DMABUF presentation with GSK Vulkan and CEF ANGLE Vulkan
3437 // - legacy-gl: force the older GtkGLArea/OpenGL bridge for diagnostics
3538 //
3639 // Low-level env vars remain diagnostic escape hatches: when explicitly set,
@@ -49,11 +52,45 @@ func applyDefaultRenderStackEnvironment(ctx context.Context) string {
4952 default:
5053 setEnvDefault(ctx, gskRendererEnvVar, gskRendererVulkan)
5154 setEnvDefault(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGDKDMABUF)
52- setEnvDefault(ctx, cef2gtkAngleBackendVar, cef2gtkAngleGLEGL)
55+ setEnvDefault(ctx, cef2gtkAngleBackendVar, cef2gtkAngleVulkan)
5356 return renderStackVulkanDMABUF
5457 }
5558 }
5659
60+// ApplyDefaultHardwareDecodeEnvironment maps Dumber's media config to CEF's
61+// developer-facing VAAPI switch env var before CEF command-line callbacks run.
62+// Existing explicit env values are preserved as low-level escape hatches.
63+func ApplyDefaultHardwareDecodeEnvironment(ctx context.Context, cfg *config.Config) {
64+ if cfg == nil || cfg.Engine.ResolveEngineType() != config.EngineTypeCEF {
65+ return
66+ }
67+ if cfg.Media.HardwareDecodingMode == config.HardwareDecodingDisable {
68+ setEnvDefault(ctx, cefEnableVAAPIEnvVar, "0")
69+ return
70+ }
71+ setEnvDefault(ctx, cefEnableVAAPIEnvVar, "1")
72+ applyDefaultLIBVADriverEnvironment(ctx)
73+}
74+
75+func applyDefaultLIBVADriverEnvironment(ctx context.Context) {
76+ if strings.TrimSpace(os.Getenv("LIBVA_DRIVER_NAME")) != "" {
77+ return
78+ }
79+ manager := renderenv.NewManager()
80+ switch manager.DetectGPUVendor(ctx) {
81+ case port.GPUVendorAMD:
82+ setEnvDefault(ctx, "LIBVA_DRIVER_NAME", "radeonsi")
83+ case port.GPUVendorIntel:
84+ setEnvDefault(ctx, "LIBVA_DRIVER_NAME", "iHD")
85+ case port.GPUVendorNVIDIA:
86+ if ctx == nil {
87+ ctx = context.Background()
88+ }
89+ logging.FromContext(ctx).Warn().
90+ Msg("cef: not defaulting LIBVA_DRIVER_NAME for NVIDIA; Chromium/CEF VAAPI support is driver-dependent")
91+ }
92+}
93+
5794 func normalizeRenderStack(value string) string {
5895 switch strings.ToLower(strings.TrimSpace(value)) {
5996 case "", renderStackAuto, renderStackVulkanDMABUF:
@@ -5,6 +5,9 @@ import (
5 "os"5 "os"
6 "strings"6 "strings"
7 7
8+ "github.com/bnema/dumber/internal/application/port"
9+ "github.com/bnema/dumber/internal/infrastructure/config"
10+ renderenv "github.com/bnema/dumber/internal/infrastructure/env"
8 "github.com/bnema/dumber/internal/logging"11 "github.com/bnema/dumber/internal/logging"
9 )12 )
10 13
@@ -30,7 +33,7 @@ const (
30 //33 //
31 // DUMBER_RENDER_STACK accepts:34 // DUMBER_RENDER_STACK accepts:
32 // - auto or empty: use the GPU-first GDK DMABUF stack35 // - auto or empty: use the GPU-first GDK DMABUF stack
33-// - vulkan-dmabuf: force GDK DMABUF presentation with GSK Vulkan and CEF ANGLE GL/EGL36+// - vulkan-dmabuf: force GDK DMABUF presentation with GSK Vulkan and CEF ANGLE Vulkan
34 // - legacy-gl: force the older GtkGLArea/OpenGL bridge for diagnostics37 // - legacy-gl: force the older GtkGLArea/OpenGL bridge for diagnostics
35 //38 //
36 // Low-level env vars remain diagnostic escape hatches: when explicitly set,39 // Low-level env vars remain diagnostic escape hatches: when explicitly set,
@@ -49,11 +52,45 @@ func applyDefaultRenderStackEnvironment(ctx context.Context) string {
49 default:52 default:
50 setEnvDefault(ctx, gskRendererEnvVar, gskRendererVulkan)53 setEnvDefault(ctx, gskRendererEnvVar, gskRendererVulkan)
51 setEnvDefault(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGDKDMABUF)54 setEnvDefault(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGDKDMABUF)
52- setEnvDefault(ctx, cef2gtkAngleBackendVar, cef2gtkAngleGLEGL)55+ setEnvDefault(ctx, cef2gtkAngleBackendVar, cef2gtkAngleVulkan)
53 return renderStackVulkanDMABUF56 return renderStackVulkanDMABUF
54 }57 }
55 }58 }
56 59
60+// ApplyDefaultHardwareDecodeEnvironment maps Dumber's media config to CEF's
61+// developer-facing VAAPI switch env var before CEF command-line callbacks run.
62+// Existing explicit env values are preserved as low-level escape hatches.
63+func ApplyDefaultHardwareDecodeEnvironment(ctx context.Context, cfg *config.Config) {
64+ if cfg == nil || cfg.Engine.ResolveEngineType() != config.EngineTypeCEF {
65+ return
66+ }
67+ if cfg.Media.HardwareDecodingMode == config.HardwareDecodingDisable {
68+ setEnvDefault(ctx, cefEnableVAAPIEnvVar, "0")
69+ return
70+ }
71+ setEnvDefault(ctx, cefEnableVAAPIEnvVar, "1")
72+ applyDefaultLIBVADriverEnvironment(ctx)
73+}
74+
75+func applyDefaultLIBVADriverEnvironment(ctx context.Context) {
76+ if strings.TrimSpace(os.Getenv("LIBVA_DRIVER_NAME")) != "" {
77+ return
78+ }
79+ manager := renderenv.NewManager()
80+ switch manager.DetectGPUVendor(ctx) {
81+ case port.GPUVendorAMD:
82+ setEnvDefault(ctx, "LIBVA_DRIVER_NAME", "radeonsi")
83+ case port.GPUVendorIntel:
84+ setEnvDefault(ctx, "LIBVA_DRIVER_NAME", "iHD")
85+ case port.GPUVendorNVIDIA:
86+ if ctx == nil {
87+ ctx = context.Background()
88+ }
89+ logging.FromContext(ctx).Warn().
90+ Msg("cef: not defaulting LIBVA_DRIVER_NAME for NVIDIA; Chromium/CEF VAAPI support is driver-dependent")
91+ }
92+}
93+
57 func normalizeRenderStack(value string) string {94 func normalizeRenderStack(value string) string {
58 switch strings.ToLower(strings.TrimSpace(value)) {95 switch strings.ToLower(strings.TrimSpace(value)) {
59 case "", renderStackAuto, renderStackVulkanDMABUF:96 case "", renderStackAuto, renderStackVulkanDMABUF:
M internal/infrastructure/cef/render_stack_env_test.go +58 -3
@@ -4,9 +4,11 @@ import (
44 "context"
55 "os"
66 "testing"
7+
8+ "github.com/bnema/dumber/internal/infrastructure/config"
79 )
810
9-func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEGL(t *testing.T) {
11+func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEVulkan(t *testing.T) {
1012 t.Setenv(dumberRenderStackEnvVar, "")
1113 t.Setenv("GSK_RENDERER", "")
1214 t.Setenv("PUREGO_CEF2GTK_BACKEND", "")
@@ -23,8 +25,8 @@ func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEGL(t *te
2325 if got := os.Getenv("PUREGO_CEF2GTK_BACKEND"); got != "gdk-dmabuf" {
2426 t.Fatalf("PUREGO_CEF2GTK_BACKEND = %q, want gdk-dmabuf", got)
2527 }
26- if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "gl-egl" {
27- t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want gl-egl", got)
28+ if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "vulkan" {
29+ t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want vulkan", got)
2830 }
2931 }
3032
@@ -68,3 +70,56 @@ func TestApplyDefaultRenderStackEnvironment_LegacyGLUsesGLArea(t *testing.T) {
6870 t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want gl-egl", got)
6971 }
7072 }
73+
74+func TestApplyDefaultHardwareDecodeEnvironment_DefaultsCEFToVAAPIForAuto(t *testing.T) {
75+ t.Setenv(cefEnableVAAPIEnvVar, "")
76+
77+ ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{
78+ Engine: config.EngineConfig{Type: config.EngineTypeCEF},
79+ Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingAuto},
80+ })
81+
82+ if got := os.Getenv(cefEnableVAAPIEnvVar); got != "1" {
83+ t.Fatalf("%s = %q, want 1", cefEnableVAAPIEnvVar, got)
84+ }
85+}
86+
87+func TestApplyDefaultHardwareDecodeEnvironment_PreservesExplicitLIBVADriver(t *testing.T) {
88+ t.Setenv(cefEnableVAAPIEnvVar, "")
89+ t.Setenv("LIBVA_DRIVER_NAME", "custom-driver")
90+
91+ ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{
92+ Engine: config.EngineConfig{Type: config.EngineTypeCEF},
93+ Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingAuto},
94+ })
95+
96+ if got := os.Getenv("LIBVA_DRIVER_NAME"); got != "custom-driver" {
97+ t.Fatalf("LIBVA_DRIVER_NAME = %q, want explicit custom-driver", got)
98+ }
99+}
100+
101+func TestApplyDefaultHardwareDecodeEnvironment_DisablesCEFVAAPIWhenMediaDisabled(t *testing.T) {
102+ t.Setenv(cefEnableVAAPIEnvVar, "")
103+
104+ ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{
105+ Engine: config.EngineConfig{Type: config.EngineTypeCEF},
106+ Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingDisable},
107+ })
108+
109+ if got := os.Getenv(cefEnableVAAPIEnvVar); got != "0" {
110+ t.Fatalf("%s = %q, want 0", cefEnableVAAPIEnvVar, got)
111+ }
112+}
113+
114+func TestApplyDefaultHardwareDecodeEnvironment_PreservesExplicitCEFVAAPIOverride(t *testing.T) {
115+ t.Setenv(cefEnableVAAPIEnvVar, "0")
116+
117+ ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{
118+ Engine: config.EngineConfig{Type: config.EngineTypeCEF},
119+ Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingAuto},
120+ })
121+
122+ if got := os.Getenv(cefEnableVAAPIEnvVar); got != "0" {
123+ t.Fatalf("%s = %q, want explicit 0", cefEnableVAAPIEnvVar, got)
124+ }
125+}
@@ -4,9 +4,11 @@ import (
4 "context"4 "context"
5 "os"5 "os"
6 "testing"6 "testing"
7+
8+ "github.com/bnema/dumber/internal/infrastructure/config"
7 )9 )
8 10
9-func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEGL(t *testing.T) {11+func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEVulkan(t *testing.T) {
10 t.Setenv(dumberRenderStackEnvVar, "")12 t.Setenv(dumberRenderStackEnvVar, "")
11 t.Setenv("GSK_RENDERER", "")13 t.Setenv("GSK_RENDERER", "")
12 t.Setenv("PUREGO_CEF2GTK_BACKEND", "")14 t.Setenv("PUREGO_CEF2GTK_BACKEND", "")
@@ -23,8 +25,8 @@ func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEGL(t *te
23 if got := os.Getenv("PUREGO_CEF2GTK_BACKEND"); got != "gdk-dmabuf" {25 if got := os.Getenv("PUREGO_CEF2GTK_BACKEND"); got != "gdk-dmabuf" {
24 t.Fatalf("PUREGO_CEF2GTK_BACKEND = %q, want gdk-dmabuf", got)26 t.Fatalf("PUREGO_CEF2GTK_BACKEND = %q, want gdk-dmabuf", got)
25 }27 }
26- if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "gl-egl" {28+ if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "vulkan" {
27- t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want gl-egl", got)29+ t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want vulkan", got)
28 }30 }
29 }31 }
30 32
@@ -68,3 +70,56 @@ func TestApplyDefaultRenderStackEnvironment_LegacyGLUsesGLArea(t *testing.T) {
68 t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want gl-egl", got)70 t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want gl-egl", got)
69 }71 }
70 }72 }
73+
74+func TestApplyDefaultHardwareDecodeEnvironment_DefaultsCEFToVAAPIForAuto(t *testing.T) {
75+ t.Setenv(cefEnableVAAPIEnvVar, "")
76+
77+ ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{
78+ Engine: config.EngineConfig{Type: config.EngineTypeCEF},
79+ Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingAuto},
80+ })
81+
82+ if got := os.Getenv(cefEnableVAAPIEnvVar); got != "1" {
83+ t.Fatalf("%s = %q, want 1", cefEnableVAAPIEnvVar, got)
84+ }
85+}
86+
87+func TestApplyDefaultHardwareDecodeEnvironment_PreservesExplicitLIBVADriver(t *testing.T) {
88+ t.Setenv(cefEnableVAAPIEnvVar, "")
89+ t.Setenv("LIBVA_DRIVER_NAME", "custom-driver")
90+
91+ ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{
92+ Engine: config.EngineConfig{Type: config.EngineTypeCEF},
93+ Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingAuto},
94+ })
95+
96+ if got := os.Getenv("LIBVA_DRIVER_NAME"); got != "custom-driver" {
97+ t.Fatalf("LIBVA_DRIVER_NAME = %q, want explicit custom-driver", got)
98+ }
99+}
100+
101+func TestApplyDefaultHardwareDecodeEnvironment_DisablesCEFVAAPIWhenMediaDisabled(t *testing.T) {
102+ t.Setenv(cefEnableVAAPIEnvVar, "")
103+
104+ ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{
105+ Engine: config.EngineConfig{Type: config.EngineTypeCEF},
106+ Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingDisable},
107+ })
108+
109+ if got := os.Getenv(cefEnableVAAPIEnvVar); got != "0" {
110+ t.Fatalf("%s = %q, want 0", cefEnableVAAPIEnvVar, got)
111+ }
112+}
113+
114+func TestApplyDefaultHardwareDecodeEnvironment_PreservesExplicitCEFVAAPIOverride(t *testing.T) {
115+ t.Setenv(cefEnableVAAPIEnvVar, "0")
116+
117+ ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{
118+ Engine: config.EngineConfig{Type: config.EngineTypeCEF},
119+ Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingAuto},
120+ })
121+
122+ if got := os.Getenv(cefEnableVAAPIEnvVar); got != "0" {
123+ t.Fatalf("%s = %q, want explicit 0", cefEnableVAAPIEnvVar, got)
124+ }
125+}
M internal/infrastructure/cef/webview.go +37 -10
@@ -1042,16 +1042,7 @@ func (wv *WebView) Destroy() {
10421042 host.CloseBrowser(1)
10431043 } else {
10441044 wv.runCloseCallbacks()
1045- }
1046- if bridge := wv.viewBridge; bridge != nil {
1047- wv.runOnGTKSync(func() {
1048- if wv.removeSizeObserver != nil {
1049- wv.removeSizeObserver()
1050- wv.removeSizeObserver = nil
1051- }
1052- _ = bridge.DetachInput()
1053- _ = bridge.Destroy()
1054- })
1045+ wv.destroyViewBridgeOnGTKSync()
10551046 }
10561047 if wv.profileCleanup != nil {
10571048 wv.profileCleanup()
@@ -1059,6 +1050,42 @@ func (wv *WebView) Destroy() {
10591050 }
10601051 }
10611052
1053+// destroyViewBridgeOnGTKSync, destroyViewBridgeOnGTKAsync, and
1054+// destroyViewBridgeOnGTKThread may race with one another during teardown. The
1055+// early viewBridge nil checks avoid scheduling unnecessary GTK work; the final
1056+// nil guard on the GTK thread is authoritative and makes duplicate scheduling
1057+// harmless.
1058+func (wv *WebView) destroyViewBridgeOnGTKSync() {
1059+ if wv == nil || wv.viewBridge == nil {
1060+ return
1061+ }
1062+ wv.runOnGTKSync(func() {
1063+ wv.destroyViewBridgeOnGTKThread()
1064+ })
1065+}
1066+
1067+func (wv *WebView) destroyViewBridgeOnGTKAsync() {
1068+ if wv == nil || wv.viewBridge == nil {
1069+ return
1070+ }
1071+ wv.runOnGTK(func() {
1072+ wv.destroyViewBridgeOnGTKThread()
1073+ })
1074+}
1075+
1076+func (wv *WebView) destroyViewBridgeOnGTKThread() {
1077+ if wv == nil || wv.viewBridge == nil {
1078+ return
1079+ }
1080+ if wv.removeSizeObserver != nil {
1081+ wv.removeSizeObserver()
1082+ wv.removeSizeObserver = nil
1083+ }
1084+ _ = wv.viewBridge.DetachInput()
1085+ _ = wv.viewBridge.Destroy()
1086+ wv.viewBridge = nil
1087+}
1088+
10621089 // ---------------------------------------------------------------------------
10631090 // NativeWidgetProvider
10641091 // ---------------------------------------------------------------------------
@@ -1042,16 +1042,7 @@ func (wv *WebView) Destroy() {
1042 host.CloseBrowser(1)1042 host.CloseBrowser(1)
1043 } else {1043 } else {
1044 wv.runCloseCallbacks()1044 wv.runCloseCallbacks()
1045- }1045+ wv.destroyViewBridgeOnGTKSync()
1046- if bridge := wv.viewBridge; bridge != nil {
1047- wv.runOnGTKSync(func() {
1048- if wv.removeSizeObserver != nil {
1049- wv.removeSizeObserver()
1050- wv.removeSizeObserver = nil
1051- }
1052- _ = bridge.DetachInput()
1053- _ = bridge.Destroy()
1054- })
1055 }1046 }
1056 if wv.profileCleanup != nil {1047 if wv.profileCleanup != nil {
1057 wv.profileCleanup()1048 wv.profileCleanup()
@@ -1059,6 +1050,42 @@ func (wv *WebView) Destroy() {
1059 }1050 }
1060 }1051 }
1061 1052
1053+// destroyViewBridgeOnGTKSync, destroyViewBridgeOnGTKAsync, and
1054+// destroyViewBridgeOnGTKThread may race with one another during teardown. The
1055+// early viewBridge nil checks avoid scheduling unnecessary GTK work; the final
1056+// nil guard on the GTK thread is authoritative and makes duplicate scheduling
1057+// harmless.
1058+func (wv *WebView) destroyViewBridgeOnGTKSync() {
1059+ if wv == nil || wv.viewBridge == nil {
1060+ return
1061+ }
1062+ wv.runOnGTKSync(func() {
1063+ wv.destroyViewBridgeOnGTKThread()
1064+ })
1065+}
1066+
1067+func (wv *WebView) destroyViewBridgeOnGTKAsync() {
1068+ if wv == nil || wv.viewBridge == nil {
1069+ return
1070+ }
1071+ wv.runOnGTK(func() {
1072+ wv.destroyViewBridgeOnGTKThread()
1073+ })
1074+}
1075+
1076+func (wv *WebView) destroyViewBridgeOnGTKThread() {
1077+ if wv == nil || wv.viewBridge == nil {
1078+ return
1079+ }
1080+ if wv.removeSizeObserver != nil {
1081+ wv.removeSizeObserver()
1082+ wv.removeSizeObserver = nil
1083+ }
1084+ _ = wv.viewBridge.DetachInput()
1085+ _ = wv.viewBridge.Destroy()
1086+ wv.viewBridge = nil
1087+}
1088+
1062 // ---------------------------------------------------------------------------1089 // ---------------------------------------------------------------------------
1063 // NativeWidgetProvider1090 // NativeWidgetProvider
1064 // ---------------------------------------------------------------------------1091 // ---------------------------------------------------------------------------
M internal/infrastructure/cef/webview_handlers.go +3 -1
@@ -657,7 +657,9 @@ func (h *handlerSet) OnBeforeClose(browser purecef.Browser) {
657657 h.wv.findCtrl.setHost(nil)
658658 }
659659 h.wv.mu.Unlock()
660- if h.wv.viewBridge != nil {
660+ if h.wv.destroyed.Load() {
661+ h.wv.destroyViewBridgeOnGTKAsync()
662+ } else if h.wv.viewBridge != nil {
661663 wv := h.wv
662664 wv.runOnGTK(func() {
663665 if wv.viewBridge == nil {
@@ -657,7 +657,9 @@ func (h *handlerSet) OnBeforeClose(browser purecef.Browser) {
657 h.wv.findCtrl.setHost(nil)657 h.wv.findCtrl.setHost(nil)
658 }658 }
659 h.wv.mu.Unlock()659 h.wv.mu.Unlock()
660- if h.wv.viewBridge != nil {660+ if h.wv.destroyed.Load() {
661+ h.wv.destroyViewBridgeOnGTKAsync()
662+ } else if h.wv.viewBridge != nil {
661 wv := h.wv663 wv := h.wv
662 wv.runOnGTK(func() {664 wv.runOnGTK(func() {
663 if wv.viewBridge == nil {665 if wv.viewBridge == nil {
A internal/infrastructure/cef/webview_lifecycle_test.go +48 -0
@@ -0,0 +1,48 @@
1+package cef
2+
3+import (
4+ "testing"
5+
6+ purecef "github.com/bnema/purego-cef/cef"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestDestroy_WithHostDefersBridgeDestroyUntilBeforeClose(t *testing.T) {
11+ bridge := &Cef2gtkAdapter{}
12+ host := &stubBrowserHost{}
13+ wv := &WebView{
14+ viewBridge: bridge,
15+ host: host,
16+ }
17+
18+ wv.Destroy()
19+
20+ require.True(t, wv.IsDestroyed())
21+ require.True(t, host.closeBrowserCalled)
22+ require.False(t, bridge.IsDestroyed(), "Destroy should request CEF close before destroying the GTK bridge")
23+
24+ wv.destroyViewBridgeOnGTKSync()
25+
26+ require.True(t, bridge.IsDestroyed())
27+}
28+
29+func TestDestroy_WithoutHostDestroysBridgeImmediately(t *testing.T) {
30+ bridge := &Cef2gtkAdapter{}
31+ wv := &WebView{viewBridge: bridge}
32+
33+ wv.Destroy()
34+
35+ require.True(t, bridge.IsDestroyed())
36+}
37+
38+// stubBrowserHost intentionally only stubs CloseBrowser. The embedded
39+// purecef.BrowserHost may be nil; calling any other BrowserHost method will
40+// panic and should be avoided in these lifecycle tests.
41+type stubBrowserHost struct {
42+ purecef.BrowserHost
43+ closeBrowserCalled bool
44+}
45+
46+func (s *stubBrowserHost) CloseBrowser(int32) {
47+ s.closeBrowserCalled = true
48+}
@@ -0,0 +1,48 @@
1+package cef
2+
3+import (
4+ "testing"
5+
6+ purecef "github.com/bnema/purego-cef/cef"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestDestroy_WithHostDefersBridgeDestroyUntilBeforeClose(t *testing.T) {
11+ bridge := &Cef2gtkAdapter{}
12+ host := &stubBrowserHost{}
13+ wv := &WebView{
14+ viewBridge: bridge,
15+ host: host,
16+ }
17+
18+ wv.Destroy()
19+
20+ require.True(t, wv.IsDestroyed())
21+ require.True(t, host.closeBrowserCalled)
22+ require.False(t, bridge.IsDestroyed(), "Destroy should request CEF close before destroying the GTK bridge")
23+
24+ wv.destroyViewBridgeOnGTKSync()
25+
26+ require.True(t, bridge.IsDestroyed())
27+}
28+
29+func TestDestroy_WithoutHostDestroysBridgeImmediately(t *testing.T) {
30+ bridge := &Cef2gtkAdapter{}
31+ wv := &WebView{viewBridge: bridge}
32+
33+ wv.Destroy()
34+
35+ require.True(t, bridge.IsDestroyed())
36+}
37+
38+// stubBrowserHost intentionally only stubs CloseBrowser. The embedded
39+// purecef.BrowserHost may be nil; calling any other BrowserHost method will
40+// panic and should be avoided in these lifecycle tests.
41+type stubBrowserHost struct {
42+ purecef.BrowserHost
43+ closeBrowserCalled bool
44+}
45+
46+func (s *stubBrowserHost) CloseBrowser(int32) {
47+ s.closeBrowserCalled = true
48+}
M internal/infrastructure/config/loader.go +0 -2
@@ -610,8 +610,6 @@ func (m *Manager) setEngineDefaults(defaults *Config) {
610610 m.viper.SetDefault("engine.webkit.network_process_memory_strict_threshold", wk.NetworkProcessMemoryStrictThreshold)
611611 }
612612
613-
614-
615613 // New returns a new default configuration instance.
616614 // This is a convenience function for getting default config without the full manager.
617615 func New() *Config {
@@ -610,8 +610,6 @@ func (m *Manager) setEngineDefaults(defaults *Config) {
610 m.viper.SetDefault("engine.webkit.network_process_memory_strict_threshold", wk.NetworkProcessMemoryStrictThreshold)610 m.viper.SetDefault("engine.webkit.network_process_memory_strict_threshold", wk.NetworkProcessMemoryStrictThreshold)
611 }611 }
612 612
613-
614-
615 // New returns a new default configuration instance.613 // New returns a new default configuration instance.
616 // This is a convenience function for getting default config without the full manager.614 // This is a convenience function for getting default config without the full manager.
617 func New() *Config {615 func New() *Config {
M internal/ui/app.go +16 -2
@@ -2581,9 +2581,23 @@ func (a *App) MainWindow() *window.MainWindow {
25812581
25822582 // Quit requests the application to quit.
25832583 func (a *App) Quit() {
2584- if a.gtkApp != nil {
2585- a.gtkApp.Quit()
2584+ if a == nil || a.gtkApp == nil {
2585+ return
2586+ }
2587+ quit := func() {
2588+ if a.gtkApp != nil {
2589+ a.gtkApp.Quit()
2590+ }
2591+ }
2592+ glibCtx := glib.MainContextDefault()
2593+ if glibCtx != nil && glibCtx.IsOwner() {
2594+ quit()
2595+ return
25862596 }
2597+ cb := glib.SourceOnceFunc(func(_ uintptr) {
2598+ quit()
2599+ })
2600+ glib.IdleAddOnce(&cb, 0)
25872601 }
25882602
25892603 // RunWithArgs is a convenience function that creates and runs an App.
@@ -2581,9 +2581,23 @@ func (a *App) MainWindow() *window.MainWindow {
2581 2581
2582 // Quit requests the application to quit.2582 // Quit requests the application to quit.
2583 func (a *App) Quit() {2583 func (a *App) Quit() {
2584- if a.gtkApp != nil {2584+ if a == nil || a.gtkApp == nil {
2585- a.gtkApp.Quit()2585+ return
2586+ }
2587+ quit := func() {
2588+ if a.gtkApp != nil {
2589+ a.gtkApp.Quit()
2590+ }
2591+ }
2592+ glibCtx := glib.MainContextDefault()
2593+ if glibCtx != nil && glibCtx.IsOwner() {
2594+ quit()
2595+ return
2586 }2596 }
2597+ cb := glib.SourceOnceFunc(func(_ uintptr) {
2598+ quit()
2599+ })
2600+ glib.IdleAddOnce(&cb, 0)
2587 }2601 }
2588 2602
2589 // RunWithArgs is a convenience function that creates and runs an App.2603 // RunWithArgs is a convenience function that creates and runs an App.

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