Publish output topology through a desktop reconciliation boundary

This commit is contained in:
Samuel Onoja committed 2026-10-02 23:46:08 +01:00
1 parent 647eaf4ad9
commit f5de30d037
16 files changed
+1351 -298

No files matched your search

+90 -6
View File
@@ -77,6 +77,8 @@ pub(super) struct Topology {
pub invalidated: HashSet<DrmNode>,
pub reconciling: bool,
pub dirty: bool,
pub layout_dirty: bool,
pub remember_applied: bool,
pub retry_timer: Option<RegistrationToken>,
pub settle_timer: Option<RegistrationToken>,
pub coalescer: crate::output_policy::Debouncer,
@@ -165,6 +167,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
}
paths.sort();
let mut ready = Vec::new();
state.begin_desktop_transition();
for node in invalidated {
remove_device(state, node);
@@ -227,7 +230,14 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
output.frame_pending = false;
output.lock_frame_pending = false;
output.power.suspended();
state.session_lock.output_added(&output.output);
state.display_presentation.remove_output(&output.output);
state
.desktop_transition
.as_mut()
.unwrap()
.redraw
.insert(output.output.clone());
if let Err(error) = output.surface.clear().and_then(|()| output.surface.reset_state()) {
tracing::warn!(%error, ?node, ?crtc, "output reset failed; recreating output");
@@ -327,10 +337,19 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
}
transaction::recover_last_output(state);
refresh_connected_info(state);
let mut desktop_changes = publish_runtime_desktop(state);
if std::mem::take(&mut state.direct_backend.as_mut().unwrap().topology.layout_dirty) {
desktop_changes.layout = true;
desktop_changes.shell = true;
desktop_changes.outputs.extend(state.space.outputs().cloned());
}
let backend = state.direct_backend.as_mut().unwrap();
backend.topology.reconciling = false;
let retry = backend.topology.retry_deadline(backend.active);
if std::mem::take(&mut backend.topology.remember_applied) {
remember_applied_configuration(state);
}
refresh_connected_info(state);
if let Some(deadline) = retry {
match state
.loop_handle
@@ -346,16 +365,81 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
}
}
state.refresh_lock_outputs();
state.refresh_idle_inhibition();
sync_battery_timer(state);
state.restore_output_focus();
state.relayout();
state.finish_desktop_transition(desktop_changes);
state.notify_monitor_state();
render_all(state);
lid::apply_suspend_policy(state);
}
/// Publish only hardware present after application, compensation and recovery.
fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges {
let mut outputs = state
.direct_backend
.as_ref()
.unwrap()
.devices
.values()
.flat_map(|device| device.outputs.values())
.filter(|output| output.mirror_source.is_none())
.map(|output| {
(
output.identity.clone(),
output.output.clone(),
output.mode,
output.settings.clone(),
)
})
.collect::<Vec<_>>();
outputs.sort_by(|left, right| left.0.cmp(&right.0));
let mut right_edge = 0;
let runtime = outputs
.into_iter()
.map(|(identity, output, mode, settings)| {
let position = settings.position.unwrap_or_else(|| {
state
.space
.output_geometry(&output)
.map(|geometry| [geometry.loc.x, geometry.loc.y])
.unwrap_or([right_edge, 0])
});
let mode = OutputMode::from(mode);
let transform = output_transform(settings.transform);
let width = (transform.transform_size(mode.size).w as f64 / settings.scale).ceil() as i32;
right_edge = right_edge.max(position[0].saturating_add(width));
crate::state::DesktopOutput {
output,
identity,
mode,
transform,
scale: Scale::Fractional(settings.scale),
position: (position[0], position[1]).into(),
}
})
.collect();
let changes = state
.publish_desktop(runtime)
.expect("validated usable output inventory");
for device in state.direct_backend.as_mut().unwrap().devices.values_mut() {
for output in device.outputs.values_mut() {
if output.mirror_source.is_none() {
if output.global.is_none() {
output.global = Some(output.output.create_global::<Ferese>(&state.display_handle));
}
} else {
// Mirrors are physical scanouts, not workspace-owning outputs.
output.output.change_current_state(
Some(OutputMode::from(output.mode)),
Some(output_transform(output.settings.transform)),
Some(Scale::Fractional(output.settings.scale)),
None,
);
}
output.surface.set_output_mode_source((&output.output).into());
}
}
changes
}
const SETTLE: Duration = Duration::from_millis(150);
/// Trailing-edge coalescing; inactive events retain dirtiness without arming a timer.
+23 -65
View File
@@ -365,6 +365,13 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
})
.and_then(|()| output.surface.commit_frame().map_err(|error| error.to_string()));
self.state.session_lock.output_added(&output.output);
self.state.display_presentation.remove_output(&output.output);
self.state
.desktop_transition
.as_mut()
.unwrap()
.redraw
.insert(output.output.clone());
output.lock_frame_pending = false;
output.surface.set_output_mode_source((&output.output).into());
cancel_output_timers(&self.state.loop_handle, output);
@@ -373,8 +380,8 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
}
}
/// All allocations/tests precede commits. Logical publication follows every
/// successful commit on this device; other devices are separately rollbackable.
/// All allocations/tests precede commits. Logical publication waits for the
/// final usable inventory; devices are independently applied and restored.
pub(super) fn apply_device(
state: &mut Ferese,
node: DrmNode,
@@ -436,8 +443,6 @@ pub(super) fn apply_device(
for crtc in &changed {
let selection = &selections[crtc];
let mut output = staged.remove(crtc).or_else(|| device.outputs.remove(crtc)).unwrap();
let old_geometry = state.space.output_geometry(&output.output);
let old_id = state.output_id(&output.output);
if output.connector != selection.connector.handle() || output.identity != selection.identity {
retire(state, &mut output, node, *crtc);
let (handle, _) = create_output(
@@ -459,65 +464,18 @@ pub(super) fn apply_device(
output.settings = selection.settings.clone();
output.mode = selection.mode;
output.mirror_source = selection.mirror_source.clone();
let position = selection.settings.position.unwrap_or_else(|| {
old_geometry
.map(|geometry| [geometry.loc.x, geometry.loc.y])
.unwrap_or_else(|| {
[
state
.space
.outputs()
.filter_map(|output| state.space.output_geometry(output))
.map(|geometry| geometry.loc.x + geometry.size.w)
.max()
.unwrap_or(0),
0,
]
})
});
output.output.change_current_state(
Some(OutputMode::from(output.mode)),
Some(output_transform(output.settings.transform)),
Some(Scale::Fractional(output.settings.scale)),
Some((position[0], position[1]).into()),
);
if output.mirror_source.is_none() {
if output.global.is_none() {
output.global = Some(output.output.create_global::<Ferese>(&state.display_handle));
}
state.space.map_output(&output.output, (position[0], position[1]));
if state.output_id(&output.output).is_none() {
state.register_output(&output.output, output.identity.clone());
}
if let Some(id) = state.output_id(&output.output)
&& let Some(geometry) = state.space.output_geometry(&output.output)
{
state.output_workspaces.update_geometry(
id,
ferese_core::OutputGeometry::new(geometry.loc.x, geometry.loc.y, geometry.size.w, geometry.size.h),
);
if old_id == Some(id)
&& let Some(old) = old_geometry
{
state.reposition_output_floats(
id,
ferese_layout::Rect::new(
old.loc.x as f64,
old.loc.y as f64,
old.size.w as f64,
old.size.h as f64,
),
ferese_layout::Rect::new(
geometry.loc.x as f64,
geometry.loc.y as f64,
geometry.size.w as f64,
geometry.size.h as f64,
),
);
}
}
}
// Resource metadata describes applied hardware. Wayland output state,
// workspace ownership and floating positions publish once after all GPUs.
state
.desktop_transition
.as_mut()
.expect("output reconciliation boundary")
.redraw
.insert(output.output.clone());
state.session_lock.output_added(&output.output);
if hardware_changes.contains(crtc) {
state.display_presentation.remove_output(&output.output);
}
output.lock_frame_pending = false;
output.surface.set_output_mode_source((&output.output).into());
cancel_output_timers(&state.loop_handle, &mut output);
@@ -536,7 +494,7 @@ pub(super) fn apply_device(
clock.reset_timing();
device.outputs.insert(*crtc, output);
}
// Add/register destinations before removing former owners, reusing evacuation.
// Retire resources now; the final inventory determines evacuation once.
for crtc in removals {
let mut output = device.outputs.remove(&crtc).unwrap();
retire(state, &mut output, node, crtc);
@@ -605,7 +563,7 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
.unwrap()
.devices
.values()
.any(|device| device.outputs.values().any(|output| output.global.is_some()))
.any(|device| device.outputs.values().any(|output| output.mirror_source.is_none()))
{
return;
}
@@ -654,7 +612,7 @@ pub(super) fn recover_last_output(state: &mut Ferese) {
.unwrap()
.devices
.values()
.any(|device| device.outputs.values().any(|output| output.global.is_some()))
.any(|device| device.outputs.values().any(|output| output.mirror_source.is_none()))
{
remember_applied_configuration(state);
state.direct_backend.as_mut().unwrap().output_error = original_error;