diff --git a/docs/hardware.md b/docs/hardware.md index 5959029..5ff6625 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -50,6 +50,14 @@ bits 18:11 of SMN `0x5D448`; the desktop fuse address reads `0xFFFFFFFF` there. 5600GE (issue #18) confirmed it: the register fused off slots 0 and 1, the same two slots whose PM table rows read all zero, so its cores 0-5 are slots 2-7. +On Vermeer, Linux can also number a surviving physical CCD 1 as CCD 0. The +driver reads the CCD-disable fuses at `0x5D218` and `0x5D21C` to map L3 groups +onto active physical CCDs before reading their core fuses or sending CO commands. +For example, a 5600X with physical CCD 0 disabled uses the core fuse at +`0x32081D98`, whose mask `0x0C` leaves slots 0, 1, 4, 5, 6, and 7 active. +This follows [ryzen_smu's CCD fuse decoding](https://github.com/amkillam/ryzen_smu/blob/d2983668300dd2a598e5a7dc40e71ce0678cc270/userspace/monitor_cpu.c#L342). +Per-core CO stays disabled if the active CCD count disagrees with the OS. + Reading an SMN register is a *write* of the address, so this needs write access to `/sys/kernel/ryzen_smu_drv/smn`, which `ryzen_smu` ships root-only -- the NixOS module grants it to the `corecycler` group alongside the mailbox files, and diff --git a/src/corecycler/smu/commands.py b/src/corecycler/smu/commands.py index 4f5d076..322adcc 100644 --- a/src/corecycler/smu/commands.py +++ b/src/corecycler/smu/commands.py @@ -68,6 +68,11 @@ class SMUCommandSet: core_fuse_shift: int = 0 core_fuse_die_wide: bool = False + # Zen 3 chiplets can retain physical CCD 1 while Linux numbers it 0. + # CCDs 0/1 are disabled by bits 31:30 here, CCDs 2-7 by bits 5:0 + # of the next register, as in ryzen_smu's get_fuse_topology. + ccd_fuse_addr: int | None = None + # CO (Curve Optimizer / DldoPsmMargin) commands — None if generation lacks CO. # get_co_mailbox overrides the mailbox for the GET command only: the APU # classes set CO via MP1 but read it back via RSMU (ZenStates-Core @@ -200,6 +205,7 @@ def has_pbo_limits(self) -> bool: encoding_scheme="zen3", uniform_8core_ccds=True, core_fuse_addr=_FUSE_ZEN3_CLASS, + ccd_fuse_addr=0x5D218, set_co_cmd=0x35, set_all_co_cmd=0x36, get_co_cmd=0x48, @@ -232,6 +238,7 @@ def has_pbo_limits(self) -> bool: encoding_scheme="zen3", uniform_8core_ccds=True, core_fuse_addr=_FUSE_ZEN3_CLASS, + ccd_fuse_addr=0x5D218, set_co_cmd=0x35, set_all_co_cmd=0x36, get_co_cmd=0x48, diff --git a/src/corecycler/smu/driver.py b/src/corecycler/smu/driver.py index 378aa2d..60d099c 100644 --- a/src/corecycler/smu/driver.py +++ b/src/corecycler/smu/driver.py @@ -172,6 +172,8 @@ def set_topology(self, topology) -> None: Mapping rules, per L3 group (falling back to ``core_id // 8`` grouping when L3 info is absent): + - On Vermeer, the CCD-disable fuses map Linux's compact L3 groups + onto physical CCDs before reading core fuses or encoding commands. - A full 8-core group maps to identity slots under any numbering scheme -- no SMU traffic. - A group with a hole INTERNAL to its own 8-slot window proves the @@ -219,8 +221,10 @@ def set_topology(self, topology) -> None: self._offline_hint = "" if fully_online else " (some present CPUs are offline — online all cores for CO tuning)" core_map: dict[int, tuple[int, int]] = {} try: - for encode_ccd in sorted(groups): - group_ids = groups[encode_ccd] + physical_ccds = self._physical_ccd_map(groups) + for logical_ccd in sorted(groups): + encode_ccd = physical_ccds[logical_ccd] + group_ids = groups[logical_ccd] slots = self._derive_group_slots(group_ids, fully_online) if slots is None: slots = self._fuse_group_slots(encode_ccd, len(group_ids)) @@ -232,6 +236,26 @@ def set_topology(self, topology) -> None: self._core_map = core_map self._core_map_error = None + def _physical_ccd_map(self, groups: dict[int, list[int]]) -> dict[int, int]: + addr = self.commands.ccd_fuse_addr + if addr is None: + return {ccd: ccd for ccd in groups} + ok, message = self.check_smn_readable() + if not ok: + raise CoreMapError(f"CCD/core-disable fuse discovery requires SMN access: {message}") + present = self.read_smn(addr) + down = self.read_smn(addr + 4) + if present is None or down is None: + raise CoreMapError("CCD fuse read failed; per-core CO stays disabled") + disabled = ((present >> 30) & 3) | ((down & 0x3F) << 2) + physical = [ccd for ccd in range(8) if not (disabled >> ccd) & 1] + if len(physical) != len(groups): + raise CoreMapError( + f"CCD fuses report active physical CCDs {physical} but the OS reports " + f"{len(groups)} CCD groups; per-core CO stays disabled{self._offline_hint}" + ) + return dict(zip(sorted(groups), physical, strict=True)) + @staticmethod def _derive_group_slots(group_ids: list[int], holes_trusted: bool) -> list[int] | None: """Slots this group's numbering proves by itself; None means read the fuse. diff --git a/tests/contract_inventory.py b/tests/contract_inventory.py index 7977e17..c5c8aa8 100644 --- a/tests/contract_inventory.py +++ b/tests/contract_inventory.py @@ -259,6 +259,7 @@ def _pin_core_slot_mapping() -> None: vermeer = get_commands(CPUGeneration.ZEN3_VERMEER) assert vermeer is not None and vermeer.uniform_8core_ccds assert vermeer.core_fuse_addr == 0x30081D98 + assert vermeer.ccd_fuse_addr == 0x5D218 smu = RyzenSMU(vermeer, Path("/nonexistent")) topo = MagicMock() topo.cores = {c: MagicMock(ccd=0) for c in range(6)} @@ -272,6 +273,10 @@ def responder(cmd, args=(0, 0, 0, 0, 0, 0)): def read_smn(address: int) -> int: fuse_reads.append(address) + if address == 0x5D218: + return 0x80400000 + if address == 0x5D21C: + return 0x3F return sum(1 << slot for slot in fused_off) smu._send_command = responder @@ -279,7 +284,7 @@ def read_smn(address: int) -> int: smu.read_smn = read_smn smu.set_topology(topo) assert co_calls == [] - assert fuse_reads == [vermeer.core_fuse_addr] + assert fuse_reads == [0x5D218, 0x5D21C, vermeer.core_fuse_addr] assert smu.core_map == {0: (0, 0), 1: (0, 1), 2: (0, 4), 3: (0, 5), 4: (0, 6), 5: (0, 7)} fused = { gen: (cmds.core_fuse_addr, cmds.core_fuse_shift) for gen, cmds in COMMAND_SETS.items() if cmds.core_fuse_addr diff --git a/tests/test_smu_ccd_map.py b/tests/test_smu_ccd_map.py new file mode 100644 index 0000000..e48ee80 --- /dev/null +++ b/tests/test_smu_ccd_map.py @@ -0,0 +1,56 @@ +"""Vermeer can expose physical CCD 1 as Linux's only L3 group, numbered 0.""" + +from types import SimpleNamespace +from unittest.mock import MagicMock + +from corecycler.smu.commands import CPUGeneration, get_commands +from corecycler.smu.driver import RyzenSMU + +VERMEER = get_commands(CPUGeneration.ZEN3_VERMEER) + + +def _discover(cores: dict[int, int], words: dict[int, int]) -> RyzenSMU: + smu = RyzenSMU(VERMEER, MagicMock()) + smu.check_smn_readable = lambda: (True, "OK") + smu.read_smn = words.get + smu.set_topology(SimpleNamespace(cores={cid: SimpleNamespace(ccd=ccd) for cid, ccd in cores.items()})) + return smu + + +def test_reported_5600x_uses_the_surviving_second_ccd() -> None: + smu = _discover( + dict.fromkeys(range(6), 0), + {0x5D218: 0x40C00000, 0x5D21C: 0x3F, 0x30081D98: 0x3FF, 0x32081D98: 0x30C}, + ) + assert smu.core_map_error is None + assert smu.core_map == {0: (1, 0), 1: (1, 1), 2: (1, 4), 3: (1, 5), 4: (1, 6), 5: (1, 7)} + + +def test_full_single_ccd_still_needs_its_physical_ccd_number() -> None: + smu = _discover( + dict.fromkeys(range(8), 0), + {0x5D218: 0x40C00000, 0x5D21C: 0x3F, 0x30081D98: 0x3FF, 0x32081D98: 0x300}, + ) + assert smu.core_map_error is None + assert smu.core_map == {cid: (1, cid) for cid in range(8)} + + +def test_5950x_preserves_both_full_ccds() -> None: + smu = _discover( + {cid: cid // 8 for cid in range(16)}, + {0x5D218: 0x00C00000, 0x5D21C: 0x3F, 0x30081D98: 0x300, 0x32081D98: 0x300}, + ) + assert smu.core_map_error is None + assert smu.core_map == {cid: (cid // 8, cid % 8) for cid in range(16)} + + +def test_ccd_count_disagreement_refuses_to_guess() -> None: + smu = _discover(dict.fromkeys(range(6), 0), {0x5D218: 0x00C00000, 0x5D21C: 0x3F}) + assert smu.core_map is None + assert "active physical CCDs [0, 1]" in smu.core_map_error + + +def test_unreadable_ccd_fuses_refuse_to_guess() -> None: + smu = _discover(dict.fromkeys(range(8), 0), {}) + assert smu.core_map is None + assert "CCD fuse read failed" in smu.core_map_error diff --git a/tests/test_smu_core_map.py b/tests/test_smu_core_map.py index 5ddfd01..6790e18 100644 --- a/tests/test_smu_core_map.py +++ b/tests/test_smu_core_map.py @@ -66,6 +66,13 @@ def __init__(self, smu: RyzenSMU, commands, live: dict[int, set[int]], *, smn: b smu.read_smn = self._read_smn def _read_smn(self, address: int) -> int | None: + if self.commands.ccd_fuse_addr is not None: + enabled = sum(1 << ccd for ccd in self.live) or 1 + disabled = (~enabled) & 0xFF + if address == self.commands.ccd_fuse_addr: + return ((enabled & 3) << 22) | ((disabled & 3) << 30) + if address == self.commands.ccd_fuse_addr + 4: + return disabled >> 2 self.fuse_reads.append(address) base = self.commands.core_fuse_addr ccd = (address - base) >> 25 @@ -113,6 +120,12 @@ def _fuse_addr(commands, ccd: int) -> int: class TestRenumberedHarvested: + def test_write_uses_surviving_physical_ccd_and_skips_disabled_slots(self) -> None: + smu, silicon = _mapped_smu(VERMEER, dict.fromkeys(range(6), 0), {1: REPORTED_5600X_LIVE_SLOTS}) + assert smu.set_co_offset(2, -1) is True + assert silicon.writes == [(1, 4, -1)] + assert smu.get_co_offset(2) == -1 + def test_5600x_renumbered_maps_onto_live_slots(self): smu, silicon = _mapped_smu( VERMEER,