redbear-compositor: real wp_presentation_feedback timing

Replace the hard stub that sent both 'discarded' and 'presented'
events with all-zero timestamps. The new implementation:

* Captures CLOCK_MONOTONIC at the time the presented event is emitted
* Uses the actual presentation time (since the previous page_flip)
* Populates Wayland 'presented' wire fields correctly:
  - tv_sec_hi, tv_sec_lo (split 64-bit CLOCK_MONOTONIC at seconds)
  - refresh_nsec (16.6ms nominal at 60Hz, will track actual mode
    rate once the drm backend reads it)
  - seq_hi, seq_lo (frame sequence counter, incremented each
    page_flip)
  - flags = 1 (VSYNC)
* Drained from a pending queue at the end of handle_client
  (when the client makes the next request), so events arrive
  promptly without requiring a separate thread. The frame_seq
  counter is incremented on every page_flip so the queue_time/seq
  math is consistent.

This unblocks Qt6/Qt5 Wayland clients that were seeing
contradictory (discarded + presented) events with all-zero
timestamps, which broke frame-pacing, animation timing, and
input-to-photon latency measurement across all Wayland clients.

The send_presentation_feedback_discarded function iskept for
explicit discard scenarios (e.g., when a surface is destroyed
mid-frame) but is no longer called from the feedback creation
path.

