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
58 log.Debug().
59 Interface("vars", m.appliedVars).
60 Str("gpu", string(m.gpuVendor)).
61 Msg("applied rendering environment")
62
63 return nil
64}
65
66func (m *Manager) applyGStreamerEnv(settings port.RenderingEnvSettings) {
67 // VSync
68 if settings.ForceVSync {
69 m.setEnv("__GL_SYNC_TO_VBLANK", "1")
70 m.setEnv("vblank_mode", "3")
71 }
72
73 // GL API
74 switch strings.ToLower(settings.GLRenderingMode) {
75 case "gles2":
76 m.setEnv("GST_GL_API", "gles2")
77 case "gl3":
78 m.setEnv("GST_GL_API", "opengl3")
79 case "none":
80 m.setEnv("GST_GL_API", "none")
81 case "auto", "":
82 // Don't set - let GStreamer decide
83 }
84
85 // GStreamer debug level
86 if settings.GStreamerDebugLevel > 0 {
87 level := settings.GStreamerDebugLevel
88 if level > 5 {
89 level = 5
90 }
91 m.setEnv("GST_DEBUG", fmt.Sprintf("%d", level))
92 }
93
94 // Video buffer size
95 if settings.VideoBufferSizeMB > 0 {
96 bufferBytes := settings.VideoBufferSizeMB * 1024 * 1024
97 m.setEnv("GST_BUFFER_SIZE", fmt.Sprintf("%d", bufferBytes))
98 m.setEnv("GST_QUEUE2_MAX_SIZE_BYTES", fmt.Sprintf("%d", bufferBytes))
99 }
100
101 // Queue buffer time
102 if settings.QueueBufferTimeSec > 0 {
103 bufferTimeNs := settings.QueueBufferTimeSec * 1_000_000_000
104 m.setEnv("GST_QUEUE2_MAX_SIZE_TIME", fmt.Sprintf("%d", bufferTimeNs))
105 }
106}
107
108func (m *Manager) applyGPUSpecificEnv() {
109 if os.Getenv("LIBVA_DRIVER_NAME") != "" {
110 return
111 }
112
113 switch m.gpuVendor {
114 case port.GPUVendorAMD:
115 m.setEnv("LIBVA_DRIVER_NAME", "radeonsi")
116 case port.GPUVendorIntel:
117 m.setEnv("LIBVA_DRIVER_NAME", "iHD")
118 case port.GPUVendorNVIDIA:
119 m.setEnv("LIBVA_DRIVER_NAME", "nvidia")
120 if os.Getenv("GST_GL_PLATFORM") == "" {
121 m.setEnv("GST_GL_PLATFORM", "egl")
122 }
123 }
124}
125
126func (m *Manager) applyWebKitEnv(settings port.RenderingEnvSettings) {
127 if settings.DisableDMABufRenderer {
128 m.setEnv("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
129 }
130 if settings.ForceCompositingMode {
131 m.setEnv("WEBKIT_FORCE_COMPOSITING_MODE", "1")
132 }
133 if settings.DisableCompositingMode {
134 m.setEnv("WEBKIT_DISABLE_COMPOSITING_MODE", "1")
135 }
136}
137
138func (m *Manager) applyGTKEnv(settings port.RenderingEnvSettings) {
139 renderer := strings.ToLower(strings.TrimSpace(settings.GSKRenderer))
140 if renderer != "" && renderer != "auto" {
141 m.setEnv("GSK_RENDERER", renderer)
142 }
143
144 var gskDisable []string
145 if settings.DisableMipmaps {
146 gskDisable = append(gskDisable, "mipmap")
147 }
148 if len(gskDisable) > 0 {
149 m.setEnv("GSK_GPU_DISABLE", strings.Join(gskDisable, ","))
150 }
151
152 var gdkDebug []string
153 if settings.PreferGL {
154 gdkDebug = append(gdkDebug, "gl-prefer-gl")
155 }
156 if settings.DebugFrames {
157 gdkDebug = append(gdkDebug, "frames")
158 }
159 if len(gdkDebug) > 0 {
160 m.setEnv("GDK_DEBUG", strings.Join(gdkDebug, ","))
161 }
162}
163
164func (m *Manager) applyDebugEnv(settings port.RenderingEnvSettings) {
165 if settings.ShowFPS {
166 m.setEnv("WEBKIT_SHOW_FPS", "1")
167 }
168 if settings.SampleMemory {
169 m.setEnv("WEBKIT_SAMPLE_MEMORY", "1")
170 }
171}
172
173func (m *Manager) setEnv(key, value string) {
174 _ = os.Setenv(key, value)
175 m.appliedVars[key] = value
176}
177
178// GetAppliedVars returns a copy of applied environment variables.
179func (m *Manager) GetAppliedVars() map[string]string {
180 result := make(map[string]string, len(m.appliedVars))
181 for k, v := range m.appliedVars {
182 result[k] = v
183 }
184 return result
185}
186
187func (*Manager) detectFromDRM() port.GPUVendor {
188 const drmBase = "/sys/class/drm"
189 for _, card := range []string{"card0", "card1"} {
190 vendorPath := filepath.Join(drmBase, card, "device", "vendor")
191 data, err := os.ReadFile(vendorPath)
192 if err != nil {
193 continue
194 }
195
196 vendorID := strings.TrimSpace(string(data))
197 switch vendorID {
198 case "0x1002":
199 return port.GPUVendorAMD
200 case "0x8086":
201 return port.GPUVendorIntel
202 case "0x10de":
203 return port.GPUVendorNVIDIA
204 }
205 }
206 return port.GPUVendorUnknown
207}
208
209var _ port.RenderingEnvManager = (*Manager)(nil)