fix(vcores): honour mask 0 and keep the guest's core cache correct #327

Open
DuckNintendo wants to merge 3 commits from DuckNintendo/Longshot:fix/vcore-mask0-cache into main
First-time contributor

Why

Mask 0 is the guest's "no affinity" value, but the host treats it as one core: a mask-0
thread cannot use another core even when the core it started on is busy. This change makes
mask 0 mean every host core, keeps the guest's cached core as the first choice, and rewrites
the guest's TLS core cell whenever the thread is placed elsewhere.

What changed

  • host/hos_thread.c: hos_thread_affinity() stores mask 0 instead of ignoring it. vc_allowed() treats mask 0 as "every host core" rather than pinning to one.
  • host/hos_thread.c: vc_pref_core() and vc_free_for() prefer the core in the guest's TLS cell, then the thread's last core, then its ideal/lowest core, and request a remask when the cached core is busy.
  • host/hos_thread.c: vc_note_mask_change() and vc_yield() force a move when a new mask excludes the held core, even with no waiter.
  • host/hos_thread.c: vc_take()/vc_sync_tls() write the guest's core cell on every take: core + 1 for a 0/single-bit mask, 0 for a multi-bit mask, and on a same-core mask change. The core slot is discovered from the guest's own AllocateTlsSlot call, not assumed.
  • tests/test_vcore_mask0.py: new host-only fixture for the above.
  • tests/test_vcore_escape.py: stubs for the new core-cache helpers.

Flags and settings

None. No switch, menu row or ini key is added or changed.

Measurements

main: 16047f55
this PR: fix/vcore-mask0-cache @ 86b51d71

Hardware: x86-64 host, Vulkan. These are CPU scheduling counters; no GPU path is involved.
Scene: HyruleField_Child at a saved spot, default settings, 1280x720, 60 fps cap.
Method: main, PR, main, PR; 10 s warm-up and 60 s sampling per run; the same save, settings
and movement pattern. FPS and draws are sampled once a second; the scheduling counters are
cumulative from process start and identical for both. The raw values of the two runs are in
the row labels.
How to reproduce: load the saved spot and hold a run for 60 s. The counter instrumentation
is in the closed testing-only PR #412 (do not merge), which contains this PR's head plus
the counters: #412
It is not part of this PR.

main @ 16047f55 this PR @ 86b51d71
FPS median (runs 59.9, 59.1 / 59.2, 60.1) 59.5 59.65
draws median (runs 2148, 2103 / 2053, 1989) 2125.5 2021
takes median 2,935,053 2,958,206
mask-0 takes median (runs 176, 164 / 160, 227) 170 193.5
mask-0 taken off the cached core (runs 2, 2 / 104, 154) 2 129
mask-0 cell wrong after the take (runs 2, 2 / 0, 0) 2 0

Rebase: the branch was rebased through 0caf0618 onto 5e072fc8 (head 96107450). The
new commits are Vulkan/actor-motion changes; they do not touch the mask-0/TLS paths
measured here (vc_allowed, vc_pref_core, vc_take, vc_sync_tls). The counters still
describe this code, and the scene remains cap-bound.

Main pins a mask-0 thread to one core, so it takes a core other than the guest's cached
core only twice in 60 s and the cell after the take is wrong twice. This PR uses the free
cores: 129 such takes in the median run, and the cell after the take is never wrong because
vc_take() rewrites it. FPS is cap-bound at 60 in both and the draw counts are comparable:
this is not a frame-rate win, it is the cache maintenance that lets mask 0 use the other
cores.

Testing

Tested: python -I -B tests/run_all.py --jobs 8 on the rebased head: 497 passed,
2 failed, 63 skipped. The failures are tests/test_module_ids.py and
tests/test_pad_layout.py; both also fail on untouched 5e072fc8. The new tests/test_vcore_mask0.py and the updated
tests/test_vcore_escape.py pass; the touched host files compile with the build's flags.
Four Hyrule Field runs (main, PR, main, PR) at the saved spot; no crash; every run stayed
on HyruleField_Child, and the counters are above. The no-save title-idle (100 s) → New
Game → intro route was also run twice per build with the same counters: both reached
Kokiri Forest; main's mask-0 off-cache takes were 1 and 1, with 1 wrong cell after the
take each; this PR's were 83 and 97, with 0 wrong cells each. The same route was run
twice more per build without the control bridge (HOS_INPUT_SCRIPT); all four runs
reached Kokiri Forest as well.

