diff --git a/Models/CpuSelection.cs b/Models/CpuSelection.cs
new file mode 100644
index 0000000..a384c07
--- /dev/null
+++ b/Models/CpuSelection.cs
@@ -0,0 +1,298 @@
+/*
+ * ThreadPilot - Advanced Windows Process and Power Plan Manager
+ * Copyright (C) 2025 Prime Build
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, version 3 only.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+namespace ThreadPilot.Models
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ ///
+ /// Identifies a logical processor without relying on a legacy 64-bit affinity mask.
+ ///
+ public readonly record struct ProcessorRef(ushort Group, byte LogicalProcessorNumber, int GlobalIndex);
+
+ ///
+ /// Stable signature used to determine whether a persisted CPU selection was created for the current topology.
+ ///
+ public sealed record CpuTopologySignature
+ {
+ public string CpuBrand { get; init; } = "Unknown";
+
+ public int LogicalProcessorCount { get; init; }
+
+ public int PhysicalCoreCount { get; init; }
+
+ public int ProcessorGroupCount { get; init; } = 1;
+
+ public int NumaNodeCount { get; init; }
+
+ public int LastLevelCacheGroupCount { get; init; }
+
+ public string Source { get; init; } = "Unknown";
+ }
+
+ ///
+ /// Metadata that explains how a CPU selection was built and whether it can be represented by legacy APIs.
+ ///
+ public sealed record CpuSelectionMetadata
+ {
+ public CpuTopologySignature? TopologySignature { get; init; }
+
+ public bool CreatedFromLegacyAffinityMask { get; init; }
+
+ public bool ContainsLogicalProcessorsBeyondLegacyMask { get; init; }
+
+ public bool HasMultipleProcessorGroups { get; init; }
+
+ public int ProcessorGroupCount { get; init; }
+
+ public int MaxGlobalLogicalProcessorIndex { get; init; } = -1;
+
+ public string SelectionReason { get; init; } = string.Empty;
+ }
+
+ ///
+ /// Lightweight topology snapshot used by the CpuSelection migration layer.
+ /// Runtime topology detection will populate this in a later phase.
+ ///
+ public sealed class CpuTopologySnapshot
+ {
+ private readonly IReadOnlyDictionary cpuSetIdsByProcessor;
+ private readonly IReadOnlyDictionary efficiencyClassesByProcessor;
+
+ private CpuTopologySnapshot(
+ IReadOnlyList logicalProcessors,
+ IReadOnlyDictionary cpuSetIdsByProcessor,
+ IReadOnlyDictionary efficiencyClassesByProcessor,
+ CpuTopologySignature signature)
+ {
+ this.LogicalProcessors = logicalProcessors;
+ this.cpuSetIdsByProcessor = cpuSetIdsByProcessor;
+ this.efficiencyClassesByProcessor = efficiencyClassesByProcessor;
+ this.Signature = signature;
+ }
+
+ public IReadOnlyList LogicalProcessors { get; }
+
+ public CpuTopologySignature Signature { get; }
+
+ public static CpuTopologySnapshot Create(
+ IEnumerable logicalProcessors,
+ IReadOnlyDictionary? cpuSetIds = null,
+ IReadOnlyDictionary? efficiencyClasses = null,
+ CpuTopologySignature? signature = null)
+ {
+ ArgumentNullException.ThrowIfNull(logicalProcessors);
+
+ var processors = logicalProcessors
+ .Distinct()
+ .OrderBy(processor => processor.GlobalIndex)
+ .ThenBy(processor => processor.Group)
+ .ThenBy(processor => processor.LogicalProcessorNumber)
+ .ToList();
+
+ var duplicatedGlobalIndexes = processors
+ .GroupBy(processor => processor.GlobalIndex)
+ .Where(group => group.Count() > 1)
+ .Select(group => group.Key)
+ .ToList();
+ if (duplicatedGlobalIndexes.Count > 0)
+ {
+ throw new ArgumentException(
+ $"GlobalIndex must be unique in a CPU topology snapshot. Duplicates: {string.Join(", ", duplicatedGlobalIndexes)}.",
+ nameof(logicalProcessors));
+ }
+
+ var processorSet = processors.ToHashSet();
+ var cpuSetMap = cpuSetIds?
+ .Where(kvp => processorSet.Contains(kvp.Key))
+ .ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
+ ?? new Dictionary();
+
+ var efficiencyClassMap = efficiencyClasses?
+ .Where(kvp => processorSet.Contains(kvp.Key))
+ .ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
+ ?? new Dictionary();
+
+ var resolvedSignature = signature ?? new CpuTopologySignature
+ {
+ LogicalProcessorCount = processors.Count,
+ ProcessorGroupCount = processors.Select(processor => processor.Group).Distinct().Count(),
+ Source = "Snapshot",
+ };
+
+ return new CpuTopologySnapshot(processors, cpuSetMap, efficiencyClassMap, resolvedSignature);
+ }
+
+ public bool TryGetCpuSetId(ProcessorRef processor, out uint cpuSetId) =>
+ this.cpuSetIdsByProcessor.TryGetValue(processor, out cpuSetId);
+
+ public bool TryGetEfficiencyClass(ProcessorRef processor, out byte efficiencyClass) =>
+ this.efficiencyClassesByProcessor.TryGetValue(processor, out efficiencyClass);
+
+ public byte? GetPerformanceEfficiencyClass()
+ {
+ if (this.efficiencyClassesByProcessor.Count == 0)
+ {
+ return null;
+ }
+
+ return this.efficiencyClassesByProcessor.Values.Max();
+ }
+ }
+
+ ///
+ /// Group-aware CPU selection model used by new persistence and migration code.
+ ///
+ public sealed record CpuSelection
+ {
+ public List CpuSetIds { get; init; } = new();
+
+ public List LogicalProcessors { get; init; } = new();
+
+ public List GlobalLogicalProcessorIndexes { get; init; } = new();
+
+ public CpuSelectionMetadata Metadata { get; init; } = new();
+
+ public static CpuSelection FromProcessors(
+ IEnumerable processors,
+ CpuTopologySnapshot topology,
+ string selectionReason = "")
+ {
+ ArgumentNullException.ThrowIfNull(processors);
+ ArgumentNullException.ThrowIfNull(topology);
+
+ var selectedProcessors = processors
+ .Distinct()
+ .OrderBy(processor => processor.GlobalIndex)
+ .ThenBy(processor => processor.Group)
+ .ThenBy(processor => processor.LogicalProcessorNumber)
+ .ToList();
+
+ var topologyProcessors = topology.LogicalProcessors.ToHashSet();
+ var missingProcessors = selectedProcessors
+ .Where(processor => !topologyProcessors.Contains(processor))
+ .ToList();
+ if (missingProcessors.Count > 0)
+ {
+ throw new ArgumentException(
+ $"CPU selection contains processor(s) not present in the topology: {string.Join(", ", missingProcessors)}.",
+ nameof(processors));
+ }
+
+ var cpuSetIds = selectedProcessors
+ .Select(processor => topology.TryGetCpuSetId(processor, out var cpuSetId) ? (uint?)cpuSetId : null)
+ .Where(cpuSetId => cpuSetId.HasValue)
+ .Select(cpuSetId => cpuSetId!.Value)
+ .Distinct()
+ .OrderBy(cpuSetId => cpuSetId)
+ .ToList();
+
+ return new CpuSelection
+ {
+ CpuSetIds = cpuSetIds,
+ LogicalProcessors = selectedProcessors,
+ GlobalLogicalProcessorIndexes = selectedProcessors
+ .Select(processor => processor.GlobalIndex)
+ .Distinct()
+ .OrderBy(index => index)
+ .ToList(),
+ Metadata = CreateMetadata(selectedProcessors, topology.Signature, createdFromLegacyAffinityMask: false, selectionReason),
+ };
+ }
+
+ public static CpuSelection FromLegacyAffinityMask(long mask, CpuTopologySnapshot topology)
+ {
+ ArgumentNullException.ThrowIfNull(topology);
+
+ var unsignedMask = unchecked((ulong)mask);
+ var selectedIndexes = new HashSet();
+ for (var bit = 0; bit < 64; bit++)
+ {
+ if ((unsignedMask & (1UL << bit)) != 0)
+ {
+ selectedIndexes.Add(bit);
+ }
+ }
+
+ var selectedProcessors = topology.LogicalProcessors
+ .Where(processor => selectedIndexes.Contains(processor.GlobalIndex))
+ .ToList();
+
+ var selection = FromProcessors(selectedProcessors, topology, "Migrated from legacy affinity mask");
+ return selection with
+ {
+ Metadata = CreateMetadata(
+ selection.LogicalProcessors,
+ topology.Signature,
+ createdFromLegacyAffinityMask: true,
+ "Migrated from legacy affinity mask"),
+ };
+ }
+
+ public static long? ToLegacyAffinityMaskOrNull(CpuSelection selection)
+ {
+ ArgumentNullException.ThrowIfNull(selection);
+
+ if (selection.LogicalProcessors.Any(processor => processor.GlobalIndex >= 64))
+ {
+ return null;
+ }
+
+ if (selection.LogicalProcessors.Select(processor => processor.Group).Distinct().Count() > 1)
+ {
+ return null;
+ }
+
+ long mask = 0;
+ foreach (var processor in selection.LogicalProcessors)
+ {
+ if (processor.GlobalIndex < 0)
+ {
+ return null;
+ }
+
+ mask |= 1L << processor.GlobalIndex;
+ }
+
+ return mask;
+ }
+
+ private static CpuSelectionMetadata CreateMetadata(
+ IReadOnlyCollection processors,
+ CpuTopologySignature signature,
+ bool createdFromLegacyAffinityMask,
+ string selectionReason)
+ {
+ var groups = processors.Select(processor => processor.Group).Distinct().ToList();
+ var maxGlobalIndex = processors.Count == 0
+ ? -1
+ : processors.Max(processor => processor.GlobalIndex);
+
+ return new CpuSelectionMetadata
+ {
+ TopologySignature = signature,
+ CreatedFromLegacyAffinityMask = createdFromLegacyAffinityMask,
+ ContainsLogicalProcessorsBeyondLegacyMask = maxGlobalIndex >= 64,
+ HasMultipleProcessorGroups = groups.Count > 1,
+ ProcessorGroupCount = groups.Count,
+ MaxGlobalLogicalProcessorIndex = maxGlobalIndex,
+ SelectionReason = selectionReason,
+ };
+ }
+ }
+}
diff --git a/Tests/ThreadPilot.Core.Tests/CpuSelectionTests.cs b/Tests/ThreadPilot.Core.Tests/CpuSelectionTests.cs
new file mode 100644
index 0000000..2ea8da7
--- /dev/null
+++ b/Tests/ThreadPilot.Core.Tests/CpuSelectionTests.cs
@@ -0,0 +1,230 @@
+namespace ThreadPilot.Core.Tests
+{
+ using ThreadPilot.Models;
+
+ public sealed class CpuSelectionTests
+ {
+ [Fact]
+ public void CpuSelection_WithGlobalIndex64_DoesNotAliasCpu0InLegacyMask()
+ {
+ var topology = CpuTopologySnapshot.Create(
+ [
+ new ProcessorRef(0, 0, 0),
+ new ProcessorRef(1, 0, 64),
+ ]);
+
+ var selection = CpuSelection.FromProcessors(
+ [new ProcessorRef(1, 0, 64)],
+ topology);
+
+ var legacyMask = CpuSelection.ToLegacyAffinityMaskOrNull(selection);
+
+ Assert.Null(legacyMask);
+ Assert.Contains(selection.LogicalProcessors, p => p.GlobalIndex == 64);
+ Assert.DoesNotContain(selection.LogicalProcessors, p => p.GlobalIndex == 0);
+ }
+
+ [Fact]
+ public void CoreMask_ToProcessorAffinity_WithCpu64Only_DocumentsLegacyAliasBug()
+ {
+ var mask = new CoreMask { Name = "CPU64 Only" };
+ for (var i = 0; i < 65; i++)
+ {
+ mask.BoolMask.Add(i == 64);
+ }
+
+ var legacyAffinity = mask.ToProcessorAffinity();
+
+ Assert.Equal(1, legacyAffinity);
+ Assert.True((legacyAffinity & 1L) != 0);
+ }
+
+ [Fact]
+ public void CpuTopologySnapshot_KeepsProcessorsWithSameLogicalIndexInDifferentGroupsDistinct()
+ {
+ var group0Cpu0 = new ProcessorRef(0, 0, 0);
+ var group1Cpu0 = new ProcessorRef(1, 0, 64);
+ var topology = CpuTopologySnapshot.Create(
+ [group0Cpu0, group1Cpu0],
+ new Dictionary
+ {
+ [group0Cpu0] = 100,
+ [group1Cpu0] = 200,
+ });
+
+ Assert.True(topology.TryGetCpuSetId(group0Cpu0, out var group0CpuSetId));
+ Assert.True(topology.TryGetCpuSetId(group1Cpu0, out var group1CpuSetId));
+ Assert.Equal(100U, group0CpuSetId);
+ Assert.Equal(200U, group1CpuSetId);
+ Assert.Equal(2, topology.LogicalProcessors.Count);
+ }
+
+ [Fact]
+ public void CpuTopologySnapshot_Create_ThrowsWhenGlobalIndexIsDuplicated()
+ {
+ var processors = new[]
+ {
+ new ProcessorRef(0, 0, 0),
+ new ProcessorRef(0, 1, 0),
+ };
+
+ var exception = Assert.Throws(() => CpuTopologySnapshot.Create(processors));
+ Assert.Contains("GlobalIndex", exception.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void CpuTopologySnapshot_Create_ThrowsWhenLogicalProcessorsIsNull()
+ {
+ Assert.Throws(() =>
+ CpuTopologySnapshot.Create(null!));
+ }
+
+ [Fact]
+ public void CpuTopologySnapshot_PerformanceEfficiencyClass_IsHighestNumericValue()
+ {
+ var eCore = new ProcessorRef(0, 8, 8);
+ var pCore = new ProcessorRef(0, 0, 0);
+ var topology = CpuTopologySnapshot.Create(
+ [pCore, eCore],
+ efficiencyClasses: new Dictionary
+ {
+ [pCore] = 2,
+ [eCore] = 0,
+ });
+
+ Assert.Equal(2, topology.GetPerformanceEfficiencyClass());
+ }
+
+ [Fact]
+ public void CpuTopologySnapshot_GetPerformanceEfficiencyClass_ReturnsNullWhenNoEfficiencyClassesExist()
+ {
+ var topology = CpuTopologySnapshot.Create([new ProcessorRef(0, 0, 0)]);
+
+ var performanceClass = topology.GetPerformanceEfficiencyClass();
+
+ Assert.Null(performanceClass);
+ }
+
+ [Fact]
+ public void FromLegacyAffinityMask_SelectsOnlyRepresentableProcessors()
+ {
+ var topology = CpuTopologySnapshot.Create(
+ [
+ new ProcessorRef(0, 0, 0),
+ new ProcessorRef(0, 1, 1),
+ new ProcessorRef(1, 0, 64),
+ ]);
+
+ var selection = CpuSelection.FromLegacyAffinityMask(0b11, topology);
+
+ Assert.Equal([0, 1], selection.GlobalLogicalProcessorIndexes);
+ Assert.DoesNotContain(selection.LogicalProcessors, p => p.GlobalIndex == 64);
+ }
+
+ [Fact]
+ public void FromLegacyAffinityMask_WithCpuSetId_SetsMigrationMetadataAndIndexes()
+ {
+ var cpu0 = new ProcessorRef(0, 0, 0);
+ var cpu2 = new ProcessorRef(0, 2, 2);
+ var topology = CpuTopologySnapshot.Create(
+ [cpu0, cpu2],
+ new Dictionary
+ {
+ [cpu0] = 300,
+ [cpu2] = 100,
+ });
+
+ var selection = CpuSelection.FromLegacyAffinityMask(0b101, topology);
+
+ Assert.True(selection.Metadata.CreatedFromLegacyAffinityMask);
+ Assert.Equal("Migrated from legacy affinity mask", selection.Metadata.SelectionReason);
+ Assert.Equal([0, 2], selection.GlobalLogicalProcessorIndexes);
+ Assert.Equal([100U, 300U], selection.CpuSetIds);
+ }
+
+ [Fact]
+ public void ToLegacyAffinityMaskOrNull_ReturnsMaskForSingleGroupBelow64()
+ {
+ var topology = CpuTopologySnapshot.Create(
+ [
+ new ProcessorRef(0, 0, 0),
+ new ProcessorRef(0, 3, 3),
+ ]);
+ var selection = CpuSelection.FromProcessors(topology.LogicalProcessors, topology);
+
+ var legacyMask = CpuSelection.ToLegacyAffinityMaskOrNull(selection);
+
+ Assert.Equal(0b1001, legacyMask);
+ }
+
+ [Fact]
+ public void FromProcessors_ThrowsWhenProcessorsIsNull()
+ {
+ var topology = CpuTopologySnapshot.Create([new ProcessorRef(0, 0, 0)]);
+
+ Assert.Throws(() =>
+ CpuSelection.FromProcessors(null!, topology));
+ }
+
+ [Fact]
+ public void FromProcessors_ThrowsWhenTopologyIsNull()
+ {
+ Assert.Throws(() =>
+ CpuSelection.FromProcessors([new ProcessorRef(0, 0, 0)], null!));
+ }
+
+ [Fact]
+ public void FromProcessors_ThrowsWhenProcessorIsNotInTopology()
+ {
+ var topology = CpuTopologySnapshot.Create([new ProcessorRef(0, 0, 0)]);
+ var missingProcessor = new ProcessorRef(0, 1, 1);
+
+ var exception = Assert.Throws(() =>
+ CpuSelection.FromProcessors([missingProcessor], topology));
+
+ Assert.Contains("topology", exception.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void FromProcessors_WithCpuSetIds_PopulatesDistinctOrderedCpuSetIds()
+ {
+ var cpu0 = new ProcessorRef(0, 0, 0);
+ var cpu1 = new ProcessorRef(0, 1, 1);
+ var cpu2 = new ProcessorRef(0, 2, 2);
+ var topology = CpuTopologySnapshot.Create(
+ [cpu0, cpu1, cpu2],
+ new Dictionary
+ {
+ [cpu0] = 200,
+ [cpu1] = 100,
+ [cpu2] = 200,
+ });
+
+ var selection = CpuSelection.FromProcessors([cpu0, cpu1, cpu2], topology);
+
+ Assert.Equal([100U, 200U], selection.CpuSetIds);
+ }
+
+ [Fact]
+ public void ToLegacyAffinityMaskOrNull_ThrowsWhenSelectionIsNull()
+ {
+ Assert.Throws(() =>
+ CpuSelection.ToLegacyAffinityMaskOrNull(null!));
+ }
+
+ [Fact]
+ public void ToLegacyAffinityMaskOrNull_ReturnsNullForMultipleProcessorGroups()
+ {
+ var topology = CpuTopologySnapshot.Create(
+ [
+ new ProcessorRef(0, 0, 0),
+ new ProcessorRef(1, 0, 64),
+ ]);
+ var selection = CpuSelection.FromProcessors(topology.LogicalProcessors, topology);
+
+ var legacyMask = CpuSelection.ToLegacyAffinityMaskOrNull(selection);
+
+ Assert.Null(legacyMask);
+ }
+ }
+}