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