diff --git a/src/state.rs b/src/state.rs index ed4ea94..4bc9e17 100644 --- a/src/state.rs +++ b/src/state.rs @@ -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, + /// Name from wl_output::Name (v4) — used to match wlr-output-management heads + pub wl_name: Option, pub transform: Option, 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 { dmabuf: Option, dmabuf_feedback: Option, xdg_output_manager: Option, + wlr_output_manager: Option, + wlr_manager_done: bool, + wlr_heads: HashMap, + wlr_head_proxy_to_name: HashMap, }, EverythingButFmt { output_info: OutputInfo, @@ -213,6 +234,10 @@ impl State { 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 State { } 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 State { 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 State { 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 State { 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 State { 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 State { 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 Dispatch for State { *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 Dispatch for State { } } } + 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 Dispatch for State { } } +// --------------------------------------------------------------------------- +// Dispatch +// --------------------------------------------------------------------------- + +impl Dispatch for State { + fn event( + state: &mut Self, + _proxy: &ZwlrOutputManagerV1, + event: ::Event, + _data: &(), + _conn: &wayland_client::Connection, + qhandle: &QueueHandle>, + ) { + 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, ZwlrOutputManagerV1, [ + zwlr_output_manager_v1::EVT_HEAD_OPCODE => (ZwlrOutputHeadV1, ()), + ]); +} + +// --------------------------------------------------------------------------- +// Dispatch +// --------------------------------------------------------------------------- + +impl Dispatch for State { + fn event( + state: &mut Self, + proxy: &ZwlrOutputHeadV1, + event: ::Event, + _data: &(), + _conn: &wayland_client::Connection, + _qhandle: &QueueHandle>, + ) { + 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, ZwlrOutputHeadV1, [ + zwlr_output_head_v1::EVT_MODE_OPCODE => (ZwlrOutputModeV1, ()), + ]); +} + +// --------------------------------------------------------------------------- +// Dispatch +// --------------------------------------------------------------------------- + +impl Dispatch for State { + fn event( + _state: &mut Self, + _proxy: &ZwlrOutputModeV1, + _event: ::Event, + _data: &(), + _conn: &wayland_client::Connection, + _qhandle: &QueueHandle>, + ) { + } +} + // --------------------------------------------------------------------------- // Dispatch // ---------------------------------------------------------------------------