WIP: fix(host): the core kept for the replay is one the process may run on #522

Draft
HumanBean wants to merge 1 commit from HumanBean/Longshot:fix/reserved-core-affinity into main
Owner

Why

When the game is kept to some of the PC's processors (taskset, a launcher's affinity, a container), the core Longshot keeps for the replay could lie outside them. The replay then ran on a processor the game was not meant to use, beside whatever else was there.

What changed

  • host: hos_reserved_cpus()'s fallback in host/hos_thread.c counts only the cores whose logical processors are all in the process's affinity mask, and keeps the last of those (none under 8 allowed logical processors, as before). With every processor allowed nothing changes. The hybrid path (last_fastest_core) is as it was, and HOS_RESERVE_CORE_MASK still overrides.
  • tests: tests/test_reserved_core.py checks the fallback's condition in the source.

It follows #445 (the replay on the reserved core on Linux too), whose measurements this skewed: the correction is in #445 (comment).

Flags and settings

None new. HOS_RESERVE_CORE_MASK=<hex> (existing) still names the core by hand.

Measurements

A fix, shown by the log line and the frames that failed before; not a performance change on a PC where the game may use every processor (the reserved core is the same one there).

The change was built and run as this commit's patch applied to the local test merge 318acabe (the same hos_reserved_cpus code; the hunk applied 10 lines lower), not as a build of this branch alone. Before and after, sessions kept to CPUs 0-5 and 12-17:

Hardware: RTX 3090, NVIDIA 620.70.06, Ryzen 9 7900X, 32 GB, CachyOS (Linux 7.3.3)
Scene: Castle Town Market, the sessions of #445's measurements, kept to CPUs 0-5 and 12-17 with sched_setaffinity
Method: the game's log and RX_VK_FRAME_LOG frame times
How to reproduce: taskset -c 0-5,12-17 ./longshot on a CPU with more cores than that, a build running on the others; read the log's reserved core mask line and the worst frame

local merge @ 318acabe, reserved core as chosen the same binary, HOS_RESERVE_CORE_MASK=20020 (where the fix now puts it by itself)
the log's reserved core reserved core mask (fallback): 0x800800 (CPUs 11, 23: outside the session's) replays run on logical processors 0x20020 while they replay (CPUs 5, 17: inside)
where the replay ran on a 13th core when the other processors were idle, sharing one with a build when they were not on one of the session's own 12

What this did to measurements is in #445 (comment): with the pin outside the set, standing in the Market measured +9 %; with it inside, +4.5 %. Some of those sessions also had frames of several seconds; they came with heavy swapping and also occurred with the core inside the set, so they are not shown to be this and are not claimed here.

With the fix, the same kind of session and no override:

