From 71b9954c28c3955c46411ebc682b83cec986d133 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Mon, 9 Nov 2026 13:50:17 +0530 Subject: [PATCH 1/2] Apply the 32-bit CPU list when the Wine build sets no _NET_WM_WOW64 --- .../powercontrol/ContainerAffinity.kt | 30 +++++++++++++++++++ .../winlator/core/Win32AppWorkarounds.java | 16 ++++++---- .../java/com/winlator/xserver/Window.java | 5 ++++ .../powercontrol/ContainerAffinityTest.kt | 21 +++++++++++++ 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/app/gamenative/powercontrol/ContainerAffinity.kt b/app/src/main/java/app/gamenative/powercontrol/ContainerAffinity.kt index dd3766b201..375a70c612 100644 --- a/app/src/main/java/app/gamenative/powercontrol/ContainerAffinity.kt +++ b/app/src/main/java/app/gamenative/powercontrol/ContainerAffinity.kt @@ -8,6 +8,7 @@ import com.winlator.winhandler.ProcessInfo import com.winlator.winhandler.WinHandler import timber.log.Timber import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors import java.util.concurrent.TimeUnit /** @@ -97,6 +98,35 @@ object ContainerAffinity { } } + private val windowProcessExecutor = Executors.newSingleThreadExecutor { Thread(it, "container-affinity-window") } + + /** + * X11 path for a Wine build that sets no _NET_WM_WOW64 on its windows (Proton 10 and later): + * the window cannot say whether its process is WoW64, so the winhandler's process list + * decides. Without that answer the process gets [mask], as before. + */ + fun applyToWindowProcess(winHandler: WinHandler, pid: Int, name: String, mask: Int, maskWoW64: Int) { + windowProcessExecutor.execute { + var processes: List? = null + for (attempt in 1..REPLY_ATTEMPTS) { + if (WinHandler.getActiveInstance() !== winHandler) return@execute + processes = listProcesses(winHandler) + if (processes != null) break + } + val info = processes?.firstOrNull { it.pid == pid } + val target = windowProcessMask(info, mask, maskWoW64) ?: return@execute + if (WinHandler.getActiveInstance() !== winHandler) return@execute + winHandler.setProcessAffinity(pid, target) + Timber.tag(TAG).i( + "Applied mask 0x${Integer.toHexString(target)} to $name (PID $pid, wow64=${info?.wow64Process ?: "unknown"})" + ) + } + } + + /** The mask for a window's process: the WoW64 list when the winhandler reports it WoW64. */ + internal fun windowProcessMask(info: ProcessInfo?, mask: Int, maskWoW64: Int): Int? = + (if (info?.wow64Process == true) maskWoW64 else mask).takeIf { it != 0 } + /** * The mask [info] should get, or null when the process is left alone. */ diff --git a/app/src/main/java/com/winlator/core/Win32AppWorkarounds.java b/app/src/main/java/com/winlator/core/Win32AppWorkarounds.java index ae4874e6b0..344fe457c8 100644 --- a/app/src/main/java/com/winlator/core/Win32AppWorkarounds.java +++ b/app/src/main/java/com/winlator/core/Win32AppWorkarounds.java @@ -1,5 +1,6 @@ package com.winlator.core; +import app.gamenative.powercontrol.ContainerAffinity; import app.gamenative.powercontrol.PowerManager; import com.winlator.core.envvars.EnvVars; import com.winlator.winhandler.WinHandler; @@ -95,7 +96,7 @@ public void applyStartupWorkarounds(String className) { applyWorkaround(workaround); } - private void setProcessAffinity(Window window, int processAffinity) { + private void setProcessAffinity(Window window) { int processId = window.getProcessId(); String className = window.getClassName(); WinHandler winHandler = this.xServer.getWinHandler(); @@ -109,6 +110,12 @@ private void setProcessAffinity(Window window, int processAffinity) { Timber.tag("PowerManager").i("Power control holds the affinity of %s, container CPU list not applied", className); return; } + if (!window.hasWoW64Property() && processId > 0) { + ContainerAffinity.INSTANCE.applyToWindowProcess(winHandler, processId, className, taskAffinityMask, taskAffinityMaskWoW64); + return; + } + int processAffinity = window.isWoW64() ? this.taskAffinityMaskWoW64 : this.taskAffinityMask; + if (processAffinity == 0) return; if (processId > 0) { winHandler.setProcessAffinity(processId, processAffinity); } else if (!className.isEmpty()) { @@ -142,11 +149,8 @@ public void applyWindowWorkarounds(Window window) { if (window.isRenderable() && !window.getClassName().isEmpty() && windowGroup == window.id) { canApplyProcessAffinity = true; } - if (canApplyProcessAffinity) { - int processAffinity = window.isWoW64() ? this.taskAffinityMaskWoW64 : this.taskAffinityMask; - if (processAffinity != 0) { - setProcessAffinity(window, processAffinity); - } + if (canApplyProcessAffinity && (this.taskAffinityMask != 0 || this.taskAffinityMaskWoW64 != 0)) { + setProcessAffinity(window); } } diff --git a/app/src/main/java/com/winlator/xserver/Window.java b/app/src/main/java/com/winlator/xserver/Window.java index 6cde3c279b..63e947c573 100644 --- a/app/src/main/java/com/winlator/xserver/Window.java +++ b/app/src/main/java/com/winlator/xserver/Window.java @@ -171,6 +171,11 @@ public boolean isWoW64() { return property != null && property.data.get(0) == 1; } + /** False when the Wine build never sets _NET_WM_WOW64, as Proton 10 and later do not. */ + public boolean hasWoW64Property() { + return getProperty(Atom.getId("_NET_WM_WOW64")) != null; + } + public long getHandle() { Property property = getProperty(Atom.getId("_NET_WM_HWND")); return property != null ? property.getLong(0) : 0; diff --git a/app/src/test/java/app/gamenative/powercontrol/ContainerAffinityTest.kt b/app/src/test/java/app/gamenative/powercontrol/ContainerAffinityTest.kt index c2d81aec67..23d63ce2a8 100644 --- a/app/src/test/java/app/gamenative/powercontrol/ContainerAffinityTest.kt +++ b/app/src/test/java/app/gamenative/powercontrol/ContainerAffinityTest.kt @@ -54,6 +54,27 @@ class ContainerAffinityTest { assertNull(ContainerAffinity.targetMask(process("Game32.exe", wow64 = true), mask, 0, pinnedGame = null, powerControlOwnsGame = false)) } + @Test + fun windowProcessReportedWoW64GetsTheWoW64List() { + assertEquals(maskWoW64, ContainerAffinity.windowProcessMask(process("Borderlands2.exe", wow64 = true), mask, maskWoW64)) + } + + @Test + fun windowProcessReported64BitGetsTheMainList() { + assertEquals(mask, ContainerAffinity.windowProcessMask(process("Game.exe"), mask, maskWoW64)) + } + + @Test + fun windowProcessMissingFromTheListGetsTheMainList() { + assertEquals(mask, ContainerAffinity.windowProcessMask(null, mask, maskWoW64)) + } + + @Test + fun windowProcessWithAnEmptyMaskGetsNoRequest() { + assertNull(ContainerAffinity.windowProcessMask(process("Game32.exe", wow64 = true), mask, 0)) + assertNull(ContainerAffinity.windowProcessMask(process("Game.exe"), 0, maskWoW64)) + } + @Test fun reportedMaskComparesOnTheLowSixteenBits() { assertTrue(ContainerAffinity.sameMask(0xF0, 0xF0)) From 981df819418733a932910ee106d273f9d311409b Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Mon, 9 Nov 2026 14:05:24 +0530 Subject: [PATCH 2/2] Send the 32-bit CPU list only when it was set explicitly --- .../powercontrol/ContainerAffinity.kt | 26 +++++++++++++++---- .../powercontrol/ContainerAffinityTest.kt | 25 +++++++++++++----- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/app/gamenative/powercontrol/ContainerAffinity.kt b/app/src/main/java/app/gamenative/powercontrol/ContainerAffinity.kt index 375a70c612..15528f0814 100644 --- a/app/src/main/java/app/gamenative/powercontrol/ContainerAffinity.kt +++ b/app/src/main/java/app/gamenative/powercontrol/ContainerAffinity.kt @@ -3,6 +3,8 @@ package app.gamenative.powercontrol import app.gamenative.PluviaApp import app.gamenative.display.GameNativeDrm import app.gamenative.events.AndroidEvent +import com.winlator.container.Container +import com.winlator.core.ProcessHelper import com.winlator.winhandler.OnGetProcessInfoListener import com.winlator.winhandler.ProcessInfo import com.winlator.winhandler.WinHandler @@ -103,9 +105,15 @@ object ContainerAffinity { /** * X11 path for a Wine build that sets no _NET_WM_WOW64 on its windows (Proton 10 and later): * the window cannot say whether its process is WoW64, so the winhandler's process list - * decides. Without that answer the process gets [mask], as before. + * decides. The WoW64 list is only sent when it was set explicitly; with the untouched default, + * or without an answer from the winhandler, the process gets [mask], as before. */ fun applyToWindowProcess(winHandler: WinHandler, pid: Int, name: String, mask: Int, maskWoW64: Int) { + val defaultMaskWoW64 = ProcessHelper.getAffinityMask(Container.getFallbackCPUListWoW64()).toShort().toInt() + if (!isExplicitWoW64Mask(maskWoW64, defaultMaskWoW64)) { + if (mask != 0) winHandler.setProcessAffinity(pid, mask) + return + } windowProcessExecutor.execute { var processes: List? = null for (attempt in 1..REPLY_ATTEMPTS) { @@ -114,7 +122,7 @@ object ContainerAffinity { if (processes != null) break } val info = processes?.firstOrNull { it.pid == pid } - val target = windowProcessMask(info, mask, maskWoW64) ?: return@execute + val target = windowProcessMask(info, mask, maskWoW64, defaultMaskWoW64) ?: return@execute if (WinHandler.getActiveInstance() !== winHandler) return@execute winHandler.setProcessAffinity(pid, target) Timber.tag(TAG).i( @@ -123,9 +131,17 @@ object ContainerAffinity { } } - /** The mask for a window's process: the WoW64 list when the winhandler reports it WoW64. */ - internal fun windowProcessMask(info: ProcessInfo?, mask: Int, maskWoW64: Int): Int? = - (if (info?.wow64Process == true) maskWoW64 else mask).takeIf { it != 0 } + internal fun isExplicitWoW64Mask(maskWoW64: Int, defaultMaskWoW64: Int): Boolean = + maskWoW64 != 0 && maskWoW64 != defaultMaskWoW64 + + /** + * The mask for a window's process: the WoW64 list when the winhandler reports it WoW64 and + * the list differs from the default, else the main list. + */ + internal fun windowProcessMask(info: ProcessInfo?, mask: Int, maskWoW64: Int, defaultMaskWoW64: Int): Int? { + val wow64 = info?.wow64Process == true && isExplicitWoW64Mask(maskWoW64, defaultMaskWoW64) + return (if (wow64) maskWoW64 else mask).takeIf { it != 0 } + } /** * The mask [info] should get, or null when the process is left alone. diff --git a/app/src/test/java/app/gamenative/powercontrol/ContainerAffinityTest.kt b/app/src/test/java/app/gamenative/powercontrol/ContainerAffinityTest.kt index 23d63ce2a8..0b9f44d5e0 100644 --- a/app/src/test/java/app/gamenative/powercontrol/ContainerAffinityTest.kt +++ b/app/src/test/java/app/gamenative/powercontrol/ContainerAffinityTest.kt @@ -54,25 +54,38 @@ class ContainerAffinityTest { assertNull(ContainerAffinity.targetMask(process("Game32.exe", wow64 = true), mask, 0, pinnedGame = null, powerControlOwnsGame = false)) } + private val defaultMaskWoW64 = 0xF0 // upper half of 8 cores + + @Test + fun windowProcessWoW64WithAnExplicitListGetsTheWoW64List() { + assertEquals(maskWoW64, ContainerAffinity.windowProcessMask(process("Borderlands2.exe", wow64 = true), mask, maskWoW64, defaultMaskWoW64 = 0x0F)) + } + @Test - fun windowProcessReportedWoW64GetsTheWoW64List() { - assertEquals(maskWoW64, ContainerAffinity.windowProcessMask(process("Borderlands2.exe", wow64 = true), mask, maskWoW64)) + fun windowProcessWoW64WithTheDefaultListGetsTheMainList() { + assertEquals(0xFF, ContainerAffinity.windowProcessMask(process("Borderlands2.exe", wow64 = true), 0xFF, defaultMaskWoW64, defaultMaskWoW64)) } @Test fun windowProcessReported64BitGetsTheMainList() { - assertEquals(mask, ContainerAffinity.windowProcessMask(process("Game.exe"), mask, maskWoW64)) + assertEquals(mask, ContainerAffinity.windowProcessMask(process("Game.exe"), mask, maskWoW64, defaultMaskWoW64)) } @Test fun windowProcessMissingFromTheListGetsTheMainList() { - assertEquals(mask, ContainerAffinity.windowProcessMask(null, mask, maskWoW64)) + assertEquals(mask, ContainerAffinity.windowProcessMask(null, mask, maskWoW64, defaultMaskWoW64)) } @Test fun windowProcessWithAnEmptyMaskGetsNoRequest() { - assertNull(ContainerAffinity.windowProcessMask(process("Game32.exe", wow64 = true), mask, 0)) - assertNull(ContainerAffinity.windowProcessMask(process("Game.exe"), 0, maskWoW64)) + assertNull(ContainerAffinity.windowProcessMask(process("Game.exe"), 0, maskWoW64, defaultMaskWoW64)) + } + + @Test + fun onlyANonDefaultNonEmptyWoW64ListIsExplicit() { + assertTrue(ContainerAffinity.isExplicitWoW64Mask(0xC0, 0xF0)) + assertFalse(ContainerAffinity.isExplicitWoW64Mask(0xF0, 0xF0)) + assertFalse(ContainerAffinity.isExplicitWoW64Mask(0, 0xF0)) } @Test