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)