Not tested: no longer route than the saved spot; no scene where the cached core is
heavily contended; no uncapped frame rate or higher resolution, so whether the extra
mask-0 placements help is not shown. The real nn::os::Mutex + second-thread SetMask
case belongs to the separate lock-held change, not this PR.

Needs other testers:

  • CPU-limited systems (Steam Deck, 4-core x86, low-TDP laptops): compare main and this PR
    in Hyrule Field, Kokiri Forest and the Market for 5-10 minutes, including transitions and
    the pause menu; look for hangs, crashes or wrong per-core data and compare the frame
    rate, expecting no hang or crash and no worse frame rate (T-5HRS). The request is open
    but needs the testing wanted label before testers can claim it.

Screenshots

Nothing on screen.

Risk and rollback

Mask-0 threads move between cores more than before (129 off-cache takes in the median run
against 2). If the cell sync were wrong, the guest would use per-core data from the wrong
core. The fixture covers the cell values and the scene run never left a wrong cell after a
take. Reverting the commit restores the pinning; there is no flag.

Reviewer notes

Start with vc_pref_core(), vc_free_for(), vc_take()/vc_sync_tls() and
vc_note_mask_change() in host/hos_thread.c; the fixture names each case. The lock-held
deferral is a separate follow-up and is not part of this PR.

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).
  • 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.
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • 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.
## Why Mask 0 is the guest's "no affinity" value, but the host treats it as one core: a mask-0 thread cannot use another core even when the core it started on is busy. This change makes mask 0 mean every host core, keeps the guest's cached core as the first choice, and rewrites the guest's TLS core cell whenever the thread is placed elsewhere. ## What changed - `host/hos_thread.c`: `hos_thread_affinity()` stores mask 0 instead of ignoring it. `vc_allowed()` treats mask 0 as "every host core" rather than pinning to one. - `host/hos_thread.c`: `vc_pref_core()` and `vc_free_for()` prefer the core in the guest's TLS cell, then the thread's last core, then its ideal/lowest core, and request a remask when the cached core is busy. - `host/hos_thread.c`: `vc_note_mask_change()` and `vc_yield()` force a move when a new mask excludes the held core, even with no waiter. - `host/hos_thread.c`: `vc_take()`/`vc_sync_tls()` write the guest's core cell on every take: `core + 1` for a 0/single-bit mask, 0 for a multi-bit mask, and on a same-core mask change. The core slot is discovered from the guest's own `AllocateTlsSlot` call, not assumed. - `tests/test_vcore_mask0.py`: new host-only fixture for the above. - `tests/test_vcore_escape.py`: stubs for the new core-cache helpers. ## Flags and settings None. No switch, menu row or ini key is added or changed. ## Measurements main: 16047f55 this PR: fix/vcore-mask0-cache @ 86b51d71 Hardware: x86-64 host, Vulkan. These are CPU scheduling counters; no GPU path is involved. Scene: `HyruleField_Child` at a saved spot, default settings, 1280x720, 60 fps cap. Method: main, PR, main, PR; 10 s warm-up and 60 s sampling per run; the same save, settings and movement pattern. FPS and draws are sampled once a second; the scheduling counters are cumulative from process start and identical for both. The raw values of the two runs are in the row labels. How to reproduce: load the saved spot and hold a run for 60 s. The counter instrumentation is in the closed testing-only PR #412 (do not merge), which contains this PR's head plus the counters: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/412 It is not part of this PR. | | main @ 16047f55 | this PR @ 86b51d71 | |---|---|---| | FPS median (runs 59.9, 59.1 / 59.2, 60.1) | 59.5 | 59.65 | | draws median (runs 2148, 2103 / 2053, 1989) | 2125.5 | 2021 | | takes median | 2,935,053 | 2,958,206 | | mask-0 takes median (runs 176, 164 / 160, 227) | 170 | 193.5 | | mask-0 taken off the cached core (runs 2, 2 / 104, 154) | 2 | 129 | | mask-0 cell wrong after the take (runs 2, 2 / 0, 0) | 2 | 0 | Rebase: the branch was rebased through `0caf0618` onto `5e072fc8` (head `96107450`). The new commits are Vulkan/actor-motion changes; they do not touch the mask-0/TLS paths measured here (`vc_allowed`, `vc_pref_core`, `vc_take`, `vc_sync_tls`). The counters still describe this code, and the scene remains cap-bound. Main pins a mask-0 thread to one core, so it takes a core other than the guest's cached core only twice in 60 s and the cell after the take is wrong twice. This PR uses the free cores: 129 such takes in the median run, and the cell after the take is never wrong because `vc_take()` rewrites it. FPS is cap-bound at 60 in both and the draw counts are comparable: this is not a frame-rate win, it is the cache maintenance that lets mask 0 use the other cores. ## Testing **Tested:** `python -I -B tests/run_all.py --jobs 8` on the rebased head: 497 passed, 2 failed, 63 skipped. The failures are `tests/test_module_ids.py` and `tests/test_pad_layout.py`; both also fail on untouched `5e072fc8`. The new `tests/test_vcore_mask0.py` and the updated `tests/test_vcore_escape.py` pass; the touched host files compile with the build's flags. Four Hyrule Field runs (main, PR, main, PR) at the saved spot; no crash; every run stayed on `HyruleField_Child`, and the counters are above. The no-save title-idle (100 s) → New Game → intro route was also run twice per build with the same counters: both reached Kokiri Forest; main's mask-0 off-cache takes were 1 and 1, with 1 wrong cell after the take each; this PR's were 83 and 97, with 0 wrong cells each. The same route was run twice more per build without the control bridge (`HOS_INPUT_SCRIPT`); all four runs reached Kokiri Forest as well. **Not tested:** no longer route than the saved spot; no scene where the cached core is heavily contended; no uncapped frame rate or higher resolution, so whether the extra mask-0 placements help is not shown. The real `nn::os::Mutex` + second-thread `SetMask` case belongs to the separate lock-held change, not this PR. **Needs other testers:** - CPU-limited systems (Steam Deck, 4-core x86, low-TDP laptops): compare main and this PR in Hyrule Field, Kokiri Forest and the Market for 5-10 minutes, including transitions and the pause menu; look for hangs, crashes or wrong per-core data and compare the frame rate, expecting no hang or crash and no worse frame rate (T-5HRS). The request is open but needs the `testing wanted` label before testers can claim it. ## Screenshots Nothing on screen. ## Risk and rollback Mask-0 threads move between cores more than before (129 off-cache takes in the median run against 2). If the cell sync were wrong, the guest would use per-core data from the wrong core. The fixture covers the cell values and the scene run never left a wrong cell after a take. Reverting the commit restores the pinning; there is no flag. ## Reviewer notes Start with `vc_pref_core()`, `vc_free_for()`, `vc_take()`/`vc_sync_tls()` and `vc_note_mask_change()` in `host/hos_thread.c`; the fixture names each case. The lock-held deferral is a separate follow-up and is not part of this PR. ## Issues None. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [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. - [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. - [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.
DuckNintendo force-pushed fix/vcore-mask0-cache from e29caaa75e
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
to 86b51d7174
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 3s
Website / Website on Cloudflare (pull_request_target) Has been skipped
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
2026-11-14 00:37:00 +00:00
Compare
Author
First-time contributor

