Merge origin/main into the pf-capture sweep
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m10s
ci / bench (push) Successful in 5m43s
windows-host / package (push) Successful in 10m48s
ci / rust-arm64 (push) Successful in 13m38s
decky / build-publish (push) Successful in 34s
deb / build-publish (push) Successful in 12m32s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 19s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 40s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 15s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 21s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 15s
android / android (push) Successful in 17m7s
deb / build-publish-host (push) Successful in 9m49s
arch / build-publish (push) Successful in 17m59s
ci / rust (push) Successful in 21m32s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6m51s
docker / build-push-arm64cross (push) Successful in 9s
docker / deploy-docs (push) Successful in 26s
deb / build-publish-client-arm64 (push) Successful in 9m55s
apple / swift (push) Successful in 6m10s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 23m10s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 23m24s
apple / screenshots (push) Successful in 24m29s
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m10s
ci / bench (push) Successful in 5m43s
windows-host / package (push) Successful in 10m48s
ci / rust-arm64 (push) Successful in 13m38s
decky / build-publish (push) Successful in 34s
deb / build-publish (push) Successful in 12m32s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 19s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 40s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 15s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 21s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 15s
android / android (push) Successful in 17m7s
deb / build-publish-host (push) Successful in 9m49s
arch / build-publish (push) Successful in 17m59s
ci / rust (push) Successful in 21m32s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6m51s
docker / build-push-arm64cross (push) Successful in 9s
docker / deploy-docs (push) Successful in 26s
deb / build-publish-client-arm64 (push) Successful in 9m55s
apple / swift (push) Successful in 6m10s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 23m10s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 23m24s
apple / screenshots (push) Successful in 24m29s
Upstream landed `fc335b39` (negotiate the cursor around what the encoder can blend) on the same files this sweep restructured. Merged rather than rebased: the sweep MOVED ~2,000 lines out of `linux/mod.rs` into `pipewire.rs`/`portal.rs`/`pw_*.rs`, so a rebase would have re-fought the same conflict in up to nine commits; merging resolves it once with both sides in view. The repo already carries merge commits (`land/sweep-all`). Two conflicts, both resolved by keeping BOTH changes: * `linux/mod.rs` — `PortalCapturer::open` gains upstream's `want_metadata_cursor` AND keeps the sweep's `PortalSession` teardown, so the portal threads now take `(setup_tx, quit_rx, want_metadata_cursor)`. The second conflict was upstream editing inside the region Phase 5.1/5.3 moved out; the split wins and upstream's change is ported to where the code now lives: `choose_cursor_mode`'s new `want_metadata` ladder (4 arms — prefer Embedded when the encoder can't blend, and warn-then-take Metadata when Embedded isn't advertised) is now in `portal.rs`, taken verbatim. * `gamestream/stream.rs` — the pooled-capturer slot keeps upstream's third reuse key (`metadata_cursor`, beside HDR-ness) AND the sweep's `result.is_ok() && capturer.is_alive()` re-pool gate. Both guard the same slot against different failures: theirs against reusing a session negotiated for the wrong cursor mode, the sweep's (L2) against reusing a dead one. Everything else auto-merged, including the `want_metadata_cursor` thread through `host/capture.rs`'s facade and `native/stream.rs`'s `session_plan::cursor_blend_for`. Verified after the merge: workspace `cargo test` green, `clippy --workspace --all-targets` clean on Linux AND on x86_64-pc-windows-msvc, `cargo fmt --check --all` clean, pf-capture 38/38. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,10 +33,14 @@ pub struct StreamConfig {
|
||||
pub hdr: bool,
|
||||
}
|
||||
|
||||
/// A pooled capturer plus the two PipeWire-negotiation-time properties reuse must match on —
|
||||
/// its HDR-ness and its metadata-cursor mode; a mismatch on either needs a fresh screencast
|
||||
/// session (see `AppState::video_cap`).
|
||||
pub type PooledCapturer = (Box<dyn Capturer>, bool, bool);
|
||||
|
||||
/// Slot for the persistent screen capturer, shared with the control plane and reused across
|
||||
/// streams so a reconnect doesn't open a second (conflicting) screencast session. The `bool` is
|
||||
/// the pooled capturer's HDR-ness (see `AppState::video_cap`).
|
||||
pub type CapturerSlot = Arc<std::sync::Mutex<Option<(Box<dyn Capturer>, bool)>>>;
|
||||
/// streams so a reconnect doesn't open a second (conflicting) screencast session.
|
||||
pub type CapturerSlot = Arc<std::sync::Mutex<Option<PooledCapturer>>>;
|
||||
|
||||
/// A pending client reference-frame-invalidation range (lost `firstFrame..=lastFrame`), set by the
|
||||
/// control plane and drained by the video thread (see [`AppState::rfi_range`](super::AppState)).
|
||||
@@ -136,7 +140,7 @@ fn run(
|
||||
running: &Arc<AtomicBool>,
|
||||
force_idr: &AtomicBool,
|
||||
rfi_range: &std::sync::Mutex<Option<(i64, i64)>>,
|
||||
video_cap: &std::sync::Mutex<Option<(Box<dyn Capturer>, bool)>>,
|
||||
video_cap: &std::sync::Mutex<Option<PooledCapturer>>,
|
||||
// Shared stats recorder for the web-console capture/graph. Threaded into `stream_body` (the
|
||||
// encode loop); per-frame sample emission is wired by a later pass.
|
||||
stats: &Arc<crate::stats_recorder::StatsRecorder>,
|
||||
@@ -250,6 +254,12 @@ fn run(
|
||||
return stream_body(
|
||||
&mut capturer,
|
||||
Some(&rebuild),
|
||||
// The virtual-output source never selects cursor-as-metadata (`set_hw_cursor` is
|
||||
// never called → the compositor EMBEDS the pointer where it can), so the encoder
|
||||
// is handed nothing to composite. gamescope remains the pointerless residual —
|
||||
// its capture carries no cursor either way (the native plane's XFixes source is
|
||||
// not wired on this plane).
|
||||
false,
|
||||
&sock,
|
||||
cfg,
|
||||
running,
|
||||
@@ -266,13 +276,34 @@ fn run(
|
||||
// pooled capturer's HDR-ness matching this stream's negotiated `cfg.hdr` — the depth is a
|
||||
// PipeWire-negotiation-time property of the screencast session, so an HDR↔SDR change needs a
|
||||
// fresh session (same pattern as the audio capturer's channel-count gate).
|
||||
// Cursor-as-metadata only where the encode backend this session resolves to composites
|
||||
// `frame.cursor` (the caps-aware negotiation — mirror of the native plane's); otherwise ask
|
||||
// the portal to EMBED the pointer so no backend × cursor-mode combination streams
|
||||
// cursorless. Synthetic frames carry no pointer either way.
|
||||
let metadata_cursor = {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Same CUDA-payload prediction SessionPlan/`handshake::cursor_forward` make:
|
||||
// the NVIDIA resolution plus the zero-copy master switch.
|
||||
let cuda_planned =
|
||||
!crate::encode::linux_zero_copy_is_vaapi() && crate::zerocopy::enabled();
|
||||
crate::encode::cursor_blend_capable(cfg.codec, cuda_planned, cfg.hdr)
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
false
|
||||
};
|
||||
let pooled = match video_cap.lock().unwrap().take() {
|
||||
Some((c, was_hdr)) if was_hdr == cfg.hdr => Some(c),
|
||||
Some((c, was_hdr)) => {
|
||||
Some((c, was_hdr, was_meta)) if was_hdr == cfg.hdr && was_meta == metadata_cursor => {
|
||||
Some(c)
|
||||
}
|
||||
Some((c, was_hdr, was_meta)) => {
|
||||
tracing::info!(
|
||||
was_hdr,
|
||||
want_hdr = cfg.hdr,
|
||||
"video source: pooled capturer depth mismatch — opening a fresh screencast session"
|
||||
was_metadata_cursor = was_meta,
|
||||
want_metadata_cursor = metadata_cursor,
|
||||
"video source: pooled capturer depth/cursor-mode mismatch — opening a fresh \
|
||||
screencast session"
|
||||
);
|
||||
drop(c);
|
||||
None
|
||||
@@ -285,8 +316,13 @@ fn run(
|
||||
c
|
||||
}
|
||||
None if pf_host_config::config().video_source.as_deref() == Some("portal") => {
|
||||
tracing::info!(hdr = cfg.hdr, "video source: portal desktop capture");
|
||||
capture::open_portal_monitor(cfg.hdr).context("open portal capturer")?
|
||||
tracing::info!(
|
||||
hdr = cfg.hdr,
|
||||
metadata_cursor,
|
||||
"video source: portal desktop capture"
|
||||
);
|
||||
capture::open_portal_monitor(cfg.hdr, metadata_cursor)
|
||||
.context("open portal capturer")?
|
||||
}
|
||||
None => {
|
||||
tracing::info!("video source: synthetic test pattern");
|
||||
@@ -298,6 +334,7 @@ fn run(
|
||||
let result = stream_body(
|
||||
&mut capturer,
|
||||
None,
|
||||
metadata_cursor,
|
||||
&sock,
|
||||
cfg,
|
||||
running,
|
||||
@@ -314,9 +351,10 @@ fn run(
|
||||
// point — and this path has no rebuild closure (unlike the virtual-output path above), so a
|
||||
// re-admitted dead capturer wedged GameStream portal video permanently, at 10 s per reconnect
|
||||
// attempt. Dropping it instead costs one fresh screencast session on the next connect. Note
|
||||
// `result` may already be `Err` here, which is itself that signal.
|
||||
// `result` may already be `Err` here, which is itself that signal. (`metadata_cursor` rides
|
||||
// along as the second reuse key, beside HDR-ness — see `PooledCapturer`.)
|
||||
if result.is_ok() && capturer.is_alive() {
|
||||
*video_cap.lock().unwrap() = Some((capturer, cfg.hdr));
|
||||
*video_cap.lock().unwrap() = Some((capturer, cfg.hdr, metadata_cursor));
|
||||
} else {
|
||||
tracing::info!(
|
||||
stream_failed = result.is_err(),
|
||||
@@ -662,6 +700,10 @@ fn stream_body(
|
||||
// Re-open the video source on capture loss (virtual-display path → follow a Desktop<->Game switch);
|
||||
// `None` for the portal/synthetic source, which has nothing to re-detect (propagate the error).
|
||||
rebuild: Option<&dyn Fn() -> Result<Box<dyn Capturer>>>,
|
||||
// The capture hands the encoder cursor bitmaps to composite (cursor-as-metadata negotiated
|
||||
// because the resolved backend blends — see the callers). `false` = the pointer is embedded
|
||||
// in the pixels (or absent), so the encoder is asked to composite nothing.
|
||||
cursor_blend: bool,
|
||||
sock: &UdpSocket,
|
||||
cfg: StreamConfig,
|
||||
running: &Arc<AtomicBool>,
|
||||
@@ -697,9 +739,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,
|
||||
// True only when THIS session's capture negotiated cursor-as-metadata — which the
|
||||
// callers grant only where the resolved backend composites (`cursor_blend_capable`).
|
||||
cursor_blend,
|
||||
)
|
||||
.context("open video encoder for stream")?;
|
||||
// Tell the encoder how deep the capturer lets it pipeline. Without this an in-place backend
|
||||
@@ -871,7 +913,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
|
||||
cursor_blend, // same capture cursor mode — see the first open
|
||||
)
|
||||
.context("reopen encoder after rebuild")?;
|
||||
// A rebuilt encoder starts unconfigured — same reason as the first open above.
|
||||
|
||||
Reference in New Issue
Block a user