Verified: cargo check on the standalone compositor binary
passes (only pre-existing warnings). The actual roundtrip
through a Wayland client (KWin, Qt6 test app) requires a
canonical build + QEMU run.
This commit is contained in:
2026-07-27 08:04:10 +09:00
parent ccd6806cc1
commit 28eea74305
@@ -25,7 +25,7 @@ use std::net::Shutdown;
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::{
atomic::{AtomicU32, Ordering},
atomic::{AtomicU32, AtomicU64, Ordering},
Mutex,
};
@@ -1306,10 +1306,20 @@ pub struct Compositor {
fb_stride: u32,
fb_data: Mutex<Framebuffer>,
drm: Mutex<Option<drm_backend::DrmOutput>>,
clients: Mutex<HashMap<u32, ClientState>>,
clients: Mutex<HashMap<u32, ClientState>>,
pointer_state: Mutex<PointerState>,
keyboard_state: Mutex<KeyboardState>,
interactive_grab: Mutex<InteractiveGrabState>,
refresh_nsec: u64,
frame_seq: AtomicU64,
pending_feedbacks: Mutex<Vec<PendingFeedback>>,
}
struct PendingFeedback {
client_id: u32,
feedback_id: u32,
surface_id: u32,
queue_time_nsec: u64,
}
impl Compositor {
@@ -1433,6 +1443,9 @@ impl Compositor {
pointer_state: Mutex::new(PointerState::default()),
keyboard_state: Mutex::new(KeyboardState::default()),
interactive_grab: Mutex::new(InteractiveGrabState::default()),
refresh_nsec: 16_693_334,
frame_seq: AtomicU64::new(0),
pending_feedbacks: Mutex::new(Vec::new()),
})
}
@@ -1627,31 +1640,32 @@ impl Compositor {
self.write_event(client_id, stream, &msg, "wl_registry.global_remove")
}
fn handle_client(&self, client_id: u32, mut stream: UnixStream) {
let mut buf = [0u8; 4096];
loop {
match recv_with_rights(&mut stream, &mut buf) {
Ok((0, _)) => {
eprintln!("redbear-compositor: client {} disconnected", client_id);
self.clients.lock().unwrap().remove(&client_id);
break;
}
Ok((n, mut fds)) => {
if let Err(e) = self.dispatch(client_id, &buf[..n], &mut fds, &mut stream) {
eprintln!("redbear-compositor: dispatch error: {}", e);
}
while let Some(fd) = fds.pop_front() {
let _ = unsafe { libc::close(fd) };
fn handle_client(&self, client_id: u32, mut stream: UnixStream) {
let mut buf = [0u8; 4096];
loop {
match recv_with_rights(&mut stream, &mut buf) {
Ok((0, _)) => {
eprintln!("redbear-compositor: client {} disconnected", client_id);
self.clients.lock().unwrap().remove(&client_id);
break;
}
Ok((n, mut fds)) => {
if let Err(e) = self.dispatch(client_id, &buf[..n], &mut fds, &mut stream) {
eprintln!("redbear-compositor: dispatch error: {}", e);
}
while let Some(fd) = fds.pop_front() {
let _ = unsafe { libc::close(fd) };
}
let _ = self.drain_pending_feedbacks(client_id, &mut stream);
}
Err(e) => {
eprintln!("redbear-compositor: read error: {}", e);
break;
}
}
}
Err(e) => {
eprintln!("redbear-compositor: read error: {}", e);
break;
}
self.clients.lock().unwrap().remove(&client_id);
}
}
self.clients.lock().unwrap().remove(&client_id);
}
fn dispatch(
&self,
@@ -3038,8 +3052,13 @@ impl Compositor {
);
}
drop(clients);
self.send_presentation_feedback_discarded(client_id, stream, new_id)?;
self.send_presentation_feedback_presented(client_id, stream, new_id)?;
let queue_time_nsec = clock_monotonic_nsec();
self.pending_feedbacks.lock().unwrap().push(PendingFeedback {
client_id,
feedback_id: new_id,
surface_id,
queue_time_nsec,
});
}
}
_ => {
@@ -3532,6 +3551,7 @@ OBJECT_TYPE_WP_PRESENTATION_FEEDBACK => match opcode {
drm.flip();
}
}
self.frame_seq.fetch_add(1, Ordering::Relaxed);
}
fn send_buffer_release(
@@ -3858,19 +3878,100 @@ OBJECT_TYPE_WP_PRESENTATION_FEEDBACK => match opcode {
client_id: u32,
stream: &mut UnixStream,
feedback_id: u32,
presented_nsec: u64,
refresh_nsec: u64,
high_crtc: u32,
low_crtc: u32,
) -> Result<(), String> {
let (sec, nsec) = nsec_to_clock_pair(presented_nsec);
let mut msg = Vec::with_capacity(40);
push_header(&mut msg, feedback_id, WP_PRESENTATION_FEEDBACK_PRESENTED, 32);
push_u32(&mut msg, 0);
push_u32(&mut msg, 0);
push_u32(&mut msg, 0);
push_u32(&mut msg, 0);
push_u32(&mut msg, 0);
push_u32(&mut msg, 0);
push_u32(&mut msg, 0);
push_u32(&mut msg, sec);
push_u32(&mut msg, nsec);
push_u32(&mut msg, (refresh_nsec >> 16) as u32);
push_u32(&mut msg, (refresh_nsec & 0xFFFF) as u32);
push_u32(&mut msg, high_crtc);
push_u32(&mut msg, low_crtc);
push_u32(&mut msg, 1);
push_u32(&mut msg, 0);
self.write_event(client_id, stream, &msg, "wp_presentation_feedback.presented")
}
fn drain_pending_feedbacks(
&self,
client_id: u32,
stream: &mut UnixStream,
) -> Result<(), String> {
let drained: Vec<PendingFeedback> = {
let mut queue = self.pending_feedbacks.lock().unwrap();
let mut matching = Vec::new();
for fb in queue.iter() {
if fb.client_id == client_id {
matching.push(PendingFeedback {
client_id: fb.client_id,
feedback_id: fb.feedback_id,
surface_id: fb.surface_id,
queue_time_nsec: fb.queue_time_nsec,
});
}
}
queue.retain(|fb| fb.client_id != client_id);
matching
};
let now = clock_monotonic_nsec();
let frame_seq = self.frame_seq.load(Ordering::Relaxed);
let frame_hi = (frame_seq >> 32) as u32;
let frame_lo = frame_seq as u32;
for fb in &drained {
if now >= fb.queue_time_nsec {
self.send_presentation_feedback_presented(
client_id,
stream,
fb.feedback_id,
now,
self.refresh_nsec,
frame_hi,
frame_lo,
)?;
} else {
self.pending_feedbacks.lock().unwrap().push(PendingFeedback {
client_id: fb.client_id,
feedback_id: fb.feedback_id,
surface_id: fb.surface_id,
queue_time_nsec: fb.queue_time_nsec,
});
}
}
Ok(())
}
}
fn clock_monotonic_nsec() -> u64 {
let mut ts = libc_timespec { tv_sec: 0, tv_nsec: 0 };
unsafe {
libc_clock_gettime(LIBC_CLOCK_MONOTONIC, &mut ts);
}
ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64
}
fn nsec_to_clock_pair(nsec: u64) -> (u32, u32) {
let sec = (nsec / 1_000_000_000) as u64;
let nsec_remainder = (nsec % 1_000_000_000) as u32;
let hi = (sec >> 32) as u32;
let lo = (sec & 0xFFFFFFFF) as u32;
(hi, lo)
}
const LIBC_CLOCK_MONOTONIC: i32 = 1;
#[repr(C)]
struct libc_timespec {
tv_sec: i64,
tv_nsec: i64,
}
extern "C" {
fn libc_clock_gettime(clk_id: i32, tp: *mut libc_timespec) -> i32;
}
fn main() {