fix(capture/mutter): drop stale re-delivered frames (the GNOME flash)
ci / web (push) Failing after 40s
apple / swift (push) Successful in 1m17s
ci / docs-site (push) Failing after 37s
ci / rust (push) Successful in 1m20s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 2m53s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (push) Successful in 5m11s
ci / web (push) Failing after 40s
apple / swift (push) Successful in 1m17s
ci / docs-site (push) Failing after 37s
ci / rust (push) Successful in 1m20s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 2m53s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (push) Successful in 5m11s
Instrumented worker-3: even on the ordered FORCE_SHM download path, Mutter re-delivers COMPLETE OLD pool buffers — 655 frames in a 15 s session whose content exactly matched an earlier frame (not damage-incremental; full old frames, in runs, ~45% during motion). NVIDIA gives no fence to prevent it, so the producer delivery can't be made clean from our side. Detect it and drop it: hash a spatial sample of each captured frame; a frame whose content equals an EARLIER distinct frame (vs the current one, whose duplicates pass through) is a stale re-delivery — skip it so the encoder never emits the flash (try_latest re-sends the last good frame; brief hold instead of a backward jump). Runs on the CPU/SHM path (where Mutter+NVIDIA capture lives); never triggers on static content or non-Mutter compositors (no reverts). PUNKTFUNK_KEEP_STALE=1 disables it for A/B. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -466,6 +466,15 @@ mod pipewire {
|
|||||||
negotiated: Arc<AtomicBool>,
|
negotiated: Arc<AtomicBool>,
|
||||||
/// Present when zero-copy is enabled: imports a dmabuf → CUDA device buffer.
|
/// Present when zero-copy is enabled: imports a dmabuf → CUDA device buffer.
|
||||||
importer: Option<crate::zerocopy::EglImporter>,
|
importer: Option<crate::zerocopy::EglImporter>,
|
||||||
|
/// GNOME stale-frame filter state. Mutter re-delivers complete OLD pool buffers on the SHM
|
||||||
|
/// download path (no GPU sync prevents it on NVIDIA); a captured frame whose content equals
|
||||||
|
/// an EARLIER distinct frame (not the immediately-previous one — that's a normal duplicate) is
|
||||||
|
/// such a stale re-delivery and gets DROPPED so the encoder never emits the flash. `stale_seen`
|
||||||
|
/// counts drops; `keep_stale` (PUNKTFUNK_KEEP_STALE=1) turns the filter off for A/B testing.
|
||||||
|
stale_recent: std::collections::VecDeque<u64>,
|
||||||
|
stale_last: u64,
|
||||||
|
stale_seen: u64,
|
||||||
|
keep_stale: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log a frame-drop reason once per process (the process callback runs per frame; a stuck
|
/// Log a frame-drop reason once per process (the process callback runs per frame; a stuck
|
||||||
@@ -786,6 +795,10 @@ mod pipewire {
|
|||||||
active,
|
active,
|
||||||
negotiated,
|
negotiated,
|
||||||
importer,
|
importer,
|
||||||
|
stale_recent: std::collections::VecDeque::new(),
|
||||||
|
stale_last: 0,
|
||||||
|
stale_seen: 0,
|
||||||
|
keep_stale: std::env::var_os("PUNKTFUNK_KEEP_STALE").is_some(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream = pw::stream::StreamBox::new(
|
let stream = pw::stream::StreamBox::new(
|
||||||
@@ -1022,6 +1035,44 @@ mod pipewire {
|
|||||||
tight[y * row..y * row + row]
|
tight[y * row..y * row + row]
|
||||||
.copy_from_slice(®ion[y * stride..y * stride + row]);
|
.copy_from_slice(®ion[y * stride..y * stride + row]);
|
||||||
}
|
}
|
||||||
|
// GNOME stale-frame filter: Mutter re-delivers complete OLD pool buffers on the SHM
|
||||||
|
// download path (no NVIDIA fence prevents it). A frame whose sampled content equals an
|
||||||
|
// EARLIER distinct frame is a stale re-delivery — DROP it so the encoder never emits
|
||||||
|
// the flash (try_latest then re-sends the last good frame). Hashes a spatial sample;
|
||||||
|
// a real return-to-prior-state is rare and dropping it is harmless. KEEP_STALE=1 = off.
|
||||||
|
{
|
||||||
|
let s: &[u8] = &tight;
|
||||||
|
let step = (s.len() / 1024).max(1);
|
||||||
|
let mut hh: u64 = 0xcbf2_9ce4_8422_2325;
|
||||||
|
let mut i = 0;
|
||||||
|
while i < s.len() {
|
||||||
|
hh = (hh ^ s[i] as u64).wrapping_mul(0x0100_0000_01b3);
|
||||||
|
i += step;
|
||||||
|
}
|
||||||
|
// `stale_last` = the current (newest) frame's hash. A frame equal to it is a normal
|
||||||
|
// duplicate (deliver). A frame equal to an OLDER distinct frame in `stale_recent`
|
||||||
|
// is a stale re-delivery (drop). Anything else is new forward content (deliver).
|
||||||
|
if hh == ud.stale_last {
|
||||||
|
// duplicate of the current frame — deliver as usual
|
||||||
|
} else if ud.stale_recent.contains(&hh) {
|
||||||
|
ud.stale_seen += 1;
|
||||||
|
if ud.stale_seen.is_power_of_two() {
|
||||||
|
tracing::warn!(
|
||||||
|
dropped = ud.stale_seen,
|
||||||
|
"GNOME stale-frame dropped (capture reverted to an earlier frame)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !ud.keep_stale {
|
||||||
|
return; // drop the stale re-delivery — don't advance `stale_last`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ud.stale_recent.push_back(hh);
|
||||||
|
if ud.stale_recent.len() > 24 {
|
||||||
|
ud.stale_recent.pop_front();
|
||||||
|
}
|
||||||
|
ud.stale_last = hh;
|
||||||
|
}
|
||||||
|
}
|
||||||
let pts_ns = SystemTime::now()
|
let pts_ns = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.map(|d| d.as_nanos() as u64)
|
.map(|d| d.as_nanos() as u64)
|
||||||
|
|||||||
Reference in New Issue
Block a user