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