1
0
Fork 0
forked from wry/wry

output_schedule: move cursor scheduler into workspace crate

This commit is contained in:
kossLAN 2026-05-29 11:48:49 -04:00
parent 11940fb6a5
commit 6d569bd4b7
No known key found for this signature in database
9 changed files with 286 additions and 209 deletions

13
Cargo.lock generated
View file

@ -740,6 +740,7 @@ dependencies = [
"jay-layout-animation", "jay-layout-animation",
"jay-libinput", "jay-libinput",
"jay-logger", "jay-logger",
"jay-output-schedule",
"jay-pango", "jay-pango",
"jay-pr-caps", "jay-pr-caps",
"jay-sighand", "jay-sighand",
@ -937,6 +938,18 @@ dependencies = [
"uapi", "uapi",
] ]
[[package]]
name = "jay-output-schedule"
version = "0.1.0"
dependencies = [
"futures-util",
"jay-async-engine",
"jay-io-uring",
"jay-utils",
"log",
"num-traits",
]
[[package]] [[package]]
name = "jay-pango" name = "jay-pango"
version = "0.1.0" version = "0.1.0"

View file

@ -47,6 +47,7 @@ members = [
"theme", "theme",
"clientmem", "clientmem",
"allocator", "allocator",
"output-schedule",
"pango", "pango",
"libinput", "libinput",
"toml-config", "toml-config",
@ -97,6 +98,7 @@ jay-gfx-types = { version = "0.1.0", path = "gfx-types" }
jay-theme = { version = "0.1.0", path = "theme" } jay-theme = { version = "0.1.0", path = "theme" }
jay-clientmem = { version = "0.1.0", path = "clientmem" } jay-clientmem = { version = "0.1.0", path = "clientmem" }
jay-allocator = { version = "0.1.0", path = "allocator" } jay-allocator = { version = "0.1.0", path = "allocator" }
jay-output-schedule = { version = "0.1.0", path = "output-schedule" }
jay-pango = { version = "0.1.0", path = "pango" } jay-pango = { version = "0.1.0", path = "pango" }
jay-libinput = { version = "0.1.0", path = "libinput" } jay-libinput = { version = "0.1.0", path = "libinput" }

View file

@ -0,0 +1,14 @@
[package]
name = "jay-output-schedule"
version = "0.1.0"
edition = "2024"
license = "GPL-3.0-only"
[dependencies]
jay-async-engine = { version = "0.1.0", path = "../async-engine" }
jay-io-uring = { version = "0.1.0", path = "../io-uring" }
jay-utils = { version = "0.1.0", path = "../utils" }
futures-util = "0.3.30"
log = "0.4.20"
num-traits = "0.2.17"

219
output-schedule/src/lib.rs Normal file
View file

@ -0,0 +1,219 @@
use {
jay_async_engine::AsyncEngine,
jay_io_uring::{IoUring, IoUringError},
jay_utils::{
asyncevent::AsyncEvent, cell_ext::CellExt, clonecell::CloneCell, errorfmt::ErrorFmt,
numcell::NumCell,
},
futures_util::{FutureExt, select},
num_traits::ToPrimitive,
std::{cell::Cell, rc::Rc},
};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum Change {
/// The backend has applied the latest changes.
None,
/// There are changes that the backend is not yet aware of.
Scheduled,
/// The backend is aware that there are changes and will apply them as part of the
/// next latch event.
AwaitingLatch,
}
pub struct OutputSchedule {
changed: AsyncEvent,
run: Cell<bool>,
damage_connector: Rc<dyn Fn()>,
hardware_cursor_damage: CloneCell<Option<Rc<dyn Fn()>>>,
cursor_hz_changed: Rc<dyn Fn(Option<f64>)>,
persistent: Rc<dyn OutputSchedulePersistent>,
last_present_nsec: Cell<u64>,
cursor_delta_nsec: Cell<Option<u64>>,
ring: Rc<IoUring>,
eng: Rc<AsyncEngine>,
vrr_enabled: Cell<bool>,
hardware_cursor_change: Cell<Change>,
software_cursor_change: Cell<Change>,
iteration: NumCell<u64>,
}
pub trait OutputSchedulePersistent {
fn vrr_cursor_hz(&self) -> Option<f64>;
fn set_vrr_cursor_hz(&self, hz: Option<f64>);
}
impl OutputSchedule {
pub fn new(
ring: Rc<IoUring>,
eng: Rc<AsyncEngine>,
persistent: Rc<dyn OutputSchedulePersistent>,
damage_connector: Rc<dyn Fn()>,
cursor_hz_changed: Rc<dyn Fn(Option<f64>)>,
) -> Self {
let slf = Self {
changed: Default::default(),
run: Default::default(),
damage_connector,
cursor_hz_changed,
ring,
eng,
vrr_enabled: Default::default(),
hardware_cursor_change: Cell::new(Change::None),
software_cursor_change: Cell::new(Change::None),
hardware_cursor_damage: Default::default(),
persistent: persistent.clone(),
last_present_nsec: Default::default(),
cursor_delta_nsec: Default::default(),
iteration: Default::default(),
};
if let Some(hz) = persistent.vrr_cursor_hz() {
slf.set_cursor_hz(hz);
}
slf
}
pub async fn drive(self: Rc<Self>) {
loop {
self.run_once().await;
while !self.run.take() {
self.changed.triggered().await;
}
}
}
fn trigger(&self) {
let trigger = self.vrr_enabled.get()
&& self.cursor_delta_nsec.is_some()
&& (self.software_cursor_change.get() == Change::Scheduled
|| self.hardware_cursor_change.get() == Change::Scheduled);
if trigger {
self.run.set(true);
self.changed.trigger();
}
}
pub fn latched(&self) {
self.last_present_nsec.set(self.eng.now().nsec());
if self.software_cursor_change.get() == Change::AwaitingLatch {
self.software_cursor_change.set(Change::None);
}
if self.hardware_cursor_change.get() == Change::AwaitingLatch {
self.hardware_cursor_change.set(Change::None);
}
self.iteration.fetch_add(1);
self.trigger();
}
pub fn vrr_enabled(&self) -> bool {
self.vrr_enabled.get()
}
pub fn set_vrr_enabled(&self, enabled: bool) {
self.vrr_enabled.set(enabled);
self.trigger();
}
pub fn set_cursor_hz(&self, hz: f64) {
let (hz, delta) = match map_cursor_hz(hz) {
None => {
log::warn!("Ignoring cursor frequency {hz}");
return;
}
Some(v) => v,
};
self.persistent.set_vrr_cursor_hz(hz);
(self.cursor_hz_changed)(hz);
self.cursor_delta_nsec.set(delta);
self.trigger();
}
pub fn set_hardware_cursor_damage(&self, damage: &Option<Rc<dyn Fn()>>) {
self.hardware_cursor_damage.set(damage.clone());
}
pub fn defer_cursor_updates(&self) -> bool {
self.vrr_enabled.get() && self.cursor_delta_nsec.is_some()
}
pub fn hardware_cursor_changed(&self) {
if self.hardware_cursor_change.get() == Change::None {
self.hardware_cursor_change.set(Change::Scheduled);
self.trigger();
}
}
pub fn software_cursor_changed(&self) {
if self.software_cursor_change.get() == Change::None {
self.software_cursor_change.set(Change::Scheduled);
self.trigger();
}
}
async fn run_once(&self) {
loop {
if self.hardware_cursor_change.get() != Change::Scheduled
&& self.software_cursor_change.get() != Change::Scheduled
{
return;
}
if !self.vrr_enabled.get() {
return;
}
let Some(duration) = self.cursor_delta_nsec.get() else {
return;
};
let iteration = self.iteration.get();
let next_present = self.last_present_nsec.get().saturating_add(duration);
let res: Result<(), IoUringError> = select! {
_ = self.changed.triggered().fuse() => continue,
v = self.ring.timeout(next_present).fuse() => v,
};
if let Err(e) = res {
log::error!("Could not wait for timer to expire: {}", ErrorFmt(e));
return;
}
if iteration == self.iteration.get() {
break;
}
}
self.commit_cursor();
}
pub fn commit_cursor(&self) {
if self.hardware_cursor_change.get() == Change::Scheduled {
if let Some(damage) = self.hardware_cursor_damage.get() {
damage();
}
self.hardware_cursor_change.set(Change::AwaitingLatch);
}
if self.software_cursor_change.get() == Change::Scheduled {
(self.damage_connector)();
self.software_cursor_change.set(Change::AwaitingLatch);
}
}
}
pub fn map_cursor_hz(hz: f64) -> Option<(Option<f64>, Option<u64>)> {
if hz <= 0.0 {
return Some((Some(0.0), Some(u64::MAX)));
}
let delta = (1_000_000_000.0 / hz).to_u64();
if delta.is_none() {
if hz > 0.0 {
return Some((None, None));
}
return None;
}
if delta == Some(0) {
return Some((None, None));
}
Some((Some(hz), delta))
}

View file

@ -49,7 +49,7 @@ use {
kbvm::KbvmContext, kbvm::KbvmContext,
leaks, leaks,
logger::Logger, logger::Logger,
output_schedule::OutputSchedule, output_schedule::create_output_schedule,
pr_caps::{PrCapsThread, pr_caps}, pr_caps::{PrCapsThread, pr_caps},
scale::Scale, scale::Scale,
sighand::{self, SighandError}, sighand::{self, SighandError},
@ -713,7 +713,7 @@ fn create_dummy_output(state: &Rc<State>) {
head_managers: HeadManagers::new(head_name, head_state), head_managers: HeadManagers::new(head_name, head_state),
wlr_output_heads: Default::default(), wlr_output_heads: Default::default(),
}); });
let schedule = Rc::new(OutputSchedule::new( let schedule = Rc::new(create_output_schedule(
state, state,
&connector_data, &connector_data,
&persistent_state, &persistent_state,

View file

@ -1528,7 +1528,7 @@ impl ConfigProxyHandler {
match connector { match connector {
Some(c) => { Some(c) => {
let connector = self.get_output_node(c)?; let connector = self.get_output_node(c)?;
connector.schedule.set_cursor_hz(&self.state, hz); connector.schedule.set_cursor_hz(hz);
} }
_ => { _ => {
let Some((hz, _)) = map_cursor_hz(hz) else { let Some((hz, _)) = map_cursor_hz(hz) else {

View file

@ -474,7 +474,7 @@ impl JayRandrRequestHandler for JayRandr {
let Some(c) = self.get_output_node(req.output) else { let Some(c) = self.get_output_node(req.output) else {
return Ok(()); return Ok(());
}; };
c.schedule.set_cursor_hz(&self.state, req.hz); c.schedule.set_cursor_hz(req.hz);
Ok(()) Ok(())
} }

View file

@ -1,215 +1,35 @@
pub use jay_output_schedule::*;
use { use {
crate::{ crate::{
async_engine::AsyncEngine,
backend::HardwareCursor,
ifs::wl_output::PersistentOutputState, ifs::wl_output::PersistentOutputState,
io_uring::{IoUring, IoUringError},
state::{ConnectorData, State}, state::{ConnectorData, State},
utils::{
asyncevent::AsyncEvent, cell_ext::CellExt, clonecell::CloneCell, errorfmt::ErrorFmt,
numcell::NumCell,
},
}, },
futures_util::{FutureExt, select}, std::rc::Rc,
num_traits::ToPrimitive,
std::{cell::Cell, rc::Rc},
}; };
#[derive(Copy, Clone, Debug, Eq, PartialEq)] impl OutputSchedulePersistent for PersistentOutputState {
enum Change { fn vrr_cursor_hz(&self) -> Option<f64> {
/// The backend has applied the latest changes. self.vrr_cursor_hz.get()
None,
/// There are changes that the backend is not yet aware of.
Scheduled,
/// The backend is aware that there are changes and will apply them as part of the
/// next latch event.
AwaitingLatch,
}
pub struct OutputSchedule {
changed: AsyncEvent,
run: Cell<bool>,
connector: Rc<ConnectorData>,
hardware_cursor: CloneCell<Option<Rc<dyn HardwareCursor>>>,
persistent: Rc<PersistentOutputState>,
last_present_nsec: Cell<u64>,
cursor_delta_nsec: Cell<Option<u64>>,
ring: Rc<IoUring>,
eng: Rc<AsyncEngine>,
vrr_enabled: Cell<bool>,
hardware_cursor_change: Cell<Change>,
software_cursor_change: Cell<Change>,
iteration: NumCell<u64>,
}
impl OutputSchedule {
pub fn new(
state: &State,
connector: &Rc<ConnectorData>,
persistent: &Rc<PersistentOutputState>,
) -> Self {
let slf = Self {
changed: Default::default(),
run: Default::default(),
connector: connector.clone(),
ring: state.ring.clone(),
eng: state.eng.clone(),
vrr_enabled: Default::default(),
hardware_cursor_change: Cell::new(Change::None),
software_cursor_change: Cell::new(Change::None),
hardware_cursor: Default::default(),
persistent: persistent.clone(),
last_present_nsec: Default::default(),
cursor_delta_nsec: Default::default(),
iteration: Default::default(),
};
if let Some(hz) = persistent.vrr_cursor_hz.get() {
slf.set_cursor_hz(state, hz);
}
slf
} }
pub async fn drive(self: Rc<Self>) { fn set_vrr_cursor_hz(&self, hz: Option<f64>) {
loop { self.vrr_cursor_hz.set(hz);
self.run_once().await;
while !self.run.take() {
self.changed.triggered().await;
}
}
}
fn trigger(&self) {
let trigger = self.vrr_enabled.get()
&& self.cursor_delta_nsec.is_some()
&& (self.software_cursor_change.get() == Change::Scheduled
|| self.hardware_cursor_change.get() == Change::Scheduled);
if trigger {
self.run.set(true);
self.changed.trigger();
}
}
pub fn latched(&self) {
self.last_present_nsec.set(self.eng.now().nsec());
if self.software_cursor_change.get() == Change::AwaitingLatch {
self.software_cursor_change.set(Change::None);
}
if self.hardware_cursor_change.get() == Change::AwaitingLatch {
self.hardware_cursor_change.set(Change::None);
}
self.iteration.fetch_add(1);
self.trigger();
}
pub fn vrr_enabled(&self) -> bool {
self.vrr_enabled.get()
}
pub fn set_vrr_enabled(&self, enabled: bool) {
self.vrr_enabled.set(enabled);
self.trigger();
}
pub fn set_cursor_hz(&self, _state: &State, hz: f64) {
let (hz, delta) = match map_cursor_hz(hz) {
None => {
log::warn!("Ignoring cursor frequency {hz}");
return;
}
Some(v) => v,
};
self.persistent.vrr_cursor_hz.set(hz);
self.connector.head_managers.handle_cursor_hz_change(hz);
self.cursor_delta_nsec.set(delta);
self.trigger();
}
pub fn set_hardware_cursor(&self, hc: &Option<Rc<dyn HardwareCursor>>) {
self.hardware_cursor.set(hc.clone());
}
pub fn defer_cursor_updates(&self) -> bool {
self.vrr_enabled.get() && self.cursor_delta_nsec.is_some()
}
pub fn hardware_cursor_changed(&self) {
if self.hardware_cursor_change.get() == Change::None {
self.hardware_cursor_change.set(Change::Scheduled);
self.trigger();
}
}
pub fn software_cursor_changed(&self) {
if self.software_cursor_change.get() == Change::None {
self.software_cursor_change.set(Change::Scheduled);
self.trigger();
}
}
async fn run_once(&self) {
loop {
if self.hardware_cursor_change.get() != Change::Scheduled
&& self.software_cursor_change.get() != Change::Scheduled
{
return;
}
if !self.vrr_enabled.get() {
return;
}
let Some(duration) = self.cursor_delta_nsec.get() else {
return;
};
let iteration = self.iteration.get();
let next_present = self.last_present_nsec.get().saturating_add(duration);
let res: Result<(), IoUringError> = select! {
_ = self.changed.triggered().fuse() => continue,
v = self.ring.timeout(next_present).fuse() => v,
};
if let Err(e) = res {
log::error!("Could not wait for timer to expire: {}", ErrorFmt(e));
return;
}
if iteration == self.iteration.get() {
break;
}
}
self.commit_cursor();
}
pub fn commit_cursor(&self) {
if self.hardware_cursor_change.get() == Change::Scheduled {
if let Some(hc) = self.hardware_cursor.get() {
hc.damage();
}
self.hardware_cursor_change.set(Change::AwaitingLatch);
}
if self.software_cursor_change.get() == Change::Scheduled {
self.connector.damage();
self.software_cursor_change.set(Change::AwaitingLatch);
}
} }
} }
pub fn map_cursor_hz(hz: f64) -> Option<(Option<f64>, Option<u64>)> { pub fn create_output_schedule(
if hz <= 0.0 { state: &State,
return Some((Some(0.0), Some(u64::MAX))); connector: &Rc<ConnectorData>,
} persistent: &Rc<PersistentOutputState>,
let delta = (1_000_000_000.0 / hz).to_u64(); ) -> OutputSchedule {
if delta.is_none() { let damage_connector = connector.clone();
if hz > 0.0 { let cursor_hz_connector = connector.clone();
return Some((None, None)); OutputSchedule::new(
} state.ring.clone(),
return None; state.eng.clone(),
} persistent.clone(),
if delta == Some(0) { Rc::new(move || damage_connector.damage()),
return Some((None, None)); Rc::new(move |hz| cursor_hz_connector.head_managers.handle_cursor_hz_change(hz)),
} )
Some((Some(hz), delta))
} }

View file

@ -7,7 +7,7 @@ use {
jay_tray_v1::JayTrayV1Global, jay_tray_v1::JayTrayV1Global,
wl_output::{BlendSpace, WlOutputGlobal}, wl_output::{BlendSpace, WlOutputGlobal},
}, },
output_schedule::OutputSchedule, output_schedule::create_output_schedule,
state::{ConnectorData, OutputData, State}, state::{ConnectorData, OutputData, State},
tree::{OutputNode, Transform, WsMoveConfig, move_ws_to_output}, tree::{OutputNode, Transform, WsMoveConfig, move_ws_to_output},
utils::{ utils::{
@ -177,7 +177,11 @@ impl ConnectorHandler {
info.primaries, info.primaries,
info.luminance, info.luminance,
)); ));
let schedule = Rc::new(OutputSchedule::new(&self.state, &self.data, &desired_state)); let schedule = Rc::new(create_output_schedule(
&self.state,
&self.data,
&desired_state,
));
let _schedule = self let _schedule = self
.state .state
.eng .eng
@ -309,7 +313,12 @@ impl ConnectorHandler {
match event { match event {
ConnectorEvent::Disconnected => break 'outer, ConnectorEvent::Disconnected => break 'outer,
ConnectorEvent::HardwareCursor(hc) => { ConnectorEvent::HardwareCursor(hc) => {
on.schedule.set_hardware_cursor(&hc); let hardware_cursor_damage = hc.as_ref().map(|hc| {
let hc = hc.clone();
Rc::new(move || hc.damage()) as Rc<dyn Fn()>
});
on.schedule
.set_hardware_cursor_damage(&hardware_cursor_damage);
on.set_hardware_cursor(hc); on.set_hardware_cursor(hc);
self.state.refresh_hardware_cursors(); self.state.refresh_hardware_cursors();
} }