1package system
2
3import (
4 "fmt"
5 "os"
6 "strconv"
7 "strings"
8)
9
10// CPUVendor represents CPU manufacturer
11type CPUVendor string
12
13const (
14 CPUVendorIntel CPUVendor = "Intel"
15 CPUVendorAMD CPUVendor = "AMD"
16 CPUVendorUnknown CPUVendor = "Unknown"
17)
18
19// AMDPStateMode represents AMD P-State driver mode
20type AMDPStateMode string
21
22const (
23 AMDPStateModeActive AMDPStateMode = "active"
24 AMDPStateModeGuided AMDPStateMode = "guided"
25 AMDPStateModePassive AMDPStateMode = "passive"
26 AMDPStateModeNone AMDPStateMode = ""
27)
28
29// AMDZenGen represents AMD Zen generation
30type AMDZenGen struct {
31 Generation string // "1", "1+", "2", "3", "3+", "4", "5", "unknown"
32 Label string // "Zen 1", "Zen+", "Zen 2", etc.
33}
34
35// CPUInfo holds detected CPU information
36type CPUInfo struct {
37 Vendor CPUVendor
38 ModelName string // Full CPU model name
39 Microcode string
40 AMDZenGen *AMDZenGen // AMD Zen generation (nil for non-AMD)
41 AMDPStateModes []AMDPStateMode
42 RecommendedPStateMode AMDPStateMode // Recommended mode for this CPU
43}
44
45// DetectCPUVendor reads /proc/cpuinfo and returns the CPU vendor
46func DetectCPUVendor() (CPUVendor, error) {
47 data, err := os.ReadFile("/proc/cpuinfo")
48 if err != nil {
49 return CPUVendorUnknown, fmt.Errorf("failed to read /proc/cpuinfo: %w", err)
50 }
51
52 content := string(data)
53 lines := strings.Split(content, "\n")
54
55 for _, line := range lines {
56 switch {
57 case strings.HasPrefix(line, "vendor_id"):
58 parts := strings.Split(line, ":")
59 if len(parts) < 2 {
60 continue
61 }
62
63 vendor := strings.TrimSpace(parts[1])
64 switch vendor {
65 case "GenuineIntel":
66 return CPUVendorIntel, nil
67 case "AuthenticAMD":
68 return CPUVendorAMD, nil
69 default:
70 return CPUVendorUnknown, nil
71 }
72 }
73 }
74
75 return CPUVendorUnknown, nil
76}
77
78// DetectMicrocode returns the appropriate microcode package based on CPU vendor
79func DetectMicrocode(vendor CPUVendor) string {
80 switch vendor {
81 case CPUVendorIntel:
82 return "intel-ucode"
83 case CPUVendorAMD:
84 return "amd-ucode"
85 default:
86 return ""
87 }
88}
89
90// CheckCPPCSupport checks if CPU supports CPPC (required for AMD P-State active mode)
91func CheckCPPCSupport() bool {
92 // Check /proc/cpuinfo for cppc flag
93 data, err := os.ReadFile("/proc/cpuinfo")
94 if err != nil {
95 return false
96 }
97
98 if strings.Contains(string(data), "cppc") {
99 return true
100 }
101
102 // Check if amd_pstate directory exists
103 _, err = os.Stat("/sys/devices/system/cpu/cpu0/cpufreq/amd_pstate")
104 return err == nil
105}
106
107// DetectAMDPStateModes detects available AMD P-State modes
108func DetectAMDPStateModes() []AMDPStateMode {
109 var modes []AMDPStateMode
110
111 // Try to read available modes from sysfs
112 data, err := os.ReadFile("/sys/devices/system/cpu/amd_pstate/status")
113 if err == nil {
114 // Parse available modes from status file
115 content := string(data)
116 if strings.Contains(content, "active") {
117 modes = append(modes, AMDPStateModeActive)
118 }
119 if strings.Contains(content, "guided") {
120 modes = append(modes, AMDPStateModeGuided)
121 }
122 if strings.Contains(content, "passive") {
123 modes = append(modes, AMDPStateModePassive)
124 }
125
126 // If we found modes, return them
127 if len(modes) > 0 {
128 return modes
129 }
130 }
131
132 // Fallback: detect based on CPPC support
133 switch CheckCPPCSupport() {
134 case true:
135 // Modern AMD Ryzen CPUs support all three modes
136 return []AMDPStateMode{
137 AMDPStateModeActive,
138 AMDPStateModeGuided,
139 AMDPStateModePassive,
140 }
141 default:
142 // Older AMD CPUs only support passive mode
143 return []AMDPStateMode{AMDPStateModePassive}
144 }
145}
146
147// GetAMDPStateModeDescription returns human-readable description for P-State mode
148func GetAMDPStateModeDescription(mode AMDPStateMode) string {
149 switch mode {
150 case AMDPStateModeActive:
151 return "active - Best performance (recommended for desktop/gaming)"
152 case AMDPStateModeGuided:
153 return "guided - Balanced performance and efficiency (recommended for laptops)"
154 case AMDPStateModePassive:
155 return "passive - Maximum compatibility (older CPUs)"
156 default:
157 return string(mode)
158 }
159}
160
161// DetectAMDZenGeneration detects AMD Zen generation based on CPU family and model
162// Reference: https://en.wikichip.org/wiki/amd/cpuid
163func DetectAMDZenGeneration() (*AMDZenGen, error) {
164 data, err := os.ReadFile("/proc/cpuinfo")
165 if err != nil {
166 return nil, fmt.Errorf("failed to read /proc/cpuinfo: %w", err)
167 }
168
169 var cpuFamily, cpuModel int
170 lines := strings.Split(string(data), "\n")
171
172 for _, line := range lines {
173 if strings.HasPrefix(line, "cpu family") {
174 parts := strings.Split(line, ":")
175 if len(parts) >= 2 {
176 val := strings.TrimSpace(parts[1])
177 cpuFamily, _ = strconv.Atoi(val)
178 }
179 }
180 if strings.HasPrefix(line, "model") && !strings.HasPrefix(line, "model name") {
181 parts := strings.Split(line, ":")
182 if len(parts) >= 2 {
183 val := strings.TrimSpace(parts[1])
184 cpuModel, _ = strconv.Atoi(val)
185 }
186 }
187
188 // Break early if we have both values
189 if cpuFamily > 0 && cpuModel >= 0 {
190 break
191 }
192 }
193
194 zenGen := &AMDZenGen{
195 Generation: "unknown",
196 Label: "Unknown",
197 }
198
199 // Detect Zen generation based on CPU family and model
200 switch cpuFamily {
201 case 23: // Family 17h
202 if cpuModel >= 0 && cpuModel <= 15 {
203 zenGen.Generation = "1"
204 zenGen.Label = "Zen 1"
205 } else if cpuModel >= 16 && cpuModel <= 31 {
206 zenGen.Generation = "1+"
207 zenGen.Label = "Zen+"
208 } else if cpuModel >= 48 && cpuModel <= 127 {
209 zenGen.Generation = "2"
210 zenGen.Label = "Zen 2"
211 } else {
212 zenGen.Generation = "2"
213 zenGen.Label = "Zen 2 (assumed)"
214 }
215 case 25: // Family 19h
216 if cpuModel >= 0 && cpuModel <= 15 {
217 zenGen.Generation = "3"
218 zenGen.Label = "Zen 3"
219 } else if cpuModel >= 32 && cpuModel <= 47 {
220 zenGen.Generation = "3"
221 zenGen.Label = "Zen 3"
222 } else if cpuModel >= 80 && cpuModel <= 95 {
223 zenGen.Generation = "3+"
224 zenGen.Label = "Zen 3+"
225 } else if cpuModel >= 96 && cpuModel <= 175 {
226 zenGen.Generation = "4"
227 zenGen.Label = "Zen 4"
228 } else {
229 zenGen.Generation = "3"
230 zenGen.Label = "Zen 3 (assumed)"
231 }
232 case 26: // Family 1Ah
233 zenGen.Generation = "5"
234 zenGen.Label = "Zen 5"
235 }
236
237 return zenGen, nil
238}
239
240// GetRecommendedPStateMode returns the best recommended mode for each Zen generation
241func GetRecommendedPStateMode(zenGen *AMDZenGen) AMDPStateMode {
242 switch zenGen.Generation {
243 case "1", "1+":
244 return AMDPStateModeNone // Use default acpi-cpufreq
245 case "2":
246 return AMDPStateModePassive // Conservative choice for Zen 2
247 case "3", "3+":
248 return AMDPStateModeActive // Best for Zen 3/3+
249 case "4", "5":
250 return AMDPStateModeActive // Best for Zen 4/5
251 default:
252 return AMDPStateModePassive
253 }
254}
255
256// GetAvailablePStateModes returns available P-State modes for a given Zen generation
257func GetAvailablePStateModes(zenGen *AMDZenGen) []AMDPStateMode {
258 hasActive := kernelVersionAtLeast(6, 3)
259 hasGuided := kernelVersionAtLeast(6, 4)
260
261 switch zenGen.Generation {
262 case "1", "1+":
263 // Zen 1 and Zen+: No CPPC support, use acpi-cpufreq
264 return []AMDPStateMode{}
265 case "2":
266 // Zen 2: Basic CPPC support, passive and active modes
267 modes := []AMDPStateMode{}
268 if hasActive {
269 modes = append(modes, AMDPStateModeActive)
270 }
271 modes = append(modes, AMDPStateModePassive)
272 return modes
273 case "3", "3+", "4", "5":
274 // Zen 3+: Full CPPC support for all modes
275 modes := []AMDPStateMode{}
276 if hasActive {
277 modes = append(modes, AMDPStateModeActive)
278 }
279 if hasGuided {
280 modes = append(modes, AMDPStateModeGuided)
281 }
282 modes = append(modes, AMDPStateModePassive)
283 return modes
284 default:
285 return []AMDPStateMode{AMDPStateModePassive}
286 }
287}
288
289func kernelVersionAtLeast(major, minor int) bool {
290 curMajor, curMinor := detectKernelVersion()
291 if curMajor == 0 && curMinor == 0 {
292 return false
293 }
294 if curMajor > major {
295 return true
296 }
297 if curMajor < major {
298 return false
299 }
300 return curMinor >= minor
301}
302
303func detectKernelVersion() (int, int) {
304 data, err := os.ReadFile("/proc/sys/kernel/osrelease")
305 if err != nil {
306 return 0, 0
307 }
308
309 version := strings.TrimSpace(string(data))
310 parts := strings.Split(version, ".")
311 if len(parts) < 2 {
312 return 0, 0
313 }
314
315 major, _ := strconv.Atoi(parts[0])
316 minorPart := strings.Split(parts[1], "-")[0]
317 minor, _ := strconv.Atoi(minorPart)
318
319 return major, minor
320}
321
322// GetPStateModeDescription returns human-readable description for P-State mode
323func GetPStateModeDescription(mode AMDPStateMode) string {
324 switch mode {
325 case AMDPStateModeActive:
326 return "CPPC autonomous with EPP hints (best performance, desktop/gaming)"
327 case AMDPStateModeGuided:
328 return "CPPC guided autonomous (balanced, laptop/hybrid)"
329 case AMDPStateModePassive:
330 return "CPPC non-autonomous (compatibility, manual governor control)"
331 default:
332 return string(mode)
333 }
334}
335
336// GetAMDPStateKernelParams returns kernel parameters needed for AMD P-State mode
337func GetAMDPStateKernelParams(zenGen *AMDZenGen, mode AMDPStateMode) string {
338 if mode == AMDPStateModeNone || mode == "" {
339 return ""
340 }
341
342 // Some Zen 2 CPUs need shared_mem parameter for active mode
343 if zenGen.Generation == "2" && mode == AMDPStateModeActive {
344 return fmt.Sprintf("amd_pstate=%s amd_pstate.shared_mem=1", mode)
345 }
346
347 return fmt.Sprintf("amd_pstate=%s", mode)
348}
349
350// GetCPUModelName extracts the CPU model name from /proc/cpuinfo
351func GetCPUModelName() string {
352 data, err := os.ReadFile("/proc/cpuinfo")
353 if err != nil {
354 return ""
355 }
356
357 lines := strings.Split(string(data), "\n")
358 for _, line := range lines {
359 if strings.HasPrefix(line, "model name") {
360 parts := strings.Split(line, ":")
361 if len(parts) >= 2 {
362 return strings.TrimSpace(parts[1])
363 }
364 }
365 }
366
367 return ""
368}
369
370// DetectCPUInfo detects all CPU information in one call (DRY)
371func DetectCPUInfo() (*CPUInfo, error) {
372 vendor, err := DetectCPUVendor()
373 if err != nil {
374 return nil, err
375 }
376
377 info := &CPUInfo{
378 Vendor: vendor,
379 ModelName: GetCPUModelName(),
380 Microcode: DetectMicrocode(vendor),
381 }
382
383 // Detect AMD-specific features only for AMD CPUs
384 if vendor == CPUVendorAMD {
385 zenGen, err := DetectAMDZenGeneration()
386 if err == nil {
387 info.AMDZenGen = zenGen
388 info.AMDPStateModes = GetAvailablePStateModes(zenGen)
389 info.RecommendedPStateMode = GetRecommendedPStateMode(zenGen)
390 }
391 }
392
393 return info, nil
394}