chore: consolidate parallel-session WIP (HOLD — do not push)
Local snapshot of intermingled in-flight work, committed to unblock the encode
refactor (a clean ffmpeg_win.rs for the vbv-dedup follow-on). These hunks span
the same files and can't be cleanly split here; the commit bundles three
distinct workstreams that each belong in their own PR:
- logging rework (~43 files: level re-tiering, structured fields, `?e`,
hot-path flood latches)
- conflicting-host detection (detect.rs + detect/{linux,windows}.rs + wiring
in main.rs/mgmt.rs/Cargo.toml/docs/packaging)
- standby-sink DWM-stall attribution (windows/display_events.rs + capture/
vdisplay wiring)
NOT verified as a combination. NOT to be pushed until the refactor is done and
these are re-verified and reorganized into their proper per-workstream PRs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -650,8 +650,6 @@ mod pipewire {
|
||||
/// Bumps whenever the bitmap (`rgba`/`bw`/`bh`) changes — stable across position-only moves,
|
||||
/// so the GPU encoder re-uploads its cursor texture only on change.
|
||||
serial: u64,
|
||||
/// One-shot guard for the "cursor present but this frame is zero-copy" notice.
|
||||
warned_zerocopy: bool,
|
||||
}
|
||||
|
||||
impl CursorState {
|
||||
@@ -1174,22 +1172,6 @@ mod pipewire {
|
||||
if ud.broken.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
// Cursor-as-metadata only reaches the frame on the CPU de-pad path below (a small
|
||||
// straight-alpha blit). The zero-copy paths hand a GPU-resident buffer straight to the
|
||||
// encoder, so the cached cursor can't be composited here — that needs a GPU blit in the
|
||||
// encoder (follow-up). Note it once, so a gamescope host (zero-copy by default) shows in
|
||||
// the logs that the metadata IS arriving even while the overlay isn't drawn yet.
|
||||
if ud.cursor.visible
|
||||
&& !ud.cursor.warned_zerocopy
|
||||
&& (ud.importer.is_some() || ud.vaapi_passthrough)
|
||||
{
|
||||
ud.cursor.warned_zerocopy = true;
|
||||
tracing::warn!(
|
||||
"cursor metadata received, but frames are delivered zero-copy (GPU-resident) — \
|
||||
the cursor overlay is composited only on the CPU capture path today; GPU-path \
|
||||
compositing (Vulkan/CUDA/VAAPI encode) is a follow-up"
|
||||
);
|
||||
}
|
||||
// SAFETY: `spa_buf` is the `*mut spa_buffer` of the PipeWire buffer we dequeued and still hold for
|
||||
// this `.process` callback (not requeued until after `consume_frame` returns), so it is live. The
|
||||
// block null-checks `spa_buf`, requires `n_datas != 0`, and null-checks the `datas` array pointer
|
||||
@@ -1241,7 +1223,7 @@ mod pipewire {
|
||||
std::sync::atomic::AtomicBool::new(true);
|
||||
if F2.swap(false, Ordering::Relaxed) {
|
||||
tracing::warn!(
|
||||
error = %format!("{e}"),
|
||||
error = %e,
|
||||
"dmabuf EXPORT_SYNC_FILE failed — no implicit-fence sync; NVIDIA \
|
||||
zero-copy may show stale frames (no producer explicit sync)"
|
||||
);
|
||||
@@ -1915,7 +1897,15 @@ mod pipewire {
|
||||
unsafe { stream.queue_raw_buffer(newest) };
|
||||
}));
|
||||
if outcome.is_err() {
|
||||
tracing::error!("panic in pipewire process callback — frame dropped");
|
||||
// In the per-frame `.process` callback: a deterministic panic (e.g. a bad
|
||||
// format) would fire this every frame, so power-of-two throttle it — enough to
|
||||
// surface the fault without evicting the whole log ring.
|
||||
static PANICS: std::sync::atomic::AtomicU64 =
|
||||
std::sync::atomic::AtomicU64::new(0);
|
||||
let n = PANICS.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if n.is_power_of_two() {
|
||||
tracing::error!(count = n, "panic in pipewire process callback — frame dropped");
|
||||
}
|
||||
}
|
||||
})
|
||||
.register()
|
||||
@@ -1930,7 +1920,11 @@ mod pipewire {
|
||||
|
||||
// Request raw video in any encoder-mappable layout, any size/framerate.
|
||||
let obj = if let Some((fw, fh)) = fixed_pod {
|
||||
tracing::info!(fw, fh, "PW DEBUG: offering fixed BGRx pod");
|
||||
tracing::info!(
|
||||
fw,
|
||||
fh,
|
||||
"pipewire: offering a fixed BGRx format pod (PUNKTFUNK_PW_FIXED_POD)"
|
||||
);
|
||||
pw::spa::pod::object!(
|
||||
pw::spa::utils::SpaTypes::ObjectParamFormat,
|
||||
pw::spa::param::ParamType::EnumFormat,
|
||||
|
||||
@@ -299,7 +299,7 @@ pub(crate) fn install_gpu_pref_hook() {
|
||||
// 100% DuplicateOutput1 E_ACCESSDENIED is diagnosable instead of silent.
|
||||
match SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) {
|
||||
Ok(()) => tracing::info!("DPI awareness set: PER_MONITOR_AWARE_V2"),
|
||||
Err(e) => tracing::warn!(error = %format!("{e:?}"),
|
||||
Err(e) => tracing::warn!(error = ?e,
|
||||
"SetProcessDpiAwarenessContext failed (already set?) — DuplicateOutput1 may E_ACCESSDENIED"),
|
||||
}
|
||||
// 0=UNAWARE 1=SYSTEM 2=PER_MONITOR(_V2). DuplicateOutput1 needs 2.
|
||||
|
||||
@@ -595,7 +595,7 @@ impl DescriptorPoller {
|
||||
.map_err(|e| {
|
||||
// Degraded, not fatal: the session streams, it just never follows a mid-session
|
||||
// HDR flip / mode-set (seq stays 0 → the consumer sees no changes).
|
||||
tracing::error!(error = %e, "IDD push: descriptor-poller thread failed to spawn");
|
||||
tracing::warn!(error = %e, "IDD push: descriptor-poller thread failed to spawn — mid-session HDR/mode changes won't be followed");
|
||||
})
|
||||
.ok();
|
||||
Self { snap, stop, thread }
|
||||
@@ -753,6 +753,13 @@ pub struct IddPushCapturer {
|
||||
/// during active flow and warns when they turn metronomic — the sole-virtual-display
|
||||
/// periodic-stutter diagnostic.
|
||||
stall_watch: StallWatch,
|
||||
/// Stall↔OS-event correlation counters for the metronomic warn: how many stalls this session,
|
||||
/// and how many had a coinciding [`crate::display_events`] event in their gap window — the
|
||||
/// discriminator between "Windows re-enumerates a monitor each cycle" (devnode churn the
|
||||
/// `pnp_disable_monitors` axis suppresses) and "the disturbance is below the OS" (GPU driver
|
||||
/// servicing a standby sink / display-poller software).
|
||||
stalls_seen: u32,
|
||||
stalls_with_os_events: u32,
|
||||
/// Host-owned ROTATING output ring NVENC encodes (one YUV texture per slot). Rotating it per frame
|
||||
/// is the precondition for pipelining the encode loop: while NVENC encodes frame N's texture on the
|
||||
/// ASIC, frame N+1's convert writes a DIFFERENT texture — the two overlap. Format = `out_format()`:
|
||||
@@ -886,6 +893,9 @@ impl IddPushCapturer {
|
||||
want_444: bool,
|
||||
keepalive: Box<dyn Send>,
|
||||
) -> std::result::Result<Self, (anyhow::Error, Box<dyn Send>)> {
|
||||
// The stall-attribution listener (idempotent): started with the first IDD-push capturer so
|
||||
// the stall log can correlate DWM holes with OS display events for the session's lifetime.
|
||||
crate::display_events::spawn_once();
|
||||
match Self::open_inner(target, preferred, client_10bit, want_444) {
|
||||
Ok(mut me) => {
|
||||
me._keepalive = keepalive;
|
||||
@@ -1146,6 +1156,8 @@ impl IddPushCapturer {
|
||||
last_liveness: Instant::now(),
|
||||
last_kick: Instant::now(),
|
||||
stall_watch: StallWatch::new(),
|
||||
stalls_seen: 0,
|
||||
stalls_with_os_events: 0,
|
||||
out_ring: Vec::new(),
|
||||
out_idx: 0,
|
||||
video_conv: None,
|
||||
@@ -1646,24 +1658,70 @@ impl IddPushCapturer {
|
||||
// doesn't read as a DWM stall.
|
||||
self.stall_watch.reset();
|
||||
} else if let Some(stall) = self.stall_watch.note_fresh(now) {
|
||||
// OS display events inside the gap (plus a lead-in margin: the event that CAUSED the
|
||||
// hole lands just before DWM stops delivering) — the attribution that turns "DWM
|
||||
// stopped composing" into "…because Windows re-enumerated SAMSUNG on HDMI".
|
||||
let window = stall.gap + Duration::from_millis(300);
|
||||
let events = now
|
||||
.checked_sub(window)
|
||||
.map(|from| crate::display_events::events_between(from, now))
|
||||
.unwrap_or_default();
|
||||
self.stalls_seen = self.stalls_seen.saturating_add(1);
|
||||
if !events.is_empty() {
|
||||
self.stalls_with_os_events = self.stalls_with_os_events.saturating_add(1);
|
||||
}
|
||||
// debug (not warn): a single hole also happens when content legitimately pauses;
|
||||
// the reportable signal is the metronomic cycle below. Mounjay-class triage runs
|
||||
// at debug level, and the web-console debug ring captures these.
|
||||
tracing::debug!(
|
||||
gap_ms = stall.gap.as_millis() as u64,
|
||||
os_display_events = %crate::display_events::summarize(&events),
|
||||
"IDD-push capture stall — the desktop was composing at speed, then DWM \
|
||||
delivered no frame for the gap; the present path stalled below capture"
|
||||
);
|
||||
if let Some(period) = stall.metronomic {
|
||||
tracing::warn!(
|
||||
period_s = format!("{:.2}", period.as_secs_f64()),
|
||||
"capture stalls are METRONOMIC — DWM stops composing the virtual display \
|
||||
on a stable period, i.e. a periodic display-path disturbance BELOW \
|
||||
capture (DWM present clock / GPU driver / display-poller software). \
|
||||
Correlate with 'slow display-descriptor poll'; if that never fires, the \
|
||||
disturbance is outside punktfunk — try display topology=primary or \
|
||||
extend (keep a physical output active), or a different refresh rate"
|
||||
);
|
||||
let suspects = crate::display_events::connected_inactive_externals();
|
||||
let suspects = if suspects.is_empty() {
|
||||
"none".to_string()
|
||||
} else {
|
||||
suspects.join(", ")
|
||||
};
|
||||
let correlated = format!("{}/{}", self.stalls_with_os_events, self.stalls_seen);
|
||||
// Half-or-more of the stalls carrying a coinciding OS event = the reaction
|
||||
// cascade is OS-visible; otherwise the disturbance never surfaces above the
|
||||
// driver. Different classes, different cures — say which one this box has.
|
||||
if self.stalls_with_os_events * 2 >= self.stalls_seen {
|
||||
tracing::warn!(
|
||||
period_s = format!("{:.2}", period.as_secs_f64()),
|
||||
os_correlated = correlated,
|
||||
connected_inactive = %suspects,
|
||||
"capture stalls are METRONOMIC and coincide with Windows monitor \
|
||||
hot-plug/re-enumeration events — a connected display (or its \
|
||||
cable/switch/AVR) re-probes the link on a timer and Windows re-reacts \
|
||||
each time. Cures, best-first: that display's OSD 'auto input \
|
||||
scan/detect' OFF (and on TVs: instant-on/quick-start + CEC off), \
|
||||
unplug its cable at the GPU, an HPD-holding adapter/dummy plug, or \
|
||||
keep it active while streaming; the pnp_disable_monitors policy axis \
|
||||
suppresses the Windows-side reaction (see connected_inactive for the \
|
||||
suspects)"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
period_s = format!("{:.2}", period.as_secs_f64()),
|
||||
os_correlated = correlated,
|
||||
connected_inactive = %suspects,
|
||||
"capture stalls are METRONOMIC with NO coinciding OS display event — \
|
||||
the disturbance is BELOW Windows: the GPU driver servicing a \
|
||||
connected-but-asleep sink (standby HPD/DDC/link probing), \
|
||||
display-poller software (the SteelSeries-GG/SignalRGB class — \
|
||||
correlate 'slow display-descriptor poll' lines), or the DWM present \
|
||||
clock (try a different refresh rate). If connected_inactive lists a \
|
||||
display, its standby probing is the prime suspect: unplug it at the \
|
||||
GPU, disable its OSD auto input scan (TVs: instant-on/quick-start + \
|
||||
CEC off), use an HPD-holding adapter/dummy, or keep it active while \
|
||||
streaming"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.last_fresh = now; // feeds the driver-death watch
|
||||
|
||||
Reference in New Issue
Block a user