Skip to content

fix(libkernel): allocate the Windows thread-local slot on first use - #6956

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
jradic:fix/libkernel-thread-local-slot-first-use
Oct 17, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
jradic:fix/libkernel-thread-local-slot-first-use

Conversation

@jradic

@jradic jradic commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

What

Setting APS5_TRACE_TIMERS=1 crashed every title at startup on Windows with Cannot bind native thread state.

The timer self-test in Time.cpp runs during libkernel's static init. WindowsThreadLocal<T>::slot is a template static, so its init order isn't guaranteed, and here it hadn't run yet. The slot was still 0, so FlsSetValue failed.

The slot is now a function-local static, so it gets allocated on first use regardless of init order. Also added guest_nanosleep_timer_self_test, which reruns guest_nanosleep_tests with APS5_TRACE_TIMERS=1 (Windows only). #6658 adds a test right after the same guest_nanosleep block; no functional overlap.

Tested

Windows 11, WinLibs GCC 15.2.0, Release.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

🤖 Generated with Claude Code

https://claude.ai/code/session_01KwsvKtVsZDLefysUM9m2QS

WindowsThreadLocal<T>::slot was a template static, so its dynamic
initialization was unordered against the timer self-test in Time.cpp.
With APS5_TRACE_TIMERS set, the self-test ran first, used slot 0 and
aborted every title with "Cannot bind native thread state". The slot is
now a function-local static. guest_nanosleep_timer_self_test runs
guest_nanosleep_tests with APS5_TRACE_TIMERS=1 on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwsvKtVsZDLefysUM9m2QS
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 4972 core/libs/prx/libkernel/CMakeLists.txt
PR 5021 core/libs/prx/libkernel/CMakeLists.txt
PR 6044 core/libs/prx/libkernel/CMakeLists.txt
PR 6222 core/libs/prx/libkernel/CMakeLists.txt
PR 6287 core/libs/prx/libkernel/CMakeLists.txt
PR 6487 core/libs/prx/libkernel/CMakeLists.txt
PR 6884 core/libs/prx/libkernel/CMakeLists.txt

@jradic

jradic commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Checked these by hand: none change the same lines. #6658 adds a test right after the same guest_nanosleep block, so whichever lands second needs a trivial rebase.

@karimibrahim07 karimibrahim07 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows check of head 75d29f5b3 merged onto main e3a8e4430 (clean merge). Windows 11 Pro (26300), Ryzen 5 3600, WinLibs GCC 15.2.0 r7, Ninja Release.

  • guest_nanosleep and the new guest_nanosleep_timer_self_test pass, --repeat until-fail:3 (about 7 s each).
  • Negative control: with main's WindowsThreadLocal.hpp and the new test kept, guest_nanosleep_timer_self_test fails with what(): Cannot bind native thread state (0xc0000409), the same error as in the description.

Code: a function-local static is the right fix. Dynamic initialization of a class template's inline static member is unordered, so any static initializer in libkernel that reaches Get() can see a zero slot. libc's HostThreadLocal.hpp already uses the same pattern (static const DWORD slot = FlsAlloc(...) inside the function), so the two thread-local helpers now match. The other WindowsThreadLocal users (TimedWait.cpp, twice, Tsd.cpp, Thread.cpp) only call Get(), so they all benefit and none depends on the old member. Destroy calling slotIndex() is safe: the slot is necessarily initialized by the time FLS calls it. I have no requests.

AI-assisted: yes. I ran the build, the tests and the negative control above.

@screamm

screamm commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Cross-validation on Windows 11 Pro 26220, NVIDIA RTX 4070 (596.21), MinGW GCC 15.2, PR head 75d29f5b3, built together with #7005 (disjoint files) on main 5f61879a9 (clean merge). Before-runs used a build of 3b10c7ef0; the libkernel Pthread/Time sources are identical between the two.

  • Full build and libs: pass.
  • Negative control on the main build: APS5_TRACE_TIMERS=1 with guest_nanosleep fails 3 of 3, exit 0xc0000409, terminate called after throwing an instance of 'std::runtime_error' / what(): Cannot bind native thread state, after the first [timers] line; without the variable it passes. No source-revert rebuild was done, the main build served as the control.
  • guest_nanosleep_timer_self_test, --repeat until-fail:3: 3 of 3 pass on an idle host (7.2 s each). On a busy host (other builds running) it took 21 s and 25 s and then hit its own 30 s limit twice, so the limit is tight under load; a larger TIMEOUT would avoid that. guest_nanosleep 3 of 3.
  • Titles with APS5_TRACE_TIMERS=1, relinked, 60 s limit:
    • ccleste PPSA59322: main aborts at 12.0 s with exit 0xC0000409 (terminate called after throwing an instance of 'std::runtime_error', what(): Cannot bind native thread state, FATAL: abort() (SIGABRT)) after one [timers] line. With this PR it runs until the kill (3323 frames, no stop line) and prints all 7 [timers] lines.
    • ScreenTester5 PPSA69420: main aborts at 10.7 s with the same message and exit code. With this PR it runs until the kill (2 frames presented, as in our earlier runs without the variable) and prints all 7 [timers] lines.
  • ctest -j8 --timeout 120 -E '^agc_driver_|^video_out_flip': 855 of 877 pass, 2 skipped, 20 timeouts on a host with other builds running; all 20 pass on serial reruns (conventions/tooling tests and four libkernel/relinker tests, 0.6 to 55 s each). The new test passed in the suite.
  • tools/check_conventions.py --base origin/main and git diff --check: pass.

Not checked: Linux/macOS (the change is Windows-only), a revert-and-rebuild of the fix, the other open PRs that touch libkernel/CMakeLists.txt.

Written with Claude Code assistance (validation by a subagent, reviewed in this session).

@oneandonlydean oneandonlydean self-assigned this Oct 17, 2026
@oneandonlydean
oneandonlydean merged commit aa819ac into boykopovar:main Oct 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants