feat(state): support compositors without xdg-output via wlr-output-management

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
dailz
2026-04-14 17:52:59 +08:00
parent ecd78492ee
commit b2e5f37cf6

View File

@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::mem;
use std::os::fd::{AsFd, OwnedFd};
use std::os::unix::io::FromRawFd;
@@ -5,11 +6,12 @@ use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::Result;
use wayland_client::backend::ObjectId;
use wayland_client::globals::{GlobalList, GlobalListContents};
use wayland_client::protocol::wl_buffer::WlBuffer;
use wayland_client::protocol::wl_output::WlOutput;
use wayland_client::protocol::wl_registry::WlRegistry;
use wayland_client::{Dispatch, Proxy, QueueHandle};
use wayland_client::{event_created_child, Dispatch, Proxy, QueueHandle};
use wayland_protocols::wp::linux_dmabuf::zv1::client::zwp_linux_buffer_params_v1::{
Event as BufferParamsEvent, Flags as BufferParamsFlags, ZwpLinuxBufferParamsV1,
};
@@ -23,6 +25,13 @@ use wayland_protocols::xdg::xdg_output::zv1::client::zxdg_output_manager_v1::Zxd
use wayland_protocols::xdg::xdg_output::zv1::client::zxdg_output_v1::{
Event as XdgOutputEvent, ZxdgOutputV1,
};
use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::{
self, Event as WlrHeadEvent, ZwlrOutputHeadV1,
};
use wayland_protocols_wlr::output_management::v1::client::zwlr_output_manager_v1::{
self, Event as WlrOutputManagerEvent, ZwlrOutputManagerV1,
};
use wayland_protocols_wlr::output_management::v1::client::zwlr_output_mode_v1::ZwlrOutputModeV1;
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::{
Event as ScreencopyFrameEvent, ZwlrScreencopyFrameV1,
};
@@ -72,6 +81,8 @@ pub struct OutputInfo {
pub struct PartialOutputInfo {
pub name: Option<String>,
/// Name from wl_output::Name (v4) — used to match wlr-output-management heads
pub wl_name: Option<String>,
pub transform: Option<Transform>,
pub physical_size: Option<(i32, i32)>,
pub logical_position: Option<(i32, i32)>,
@@ -84,6 +95,7 @@ impl Default for PartialOutputInfo {
fn default() -> Self {
Self {
name: None,
wl_name: None,
transform: None,
physical_size: None,
logical_position: None,
@@ -93,6 +105,11 @@ impl Default for PartialOutputInfo {
}
}
/// Stores head info from wlr-output-management for name-based matching with wl_output.
struct WlrHeadInfo {
position: Option<(i32, i32)>,
}
/// User data for XdgOutput dispatch to identify which WlOutput it belongs to.
pub struct OutputId(pub u32);
@@ -109,6 +126,10 @@ pub enum EncConstructionStage<S: CaptureSource> {
dmabuf: Option<ZwpLinuxDmabufV1>,
dmabuf_feedback: Option<ZwpLinuxDmabufFeedbackV1>,
xdg_output_manager: Option<ZxdgOutputManagerV1>,
wlr_output_manager: Option<ZwlrOutputManagerV1>,
wlr_manager_done: bool,
wlr_heads: HashMap<String, WlrHeadInfo>,
wlr_head_proxy_to_name: HashMap<ObjectId, String>,
},
EverythingButFmt {
output_info: OutputInfo,
@@ -213,6 +234,10 @@ impl<S: CaptureSource> State<S> {
dmabuf: None,
dmabuf_feedback: None,
xdg_output_manager: None,
wlr_output_manager: None,
wlr_manager_done: false,
wlr_heads: HashMap::new(),
wlr_head_proxy_to_name: HashMap::new(),
},
in_flight_surface: InFlightSurface::None,
starting_timestamp: None,
@@ -468,8 +493,30 @@ impl<S: CaptureSource> State<S> {
}
fn try_finalize_output(&mut self, _idx: usize) -> bool {
// Merge wlr head position info into outputs (needed for niri path)
if let EncConstructionStage::ProbingOutputs {
outputs, wlr_heads, ..
} = &mut self.stage
{
for info in outputs.iter_mut() {
if info.logical_position.is_none() {
if let Some(ref wl_name) = info.wl_name {
if let Some(head_info) = wlr_heads.get(wl_name) {
info.logical_position = head_info.position;
}
}
}
}
}
let (target_idx, output_count) = match &self.stage {
EncConstructionStage::ProbingOutputs { outputs, .. } => {
EncConstructionStage::ProbingOutputs {
outputs,
xdg_output_manager,
wlr_manager_done,
..
} => {
let has_xdg = xdg_output_manager.is_some();
let output_count = outputs.len();
let idx = if let Some(ref name) = self.args.output_name {
let pos = outputs
@@ -503,13 +550,25 @@ impl<S: CaptureSource> State<S> {
match idx {
Some(i) => {
let info = &outputs[i];
if info.done_count < 2
|| info.name.is_none()
|| info.transform.is_none()
|| info.physical_size.is_none()
|| info.logical_position.is_none()
{
return false;
if has_xdg {
// xdg-output path (Sway/Hyprland) — strict checks
if info.done_count < 2
|| info.name.is_none()
|| info.transform.is_none()
|| info.physical_size.is_none()
|| info.logical_position.is_none()
{
return false;
}
} else {
// wlr-output-management path (niri) — relaxed checks
if info.done_count < 1 || !wlr_manager_done {
return false;
}
if info.transform.is_none() || info.physical_size.is_none() {
return false;
}
// name and logical_position can use defaults
}
(i, output_count)
}
@@ -530,11 +589,15 @@ impl<S: CaptureSource> State<S> {
let (
outputs,
bound_outputs,
_output_names,
output_names,
screencopy_manager,
dmabuf,
dmabuf_feedback,
_xdg_output_manager,
_wlr_output_manager,
_wlr_manager_done,
_wlr_heads,
_wlr_head_proxy_to_name,
) = match probing {
EncConstructionStage::ProbingOutputs {
outputs,
@@ -544,6 +607,10 @@ impl<S: CaptureSource> State<S> {
dmabuf,
dmabuf_feedback,
xdg_output_manager,
wlr_output_manager,
wlr_manager_done,
wlr_heads,
wlr_head_proxy_to_name,
} => (
outputs,
bound_outputs,
@@ -552,6 +619,10 @@ impl<S: CaptureSource> State<S> {
dmabuf,
dmabuf_feedback,
xdg_output_manager,
wlr_output_manager,
wlr_manager_done,
wlr_heads,
wlr_head_proxy_to_name,
),
_ => unreachable!(),
};
@@ -562,10 +633,14 @@ impl<S: CaptureSource> State<S> {
let info = &outputs[target_idx];
let output_info = OutputInfo {
name: info.name.clone().unwrap(),
name: info
.name
.clone()
.or(info.wl_name.clone())
.unwrap_or_else(|| format!("output-{}", output_names[target_idx])),
transform: info.transform.unwrap(),
physical_size: info.physical_size.unwrap(),
logical_position: info.logical_position.unwrap(),
logical_position: info.logical_position.unwrap_or((0, 0)),
};
let output = bound_outputs[target_idx].clone();
@@ -716,6 +791,17 @@ impl<S: CaptureSource> Dispatch<WlRegistry, GlobalListContents> for State<S> {
*xdg_output_manager = Some(xdg_mgr);
}
}
"zwlr_output_manager_v1" => {
let v = version.min(4);
tracing::debug!("Binding zwlr_output_manager_v1 v{v} (name={name})");
let mgr: ZwlrOutputManagerV1 = registry.bind(name, v, qhandle, ());
if let EncConstructionStage::ProbingOutputs {
wlr_output_manager, ..
} = &mut state.stage
{
*wlr_output_manager = Some(mgr);
}
}
_ => {}
},
RegistryEvent::GlobalRemove { name } => {
@@ -805,6 +891,13 @@ impl<S: CaptureSource> Dispatch<WlOutput, OutputId> for State<S> {
}
}
}
OutputEvent::Name { name } => {
if let EncConstructionStage::ProbingOutputs { outputs, .. } = &mut state.stage {
if let Some(info) = outputs.get_mut(idx) {
info.wl_name = Some(name);
}
}
}
_ => {}
}
}
@@ -1050,6 +1143,119 @@ impl<S: CaptureSource> Dispatch<ZxdgOutputManagerV1, ()> for State<S> {
}
}
// ---------------------------------------------------------------------------
// Dispatch<ZwlrOutputManagerV1, ()>
// ---------------------------------------------------------------------------
impl<S: CaptureSource> Dispatch<ZwlrOutputManagerV1, ()> for State<S> {
fn event(
state: &mut Self,
_proxy: &ZwlrOutputManagerV1,
event: <ZwlrOutputManagerV1 as Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
qhandle: &QueueHandle<State<S>>,
) {
match event {
WlrOutputManagerEvent::Head { head } => {
let _head: ZwlrOutputHeadV1 = head;
tracing::debug!("wlr output head advertised");
}
WlrOutputManagerEvent::Done { .. } => {
if let EncConstructionStage::ProbingOutputs {
wlr_manager_done,
outputs,
..
} = &mut state.stage
{
*wlr_manager_done = true;
let count = outputs.len();
for idx in 0..count {
state.try_finalize_output(idx);
}
}
}
WlrOutputManagerEvent::Finished { .. } => {
tracing::warn!("zwlr_output_manager_v1::Finished received during probing");
}
_ => {}
}
}
event_created_child!(State<S>, ZwlrOutputManagerV1, [
zwlr_output_manager_v1::EVT_HEAD_OPCODE => (ZwlrOutputHeadV1, ()),
]);
}
// ---------------------------------------------------------------------------
// Dispatch<ZwlrOutputHeadV1, ()>
// ---------------------------------------------------------------------------
impl<S: CaptureSource> Dispatch<ZwlrOutputHeadV1, ()> for State<S> {
fn event(
state: &mut Self,
proxy: &ZwlrOutputHeadV1,
event: <ZwlrOutputHeadV1 as Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &QueueHandle<State<S>>,
) {
match event {
WlrHeadEvent::Name { name } => {
if let EncConstructionStage::ProbingOutputs {
wlr_heads,
wlr_head_proxy_to_name,
..
} = &mut state.stage
{
wlr_heads
.entry(name.clone())
.or_insert(WlrHeadInfo { position: None });
wlr_head_proxy_to_name.insert(proxy.id(), name);
}
}
WlrHeadEvent::Position { x, y } => {
if let EncConstructionStage::ProbingOutputs {
wlr_heads,
wlr_head_proxy_to_name,
..
} = &mut state.stage
{
if let Some(name) = wlr_head_proxy_to_name.get(&proxy.id()) {
if let Some(head) = wlr_heads.get_mut(name) {
head.position = Some((x, y));
}
}
}
}
WlrHeadEvent::Finished { .. } => {
tracing::debug!("zwlr_output_head_v1::Finished received");
}
_ => {}
}
}
event_created_child!(State<S>, ZwlrOutputHeadV1, [
zwlr_output_head_v1::EVT_MODE_OPCODE => (ZwlrOutputModeV1, ()),
]);
}
// ---------------------------------------------------------------------------
// Dispatch<ZwlrOutputModeV1, ()>
// ---------------------------------------------------------------------------
impl<S: CaptureSource> Dispatch<ZwlrOutputModeV1, ()> for State<S> {
fn event(
_state: &mut Self,
_proxy: &ZwlrOutputModeV1,
_event: <ZwlrOutputModeV1 as Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &QueueHandle<State<S>>,
) {
}
}
// ---------------------------------------------------------------------------
// Dispatch<ZwlrScreencopyManagerV1, ()>
// ---------------------------------------------------------------------------