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