WIP: perf(linux): the Vulkan back end's thread-local data in the static TLS block, through TLS descriptors #232
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!232
Loading…
Reference in a new issue
No description provided.
Delete branch "dukenheimer/Longshot:opus/static-tls"
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?
Why
On Linux the Vulkan back end (
longshot_vk.so) is loaded withdlopen, so everythread_localaccess in it went through__tls_get_addr, a call into the dynamic loader, thousands of times a frame on the replay's render thread. On a Steam Deck that call was 2.4% of the render thread, with more in the accessors around it. This puts the back end's thread-local data in the static TLS block, where an access is a constant offset.Measured on the Steam Deck alone: it works as designed and gains no fps (below): the frame there is set by the readback and GPU waits, not by the replay's TLS accesses. Recommendation: not worth merging as it is. The restart at every Linux start, the
GLIBC_TUNABLEShandling, the TLS descriptors and the link-time check are machinery for a gain nobody has measured, so I suggest closing it. The one part with a measured effect and no start-up behaviour is the accessor change (TLS_OBJECTalways inlined,tls_makecold: in a Deck profiledw()'s 5.1% of the render thread folded away and the frame's instructions fell 5%); if maintainers want it, it can go as a small PR of its own, on every platform and the default TLS model.What changed
nx2/build.pycompiles the back end with-mtls-dialect=gnu2(TLS descriptors;vk_tls_flags).RX_VK_TLS=gd: the default model, as before; another machine than x86-64 keeps the default model, with a line saying so. At the link,nx2/elf_tls.pyreads the linkedlongshot_vk.so'sPT_TLSand prints it; the build fails when the segment (rounded to its alignment) plus 8 KB kept for the drivers' own initial-exec TLS doesn't fitHOS_STATIC_TLS_ROOM, naming the fixes (the data on the heap throughTLS_OBJECT, a bigger room, orRX_VK_TLS=gd).static_tls_room()inhost/hos_main.c, first thing inmain: the game restarts itself once (execvof its own path, read from/proc/self/exe, so the process keeps the namelongshotand its PID) withglibc.rtld.optional_static_tls=98304added toGLIBC_TUNABLES, so the loader placeslongshot_vk.so's TLS in the static block and its descriptors resolve to a constant offset. Started again, it putsGLIBC_TUNABLESback as it was (HOS_STATIC_TLS_WAS) and drops its markers, so what the game starts (xdg-open, a browser) doesn't get the room on its threads; glibc read the tunable at the start already, and a Restart now (host/hos_relaunch.c, from/proc/self/environ) keeps it.HOS_STATIC_TLSfollows the switches' rule (env_value_off:0,false,off,no, empty) and is read fromlongshot_env.txt(orHOS_ENV_FILE's file) as well as the environment:env_file_peekreads that one line beforeload_longshot_envloads the file, the environment first as for every switch.static TLS:line once the log is open (started again with the room; or why not: the switch, aGLIBC_TUNABLESthat sets the tunable already, a failedexecvwith its error, aGLIBC_TUNABLEStoo long to add to), and at the renderer's startvk: TLS: <n> bytes, in the static TLS blockorNOT in the static TLS block: every thread_local access ... goes through glibc's dynamic path(tls_placeingpu/vk/vk_main.cpp: a thread_local's distance below the thread pointer, and the module'sPT_TLSfromdl_iterate_phdr).sampler_for's 128 KB per-thread table (gpu/vk/vk_draw.cpp) is on the heap, a namespace-scopeTLS_OBJECT(sampler_table, SamplerTable)freed at thread exit with the others: the back end's TLS segment goes from ~178 KB to 47,936 bytes (46.8 KB), which fits the room above.TLS_OBJECTaccessors (dw(),pass()and the rest,vk_draw.cpp) are always inlined, and their first-use allocation (tls_make) is out of line and cold. Inlined, thenewmade every accessor a full function that saved six registers around the TLS lookup, ~50 calls a draw; a Steam Deck profile haddw()alone at 5.1% of the render thread. This part helps whatever TLS model is used.tests/test_static_tls.py(below);tests/test_tls_objects.pychecks the accessor's shape;tests/test_sampler_admission.pybuildssampler_forwith its table;tests/display_startup_failure_test.cppgets astatic_tls_roomstub, since themainit cuts out now calls it.docs/development.mdhasHOS_STATIC_TLS(with the switches) andRX_VK_TLS(under Building).No translator file changes (
vk_shader.cpp,vk_bank.cpp,vk_state.h,vk_api.h,eden/,sirit/,compat/): the shader caches stay valid.How TLS descriptors fall back (from the review): when glibc has no room left, a TLSDESC module is placed in a block of its own and its descriptors take
_dl_tlsdesc_dynamic;dlopendoes not fail ("cannot allocate memory in static TLS block" is initial-exec's error, which this doesn't add). So the risk is silent slowness, never a game that won't start, and that path is slower than main's default model (hckrman101 measured TLS descriptors 8-14% slower on a PC in a CPU-only replay benchmark). Hence the build check and the two log lines.Flags and settings
HOS_STATIC_TLS(environment orlongshot_env.txt)0,false,off,no, empty): no restart for static TLS room; the back end's TLS then takes glibc's dynamic descriptor path, slower than mainRX_VK_TLS=gd(build)gdis main's baseline for an A/BBoth are in
docs/development.md.HOS_STATIC_TLS_DONEandHOS_STATIC_TLS_WASare the restart's own markers, set by the game for the process it starts and removed by it, not switches to set.HOS_ENV_FILEis the existing switch (another file thanlongshot_env.txt), read here too forHOS_STATIC_TLS.Measurements
Steam Deck OLED, this PR alone (Deck agent, 2026-11-13): no fps change.
main:
d964886+ #225 (the Steam Deck profile, which the Deck needs: plain main runs out of memory there), e298bd4this PR: opus/static-tls @
fc96d3e, merged on that base (a full Linux build)Hardware: Steam Deck OLED (AMD Custom GPU 0932, RADV VANGOGH, Mesa 26.2.0-devel), SteamOS 3.9.5, 16 GB (swapping in every run)
Scene: Kokiri Forest, the moving route (125 s, measured 30-110 s), the user's options (Deck profile: 2048x1152 output, FSR 3.1 ~50%, frame generation 2x), stock clocks, memory guard on
Method: one warm-up start each, then interleaved base, this PR, ..., this PR, base; fps from the Deck agent's frame log ("real": the game's frames); threads from
/proc/<pid>/statusevery 5 sHow to reproduce: the Deck agent's harness, data in
deck-s3(X232-, LB-x)d964886+ #225fc96d3eThe warm-up kept every program key (47,797: the stamp is unchanged). Real fps is within the run-to-run spread (about ±1). The static reserve costs no thread and no measurable memory.
The TLS segment, measured at the link:
longshot_vk.so: TLS segment 47936 bytes (aligned to 16): fits the 98304 bytes of static TLS room (42176 to spare, 8192 kept for drivers').Earlier, on a Hookshot-based build: render-thread
perfbefore and after the accessor change:dw()(5.1%) folded away into its callers (build_descriptors+dw()12.6% → 9.1% together), the TLS descriptor stub 3.4% → 2.1%, instructions a frame −5%.Testing
Tested:
python -I -B tests/run_all.py, Linux (Ubuntu 22.04 container, clang 23, the CPM Vulkan headers) and Windows (llvm-mingw): this head @3ccb48a, rebased on main5a8ae7e, fails exactly what main @5a8ae7efails: Linux 5 (test_cpp_preflight,test_dev_packs,test_pad_layout,test_prof_clock, the byte-compile without.git), Windows 4 (test_bake_portability,test_fpx_sticky,test_pad_layout,test_spawn_emit); 511 / 468 passed.tests/test_static_tls.pywith gcc 13 / g++ 13 (Ubuntu 24.04, glibc 2.39) and in the suite's container (glibc 2.35):vk_tls_flagson x86-64 / aarch64 /RX_VK_TLS=gd/ Windows;nx2/elf_tls.pyon ELF files made in the test (a segment that fits, one past the margin, one pushed out by its alignment, none, not ELF) and the link's call of it;static_tls_roomcut out and run withexecvstubbed in 16 cases (the switch's values from the environment, fromlongshot_env.txtwith a BOM, spaces and later lines, fromHOS_ENV_FILE, the environment over the file, a tunable already set, an empty and an existingGLIBC_TUNABLES, a failed exec, the game started again with and without aGLIBC_TUNABLESbefore it), each with its log note; andtls_placecut out into a module with a 48 KB TLS segment, built with-mtls-dialect=gnu2and dlopen'ed: "dynamic" without the room, "static" with it.fc96d3e(above, 2 interleaved pairs): the restart and the static block confirmed by the log, threads and RSS as main's.Not tested:
GLIBC_TUNABLES), Flatpak/AppImage starts.Needs other testers:
longshot, the log hasstatic TLS: started again ...andvk: TLS: ... in the static TLS block; fps this head against main (request to open)Screenshots
Nothing on screen.
Risk and rollback
optional_static_tlsthat is too small shows asvk: TLS: ... NOT in the static TLS block.pthread_createwithEINVAL. None is known; the Deck's thread count and RSS are among the measurements owed.execv), so a launcher waiting on it sees the same process.GLIBC_TUNABLESis put back once started again, so children don't inherit the room.HOS_STATIC_TLS=0disables the restart, a build withRX_VK_TLS=gdrestores main's model, and reverting the commits removes it all.Reviewer notes
Read
static_tls_roomandenv_file_peekinhost/hos_main.cfirst, thenvk_tls_flagsand the link's check innx2/build.pywithnx2/elf_tls.py, thentls_placeingpu/vk/vk_main.cpp, thenTLS_OBJECTandsampler_tableingpu/vk/vk_draw.cpp.Issues
None.
Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.docs/tested-configurations.mdfor each measured run.mainand passing here.tests/that reads changed source updated in this PR, not weakened.Thanks: the idea holds up. Moving
sampler_for's 128 KB table off the TLS segment is what makes static placement possible at all. The cold, out-of-linetls_makeis a clean fix whatever TLS model is used. And the restart is careful: it keeps the PID, uses the real path so the name stayslongshot, appends to an existingGLIBC_TUNABLESand puts it back if the exec fails. One thing worth knowing: TLS descriptors fail gracefully. When glibc has no room, a TLSDESC module goes to the dynamic path. It doesn't faildlopenwith "cannot allocate memory in static TLS block" (that error is for initial-exec TLS, which this PR doesn't add). So the risk is silent slowness, not a game that won't start. The points below follow from that.Must change
The fallback is slower than main, nothing says which path you got, and the planned A/B leaves main out. With
-mtls-dialect=gnu2always on, the module's TLS can miss the static block. That happens withHOS_STATIC_TLS=0, with aGLIBC_TUNABLESthat already names a smalleroptional_static_tls, when the exec fails, or once the TLS segment grows past the room. Each access then goes through_dl_tlsdesc_dynamic. That is the path hckrman101 measured 8 to 14% slower than the default model. Nothing is logged in any of these cases. Please:dl_iterate_phdr'sdlpi_tls_dataor the distance of athread_localfrom the thread pointer;Also, the A/B in the description is
HOS_STATIC_TLS=0against the default on the same build. That compares gnu2-dynamic with gnu2-static and leaves out main'sgdbuild, which is the real baseline. The measurement owed is this branch against main (or against anRX_VK_TLS=gdbuild) at named commits, with several interleaved runs on the Deck and on a Linux desktop.Nothing checks the TLS size against the room, at build time or at run time. "~48 KB" against a 96 KB
HOS_STATIC_TLS_ROOMis stated, not measured in the PR, and nothing stops it from growing. One newthread_localarray, like the 128 KB table this PR moves, would push the module back to the slow path with no warning.test_static_tls.pyonly checks thatsampler_for's table isn't a TLS array. Please havenx2/build.pyreadPT_TLS(p_memszand alignment) from the linkedlongshot_vk.so, print it, and fail or warn loudly when it doesn't fitHOS_STATIC_TLS_ROOMwith some margin. Then put the measured size in the description.HOS_STATIC_TLS=0inlongshot_env.txtdoes nothing, and only the exact value0turns it off.static_tls_roomruns beforeload_longshot_env(), so the switch works only from the real environment. That isn't how players set switches. It also compares withstrcmp(off, "0")rather than the project's one rule (env_value_off:false,off,no, empty). If the switch has to be read before the file, say so where the switch is documented. Useenv_value_offeither way."Rebased on current
main" is ticked but false.main@originhas moved on:08771847(#78) and1ef93e79(#54, which changeshost/hos_main.cinmemory_notice_text). There's no textual conflict, but rebase or untick the box.Should change
GLIBC_TUNABLESleaks to child processes. After the restart,GLIBC_TUNABLESandHOS_STATIC_TLS_DONEstay inenviron. So anything the game spawns (share_spawn'sxdg-open, and a browser it may start) also gets a 96 KB optional static TLS reserve on every thread. glibc reads tunables only at process start. You could restore the oldGLIBC_TUNABLESinenvirononce the restarted process is up.hos_relaunchreads/proc/self/environ, so it still sees the start-up value and a Restart now keeps the tunable.pthread_createwithEINVAL. Worth a line in Risk, and a look at the thread count and RSS on the Deck run.sampler_for'sstatic thread_local std::unique_ptr<Table>now costs a guard check and a__cxa_thread_atexitregistration, plus a second TLS access on each call where there used to be one. It also goes against the file's TLS_OBJECT convention, which avoids exactly this. Consider a namespace-scopeTLS_OBJECT(sampler_table, SamplerTable); the thread exit then frees it with the others.tests/test_tls_objects.pydoesn't catch function-local ones, by design.-mtls-dialect=gnu2on x86-64. The flag is x86-64 only (clang rejects it on aarch64). Linux builds are x86-64 today, anddeps.shpins LLVM 23, which accepts it. Still, aplatform.machine()check costs nothing and leaves a clear error if that ever changes.tests/display_startup_failure_test.cpp: the stub's line now ends withdriver_cache_folder's old comment (// (host/hos_main.c: the folder __GL_SHADER_DISK_CACHE_PATH names, made)), anddriver_cache_folderhas lost it.docs/development.mdstill needsHOS_STATIC_TLSandRX_VK_TLS(the box is unticked, which is correct).Evidence
There are no Deck numbers for this branch alone. 15.1 against 13.6 fps comes from a combined build, one run each, against a Hookshot-based build. The perf breakdown (descriptor stub 3.4% to 2.1%,
dw()folded away) supports the accessor change on its own merits. It doesn't show that static TLS beats main'sgd. Windows is affected only by the accessor attributes and the heap table. Both look right for clang/llvm-mingw, but they're untested in the game there, which the description already says.What I couldn't check: I built and ran nothing. The real
PT_TLSsize oflongshot_vk.sois unverified (no built.sohere), and so is how third-party libraries' threads behave with the bigger static TLS.@ -2593,0 +2595,4 @@// (longshot) on the heap, a thread's own: 128 KB of zeros in the module's TLS segment made it too big for the static TLS room// the game reserves (host/hos_main.c static_tls_room), and every thread_local of the back end went through __tls_get_addrstruct Table { Entry e[2][4096]{}; }; // (gen 0: never filled; the generation starts at 1)static thread_local std::unique_ptr<Table> table;A function-local thread_local with a destructor: each call now checks a guard and does a second TLS access, and the first one registers with
__cxa_thread_atexit. The file's TLS_OBJECT convention exists to avoid exactly this. Consider a namespace-scopeTLS_OBJECT(sampler_table, SamplerTable).@ -2056,0 +2060,4 @@module's ~48 KB in the static block, where its TLS descriptors (nx2/build.py, -mtls-dialect=gnu2) resolve to a constant offset.So the game starts itself again once, first of all, with the tunable added to GLIBC_TUNABLES. Kept as it is: HOS_STATIC_TLS=0, aGLIBC_TUNABLES that names the tunable already, or an exec that failed (the game goes on without it) */#define HOS_STATIC_TLS_ROOM 98304Nothing ties this to the module's real TLS size. Could
nx2/build.pyreadPT_TLSp_memsz and alignment from the linked longshot_vk.so, print it, and warn or fail when it doesn't fit here with a margin? Otherwise one newthread_localarray quietly undoes this PR.@ -2056,0 +2062,4 @@GLIBC_TUNABLES that names the tunable already, or an exec that failed (the game goes on without it) */#define HOS_STATIC_TLS_ROOM 98304static void static_tls_room(char **argv) {const char *off = getenv("HOS_STATIC_TLS");This runs before
load_longshot_env(), soHOS_STATIC_TLS=0in longshot_env.txt has no effect. Also,strcmp(off, "0")missesoff,false,noand empty; please useenv_value_off(gpu/vk/compat/longshot_env.h), the switches' one rule.@ -2056,0 +2065,4 @@const char *off = getenv("HOS_STATIC_TLS");if ((off && !strcmp(off, "0")) || env_on("HOS_STATIC_TLS_DONE")) return;const char *was = getenv("GLIBC_TUNABLES");if (was && strstr(was, "glibc.rtld.optional_static_tls")) return;A tunable that's already set (a launcher's or a user's smaller
optional_static_tls) is kept silently. With gnu2 always built in, the back end then runs on the dynamic descriptor path, which hckrman101 measured slower than main's default model. Please log this, and log once at back-end start whether the module's TLS actually landed in the static block (for exampledl_iterate_phdr'sdlpi_tls_dataagainst the thread pointer).@ -2056,0 +2079,4 @@const ssize_t len = readlink("/proc/self/exe", self, sizeof self - 1);const char *path = "/proc/self/exe";if (len > 0) self[len] = 0, path = self;execv(path, argv);After a successful restart,
GLIBC_TUNABLES(andHOS_STATIC_TLS_DONE) stay inenvironand are inherited byxdg-openand whatever it starts. glibc reads tunables only at start-up, so the restarted process could put the old value back.hos_relaunchreads /proc/self/environ, so Restart now would still keep the tunable.@ -1184,0 +1184,4 @@# was a call of __tls_get_addr: 2.4% of the Steam Deck's render thread, with the accessors around it more. A descriptor of a# module whose TLS the loader placed in the static block (host/hos_main.c static_tls_room) is a constant offset, as the# executable's own are. RX_VK_TLS=gd: the default model, as before)vk_tls = ["-mtls-dialect=gnu2"] if LINUX and os.environ.get("RX_VK_TLS", "gnu2") != "gd" else []-mtls-dialect=gnu2is x86-64 only (clang errors on aarch64). Builds are x86-64 today, but gating this onplatform.machine()costs nothing.@ -102,2 +102,3 @@static void driver_cache_room() {} // (host/hos_main.c: the drivers' shader caches' size, #2033)static void driver_cache_folder() {} // (host/hos_main.c: the folder __GL_SHADER_DISK_CACHE_PATH names, made)static void driver_cache_folder() {}static void static_tls_room(char**) {} // (host/hos_main.c: the static TLS restart, first in main) // (host/hos_main.c: the folder __GL_SHADER_DISK_CACHE_PATH names, made)driver_cache_folder's comment ended up at the end of this line; it belongs on the line above.9909b9ef471b4cca2e9c1b4cca2e9cfc96d3ed56Thanks for the review, and for the point that TLSDESC falls back silently rather than failing: that shaped the changes. Point by point, at
fc96d3e(rebased on mainfbd4202):Must change
vk: TLS: <n> bytes, in the static TLS block (<d> below the thread pointer), or... NOT in the static TLS block: every thread_local access ... goes through glibc's dynamic path(tls_placeingpu/vk/vk_main.cpp). It takes a thread_local's distance below the thread pointer (%fs:0) and the module'sPT_TLSfromdl_iterate_phdr.host/hos_main.clogs astatic TLS:line. It says either that the game started again with the room, or why it didn't: the switch (with its value), aGLIBC_TUNABLESthat sets the tunable already (with its value), a failedexecv(with path andstrerror), or aGLIBC_TUNABLEStoo long to add to.tests/test_static_tls.pycutstls_placeinto a module with a 48 KB TLS segment, built with-mtls-dialect=gnu2, and dlopens it. It reads "dynamic" without the room and "static" with it, on glibc 2.35 (the suite's container) and 2.39.RX_VK_TLS=gdon the same code, with several interleaved runs on the Deck and a Linux desktop.HOS_STATIC_TLS=0against default is no longer offered as the baseline.nx2/elf_tls.pyreadsPT_TLS(p_memsz,p_align) from the linkedlongshot_vk.so.nx2/build.pyprints it after the link and fails the build when the segment, rounded to its alignment, plus 8 KB kept for the drivers' own initial-exec TLS, doesn't fitHOS_STATIC_TLS_ROOM. The room is read fromhost/hos_main.c, so there is one number. The error names the fixes: aTLS_OBJECT, a bigger room, orRX_VK_TLS=gd. Measured on this head in the Linux build container:longshot_vk.so: TLS segment 47936 bytes (aligned to 16): fits the 98304 bytes of static TLS room (42176 to spare, 8192 kept for drivers'), matchingreadelf -lW(MemSiz 0x00bb40). The test checks the reader on ELF files it makes (one that fits, one past the margin, one pushed out by its alignment, none, not ELF) and checks that the link calls it.HOS_STATIC_TLSinlongshot_env.txt, and the rule: it's read withenv_value_off(0,false,off,no, empty, any case). When the environment doesn't set it,env_file_peekreads its last line fromlongshot_env.txt(orHOS_ENV_FILE's file) beforeload_longshot_env, with the same BOM and whitespace handling, and the environment first.docs/development.mdsays it's read before the rest. Tested from the environment, from the file (BOM, spaces, later lines winning, similar names not matching), fromHOS_ENV_FILE, and with the environment over the file.fbd4202(after #54, #78 and the 8 since), with no conflicts.Should change
GLIBC_TUNABLESleaking to children: before the exec the old value goes intoHOS_STATIC_TLS_WAS. The restarted game putsGLIBC_TUNABLESback (or unsets it) and drops both markers, soxdg-openand what it starts don't get the room.hos_relaunchstill reads the start's/proc/self/environ, so Restart now keeps the tunable and the restarted game restores it again. A failed exec now clears the markers too. Tested with and without an earlierGLIBC_TUNABLES.EINVAL; the 46.8 KB copied and zeroed into every thread), and the Deck's thread count and RSS against main are among the measurements owed.sampler_for's table: it's now a namespace-scopeTLS_OBJECT(sampler_table, SamplerTable), freed at thread exit with the others, andsampler_forreadssampler_table().e[...].tests/test_sampler_admission.pybuildssampler_forwith it.vk_tls_flags()innx2/build.pygives-mtls-dialect=gnu2only on Linux x86-64 withoutRX_VK_TLS=gd. Another machine gets a line saying so and the default model. The test runs it for x86_64, AMD64, aarch64,gdand Windows.display_startup_failure_test.cpp: both comments are back on their own lines.docs/development.md:HOS_STATIC_TLSis with the switches andRX_VK_TLSis in the Building table.Tests (
python -I -B tests/run_all.py): on Linux,fc96d3eand main @fbd4202fail the same 4 (test_cpp_preflight,test_dev_packs,test_prof_clock, the byte-compile without.git); on Windows, 453 passed and the same 5 failed as on main (test_fpx_sticky,test_bake_portability,test_check_third_party,test_spawn_emit,test_release).test_shader_stamp_cacheruns a slice ofnx2/build.py, so it neededvk_tls_flagsin its namespace (fc96d3e).Not done: no run in the game. This version hasn't run on any machine, and the measurement is owed as above, so it stays WIP.
Steam Deck: works as designed, no measurable change in fps.
Setup:
Hardware and OS: Steam Deck OLED, SteamOS 3.9.5 (kernel 7.2.7-valve2), RADV Mesa 26.2.0-devel.
Base: main
d964886+ the Steam Deck profile (#225), because plain main runs out of memory on the Deck. This PR is merged on that base.Settings: the user's options, stock clocks, the moving Kokiri Forest route, warm caches, memory guard on.
Order: one warm-up start each, then base, #231, #232, #156, then #156, #232, #231, base, interleaved.
On this Deck and route one run spreads about ±1 real fps.
TLS lines:
static TLS: started again with glibc.rtld.optional_static_tls=98304 for the Vulkan back end's thread-local datavk: TLS: 48000 bytes, in the static TLS block (89554 below the thread pointer): each access a constant offsetThreads: 127 at most in every run, with and without the PR (/proc//status, sampled every 5 s).
The warm-up kept every program key (47797, stamp unchanged). The Deck's frame is bound by the readback and GPU waits, so a cheaper TLS access doesn't show as fps here.
fc96d3ed563ccb48a8f0Steam Deck, this PR alone: it works as designed, and gains no fps. These are the Deck agent's runs (comment above) on a Steam Deck OLED, main
d964886+ #225, 2 interleaved pairs:vk: TLS: 48000 bytes, in the static TLS block.Recommendation: not worth merging as it is; I suggest closing it.
GLIBC_TUNABLEShandling, the TLS descriptors and the link-time check are machinery for a gain nobody has measured. The Deck's frame is set by readback and GPU waits, not by TLS accesses.TLS_OBJECTalways inlined andtls_makecold. In a Deck profile,dw()'s 5.1% folded away and the frame's instructions fell 5%. If maintainers want it, it can go as a small PR of its own, on every platform and with the default TLS model.Also in this update:
5a8ae7e(head3ccb48a). The only conflict was in docs/development.md, where #238's new paragraph sits; I kept both.docs/tested-configurations.mdhas the Deck runs.Measured on a 6-core desktop, Linux: a small gain in the Market, at the edge of noise
Both runs of this PR have a lower replay cost per draw than all four runs of its base and of #231 alongside it: about 1% off the Presentation thread's replay, +0.5% fps. The frame there is not the replay alone (23.8 ms against 18.5 ms of replay and 12.8 ms of GPU), so the gain in fps is smaller than the replay's.
main:
5a8ae7e3(this PR's merge base), built aslongshot abc-basethis PR:
3ccb48a8, built aslongshot abc-232Baseline: main @
5a8ae7e3Hardware: RX 9070 XT 16 GB, Mesa RADV 26.2.6 (GFX1201); Ryzen 5 5600X (6 cores, 12 threads), 31 GB; Fedora 45, kernel 7.3.3, KDE Plasma (Wayland)
Settings: output 2560x1440 in a 1280x720 window (mailbox), AMD FSR 4 Native AA (fsr4-vk, 4.1.1 FP8), HDR Expanded, sharpness 50%, AF 16x, VRAM budget 11 GB, frame generation off;
HOS_FPS_MAX=240(uncapped),HOS_PRESENT_TRACEon for every runScene: the Market (child) from one save, the same time of day each run, standing still: no input at all
Method: one full build per side from the Linux container (
scripts/linux/docker.sh, then the AppImage's--cli --fsr), each in its own folder; a warm-up start of each; then six runs through the control bridge (scripts/longshot_ctl.py launch --save --slot, A until in play, the Market loaded and no event, 10 s, then 120 s measured,quit), interleaved base, #231, this PR, this PR, #231, base. Medians of the two runs a side.How to reproduce: a child Market save;
longshot_ctl.py launch <game> --save <save folder> --slot <NN> --env HOS_FPS_MAX=240 --env HOS_PRESENT_TRACE=<csv>; frame times from the trace between twostatusframes 120 s apart5a8ae7e3(run 1 / run 6)3ccb48a8(run 3 / run 4)#231 in the same session (runs 2 and 5): 41.6 / 41.8 fps, 7.08 / 7.06 ms per 1000 draws, within this PC's run-to-run spread of the base.
The change took effect:
A status line from each side, in the measured window:
Also checked:
python -I -B tests/run_all.pyon this PR: 469 passed, 2 failed, 74 skipped. The two (tests/test_menu_keys.py,tests/test_pad_layout.py) fail on main @5a8ae7e3on this PC too, withtests/test_display_pointer.pyonce more there. No device lost in any run.Read head
3ccb48a8against its base5a8ae7e3and current maincf605c85: the complete diff, description, four comments, the earlier review and its seven inline comments, and the startup, restart, build and sampler consumers. Checked the published arithmetic and the actual merge conflicts. No tests, game or renderer build were run; the description recommends closing this version.What's good
Must change if this version proceeds
gpu/vk/vk_draw.cpp, lines 2724-2730, replaces the fixed TLS table with a first-use allocation throughtls_make. If that 128 KiB allocation throws,sampler_forexits before it can look up an already-owned sampler or return a failed handle. Neitherbuild_descriptorsnor the replay worker catches that exception, so it terminates the process on the threaded replay path. Main's table did not allocate at that lookup, andsampler_makealready treats failures of its other acceleration caches as recoverable. Keep that policy for this cache too, falling back tosampler_makewhen its storage cannot be obtained. Add an allocation-failure case using the real heap-backed accessor, including a sampler already owned globally:tests/test_sampler_admission.py, lines 38-44, currently replaces it with a fixed TLS array and cannot exercise the new failure. This is a source finding, not an observed game crash.Earlier review, at this head
The review being followed up is #232 (comment).
gdlog wording still needs the smaller correction below.p_memsz, alignment, the host's reserve and an 8 KiB margin. The description supplies the build's measured size. I did not inspect that binary.HOS_STATIC_TLSfrom the file and accept all off valuesenv_value_off.5a8ae7e3, but main has moved again. There are now real conflicts inhost/hos_main.canddocs/tested-configurations.md, not merely the WIP status./proc/self/environ.TLS_OBJECTconvention for the sampler tablevk_tls_flags; Windows and other host architectures keep the old flags.docs/development.md.Measurement and smaller points
gpu/vk/vk_main.cpp, lines 239-245, always describes a dynamic placement as the slower descriptor path, even in anRX_VK_TLS=gdbuild. Carry the selected model into the diagnostic, so the intended baseline does not warn that it is slower than itself.docs/tested-configurations.mdrow into separate run files underdocs/tested/; current main freezes the old table. Resolve the startup conflict while preserving both the new layout initialization and the requirement to reserve TLS before other startup work.Not checked
No failure injection, GCC/Clang fixture execution, Windows check, game, full suite or performance measurement here. I checked the published tables and quoted logs, not their underlying captures. A fault-injection test of the real sampler accessor would settle the new failure path; a controlled accessor-only versus full-change comparison would settle the extra TLS machinery's value. The source-check jobs are blocked, not passing: https://git.magicbean.dev/MagicBeanDev/Longshot/actions/runs/3412.
View command line instructions
Manual merge helper
Use this merge commit message when completing the merge manually.
Checkout
From your project repository, check out a new branch and test the changes.