v0.31.0 bf0756b

manager.go RawHistoryFolder

1// Package env provides environment variable management for rendering subsystems.
2package env
3
4import (
5	"context"
6	"fmt"
7	"maps"
8	"os"
9	"os/exec"
10	"path/filepath"
11	"strings"
12
13	"github.com/bnema/dumber/internal/application/port"
14	"github.com/bnema/dumber/internal/logging"
15)
16
17// IsFlatpak returns true if the application is running inside a Flatpak sandbox.
18// The Flatpak runtime provides all required libraries, so host pkg-config checks
19// should be skipped when running in this environment.
20func IsFlatpak() bool {
21	_, err := os.Stat("/.flatpak-info")
22	return err == nil
23}
24
25// IsPacman returns true if the application was installed via pacman (including AUR).
26// Packages managed by pacman should not self-update; use pacman/AUR helpers instead.
27// Detection uses `pacman -Qo` to check if the binary is owned by a package.
28func IsPacman() bool {
29	exe, err := os.Executable()
30	if err != nil {
31		return false
32	}
33	resolved, err := filepath.EvalSymlinks(exe)
34	if err != nil {
35		return false
36	}
37
38	// Check if pacman exists and if it owns this binary
39	pacman, err := exec.LookPath("pacman")
40	if err != nil {
41		return false
42	}
43
44	// pacman -Qo returns 0 if the file is owned by a package
45	cmd := exec.Command(pacman, "-Qo", resolved)
46	return cmd.Run() == nil
47}
48
49// Manager implements port.RenderingEnvManager for configuring
50// environment variables for GStreamer, WebKit, and GTK/GSK.
51type Manager struct {
52	appliedVars map[string]string
53	gpuVendor   port.GPUVendor
54}
55
56// NewManager creates a new environment manager.
57func NewManager() *Manager {
58	return &Manager{
59		appliedVars: make(map[string]string),
60		gpuVendor:   port.GPUVendorUnknown,
61	}
62}
63
64// DetectGPUVendor identifies the primary GPU vendor from system info.
65func (m *Manager) DetectGPUVendor(ctx context.Context) port.GPUVendor {
66	log := logging.FromContext(ctx)
67
68	vendor := m.detectFromDRM()
69	if vendor != port.GPUVendorUnknown {
70		m.gpuVendor = vendor
71		log.Debug().Str("vendor", string(vendor)).Msg("detected GPU vendor from DRM")
72		return vendor
73	}
74
75	log.Debug().Msg("could not detect GPU vendor, using unknown")
76	m.gpuVendor = port.GPUVendorUnknown
77	return port.GPUVendorUnknown
78}
79
80// ApplyEnvironment sets all rendering environment variables.
81// Must be called before GTK/WebKit initialization.
82func (m *Manager) ApplyEnvironment(ctx context.Context, settings RenderingSettings) error {
83	log := logging.FromContext(ctx)
84	m.appliedVars = make(map[string]string)
85
86	m.applyGStreamerEnv(settings)
87	m.applyGPUSpecificEnv()
88	m.applyWebKitEnv(settings)
89	m.applyGTKEnv(settings)
90	m.applyDebugEnv(settings)
91	m.applySkiaEnv(settings)
92
93	log.Debug().
94		Interface("vars", m.appliedVars).
95		Str("gpu", string(m.gpuVendor)).
96		Msg("applied rendering environment")
97
98	return nil
99}
100
101func (m *Manager) applyGStreamerEnv(settings RenderingSettings) {
102	// VSync
103	if settings.ForceVSync {
104		m.setEnv("__GL_SYNC_TO_VBLANK", "1")
105		m.setEnv("vblank_mode", "3")
106	}
107
108	// GL API
109	switch strings.ToLower(settings.GLRenderingMode) {
110	case "gles2":
111		m.setEnv("GST_GL_API", "gles2")
112	case "gl3":
113		m.setEnv("GST_GL_API", "opengl3")
114	case "none":
115		m.setEnv("GST_GL_API", "none")
116	case "auto", "":
117		// Don't set - let GStreamer decide
118	}
119
120	// GStreamer debug level
121	if settings.GStreamerDebugLevel > 0 {
122		level := min(settings.GStreamerDebugLevel, 5)
123		m.setEnv("GST_DEBUG", fmt.Sprintf("%d", level))
124	}
125
126	// NOTE: GST_BUFFER_SIZE, GST_QUEUE2_MAX_SIZE_BYTES, GST_QUEUE2_MAX_SIZE_TIME
127	// are NOT valid GStreamer environment variables. They are element properties
128	// that must be set programmatically on individual GStreamer elements.
129	// WebKitGTK manages its own GStreamer pipeline internally.
130}
131
132func (m *Manager) applyGPUSpecificEnv() {
133	if os.Getenv("LIBVA_DRIVER_NAME") != "" {
134		return
135	}
136
137	switch m.gpuVendor {
138	case port.GPUVendorAMD:
139		m.setEnv("LIBVA_DRIVER_NAME", "radeonsi")
140	case port.GPUVendorIntel:
141		m.setEnv("LIBVA_DRIVER_NAME", "iHD")
142	case port.GPUVendorNVIDIA:
143		m.setEnv("LIBVA_DRIVER_NAME", "nvidia")
144		if os.Getenv("GST_GL_PLATFORM") == "" {
145			m.setEnv("GST_GL_PLATFORM", "egl")
146		}
147	}
148}
149
150func (m *Manager) applyWebKitEnv(settings RenderingSettings) {
151	if settings.DisableDMABufRenderer {
152		m.setEnv("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
153	}
154	if settings.ForceCompositingMode {
155		m.setEnv("WEBKIT_FORCE_COMPOSITING_MODE", "1")
156	}
157	if settings.DisableCompositingMode {
158		m.setEnv("WEBKIT_DISABLE_COMPOSITING_MODE", "1")
159	}
160}
161
162func (m *Manager) applyGTKEnv(settings RenderingSettings) {
163	renderer := strings.ToLower(strings.TrimSpace(settings.GSKRenderer))
164	if renderer != "" && renderer != "auto" {
165		m.setEnv("GSK_RENDERER", renderer)
166	}
167
168	var gskDisable []string
169	if settings.DisableMipmaps {
170		gskDisable = append(gskDisable, "mipmap")
171	}
172	if len(gskDisable) > 0 {
173		m.setEnv("GSK_GPU_DISABLE", strings.Join(gskDisable, ","))
174	}
175
176	var gdkDebug []string
177	if settings.PreferGL {
178		gdkDebug = append(gdkDebug, "gl-prefer-gl")
179	}
180	if settings.DebugFrames {
181		gdkDebug = append(gdkDebug, "frames")
182	}
183	if len(gdkDebug) > 0 {
184		m.setEnv("GDK_DEBUG", strings.Join(gdkDebug, ","))
185	}
186}
187
188func (m *Manager) applyDebugEnv(settings RenderingSettings) {
189	if settings.ShowFPS {
190		m.setEnv("WEBKIT_SHOW_FPS", "1")
191	}
192	if settings.SampleMemory {
193		m.setEnv("WEBKIT_SAMPLE_MEMORY", "1")
194	}
195}
196
197func (m *Manager) applySkiaEnv(settings RenderingSettings) {
198	// CPU painting threads: only set if > 0 and env var not already set
199	if settings.SkiaCPUPaintingThreads > 0 && os.Getenv("WEBKIT_SKIA_CPU_PAINTING_THREADS") == "" {
200		m.setEnv("WEBKIT_SKIA_CPU_PAINTING_THREADS", fmt.Sprintf("%d", settings.SkiaCPUPaintingThreads))
201	}
202
203	// GPU painting threads: only set if >= 0 (0 is valid: disables GPU tile painting)
204	if settings.SkiaGPUPaintingThreads >= 0 && os.Getenv("WEBKIT_SKIA_GPU_PAINTING_THREADS") == "" {
205		m.setEnv("WEBKIT_SKIA_GPU_PAINTING_THREADS", fmt.Sprintf("%d", settings.SkiaGPUPaintingThreads))
206	}
207
208	// CPU rendering: only set if true and env var not already set
209	if settings.SkiaEnableCPURendering && os.Getenv("WEBKIT_SKIA_ENABLE_CPU_RENDERING") == "" {
210		m.setEnv("WEBKIT_SKIA_ENABLE_CPU_RENDERING", "1")
211	}
212}
213
214func (m *Manager) setEnv(key, value string) {
215	_ = os.Setenv(key, value)
216	m.appliedVars[key] = value
217}
218
219// GetAppliedVars returns a copy of applied environment variables.
220func (m *Manager) GetAppliedVars() map[string]string {
221	result := make(map[string]string, len(m.appliedVars))
222	maps.Copy(result, m.appliedVars)
223	return result
224}
225
226func (*Manager) detectFromDRM() port.GPUVendor {
227	const drmBase = "/sys/class/drm"
228	for _, card := range []string{"card0", "card1"} {
229		vendorPath := filepath.Join(drmBase, card, "device", "vendor")
230		data, err := os.ReadFile(vendorPath)
231		if err != nil {
232			continue
233		}
234
235		vendorID := strings.TrimSpace(string(data))
236		switch vendorID {
237		case "0x1002":
238			return port.GPUVendorAMD
239		case "0x8086":
240			return port.GPUVendorIntel
241		case "0x10de":
242			return port.GPUVendorNVIDIA
243		}
244	}
245	return port.GPUVendorUnknown
246}
247
248// Ensure Manager implements port.RenderingEnvManager (DetectGPUVendor, GetAppliedVars).
249// ApplyEnvironment takes the infra-local RenderingSettings type instead of a port type.
250var _ port.RenderingEnvManager = (*Manager)(nil)

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