diff --git a/Models/CoreMask.cs b/Models/CoreMask.cs
index 5d3f0ef..b61cfdc 100644
--- a/Models/CoreMask.cs
+++ b/Models/CoreMask.cs
@@ -71,7 +71,9 @@ public partial class CoreMask : ObservableObject
public int SelectedCoreCount => this.BoolMask.Count(b => b);
///
- /// Converts the boolean mask to a processor affinity value.
+ /// Converts the boolean mask to a legacy 64-bit processor affinity value.
+ /// This is only safe for single processor-group selections below CPU 64;
+ /// topology-aware apply paths must prefer .
///
public long ToProcessorAffinity()
{
@@ -162,4 +164,3 @@ public override string ToString()
}
}
}
-
diff --git a/Services/ProcessAffinityApplyCoordinator.cs b/Services/ProcessAffinityApplyCoordinator.cs
new file mode 100644
index 0000000..66cf0c0
--- /dev/null
+++ b/Services/ProcessAffinityApplyCoordinator.cs
@@ -0,0 +1,178 @@
+/*
+ * ThreadPilot - process tab affinity apply coordination.
+ */
+namespace ThreadPilot.Services
+{
+ using Microsoft.Extensions.Logging;
+ using ThreadPilot.Models;
+
+ public interface IProcessAffinityApplyCoordinator
+ {
+ Task ApplyCoreMaskAsync(
+ ProcessModel process,
+ CoreMask coreMask,
+ CancellationToken cancellationToken = default);
+
+ Task ApplyCoreSelectionAsync(
+ ProcessModel process,
+ IReadOnlyList boolMask,
+ string selectionReason,
+ CancellationToken cancellationToken = default);
+ }
+
+ public sealed class ProcessAffinityApplyCoordinator : IProcessAffinityApplyCoordinator
+ {
+ private readonly IAffinityApplyService affinityApplyService;
+ private readonly ICpuTopologyProvider? cpuTopologyProvider;
+ private readonly CpuSelectionMigrationService cpuSelectionMigrationService;
+ private readonly ILogger logger;
+
+ public ProcessAffinityApplyCoordinator(
+ IAffinityApplyService affinityApplyService,
+ ICpuTopologyProvider? cpuTopologyProvider,
+ CpuSelectionMigrationService cpuSelectionMigrationService,
+ ILogger logger)
+ {
+ this.affinityApplyService = affinityApplyService ?? throw new ArgumentNullException(nameof(affinityApplyService));
+ this.cpuTopologyProvider = cpuTopologyProvider;
+ this.cpuSelectionMigrationService = cpuSelectionMigrationService ?? throw new ArgumentNullException(nameof(cpuSelectionMigrationService));
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public Task ApplyCoreMaskAsync(
+ ProcessModel process,
+ CoreMask coreMask,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(coreMask);
+
+ if (HasSelectionPayload(coreMask.CpuSelection))
+ {
+ return this.affinityApplyService.ApplyAsync(process, coreMask.CpuSelection!);
+ }
+
+ return this.ApplyCoreSelectionAsync(
+ process,
+ coreMask.BoolMask.ToList(),
+ $"Manual Process tab mask '{coreMask.Name}'",
+ cancellationToken);
+ }
+
+ public async Task ApplyCoreSelectionAsync(
+ ProcessModel process,
+ IReadOnlyList boolMask,
+ string selectionReason,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(process);
+ ArgumentNullException.ThrowIfNull(boolMask);
+
+ if (boolMask.Count == 0 || !boolMask.Any(selected => selected))
+ {
+ return AffinityApplyResult.Failed(
+ AffinityApplyErrorCodes.InvalidSelection,
+ ProcessOperationUserMessages.InvalidTopology,
+ "Manual CPU selection is empty.",
+ isInvalidTopology: true,
+ failureReason: AffinityApplyFailureReason.InvalidMask);
+ }
+
+ var migratedSelection = await this.TryMigrateToCpuSelectionAsync(
+ boolMask,
+ selectionReason,
+ cancellationToken).ConfigureAwait(false);
+ if (migratedSelection != null)
+ {
+ return await this.affinityApplyService.ApplyAsync(process, migratedSelection).ConfigureAwait(false);
+ }
+
+ if (!TryBuildSafeLegacyMask(boolMask, out var legacyMask, out var legacyFailure))
+ {
+ return legacyFailure;
+ }
+
+ return await this.affinityApplyService.ApplyAsync(process, legacyMask).ConfigureAwait(false);
+ }
+
+ private async Task TryMigrateToCpuSelectionAsync(
+ IReadOnlyList boolMask,
+ string selectionReason,
+ CancellationToken cancellationToken)
+ {
+ if (this.cpuTopologyProvider == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ var topology = await this.cpuTopologyProvider.GetTopologySnapshotAsync(cancellationToken).ConfigureAwait(false);
+ var migrated = this.cpuSelectionMigrationService.MigrateFromLegacyCoreMask(boolMask, topology);
+ if (!HasSelectionPayload(migrated.Selection))
+ {
+ return null;
+ }
+
+ return migrated.Selection with
+ {
+ Metadata = migrated.Selection.Metadata with
+ {
+ SelectionReason = string.IsNullOrWhiteSpace(selectionReason)
+ ? migrated.Selection.Metadata.SelectionReason
+ : selectionReason,
+ },
+ };
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ this.logger.LogDebug(ex, "Could not migrate manual Process tab CPU selection to CpuSelection");
+ return null;
+ }
+ }
+
+ private static bool HasSelectionPayload(CpuSelection? selection) =>
+ selection != null &&
+ (selection.CpuSetIds.Count > 0 || selection.LogicalProcessors.Count > 0);
+
+ private static bool TryBuildSafeLegacyMask(
+ IReadOnlyList boolMask,
+ out long legacyMask,
+ out AffinityApplyResult failure)
+ {
+ legacyMask = 0;
+ failure = default!;
+
+ if (boolMask.Count > 64)
+ {
+ failure = AffinityApplyResult.Failed(
+ AffinityApplyErrorCodes.LegacyFallbackUnsafe,
+ ProcessOperationUserMessages.LegacyFallbackBlocked,
+ "Manual CPU selection exceeds the legacy single-group 64-bit affinity mask.",
+ isLegacyFallbackBlocked: true,
+ failureReason: AffinityApplyFailureReason.InvalidMask);
+ return false;
+ }
+
+ for (var bit = 0; bit < boolMask.Count; bit++)
+ {
+ if (boolMask[bit])
+ {
+ legacyMask |= 1L << bit;
+ }
+ }
+
+ if (legacyMask == 0)
+ {
+ failure = AffinityApplyResult.Failed(
+ AffinityApplyErrorCodes.InvalidSelection,
+ ProcessOperationUserMessages.InvalidTopology,
+ "Manual CPU selection does not contain any enabled CPUs.",
+ isInvalidTopology: true,
+ failureReason: AffinityApplyFailureReason.InvalidMask);
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/Services/ServiceConfiguration.cs b/Services/ServiceConfiguration.cs
index 5e6c007..2b86b48 100644
--- a/Services/ServiceConfiguration.cs
+++ b/Services/ServiceConfiguration.cs
@@ -106,6 +106,7 @@ private static IServiceCollection ConfigureCoreSystemServices(this IServiceColle
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton(ProcessMemoryPriorityNativeApi.Instance);
services.AddSingleton();
services.AddSingleton();
diff --git a/Tests/ThreadPilot.Core.Tests/ProcessAffinityApplyCoordinatorTests.cs b/Tests/ThreadPilot.Core.Tests/ProcessAffinityApplyCoordinatorTests.cs
new file mode 100644
index 0000000..4c72b7e
--- /dev/null
+++ b/Tests/ThreadPilot.Core.Tests/ProcessAffinityApplyCoordinatorTests.cs
@@ -0,0 +1,274 @@
+namespace ThreadPilot.Core.Tests
+{
+ using Microsoft.Extensions.Logging.Abstractions;
+ using ThreadPilot.Models;
+ using ThreadPilot.Services;
+
+ public sealed class ProcessAffinityApplyCoordinatorTests
+ {
+ [Fact]
+ public async Task ApplyCoreMaskAsync_WithCpuSelection_UsesCpuSelectionPath()
+ {
+ var process = CreateProcess();
+ var selection = new CpuSelection
+ {
+ LogicalProcessors = [new ProcessorRef(0, 0, 0), new ProcessorRef(0, 2, 2)],
+ GlobalLogicalProcessorIndexes = [0, 2],
+ };
+ var mask = CreateMask([true, false, true]);
+ mask.CpuSelection = selection;
+ var affinity = new RecordingAffinityApplyService();
+ var coordinator = CreateCoordinator(affinity);
+
+ var result = await coordinator.ApplyCoreMaskAsync(process, mask);
+
+ Assert.True(result.Success);
+ Assert.Equal(1, affinity.CpuSelectionApplyCalls);
+ Assert.Equal(0, affinity.LegacyApplyCalls);
+ Assert.Same(selection, affinity.LastSelection);
+ }
+
+ [Fact]
+ public async Task ApplyCoreMaskAsync_WithCpu64Selection_DoesNotUseLegacyMaskOrAliasCpu0()
+ {
+ var process = CreateProcess();
+ var cpu64 = new ProcessorRef(1, 0, 64);
+ var mask = CreateMask(Enumerable.Range(0, 65).Select(index => index == 64).ToList());
+ mask.CpuSelection = new CpuSelection
+ {
+ LogicalProcessors = [cpu64],
+ GlobalLogicalProcessorIndexes = [64],
+ };
+ var affinity = new RecordingAffinityApplyService();
+ var coordinator = CreateCoordinator(affinity);
+
+ var result = await coordinator.ApplyCoreMaskAsync(process, mask);
+
+ Assert.True(result.Success);
+ Assert.Equal(0, affinity.LegacyApplyCalls);
+ var applied = Assert.Single(affinity.LastSelection!.LogicalProcessors);
+ Assert.Equal(cpu64, applied);
+ Assert.DoesNotContain(affinity.LastSelection.LogicalProcessors, processor => processor.GlobalIndex == 0);
+ }
+
+ [Fact]
+ public async Task ApplyCoreMaskAsync_WithMultiGroupSelection_DoesNotUseLegacyMask()
+ {
+ var process = CreateProcess();
+ var selection = new CpuSelection
+ {
+ LogicalProcessors = [new ProcessorRef(0, 0, 0), new ProcessorRef(1, 0, 64)],
+ GlobalLogicalProcessorIndexes = [0, 64],
+ };
+ var mask = CreateMask(Enumerable.Repeat(true, 65).ToList());
+ mask.CpuSelection = selection;
+ var affinity = new RecordingAffinityApplyService();
+ var coordinator = CreateCoordinator(affinity);
+
+ var result = await coordinator.ApplyCoreMaskAsync(process, mask);
+
+ Assert.True(result.Success);
+ Assert.Equal(1, affinity.CpuSelectionApplyCalls);
+ Assert.Equal(0, affinity.LegacyApplyCalls);
+ }
+
+ [Fact]
+ public async Task ApplyCoreMaskAsync_WithoutCpuSelectionAndWithoutTopology_UsesLegacyForSingleGroupMask()
+ {
+ var process = CreateProcess();
+ var affinity = new RecordingAffinityApplyService();
+ var coordinator = CreateCoordinator(affinity, topologyProvider: null);
+
+ var result = await coordinator.ApplyCoreMaskAsync(process, CreateMask([true, false, true]));
+
+ Assert.True(result.Success);
+ Assert.Equal(1, affinity.LegacyApplyCalls);
+ Assert.Equal(0b101, affinity.LastLegacyMask);
+ Assert.Equal(0, affinity.CpuSelectionApplyCalls);
+ }
+
+ [Fact]
+ public async Task ApplyCoreMaskAsync_WithoutCpuSelection_MigratesToCpuSelectionWhenTopologyIsAvailable()
+ {
+ var process = CreateProcess();
+ var topology = CpuTopologySnapshot.Create(
+ [new ProcessorRef(0, 0, 0), new ProcessorRef(0, 1, 1), new ProcessorRef(0, 2, 2)]);
+ var affinity = new RecordingAffinityApplyService();
+ var coordinator = CreateCoordinator(affinity, new FakeCpuTopologyProvider(topology));
+
+ var result = await coordinator.ApplyCoreMaskAsync(process, CreateMask([true, false, true]));
+
+ Assert.True(result.Success);
+ Assert.Equal(1, affinity.CpuSelectionApplyCalls);
+ Assert.Equal(0, affinity.LegacyApplyCalls);
+ Assert.Equal([0, 2], affinity.LastSelection!.GlobalLogicalProcessorIndexes);
+ }
+
+ [Fact]
+ public async Task ApplyCoreMaskAsync_WithCpu64BoolMaskAndTopology_UsesCpuSelectionPath()
+ {
+ var process = CreateProcess();
+ var processors = Enumerable.Range(0, 65)
+ .Select(index => index < 64
+ ? new ProcessorRef(0, (byte)index, index)
+ : new ProcessorRef(1, 0, index))
+ .ToList();
+ var topology = CpuTopologySnapshot.Create(processors);
+ var boolMask = Enumerable.Range(0, 65).Select(index => index == 64).ToList();
+ var affinity = new RecordingAffinityApplyService();
+ var coordinator = CreateCoordinator(affinity, new FakeCpuTopologyProvider(topology));
+
+ var result = await coordinator.ApplyCoreMaskAsync(process, CreateMask(boolMask));
+
+ Assert.True(result.Success);
+ Assert.Equal(1, affinity.CpuSelectionApplyCalls);
+ Assert.Equal(0, affinity.LegacyApplyCalls);
+ var applied = Assert.Single(affinity.LastSelection!.LogicalProcessors);
+ Assert.Equal(new ProcessorRef(1, 0, 64), applied);
+ Assert.DoesNotContain(affinity.LastSelection.LogicalProcessors, processor => processor.GlobalIndex == 0);
+ }
+
+ [Fact]
+ public async Task ApplyCoreMaskAsync_WhenTopologyUnavailableAndMaskIsUnsafe_BlocksLegacyFallback()
+ {
+ var process = CreateProcess();
+ var boolMask = Enumerable.Range(0, 65).Select(index => index == 64).ToList();
+ var affinity = new RecordingAffinityApplyService();
+ var coordinator = CreateCoordinator(affinity, topologyProvider: null);
+
+ var result = await coordinator.ApplyCoreMaskAsync(process, CreateMask(boolMask));
+
+ Assert.False(result.Success);
+ Assert.Equal(AffinityApplyErrorCodes.LegacyFallbackUnsafe, result.ErrorCode);
+ Assert.Equal(ProcessOperationUserMessages.LegacyFallbackBlocked, result.UserMessage);
+ Assert.Equal(0, affinity.LegacyApplyCalls);
+ Assert.Equal(0, affinity.CpuSelectionApplyCalls);
+ }
+
+ [Fact]
+ public async Task ApplyCoreMaskAsync_WhenCpuSelectionAccessDenied_ReturnsSafeAccessDeniedMessage()
+ {
+ var process = CreateProcess();
+ var affinity = new RecordingAffinityApplyService
+ {
+ CpuSelectionResult = AffinityApplyResult.Failed(
+ AffinityApplyErrorCodes.AccessDenied,
+ ProcessOperationUserMessages.AccessDenied,
+ "Access is denied.",
+ isAccessDenied: true),
+ };
+ var mask = CreateMask([true]);
+ mask.CpuSelection = new CpuSelection
+ {
+ LogicalProcessors = [new ProcessorRef(0, 0, 0)],
+ GlobalLogicalProcessorIndexes = [0],
+ };
+ var coordinator = CreateCoordinator(affinity);
+
+ var result = await coordinator.ApplyCoreMaskAsync(process, mask);
+
+ Assert.False(result.Success);
+ Assert.Equal(AffinityApplyErrorCodes.AccessDenied, result.ErrorCode);
+ Assert.Equal(ProcessOperationUserMessages.AccessDenied, result.UserMessage);
+ Assert.DoesNotContain("bypass", result.UserMessage, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task ApplyCoreMaskAsync_WhenCpuSelectionAntiCheatBlocked_ReturnsNoBypassMessage()
+ {
+ var process = CreateProcess();
+ var affinity = new RecordingAffinityApplyService
+ {
+ CpuSelectionResult = AffinityApplyResult.Failed(
+ AffinityApplyErrorCodes.AntiCheatOrProtectedProcessLikely,
+ ProcessOperationUserMessages.AntiCheatProtectedLikely,
+ "Protected process.",
+ isAccessDenied: true,
+ isAntiCheatLikely: true),
+ };
+ var mask = CreateMask([true]);
+ mask.CpuSelection = new CpuSelection
+ {
+ LogicalProcessors = [new ProcessorRef(0, 0, 0)],
+ GlobalLogicalProcessorIndexes = [0],
+ };
+ var coordinator = CreateCoordinator(affinity);
+
+ var result = await coordinator.ApplyCoreMaskAsync(process, mask);
+
+ Assert.False(result.Success);
+ Assert.Equal(AffinityApplyErrorCodes.AntiCheatOrProtectedProcessLikely, result.ErrorCode);
+ Assert.Equal(ProcessOperationUserMessages.AntiCheatProtectedLikely, result.UserMessage);
+ Assert.Contains("will not try to bypass", result.UserMessage, StringComparison.OrdinalIgnoreCase);
+ Assert.DoesNotContain("disable anti-cheat", result.UserMessage, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static ProcessAffinityApplyCoordinator CreateCoordinator(
+ RecordingAffinityApplyService affinity,
+ ICpuTopologyProvider? topologyProvider = null) =>
+ new(
+ affinity,
+ topologyProvider,
+ new CpuSelectionMigrationService(),
+ NullLogger.Instance);
+
+ private static ProcessModel CreateProcess() =>
+ new()
+ {
+ ProcessId = 42,
+ Name = "game.exe",
+ ProcessorAffinity = 1,
+ };
+
+ private static CoreMask CreateMask(IReadOnlyList boolMask)
+ {
+ var mask = new CoreMask { Name = "Manual" };
+ foreach (var bit in boolMask)
+ {
+ mask.BoolMask.Add(bit);
+ }
+
+ return mask;
+ }
+
+ private sealed class FakeCpuTopologyProvider(CpuTopologySnapshot snapshot) : ICpuTopologyProvider
+ {
+ public Task GetTopologySnapshotAsync(CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return Task.FromResult(snapshot);
+ }
+ }
+
+ private sealed class RecordingAffinityApplyService : IAffinityApplyService
+ {
+ public int LegacyApplyCalls { get; private set; }
+
+ public int CpuSelectionApplyCalls { get; private set; }
+
+ public long? LastLegacyMask { get; private set; }
+
+ public CpuSelection? LastSelection { get; private set; }
+
+ public AffinityApplyResult LegacyResult { get; init; } =
+ AffinityApplyResult.SucceededWithLegacyFallback(1, 1);
+
+ public AffinityApplyResult CpuSelectionResult { get; init; } =
+ AffinityApplyResult.SucceededWithCpuSets("CPU Sets applied.");
+
+ public Task ApplyAsync(ProcessModel process, long requestedMask)
+ {
+ this.LegacyApplyCalls++;
+ this.LastLegacyMask = requestedMask;
+ return Task.FromResult(this.LegacyResult);
+ }
+
+ public Task ApplyAsync(ProcessModel process, CpuSelection selection)
+ {
+ this.CpuSelectionApplyCalls++;
+ this.LastSelection = selection;
+ return Task.FromResult(this.CpuSelectionResult);
+ }
+ }
+ }
+}
diff --git a/Tests/ThreadPilot.Core.Tests/ProcessViewModelAffinityTests.cs b/Tests/ThreadPilot.Core.Tests/ProcessViewModelAffinityTests.cs
index 513ee29..a8c4a54 100644
--- a/Tests/ThreadPilot.Core.Tests/ProcessViewModelAffinityTests.cs
+++ b/Tests/ThreadPilot.Core.Tests/ProcessViewModelAffinityTests.cs
@@ -61,7 +61,39 @@ public async Task SelectingCoreMask_ReportsPendingAffinityWithoutChangingCurrent
Assert.Equal("Core mask staged. Use Apply Affinity to change Windows affinity.", viewModel.AffinityEditStateText);
}
+ [Fact]
+ public void ConstructorFallbackCoordinator_ReceivesTopologyProviderWhenProvided()
+ {
+ var processService = new Mock(MockBehavior.Loose);
+ var gameModeService = new Mock(MockBehavior.Loose);
+ var topologyProvider = new Mock(MockBehavior.Strict);
+
+ var viewModel = CreateViewModel(
+ processService.Object,
+ gameModeService.Object,
+ cpuTopologyProvider: topologyProvider.Object);
+
+ var coordinator = typeof(ProcessViewModel)
+ .GetField(
+ "processAffinityApplyCoordinator",
+ System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
+ .GetValue(viewModel);
+ var provider = typeof(ProcessAffinityApplyCoordinator)
+ .GetField(
+ "cpuTopologyProvider",
+ System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
+ .GetValue(coordinator);
+
+ Assert.Same(topologyProvider.Object, provider);
+ }
+
private static ProcessViewModel CreateViewModel(IProcessService processService, IGameModeService gameModeService)
+ => CreateViewModel(processService, gameModeService, cpuTopologyProvider: null);
+
+ private static ProcessViewModel CreateViewModel(
+ IProcessService processService,
+ IGameModeService gameModeService,
+ ICpuTopologyProvider? cpuTopologyProvider)
{
var virtualizedProcessService = new Mock(MockBehavior.Loose);
virtualizedProcessService.SetupProperty(
@@ -86,7 +118,8 @@ private static ProcessViewModel CreateViewModel(IProcessService processService,
systemTrayService.Object,
coreMaskService.Object,
associationService.Object,
- gameModeService);
+ gameModeService,
+ cpuTopologyProvider: cpuTopologyProvider);
}
private static CpuTopologyModel CreateTwoCoreTopology()
diff --git a/ViewModels/ProcessViewModel.Behaviors.partial.cs b/ViewModels/ProcessViewModel.Behaviors.partial.cs
index 47f1f33..b9b0e72 100644
--- a/ViewModels/ProcessViewModel.Behaviors.partial.cs
+++ b/ViewModels/ProcessViewModel.Behaviors.partial.cs
@@ -460,6 +460,14 @@ private long CalculateAffinityMask()
return selectedCores.Aggregate(0L, (mask, core) => mask | core.AffinityMask);
}
+ private List GetPendingCoreSelectionMask()
+ {
+ return this.CpuCores
+ .OrderBy(core => core.LogicalCoreId)
+ .Select(core => core.IsSelected)
+ .ToList();
+ }
+
private void UpdateAffinityDisplayState()
{
var currentMask = this.SelectedProcess?.ProcessorAffinity;
@@ -697,18 +705,25 @@ private async Task SetAffinity()
try
{
- var affinityMask = this.CalculateAffinityMask();
+ var pendingSelection = this.GetPendingCoreSelectionMask();
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
this.SetStatus($"Setting affinity for {selectedProcess.Name}...");
});
- var result = await this.affinityApplyService.ApplyAsync(selectedProcess, affinityMask);
+ var result = await this.processAffinityApplyCoordinator.ApplyCoreSelectionAsync(
+ selectedProcess,
+ pendingSelection,
+ "Manual Process tab CPU selection");
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
- this.UpdateCoreSelections(selectedProcess.ProcessorAffinity, true);
+ if (!result.UsedCpuSets)
+ {
+ this.UpdateCoreSelections(selectedProcess.ProcessorAffinity, true);
+ }
+
selectedProcess.ForceNotifyProcessorAffinityChanged();
this.OnPropertyChanged(nameof(this.SelectedProcess));
@@ -899,26 +914,20 @@ private async Task ApplyCoreMaskToProcessAsync(CoreMask mask)
"Applying mask '{MaskName}' to process {ProcessName} (PID: {ProcessId})",
mask.Name, selectedProcess.Name, selectedProcess.ProcessId);
- // Convert mask to affinity
- long affinity = mask.ToProcessorAffinity();
-
- if (affinity == 0)
- {
- this.Logger.LogWarning("Mask '{MaskName}' produces zero affinity, skipping", mask.Name);
- this.SetStatus("Invalid mask: no cores selected");
- return;
- }
-
// Disable Windows Game Mode for better CPU affinity control
// Game Mode can interfere with CPU Sets, particularly on AMD systems
await this.gameModeService.DisableGameModeForAffinityAsync();
- var result = await this.affinityApplyService.ApplyAsync(selectedProcess, affinity);
+ var result = await this.processAffinityApplyCoordinator.ApplyCoreMaskAsync(selectedProcess, mask);
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
selectedProcess.ForceNotifyProcessorAffinityChanged();
- this.UpdateCoreSelections(selectedProcess.ProcessorAffinity, true);
+ if (!result.UsedCpuSets)
+ {
+ this.UpdateCoreSelections(selectedProcess.ProcessorAffinity, true);
+ }
+
this.OnPropertyChanged(nameof(this.SelectedProcess));
});
@@ -988,21 +997,30 @@ private async Task QuickApplyAffinityAndPowerPlan()
try
{
+ var affinityAppliedWithCpuSets = false;
+
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
this.SetStatus($"Applying pending settings to {selectedProcess.Name}...");
});
// Apply CPU affinity
- var affinityMask = this.CalculateAffinityMask();
- if (affinityMask > 0)
+ var pendingSelection = this.GetPendingCoreSelectionMask();
+ if (pendingSelection.Any(selected => selected))
{
- var result = await this.affinityApplyService.ApplyAsync(selectedProcess, affinityMask);
+ var result = await this.processAffinityApplyCoordinator.ApplyCoreSelectionAsync(
+ selectedProcess,
+ pendingSelection,
+ "Manual Process tab quick apply CPU selection");
if (!result.Success)
{
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
- this.UpdateCoreSelections(selectedProcess.ProcessorAffinity, true);
+ if (!result.UsedCpuSets)
+ {
+ this.UpdateCoreSelections(selectedProcess.ProcessorAffinity, true);
+ }
+
selectedProcess.ForceNotifyProcessorAffinityChanged();
this.OnPropertyChanged(nameof(this.SelectedProcess));
this.SetStatus(result.Message, false);
@@ -1012,6 +1030,7 @@ await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
this.HasPendingAffinityEdits = false;
this.UpdateAffinityDisplayState();
+ affinityAppliedWithCpuSets = result.UsedCpuSets;
}
// Apply power plan if selected
@@ -1024,7 +1043,15 @@ await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
- this.UpdateCoreSelections(selectedProcess.ProcessorAffinity, true);
+ if (!affinityAppliedWithCpuSets)
+ {
+ this.UpdateCoreSelections(selectedProcess.ProcessorAffinity, true);
+ }
+ else
+ {
+ this.UpdateAffinityDisplayState();
+ }
+
selectedProcess.ForceNotifyProcessorAffinityChanged();
this.OnPropertyChanged(nameof(this.SelectedProcess));
});
@@ -1786,4 +1813,3 @@ protected override void OnDispose()
}
}
}
-
diff --git a/ViewModels/ProcessViewModel.cs b/ViewModels/ProcessViewModel.cs
index 6e0d754..ba40e8a 100644
--- a/ViewModels/ProcessViewModel.cs
+++ b/ViewModels/ProcessViewModel.cs
@@ -45,6 +45,7 @@ public partial class ProcessViewModel : BaseViewModel
private readonly IProcessPowerPlanAssociationService associationService;
private readonly IGameModeService gameModeService;
private readonly IAffinityApplyService affinityApplyService;
+ private readonly IProcessAffinityApplyCoordinator processAffinityApplyCoordinator;
private System.Timers.Timer? refreshTimer;
private bool isUiRefreshPaused;
private bool isProcessViewActive = true;
@@ -177,6 +178,8 @@ public ProcessViewModel(
IProcessPowerPlanAssociationService associationService,
IGameModeService gameModeService,
IAffinityApplyService? affinityApplyService = null,
+ IProcessAffinityApplyCoordinator? processAffinityApplyCoordinator = null,
+ ICpuTopologyProvider? cpuTopologyProvider = null,
IEnhancedLoggingService? enhancedLoggingService = null)
: base(logger, enhancedLoggingService)
{
@@ -194,6 +197,11 @@ public ProcessViewModel(
this.processService,
this.cpuTopologyService,
NullLogger.Instance);
+ this.processAffinityApplyCoordinator = processAffinityApplyCoordinator ?? new ProcessAffinityApplyCoordinator(
+ this.affinityApplyService,
+ cpuTopologyProvider,
+ new CpuSelectionMigrationService(),
+ NullLogger.Instance);
// Subscribe to topology detection events
this.cpuTopologyService.TopologyDetected += this.OnTopologyDetected;
@@ -217,4 +225,3 @@ public ProcessViewModel(
}
}
}
-