feat(capture): true SHM path (PUNKTFUNK_FORCE_SHM) for race-free Mutter+NVIDIA
ci / web (push) Failing after 37s
apple / swift (push) Failing after 1m3s
ci / rust (push) Failing after 1m11s
ci / docs-site (push) Failing after 43s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
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 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 2m55s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (push) Successful in 5m17s
ci / web (push) Failing after 37s
apple / swift (push) Failing after 1m3s
ci / rust (push) Failing after 1m11s
ci / docs-site (push) Failing after 43s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
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 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 2m55s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (push) Successful in 5m17s
Empirically, Mutter+NVIDIA dmabuf capture has NO working GPU sync — confirmed on worker-3: explicit sync fails buffer alloc (EINVAL, no cogl sync_fd), and the dmabuf carries no implicit fence (EXPORT_SYNC_FILE waited=false). So any dmabuf read — zero-copy import OR mmap — races Mutter's render and flashes the buffer's previous frame. The prior "CPU fallback" still listed DmaBuf in its buffer types, so Mutter kept handing dmabufs and it never fixed anything (got worse). PUNKTFUNK_FORCE_SHM=1 offers MemPtr+MemFd ONLY (no DmaBuf), forcing Mutter to glReadPixels-download into mappable memory — which orders against its render, so the frame is complete + current by construction (race-free). Costs the download (~3 ms) + zero-copy; correct at 1080p/4K60. KWin/gamescope are unaffected (they blit into the buffer, no read-before-render race) and keep zero-copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -665,6 +665,29 @@ mod pipewire {
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a Buffers param for a TRUE SHM path: MemPtr + MemFd only, NO DmaBuf. Forces the
|
||||
/// producer to download into mappable memory (Mutter's `glReadPixels`), which orders against its
|
||||
/// render — so the frame is complete and current by construction. This is the only race-free
|
||||
/// capture of Mutter's virtual monitor on NVIDIA: the compositor renders straight into the buffer
|
||||
/// pool, NVIDIA attaches no implicit dmabuf fence (verified: `EXPORT_SYNC_FILE` waited=false) and
|
||||
/// can't produce an explicit sync_fd, so any dmabuf read (zero-copy OR mmap) races the render and
|
||||
/// flashes the buffer's previous frame. Excluding DmaBuf is what makes the difference vs.
|
||||
/// `build_mappable_buffers` (which still let Mutter hand dmabufs).
|
||||
fn build_shm_only_buffers() -> Result<Vec<u8>> {
|
||||
serialize_pod(pw::spa::pod::Object {
|
||||
type_: pw::spa::utils::SpaTypes::ObjectParamBuffers.as_raw(),
|
||||
id: pw::spa::param::ParamType::Buffers.as_raw(),
|
||||
properties: vec![pw::spa::pod::Property {
|
||||
key: pw::spa::sys::SPA_PARAM_BUFFERS_dataType,
|
||||
flags: pw::spa::pod::PropertyFlags::empty(),
|
||||
value: pw::spa::pod::Value::Int(
|
||||
(1i32 << pw::spa::sys::SPA_DATA_MemPtr)
|
||||
| (1i32 << pw::spa::sys::SPA_DATA_MemFd),
|
||||
),
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a Buffers param requesting dmabuf-only buffers.
|
||||
fn build_dmabuf_buffers() -> Result<Vec<u8>> {
|
||||
serialize_pod(pw::spa::pod::Object {
|
||||
@@ -736,8 +759,16 @@ mod pipewire {
|
||||
if importer.is_some() && !modifiers.contains(&0) {
|
||||
modifiers.push(0); // DRM_FORMAT_MOD_LINEAR
|
||||
}
|
||||
let want_dmabuf = importer.is_some() && !modifiers.is_empty();
|
||||
if zerocopy && !want_dmabuf {
|
||||
// PUNKTFUNK_FORCE_SHM=1 forces the race-free download path (SHM, no dmabuf) — required on
|
||||
// Mutter+NVIDIA where dmabuf capture has no working sync and shows stale frames. KWin/
|
||||
// gamescope don't need it (they blit into the buffer, so no read-before-render race).
|
||||
let force_shm = std::env::var("PUNKTFUNK_FORCE_SHM").as_deref() == Ok("1");
|
||||
let want_dmabuf = importer.is_some() && !modifiers.is_empty() && !force_shm;
|
||||
if force_shm {
|
||||
tracing::info!(
|
||||
"capture: PUNKTFUNK_FORCE_SHM — race-free SHM download path (no dmabuf, no zero-copy)"
|
||||
);
|
||||
} else if zerocopy && !want_dmabuf {
|
||||
tracing::warn!("zero-copy: no EGL-importable dmabuf modifiers — using CPU path");
|
||||
} else if want_dmabuf {
|
||||
tracing::info!(
|
||||
@@ -1069,6 +1100,9 @@ mod pipewire {
|
||||
Some(build_dmabuf_format(&modifiers, preferred)?),
|
||||
Some(build_dmabuf_buffers()?),
|
||||
)
|
||||
} else if force_shm {
|
||||
// True SHM: exclude DmaBuf so Mutter MUST download (glReadPixels orders against render).
|
||||
(None, Some(build_shm_only_buffers()?))
|
||||
} else {
|
||||
// CPU path still accepts mappable dmabufs (gamescope offers only those once its
|
||||
// modifier-bearing format pod wins the intersection).
|
||||
|
||||
Reference in New Issue
Block a user