Skip to content

feat(linux/xdgportal): use accurate frame timestamps from PipeWire metadata#4780

Open
psyke83 wants to merge 3 commits intoLizardByte:masterfrom
psyke83:xdg_pw-timestamps
Open

feat(linux/xdgportal): use accurate frame timestamps from PipeWire metadata#4780
psyke83 wants to merge 3 commits intoLizardByte:masterfrom
psyke83:xdg_pw-timestamps

Conversation

@psyke83
Copy link
Contributor

@psyke83 psyke83 commented Feb 26, 2026

Description

feat(linux/xdgportal): use accurate frame timestamps from PipeWire metadata

On new frame:

  • Sample Sunshine's steady_clock and PipeWire clock.
  • Measure frame age using difference between PW clock "now" and pts.
  • Sanity check age (limit of 100ms or 5 frames in past, 2ms in future).
    This is needed to filter large time differences in the last received pts
    when the desktop goes idle, clock is reset, etc.
  • If age check passes, use hardware timestamp, otherwise use steady clock.

(Note: this PR temporarily includes the event driven capture changes of #4768 as the are a prerequisite).

Screenshot

Issues Fixed or Closed

Roadmap Issues

Type of Change

  • feat: New feature (non-breaking change which adds functionality)
  • fix: Bug fix (non-breaking change which fixes an issue)
  • docs: Documentation only changes
  • style: Changes that do not affect the meaning of the code (white-space, formatting, missing semicolons, etc.)
  • refactor: Code change that neither fixes a bug nor adds a feature
  • perf: Code change that improves performance
  • test: Adding missing tests or correcting existing tests
  • build: Changes that affect the build system or external dependencies
  • ci: Changes to CI configuration files and scripts
  • chore: Other changes that don't modify src or test files
  • revert: Reverts a previous commit
  • BREAKING CHANGE: Introduces a breaking change (can be combined with any type above)

Checklist

  • Code follows the style guidelines of this project
  • Code has been self-reviewed
  • Code has been commented, particularly in hard-to-understand areas
  • Code docstring/documentation-blocks for new or existing methods/components have been added or updated
  • Unit tests have been added or updated for any new or modified functionality

AI Usage

  • None: No AI tools were used in creating this PR
  • Light: AI provided minor assistance (formatting, simple suggestions)
  • Moderate: AI helped with code generation or debugging specific parts
  • Heavy: AI generated most or all of the code changes

@psyke83
Copy link
Contributor Author

psyke83 commented Feb 26, 2026

Pinging @andygrundman in case you want to try it out.

This PR temporarily includes the same commits in #4768 as a prerequisite.

When in-game, the real timestamps are being processed consecutively without any noticeable breaks, and generally they look a bit more inflated than the current (less accurate) steady_clock::now() sampling. Unless I made a mistake, however, these larger values should be more reflective of the actual compositor overhead rather than simply sampling the time when the buffer is received on Sunshine's side. When the desktop is idle, it falls back to the inaccurate sampling due to the "pw now - pts" delta being too large, but reverts back to the hardware timestamps when fresh data is sampled that meets the age check thresholds.

Unfortunately, this current logic doesn't seem to work well on mutter, as it reports very low min/avg/max host latency values (sub 0.3ms, for ex), so it either needs work, or perhaps pw timestamps are unworkable due to compositor variance in pts calculations.

@psyke83 psyke83 marked this pull request as ready for review February 26, 2026 10:18
@ReenigneArcher ReenigneArcher added this to the xdg portal grab milestone Feb 26, 2026
Summary of changes:
* Synchronized capture:
  - Move frame_cv wait to beginning of loop as a more efficient way to
    wait for a new buffer whilst ensuring that unintentional buffer
    reuse doesn't occur.
* Improved duplicate detection:
  - Transition to duplicate detection logic using a combination
    of SPA_META_Header (PTS) and SPA_META_VideoDamage metadata.
  - Drop frames when PTS delta is zero and no damage is reported;
    this helps to smoothen out stuttering unique to KWin.
* Destroying the core and context during a reinit can cause a deadlock
which can be reproduced when PW_STREAM_FLAG_RT_PROCESS is set. Fix
by ensuring that full teardown is only done in pipewire destructor.
* Add a delay before sending the interrupt signal, as the GNOME
XDG portal needs time to detect changes, otherwise Sunshine will
crash when the users manually stops the stream on mutter.
…tadata

On new frame:
* Sample Sunshine's steady_clock and PipeWire clock.
* Measure frame age using difference between PW clock "now" and pts.
* Sanity check age (limit of 100ms or 5 frames in past, half a frame delay in future).
  This is needed to filter large time differences in the last received pts
  when the desktop goes idle, clock is reset, etc.
* If age check passes, use hardware timestamp, otherwise use steady clock.
@sonarqubecloud
Copy link

last_seq = img_egl->seq.value();
last_pts = img_egl->pts.value();

// Snapshot both clock domains simultaneously
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is all this time code doing? If the frame is a duplicate, frame_timestamp should be left null. It gets filled in by code around stream.cpp:1471.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update_frame_metadata() is only called on frames that pass a duplicate and snapshot timeout check. Previously, portalgrab's snapshot timeout was a no-op, but reactive capture (and the updates in this PR) make it actually block for new data, so it was only timing out for extreme cases where no data was being received.

The timeout is 1000ms, so only frames that are extremely late will hit the timeout.

img_egl->frame_timestamp = steady_now;

if (age_ns < future_tolerance_ns) {
BOOST_LOG(debug) << "Frame age was future dated: " << (age_ns / 1000) << "us";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this code will be removed but just FYI, use verbose log level for anything that logs very frequently or in a loop, etc.

Copy link
Contributor Author

@psyke83 psyke83 Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good advice, but I suspect any logging level might be too chatty to be included in a final PR state. Right now, the change is not appropriate as there is just way too much compositor variance to be confident that this is working as expected, so either it needs some changes to the logic, or might just be better to drop it entirely. More eyes on the code might help figure out if the former is possible.

In the case of KWin (6.6.1), stale PTS data explodes the host latency when returning from idle - that necessitates the positive (past) age check. In the case of mutter, I don't think that frames returning from idle have stale pts timestamps, but it has the opposite issue where timestamps end up consistently future dated sometimes by as high as -2ms for an active stream running at 60fps.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, if we can't trust pipewire it's probably fine to just use now().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants