Files
libretroadmin c906441de1 GS/Vulkan: a new TFX key draws with its dynamic sibling while its pipeline is built
A draw whose pipeline key was new built that pipeline on the render
thread before it drew. On lavapipe that is a 20-90 ms frame per new
key, and on real drivers it is the hitch every first meeting with a
shader costs.

Each TFX fragment module now has a dynamic sibling. It is the same
tfx.glsl compiled with DYNAMIC: the PS_ fields come from a 16-byte key
in push constants instead of specialization constants.
tools/vk_spirv.py packs the key:
 - each field gets its selector's width, and none crosses a word;
 - the sibling's decode is written into its source;
 - the field positions go out as a table that VKTFXDynamicKey packs the
   key by.
The sibling pipeline of a key keeps the state the pipeline is built from
and the module axes (VKDynamicPS), so one sibling serves every key of
one fixed state.

On a miss, GetTFXPipelineMissed:
 - takes what the warm-up thread has finished;
 - if the key's sibling is there, draws with it, pushing the key from
   ApplyTFXState, and asks the thread for the key's own pipeline;
 - otherwise builds the key's pipeline on the render thread as before,
   and asks the thread for the sibling, so the next new key of that
   state need not wait.
Draws keep going through the lookup until the key's pipeline lands; it
is then taken (also once a frame) and listed for the next device. Only
pipelines of their own are kept as the last pipeline.

The warm-up thread now stays for the device's life:
 - it builds the utility pipelines, then the listed keys, then their
   siblings, so a new key of a state the last session knew draws at
   once;
 - it serves what the render thread asks between items of that, ahead
   of them;
 - it then sleeps on an eventcount until asked. There are no locks:
   jobs go to it in an SPSC ring, results come back in the other.
     - At most 64 jobs are out at once.
     - The result ring has room for all of them.
     - Every job is answered, built or not, so a key never waits forever.

Cost: three dynamic fragment modules, 268 KB of SPIR-V; still far less
than glslang was.

Tests, on lavapipe:
 - tests/vkpipewarm adds, for each case, from nothing:
    - LRPS2_TEST_VK_PIPELINES=sibling: every draw with a sibling;
    - =async: every new key draws with its sibling while the thread
      builds its own.
   Both present the same picture as the specialized pipelines. The
   async run ends in the same state, and its pipelines come from the
   thread while siblings drew.
 - All 74 tests/swdraw cases at 1x and 2x present master's picture in
   the default, sibling and async modes.
 - tests/vkpipekey (CI) also checks the sibling selector and the key:
    - the sibling is made the same module as its key;
    - its packed key unpacks, by the generated table, to the selector's
      fields;
    - no field holds more than the key's bits for it.
   Each of these fails it:
    - dropping a field VKDynamicPS keeps;
    - shifting the packing by a bit;
    - giving tfx.glsl fewer bits for DATE than the selector has.
 - tests/vkspirv covers the dynamic modules' patching too.
 - ASan/UBSan, and TSan with the renderer, retro_spsc, the eventcount
   and rthreads instrumented, are clean over tests/vkpipewarm (all
   lanes) and tests/vknegotiate. Driver and MTVU races in
   uninstrumented code were suppressed for this run only.

The colour varying, the target read and the outputs are module axes as
before, and the key keeps them, so the sibling never reads what the
pipeline's render pass lacks.
2026-11-01 22:53:40 +01:00
..