feat(vk): a state list record with no variant prepares the plain one; 11,985 pipelines from Fritchii's recipe #2134
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!2134
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/state-list-plain-variant"
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?
Lists converted from records that hold no texture answers, like Fritchii's shader recipe, cannot name a program variant. 11,985 of the recipe's pipelines that no list here holds have vertex input the lists here fix but no known variant; a build preparing them with the variant nothing bound makes builds most of them as the draws will ask.
state_list_prepare: a record with variant 0 and no answers or questions stands for the variant the program's translation makes with nothing bound, each texture as its instruction declares it. When the record's own variant is not found (a program without textures has variant 0 already and is found as before), it is made with seed_plain_of and the pipeline noted with it. Draws whose textures differ make their own variant as before; the prepared pipeline is just not used. Not seed_variant_of with empty answers: that variant would hold no answers and stand for any textures. The log's state list line counts them ("by nothing bound").
seed_plain_of shares seed_variant_of's body (seed_variant_made), which hands the translation questions and answers only when given; without them it answers what nothing bound answers and notes the questions, as the seed's own translation does.
contrib/pmwork-personal-fritchii-recipe-plain: those 11,985 records (11,111 graphics, 874 mesh, 4,785 programs). A build without this rule leaves them out ("whose program variant was not made"). test_state_list_plain.py checks the rule in the source.
Since review (hckrman101):
program_find_portable(key, 0)never found it, and each prepare translated the list's ~4.8k plain programs again and appended them tohookshot_programs.bin. Nowprogram_find_plain(key)finds the loaded variant by its answers (an answer for every slot, each its declared type; onedeclared_answerhelper shared withrecord_answers), andstate_list_preparequeues a plain record only when that finds nothing. The prepare's log before this fix: a first prepare in a fresh folder made "4798 by nothing bound (4798 made)", and every later prepare made them again. With it, even a cold first prepare shows "0 by nothing bound (0 made)": a plain variant is the program's own first translation, so the 11,985 pipelines are noted "by nothing bound" with no extra translation (cost measured in the comments).34f05785, or #2143's lists.test_state_list_plain.pychecks the lookup and both its uses;test_program_answer_domains.pycopiesdeclared_answerwithrecord_answers; main's newtest_seed_variant_cleanup.py(#2323) copiesseed_variant_madewithseed_variant_of.tests/run_all.pyon Windows after merging main at34f05785: 379 passed, 31 skipped, 2 failed, both failing on untouched main here too (test_recorder_sync,test_vram_budget_row). A Windows build compiles.Thanks, the idea holds up. A record with variant 0 and no answers or questions can't come from a real recording of a textured program, so old lists aren't misread. Programs without textures still hit
program_find_portable(key, 0)first. Builds without this rule just count these records as "whose program variant was not made". The record format and both cache stamps are unchanged, which is right because nothing on disk changes shape. Going through the textures as their instructions declare them (record_answerswith nothing bound, non-empty answers) also keeps the plain variant from passing as "any textures" instill_valid. Good catch, and the test pins it.One real problem, inline: the plain variant is never found by its own hash. So every
state_list_preparerun (each prepare, and each Builder run withrefresh_states) translates all of these programs again (about 4.8k with the contrib list) and appends a duplicate record for each tohookshot_programs.bin.program_cache_savedoesn't dedupe.Smaller points:
pmwork-personal-fritchii-recipe's note, and the README text coveringcontrib/pmwork-personal-fritchii-recipe*comes from #2133. Neither is on main yet, so this should land after #2133. Can you also confirm the 11,985 records are disjoint from #2133's list (and #2143's, if that lands first)? "Not held by main's lists atf915af9" doesn't say whether #2133 was counted.variant_workerthread-spawn line right after your hunk. It's a trivial conflict, whichever lands second.CI: everything passes except Windows part 1/2, where
test_native_astc_texture_count.pyfails (scanfdeprecated under -Werror). This PR doesn't touch that test.The plain variant's
portable_variantis the hash of its declared-type answers, not 0. Soprogram_find_portable(key, 0)misses it on every run, even when it's already loaded:seed_program_oftranslated it with nothing bound on the first run, andprogram_cache_load_somehas just read it back in. Every plain record's key then goes back intovariant_of.seed_variant_madedoes dedupe in memory, but only after a full translation, andtranslate_intohas already calledprogram_cache_save. The result: ~4.8k translations on every prepare/refresh, andhookshot_programs.bingrows by that many records each time.Could you look up the loaded plain variant before queueing? For example, a
program_find_plain(key)that returns the ready variant whose answers each equal whatrecord_answersgives an unbound slot (the declared type /is_integerbit, aftercanonical_answer). Then only queueseed_plain_ofwhen that returns null.Fixed in
865f8327.program_find_plain(key)returns the loaded, ready variant whose answers cover every slotrecord_answerscounts, each the declared type aftercanonical_answer. The declared type now comes from one helper,declared_answer, whichrecord_answersalso uses for an unbound slot, so the two can't drift apart.state_list_preparequeues a plain record only whenprogram_find_plainreturns null, and notes the loaded variant's pipeline from it too.test_state_list_plain.pychecks both places.Thanks for the review. The fix is in
865f8327(reply inline), on top of a merge of main. On the other points:34f05785(#2429's and #2445's included), or in #2143's lists.The code is sound and the earlier bug is fixed:
program_find_plainfinds the loaded plain variant by its answers before anything is queued, so nothing is retranslated and saved on each prepare, andmade_plain[i]is one index per worker.Whether to ship it depends on #2133. This adds 11,985 converted Fritchii pipelines, including predicted states. With #2133 the shipped list grows about 53% (25.2k to 38.7k), plus 4,798 program translations on first start (10.2 s on an RTX 5090 machine), with nothing measured on low-end hardware. The title is cut off with "…" and needs fixing before squash.
Thanks for the quick turnaround. Re-checked at
8f3910d.Fixed:
program_find_plainfinds the loaded plain variant by its answers (one per counted slot, eachcanonical_answerofdeclared_answer), andstate_list_prepareonly queues a plain record when that returns null. It's also used when noting the pipeline, so a variant loaded from the cache still gets its pipeline prepared. Becauserecord_answersnow uses the samedeclared_answer, the two can't drift apart. It takesshader_muonce and calls nothing that locks it again. Your log numbers ("0 by nothing bound (0 made)" on a second prepare) match the code.34f05785and #2143 answers my question.test_seed_variant_cleanup.pycopiesseed_variant_madetoo, andtest_state_list_plain.pypins the lookup and both places that use it. CI is all green and the PR merges cleanly. #2414 is still a trivial conflict for whichever lands second.Still open: the cost-versus-benefit point @Joey-Magnus raised. With #2133 in, this grows the shipped list by about half and adds ~4.8k program translations to the first start (10 s on a 5090), and nobody has measured it on low-end hardware or counted how many of the 11,985 pipelines draws actually use. My earlier "doesn't block" only covered the case where a prepared pipeline goes unused. It didn't account for the first-start cost everyone pays. A first-prepare time on a low-end machine (or a use count from a play run) would settle that, and whether it's worth shipping is NotRexed's call. The code is ready from my side.
Nit: the title is still cut off at "…", so please fix it before squash (Joey's point).
Confirmed at
8f3910d: the plain record is only queued whenprogram_find_plainfinds nothing, and the noting path falls back to it as well. Resolved, thanks.Thanks @Joey-Magnus. I measured the cost, and one number in my earlier comment was wrong.
No extra translations. The "4798 by nothing bound (4798 made)" I quoted came from a first prepare before the
program_find_plainfix, which re-made the plain variants it couldn't find. A plain variant is the program's own first translation (nothing bound), so a fresh prepare already has it. With the fix, a cold first start shows0 by nothing bound (0 made), ... 11985 by nothing boundnoted.What it adds is pipelines. This head merged with today's main (so #2133's list is in),
hookshot --prepare-shadersfrom cold caches each time (Hookshot's program and pipeline caches deleted, NVIDIA's shader disk cache pointed at an empty folder), Ryzen 7 9800X3D (16 threads) + RTX 5090:The whole cold prepare took 896 s and 687 s, but those totals aren't comparable: the first run was slower in every phase, including the shader translation the list doesn't touch (328 vs 232 s), so it paid a one-time cost (the files not yet in the OS cache). The precompile line is the like-for-like part. I haven't measured a smaller CPU yet (an 8-CPU affinity run is the next step), or how many of these pipelines play actually draws.
Title fixed too.
github-sync referenced this pull request2026-11-12 06:22:54 +00:00
github-sync referenced this pull request2026-11-12 07:28:03 +00:00