Vulkan: vk_draw.cpp's per-thread objects without the TLS init check on every use #1035
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1035
Loading…
Reference in a new issue
No description provided.
Delete branch "render-cpu-fixes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
With llvm-mingw, every use of a
thread_localwhose type has a constructor or destructor calls the file's TLS init function. That call saves registers, finds the thread's TLS block and checks a guard byte, however long ago the thread set the object up.vk_draw.cpphas twelve such objects (dw,pass,handle_hits,pushed,bank_cache, ...), used dozens of times per draw. Found with the sampling profiler (HOS_SAMPLE=Presentation+scripts/prof.py): ~2.4% of the replay thread.They are now
TLS_OBJECT(name, type).name()returns the calling thread's object through a plainthread_localpointer, which needs no init function. Each object is made on the thread's first use and deleted when the thread ends. Only the list that owns them has a destructor, and only a first use touches it. The change at the use sites is mechanical (dw.becomesdw().). Because the names are now functions, a use left unchanged fails to compile instead of quietly paying the check again.tests/test_tls_objects.pyfails if a namespace-scopethread_localinvk_draw.cpphas a container type, or a struct type of the file holding one.This PR first also read
RX_VK_TAAU_KEYSonce instead of on every dispatch. Main has since fixed that indf1e26d(the lookup after the stand-in's early returns), so that part is dropped in favour of main's.tests/test_taau_env_lookup.py's fixture now hasdwas a call, likevk_draw.cpp, and so dotest_handle_lookup.py,test_pass_hazard.pyandtest_push_state.py, which read the source.Measured on a Ryzen 9 3900X with an RTX 3080 on Windows 11, on main
8b2e1d3(v2.0.8-rc.1) with this branch merged in. Kokiri Forest after loading a save, headless, input timed in game frames (HOS_INPUT_FRAME_CLOCK=1), live numbers over presents 3120-5520, two runs each, in two rounds (the second in reverse order):On v2.0.5 the profile showed the init function gone (0.00% of the replay thread, from 2.38%).
python -I -B tests/run_all.pyon main8b2e1d3with all three of my open PRs merged: the same 12 failures as main itself on this machine (missing tools, and an older g++ that rejectsvolatilecompound assignment), and every other test passes.🤖 Generated with Claude Code
I don't see a correctness problem: the
::mkwvk::names resolve to the unnamed-namespace thread-locals, each reference is bound per call on the calling thread (the recursivebind_descriptorscall binds again), and thestatickeysis fine. My comments are about the approach and coverage rather than bugs.Same in
build_descriptors:dump_written.clear()/dump_inputs.clear()here run unconditionally through the wrapper even with dumping off, andbank_cacheviauniform_rangeis the hottest remaining TLS access in this function. The "thread-locals once" comment above is only half true as it stands.nit: the locals shadow the thread-locals with the same names (
dw,draw_sampled, andhandle_hitsat 1491), so if one of these bindings is ever removed the code still compiles against the thread-local and the slowdown comes back silently (and-Wshadowwarns on each). A distinct name likeDescWriter& w = dw;, or the struct above, makes it explicit. The three sites also spell it differently (auto&vs the full type).The underlying cost is that every namespace-scope
thread_localwith a non-trivial constructor/destructor goes through the TLS init wrapper on each use, and binding a reference at the top of three functions fixes just those three. Other hot users still pay it:uniform_range'sbank_cache(once per uniform binding per draw, ~1584),gfx_sampled = draw_sampledin the draw path (~4809),draw_hazards, and so on. The next profile will likely want the same patch in more places. Putting the per-thread descriptor state in one struct reached through a trivially initialisedthread_local Tls* tls = nullptr;(allocated on first use) would remove the wrapper for every user in one go. What do you think?pushedis a thread-local with vector members too, so this line still goes through the wrapper (twice on the fallback path that re-entersbind_descriptors), one line below thedwit just fixed.minor: this now reads the env once, but it still duplicates
taau_key'sRX_VK_TAAU_KEYSlookup with a different default (fd029a317a9a4b94here vs-there), and the static guard is checked before thetaau_keyearly return on every dispatch. One sharedtaau_keys()helper, read after the early return, would remove both.Agreed, and done in
04743e0. With the references, the TLS init function was still 1.06% of the replay thread (indraw,begin_pass,draw_hazards,dynamic_stateandtexture_view_of); with all twelve objects behindTLS_OBJECTit's 0.00%. Fps doesn't move measurably beyond the first version, since this part is ~0.2 ms a frame, but nothing pays it anymore, andtests/test_tls_objects.py(7033e84) fails if a new one appears.pushedisTLS_OBJECT(pushed, std::array<Pushed, 2>)now, like the rest.All of them are
TLS_OBJECTs now:dump_written,dump_sampled,dump_inputsandbank_cacheamong the twelve.The references are gone. The names are functions now, so a use left as it was doesn't compile, rather than silently going back through the wrapper.
Done in
fb50ab2: onetaau_keys(), read only aftertaau_keyhas matched. The stand-in's copy only fedtyped_upscale's log line, which now shows the list in effect.Nice, this addresses everything I raised. All twelve objects are behind
TLS_OBJECT, the old names are functions so a leftover use won't compile,pushed/dump_*/bank_cacheare covered, andtaau_keys()is read only after a match. I also checked first-use init (new T()zeroes the same way, theBankCachedefaults can't give a false hit), per-thread cleanup at thread exit, and that no object is reached from a different thread than before. Looks good.Non-blocking suggestions if you touch it again:
tests/test_tls_objects.pyis a heuristic with some holes. Types from headers (Program,std::mutex,std::function<...>), aliases/auto,std::optional<Pass>, user-written destructors and multi-line declarations all pass it. Puttingconstiniton the remaining plain thread_locals, plusstatic_assert(std::is_trivially_destructible_v<T>), would get the compiler to enforce it on every build.reinterpret_cast<void**>(&slot)with*it->slot = nullptrwrites aT*through avoid*lvalue, which is technically a strict-aliasing violation. A type-erased deleter that takesT**and clears it ([](void* s){ T*& p = *static_cast<T**>(s); delete p; p = nullptr; }) avoids that.build_descriptors'dw()calls,end_pass'spass()uses), a localDescWriter& w = dw();at the top would save reloading and null-checking the slot on every use, without bringing back the shadowing.Measured this PR on Windows / NVIDIA, in case it helps: RTX 3070 8 GB, i7-14700KF, Windows 11, 32 GB.
Build: origin/main
8b2e1d3bwith this PR merged in (plus an unrelated audio change, #1063). Two hunks conflicted withcurrent main in
gpu/vk/vk_draw.cpp(the TAAU keys:getenv("RX_VK_TAAU_KEYS")againsttaau_keys(), anddw.imgsagainst
dw().imgs); I took this PR's side for both.Kokiri Forest, A/B in ABBA order (same save, same settings; 30 s standing, then 40 s walking; median fps):
This PR is ahead in all 6 comparisons: +4.3 fps standing and +5.6 walking on average. No flicker in either.
A tour of 26 debug-teleport destinations (#785's tools, 20 s each, one run per build, so noisier than the A/B
above): no device lost, no flicker. Overworld areas are mostly faster: Kokiri Forest +11.2, Hyrule Field +11.7, Zora's
River +8.9, Death Mountain Trail +7.0, Hyrule Castle +6.4, Kakariko +5.3. A few areas late in the tour were slower
(Spirit Temple -15.8, Forest Temple -5.5), but main's numbers for those came from a shorter tour that started fresh, so
I would not read much into them.
Main is merged in. Main's
df1e26dtook care of the TAAU key lookups, so I dropped my version of that and kept main's, with its test (its fixture now hasdwas a call). What's left is theTLS_OBJECTchange. Re-measured on main8b2e1d3(v2.0.8-rc.1): +1.7% standing and panning. Main is merged in again since (016d582: two statistics counters it removed sat on lines this branch had changed). Title and body are updated to match.