vk: descriptor slotgen and sampler cache #1330
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!1330
Loading…
Reference in a new issue
No description provided.
Delete branch "pr/vk-descriptor-slotgen"
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?
What
Replay-thread optimizations for descriptor and sampler resolution, default off and switchable by menu row and environment variable (one shared code path).
1. Descriptor slotgen (
RX_VK_DESC_SLOTGEN=1, menu: Graphics → Descriptor reuse)Descriptor reuse currently keys on the global registrations' generation, so any texture registration anywhere (the game streams constantly) rebuilds every draw's descriptors. A draw now keeps its descriptors while the texture slots it actually samples are unchanged: the build records
(slot, version)for every handle it resolved and the same draw revalidates by slot version instead of the global generation. Draws that sampled an unregistered slot keep the old global-generation check (a registration is the only thing that could make such a handle resolve). An eviction sweep (destroy_later) retires views behind the slots' backs without bumping any slot or sampler version, so the keep-check revalidatestexture_destroy_generation()as well — this also covers thetexture_view_ofstaleness question.2. Sampler cache (
RX_VK_SAMPLER_CACHE, menu: Graphics → Sampler cache)Each texture of each draw resolved its sampler through
reg_mu+ a copy of the registration + hash + map lookup (~48k times per frame at 3000 draws). Sampler ids now answer from a per-thread table keyed on the id's slot version (thesamp_versionstable in vk_bank, like the texture slots'slot_versions): resolving an id that was not registered anew takes no lock. One switch, documented at the declaration:RX_VK_SAMPLER_CACHE=0turns off both the outer id table and the inner parameters' cache;=1or the menu row turns the inner cache on (the outer table stays on, exactly as it is without the option — so the menu row never silently changes the old default-on path).Measurements (RTX 4090, 5120x1440, TOTK)
Same binary, environment variables off = the old path (A/B identity):
(median "a frame's replay time" over 240 s scripted gameplay runs, ~3200 draws/frame; the replay thread is the fps ceiling — at 1000/4.64 ms it moves from ~84 to ~215 fps of headroom. Item-2-alone numbers follow in a comment.)
Response to the review
texture_destroy_generation()now participates in the slotgen keep-check, so adestroy_latersweep invalidates the kept descriptors on its own.seenmap insampler_make: dropped the unreachablesize() > 4096clear (ids are < 4096, the map cannot outgrow them).draw().Thanks, the measured numbers are nice. Two things up front.
This is two unrelated PRs. The descriptor slotgen + sampler cache are a Vulkan replay-thread optimization (vk_bank/vk_draw/vk_replay/vk_state). The Frame rate row is a pacing / game-clock change (host/nn_vi.c, nx2/oot_patches.py, the menu row, tests/test_present_trace.py). Nothing in one needs the other. Please split them: the first is easy to review and bisect on its own, and the second is the riskier one (see below). It is also currently conflicting with main (14 behind,
dirty) in vk_menu.cpp / vk_menu.h, so it needs a rebase anyway. CI is green. Option id 10 is free on main, no collision (0-9, 16-22, 24-26, 32-35, 4096+ all untouched), and the ini keys derived from the row names (descriptor_reuse,sampler_cache) match whatmenu_env_onreads.Sampler cache / slotgen (looks correct to me).
samplersare never destroyed, andseenholdsnvn_samp_parby value, so nothing dangles.compareandaniso_override(); the newseentable is keyed by id and validated bysamp_versions[id], which is bumped insidereg_muafter the write. A reader that gets the new params under an old version just refetches next time, so races only err on the safe side. Unregistered ids / ids >= 4096 are never cached.texture_view_ofalso keys ontexture_destroy_generation()(eviction in vk_texture.cpp callsdestroy_later, which bumps destroy_gen but not reg_gen or any slot version). That is the same for the old check, so not a regression, but with slotgen the churn that used to force rebuilds is gone, so if you have a view of how longlast_desc_*can live across an eviction sweep, it's worth confirming it is reset there.sampler_foralready has a per-thread table keyed onsampler_registrations_generation()(default on), which only changes when sampler contents change. The newseencache only helps on that table's miss path. The 4.64 vs 11.87 ms number is for items 1+2 together; can you give item 2 alone? AlsoRX_VK_SAMPLER_CACHEnow means two things with opposite defaults (outer table on unless=0, new inner cache off unless=1/menu). Nit: the "Sampler cache" menu Off position does not turn the old table off.if (seen.size() > 4096) seen.clear();can never fire (ids are < 4096).Frame rate row (needs work before it ships).
realtime_steprewrite changes the default path for everyone, not just users of the new row. Before, the step averaged up to 5 frames and clamped the written rate to 15..60. Now it is a 100 ms count with no upper clamp, and*slot = fpsis written for anything up to 9999 even whenHOS_UNL_STEPis off. Per your own comment the engine stores60/fpswith an integer divide, so anyfps > 60(say the smoothed measure rounding to 61 on a paced 60 loop) gives 60/61 = 0 in those rate fields. Without the float patch nothing guards that. Please clamp to 60 unless the unlimited step is active, so Off stays the old behaviour.HOS_UNL_STEP; that is only read from the environment (getenv("HOS_UNL_STEP")in realtime_step). A user choosing Unlimited in the menu gets fps > 60 written to the engine without the host-float step, i.e. the broken fast-forward the description explains. The row should turn this on itself (and the oot_patches patch should be mentioned as required: it only exists in a rebuilt lift output).patch_unlimited_step(applies once, idempotent, leaves other game versions alone) orfps_lock_wanted; tests/test_present_trace.py only gets a stub. Please add a small test for the patch next to the other oot_patches tests and register it in tests/run_all.py.hos_logdereferenceshos_unl_step_probe2 + 0xd58as a guest pointer, which is fine only on the version the patch matched.So: the slotgen/sampler half looks good to me once split out; the Frame rate half I would hold until the default-path change and the Unlimited-row wiring are sorted.
Split as requested: this PR is now only the slotgen + sampler cache (rebased on current main, merged with the #792 fallback fix in
draw()); the Frame rate row is #1367.Your points, in this branch:
texture_view_ofstaleness: the keep-check now also revalidatestexture_destroy_generation()— adestroy_latersweep bumps only that, so kept descriptors cover it.last_desc_*needs no reset across a sweep for the same reason: the versions the keep-check reads (texture_slot_version) survive eviction, and anything the sweep retired shows up in the destroy generation.seen.clear(): dropped, ids are < 4096.RX_VK_SAMPLER_CACHE: one switch, stated at the declaration:=0turns off both the outer id table and the inner parameters' cache;=1or the menu row turns the inner cache on and leaves the outer table exactly as it is without the option (the row therefore never changes the old default-on path).RX_VK_DESC_SLOTGEN=0, RX_VK_SAMPLER_CACHE=1), I will post them here.Thanks for splitting this out, and for the rebase. This one is now only slotgen + the sampler cache, and the Frame rate row lives in #1367, so none of my pacing points apply here.
Checked against my last round:
desc_slots_keptnow also checkstexture_destroy_generation(), andbuild_descriptorsreads it at the start of the build. Adestroy_latersweep forces a rebuild now. Your reasoning aboutlast_desc_*holds too:last_desc_validis reset at every recording start anyway, so a kept set never outlives the recording it was built in. That also means skippingtexture_view_of's per-submissiontexture_touchon reuse is no different from what main already does.seen.clear(): dropped. Good.RX_VK_SAMPLER_CACHE: the two meanings are written down at the declaration now, and=0turns off both layers consistently (sampler_for'sonandmenu_env_onboth read it). That works for me.I re-read the rebased diff as a whole. The new texel-buffer, texture and image handle reads all go through
desc_fp_notebefore they resolve. Storage-pointer handles count as 0, so they fall back to the global generation. The #792 fallback condition is still insame_desc. Neither PR uses an option id: the rows are read straight fromhookshot_options.inibymenu_env_on, and the keysdescriptor_reuseandsampler_cachematch whatini_keywrites. The id 10 thatfps_lockused is in #1367, which is closed, and none of the open PRs uses id 10 either. CI is green and the PR merges cleanly.Two small nits inline. Approving.
Nit (perf only): this bumps on every rewrite, including the identical ones the
samecheck above exists for. So theseencache insampler_makemisses after every rewrite of the same parameters. If the game rewrites its samplers often, as the comment above suggests, bumping only when!samewould let the cache hit more. It is still correct either way, because a reader refetches on a miss.Nit: the rebase dropped the issue 640 / #792 comment that explained
(!p.fallback || dw().prog == &p), but the condition itself is still here. Could you keep that comment? Without it the clause is easy to "simplify" away later.I measured Descriptor reuse and Sampler cache in Ocarina of Time, to see whether they should be on by default. In this game neither makes a measurable difference, and neither broke anything.
Setup: RTX 3090, Ryzen 9 7900X, Linux, main at
854c34c, 1920x1080. A scripted route from a save: Kokiri Forest, into a house and back out (a scene change), then camera turns around the forest. Presents 1100 to 6500, about 2 to 2.5 minutes of play. Four configurations, three runs each, run back to back in rotating order. Runs that overlapped other GPU work were dropped. "Off" means the variables are unset; see note 2.The same configuration varied by up to 1.6 ms between rounds. "Cache only" looks about 0.4 ms faster, but "both on" includes the cache and is not faster, so I read it as noise.
Why there's little to gain here:
Correctness: no validation errors, device loss, crashes or hangs in 25 runs, and the "ready" log lines appear exactly when each option is on. The game doesn't replay frame-exactly, so I compared the best-aligned frame (inside the house) across five runs. Every configuration differs from off by about as much as two off runs differ from each other: about 1% of pixels, from the hearts' rotation. I saw no stale or wrong textures. Reading the code, I found no problem in the keep-check (eviction generation, unregistered slots) or in the inner cache's read order.
Two notes:
hookshot_options.ini, so anyone who has saved options since this PR hasdescriptor_reuse=0andsampler_cache=0written. Changing the default would only reach new installs, unless the key is renamed or migrated.RX_VK_SAMPLER_CACHE=0is slower than leaving it unset. It also turns off the outer sampler table that is on by default (vk_draw.cpp, near line 1739). Anyone comparing "off" with=0gets a slower baseline than real players have. It might be worth making=0turn off only the inner cache.These may well help in other games. For OoT, leaving both off by default looks right.