Files
punktfunk/crates/pyrowave-sys/patches/0005-global-priority-queue.patch
T
enricobuehler 9232631299 feat(pf-encode): PyroWave had no encode split — so the one cost this program protects was unmeasurable
Wave-2 PW1's exit criterion, and the instrument it needed.

VAAPI and direct NVENC both log a PUNKTFUNK_PERF submit split. PyroWave did not — which meant the
single encoder the GPU-priority work exists to defend was the one you could not put a number on.
Adds per-frame timing of the synchronous encode (whole `submit`: CSC + encode + fence wait +
packetize, which for this backend IS the encode), summarised every 2 s as mean/p50/p99/max.

p99 rather than mean-only on purpose. The failure patch 0005 describes is a TAIL event — frames
going ~2 ms to 15-18 ms at 95 % game load while the mean barely moves — so a mean-only readout
would report "fine" straight through the thing being measured.

WHAT IT MEASURED — .21, RTX 5070 Ti (610.57.04), GRID 2 benchmark loop saturating the GPU at
54-87 %, PyroWave 1080p, same binary both arms (only CAP_SYS_NICE differs), 30-frame windows with
the warm-up window dropped:

  arm                        p50        p99        worst frame
  default priority (refused) ~2.6 ms    ~6.4 ms    9.5 ms
  REALTIME granted           ~3.2 ms    ~4.4 ms    5.4 ms
  REALTIME granted (repeat)  ~3.35 ms   ~4.8 ms    5.1 ms

p99 down ~30 %, worst frame roughly halved, for ~0.6 ms on the median. For a streaming encoder
that is the right side of the trade — the tail is what becomes a visible hitch.

This CONTRADICTS the patch's only prior datum (RTX 4090 / Windows / WDDM: "did not reduce the
spikes"), so patch 0005's header now records the Linux/NVIDIA result beside it, with an explicit
"do NOT delete this patch on the strength of the WDDM result — the two stacks disagree". Header
prose only; the diff hunks stay byte-identical and `git diff crates/pyrowave-sys/vendor/` is
untouched by this commit.

Caveats recorded rather than buried: the arms were not interleaved and the game load drifted
between them, capture was frame-starved (~2.5 fps) so this is encode latency under contention and
not a full-rate stream, and it is two granted runs against one refused run. The direction held
across all 25 windows.

Also worth knowing for anyone repeating this: `encode_fps` is a VACUOUS metric on this rig. A
headless gamescope with no real content emits ~12 fps, so both arms simply report the capture rate.
Measure latency, not throughput.

Gates green at CI parity.
2026-08-08 18:40:52 +02:00

153 lines
7.9 KiB
Diff