Hyrule Field measurement added to the description. Two interleaved runs each, 10 s warm-up and 60 s sampling at the same saved spot:

  • main: 2 mask-0 takes off the cached core, 2 wrong cells after the take.
  • this PR: 129 off-cache takes (median), 0 wrong cells after the take.
  • FPS median 59.5 on main and 59.65 here; the scene is cap-bound at 60.

So this is the cache maintenance that lets mask 0 use the other cores, not a frame-rate win. The earlier 90 s/two-core counts were from this PR's own policy, not a main-vs-PR bug count; the description now uses the scene comparison.

Hyrule Field measurement added to the description. Two interleaved runs each, 10 s warm-up and 60 s sampling at the same saved spot: - main: 2 mask-0 takes off the cached core, 2 wrong cells after the take. - this PR: 129 off-cache takes (median), 0 wrong cells after the take. - FPS median 59.5 on main and 59.65 here; the scene is cap-bound at 60. So this is the cache maintenance that lets mask 0 use the other cores, not a frame-rate win. The earlier 90 s/two-core counts were from this PR's own policy, not a main-vs-PR bug count; the description now uses the scene comparison.
DuckNintendo force-pushed fix/vcore-mask0-cache from 86b51d7174
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 3s
Website / Website on Cloudflare (pull_request_target) Has been skipped
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
to f55b29d6ec
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 15s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
2026-11-14 10:53:31 +00:00
Compare
Author
First-time contributor

