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 // NOTE: GST_BUFFER_SIZE, GST_QUEUE2_MAX_SIZE_BYTES, GST_QUEUE2_MAX_SIZE_TIME
95 // are NOT valid GStreamer environment variables. They are element properties
96 // that must be set programmatically on individual GStreamer elements.
97 // WebKitGTK manages its own GStreamer pipeline internally.
98}
99
100func (m *Manager) applyGPUSpecificEnv() {
101 if os.Getenv("LIBVA_DRIVER_NAME") != "" {
102 return
103 }
104
105 switch m.gpuVendor {
106 case port.GPUVendorAMD:
107 m.setEnv("LIBVA_DRIVER_NAME", "radeonsi")
108 case port.GPUVendorIntel:
109 m.setEnv("LIBVA_DRIVER_NAME", "iHD")
110 case port.GPUVendorNVIDIA:
111 m.setEnv("LIBVA_DRIVER_NAME", "nvidia")
112 if os.Getenv("GST_GL_PLATFORM") == "" {
113 m.setEnv("GST_GL_PLATFORM", "egl")
114 }
115 }
116}
117
118func (m *Manager) applyWebKitEnv(settings port.RenderingEnvSettings) {
119 if settings.DisableDMABufRenderer {
120 m.setEnv("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
121 }
122 if settings.ForceCompositingMode {
123 m.setEnv("WEBKIT_FORCE_COMPOSITING_MODE", "1")
124 }
125 if settings.DisableCompositingMode {
126 m.setEnv("WEBKIT_DISABLE_COMPOSITING_MODE", "1")
127 }
128}
129
130func (m *Manager) applyGTKEnv(settings port.RenderingEnvSettings) {
131 renderer := strings.ToLower(strings.TrimSpace(settings.GSKRenderer))
132 if renderer != "" && renderer != "auto" {
133 m.setEnv("GSK_RENDERER", renderer)
134 }
135
136 var gskDisable []string
137 if settings.DisableMipmaps {
138 gskDisable = append(gskDisable, "mipmap")
139 }
140 if len(gskDisable) > 0 {
141 m.setEnv("GSK_GPU_DISABLE", strings.Join(gskDisable, ","))
142 }
143
144 var gdkDebug []string
145 if settings.PreferGL {
146 gdkDebug = append(gdkDebug, "gl-prefer-gl")
147 }
148 if settings.DebugFrames {
149 gdkDebug = append(gdkDebug, "frames")
150 }
151 if len(gdkDebug) > 0 {
152 m.setEnv("GDK_DEBUG", strings.Join(gdkDebug, ","))
153 }
154}
155
156func (m *Manager) applyDebugEnv(settings port.RenderingEnvSettings) {
157 if settings.ShowFPS {
158 m.setEnv("WEBKIT_SHOW_FPS", "1")
159 }
160 if settings.SampleMemory {
161 m.setEnv("WEBKIT_SAMPLE_MEMORY", "1")
162 }
163}
164
165func (m *Manager) setEnv(key, value string) {
166 _ = os.Setenv(key, value)
167 m.appliedVars[key] = value
168}
169
170// GetAppliedVars returns a copy of applied environment variables.
171func (m *Manager) GetAppliedVars() map[string]string {
172 result := make(map[string]string, len(m.appliedVars))
173 for k, v := range m.appliedVars {
174 result[k] = v
175 }
176 return result
177}
178
179func (*Manager) detectFromDRM() port.GPUVendor {
180 const drmBase = "/sys/class/drm"
181 for _, card := range []string{"card0", "card1"} {
182 vendorPath := filepath.Join(drmBase, card, "device", "vendor")
183 data, err := os.ReadFile(vendorPath)
184 if err != nil {
185 continue
186 }
187
188 vendorID := strings.TrimSpace(string(data))
189 switch vendorID {
190 case "0x1002":
191 return port.GPUVendorAMD
192 case "0x8086":
193 return port.GPUVendorIntel
194 case "0x10de":
195 return port.GPUVendorNVIDIA
196 }
197 }
198 return port.GPUVendorUnknown
199}
200
201var _ port.RenderingEnvManager = (*Manager)(nil)