Elevated global-priority encode queue — PUNKTFUNK LOCAL PATCH.
Not upstream. PyroWave's wavelet encode runs on the GPU's compute/shader cores, so a GPU-bound
game starves it: `pyrowave_encoder_encode_gpu_synchronous` spikes from ~2 ms to ~15 ms under a
95%+ game load and the stream framerate collapses (NVENC is immune — it uses the separate encoder
ASIC). A WDDM *process* scheduling-priority raise does not help (it orders packet submission, not
hardware preemption). This requests a global-priority Vulkan *queue* — the actual NVIDIA compute-
preemption lever — on the pyrowave encode device.
`Context::create_device` (the pyrowave path uses plain vkCreateDevice, not the vpCreateDevice
profile path): enable VK_KHR_global_priority (or the EXT alias), chain a
VkDeviceQueueGlobalPriorityCreateInfoKHR into every queue-create-info, and a create loop that tries
`PYROWAVE_QUEUE_PRIORITY` (off|high|realtime, default realtime) → HIGH → no-priority, downgrading on
VK_ERROR_NOT_PERMITTED_KHR so a refused class NEVER regresses the encoder. Gated on !inherit_info
(only the fresh encoder device). This Granite copy is vendored solely for the PyroWave codec.
NOTE: on an RTX 4090 / Windows / WDDM this did not reduce the spikes (the graphics-vs-compute
preemption granularity is the wall) — kept because it is correct, harmless (graceful fallback), and
may help other GPUs/drivers. Reduce the encode's GPU cost (4:2:0/8-bit) or use H.265 for a
GPU-saturated game. **That measurement is Windows/WDDM and does NOT transfer to Linux** — a
different driver stack with a different preemption model.
MEASURED ON LINUX/NVIDIA 2026-08-08, and it comes out the OTHER WAY: the elevated queue DOES cut
the tail. RTX 5070 Ti (driver 610.57.04), GRID 2 benchmark loop saturating the GPU at 54-87 %,
PyroWave 1080p, same binary in both arms (the only difference is CAP_SYS_NICE, i.e. whether the
class is granted at all), steady-state windows of 30 frames:
arm p50 p99 worst frame
default priority (refused) ~2.6 ms ~6.4 ms 9.5 ms
REALTIME granted ~3.2 ms ~4.4 ms 5.4 ms (repeat: p50 ~3.35, p99 ~4.8)
So on this stack the priority class buys a materially tighter TAIL — p99 down ~30 %, worst frame
roughly halved — at the cost of ~0.6 ms on the median. For a streaming encoder that is the right
side of the trade: the tail is what shows up as a visible hitch. Do NOT delete this patch on the
strength of the RTX 4090/WDDM result above; the two stacks disagree.
Caveats, so the number is not over-read: the arms were not interleaved and the background game
load drifted between them, capture was frame-starved (~2.5 fps) so this measures encode latency
under contention rather than a full-rate stream, and it is two granted runs against one refused
run. The direction was consistent across all 25 measurement windows.
NOTE 2 — WHERE THIS PATCH IS ACTUALLY LIVE. It is gated `if (!inherit_info)`, and only the WINDOWS
path leaves `inherit_info` null: `crates/pf-encode/src/enc/windows/pyrowave.rs` calls
`pyrowave_create_device_by_compat`, so Granite builds the device itself and this block runs.
**On LINUX it has never done anything.** `crates/pf-encode/src/enc/linux/pyrowave.rs::open_inner`
passes its own instance/device create-infos into `pyrowave_device_create_info`, Granite's
`MyDeviceFactory::get_existing_create_info()` returns them, `create_device` takes the inherit
branch, and the whole block above is skipped. The Linux request is therefore wired natively in
**`crates/pf-encode/src/enc/linux/pyrowave.rs`** (search `queue_priority_candidates`), which
implements the SAME env grammar and the SAME downgrade ladder so one knob means one thing on both
platforms. If you change the grammar here, change it there in the same commit.
diff --git a/crates/pyrowave-sys/vendor/pyrowave/Granite/vulkan/context.cpp b/crates/pyrowave-sys/vendor/pyrowave/Granite/vulkan/context.cpp
index 5257fc33..479eeded 100644
--- a/crates/pyrowave-sys/vendor/pyrowave/Granite/vulkan/context.cpp
+++ b/crates/pyrowave-sys/vendor/pyrowave/Granite/vulkan/context.cpp
@@ -2115,6 +2115,51 @@ bool Context::create_device(VkPhysicalDevice gpu_, VkSurfaceKHR surface,
vpGetProfileProperties(profile.profile, &props);
#endif
+ // PUNKTFUNK PATCH (patches/0005-global-priority-queue.patch, NOT upstream): request a high
+ // global-priority queue so PyroWave's compute-shader encode can PREEMPT a GPU-bound game on
+ // the shared shader cores. A WDDM process-priority raise does not help (it only orders packet
+ // submission, not hardware preemption); a global-priority queue is the actual NVIDIA compute-
+ // preemption lever. `PYROWAVE_QUEUE_PRIORITY` = off | high | realtime (default realtime). The
+ // device-create loop below downgrades on NOT_PERMITTED, so a refused class NEVER fails the
+ // encoder — it just runs at default priority. Only the fresh encoder device (no inherit_info)
+ // is affected; this Granite copy is vendored solely for the PyroWave codec.
+ Util::SmallVector<VkQueueGlobalPriorityKHR> pf_priority_candidates;
+ Util::SmallVector<VkDeviceQueueGlobalPriorityCreateInfoKHR> pf_global_priority_infos;
+ if (!inherit_info)
+ {
+ const char *pf_gp_ext = nullptr;
+ if (has_extension(VK_KHR_GLOBAL_PRIORITY_EXTENSION_NAME))
+ pf_gp_ext = VK_KHR_GLOBAL_PRIORITY_EXTENSION_NAME;
+ else if (has_extension(VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME))
+ pf_gp_ext = VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME;
+ std::string pf_prio;
+ if (!Util::get_environment("PYROWAVE_QUEUE_PRIORITY", pf_prio))
+ pf_prio = "realtime";
+ for (auto &pf_ch : pf_prio)
+ if (pf_ch >= 'A' && pf_ch <= 'Z')
+ pf_ch = char(pf_ch + 32);
+ if (pf_gp_ext && pf_prio != "off")
+ {
+ if (pf_prio == "high")
+ {
+ pf_priority_candidates.push_back(VK_QUEUE_GLOBAL_PRIORITY_HIGH_KHR);
+ }
+ else
+ {
+ pf_priority_candidates.push_back(VK_QUEUE_GLOBAL_PRIORITY_REALTIME_KHR);
+ pf_priority_candidates.push_back(VK_QUEUE_GLOBAL_PRIORITY_HIGH_KHR);
+ }
+ enabled_extensions.push_back(pf_gp_ext);
+ pf_global_priority_infos.resize(queue_infos.size());
+ for (size_t pf_i = 0; pf_i < queue_infos.size(); pf_i++)
+ {
+ pf_global_priority_infos[pf_i] = { VK_STRUCTURE_TYPE_DEVICE_QUEUE_GLOBAL_PRIORITY_CREATE_INFO_KHR };
+ pf_global_priority_infos[pf_i].globalPriority = pf_priority_candidates.front();
+ queue_infos[pf_i].pNext = &pf_global_priority_infos[pf_i];
+ }
+ }
+ }
+
if (inherit_info)
{
device_info.enabledExtensionCount = inherit_info->enabledExtensionCount;
@@ -2144,8 +2189,41 @@ bool Context::create_device(VkPhysicalDevice gpu_, VkSurfaceKHR surface,
if (device == VK_NULL_HANDLE)
return false;
}
- else if (vkCreateDevice(gpu, &device_info, nullptr, &device) != VK_SUCCESS)
- return false;
+ else
+ {
+ // PUNKTFUNK: try the requested global-priority class, downgrade through the
+ // candidate list on NOT_PERMITTED, then finally create with no global priority.
+ VkResult pf_res;
+ if (pf_priority_candidates.empty())
+ {
+ pf_res = vkCreateDevice(gpu, &device_info, nullptr, &device);
+ }
+ else
+ {
+ pf_res = VK_ERROR_NOT_PERMITTED_KHR;
+ for (size_t pf_a = 0; pf_a < pf_priority_candidates.size(); pf_a++)
+ {
+ for (auto &pf_gp : pf_global_priority_infos)
+ pf_gp.globalPriority = pf_priority_candidates[pf_a];
+ pf_res = vkCreateDevice(gpu, &device_info, nullptr, &device);
+ if (pf_res != VK_ERROR_NOT_PERMITTED_KHR)
+ break;
+ LOGW("PyroWave: global queue priority %u not permitted; downgrading.\n",
+ unsigned(pf_priority_candidates[pf_a]));
+ }
+ if (pf_res == VK_ERROR_NOT_PERMITTED_KHR)
+ {
+ for (auto &pf_qi : queue_infos)
+ pf_qi.pNext = nullptr;
+ pf_res = vkCreateDevice(gpu, &device_info, nullptr, &device);
+ LOGW("PyroWave: all global queue priorities refused; default priority.\n");
+ }
+ else
+ LOGI("PyroWave: encode device created with an elevated global queue priority.\n");
+ }
+ if (pf_res != VK_SUCCESS)
+ return false;
+ }
}
if (inherit_info)