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:
@@ -25,7 +25,7 @@ use std::net::Shutdown;
|
|||||||
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
|
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
|
||||||
use std::os::unix::net::{UnixListener, UnixStream};
|
use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicU32, Ordering},
|
atomic::{AtomicU32, AtomicU64, Ordering},
|
||||||
Mutex,
|
Mutex,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1310,6 +1310,16 @@ pub struct Compositor {
|
|||||||
pointer_state: Mutex<PointerState>,
|
pointer_state: Mutex<PointerState>,
|
||||||
keyboard_state: Mutex<KeyboardState>,
|
keyboard_state: Mutex<KeyboardState>,
|
||||||
interactive_grab: Mutex<InteractiveGrabState>,
|
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 {
|
impl Compositor {
|
||||||
@@ -1433,6 +1443,9 @@ impl Compositor {
|
|||||||
pointer_state: Mutex::new(PointerState::default()),
|
pointer_state: Mutex::new(PointerState::default()),
|
||||||
keyboard_state: Mutex::new(KeyboardState::default()),
|
keyboard_state: Mutex::new(KeyboardState::default()),
|
||||||
interactive_grab: Mutex::new(InteractiveGrabState::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")
|
self.write_event(client_id, stream, &msg, "wl_registry.global_remove")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_client(&self, client_id: u32, mut stream: UnixStream) {
|
fn handle_client(&self, client_id: u32, mut stream: UnixStream) {
|
||||||
let mut buf = [0u8; 4096];
|
let mut buf = [0u8; 4096];
|
||||||
loop {
|
loop {
|
||||||
match recv_with_rights(&mut stream, &mut buf) {
|
match recv_with_rights(&mut stream, &mut buf) {
|
||||||
Ok((0, _)) => {
|
Ok((0, _)) => {
|
||||||
eprintln!("redbear-compositor: client {} disconnected", client_id);
|
eprintln!("redbear-compositor: client {} disconnected", client_id);
|
||||||
self.clients.lock().unwrap().remove(&client_id);
|
self.clients.lock().unwrap().remove(&client_id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Ok((n, mut fds)) => {
|
Ok((n, mut fds)) => {
|
||||||
if let Err(e) = self.dispatch(client_id, &buf[..n], &mut fds, &mut stream) {
|
if let Err(e) = self.dispatch(client_id, &buf[..n], &mut fds, &mut stream) {
|
||||||
eprintln!("redbear-compositor: dispatch error: {}", e);
|
eprintln!("redbear-compositor: dispatch error: {}", e);
|
||||||
}
|
}
|
||||||
while let Some(fd) = fds.pop_front() {
|
while let Some(fd) = fds.pop_front() {
|
||||||
let _ = unsafe { libc::close(fd) };
|
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) => {
|
self.clients.lock().unwrap().remove(&client_id);
|
||||||
eprintln!("redbear-compositor: read error: {}", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
self.clients.lock().unwrap().remove(&client_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dispatch(
|
fn dispatch(
|
||||||
&self,
|
&self,
|
||||||
@@ -3038,8 +3052,13 @@ impl Compositor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
drop(clients);
|
drop(clients);
|
||||||
self.send_presentation_feedback_discarded(client_id, stream, new_id)?;
|
let queue_time_nsec = clock_monotonic_nsec();
|
||||||
self.send_presentation_feedback_presented(client_id, stream, new_id)?;
|
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();
|
drm.flip();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.frame_seq.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_buffer_release(
|
fn send_buffer_release(
|
||||||
@@ -3858,19 +3878,100 @@ OBJECT_TYPE_WP_PRESENTATION_FEEDBACK => match opcode {
|
|||||||
client_id: u32,
|
client_id: u32,
|
||||||
stream: &mut UnixStream,
|
stream: &mut UnixStream,
|
||||||
feedback_id: u32,
|
feedback_id: u32,
|
||||||
|
presented_nsec: u64,
|
||||||
|
refresh_nsec: u64,
|
||||||
|
high_crtc: u32,
|
||||||
|
low_crtc: u32,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
let (sec, nsec) = nsec_to_clock_pair(presented_nsec);
|
||||||
let mut msg = Vec::with_capacity(40);
|
let mut msg = Vec::with_capacity(40);
|
||||||
push_header(&mut msg, feedback_id, WP_PRESENTATION_FEEDBACK_PRESENTED, 32);
|
push_header(&mut msg, feedback_id, WP_PRESENTATION_FEEDBACK_PRESENTED, 32);
|
||||||
push_u32(&mut msg, 0);
|
push_u32(&mut msg, sec);
|
||||||
push_u32(&mut msg, 0);
|
push_u32(&mut msg, nsec);
|
||||||
push_u32(&mut msg, 0);
|
push_u32(&mut msg, (refresh_nsec >> 16) as u32);
|
||||||
push_u32(&mut msg, 0);
|
push_u32(&mut msg, (refresh_nsec & 0xFFFF) as u32);
|
||||||
push_u32(&mut msg, 0);
|
push_u32(&mut msg, high_crtc);
|
||||||
push_u32(&mut msg, 0);
|
push_u32(&mut msg, low_crtc);
|
||||||
push_u32(&mut msg, 0);
|
push_u32(&mut msg, 1);
|
||||||
push_u32(&mut msg, 0);
|
push_u32(&mut msg, 0);
|
||||||
self.write_event(client_id, stream, &msg, "wp_presentation_feedback.presented")
|
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() {
|
fn main() {
|
||||||
|
|||||||
Reference in New Issue
Block a user