before (local merge @ 318acabe)
[hos host] vk: reserved core mask (fallback): 0x800800
after (318acabe + this commit's change), no HOS_RESERVE_CORE_MASK
[hos host] vk: reserved core mask (fallback): 0x20020
[hos sead::MainThread] vk: replays run on logical processors 0x20020 while they replay (RX_VK_REPLAY_PIN=0: off)

0x20020 is CPUs 5 and 17, the last core of the session's own set.

Testing

Tested: in the game on Linux (RTX 3090, NVIDIA 620.70.06, Ryzen 9 7900X, CachyOS): one session kept to CPUs 0-5 and 12-17, built from the local test merge 318acabe with this commit's change applied; the log shows the reserved core inside the set (above), the game reached the Market and ran the route. The game-free checks were not run locally (CI runs them).

Not tested: a build of this branch alone. A session with every processor allowed on the fixed build (the code keeps the last core as before when no processor is excluded; read, not run). Windows (the same code path: GetProcessAffinityMask and GetLogicalProcessorInformation). CPUs with performance and efficiency cores (their path is unchanged). A PC with more than 64 logical processors (only the first group's mask is read, as before).

Needs other testers: None

Screenshots

Nothing on screen.

Risk and rollback

On a PC where the game may run on every processor the choice is the same as before. A process kept to fewer than 8 logical processors now has no reserved core where it had one outside its set before; that is the intent. Reverting the commit restores the old choice; HOS_RESERVE_CORE_MASK and HOS_RESERVE_CORE=0 override it either way.

Reviewer notes

The whole change is the affinity mask read and one condition in the loop over the cores.

Issues

None

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above). Not run locally: waiting for CI.
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. A fix, shown by the log line before and after (CONTRIBUTING.md, "Measurements"), on the local test merge with and without the change.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. Linux: built here (in the test merge); Windows: waiting for CI.

🤖 Generated with Claude Code

https://claude.ai/code/session_017vw8e34avQ5cTCWow16MXn

## Why When the game is kept to some of the PC's processors (taskset, a launcher's affinity, a container), the core Longshot keeps for the replay could lie outside them. The replay then ran on a processor the game was not meant to use, beside whatever else was there. ## What changed - host: `hos_reserved_cpus()`'s fallback in `host/hos_thread.c` counts only the cores whose logical processors are all in the process's affinity mask, and keeps the last of those (none under 8 allowed logical processors, as before). With every processor allowed nothing changes. The hybrid path (`last_fastest_core`) is as it was, and `HOS_RESERVE_CORE_MASK` still overrides. - tests: `tests/test_reserved_core.py` checks the fallback's condition in the source. It follows https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/445 (the replay on the reserved core on Linux too), whose measurements this skewed: the correction is in https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/445#issuecomment-18710. ## Flags and settings None new. `HOS_RESERVE_CORE_MASK=<hex>` (existing) still names the core by hand. ## Measurements A fix, shown by the log line and the frames that failed before; not a performance change on a PC where the game may use every processor (the reserved core is the same one there). The change was built and run as this commit's patch applied to the local test merge 318acabe (the same `hos_reserved_cpus` code; the hunk applied 10 lines lower), not as a build of this branch alone. Before and after, sessions kept to CPUs 0-5 and 12-17: Hardware: RTX 3090, NVIDIA 620.70.06, Ryzen 9 7900X, 32 GB, CachyOS (Linux 7.3.3) Scene: Castle Town Market, the sessions of https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/445's measurements, kept to CPUs 0-5 and 12-17 with `sched_setaffinity` Method: the game's log and `RX_VK_FRAME_LOG` frame times How to reproduce: `taskset -c 0-5,12-17 ./longshot` on a CPU with more cores than that, a build running on the others; read the log's `reserved core mask` line and the worst frame | | local merge @ 318acabe, reserved core as chosen | the same binary, `HOS_RESERVE_CORE_MASK=20020` (where the fix now puts it by itself) | |---|---|---| | the log's reserved core | `reserved core mask (fallback): 0x800800` (CPUs 11, 23: outside the session's) | `replays run on logical processors 0x20020 while they replay` (CPUs 5, 17: inside) | | where the replay ran | on a 13th core when the other processors were idle, sharing one with a build when they were not | on one of the session's own 12 | What this did to measurements is in https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/445#issuecomment-18710: with the pin outside the set, standing in the Market measured +9 %; with it inside, +4.5 %. Some of those sessions also had frames of several seconds; they came with heavy swapping and also occurred with the core inside the set, so they are not shown to be this and are not claimed here. With the fix, the same kind of session and no override: ``` before (local merge @ 318acabe) [hos host] vk: reserved core mask (fallback): 0x800800 after (318acabe + this commit's change), no HOS_RESERVE_CORE_MASK [hos host] vk: reserved core mask (fallback): 0x20020 [hos sead::MainThread] vk: replays run on logical processors 0x20020 while they replay (RX_VK_REPLAY_PIN=0: off) ``` 0x20020 is CPUs 5 and 17, the last core of the session's own set. ## Testing **Tested:** in the game on Linux (RTX 3090, NVIDIA 620.70.06, Ryzen 9 7900X, CachyOS): one session kept to CPUs 0-5 and 12-17, built from the local test merge 318acabe with this commit's change applied; the log shows the reserved core inside the set (above), the game reached the Market and ran the route. The game-free checks were not run locally (CI runs them). **Not tested:** a build of this branch alone. A session with every processor allowed on the fixed build (the code keeps the last core as before when no processor is excluded; read, not run). Windows (the same code path: `GetProcessAffinityMask` and `GetLogicalProcessorInformation`). CPUs with performance and efficiency cores (their path is unchanged). A PC with more than 64 logical processors (only the first group's mask is read, as before). **Needs other testers:** None ## Screenshots Nothing on screen. ## Risk and rollback On a PC where the game may run on every processor the choice is the same as before. A process kept to fewer than 8 logical processors now has no reserved core where it had one outside its set before; that is the intent. Reverting the commit restores the old choice; `HOS_RESERVE_CORE_MASK` and `HOS_RESERVE_CORE=0` override it either way. ## Reviewer notes The whole change is the affinity mask read and one condition in the loop over the cores. ## Issues None ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [ ] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). Not run locally: waiting for CI. - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. A fix, shown by the log line before and after (CONTRIBUTING.md, "Measurements"), on the local test merge with and without the change. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. Linux: built here (in the test merge); Windows: waiting for CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017vw8e34avQ5cTCWow16MXn
fix(host): the core kept for the replay is one the process may run on
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 6s
CI / Conventional Commit title (pull_request) Successful in 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 11s
Third-party parts / As their notes say (pull_request) Successful in 31s
53d2c2bd6c
hos_reserved_cpus' fallback took the last core of every logical processor the PC has. A process kept to some of
them (taskset, a launcher's affinity) then had its replay pinned outside its set, which a thread may leave: the
replay thread (RX_VK_ASYNC) and every synchronous replay under RX_VK_REPLAY_PIN ran beside whatever else was there.

Seen on a Ryzen 9 7900X, Linux: sessions kept to CPUs 0-5,12-17 with sched_setaffinity logged
"reserved core mask (fallback): 0x800800" (CPUs 11 and 23, the other CCD) and replayed there. With builds running on
that CCD the replay thread was starved: frames of 2-8 s ("slow present: 4818.7 ms", "vcores: ... ran without a
core after 4713.8 ms"); with none it had a 13th core the session was not meant to have, so timings of anything that
moves work onto that core were off either way. With HOS_RESERVE_CORE_MASK=20020 (CPUs 5 and 17, inside the set) the
same sessions had no such frames (worst 42 ms).

Now the fallback counts only the cores whose logical processors are all in the process's affinity mask, and keeps the
last of those (none under 8 allowed logical processors, as before). With every processor allowed nothing changes.
The hybrid path (last_fastest_core) is as it was. HOS_RESERVE_CORE_MASK still overrides.

Not built and not run: a host change needs a game build. tests/test_reserved_core.py checks the fallback's condition
in the source.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Claude-Session: https://claude.ai/code/session_017vw8e34avQ5cTCWow16MXn
HumanBean force-pushed fix/reserved-core-affinity from 53d2c2bd6c
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 6s
CI / Conventional Commit title (pull_request) Successful in 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 11s
Third-party parts / As their notes say (pull_request) Successful in 31s
to 63976e479c
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 14s
CI / Workflow lint (actionlint) (pull_request) Successful in 21s
Third-party parts / As their notes say (pull_request) Successful in 31s
PR description / Description follows the template (pull_request_target) Successful in 15s
2026-11-14 17:43:05 +00:00
Compare
Author
Owner

The commit's message and the description no longer attribute the multi-second frames of the test sessions to this: they also occurred with the core inside the set and came with heavy swapping. What is shown is where the replay ran (the log lines) and what that did to the timings of #445.

The commit's message and the description no longer attribute the multi-second frames of the test sessions to this: they also occurred with the core inside the set and came with heavy swapping. What is shown is where the replay ran (the log lines) and what that did to the timings of https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/445.
Author
Owner

Built and run: with the change, a session kept to CPUs 0-5 and 12-17 and no override logs reserved core mask (fallback): 0x20020 (CPUs 5 and 17, inside the set) where it logged 0x800800 before. Built as this commit's patch on the local test merge 318acabe, not as this branch alone; the description says so.

Built and run: with the change, a session kept to CPUs 0-5 and 12-17 and no override logs `reserved core mask (fallback): 0x20020` (CPUs 5 and 17, inside the set) where it logged 0x800800 before. Built as this commit's patch on the local test merge 318acabe, not as this branch alone; the description says so.
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 14s
CI / Workflow lint (actionlint) (pull_request) Successful in 21s
Third-party parts / As their notes say (pull_request) Successful in 31s
PR description / Description follows the template (pull_request_target) Successful in 15s
This pull request is marked as a work in progress.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/reserved-core-affinity:HumanBean-fix/reserved-core-affinity'
git switch 'HumanBean-fix/reserved-core-affinity'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!522
No description provided.