use std::sync::{
Mutex,
atomic::{AtomicBool, AtomicU8, Ordering},
};
use std::time::Instant;
use smallvec::SmallVec;
use smithay_client_toolkit::reexports::calloop::ping::Ping;
use crate::app_state::with_app_state;
use crate::reactive::invalidation::clear_widget_subscribers;
use crate::tree::{Tree, WidgetId};
#[derive(Default)]
struct JobQueue {
set: rustc_hash::FxHashSet<Job>,
vec: Vec<Job>,
}
impl JobQueue {
fn push(&mut self, job: Job) {
if self.set.insert(job) {
self.vec.push(job);
}
}
fn drain_all(&mut self, replacement: Vec<Job>) -> Vec<Job> {
self.set.clear();
std::mem::replace(&mut self.vec, replacement)
}
fn drain_non_animation(&mut self, mut buf: Vec<Job>) -> Vec<Job> {
self.vec.retain(|job| {
if job.job_type == JobType::Animation {
true
} else {
self.set.remove(job);
buf.push(*job);
false
}
});
buf
}
fn is_empty(&self) -> bool {
self.vec.is_empty()
}
}
#[derive(Default)]
pub(crate) struct JobQueues {
inbox: JobQueue,
per_root: rustc_hash::FxHashMap<WidgetId, JobQueue>,
orphans: JobQueue,
spare: Vec<Vec<Job>>,
}
impl JobQueues {
fn spare_buf(&mut self) -> Vec<Job> {
self.spare.pop().unwrap_or_default()
}
fn recycle(&mut self, mut buf: Vec<Job>) {
if self.spare.len() < 4 {
buf.clear();
self.spare.push(buf);
}
}
fn is_empty(&self) -> bool {
self.inbox.is_empty()
&& self.orphans.is_empty()
&& self.per_root.values().all(JobQueue::is_empty)
}
}
#[derive(Clone, Copy)]
pub(crate) struct ScheduledJob {
at: Instant,
job: Job,
request: JobRequest,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum JobType {
Layout,
Paint,
Reconcile,
Unregister,
Animation,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RequiredJob {
None,
Paint,
Layout,
}
#[derive(Clone, Copy, Debug)]
pub enum JobRequest {
Layout,
Paint,
Reconcile,
Unregister,
Animation(RequiredJob),
}
impl JobRequest {
fn job_type(self) -> JobType {
match self {
JobRequest::Layout => JobType::Layout,
JobRequest::Paint => JobType::Paint,
JobRequest::Reconcile => JobType::Reconcile,
JobRequest::Unregister => JobType::Unregister,
JobRequest::Animation(_) => JobType::Animation,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Job {
pub widget_id: WidgetId,
pub job_type: JobType,
}
pub fn request_job(widget_id: WidgetId, request: JobRequest) {
with_app_state(|app| {
let mut jobs = app.pending_jobs.borrow_mut();
let inbox = &mut jobs.inbox;
match request {
JobRequest::Animation(required) => {
inbox.push(Job {
widget_id,
job_type: JobType::Animation,
});
match required {
RequiredJob::None => {}
RequiredJob::Paint => {
inbox.push(Job {
widget_id,
job_type: JobType::Paint,
});
}
RequiredJob::Layout => {
inbox.push(Job {
widget_id,
job_type: JobType::Layout,
});
}
}
}
_ => {
let job_type = match request {
JobRequest::Layout => JobType::Layout,
JobRequest::Paint => JobType::Paint,
JobRequest::Reconcile => JobType::Reconcile,
JobRequest::Unregister => JobType::Unregister,
JobRequest::Animation(_) => unreachable!(),
};
inbox.push(Job {
widget_id,
job_type,
});
}
}
});
wake_loop();
}
pub fn request_job_at(widget_id: WidgetId, request: JobRequest, at: Instant) {
let job = Job {
widget_id,
job_type: request.job_type(),
};
with_app_state(|app| {
let mut scheduled = app.scheduled_jobs.borrow_mut();
match scheduled.iter_mut().find(|entry| entry.job == job) {
Some(entry) => {
entry.at = at;
entry.request = request;
}
None => scheduled.push(ScheduledJob { at, job, request }),
}
});
}
pub fn next_deadline() -> Option<Instant> {
with_app_state(|app| {
app.scheduled_jobs
.borrow()
.iter()
.map(|entry| entry.at)
.min()
})
}
pub fn promote_due_jobs() {
let due: SmallVec<[ScheduledJob; 4]> = with_app_state(|app| {
let now = Instant::now();
let mut scheduled = app.scheduled_jobs.borrow_mut();
let mut due = SmallVec::new();
scheduled.retain(|entry| {
if entry.at <= now {
due.push(*entry);
false
} else {
true
}
});
due
});
for entry in due {
request_job(entry.job.widget_id, entry.request);
}
}
pub(crate) fn cancel_scheduled_jobs(widget_id: WidgetId) {
with_app_state(|app| {
app.scheduled_jobs
.borrow_mut()
.retain(|entry| entry.job.widget_id != widget_id)
});
}
pub(crate) fn teardown_widget_subtree(tree: &mut crate::tree::Tree, root: WidgetId) {
for id in tree.collect_subtree_post_order(root) {
clear_widget_subscribers(id);
cancel_scheduled_jobs(id);
tree.unregister(id);
}
}
pub fn distribute_jobs(tree: &Tree, active_roots: &rustc_hash::FxHashSet<WidgetId>) {
with_app_state(|app| {
let mut jobs = app.pending_jobs.borrow_mut();
let dead: SmallVec<[WidgetId; 2]> = jobs
.per_root
.keys()
.filter(|root| !active_roots.contains(root))
.copied()
.collect();
for root in dead {
if let Some(mut queue) = jobs.per_root.remove(&root) {
for job in queue.vec.drain(..) {
jobs.orphans.push(job);
}
}
}
if jobs.inbox.is_empty() {
return;
}
let buf = jobs.spare_buf();
let pending = jobs.inbox.drain_all(buf);
for job in &pending {
match tree.surface_root_of(job.widget_id) {
Some(root) if active_roots.contains(&root) => {
jobs.per_root.entry(root).or_default().push(*job);
}
_ => jobs.orphans.push(*job),
}
}
jobs.recycle(pending);
});
}
pub fn drain_surface_jobs(root: WidgetId) -> Vec<Job> {
with_app_state(|app| {
let mut jobs = app.pending_jobs.borrow_mut();
let buf = jobs.spare_buf();
match jobs.per_root.get_mut(&root) {
Some(queue) => queue.drain_all(buf),
None => buf,
}
})
}
pub fn drain_surface_non_animation_jobs(root: WidgetId) -> Vec<Job> {
with_app_state(|app| {
let mut jobs = app.pending_jobs.borrow_mut();
let buf = jobs.spare_buf();
match jobs.per_root.get_mut(&root) {
Some(queue) => queue.drain_non_animation(buf),
None => buf,
}
})
}
pub fn drain_orphan_jobs() -> Vec<Job> {
with_app_state(|app| {
let mut jobs = app.pending_jobs.borrow_mut();
let buf = jobs.spare_buf();
jobs.orphans.drain_all(buf)
})
}
pub fn recycle_job_buffer(buf: Vec<Job>) {
with_app_state(|app| app.pending_jobs.borrow_mut().recycle(buf));
}
pub fn process_jobs(jobs: &[Job], tree: &mut Tree, layout_roots: &mut Vec<WidgetId>) {
let mut unregister = SmallVec::<[WidgetId; 4]>::new();
let mut animation = SmallVec::<[WidgetId; 8]>::new();
let mut reconcile = SmallVec::<[WidgetId; 4]>::new();
let mut paint = SmallVec::<[WidgetId; 8]>::new();
let mut layout = SmallVec::<[WidgetId; 8]>::new();
for job in jobs {
match job.job_type {
JobType::Unregister => unregister.push(job.widget_id),
JobType::Animation => animation.push(job.widget_id),
JobType::Reconcile => reconcile.push(job.widget_id),
JobType::Paint => paint.push(job.widget_id),
JobType::Layout => layout.push(job.widget_id),
}
}
for id in unregister {
clear_widget_subscribers(id);
cancel_scheduled_jobs(id);
tree.unregister(id);
}
for id in animation {
tree.with_widget_mut(id, |widget, wid, tree| {
widget.advance_animations(tree, wid);
});
}
for id in reconcile {
tree.with_widget_mut(id, |widget, wid, tree| {
widget.reconcile_children(tree, wid);
});
if let Some(root) = tree.mark_needs_layout(id)
&& !layout_roots.contains(&root)
{
layout_roots.push(root);
}
}
for id in paint {
tree.with_widget_mut(id, |widget, wid, tree| {
crate::tree::refresh_paint_reach(&*widget, tree, wid);
});
tree.mark_needs_paint(id);
}
for id in layout {
if let Some(root) = tree.mark_needs_layout(id)
&& !layout_roots.contains(&root)
{
layout_roots.push(root);
}
}
}
pub fn has_pending_jobs() -> bool {
with_app_state(|app| !app.pending_jobs.borrow().is_empty())
}
#[cfg(test)]
pub(crate) fn queued_job_types(widget_id: WidgetId) -> Vec<JobType> {
with_app_state(|app| {
let jobs = app.pending_jobs.borrow();
jobs.inbox
.vec
.iter()
.chain(jobs.orphans.vec.iter())
.chain(jobs.per_root.values().flat_map(|queue| queue.vec.iter()))
.filter(|job| job.widget_id == widget_id)
.map(|job| job.job_type)
.collect()
})
}
#[cfg(test)]
pub(crate) fn pump_and_layout(
tree: &mut Tree,
root: WidgetId,
constraints: crate::layout::Constraints,
) -> Option<crate::layout::Size> {
pump(tree, root);
tree.layout_widget(root, constraints)
}
#[cfg(test)]
pub(crate) fn pump_and_measure(
tree: &mut Tree,
root: WidgetId,
constraints: crate::layout::Constraints,
) -> Option<crate::layout::Size> {
pump(tree, root);
tree.measure_widget(root, constraints)
}
#[cfg(test)]
fn pump(tree: &mut Tree, root: WidgetId) {
let roots: rustc_hash::FxHashSet<WidgetId> = [root].into_iter().collect();
distribute_jobs(tree, &roots);
let drained = drain_surface_jobs(root);
let mut layout_roots = Vec::new();
process_jobs(&drained, tree, &mut layout_roots);
recycle_job_buffer(drained);
recycle_job_buffer(drain_orphan_jobs());
}
#[cfg(test)]
pub(crate) fn clear_scheduled_jobs() {
with_app_state(|app| app.scheduled_jobs.borrow_mut().clear());
}
#[cfg(test)]
pub(crate) fn clear_pending_jobs() {
with_app_state(|app| app.pending_jobs.take());
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ExitRequest {
Running = 0,
Quit = 1,
Restart = 2,
}
static EXIT_REQUEST: AtomicU8 = AtomicU8::new(ExitRequest::Running as u8);
pub(crate) fn set_exit_request(req: ExitRequest) {
EXIT_REQUEST.store(req as u8, Ordering::Release);
if let Ok(guard) = WAKEUP_PING.lock()
&& let Some(ref ping) = *guard
{
ping.ping();
}
}
pub(crate) fn get_exit_request() -> ExitRequest {
match EXIT_REQUEST.load(Ordering::Acquire) {
1 => ExitRequest::Quit,
2 => ExitRequest::Restart,
_ => ExitRequest::Running,
}
}
static WAKE_REQUESTED: AtomicBool = AtomicBool::new(false);
static PING_SENT: AtomicBool = AtomicBool::new(false);
static WAKEUP_PING: Mutex<Option<Ping>> = Mutex::new(None);
pub fn init_wakeup(ping: Ping) {
if let Ok(mut guard) = WAKEUP_PING.lock() {
*guard = Some(ping);
}
}
pub(crate) fn wake_loop() {
WAKE_REQUESTED.store(true, Ordering::Relaxed);
if PING_SENT.load(Ordering::Acquire) {
return; }
let Ok(guard) = WAKEUP_PING.lock() else {
return;
};
let Some(ping) = guard.as_ref() else {
return; };
if !PING_SENT.swap(true, Ordering::Release) {
ping.ping();
}
}
#[cfg(test)]
pub(crate) fn ping_in_flight() -> bool {
PING_SENT.load(Ordering::Acquire)
}
#[cfg(test)]
pub(crate) fn wakeup_test_lock() -> WakeupTestState {
static LOCK: Mutex<()> = Mutex::new(());
WakeupTestState(LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner()))
}
#[cfg(test)]
pub(crate) struct WakeupTestState(#[allow(dead_code)] std::sync::MutexGuard<'static, ()>);
#[cfg(test)]
impl Drop for WakeupTestState {
fn drop(&mut self) {
reset_wakeup();
crate::ingress::reset_ingress();
}
}
pub(crate) fn mark_loop_awake() {
PING_SENT.store(false, Ordering::Relaxed);
}
pub(crate) fn reset_wakeup() {
WAKE_REQUESTED.store(false, Ordering::Relaxed);
PING_SENT.store(false, Ordering::Relaxed);
EXIT_REQUEST.store(ExitRequest::Running as u8, Ordering::Relaxed);
if let Ok(mut guard) = WAKEUP_PING.lock() {
*guard = None;
}
}
pub fn take_wake_request() -> bool {
WAKE_REQUESTED.swap(false, Ordering::Relaxed)
}
pub fn wake_request_pending() -> bool {
WAKE_REQUESTED.load(Ordering::Relaxed)
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::layout::{Constraints, Size};
use crate::tree::LayoutCtx;
use crate::widgets::Widget;
struct TestWidget;
impl Widget for TestWidget {
fn layout(&mut self, _: &mut LayoutCtx, _: Constraints) -> Size {
Size::zero()
}
fn paint(&self, _: &mut crate::renderer::PaintContext) {}
}
fn two_surface_tree() -> (Tree, [WidgetId; 2], [WidgetId; 2]) {
let mut tree = Tree::new();
let root_a = tree.register(Box::new(TestWidget));
let root_b = tree.register(Box::new(TestWidget));
let child_a = tree.register(Box::new(TestWidget));
let child_b = tree.register(Box::new(TestWidget));
tree.set_parent(child_a, root_a);
tree.set_parent(child_b, root_b);
(tree, [root_a, root_b], [child_a, child_b])
}
fn roots_set(roots: &[WidgetId]) -> rustc_hash::FxHashSet<WidgetId> {
roots.iter().copied().collect()
}
fn push(widget_id: WidgetId, job_type: JobType) -> Job {
let job = Job {
widget_id,
job_type,
};
with_app_state(|app| app.pending_jobs.borrow_mut().inbox.push(job));
job
}
#[test]
fn a_scheduled_job_is_not_work_for_this_frame() {
clear_pending_jobs();
clear_scheduled_jobs();
let (mut tree, ..) = two_surface_tree();
let widget = tree.register(Box::new(TestWidget));
request_job_at(
widget,
JobRequest::Paint,
Instant::now() + Duration::from_secs(60),
);
assert!(
!has_pending_jobs(),
"a job due in a minute must not keep the loop polling for a minute"
);
assert!(
next_deadline().is_some(),
"but the loop has to know about it"
);
}
#[test]
fn a_due_job_becomes_an_ordinary_one() {
clear_pending_jobs();
clear_scheduled_jobs();
let (mut tree, ..) = two_surface_tree();
let widget = tree.register(Box::new(TestWidget));
request_job_at(widget, JobRequest::Paint, Instant::now());
promote_due_jobs();
assert!(has_pending_jobs());
assert!(
next_deadline().is_none(),
"and it is no longer scheduled, or it would fire every frame"
);
}
#[test]
fn rescheduling_moves_the_deadline_instead_of_adding_one() {
clear_pending_jobs();
clear_scheduled_jobs();
let (mut tree, ..) = two_surface_tree();
let widget = tree.register(Box::new(TestWidget));
let far = Instant::now() + Duration::from_secs(60);
request_job_at(widget, JobRequest::Paint, far);
request_job_at(widget, JobRequest::Paint, far);
request_job_at(widget, JobRequest::Paint, Instant::now());
promote_due_jobs();
assert!(has_pending_jobs());
assert!(next_deadline().is_none(), "three asks, one entry");
}
#[test]
fn an_animation_keeps_its_follow_up_across_the_wait() {
clear_pending_jobs();
clear_scheduled_jobs();
let (mut tree, ..) = two_surface_tree();
let widget = tree.register(Box::new(TestWidget));
request_job_at(
widget,
JobRequest::Animation(RequiredJob::Paint),
Instant::now(),
);
promote_due_jobs();
let queued: Vec<JobType> = with_app_state(|app| {
app.pending_jobs
.borrow()
.inbox
.vec
.iter()
.filter(|job| job.widget_id == widget)
.map(|job| job.job_type)
.collect()
});
assert!(queued.contains(&JobType::Animation));
assert!(
queued.contains(&JobType::Paint),
"the caret has to be redrawn, not only advanced: {queued:?}"
);
}
#[test]
fn a_widget_leaving_the_tree_stops_waking_the_loop() {
clear_pending_jobs();
clear_scheduled_jobs();
let (mut tree, ..) = two_surface_tree();
let widget = tree.register(Box::new(TestWidget));
request_job_at(
widget,
JobRequest::Paint,
Instant::now() + Duration::from_secs(60),
);
teardown_widget_subtree(&mut tree, widget);
assert!(
next_deadline().is_none(),
"a caret that no longer exists must not keep the loop on a timer"
);
}
#[test]
fn a_deferred_unregister_takes_the_deadlines_with_it() {
clear_pending_jobs();
clear_scheduled_jobs();
let (mut tree, ..) = two_surface_tree();
let widget = tree.register(Box::new(TestWidget));
request_job_at(
widget,
JobRequest::Paint,
Instant::now() + Duration::from_secs(60),
);
let mut layout_roots = Vec::new();
process_jobs(
&[Job {
widget_id: widget,
job_type: JobType::Unregister,
}],
&mut tree,
&mut layout_roots,
);
assert!(
next_deadline().is_none(),
"a widget unregistered through the job queue must drop its deadlines too"
);
}
#[test]
fn jobs_are_routed_to_their_owning_surface() {
clear_pending_jobs();
let (tree, roots, children) = two_surface_tree();
let active = roots_set(&roots);
let job_a = push(children[0], JobType::Paint);
let job_b = push(children[1], JobType::Layout);
distribute_jobs(&tree, &active);
let drained_a = drain_surface_jobs(roots[0]);
assert_eq!(drained_a, vec![job_a]);
let drained_b = drain_surface_jobs(roots[1]);
assert_eq!(drained_b, vec![job_b]);
assert!(drain_surface_jobs(roots[0]).is_empty());
assert!(!has_pending_jobs() || drain_orphan_jobs().is_empty());
}
#[test]
fn gated_surface_animations_survive_other_surfaces_drains() {
clear_pending_jobs();
let (tree, roots, children) = two_surface_tree();
let active = roots_set(&roots);
let anim_a = push(children[0], JobType::Animation);
distribute_jobs(&tree, &active);
assert!(drain_surface_jobs(roots[1]).is_empty());
assert!(drain_surface_non_animation_jobs(roots[1]).is_empty());
let drained_a = drain_surface_jobs(roots[0]);
assert_eq!(drained_a, vec![anim_a]);
}
#[test]
fn drain_surface_non_animation_keeps_animation_jobs() {
clear_pending_jobs();
let (tree, roots, children) = two_surface_tree();
let active = roots_set(&roots);
let anim = push(children[0], JobType::Animation);
let paint = push(children[0], JobType::Paint);
let layout = push(roots[0], JobType::Layout);
distribute_jobs(&tree, &active);
let drained = drain_surface_non_animation_jobs(roots[0]);
assert_eq!(drained.len(), 2);
assert!(drained.contains(&paint));
assert!(drained.contains(&layout));
let remaining = drain_surface_jobs(roots[0]);
assert_eq!(remaining, vec![anim]);
}
#[test]
fn jobs_without_a_live_surface_go_to_the_orphan_lane() {
clear_pending_jobs();
let (mut tree, roots, children) = two_surface_tree();
let active = roots_set(&roots);
let ghost = children[1];
tree.unregister(ghost);
let orphan_job = push(ghost, JobType::Unregister);
distribute_jobs(&tree, &active);
assert_eq!(drain_orphan_jobs(), vec![orphan_job]);
assert!(drain_surface_jobs(roots[1]).is_empty());
}
#[test]
fn dead_surface_queues_are_retired_into_the_orphan_lane() {
clear_pending_jobs();
let (tree, roots, children) = two_surface_tree();
let active = roots_set(&roots);
let job_a = push(children[0], JobType::Unregister);
distribute_jobs(&tree, &active);
let only_b = roots_set(&roots[1..]);
distribute_jobs(&tree, &only_b);
assert_eq!(drain_orphan_jobs(), vec![job_a]);
assert!(!has_pending_jobs());
}
#[test]
fn inbox_dedup_survives_distribution() {
clear_pending_jobs();
let (tree, roots, children) = two_surface_tree();
let active = roots_set(&roots);
let job = push(children[0], JobType::Paint);
push(children[0], JobType::Paint);
distribute_jobs(&tree, &active);
push(children[0], JobType::Paint);
distribute_jobs(&tree, &active);
assert_eq!(drain_surface_jobs(roots[0]), vec![job]);
}
}
#[cfg(test)]
mod wakeup_contract {
use super::*;
use smithay_client_toolkit::reexports::calloop::{EventLoop, ping::make_ping};
use std::time::Duration;
#[test]
fn an_exit_request_writes_its_own_ping() {
let _state = wakeup_test_lock();
let mut event_loop: EventLoop<bool> = EventLoop::try_new().expect("event loop");
let (ping, source) = make_ping().expect("ping");
event_loop
.handle()
.insert_source(source, |_, _, woken: &mut bool| *woken = true)
.expect("insert ping source");
let woke = (0..64).any(|_| {
init_wakeup(ping.clone());
let mut drained = false;
let _ = event_loop.dispatch(Some(Duration::ZERO), &mut drained);
PING_SENT.store(true, Ordering::Relaxed);
set_exit_request(ExitRequest::Quit);
let mut woken = false;
let _ = event_loop.dispatch(Some(Duration::from_millis(20)), &mut woken);
woken
});
assert!(
woke,
"an exit request must wake a loop that already had a ping armed"
);
}
#[test]
fn a_request_pings_even_when_the_frame_flag_is_already_set() {
let _state = wakeup_test_lock();
let mut event_loop: EventLoop<bool> = EventLoop::try_new().expect("event loop");
let (ping, source) = make_ping().expect("ping");
event_loop
.handle()
.insert_source(source, |_, _, woken: &mut bool| *woken = true)
.expect("insert ping source");
for _ in 0..64 {
init_wakeup(ping.clone());
let mut drained = false;
let _ = event_loop.dispatch(Some(Duration::ZERO), &mut drained);
WAKE_REQUESTED.store(true, Ordering::Relaxed);
PING_SENT.store(false, Ordering::Relaxed);
wake_loop();
let mut woken = false;
let _ = event_loop.dispatch(Some(Duration::from_millis(20)), &mut woken);
if woken {
assert!(
PING_SENT.load(Ordering::Relaxed),
"a sent ping must leave the coalescing flag armed"
);
return;
}
}
panic!(
"wake_loop must ping while WAKE_REQUESTED is set — gating on \
that flag is what lost wakeups"
);
}
}