diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index 684e454cb..095a60286 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -1891,13 +1891,13 @@ impl Engine { None if allowed || current > 0 => crate::detect::run_timeout(std::process::Command::new(&smi).args(["-i", &device, "-pl", ¤t.to_string()]), None, Duration::from_secs(20)).map(|out| out.contains("All done")), None => Some(false), }; - shared.send(Cmd::TuneProbe(idx, Ok(TuneProbe { clock_max_mhz: clock_max, clock_min_mhz: 0, driver, direct: direct.unwrap_or(false), amd_ordinal: -1 }))); + shared.send(Cmd::TuneProbe(idx, Ok(TuneProbe { clock_max_mhz: clock_max, clock_min_mhz: 0, driver, direct: direct.unwrap_or(false), amd_ordinal: -1, ..Default::default() }))); }); } "amd" => { let Some(exe) = self.bins.telemetry.clone() else { self.sweep_pending = None; - self.shared.send(Cmd::TuneProbe(idx, Ok(TuneProbe { clock_max_mhz: 0, clock_min_mhz: 0, driver: c.driver.clone(), direct: false, amd_ordinal: -1 }))); + self.shared.send(Cmd::TuneProbe(idx, Ok(TuneProbe { clock_max_mhz: 0, clock_min_mhz: 0, driver: c.driver.clone(), direct: false, amd_ordinal: -1, ..Default::default() }))); return; }; let ordinal = c.amd_ordinal; @@ -1906,14 +1906,18 @@ impl Engine { let out = crate::detect::run_timeout(std::process::Command::new(&exe).arg("--tune"), None, Duration::from_secs(20)).unwrap_or_default(); let t = out.lines().filter_map(parse_amd_tune).find(|t| ordinal < 0 || t.ordinal as i64 == ordinal); shared.send(Cmd::TuneProbe(idx, Ok(match t { - Some(t) if t.ok => TuneProbe { clock_max_mhz: t.gmax_max as u32, clock_min_mhz: t.gmax_min as u32, driver, direct: true, amd_ordinal: t.ordinal as i64 }, - Some(t) => TuneProbe { clock_max_mhz: 0, clock_min_mhz: 0, driver, direct: false, amd_ordinal: t.ordinal as i64 }, - None => TuneProbe { clock_max_mhz: 0, clock_min_mhz: 0, driver, direct: false, amd_ordinal: -1 }, + // PC 1's 9070 XT (ember-tune-pc1-1, 22:30 UTC): `gmax 0 gmax_range -500 1000`, an OFFSET from + // the stock clock, not MHz; a range with a negative floor is an offset range and the clock + // knob stays closed until the stock clock is known (the power limit is the AMD lever), and + // `plimit_range -30 10` bounds the power ladder (the percent scale rides power_* below) + Some(t) if t.ok => TuneProbe { clock_max_mhz: if t.gmax_min >= 0.0 && t.gmax_max > 0.0 { t.gmax_max as u32 } else { 0 }, clock_min_mhz: if t.gmax_min > 0.0 { t.gmax_min as u32 } else { 0 }, driver, direct: true, amd_ordinal: t.ordinal as i64, plimit_min: t.plimit_min, plimit_max: t.plimit_max }, + Some(t) => TuneProbe { clock_max_mhz: 0, clock_min_mhz: 0, driver, direct: false, amd_ordinal: t.ordinal as i64, ..Default::default() }, + None => TuneProbe { clock_max_mhz: 0, clock_min_mhz: 0, driver, direct: false, amd_ordinal: -1, ..Default::default() }, }))); }); } _ => { - self.shared.send(Cmd::TuneProbe(idx, Ok(TuneProbe { clock_max_mhz: 0, clock_min_mhz: 0, driver: c.driver.clone(), direct: false, amd_ordinal: -1 }))); + self.shared.send(Cmd::TuneProbe(idx, Ok(TuneProbe { clock_max_mhz: 0, clock_min_mhz: 0, driver: c.driver.clone(), direct: false, amd_ordinal: -1, ..Default::default() }))); } } } @@ -1939,7 +1943,13 @@ impl Engine { // NVIDIA control: this process is elevated (direct), or Power control is on so the one-prompt helper may run. // The --sweep job alone never counts: it must not raise a prompt on a PC with nobody there (5 October 2026). let power_control = probe.direct || self.shared.settings.lock().unwrap().power_control; - let limits = crate::ember::Limits { power_default_w: c.power_default_w, power_min_w: c.power_min_w, power_max_w: c.power_max_w, clock_max_mhz: probe.clock_max_mhz, clock_min_mhz: probe.clock_min_mhz }; + // AMD's power limit is a percent offset from the default (ADLX): the plan's watts scale becomes a percent + // scale (default 100, floor 100 + plimit_min, ceiling 100 + plimit_max), tune_apply sends pct - 100 + let limits = if c.vendor == "amd" && probe.direct && probe.plimit_max >= probe.plimit_min && probe.plimit_min > -100.0 { + crate::ember::Limits { power_default_w: 100.0, power_min_w: 100.0 + probe.plimit_min, power_max_w: 100.0 + probe.plimit_max, clock_max_mhz: probe.clock_max_mhz, clock_min_mhz: probe.clock_min_mhz } + } else { + crate::ember::Limits { power_default_w: c.power_default_w, power_min_w: c.power_min_w, power_max_w: c.power_max_w, clock_max_mhz: probe.clock_max_mhz, clock_min_mhz: probe.clock_min_mhz } + }; let control = match c.vendor.as_str() { "nvidia" => crate::ember::control_reason("nvidia", &limits, &c.device, power_control, false), "amd" => crate::ember::control_reason("amd", &limits, &c.device, power_control, probe.direct && probe.amd_ordinal >= 0), @@ -2113,7 +2123,8 @@ impl Engine { return; } let n = c.amd_ordinal.to_string(); - let offset = step.point.power_pct as i64 - 100; + // the step's limit on the AMD scale is a percent (the probe's Limits); the offset is that minus 100 + let offset = if c.power_default_w <= 0.0 { step.watts.round() as i64 - 100 } else { step.point.power_pct as i64 - 100 }; let unlocked = clock == 0 && offset == 0; let gmax = if clock > 0 { clock } else { c.clock_max_mhz }; std::thread::spawn(move || { @@ -3465,6 +3476,9 @@ pub struct TuneProbe { /// NVIDIA: this process sets limits itself (elevated); AMD: the helper answered its `--tune` line with ok pub direct: bool, pub amd_ordinal: i64, + /// AMD: the power offset range in percent from the `tune` line (PC 1's 9070 XT: -30 to 10) + pub plimit_min: f64, + pub plimit_max: f64, } /// One `tune` line of igneum-gpu-telemetry --tune: