feat(amd): add comprehensive AMD CPU detection and P-State configuration
- Add Zen generation detection (Zen 1-5) based on CPU family/model - Implement AMD P-State mode detection and recommendations - Add interactive P-State selection form with descriptions - Support active, guided, and passive modes based on CPU generation - Generate proper kernel parameters including Zen 2 shared_mem workaround - Add new StateAMDPStatePrompt UI state between options and confirmation - Show AMD P-State configuration in confirmation screen - Update boot phase to use Zen-aware kernel parameter generation Matches functionality from legacy shell script install/base/amd-cpu.sh
- commit
- ea9cd09fcba5a662251081369ec1ecb5a48a2609
- author
- bnema · · 2025-10-26 21:32 UTC
- parent
- 2b21852
- signature
- unsigned
4 files · +290 -11
M internal/phases/boot.go +15 -3
| @@ -195,9 +195,21 @@ func (p *BootPhase) configureLimine(progressChan chan<- ProgressUpdate) error { | |||
| 195 | 195 | // Add AMD-specific kernel parameters if configured | |
| 196 | 196 | switch { | |
| 197 | 197 | case p.config.AMDPState != "": | |
| 198 | - | amdParams := fmt.Sprintf("amd_pstate=%s", p.config.AMDPState) | |
| 199 | - | kernelParams = fmt.Sprintf("%s %s", kernelParams, amdParams) | |
| 200 | - | p.SendOutput(progressChan, fmt.Sprintf("Added AMD P-State: %s", p.config.AMDPState)) | |
| 198 | + | // Detect CPU to get Zen generation for proper kernel params | |
| 199 | + | cpuInfo, err := system.DetectCPUInfo() | |
| 200 | + | var amdParams string | |
| 201 | + | if err == nil && cpuInfo.AMDZenGen != nil { | |
| 202 | + | // Use helper function that adds extra params for Zen 2 if needed | |
| 203 | + | amdParams = system.GetAMDPStateKernelParams(cpuInfo.AMDZenGen, system.AMDPStateMode(p.config.AMDPState)) | |
| 204 | + | } else { | |
| 205 | + | // Fallback to simple parameter if detection fails | |
| 206 | + | amdParams = fmt.Sprintf("amd_pstate=%s", p.config.AMDPState) | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | if amdParams != "" { | |
| 210 | + | kernelParams = fmt.Sprintf("%s %s", kernelParams, amdParams) | |
| 211 | + | p.SendOutput(progressChan, fmt.Sprintf("Added AMD P-State: %s", amdParams)) | |
| 212 | + | } | |
| 201 | 213 | } | |
| 202 | 214 | ||
| 203 | 215 | // Add quiet/splash parameters | |
| @@ -195,9 +195,21 @@ func (p *BootPhase) configureLimine(progressChan chan<- ProgressUpdate) error { | |||||
| 195 | // Add AMD-specific kernel parameters if configured | 195 | // Add AMD-specific kernel parameters if configured | ||
| 196 | switch { | 196 | switch { | ||
| 197 | case p.config.AMDPState != "": | 197 | case p.config.AMDPState != "": | ||
| 198 | - | amdParams := fmt.Sprintf("amd_pstate=%s", p.config.AMDPState) | 198 | + | // Detect CPU to get Zen generation for proper kernel params |
| 199 | - | kernelParams = fmt.Sprintf("%s %s", kernelParams, amdParams) | 199 | + | cpuInfo, err := system.DetectCPUInfo() |
| 200 | - | p.SendOutput(progressChan, fmt.Sprintf("Added AMD P-State: %s", p.config.AMDPState)) | 200 | + | var amdParams string |
| 201 | + | if err == nil && cpuInfo.AMDZenGen != nil { | |||
| 202 | + | // Use helper function that adds extra params for Zen 2 if needed | |||
| 203 | + | amdParams = system.GetAMDPStateKernelParams(cpuInfo.AMDZenGen, system.AMDPStateMode(p.config.AMDPState)) | |||
| 204 | + | } else { | |||
| 205 | + | // Fallback to simple parameter if detection fails | |||
| 206 | + | amdParams = fmt.Sprintf("amd_pstate=%s", p.config.AMDPState) | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | if amdParams != "" { | |||
| 210 | + | kernelParams = fmt.Sprintf("%s %s", kernelParams, amdParams) | |||
| 211 | + | p.SendOutput(progressChan, fmt.Sprintf("Added AMD P-State: %s", amdParams)) | |||
| 212 | + | } | |||
| 201 | } | 213 | } | ||
| 202 | 214 | ||||
| 203 | // Add quiet/splash parameters | 215 | // Add quiet/splash parameters | ||
M internal/system/cpu.go +179 -2
| @@ -3,6 +3,7 @@ package system | |||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | 5 | "os" | |
| 6 | + | "strconv" | |
| 6 | 7 | "strings" | |
| 7 | 8 | ) | |
| 8 | 9 | ||
| @@ -22,13 +23,23 @@ const ( | |||
| 22 | 23 | AMDPStateModeActive AMDPStateMode = "active" | |
| 23 | 24 | AMDPStateModeGuided AMDPStateMode = "guided" | |
| 24 | 25 | AMDPStateModePassive AMDPStateMode = "passive" | |
| 26 | + | AMDPStateModeNone AMDPStateMode = "" | |
| 25 | 27 | ) | |
| 26 | 28 | ||
| 29 | + | // AMDZenGen represents AMD Zen generation | |
| 30 | + | type AMDZenGen struct { | |
| 31 | + | Generation string // "1", "1+", "2", "3", "3+", "4", "5", "unknown" | |
| 32 | + | Label string // "Zen 1", "Zen+", "Zen 2", etc. | |
| 33 | + | } | |
| 34 | + | ||
| 27 | 35 | // CPUInfo holds detected CPU information | |
| 28 | 36 | type CPUInfo struct { | |
| 29 | 37 | Vendor CPUVendor | |
| 38 | + | ModelName string // Full CPU model name | |
| 30 | 39 | Microcode string | |
| 40 | + | AMDZenGen *AMDZenGen // AMD Zen generation (nil for non-AMD) | |
| 31 | 41 | AMDPStateModes []AMDPStateMode | |
| 42 | + | RecommendedPStateMode AMDPStateMode // Recommended mode for this CPU | |
| 32 | 43 | } | |
| 33 | 44 | ||
| 34 | 45 | // DetectCPUVendor reads /proc/cpuinfo and returns the CPU vendor | |
| @@ -161,6 +172,166 @@ func GetAMDPStateModeDescription(mode AMDPStateMode) string { | |||
| 161 | 172 | } | |
| 162 | 173 | } | |
| 163 | 174 | ||
| 175 | + | // DetectAMDZenGeneration detects AMD Zen generation based on CPU family and model | |
| 176 | + | // Reference: https://en.wikichip.org/wiki/amd/cpuid | |
| 177 | + | func DetectAMDZenGeneration() (*AMDZenGen, error) { | |
| 178 | + | data, err := os.ReadFile("/proc/cpuinfo") | |
| 179 | + | if err != nil { | |
| 180 | + | return nil, fmt.Errorf("failed to read /proc/cpuinfo: %w", err) | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | var cpuFamily, cpuModel int | |
| 184 | + | lines := strings.Split(string(data), "\n") | |
| 185 | + | ||
| 186 | + | for _, line := range lines { | |
| 187 | + | if strings.HasPrefix(line, "cpu family") { | |
| 188 | + | parts := strings.Split(line, ":") | |
| 189 | + | if len(parts) >= 2 { | |
| 190 | + | val := strings.TrimSpace(parts[1]) | |
| 191 | + | cpuFamily, _ = strconv.Atoi(val) | |
| 192 | + | } | |
| 193 | + | } | |
| 194 | + | if strings.HasPrefix(line, "model") && !strings.HasPrefix(line, "model name") { | |
| 195 | + | parts := strings.Split(line, ":") | |
| 196 | + | if len(parts) >= 2 { | |
| 197 | + | val := strings.TrimSpace(parts[1]) | |
| 198 | + | cpuModel, _ = strconv.Atoi(val) | |
| 199 | + | } | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | // Break early if we have both values | |
| 203 | + | if cpuFamily > 0 && cpuModel >= 0 { | |
| 204 | + | break | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | zenGen := &AMDZenGen{ | |
| 209 | + | Generation: "unknown", | |
| 210 | + | Label: "Unknown", | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | // Detect Zen generation based on CPU family and model | |
| 214 | + | switch cpuFamily { | |
| 215 | + | case 23: // Family 17h | |
| 216 | + | if cpuModel >= 0 && cpuModel <= 15 { | |
| 217 | + | zenGen.Generation = "1" | |
| 218 | + | zenGen.Label = "Zen 1" | |
| 219 | + | } else if cpuModel >= 16 && cpuModel <= 31 { | |
| 220 | + | zenGen.Generation = "1+" | |
| 221 | + | zenGen.Label = "Zen+" | |
| 222 | + | } else if cpuModel >= 48 && cpuModel <= 127 { | |
| 223 | + | zenGen.Generation = "2" | |
| 224 | + | zenGen.Label = "Zen 2" | |
| 225 | + | } else { | |
| 226 | + | zenGen.Generation = "2" | |
| 227 | + | zenGen.Label = "Zen 2 (assumed)" | |
| 228 | + | } | |
| 229 | + | case 25: // Family 19h | |
| 230 | + | if cpuModel >= 0 && cpuModel <= 15 { | |
| 231 | + | zenGen.Generation = "3" | |
| 232 | + | zenGen.Label = "Zen 3" | |
| 233 | + | } else if cpuModel >= 32 && cpuModel <= 47 { | |
| 234 | + | zenGen.Generation = "3" | |
| 235 | + | zenGen.Label = "Zen 3" | |
| 236 | + | } else if cpuModel >= 80 && cpuModel <= 95 { | |
| 237 | + | zenGen.Generation = "3+" | |
| 238 | + | zenGen.Label = "Zen 3+" | |
| 239 | + | } else if cpuModel >= 96 && cpuModel <= 175 { | |
| 240 | + | zenGen.Generation = "4" | |
| 241 | + | zenGen.Label = "Zen 4" | |
| 242 | + | } else { | |
| 243 | + | zenGen.Generation = "3" | |
| 244 | + | zenGen.Label = "Zen 3 (assumed)" | |
| 245 | + | } | |
| 246 | + | case 26: // Family 1Ah | |
| 247 | + | zenGen.Generation = "5" | |
| 248 | + | zenGen.Label = "Zen 5" | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | return zenGen, nil | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | // GetRecommendedPStateMode returns the best recommended mode for each Zen generation | |
| 255 | + | func GetRecommendedPStateMode(zenGen *AMDZenGen) AMDPStateMode { | |
| 256 | + | switch zenGen.Generation { | |
| 257 | + | case "1", "1+": | |
| 258 | + | return AMDPStateModeNone // Use default acpi-cpufreq | |
| 259 | + | case "2": | |
| 260 | + | return AMDPStateModePassive // Conservative choice for Zen 2 | |
| 261 | + | case "3", "3+": | |
| 262 | + | return AMDPStateModeActive // Best for Zen 3/3+ | |
| 263 | + | case "4", "5": | |
| 264 | + | return AMDPStateModeActive // Best for Zen 4/5 | |
| 265 | + | default: | |
| 266 | + | return AMDPStateModePassive | |
| 267 | + | } | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | // GetAvailablePStateModes returns available P-State modes for a given Zen generation | |
| 271 | + | func GetAvailablePStateModes(zenGen *AMDZenGen) []AMDPStateMode { | |
| 272 | + | switch zenGen.Generation { | |
| 273 | + | case "1", "1+": | |
| 274 | + | // Zen 1 and Zen+: No CPPC support, use acpi-cpufreq | |
| 275 | + | return []AMDPStateMode{} | |
| 276 | + | case "2": | |
| 277 | + | // Zen 2: Basic CPPC support, passive and active modes | |
| 278 | + | return []AMDPStateMode{AMDPStateModePassive, AMDPStateModeActive} | |
| 279 | + | case "3", "3+", "4", "5": | |
| 280 | + | // Zen 3+: Full CPPC support for all modes | |
| 281 | + | return []AMDPStateMode{AMDPStateModeActive, AMDPStateModeGuided, AMDPStateModePassive} | |
| 282 | + | default: | |
| 283 | + | return []AMDPStateMode{AMDPStateModePassive} | |
| 284 | + | } | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | // GetPStateModeDescription returns human-readable description for P-State mode | |
| 288 | + | func GetPStateModeDescription(mode AMDPStateMode) string { | |
| 289 | + | switch mode { | |
| 290 | + | case AMDPStateModeActive: | |
| 291 | + | return "CPPC autonomous with EPP hints (best performance, desktop/gaming)" | |
| 292 | + | case AMDPStateModeGuided: | |
| 293 | + | return "CPPC guided autonomous (balanced, laptop/hybrid)" | |
| 294 | + | case AMDPStateModePassive: | |
| 295 | + | return "CPPC non-autonomous (compatibility, manual governor control)" | |
| 296 | + | default: | |
| 297 | + | return string(mode) | |
| 298 | + | } | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | // GetAMDPStateKernelParams returns kernel parameters needed for AMD P-State mode | |
| 302 | + | func GetAMDPStateKernelParams(zenGen *AMDZenGen, mode AMDPStateMode) string { | |
| 303 | + | if mode == AMDPStateModeNone || mode == "" { | |
| 304 | + | return "" | |
| 305 | + | } | |
| 306 | + | ||
| 307 | + | // Some Zen 2 CPUs need shared_mem parameter for active mode | |
| 308 | + | if zenGen.Generation == "2" && mode == AMDPStateModeActive { | |
| 309 | + | return fmt.Sprintf("amd_pstate=%s amd_pstate.shared_mem=1", mode) | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | return fmt.Sprintf("amd_pstate=%s", mode) | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | // GetCPUModelName extracts the CPU model name from /proc/cpuinfo | |
| 316 | + | func GetCPUModelName() string { | |
| 317 | + | data, err := os.ReadFile("/proc/cpuinfo") | |
| 318 | + | if err != nil { | |
| 319 | + | return "" | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | lines := strings.Split(string(data), "\n") | |
| 323 | + | for _, line := range lines { | |
| 324 | + | if strings.HasPrefix(line, "model name") { | |
| 325 | + | parts := strings.Split(line, ":") | |
| 326 | + | if len(parts) >= 2 { | |
| 327 | + | return strings.TrimSpace(parts[1]) | |
| 328 | + | } | |
| 329 | + | } | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | return "" | |
| 333 | + | } | |
| 334 | + | ||
| 164 | 335 | // DetectCPUInfo detects all CPU information in one call (DRY) | |
| 165 | 336 | func DetectCPUInfo() (*CPUInfo, error) { | |
| 166 | 337 | vendor, err := DetectCPUVendor() | |
| @@ -171,13 +342,19 @@ func DetectCPUInfo() (*CPUInfo, error) { | |||
| 171 | 342 | ||
| 172 | 343 | info := &CPUInfo{ | |
| 173 | 344 | Vendor: vendor, | |
| 345 | + | ModelName: GetCPUModelName(), | |
| 174 | 346 | Microcode: DetectMicrocode(vendor), | |
| 175 | 347 | } | |
| 176 | 348 | ||
| 177 | - | // Detect AMD P-State modes only for AMD CPUs | |
| 349 | + | // Detect AMD-specific features only for AMD CPUs | |
| 178 | 350 | switch vendor { | |
| 179 | 351 | case CPUVendorAMD: | |
| 180 | - | info.AMDPStateModes = DetectAMDPStateModes() | |
| 352 | + | zenGen, err := DetectAMDZenGeneration() | |
| 353 | + | if err == nil { | |
| 354 | + | info.AMDZenGen = zenGen | |
| 355 | + | info.AMDPStateModes = GetAvailablePStateModes(zenGen) | |
| 356 | + | info.RecommendedPStateMode = GetRecommendedPStateMode(zenGen) | |
| 357 | + | } | |
| 181 | 358 | } | |
| 182 | 359 | ||
| 183 | 360 | return info, nil | |
| @@ -3,6 +3,7 @@ package system | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | "os" | 5 | "os" | ||
| 6 | + | "strconv" | |||
| 6 | "strings" | 7 | "strings" | ||
| 7 | ) | 8 | ) | ||
| 8 | 9 | ||||
| @@ -22,13 +23,23 @@ const ( | |||||
| 22 | AMDPStateModeActive AMDPStateMode = "active" | 23 | AMDPStateModeActive AMDPStateMode = "active" | ||
| 23 | AMDPStateModeGuided AMDPStateMode = "guided" | 24 | AMDPStateModeGuided AMDPStateMode = "guided" | ||
| 24 | AMDPStateModePassive AMDPStateMode = "passive" | 25 | AMDPStateModePassive AMDPStateMode = "passive" | ||
| 26 | + | AMDPStateModeNone AMDPStateMode = "" | |||
| 25 | ) | 27 | ) | ||
| 26 | 28 | ||||
| 29 | + | // AMDZenGen represents AMD Zen generation | |||
| 30 | + | type AMDZenGen struct { | |||
| 31 | + | Generation string // "1", "1+", "2", "3", "3+", "4", "5", "unknown" | |||
| 32 | + | Label string // "Zen 1", "Zen+", "Zen 2", etc. | |||
| 33 | + | } | |||
| 34 | + | ||||
| 27 | // CPUInfo holds detected CPU information | 35 | // CPUInfo holds detected CPU information | ||
| 28 | type CPUInfo struct { | 36 | type CPUInfo struct { | ||
| 29 | Vendor CPUVendor | 37 | Vendor CPUVendor | ||
| 38 | + | ModelName string // Full CPU model name | |||
| 30 | Microcode string | 39 | Microcode string | ||
| 40 | + | AMDZenGen *AMDZenGen // AMD Zen generation (nil for non-AMD) | |||
| 31 | AMDPStateModes []AMDPStateMode | 41 | AMDPStateModes []AMDPStateMode | ||
| 42 | + | RecommendedPStateMode AMDPStateMode // Recommended mode for this CPU | |||
| 32 | } | 43 | } | ||
| 33 | 44 | ||||
| 34 | // DetectCPUVendor reads /proc/cpuinfo and returns the CPU vendor | 45 | // DetectCPUVendor reads /proc/cpuinfo and returns the CPU vendor | ||
| @@ -161,6 +172,166 @@ func GetAMDPStateModeDescription(mode AMDPStateMode) string { | |||||
| 161 | } | 172 | } | ||
| 162 | } | 173 | } | ||
| 163 | 174 | ||||
| 175 | + | // DetectAMDZenGeneration detects AMD Zen generation based on CPU family and model | |||
| 176 | + | // Reference: https://en.wikichip.org/wiki/amd/cpuid | |||
| 177 | + | func DetectAMDZenGeneration() (*AMDZenGen, error) { | |||
| 178 | + | data, err := os.ReadFile("/proc/cpuinfo") | |||
| 179 | + | if err != nil { | |||
| 180 | + | return nil, fmt.Errorf("failed to read /proc/cpuinfo: %w", err) | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | var cpuFamily, cpuModel int | |||
| 184 | + | lines := strings.Split(string(data), "\n") | |||
| 185 | + | ||||
| 186 | + | for _, line := range lines { | |||
| 187 | + | if strings.HasPrefix(line, "cpu family") { | |||
| 188 | + | parts := strings.Split(line, ":") | |||
| 189 | + | if len(parts) >= 2 { | |||
| 190 | + | val := strings.TrimSpace(parts[1]) | |||
| 191 | + | cpuFamily, _ = strconv.Atoi(val) | |||
| 192 | + | } | |||
| 193 | + | } | |||
| 194 | + | if strings.HasPrefix(line, "model") && !strings.HasPrefix(line, "model name") { | |||
| 195 | + | parts := strings.Split(line, ":") | |||
| 196 | + | if len(parts) >= 2 { | |||
| 197 | + | val := strings.TrimSpace(parts[1]) | |||
| 198 | + | cpuModel, _ = strconv.Atoi(val) | |||
| 199 | + | } | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | // Break early if we have both values | |||
| 203 | + | if cpuFamily > 0 && cpuModel >= 0 { | |||
| 204 | + | break | |||
| 205 | + | } | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | zenGen := &AMDZenGen{ | |||
| 209 | + | Generation: "unknown", | |||
| 210 | + | Label: "Unknown", | |||
| 211 | + | } | |||
| 212 | + | ||||
| 213 | + | // Detect Zen generation based on CPU family and model | |||
| 214 | + | switch cpuFamily { | |||
| 215 | + | case 23: // Family 17h | |||
| 216 | + | if cpuModel >= 0 && cpuModel <= 15 { | |||
| 217 | + | zenGen.Generation = "1" | |||
| 218 | + | zenGen.Label = "Zen 1" | |||
| 219 | + | } else if cpuModel >= 16 && cpuModel <= 31 { | |||
| 220 | + | zenGen.Generation = "1+" | |||
| 221 | + | zenGen.Label = "Zen+" | |||
| 222 | + | } else if cpuModel >= 48 && cpuModel <= 127 { | |||
| 223 | + | zenGen.Generation = "2" | |||
| 224 | + | zenGen.Label = "Zen 2" | |||
| 225 | + | } else { | |||
| 226 | + | zenGen.Generation = "2" | |||
| 227 | + | zenGen.Label = "Zen 2 (assumed)" | |||
| 228 | + | } | |||
| 229 | + | case 25: // Family 19h | |||
| 230 | + | if cpuModel >= 0 && cpuModel <= 15 { | |||
| 231 | + | zenGen.Generation = "3" | |||
| 232 | + | zenGen.Label = "Zen 3" | |||
| 233 | + | } else if cpuModel >= 32 && cpuModel <= 47 { | |||
| 234 | + | zenGen.Generation = "3" | |||
| 235 | + | zenGen.Label = "Zen 3" | |||
| 236 | + | } else if cpuModel >= 80 && cpuModel <= 95 { | |||
| 237 | + | zenGen.Generation = "3+" | |||
| 238 | + | zenGen.Label = "Zen 3+" | |||
| 239 | + | } else if cpuModel >= 96 && cpuModel <= 175 { | |||
| 240 | + | zenGen.Generation = "4" | |||
| 241 | + | zenGen.Label = "Zen 4" | |||
| 242 | + | } else { | |||
| 243 | + | zenGen.Generation = "3" | |||
| 244 | + | zenGen.Label = "Zen 3 (assumed)" | |||
| 245 | + | } | |||
| 246 | + | case 26: // Family 1Ah | |||
| 247 | + | zenGen.Generation = "5" | |||
| 248 | + | zenGen.Label = "Zen 5" | |||
| 249 | + | } | |||
| 250 | + | ||||
| 251 | + | return zenGen, nil | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | // GetRecommendedPStateMode returns the best recommended mode for each Zen generation | |||
| 255 | + | func GetRecommendedPStateMode(zenGen *AMDZenGen) AMDPStateMode { | |||
| 256 | + | switch zenGen.Generation { | |||
| 257 | + | case "1", "1+": | |||
| 258 | + | return AMDPStateModeNone // Use default acpi-cpufreq | |||
| 259 | + | case "2": | |||
| 260 | + | return AMDPStateModePassive // Conservative choice for Zen 2 | |||
| 261 | + | case "3", "3+": | |||
| 262 | + | return AMDPStateModeActive // Best for Zen 3/3+ | |||
| 263 | + | case "4", "5": | |||
| 264 | + | return AMDPStateModeActive // Best for Zen 4/5 | |||
| 265 | + | default: | |||
| 266 | + | return AMDPStateModePassive | |||
| 267 | + | } | |||
| 268 | + | } | |||
| 269 | + | ||||
| 270 | + | // GetAvailablePStateModes returns available P-State modes for a given Zen generation | |||
| 271 | + | func GetAvailablePStateModes(zenGen *AMDZenGen) []AMDPStateMode { | |||
| 272 | + | switch zenGen.Generation { | |||
| 273 | + | case "1", "1+": | |||
| 274 | + | // Zen 1 and Zen+: No CPPC support, use acpi-cpufreq | |||
| 275 | + | return []AMDPStateMode{} | |||
| 276 | + | case "2": | |||
| 277 | + | // Zen 2: Basic CPPC support, passive and active modes | |||
| 278 | + | return []AMDPStateMode{AMDPStateModePassive, AMDPStateModeActive} | |||
| 279 | + | case "3", "3+", "4", "5": | |||
| 280 | + | // Zen 3+: Full CPPC support for all modes | |||
| 281 | + | return []AMDPStateMode{AMDPStateModeActive, AMDPStateModeGuided, AMDPStateModePassive} | |||
| 282 | + | default: | |||
| 283 | + | return []AMDPStateMode{AMDPStateModePassive} | |||
| 284 | + | } | |||
| 285 | + | } | |||
| 286 | + | ||||
| 287 | + | // GetPStateModeDescription returns human-readable description for P-State mode | |||
| 288 | + | func GetPStateModeDescription(mode AMDPStateMode) string { | |||
| 289 | + | switch mode { | |||
| 290 | + | case AMDPStateModeActive: | |||
| 291 | + | return "CPPC autonomous with EPP hints (best performance, desktop/gaming)" | |||
| 292 | + | case AMDPStateModeGuided: | |||
| 293 | + | return "CPPC guided autonomous (balanced, laptop/hybrid)" | |||
| 294 | + | case AMDPStateModePassive: | |||
| 295 | + | return "CPPC non-autonomous (compatibility, manual governor control)" | |||
| 296 | + | default: | |||
| 297 | + | return string(mode) | |||
| 298 | + | } | |||
| 299 | + | } | |||
| 300 | + | ||||
| 301 | + | // GetAMDPStateKernelParams returns kernel parameters needed for AMD P-State mode | |||
| 302 | + | func GetAMDPStateKernelParams(zenGen *AMDZenGen, mode AMDPStateMode) string { | |||
| 303 | + | if mode == AMDPStateModeNone || mode == "" { | |||
| 304 | + | return "" | |||
| 305 | + | } | |||
| 306 | + | ||||
| 307 | + | // Some Zen 2 CPUs need shared_mem parameter for active mode | |||
| 308 | + | if zenGen.Generation == "2" && mode == AMDPStateModeActive { | |||
| 309 | + | return fmt.Sprintf("amd_pstate=%s amd_pstate.shared_mem=1", mode) | |||
| 310 | + | } | |||
| 311 | + | ||||
| 312 | + | return fmt.Sprintf("amd_pstate=%s", mode) | |||
| 313 | + | } | |||
| 314 | + | ||||
| 315 | + | // GetCPUModelName extracts the CPU model name from /proc/cpuinfo | |||
| 316 | + | func GetCPUModelName() string { | |||
| 317 | + | data, err := os.ReadFile("/proc/cpuinfo") | |||
| 318 | + | if err != nil { | |||
| 319 | + | return "" | |||
| 320 | + | } | |||
| 321 | + | ||||
| 322 | + | lines := strings.Split(string(data), "\n") | |||
| 323 | + | for _, line := range lines { | |||
| 324 | + | if strings.HasPrefix(line, "model name") { | |||
| 325 | + | parts := strings.Split(line, ":") | |||
| 326 | + | if len(parts) >= 2 { | |||
| 327 | + | return strings.TrimSpace(parts[1]) | |||
| 328 | + | } | |||
| 329 | + | } | |||
| 330 | + | } | |||
| 331 | + | ||||
| 332 | + | return "" | |||
| 333 | + | } | |||
| 334 | + | ||||
| 164 | // DetectCPUInfo detects all CPU information in one call (DRY) | 335 | // DetectCPUInfo detects all CPU information in one call (DRY) | ||
| 165 | func DetectCPUInfo() (*CPUInfo, error) { | 336 | func DetectCPUInfo() (*CPUInfo, error) { | ||
| 166 | vendor, err := DetectCPUVendor() | 337 | vendor, err := DetectCPUVendor() | ||
| @@ -171,13 +342,19 @@ func DetectCPUInfo() (*CPUInfo, error) { | |||||
| 171 | 342 | ||||
| 172 | info := &CPUInfo{ | 343 | info := &CPUInfo{ | ||
| 173 | Vendor: vendor, | 344 | Vendor: vendor, | ||
| 345 | + | ModelName: GetCPUModelName(), | |||
| 174 | Microcode: DetectMicrocode(vendor), | 346 | Microcode: DetectMicrocode(vendor), | ||
| 175 | } | 347 | } | ||
| 176 | 348 | ||||
| 177 | - | // Detect AMD P-State modes only for AMD CPUs | 349 | + | // Detect AMD-specific features only for AMD CPUs |
| 178 | switch vendor { | 350 | switch vendor { | ||
| 179 | case CPUVendorAMD: | 351 | case CPUVendorAMD: | ||
| 180 | - | info.AMDPStateModes = DetectAMDPStateModes() | 352 | + | zenGen, err := DetectAMDZenGeneration() |
| 353 | + | if err == nil { | |||
| 354 | + | info.AMDZenGen = zenGen | |||
| 355 | + | info.AMDPStateModes = GetAvailablePStateModes(zenGen) | |||
| 356 | + | info.RecommendedPStateMode = GetRecommendedPStateMode(zenGen) | |||
| 357 | + | } | |||
| 181 | } | 358 | } | ||
| 182 | 359 | ||||
| 183 | return info, nil | 360 | return info, nil | ||
M internal/ui/installer_forms.go +41 -0
| @@ -22,6 +22,12 @@ func CreatePreflightForm(cfg *config.Config, fb *components.FormBuilder) *huh.Fo | |||
| 22 | 22 | cfg.Timezone = config.TimezoneDefault | |
| 23 | 23 | } | |
| 24 | 24 | ||
| 25 | + | // Set default keymap if not already set | |
| 26 | + | switch { | |
| 27 | + | case cfg.Keymap == "": | |
| 28 | + | cfg.Keymap = config.KeymapDefault | |
| 29 | + | } | |
| 30 | + | ||
| 25 | 31 | return fb.CreateForm( | |
| 26 | 32 | // System configuration | |
| 27 | 33 | huh.NewGroup( | |
| @@ -181,6 +187,41 @@ func CreateOptionsForm(cfg *config.Config, fb *components.FormBuilder) *huh.Form | |||
| 181 | 187 | return fb.CreateForm(groups...) | |
| 182 | 188 | } | |
| 183 | 189 | ||
| 190 | + | // CreateAMDPStateForm creates AMD P-State driver selection form | |
| 191 | + | // This form is only shown for AMD CPUs that support P-State | |
| 192 | + | func CreateAMDPStateForm(cfg *config.Config, cpuInfo *system.CPUInfo, fb *components.FormBuilder) *huh.Form { | |
| 193 | + | // Build P-State mode options based on available modes | |
| 194 | + | var modeOptions []huh.Option[string] | |
| 195 | + | ||
| 196 | + | for _, mode := range cpuInfo.AMDPStateModes { | |
| 197 | + | desc := system.GetPStateModeDescription(mode) | |
| 198 | + | label := fmt.Sprintf("%s - %s", mode, desc) | |
| 199 | + | ||
| 200 | + | // Mark recommended mode | |
| 201 | + | if mode == cpuInfo.RecommendedPStateMode { | |
| 202 | + | label += " (recommended)" | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | modeOptions = append(modeOptions, huh.NewOption(label, string(mode))) | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | // Build description text | |
| 209 | + | zenLabel := "Unknown" | |
| 210 | + | if cpuInfo.AMDZenGen != nil { | |
| 211 | + | zenLabel = cpuInfo.AMDZenGen.Label | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | description := fmt.Sprintf("Detected: %s\nCPU: %s\n\nNote: Requires CPPC enabled in UEFI (AMD CBS > NBIO > SMU > CPPC)", | |
| 215 | + | zenLabel, cpuInfo.ModelName) | |
| 216 | + | ||
| 217 | + | return fb.CreateForm( | |
| 218 | + | huh.NewGroup( | |
| 219 | + | fb.SelectWithOptions("AMD P-State Mode", modeOptions, &cfg.AMDPState). | |
| 220 | + | Description(description), | |
| 221 | + | ).Title("AMD P-State Configuration"), | |
| 222 | + | ) | |
| 223 | + | } | |
| 224 | + | ||
| 184 | 225 | // FormatDiskOption formats a disk into a user-friendly option string (DRY helper) | |
| 185 | 226 | func FormatDiskOption(disk system.Disk) string { | |
| 186 | 227 | label := fmt.Sprintf("%s (%s)", disk.Path, disk.Size) | |
| @@ -22,6 +22,12 @@ func CreatePreflightForm(cfg *config.Config, fb *components.FormBuilder) *huh.Fo | |||||
| 22 | cfg.Timezone = config.TimezoneDefault | 22 | cfg.Timezone = config.TimezoneDefault | ||
| 23 | } | 23 | } | ||
| 24 | 24 | ||||
| 25 | + | // Set default keymap if not already set | |||
| 26 | + | switch { | |||
| 27 | + | case cfg.Keymap == "": | |||
| 28 | + | cfg.Keymap = config.KeymapDefault | |||
| 29 | + | } | |||
| 30 | + | ||||
| 25 | return fb.CreateForm( | 31 | return fb.CreateForm( | ||
| 26 | // System configuration | 32 | // System configuration | ||
| 27 | huh.NewGroup( | 33 | huh.NewGroup( | ||
| @@ -181,6 +187,41 @@ func CreateOptionsForm(cfg *config.Config, fb *components.FormBuilder) *huh.Form | |||||
| 181 | return fb.CreateForm(groups...) | 187 | return fb.CreateForm(groups...) | ||
| 182 | } | 188 | } | ||
| 183 | 189 | ||||
| 190 | + | // CreateAMDPStateForm creates AMD P-State driver selection form | |||
| 191 | + | // This form is only shown for AMD CPUs that support P-State | |||
| 192 | + | func CreateAMDPStateForm(cfg *config.Config, cpuInfo *system.CPUInfo, fb *components.FormBuilder) *huh.Form { | |||
| 193 | + | // Build P-State mode options based on available modes | |||
| 194 | + | var modeOptions []huh.Option[string] | |||
| 195 | + | ||||
| 196 | + | for _, mode := range cpuInfo.AMDPStateModes { | |||
| 197 | + | desc := system.GetPStateModeDescription(mode) | |||
| 198 | + | label := fmt.Sprintf("%s - %s", mode, desc) | |||
| 199 | + | ||||
| 200 | + | // Mark recommended mode | |||
| 201 | + | if mode == cpuInfo.RecommendedPStateMode { | |||
| 202 | + | label += " (recommended)" | |||
| 203 | + | } | |||
| 204 | + | ||||
| 205 | + | modeOptions = append(modeOptions, huh.NewOption(label, string(mode))) | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | // Build description text | |||
| 209 | + | zenLabel := "Unknown" | |||
| 210 | + | if cpuInfo.AMDZenGen != nil { | |||
| 211 | + | zenLabel = cpuInfo.AMDZenGen.Label | |||
| 212 | + | } | |||
| 213 | + | ||||
| 214 | + | description := fmt.Sprintf("Detected: %s\nCPU: %s\n\nNote: Requires CPPC enabled in UEFI (AMD CBS > NBIO > SMU > CPPC)", | |||
| 215 | + | zenLabel, cpuInfo.ModelName) | |||
| 216 | + | ||||
| 217 | + | return fb.CreateForm( | |||
| 218 | + | huh.NewGroup( | |||
| 219 | + | fb.SelectWithOptions("AMD P-State Mode", modeOptions, &cfg.AMDPState). | |||
| 220 | + | Description(description), | |||
| 221 | + | ).Title("AMD P-State Configuration"), | |||
| 222 | + | ) | |||
| 223 | + | } | |||
| 224 | + | ||||
| 184 | // FormatDiskOption formats a disk into a user-friendly option string (DRY helper) | 225 | // FormatDiskOption formats a disk into a user-friendly option string (DRY helper) | ||
| 185 | func FormatDiskOption(disk system.Disk) string { | 226 | func FormatDiskOption(disk system.Disk) string { | ||
| 186 | label := fmt.Sprintf("%s (%s)", disk.Path, disk.Size) | 227 | label := fmt.Sprintf("%s (%s)", disk.Path, disk.Size) | ||
M internal/ui/model.go +55 -6
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "github.com/bnema/archup/internal/config" | |
| 7 | 7 | "github.com/bnema/archup/internal/logger" | |
| 8 | 8 | "github.com/bnema/archup/internal/phases" | |
| 9 | + | "github.com/bnema/archup/internal/system" | |
| 9 | 10 | "github.com/bnema/archup/internal/ui/components" | |
| 10 | 11 | "github.com/bnema/archup/internal/ui/styles" | |
| 11 | 12 | "github.com/charmbracelet/bubbles/spinner" | |
| @@ -22,6 +23,7 @@ const ( | |||
| 22 | 23 | StatePreflightForm | |
| 23 | 24 | StateDiskForm | |
| 24 | 25 | StateOptionsForm | |
| 26 | + | StateAMDPStatePrompt // NEW: AMD P-State selection (shown only for AMD CPUs) | |
| 25 | 27 | StateConfirmation | |
| 26 | 28 | StateExecuting | |
| 27 | 29 | StateComplete | |
| @@ -42,6 +44,7 @@ type Model struct { | |||
| 42 | 44 | width int | |
| 43 | 45 | height int | |
| 44 | 46 | err error | |
| 47 | + | cpuInfo *system.CPUInfo // Detected CPU information | |
| 45 | 48 | } | |
| 46 | 49 | ||
| 47 | 50 | // NewModel creates a new installer model | |
| @@ -116,10 +119,21 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 116 | 119 | m.currentForm = CreateDiskSelectionForm(m.config, m.formBuilder) | |
| 117 | 120 | return m, m.currentForm.Init() | |
| 118 | 121 | ||
| 119 | - | case StateConfirmation: | |
| 122 | + | case StateAMDPStatePrompt: | |
| 120 | 123 | m.state = StateOptionsForm | |
| 121 | 124 | m.currentForm = CreateOptionsForm(m.config, m.formBuilder) | |
| 122 | 125 | return m, m.currentForm.Init() | |
| 126 | + | ||
| 127 | + | case StateConfirmation: | |
| 128 | + | // Go back to AMD P-State prompt if we have AMD CPU, otherwise options | |
| 129 | + | if m.cpuInfo != nil && m.cpuInfo.Vendor == system.CPUVendorAMD && len(m.cpuInfo.AMDPStateModes) > 0 { | |
| 130 | + | m.state = StateAMDPStatePrompt | |
| 131 | + | m.currentForm = CreateAMDPStateForm(m.config, m.cpuInfo, m.formBuilder) | |
| 132 | + | } else { | |
| 133 | + | m.state = StateOptionsForm | |
| 134 | + | m.currentForm = CreateOptionsForm(m.config, m.formBuilder) | |
| 135 | + | } | |
| 136 | + | return m, m.currentForm.Init() | |
| 123 | 137 | } | |
| 124 | 138 | } | |
| 125 | 139 | ||
| @@ -161,7 +175,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 161 | 175 | ||
| 162 | 176 | // Update current form if showing one | |
| 163 | 177 | switch m.state { | |
| 164 | - | case StatePreflightForm, StateDiskForm, StateOptionsForm: | |
| 178 | + | case StatePreflightForm, StateDiskForm, StateOptionsForm, StateAMDPStatePrompt: | |
| 165 | 179 | switch { | |
| 166 | 180 | case m.currentForm != nil: | |
| 167 | 181 | // Don't pass WindowSizeMsg to form to preserve our max width | |
| @@ -200,7 +214,7 @@ func (m *Model) View() string { | |||
| 200 | 214 | centerText(WelcomeTagline, m.width) + "\n\n" + | |
| 201 | 215 | centerText(styles.HelpStyle.Render("Press ENTER to start installation"), m.width) | |
| 202 | 216 | ||
| 203 | - | case StatePreflightForm, StateDiskForm, StateOptionsForm: | |
| 217 | + | case StatePreflightForm, StateDiskForm, StateOptionsForm, StateAMDPStatePrompt: | |
| 204 | 218 | switch { | |
| 205 | 219 | case m.currentForm != nil: | |
| 206 | 220 | header := centerText(m.renderPhaseHeader(), m.width) | |
| @@ -257,8 +271,28 @@ func (m *Model) handleFormComplete() tea.Cmd { | |||
| 257 | 271 | m.logger.Info("Options form completed", | |
| 258 | 272 | "kernel", m.config.KernelChoice, | |
| 259 | 273 | "encryption", m.config.EncryptionType) | |
| 260 | - | // Move directly to confirmation | |
| 261 | - | // Encryption password is already handled in preflight form | |
| 274 | + | ||
| 275 | + | // Detect CPU and check if we need to show AMD P-State prompt | |
| 276 | + | cpuInfo, err := system.DetectCPUInfo() | |
| 277 | + | if err == nil { | |
| 278 | + | m.cpuInfo = cpuInfo | |
| 279 | + | // Show AMD P-State prompt only for AMD CPUs with P-State support | |
| 280 | + | if cpuInfo.Vendor == system.CPUVendorAMD && len(cpuInfo.AMDPStateModes) > 0 { | |
| 281 | + | m.logger.Info("AMD CPU detected - showing P-State configuration", | |
| 282 | + | "zen_gen", cpuInfo.AMDZenGen.Label, | |
| 283 | + | "modes", len(cpuInfo.AMDPStateModes)) | |
| 284 | + | m.state = StateAMDPStatePrompt | |
| 285 | + | m.currentForm = CreateAMDPStateForm(m.config, cpuInfo, m.formBuilder) | |
| 286 | + | return m.currentForm.Init() | |
| 287 | + | } | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | // Move directly to confirmation if not AMD or detection failed | |
| 291 | + | m.state = StateConfirmation | |
| 292 | + | return nil | |
| 293 | + | ||
| 294 | + | case StateAMDPStatePrompt: | |
| 295 | + | m.logger.Info("AMD P-State configuration completed", "mode", m.config.AMDPState) | |
| 262 | 296 | m.state = StateConfirmation | |
| 263 | 297 | return nil | |
| 264 | 298 | ||
| @@ -277,6 +311,12 @@ func (m *Model) renderConfirmation() string { | |||
| 277 | 311 | s += fmt.Sprintf("Disk: %s\n", m.config.TargetDisk) | |
| 278 | 312 | s += fmt.Sprintf("Kernel: %s\n", m.config.KernelChoice) | |
| 279 | 313 | s += fmt.Sprintf("Encryption: %s\n", m.config.EncryptionType) | |
| 314 | + | ||
| 315 | + | // Show AMD P-State mode if configured | |
| 316 | + | if m.config.AMDPState != "" { | |
| 317 | + | s += fmt.Sprintf("AMD P-State: %s\n", m.config.AMDPState) | |
| 318 | + | } | |
| 319 | + | ||
| 280 | 320 | s += "\n" | |
| 281 | 321 | s += styles.WarningStyle.Render("WARNING: This will erase all data on "+m.config.TargetDisk) + "\n\n" | |
| 282 | 322 | s += styles.HelpStyle.Render("Press ENTER to continue, Ctrl+C to cancel") | |
| @@ -330,10 +370,19 @@ func (m *Model) renderPhaseHeader() string { | |||
| 330 | 370 | case StateOptionsForm: | |
| 331 | 371 | stepNum = "3" | |
| 332 | 372 | stepName = "Installation Options" | |
| 373 | + | case StateAMDPStatePrompt: | |
| 374 | + | stepNum = "4" | |
| 375 | + | stepName = "AMD P-State Configuration" | |
| 333 | 376 | default: | |
| 334 | 377 | return "" | |
| 335 | 378 | } | |
| 336 | 379 | ||
| 337 | - | header := fmt.Sprintf("ArchUp Install - Step %s/3: %s", stepNum, stepName) | |
| 380 | + | // Total steps depends on whether AMD prompt is shown | |
| 381 | + | totalSteps := "3" | |
| 382 | + | if m.state == StateAMDPStatePrompt { | |
| 383 | + | totalSteps = "4" | |
| 384 | + | } | |
| 385 | + | ||
| 386 | + | header := fmt.Sprintf("ArchUp Install - Step %s/%s: %s", stepNum, totalSteps, stepName) | |
| 338 | 387 | return styles.TitleStyle.Render(header) | |
| 339 | 388 | } | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "github.com/bnema/archup/internal/config" | 6 | "github.com/bnema/archup/internal/config" | ||
| 7 | "github.com/bnema/archup/internal/logger" | 7 | "github.com/bnema/archup/internal/logger" | ||
| 8 | "github.com/bnema/archup/internal/phases" | 8 | "github.com/bnema/archup/internal/phases" | ||
| 9 | + | "github.com/bnema/archup/internal/system" | |||
| 9 | "github.com/bnema/archup/internal/ui/components" | 10 | "github.com/bnema/archup/internal/ui/components" | ||
| 10 | "github.com/bnema/archup/internal/ui/styles" | 11 | "github.com/bnema/archup/internal/ui/styles" | ||
| 11 | "github.com/charmbracelet/bubbles/spinner" | 12 | "github.com/charmbracelet/bubbles/spinner" | ||
| @@ -22,6 +23,7 @@ const ( | |||||
| 22 | StatePreflightForm | 23 | StatePreflightForm | ||
| 23 | StateDiskForm | 24 | StateDiskForm | ||
| 24 | StateOptionsForm | 25 | StateOptionsForm | ||
| 26 | + | StateAMDPStatePrompt // NEW: AMD P-State selection (shown only for AMD CPUs) | |||
| 25 | StateConfirmation | 27 | StateConfirmation | ||
| 26 | StateExecuting | 28 | StateExecuting | ||
| 27 | StateComplete | 29 | StateComplete | ||
| @@ -42,6 +44,7 @@ type Model struct { | |||||
| 42 | width int | 44 | width int | ||
| 43 | height int | 45 | height int | ||
| 44 | err error | 46 | err error | ||
| 47 | + | cpuInfo *system.CPUInfo // Detected CPU information | |||
| 45 | } | 48 | } | ||
| 46 | 49 | ||||
| 47 | // NewModel creates a new installer model | 50 | // NewModel creates a new installer model | ||
| @@ -116,10 +119,21 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||||
| 116 | m.currentForm = CreateDiskSelectionForm(m.config, m.formBuilder) | 119 | m.currentForm = CreateDiskSelectionForm(m.config, m.formBuilder) | ||
| 117 | return m, m.currentForm.Init() | 120 | return m, m.currentForm.Init() | ||
| 118 | 121 | ||||
| 119 | - | case StateConfirmation: | 122 | + | case StateAMDPStatePrompt: |
| 120 | m.state = StateOptionsForm | 123 | m.state = StateOptionsForm | ||
| 121 | m.currentForm = CreateOptionsForm(m.config, m.formBuilder) | 124 | m.currentForm = CreateOptionsForm(m.config, m.formBuilder) | ||
| 122 | return m, m.currentForm.Init() | 125 | return m, m.currentForm.Init() | ||
| 126 | + | ||||
| 127 | + | case StateConfirmation: | |||
| 128 | + | // Go back to AMD P-State prompt if we have AMD CPU, otherwise options | |||
| 129 | + | if m.cpuInfo != nil && m.cpuInfo.Vendor == system.CPUVendorAMD && len(m.cpuInfo.AMDPStateModes) > 0 { | |||
| 130 | + | m.state = StateAMDPStatePrompt | |||
| 131 | + | m.currentForm = CreateAMDPStateForm(m.config, m.cpuInfo, m.formBuilder) | |||
| 132 | + | } else { | |||
| 133 | + | m.state = StateOptionsForm | |||
| 134 | + | m.currentForm = CreateOptionsForm(m.config, m.formBuilder) | |||
| 135 | + | } | |||
| 136 | + | return m, m.currentForm.Init() | |||
| 123 | } | 137 | } | ||
| 124 | } | 138 | } | ||
| 125 | 139 | ||||
| @@ -161,7 +175,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||||
| 161 | 175 | ||||
| 162 | // Update current form if showing one | 176 | // Update current form if showing one | ||
| 163 | switch m.state { | 177 | switch m.state { | ||
| 164 | - | case StatePreflightForm, StateDiskForm, StateOptionsForm: | 178 | + | case StatePreflightForm, StateDiskForm, StateOptionsForm, StateAMDPStatePrompt: |
| 165 | switch { | 179 | switch { | ||
| 166 | case m.currentForm != nil: | 180 | case m.currentForm != nil: | ||
| 167 | // Don't pass WindowSizeMsg to form to preserve our max width | 181 | // Don't pass WindowSizeMsg to form to preserve our max width | ||
| @@ -200,7 +214,7 @@ func (m *Model) View() string { | |||||
| 200 | centerText(WelcomeTagline, m.width) + "\n\n" + | 214 | centerText(WelcomeTagline, m.width) + "\n\n" + | ||
| 201 | centerText(styles.HelpStyle.Render("Press ENTER to start installation"), m.width) | 215 | centerText(styles.HelpStyle.Render("Press ENTER to start installation"), m.width) | ||
| 202 | 216 | ||||
| 203 | - | case StatePreflightForm, StateDiskForm, StateOptionsForm: | 217 | + | case StatePreflightForm, StateDiskForm, StateOptionsForm, StateAMDPStatePrompt: |
| 204 | switch { | 218 | switch { | ||
| 205 | case m.currentForm != nil: | 219 | case m.currentForm != nil: | ||
| 206 | header := centerText(m.renderPhaseHeader(), m.width) | 220 | header := centerText(m.renderPhaseHeader(), m.width) | ||
| @@ -257,8 +271,28 @@ func (m *Model) handleFormComplete() tea.Cmd { | |||||
| 257 | m.logger.Info("Options form completed", | 271 | m.logger.Info("Options form completed", | ||
| 258 | "kernel", m.config.KernelChoice, | 272 | "kernel", m.config.KernelChoice, | ||
| 259 | "encryption", m.config.EncryptionType) | 273 | "encryption", m.config.EncryptionType) | ||
| 260 | - | // Move directly to confirmation | 274 | + | |
| 261 | - | // Encryption password is already handled in preflight form | 275 | + | // Detect CPU and check if we need to show AMD P-State prompt |
| 276 | + | cpuInfo, err := system.DetectCPUInfo() | |||
| 277 | + | if err == nil { | |||
| 278 | + | m.cpuInfo = cpuInfo | |||
| 279 | + | // Show AMD P-State prompt only for AMD CPUs with P-State support | |||
| 280 | + | if cpuInfo.Vendor == system.CPUVendorAMD && len(cpuInfo.AMDPStateModes) > 0 { | |||
| 281 | + | m.logger.Info("AMD CPU detected - showing P-State configuration", | |||
| 282 | + | "zen_gen", cpuInfo.AMDZenGen.Label, | |||
| 283 | + | "modes", len(cpuInfo.AMDPStateModes)) | |||
| 284 | + | m.state = StateAMDPStatePrompt | |||
| 285 | + | m.currentForm = CreateAMDPStateForm(m.config, cpuInfo, m.formBuilder) | |||
| 286 | + | return m.currentForm.Init() | |||
| 287 | + | } | |||
| 288 | + | } | |||
| 289 | + | ||||
| 290 | + | // Move directly to confirmation if not AMD or detection failed | |||
| 291 | + | m.state = StateConfirmation | |||
| 292 | + | return nil | |||
| 293 | + | ||||
| 294 | + | case StateAMDPStatePrompt: | |||
| 295 | + | m.logger.Info("AMD P-State configuration completed", "mode", m.config.AMDPState) | |||
| 262 | m.state = StateConfirmation | 296 | m.state = StateConfirmation | ||
| 263 | return nil | 297 | return nil | ||
| 264 | 298 | ||||
| @@ -277,6 +311,12 @@ func (m *Model) renderConfirmation() string { | |||||
| 277 | s += fmt.Sprintf("Disk: %s\n", m.config.TargetDisk) | 311 | s += fmt.Sprintf("Disk: %s\n", m.config.TargetDisk) | ||
| 278 | s += fmt.Sprintf("Kernel: %s\n", m.config.KernelChoice) | 312 | s += fmt.Sprintf("Kernel: %s\n", m.config.KernelChoice) | ||
| 279 | s += fmt.Sprintf("Encryption: %s\n", m.config.EncryptionType) | 313 | s += fmt.Sprintf("Encryption: %s\n", m.config.EncryptionType) | ||
| 314 | + | ||||
| 315 | + | // Show AMD P-State mode if configured | |||
| 316 | + | if m.config.AMDPState != "" { | |||
| 317 | + | s += fmt.Sprintf("AMD P-State: %s\n", m.config.AMDPState) | |||
| 318 | + | } | |||
| 319 | + | ||||
| 280 | s += "\n" | 320 | s += "\n" | ||
| 281 | s += styles.WarningStyle.Render("WARNING: This will erase all data on "+m.config.TargetDisk) + "\n\n" | 321 | s += styles.WarningStyle.Render("WARNING: This will erase all data on "+m.config.TargetDisk) + "\n\n" | ||
| 282 | s += styles.HelpStyle.Render("Press ENTER to continue, Ctrl+C to cancel") | 322 | s += styles.HelpStyle.Render("Press ENTER to continue, Ctrl+C to cancel") | ||
| @@ -330,10 +370,19 @@ func (m *Model) renderPhaseHeader() string { | |||||
| 330 | case StateOptionsForm: | 370 | case StateOptionsForm: | ||
| 331 | stepNum = "3" | 371 | stepNum = "3" | ||
| 332 | stepName = "Installation Options" | 372 | stepName = "Installation Options" | ||
| 373 | + | case StateAMDPStatePrompt: | |||
| 374 | + | stepNum = "4" | |||
| 375 | + | stepName = "AMD P-State Configuration" | |||
| 333 | default: | 376 | default: | ||
| 334 | return "" | 377 | return "" | ||
| 335 | } | 378 | } | ||
| 336 | 379 | ||||
| 337 | - | header := fmt.Sprintf("ArchUp Install - Step %s/3: %s", stepNum, stepName) | 380 | + | // Total steps depends on whether AMD prompt is shown |
| 381 | + | totalSteps := "3" | |||
| 382 | + | if m.state == StateAMDPStatePrompt { | |||
| 383 | + | totalSteps = "4" | |||
| 384 | + | } | |||
| 385 | + | ||||
| 386 | + | header := fmt.Sprintf("ArchUp Install - Step %s/%s: %s", stepNum, totalSteps, stepName) | |||
| 338 | return styles.TitleStyle.Render(header) | 387 | return styles.TitleStyle.Render(header) | ||
| 339 | } | 388 | } | ||