Rebased onto current main 0caf0618; head is f55b29d6. The suite on the rebased head is 497 passed / 2 failed / 63 skipped; both failures (test_module_ids.py, test_pad_layout.py) also fail on untouched 0caf0618. The Hyrule Field table was taken at 16047f55/86b51d71; the rebase changes the hos_thread_of current-thread fast path and condition-variable signaling, not the mask-0/TLS paths measured.

Rebased onto current `main` `0caf0618`; head is `f55b29d6`. The suite on the rebased head is 497 passed / 2 failed / 63 skipped; both failures (`test_module_ids.py`, `test_pad_layout.py`) also fail on untouched `0caf0618`. The Hyrule Field table was taken at `16047f55`/`86b51d71`; the rebase changes the `hos_thread_of` current-thread fast path and condition-variable signaling, not the mask-0/TLS paths measured.
Author
First-time contributor

Counters are in the closed testing-only PR #412 (do not merge): #412

It is #327's head plus the PERF counters and the report call. Closed so it cannot be merged; the diff stays available on the PR page.

Counters are in the closed testing-only PR #412 (do not merge): https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/412 It is #327's head plus the PERF counters and the report call. Closed so it cannot be merged; the diff stays available on the PR page.
DuckNintendo changed title from WIP: fix(vcores): honour mask 0 and keep the guest's core cache correct to fix(vcores): honour mask 0 and keep the guest's core cache correct 2026-11-14 10:55:39 +00:00
Author
First-time contributor

Additional no-save route with the counters: title idle 100 s, then New Game and the intro video, two runs per build.

  • main: mask-0 takes 66/100, 1/1 off the cached core, 1/1 wrong cell after the take.
  • this PR: 132/168 takes, 83/97 off-cache, 0/0 wrong cells after the take.
  • Both reached Kokiri Forest; no crash in 2/2 each.

Same pattern as Hyrule Field: main pins the thread, so its cell stays aligned; the PR uses the free cores and rewrites the cell at every take.

Additional no-save route with the counters: title idle 100 s, then New Game and the intro video, two runs per build. - main: mask-0 takes 66/100, 1/1 off the cached core, 1/1 wrong cell after the take. - this PR: 132/168 takes, 83/97 off-cache, 0/0 wrong cells after the take. - Both reached Kokiri Forest; no crash in 2/2 each. Same pattern as Hyrule Field: main pins the thread, so its cell stays aligned; the PR uses the free cores and rewrites the cell at every take.
Author
First-time contributor

Rebased through 0caf0618 onto current main 5e072fc8; head is 96107450. Suite on the rebased head: 497 passed / 2 failed / 63 skipped; both failures (test_module_ids.py, test_pad_layout.py) also fail on untouched 5e072fc8. The new main commits are Vulkan/actor-motion changes, not the mask-0/TLS paths.

Rebased through `0caf0618` onto current `main` `5e072fc8`; head is `96107450`. Suite on the rebased head: 497 passed / 2 failed / 63 skipped; both failures (`test_module_ids.py`, `test_pad_layout.py`) also fail on untouched `5e072fc8`. The new main commits are Vulkan/actor-motion changes, not the mask-0/TLS paths.
DuckNintendo force-pushed fix/vcore-mask0-cache from f55b29d6ec
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 15s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
to 96107450ac
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 5s
Website / Website on Cloudflare (pull_request_target) Has been skipped
2026-11-14 14:31:56 +00:00
Compare
Author
First-time contributor

Test request T-5HRS is open for CPU-limited systems: Steam Deck, 4-core x86 or a low-TDP laptop; Hyrule Field, Kokiri Forest and the Market, comparing main with this PR and looking for hangs, crashes, wrong per-core data and frame-rate regressions.

The request cannot be claimed yet: #327 is not from a collaborator and has no testing wanted label. Could a maintainer add that label? The token I have gets 403 when it tries.

Test request **T-5HRS** is open for CPU-limited systems: Steam Deck, 4-core x86 or a low-TDP laptop; Hyrule Field, Kokiri Forest and the Market, comparing main with this PR and looking for hangs, crashes, wrong per-core data and frame-rate regressions. The request cannot be claimed yet: #327 is not from a collaborator and has no `testing wanted` label. Could a maintainer add that label? The token I have gets 403 when it tries.
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 5s
Website / Website on Cloudflare (pull_request_target) Has been skipped
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/vcore-mask0-cache:DuckNintendo-fix/vcore-mask0-cache'
git switch 'DuckNintendo-fix/vcore-mask0-cache'
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!327
No description provided.