linux: the process affinity mask is the process's, not the asking thread's #1430

Merged
leonardochappuis merged 1 commit from fix/linux-process-affinity into main 2026-11-10 16:16:14 +00:00
leonardochappuis commented 2026-11-10 01:58:39 +00:00 (Migrated from github.com)

What

On Linux, GetProcessAffinityMask (host/posix/win32_posix.c) returned sched_getaffinity(0): the calling thread's mask, since Linux has no process-level mask apart from its threads'. hos_thread_pin uses it to decide whether the reserved core is available for the Presentation Thread (render_thread_on_reserved, 2026-11-08). By then the asking game thread has itself been pinned off the reserved core, so the "process" mask had no reserved core in it, the test fell through to the else branch, and the Presentation Thread was pinned with every other game thread. The feature never applied on Linux, and its threads: Presentation Thread on the reserved core log line never appeared.

The mask is now captured once in a constructor, before main, when nothing is pinned yet, which matches what Windows reports.

Seen

Ryzen 9 9950X3D (16 cores, 32 logical processors), v2.0.11 Linux build, default switches. Every game thread including Presentation Thread had Cpus_allowed_list 0-14,16-30; the reserved core (15+31) held only the mkw_vk display thread. The log had no threads: line.

With HOS_PIN='Presentation=80008000' as a stand-in for the fix, the Presentation Thread ran on 15,31 next to mkw_vk display, the layout the pin intends.

Checked

  • clang -fsyntax-only -Wall -Wextra on the file: clean.
  • A standalone program with the same constructor: after pinning the main thread to one CPU, the old way reports 0x8, the captured mask still 0xffffffff.
  • Not yet rebuilt the full game with the change (a complete lift on this machine takes hours); the HOS_PIN run above exercised the same final affinity.

Both copies of win32_posix.c (the game's and hookshot_vk.so's) get the constructor; only the game's copy feeds hos_thread_pin, the .so's serves GetSystemInfo.

🤖 Generated with Claude Code

## What On Linux, `GetProcessAffinityMask` (host/posix/win32_posix.c) returned `sched_getaffinity(0)`: the **calling thread's** mask, since Linux has no process-level mask apart from its threads'. `hos_thread_pin` uses it to decide whether the reserved core is available for the Presentation Thread (`render_thread_on_reserved`, 2026-11-08). By then the asking game thread has itself been pinned off the reserved core, so the "process" mask had no reserved core in it, the test fell through to the else branch, and the Presentation Thread was pinned with every other game thread. The feature never applied on Linux, and its `threads: Presentation Thread on the reserved core` log line never appeared. The mask is now captured once in a constructor, before `main`, when nothing is pinned yet, which matches what Windows reports. ## Seen Ryzen 9 9950X3D (16 cores, 32 logical processors), v2.0.11 Linux build, default switches. Every game thread including `Presentation Thread` had `Cpus_allowed_list 0-14,16-30`; the reserved core (15+31) held only the `mkw_vk display` thread. The log had no `threads:` line. With `HOS_PIN='Presentation=80008000'` as a stand-in for the fix, the Presentation Thread ran on `15,31` next to `mkw_vk display`, the layout the pin intends. ## Checked - `clang -fsyntax-only -Wall -Wextra` on the file: clean. - A standalone program with the same constructor: after pinning the main thread to one CPU, the old way reports `0x8`, the captured mask still `0xffffffff`. - Not yet rebuilt the full game with the change (a complete lift on this machine takes hours); the `HOS_PIN` run above exercised the same final affinity. Both copies of win32_posix.c (the game's and hookshot_vk.so's) get the constructor; only the game's copy feeds `hos_thread_pin`, the .so's serves `GetSystemInfo`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
hckrman101 (Migrated from github.com) approved these changes 2026-11-10 04:17:33 +00:00
hckrman101 (Migrated from github.com) left a comment

Looks right to me. sched_getaffinity(0) is per-thread on Linux (pid 0 means the calling tid), so once hos_thread_pin has moved the asking thread off the reserved core, proc & ~reserved can never contain it and the Presentation Thread falls through to the else branch. Capturing the mask once in a constructor, before any thread is pinned, gives hos_thread_pin the same "process" mask Windows reports from GetProcessAffinityMask. It still honours a taskset or cgroup restriction applied at launch. The only other caller is GetSystemInfo (dwActiveProcessorMask), which gets the launch mask too, also what Windows does. The constructor in the hookshot_vk.so copy is harmless, as you say.

Nit: the lazy fallback (if (!process_affinity) init_process_affinity()) re-reads the calling thread's mask, which is the old bug, just only when the constructor failed. That's so unlikely that I'd leave it, or default the mask to all CPUs instead. A thread-safety fix isn't needed either, since the write is idempotent.

No test covers this, which is fair, since it needs real affinity and a pinned thread. No CI checks are reported on the branch. I didn't build it.

Looks right to me. sched_getaffinity(0) is per-thread on Linux (pid 0 means the calling tid), so once hos_thread_pin has moved the asking thread off the reserved core, proc & ~reserved can never contain it and the Presentation Thread falls through to the else branch. Capturing the mask once in a constructor, before any thread is pinned, gives hos_thread_pin the same "process" mask Windows reports from GetProcessAffinityMask. It still honours a taskset or cgroup restriction applied at launch. The only other caller is GetSystemInfo (dwActiveProcessorMask), which gets the launch mask too, also what Windows does. The constructor in the hookshot_vk.so copy is harmless, as you say. Nit: the lazy fallback (`if (!process_affinity) init_process_affinity()`) re-reads the calling thread's mask, which is the old bug, just only when the constructor failed. That's so unlikely that I'd leave it, or default the mask to all CPUs instead. A thread-safety fix isn't needed either, since the write is idempotent. No test covers this, which is fair, since it needs real affinity and a pinned thread. No CI checks are reported on the branch. I didn't build it.
Commenting is not possible because the repository is archived.
No description provided.