feat(encode): RGB-direct (EFC) is now the DEFAULT on capable hosts — gated by a session cursor-blend hint
apple / screenshots (push) Has been cancelled
apple / swift (push) Has been cancelled
android / android (push) Has been cancelled
ci / web (push) Successful in 55s
deb / build-publish (push) Has been cancelled
deb / build-publish-host (push) Has been cancelled
ci / docs-site (push) Successful in 55s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
decky / build-publish (push) Successful in 20s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 13s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 56s
ci / bench (push) Successful in 7m8s
ci / rust (push) Failing after 9m14s
windows-host / package (push) Successful in 10m24s
arch / build-publish (push) Successful in 12m58s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m25s
docker / deploy-docs (push) Successful in 28s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 20m25s

B2 default-on (design/vulkan-rgb-direct-encode.md): wherever the probe
passes, Vulkan Video sessions now take the EFC RGB source by default —
direct import on aligned modes, padded-copy staging on unaligned ones
(1080p). PUNKTFUNK_VULKAN_RGB_DIRECT becomes the override: =0 disables,
=1 forces, unset = the default below.

The one session class that must NOT default on: cursor-as-metadata
captures (every non-gamescope compositor), where the CSC shader's blend
IS the visible pointer — the EFC cannot composite, and defaulting there
would silently drop the cursor from the stream. The hint rides the
existing plumbing:

- SessionPlan gains cursor_blend, resolved once where the compositor is
  known (gamescope embeds the pointer itself → false; kwin/mutter/
  wlroots/hyprland → true), and shows up in the logged plan line.
- open_video/open_video_backend thread it through (native pump: all
  three encoder-open sites read plan.cursor_blend; GameStream monitor
  capture: true — it negotiates metadata cursor; spike: false).
- VulkanVideoEncoder::open resolves: env override, else ON iff the
  session never hands us cursor bitmaps. The warn-once for a cursor on
  an RGB session (forced via =1) stays.

Verified on-hw box (Linux): pf-encode + punktfunk-host compile, clippy
clean, unit suite green. The GPU paths themselves are unchanged from the
smoke-validated 96e19986 — this commit only changes which sessions
select them by default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
enricobuehler
2026-07-20 23:19:46 +02:00
parent 3736bbdc73
commit c6caeca5bc
6 changed files with 81 additions and 16 deletions
@@ -658,6 +658,9 @@ fn stream_body(
// GameStream/Moonlight stays 4:2:0 — stock Moonlight clients can't decode 4:4:4, and the
// Windows IDD-push capturer can't yet deliver full-chroma frames. 4:4:4 is punktfunk/1-native only.
encode::ChromaFormat::Yuv420,
// Desktop monitor capture negotiates cursor-as-metadata where available — the encoder
// may be handed cursor bitmaps to composite.
true,
)
.context("open video encoder for stream")?;
// FEC overhead percent (Sunshine default 20). Override with PUNKTFUNK_FEC_PCT (0 = data-only).
@@ -811,6 +814,7 @@ fn stream_body(
frame.is_cuda(),
gs_bit_depth(frame.format),
encode::ChromaFormat::Yuv420, // GameStream stays 4:2:0
true, // metadata-cursor capture — see the first open
)
.context("reopen encoder after rebuild")?;
supports_rfi = enc.caps().supports_rfi;
+15 -2
View File
@@ -983,7 +983,12 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
// path now reads this typed `SessionPlan` instead of re-deriving from config at each dispatch site
// (the latent "capture and encode disagree on the backend" hazard, plan §2.4). `bit_depth` is the
// only per-session input — capture/topology/encoder are otherwise pure functions of `HostConfig`.
let mut plan = crate::session_plan::SessionPlan::resolve(ctx.bit_depth, ctx.chroma, ctx.codec);
let mut plan = crate::session_plan::SessionPlan::resolve(
ctx.bit_depth,
ctx.chroma,
ctx.codec,
ctx.compositor != pf_vdisplay::Compositor::Gamescope,
);
// PyroWave rides the datagram-aligned wire mode (§4.4): every encoder this session opens
// packetizes at the negotiated shard payload, so a lost datagram costs blocks, not frames.
if ctx.codec == crate::encode::Codec::PyroWave {
@@ -1632,6 +1637,7 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
frame.is_cuda(),
bit_depth,
plan.chroma,
plan.cursor_blend,
) {
Ok(mut new_enc) => {
tracing::info!(
@@ -2551,6 +2557,7 @@ fn try_inplace_resize(
new_frame.is_cuda(),
bit_depth,
plan.chroma,
plan.cursor_blend,
) {
Ok(e) => e,
Err(e) => {
@@ -2619,7 +2626,12 @@ pub(super) fn prepare_display(
// Same plan resolution as `virtual_stream` (pure in these inputs + host config), including
// PyroWave's datagram-aligned wire mode — `Session::shard_payload()` echoes the negotiated
// Welcome value passed here.
let mut plan = crate::session_plan::SessionPlan::resolve(bit_depth, chroma, codec);
let mut plan = crate::session_plan::SessionPlan::resolve(
bit_depth,
chroma,
codec,
compositor != pf_vdisplay::Compositor::Gamescope,
);
if codec == crate::encode::Codec::PyroWave {
plan.wire_chunk = Some(shard_payload as usize);
}
@@ -2871,6 +2883,7 @@ fn build_pipeline(
frame.is_cuda(),
bit_depth,
plan.chroma,
plan.cursor_blend,
)
.context("open video encoder")?;
if let Some(t) = trace {
@@ -103,6 +103,11 @@ pub struct SessionPlan {
/// PyroWave session — applied to EVERY encoder this plan opens (initial + all rebuilds) so
/// AUs stay shard-aligned across mode/bitrate/stall rebuilds. `None` for the H.26x codecs.
pub wire_chunk: Option<usize>,
/// The session may hand the encoder cursor bitmaps to composite (cursor-as-metadata
/// captures — every non-gamescope compositor; gamescope embeds the pointer itself).
/// Encoders whose fast path cannot blend (the Vulkan EFC RGB-direct source) stay on their
/// blending path when this is set, so the pointer never silently vanishes from the stream.
pub cursor_blend: bool,
}
impl SessionPlan {
@@ -112,6 +117,7 @@ impl SessionPlan {
bit_depth: u8,
chroma: crate::encode::ChromaFormat,
codec: crate::encode::Codec,
cursor_blend: bool,
) -> Self {
SessionPlan {
capture: CaptureBackend::resolve(),
@@ -122,6 +128,7 @@ impl SessionPlan {
chroma,
codec,
wire_chunk: None,
cursor_blend,
}
}
+1
View File
@@ -147,6 +147,7 @@ pub fn run(opts: Options) -> Result<()> {
first.is_cuda(),
8, // spike synthetic harness: 8-bit
encode::ChromaFormat::Yuv420, // ...and 4:2:0
false, // synthetic frames carry no cursor
)
.context("open encoder")?;